@@ -2,7 +2,7 @@ import { argv, stderr, stdout } from 'node:process';
2
2
import { fileURLToPath } from 'node:url' ;
3
3
import { formatWithOptions , parseArgs , styleText } from 'node:util' ;
4
4
5
- import { baseLanguage , getResourceLanguages , readResource , writeResource } from './common.mts' ;
5
+ import { baseLanguage , getLanguagePlurals , getResourceLanguages , readResource , writeResource } from './common.mts' ;
6
6
7
7
type TaskOptions = {
8
8
fix ?: boolean ;
@@ -14,17 +14,26 @@ const describeTask =
14
14
(
15
15
task : string ,
16
16
fn : ( ) => AsyncGenerator < {
17
- lint : ( collectError : ( format ?: any , ...param : any [ ] ) => void ) => Promise < void > ;
18
- fix : ( ) => Promise < void > ;
17
+ lint : ( reportError : ( format ?: any , ...param : any [ ] ) => void ) => Promise < void > ;
18
+ fix : ( throwError : ( format ?: any , ... param : any [ ] ) => void ) => Promise < void > ;
19
19
} > ,
20
20
) =>
21
21
async ( options : TaskOptions ) => {
22
22
for await ( const result of fn ( ) ) {
23
23
if ( ! result ) continue ;
24
24
25
25
if ( options . fix ) {
26
- await result . fix ( ) ;
27
- console . log ( styleText ( 'blue' , '✔' , { stream : stdout } ) , styleText ( 'gray' , `${ task } :` , { stream : stdout } ) , 'fixes applied' ) ;
26
+ try {
27
+ await result . fix ( ( format , ...param ) => {
28
+ throw new Error ( formatWithOptions ( { colors : ! ! styleText ( 'blue' , `.` , { stream : stdout } ) } , format , ...param ) ) ;
29
+ } ) ;
30
+
31
+ console . log ( styleText ( 'blue' , '✔' , { stream : stdout } ) , styleText ( 'gray' , `${ task } :` , { stream : stdout } ) , 'fixes applied' ) ;
32
+ } catch ( error ) {
33
+ console . error ( styleText ( 'red' , '✘' , { stream : stdout } ) , styleText ( 'gray' , `${ task } :` , { stream : stdout } ) , error instanceof Error ? error . message : error ) ;
34
+ console . error ( styleText ( 'gray' , ` cannot apply fixes automatically, run without --fix to see all errors` , { stream : stdout } ) ) ;
35
+ errorCount ++ ;
36
+ }
28
37
} else {
29
38
const stderrSupportsColor = styleText ( 'blue' , `.` , { stream : stderr } ) !== '.' ;
30
39
@@ -41,8 +50,7 @@ const describeTask =
41
50
} ;
42
51
43
52
/**
44
- * Sort keys of the base language (en) alphabetically
45
- * and write back the sorted resource file if necessary
53
+ * Sort keys of the base language (en) alphabetically and write back the sorted resource file if necessary
46
54
*/
47
55
const sortBaseKeys = describeTask ( 'sort-base-keys' , async function * ( ) {
48
56
const baseResource = await readResource ( baseLanguage ) ;
@@ -53,7 +61,7 @@ const sortBaseKeys = describeTask('sort-base-keys', async function* () {
53
61
if ( keys . join ( ',' ) === sortedKeys . join ( ',' ) ) return ;
54
62
55
63
yield {
56
- lint : async ( collectError ) => {
64
+ lint : async ( reportError ) => {
57
65
for ( let i = 0 ; i < keys . length ; i ++ ) {
58
66
const key = keys [ i ] ;
59
67
const beforeKey = keys . at ( i - 1 ) ;
@@ -62,9 +70,9 @@ const sortBaseKeys = describeTask('sort-base-keys', async function* () {
62
70
63
71
if ( beforeKey !== expectedBeforeKey ) {
64
72
if ( expectedBeforeKey ) {
65
- collectError ( '%o should be after %o' , keys [ i ] , expectedBeforeKey ) ;
73
+ reportError ( '%o should be after %o' , keys [ i ] , expectedBeforeKey ) ;
66
74
} else {
67
- collectError ( '%o should be the first key' , keys [ i ] ) ;
75
+ reportError ( '%o should be the first key' , keys [ i ] ) ;
68
76
}
69
77
}
70
78
}
@@ -110,7 +118,7 @@ const sortKeys = describeTask('sort-keys', async function* () {
110
118
if ( Object . keys ( resource ) . join ( ',' ) === Object . keys ( sortedResource ) . join ( ',' ) ) continue ;
111
119
112
120
yield {
113
- lint : async ( collectError ) => {
121
+ lint : async ( reportError ) => {
114
122
const keys = Object . keys ( resource ) ;
115
123
const sortedKeys = Object . keys ( sortedResource ) ;
116
124
@@ -124,9 +132,9 @@ const sortKeys = describeTask('sort-keys', async function* () {
124
132
125
133
if ( beforeKey !== expectedBeforeKey ) {
126
134
if ( expectedBeforeKey ) {
127
- collectError ( '%s: %o should be after %o' , language , keys [ i ] , expectedBeforeKey ) ;
135
+ reportError ( '%s: %o should be after %o' , language , keys [ i ] , expectedBeforeKey ) ;
128
136
} else {
129
- collectError ( '%s: %o should be the first key' , language , keys [ i ] ) ;
137
+ reportError ( '%s: %o should be the first key' , language , keys [ i ] ) ;
130
138
}
131
139
}
132
140
}
@@ -156,10 +164,10 @@ const wipeExtraKeys = describeTask('wipe-extra-keys', async function* () {
156
164
if ( resourceKeys . difference ( baseKeys ) . size === 0 ) continue ;
157
165
158
166
yield {
159
- lint : async ( collectError ) => {
167
+ lint : async ( reportError ) => {
160
168
const extraKeys = resourceKeys . difference ( baseKeys ) ;
161
169
for ( const key of extraKeys ) {
162
- collectError ( '%s: has extra key %o' , language , key ) ;
170
+ reportError ( '%s: has extra key %o' , language , key ) ;
163
171
}
164
172
} ,
165
173
fix : async ( ) => {
@@ -176,15 +184,88 @@ const wipeExtraKeys = describeTask('wipe-extra-keys', async function* () {
176
184
}
177
185
} ) ;
178
186
187
+ /**
188
+ * Wipes invalid plural forms from all language files (only "zero", "one", "two", "few", "many", "other" are valid)
189
+ */
190
+ const wipeInvalidPlurals = describeTask ( 'wipe-invalid-plurals' , async function * ( ) {
191
+ const languages = await getResourceLanguages ( ) ;
192
+
193
+ for await ( const language of languages ) {
194
+ const resource = await readResource ( language ) ;
195
+ const plurals = getLanguagePlurals ( language ) . concat ( [ 'zero' ] ) ; // 'zero' is special in i18next
196
+
197
+ for ( const [ key , translation ] of Object . entries ( resource ) ) {
198
+ if ( typeof translation !== 'object' || ! translation ) continue ;
199
+
200
+ const translationPlurals = Object . keys ( translation ) ;
201
+ for ( const plural of translationPlurals ) {
202
+ if ( ! plurals . includes ( plural ) ) {
203
+ yield {
204
+ lint : async ( reportError ) => {
205
+ reportError ( '%s: key %o has invalid plural form %o' , language , key , plural ) ;
206
+ } ,
207
+ fix : async ( ) => {
208
+ const fixedResource : Record < string , unknown > = { ...resource } ;
209
+ fixedResource [ key ] = Object . fromEntries (
210
+ Object . entries ( translation ) . filter ( ( [ p ] ) => plurals . includes ( p ) ) ,
211
+ ) ;
212
+ await writeResource ( language , fixedResource ) ;
213
+ }
214
+ } ;
215
+ }
216
+ }
217
+ }
218
+ }
219
+ } ) ;
220
+
221
+ /**
222
+ * Finds missing plural forms in all language files
223
+ */
224
+ const findMissingPlurals = describeTask ( 'find-missing-plurals' , async function * ( ) {
225
+ const languages = await getResourceLanguages ( ) ;
226
+
227
+ for await ( const language of languages ) {
228
+ if ( language === baseLanguage ) continue ;
229
+
230
+ const resource = await readResource ( language ) ;
231
+ const baseResource = await readResource ( baseLanguage ) ;
232
+ const plurals = getLanguagePlurals ( language ) ;
233
+
234
+ for ( const [ key , translation ] of Object . entries ( baseResource ) ) {
235
+ if ( typeof translation !== 'object' || ! translation ) continue ;
236
+ if ( ! ( key in resource ) ) continue ;
237
+
238
+ const translationPlurals = Object . keys ( translation ) ;
239
+ const resourceTranslation = resource [ key ] ;
240
+ if ( typeof resourceTranslation !== 'object' || ! resourceTranslation ) continue ;
241
+
242
+ for ( const plural of translationPlurals ) {
243
+ if ( ! plurals . includes ( plural ) ) continue ;
244
+ if ( plural in resourceTranslation ) continue ;
245
+ yield {
246
+ lint : async ( reportError ) => {
247
+ reportError ( '%s: key %o is missing plural form %o' , language , key , plural ) ;
248
+ } ,
249
+ fix : async ( throwError ) => {
250
+ throwError ( '%s: key %o is missing plural form %o' , language , key , plural ) ;
251
+ }
252
+ } ;
253
+ }
254
+ }
255
+ }
256
+ } ) ;
257
+
179
258
const tasksByName = {
180
259
'sort-base-keys' : sortBaseKeys ,
181
260
'sort-keys' : sortKeys ,
182
261
'wipe-extra-keys' : wipeExtraKeys ,
262
+ 'wipe-invalid-plurals' : wipeInvalidPlurals ,
263
+ 'find-missing-plurals' : findMissingPlurals ,
183
264
} as const ;
184
265
185
266
async function check ( { fix, task } : { fix ?: boolean ; task ?: string [ ] } = { } ) {
186
267
// We're lenient by default excluding 'sort-base-keys' from the default tasks
187
- const tasks = new Set < keyof typeof tasksByName > ( [ 'sort-keys' , 'wipe-extra-keys' ] ) ;
268
+ const tasks = new Set < keyof typeof tasksByName > ( [ 'sort-keys' , 'wipe-extra-keys' , 'wipe-invalid-plurals' , 'find-missing-plurals' ] ) ;
188
269
189
270
if ( task ?. length ) {
190
271
tasks . clear ( ) ;
0 commit comments