@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.
|
|
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.
|
|
38
|
-
"@wordpress/prettier-config": "^4.
|
|
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": "
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
}
|
package/utils/constants.js
CHANGED
|
@@ -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
|
};
|