diff --git a/packages/eslint-plugin/rules/__tests__/i18n-translator-comments.js b/packages/eslint-plugin/rules/__tests__/i18n-translator-comments.js index c19c57f0cb55a6..a3814a24fb845b 100644 --- a/packages/eslint-plugin/rules/__tests__/i18n-translator-comments.js +++ b/packages/eslint-plugin/rules/__tests__/i18n-translator-comments.js @@ -87,6 +87,22 @@ _x( '"%1$s"/ %2$s', 'caption' ); _x( '"%1$s"/ %2$s', 'caption' ); `, }, + { + code: ` // translators: %s: Hello at 6:00 AM + i18n.sprintf( i18n.__( 'Hello at %s' ), '6:00 AM' );`, + }, + { + code: `// translators: %.2f: Percentage + i18n.sprintf( i18n.__( 'Percentage: %.2f' ), 1.00 );`, + }, + { + code: `// translators: %.*f: Percentage + i18n.sprintf( i18n.__( 'Percentage: %.*f' ), 2, 1.00 );`, + }, + { + code: `// translators: %(named).2s: truncated name + i18n.sprintf( i18n.__( 'Truncated name: %(named).2s' ), { named: 'Long Name' } );`, + }, ], invalid: [ { diff --git a/packages/eslint-plugin/rules/i18n-translator-comments.js b/packages/eslint-plugin/rules/i18n-translator-comments.js index 10a94d8a6c5d0e..b5b3302142364c 100644 --- a/packages/eslint-plugin/rules/i18n-translator-comments.js +++ b/packages/eslint-plugin/rules/i18n-translator-comments.js @@ -50,7 +50,9 @@ function extractTranslatorKeys( commentText ) { while ( ( match = REGEXP_COMMENT_PLACEHOLDER.exec( commentBody ) ) !== null ) { - keys.set( match[ 1 ], keys.get( match[ 1 ] ) || match[ 2 ] === ':' ); + const rawKey = match[ 1 ]; + const hasColon = match.groups?.colon?.trim() === ':'; + keys.set( rawKey, keys.get( rawKey ) || hasColon ); } return keys; @@ -211,8 +213,6 @@ module.exports = { } ) : []; - // console.log({extra, keysInComment, placeholdersUsed}); - if ( extra.length > 0 ) { context.report( { node, diff --git a/packages/eslint-plugin/utils/constants.js b/packages/eslint-plugin/utils/constants.js index 16ae5dab17adcc..bfd873a8f24bca 100644 --- a/packages/eslint-plugin/utils/constants.js +++ b/packages/eslint-plugin/utils/constants.js @@ -56,19 +56,47 @@ const REGEXP_SPRINTF_PLACEHOLDER_UNORDERED = /(?:(?[a-zA-Z_][a-zA-Z0-9_]*\) — Named placeholder in the form: %(name) + * (?:\.\d+|\.\*)? — Optional precision: .2 or .* + * [sdf] — Format specifier: s, d, or f + * + * | + * (?[1-9][0-9]*)\$? — Positional placeholder like %1$ + * (?:\.\d+|\.\*)? — Optional precision + * [sdf] — Format specifier + * + * | — OR + * (?:\.\d+|\.\*)?[sdf] — Unnamed placeholder with optional precision + * + * | [1-9][0-9]* — Bare positional key like `1`, `2` + * | [sdf] — Just a format type + * | [a-zA-Z_][a-zA-Z0-9_]* — Bare named key (used in comments) + * ) + * ) + * + * (?:[ \t]+)? — Optional named group `colon`, matches a colon followed by space or tab, + * indicating that this placeholder has a description in the comment. + * + * Flags: + * g — global, so it matches all placeholders in the comment string. + * ``` */ const REGEXP_COMMENT_PLACEHOLDER = - /(?:^|\s|,)\s*(%[sdf]|%?[a-zA-Z0-9_]+|%[0-9]+\$?[sdf]{0,1})(:)?/g; + /(?:^|\s|,)\s*(%?(?:\((?[a-zA-Z_][a-zA-Z0-9_]*)\)(?:\.\d+|\.\*)?[sdf]|(?[1-9][0-9]*)\$?(?:\.\d+|\.\*)?[sdf]|(?:\.\d+|\.\*)?[sdf]|[1-9][0-9]*|[sdf]|[a-zA-Z_][a-zA-Z0-9_]*))(?:[ \t]+)?/g; module.exports = { TRANSLATION_FUNCTIONS,