@wordpress/eslint-plugin 22.11.0 → 22.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/eslint-plugin",
3
- "version": "22.11.0",
3
+ "version": "22.13.0",
4
4
  "description": "ESLint plugin for WordPress development.",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -34,8 +34,8 @@
34
34
  "@babel/eslint-parser": "7.25.7",
35
35
  "@typescript-eslint/eslint-plugin": "^6.4.1",
36
36
  "@typescript-eslint/parser": "^6.4.1",
37
- "@wordpress/babel-preset-default": "^8.25.0",
38
- "@wordpress/prettier-config": "^4.25.0",
37
+ "@wordpress/babel-preset-default": "^8.27.0",
38
+ "@wordpress/prettier-config": "^4.27.0",
39
39
  "cosmiconfig": "^7.0.0",
40
40
  "eslint-config-prettier": "^8.3.0",
41
41
  "eslint-plugin-import": "^2.25.2",
@@ -66,5 +66,5 @@
66
66
  "publishConfig": {
67
67
  "access": "public"
68
68
  },
69
- "gitHead": "d1acd76ffff33ab01f0a948d2f51e5e45c95158d"
69
+ "gitHead": "abe06a6f2aef8d03c30ea9d5b3e133f041e523b1"
70
70
  }
@@ -18,84 +18,155 @@ ruleTester.run( 'i18n-translator-comments', rule, {
18
18
  valid: [
19
19
  {
20
20
  code: `
21
- // translators: %s: Color
22
- sprintf( __( 'Color: %s' ), color );`,
21
+ // translators: %s: Color
22
+ sprintf( __( 'Color: %s' ), color );`,
23
23
  },
24
24
  {
25
25
  code: `
26
- sprintf(
27
- // translators: %s: Address.
28
- __( 'Address: %s' ),
29
- address
30
- );`,
26
+ sprintf(
27
+ // translators: %s: Address.
28
+ __( 'Address: %s' ),
29
+ address
30
+ );`,
31
31
  },
32
32
  {
33
33
  code: `
34
- // translators: %s: Color
35
- i18n.sprintf( i18n.__( 'Color: %s' ), color );`,
34
+ // translators: %s: Color
35
+ i18n.sprintf( i18n.__( 'Color: %s' ), color );`,
36
36
  },
37
37
  {
38
38
  code: `
39
- sprintf(
40
- /*
41
- * translators: %s is the name of the city we couldn't locate.
42
- * Replace the examples with cities related to your locale. Test that
43
- * they match the expected location and have upcoming events before
44
- * including them. If no cities related to your locale have events,
45
- * then use cities related to your locale that would be recognizable
46
- * to most users. Use only the city name itself, without any region
47
- * or country. Use the endonym (native locale name) instead of the
48
- * English name if possible.
49
- */
50
- __( 'We couldn’t locate %s. Please try another nearby city. For example: Kansas City; Springfield; Portland.' ),
51
- templateParams.unknownCity
52
- );`,
39
+ sprintf(
40
+ /*
41
+ * translators: %s is the name of the city we couldn't locate.
42
+ * Replace the examples with cities related to your locale. Test that
43
+ * they match the expected location and have upcoming events before
44
+ * including them. If no cities related to your locale have events,
45
+ * then use cities related to your locale that would be recognizable
46
+ * to most users. Use only the city name itself, without any region
47
+ * or country. Use the endonym (native locale name) instead of the
48
+ * English name if possible.
49
+ */
50
+ __( 'We couldn’t locate %s. Please try another nearby city. For example: Kansas City; Springfield; Portland.' ),
51
+ templateParams.unknownCity
52
+ );`,
53
+ },
54
+ {
55
+ code: `
56
+ // translators: %s: Name
57
+ sprintf( __( 'Name: %s' ), 'hi' );`,
58
+ },
59
+ {
60
+ code: `// translators: city: City
61
+ sprintf( __( 'City: %(city)s' ),{ city: 'New York' });`,
62
+ },
63
+ {
64
+ code: `
65
+ // translators: 1: Address, 2: City
66
+ sprintf( __( 'Address: %1$s, City: %2$s' ), address, city );`,
67
+ },
68
+ {
69
+ code: `
70
+ // translators: %s: 1 or 2
71
+ sprintf( __( '%s point' ), number );`,
72
+ },
73
+ {
74
+ code: `
75
+ /* translators: accessibility text. %1: current block position (number). %2: next block position (number) */
76
+ __( 'Move block left from position %1$s to position %2$s');`,
77
+ },
78
+ {
79
+ code: `
80
+ // translators: %1s: Title of a media work from Openverse; %2s: Work's licence e.g: "CC0 1.0".
81
+ _x( '"%1$s"/ %2$s', 'caption' );
82
+ `,
83
+ },
84
+ {
85
+ code: `
86
+ // translators: %1$s: Title of a media work from Openverse; %2$s: Work's licence e.g: "CC0 1.0".
87
+ _x( '"%1$s"/ %2$s', 'caption' );
88
+ `,
53
89
  },
54
90
  ],
55
91
  invalid: [
56
92
  {
57
93
  code: `
58
- sprintf( __( 'Color: %s' ), color );`,
94
+ sprintf( __( 'Color: %s' ), color );`,
59
95
  errors: [ { messageId: 'missing' } ],
60
96
  },
61
97
  {
62
98
  code: `
63
- sprintf(
64
- __( 'Address: %s' ),
65
- address
66
- );`,
99
+ sprintf(
100
+ __( 'Address: %s' ),
101
+ address
102
+ );`,
67
103
  errors: [ { messageId: 'missing' } ],
68
104
  },
69
105
  {
70
106
  code: `
71
- // translators: %s: Name
72
- var name = '';
73
- sprintf( __( 'Name: %s' ), name );`,
107
+ // translators: %s: Name
108
+ var name = '';
109
+ sprintf( __( 'Name: %s' ), name );`,
74
110
  errors: [ { messageId: 'missing' } ],
75
111
  },
76
112
  {
77
113
  code: `
78
- // translators: %s: Surname
79
- console.log(
80
- sprintf( __( 'Surname: %s' ), name )
81
- );`,
114
+ // translators: %s: Surname
115
+ console.log(
116
+ sprintf( __( 'Surname: %s' ), name )
117
+ );`,
82
118
  errors: [ { messageId: 'missing' } ],
83
119
  },
84
120
  {
85
121
  code: `
86
- // translators: %s: Preference
87
- console.log(
88
- sprintf(
89
- __( 'Preference: %s' ),
90
- preference
91
- )
92
- );`,
122
+ // translators: %s: Preference
123
+ console.log(
124
+ sprintf(
125
+ __( 'Preference: %s' ),
126
+ preference
127
+ )
128
+ );`,
93
129
  errors: [ { messageId: 'missing' } ],
94
130
  },
95
131
  {
96
132
  code: `
97
- i18n.sprintf( i18n.__( 'Color: %s' ), color );`,
133
+ i18n.sprintf( i18n.__( 'Color: %s' ), color );`,
98
134
  errors: [ { messageId: 'missing' } ],
99
135
  },
136
+ {
137
+ code: `// translators: %d: Address
138
+ i18n.sprintf( i18n.__( 'Address: %s' ), address );`,
139
+ errors: [ { messageId: 'missingKeys' } ],
140
+ },
141
+ {
142
+ code: ` // translators: %s: City
143
+ i18n.sprintf( i18n.__( 'City: %(city)s' ), { city: 'New York' } );`,
144
+ errors: [
145
+ { messageId: 'missingKeys', data: { keys: [ 'city' ] } },
146
+ ],
147
+ },
148
+ {
149
+ code: `// translators: 1: Address
150
+ i18n.sprintf( i18n.__( 'Address: %1$s, City: %2$s' ), address, city );`,
151
+ errors: [ { messageId: 'missingKeys', data: { keys: [ '2' ] } } ],
152
+ },
153
+ {
154
+ code: `// translators: %s: City %d: Number
155
+ i18n.sprintf( i18n.__( 'City: %s' ), city, number );`,
156
+ errors: [
157
+ { messageId: 'extraPlaceholders', data: { keys: [ '%d' ] } },
158
+ ],
159
+ },
160
+ {
161
+ code: `
162
+ // translators: %s: hi, 1: okay, 2: bye
163
+ i18n.sprintf( i18n.__( '%s point' ), number );`,
164
+ errors: [
165
+ {
166
+ messageId: 'extraPlaceholders',
167
+ data: { keys: [ '1', '2' ] },
168
+ },
169
+ ],
170
+ },
100
171
  ],
101
172
  } );
@@ -8,6 +8,53 @@ const {
8
8
  getTranslateFunctionArgs,
9
9
  getTextContentFromNode,
10
10
  } = require( '../utils' );
11
+ const { REGEXP_COMMENT_PLACEHOLDER } = require( '../utils/constants' );
12
+
13
+ /**
14
+ * Extracts placeholders from a string.
15
+ *
16
+ * @param {string} str - The string to extract placeholders from.
17
+ * @return {string[]} An array of objects representing the placeholders found in the string.
18
+ */
19
+ function extractPlaceholders( str ) {
20
+ const matches = [];
21
+ let match;
22
+ REGEXP_SPRINTF_PLACEHOLDER.lastIndex = 0;
23
+
24
+ while ( ( match = REGEXP_SPRINTF_PLACEHOLDER.exec( str ) ) !== null ) {
25
+ const index = match[ 3 ]; // from %1$s
26
+ const name = match[ 5 ]; // from %(name)s
27
+ matches.push( index ?? name ?? match[ 0 ] );
28
+ }
29
+ return matches;
30
+ }
31
+
32
+ /**
33
+ * Extracts translator keys from a comment text.
34
+ *
35
+ * @param {string} commentText - The text of the comment to extract keys from.
36
+ * @return {Map<string, boolean>} A set of translator keys found in the comment text.
37
+ */
38
+ function extractTranslatorKeys( commentText ) {
39
+ const keys = new Map();
40
+ let match;
41
+
42
+ match = commentText.match( /translators:\s*(.*)/i );
43
+ if ( ! match ) {
44
+ return keys;
45
+ }
46
+
47
+ const commentBody = match[ 1 ];
48
+
49
+ // Match placeholders in the comment body.
50
+ while (
51
+ ( match = REGEXP_COMMENT_PLACEHOLDER.exec( commentBody ) ) !== null
52
+ ) {
53
+ keys.set( match[ 1 ], keys.get( match[ 1 ] ) || match[ 2 ] === ':' );
54
+ }
55
+
56
+ return keys;
57
+ }
11
58
 
12
59
  module.exports = {
13
60
  meta: {
@@ -15,6 +62,10 @@ module.exports = {
15
62
  messages: {
16
63
  missing:
17
64
  'Translation function with placeholders is missing preceding translator comment',
65
+ missingKeys:
66
+ 'Translator comment missing description(s) for placeholder(s): {{ keys }}.',
67
+ extraPlaceholders:
68
+ 'Translator comment has extra placeholder(s): {{ keys }}.',
18
69
  },
19
70
  },
20
71
  create( context ) {
@@ -98,6 +149,82 @@ module.exports = {
98
149
  }
99
150
 
100
151
  if ( /translators:\s*\S+/i.test( commentText ) ) {
152
+ const keysInComment =
153
+ extractTranslatorKeys( commentText );
154
+ const placeholdersUsed =
155
+ candidates.flatMap( extractPlaceholders );
156
+
157
+ const keysInCommentArr = [ ...keysInComment.keys() ];
158
+
159
+ // Check and filter placeholders that are not present in the comment.
160
+ const missing = placeholdersUsed.filter( ( key ) => {
161
+ // Regex to match the key and its potential formats in the array.
162
+ const regex = new RegExp( `%?${ key }(\\$[sdf])?` );
163
+ return ! keysInCommentArr.some( ( y ) =>
164
+ regex.test( y )
165
+ );
166
+ } );
167
+
168
+ if ( missing.length > 0 ) {
169
+ context.report( {
170
+ node,
171
+ messageId: 'missingKeys',
172
+ data: {
173
+ keys: missing.join( ', ' ),
174
+ },
175
+ } );
176
+
177
+ return;
178
+ }
179
+
180
+ const extra = keysInComment.size
181
+ ? [ ...keysInComment.keys() ].filter( ( key ) => {
182
+ const normalizedKey = key.replace(
183
+ /^%/,
184
+ ''
185
+ );
186
+
187
+ // Only allow numeric or printf-style placeholders
188
+ const isNumbered = /^[0-9]+$/.test(
189
+ normalizedKey
190
+ );
191
+ const isPrintf = [
192
+ '%s',
193
+ '%d',
194
+ '%f',
195
+ ].includes( key );
196
+
197
+ // Only add if it's not already in allowedUsed
198
+ const isValidType =
199
+ ( isNumbered &&
200
+ keysInComment.get(
201
+ normalizedKey
202
+ ) ) ||
203
+ isPrintf;
204
+ const isUnused =
205
+ ! placeholdersUsed.includes( key ) &&
206
+ ! placeholdersUsed.includes(
207
+ normalizedKey
208
+ );
209
+
210
+ return isValidType && isUnused;
211
+ } )
212
+ : [];
213
+
214
+ // console.log({extra, keysInComment, placeholdersUsed});
215
+
216
+ if ( extra.length > 0 ) {
217
+ context.report( {
218
+ node,
219
+ messageId: 'extraPlaceholders',
220
+ data: {
221
+ keys: extra.join( ',' ),
222
+ },
223
+ } );
224
+
225
+ return;
226
+ }
227
+
101
228
  return;
102
229
  }
103
230
  }
@@ -55,8 +55,24 @@ const REGEXP_SPRINTF_PLACEHOLDER =
55
55
  const REGEXP_SPRINTF_PLACEHOLDER_UNORDERED =
56
56
  /(?:(?<!%)%[+-]?(?:(?:0|'.)?-?[0-9]*(?:\.(?:[ 0]|'.)?[0-9]+)?|(?:[ ])?-?[0-9]+(?:\.(?:[ 0]|'.)?[0-9]+)?)[bcdeEfFgGosuxX])/;
57
57
 
58
+ /**
59
+ * Regular expression matching comment placeholders.
60
+ *
61
+ * /(?:^|\s|,)\s*(%[sdf]|%?[a-zA-Z0-9_]+|%[0-9]+\$?[sdf]{0,1})(:)?/g;
62
+ * ▲ ▲ ▲ ▲ ▲
63
+ * │ │ │ │ │
64
+ * │ │ │ │ └─ Match colon at the end of the placeholder ( optional )
65
+ * │ │ │ └─ Match a Index placeholder but allow variations (e.g. %1, %1s, %1$d)
66
+ * │ │ └─ Match a placeholder with index or named argument (e.g. %1, %name, %2)
67
+ * │ └─ Match Unamed placeholder (e.g. %s, %d)
68
+ * └─ Match the start of string, whitespace, or comma
69
+ */
70
+ const REGEXP_COMMENT_PLACEHOLDER =
71
+ /(?:^|\s|,)\s*(%[sdf]|%?[a-zA-Z0-9_]+|%[0-9]+\$?[sdf]{0,1})(:)?/g;
72
+
58
73
  module.exports = {
59
74
  TRANSLATION_FUNCTIONS,
60
75
  REGEXP_SPRINTF_PLACEHOLDER,
61
76
  REGEXP_SPRINTF_PLACEHOLDER_UNORDERED,
77
+ REGEXP_COMMENT_PLACEHOLDER,
62
78
  };