@@ -43,31 +43,19 @@ const args = process.argv.slice(2).reduce(
4343 { area : "both" } ,
4444)
4545
46- // Show help if requested
4746if ( args . help ) {
4847 console . log ( `
49- Locale Key Ordering Linter
48+ Locale Key Ordering Linter - Ensures consistent key ordering across locale files
5049
51- A utility script to ensure consistent key ordering across all locale files.
52- Compares the key ordering in non-English locale files to the English reference
53- to identify any ordering mismatches.
54-
55- Usage:
56- node scripts/lint-locale-key-ordering.js [options]
57- tsx scripts/lint-locale-key-ordering.js [options]
50+ Usage: tsx scripts/lint-locale-key-ordering.js [options]
5851
5952Options:
60- --locale=<locale> Only check a specific locale (e.g. --locale=fr)
61- --file=<file> Only check a specific file (e.g. --file=chat.json)
62- --area=<area> Only check a specific area (core, webview, or both)
63- 'core' = Backend (src/i18n/locales)
64- 'webview' = Frontend UI (webview-ui/src/i18n/locales)
65- 'both' = Check both areas (default)
66- --help Show this help message
53+ --locale=<locale> Check specific locale (e.g. --locale=fr)
54+ --file=<file> Check specific file (e.g. --file=chat.json)
55+ --area=<area> Check area: core, webview, or both (default)
56+ --help Show this help
6757
68- Exit Codes:
69- 0 = All key ordering is consistent
70- 1 = Key ordering inconsistencies found
58+ Exit: 0=consistent, 1=issues found
7159 ` )
7260 process . exit ( 0 )
7361}
@@ -81,176 +69,118 @@ const LOCALES_DIRS = {
8169// Determine which areas to check based on args
8270const areasToCheck = args . area === "both" ? [ "core" , "webview" ] : [ args . area ]
8371
84- /**
85- * Extract keys from a JSON object in the order they appear
86- * @param {Object } obj - The JSON object
87- * @param {string } prefix - The current key prefix for nested objects
88- * @returns {string[] } Array of dot-notation keys in order
89- */
72+ // Extract keys from JSON object recursively in dot notation
9073function extractKeysInOrder ( obj , prefix = "" ) {
9174 const keys = [ ]
92-
9375 for ( const [ key , value ] of Object . entries ( obj ) ) {
9476 const fullKey = prefix ? `${ prefix } .${ key } ` : key
95-
77+ keys . push ( fullKey )
9678 if ( typeof value === "object" && value !== null && ! Array . isArray ( value ) ) {
97- // For nested objects, add the parent key first, then recursively add child keys
98- keys . push ( fullKey )
9979 keys . push ( ...extractKeysInOrder ( value , fullKey ) )
100- } else {
101- // For primitive values, just add the key
102- keys . push ( fullKey )
10380 }
10481 }
105-
10682 return keys
10783}
10884
109- /**
110- * Compare two arrays of keys and find ordering differences
111- * @param {string[] } englishKeys - Keys from English locale
112- * @param {string[] } localeKeys - Keys from target locale
113- * @returns {Object } Object containing ordering issues
114- */
85+ // Compare key ordering and find differences
11586function compareKeyOrdering ( englishKeys , localeKeys ) {
116- const issues = {
117- missing : [ ] ,
118- extra : [ ] ,
119- outOfOrder : [ ] ,
120- }
121-
122- // Find missing and extra keys
12387 const englishSet = new Set ( englishKeys )
12488 const localeSet = new Set ( localeKeys )
89+ const missing = englishKeys . filter ( ( key ) => ! localeSet . has ( key ) )
90+ const extra = localeKeys . filter ( ( key ) => ! englishSet . has ( key ) )
12591
126- issues . missing = englishKeys . filter ( ( key ) => ! localeSet . has ( key ) )
127- issues . extra = localeKeys . filter ( ( key ) => ! englishSet . has ( key ) )
128-
129- // Check ordering for common keys
13092 const commonKeys = englishKeys . filter ( ( key ) => localeSet . has ( key ) )
13193 const localeCommonKeys = localeKeys . filter ( ( key ) => englishSet . has ( key ) )
94+ const outOfOrder = [ ]
13295
13396 for ( let i = 0 ; i < commonKeys . length ; i ++ ) {
13497 if ( commonKeys [ i ] !== localeCommonKeys [ i ] ) {
135- issues . outOfOrder . push ( {
98+ outOfOrder . push ( {
13699 expected : commonKeys [ i ] ,
137100 actual : localeCommonKeys [ i ] ,
138- position : i ,
139101 } )
140102 }
141103 }
142104
143- return issues
105+ return { missing , extra , outOfOrder }
144106}
145107
146- /**
147- * Check key ordering for a specific area
148- * @param {string } area - Area to check ('core' or 'webview')
149- * @returns {boolean } True if there are ordering issues
150- */
151108function checkAreaKeyOrdering ( area ) {
152109 const LOCALES_DIR = LOCALES_DIRS [ area ]
153-
154- // Get all locale directories (excluding English)
155110 const allLocales = fs . readdirSync ( LOCALES_DIR ) . filter ( ( item ) => {
156- const stats = fs . statSync ( path . join ( LOCALES_DIR , item ) )
157- return stats . isDirectory ( ) && item !== "en"
111+ return fs . statSync ( path . join ( LOCALES_DIR , item ) ) . isDirectory ( ) && item !== "en"
158112 } )
159113
160- // Filter to the specified locale if provided
161114 const locales = args . locale ? allLocales . filter ( ( locale ) => locale === args . locale ) : allLocales
162-
163115 if ( args . locale && locales . length === 0 ) {
164116 console . error ( `Error: Locale '${ args . locale } ' not found in ${ LOCALES_DIR } ` )
165117 process . exit ( 1 )
166118 }
167119
168- console . log (
169- `\n${ area === "core" ? "BACKEND" : "FRONTEND" } - Checking key ordering for ${ locales . length } locale(s): ${ locales . join ( ", " ) } ` ,
170- )
120+ console . log ( `\n${ area } - Checking key ordering for ${ locales . length } locale(s): ${ locales . join ( ", " ) } ` )
171121
172- // Get all English JSON files
173122 const englishDir = path . join ( LOCALES_DIR , "en" )
174123 let englishFiles = fs . readdirSync ( englishDir ) . filter ( ( file ) => file . endsWith ( ".json" ) && ! file . startsWith ( "." ) )
175124
176- // Filter to the specified file if provided
177125 if ( args . file ) {
178126 if ( ! englishFiles . includes ( args . file ) ) {
179127 console . error ( `Error: File '${ args . file } ' not found in ${ englishDir } ` )
180128 process . exit ( 1 )
181129 }
182- englishFiles = englishFiles . filter ( ( file ) => file === args . file )
130+ englishFiles = [ args . file ]
183131 }
184132
185133 console . log ( `Checking ${ englishFiles . length } file(s): ${ englishFiles . join ( ", " ) } ` )
186-
187134 let hasOrderingIssues = false
188135
189- // Check each locale
190136 for ( const locale of locales ) {
191- let localeHasIssues = false
192137 const localeIssues = [ ]
193138
194139 for ( const fileName of englishFiles ) {
195140 const englishFilePath = path . join ( englishDir , fileName )
196141 const localeFilePath = path . join ( LOCALES_DIR , locale , fileName )
197142
198- // Check if the locale file exists
199143 if ( ! fs . existsSync ( localeFilePath ) ) {
200- localeHasIssues = true
201- localeIssues . push ( ` ⚠️ ${ fileName } : File missing in ${ locale } ` )
144+ localeIssues . push ( ` ⚠️ ${ fileName } : File missing` )
202145 continue
203146 }
204147
205- // Load and parse both files
206- let englishContent , localeContent
207-
208148 try {
209- englishContent = JSON . parse ( fs . readFileSync ( englishFilePath , "utf8" ) )
210- localeContent = JSON . parse ( fs . readFileSync ( localeFilePath , "utf8" ) )
211- } catch ( e ) {
212- localeHasIssues = true
213- localeIssues . push ( ` ❌ ${ fileName } : JSON parsing error - ${ e . message } ` )
214- continue
215- }
216-
217- // Extract keys in order
218- const englishKeys = extractKeysInOrder ( englishContent )
219- const localeKeys = extractKeysInOrder ( localeContent )
220-
221- // Compare ordering
222- const issues = compareKeyOrdering ( englishKeys , localeKeys )
223-
224- if ( issues . missing . length > 0 || issues . extra . length > 0 || issues . outOfOrder . length > 0 ) {
225- localeHasIssues = true
226- localeIssues . push ( ` ❌ ${ fileName } : Key ordering issues found` )
227-
228- if ( issues . missing . length > 0 ) {
229- localeIssues . push (
230- ` Missing keys: ${ issues . missing . slice ( 0 , 3 ) . join ( ", " ) } ${ issues . missing . length > 3 ? ` (+${ issues . missing . length - 3 } more)` : "" } ` ,
231- )
232- }
233-
234- if ( issues . extra . length > 0 ) {
235- localeIssues . push (
236- ` Extra keys: ${ issues . extra . slice ( 0 , 3 ) . join ( ", " ) } ${ issues . extra . length > 3 ? ` (+${ issues . extra . length - 3 } more)` : "" } ` ,
237- )
238- }
239-
240- if ( issues . outOfOrder . length > 0 ) {
241- const firstMismatches = issues . outOfOrder
242- . slice ( 0 , 2 )
243- . map ( ( issue ) => `expected '${ issue . expected } ' but found '${ issue . actual } '` )
244- . join ( ", " )
245- localeIssues . push (
246- ` Order mismatches: ${ firstMismatches } ${ issues . outOfOrder . length > 2 ? ` (+${ issues . outOfOrder . length - 2 } more)` : "" } ` ,
247- )
149+ const englishContent = JSON . parse ( fs . readFileSync ( englishFilePath , "utf8" ) )
150+ const localeContent = JSON . parse ( fs . readFileSync ( localeFilePath , "utf8" ) )
151+ const issues = compareKeyOrdering ( extractKeysInOrder ( englishContent ) , extractKeysInOrder ( localeContent ) )
152+
153+ if ( issues . missing . length + issues . extra . length + issues . outOfOrder . length > 0 ) {
154+ localeIssues . push ( ` ❌ ${ fileName } : Key ordering issues` )
155+
156+ if ( issues . missing . length > 0 ) {
157+ const preview = issues . missing . slice ( 0 , 3 ) . join ( ", " )
158+ localeIssues . push (
159+ ` Missing: ${ preview } ${ issues . missing . length > 3 ? ` (+${ issues . missing . length - 3 } more)` : "" } ` ,
160+ )
161+ }
162+ if ( issues . extra . length > 0 ) {
163+ const preview = issues . extra . slice ( 0 , 3 ) . join ( ", " )
164+ localeIssues . push (
165+ ` Extra: ${ preview } ${ issues . extra . length > 3 ? ` (+${ issues . extra . length - 3 } more)` : "" } ` ,
166+ )
167+ }
168+ if ( issues . outOfOrder . length > 0 ) {
169+ const preview = issues . outOfOrder
170+ . slice ( 0 , 2 )
171+ . map ( ( issue ) => `expected '${ issue . expected } ' but found '${ issue . actual } '` )
172+ . join ( ", " )
173+ localeIssues . push (
174+ ` Order: ${ preview } ${ issues . outOfOrder . length > 2 ? ` (+${ issues . outOfOrder . length - 2 } more)` : "" } ` ,
175+ )
176+ }
248177 }
178+ } catch ( e ) {
179+ localeIssues . push ( ` ❌ ${ fileName } : JSON error - ${ e . message } ` )
249180 }
250181 }
251182
252- // Only print issues
253- if ( localeHasIssues ) {
183+ if ( localeIssues . length > 0 ) {
254184 console . log ( `\n 📋 Checking locale: ${ locale } ` )
255185 localeIssues . forEach ( ( issue ) => console . log ( issue ) )
256186 hasOrderingIssues = true
@@ -260,40 +190,23 @@ function checkAreaKeyOrdering(area) {
260190 return hasOrderingIssues
261191}
262192
263- /**
264- * Main function to check locale key ordering
265- */
266193function lintLocaleKeyOrdering ( ) {
267194 try {
268195 console . log ( "🔍 Starting locale key ordering check..." )
196+ const anyAreaHasIssues = areasToCheck . some ( ( area ) => checkAreaKeyOrdering ( area ) )
269197
270- let anyAreaHasIssues = false
271-
272- // Check each requested area
273- for ( const area of areasToCheck ) {
274- const hasIssues = checkAreaKeyOrdering ( area )
275- anyAreaHasIssues = anyAreaHasIssues || hasIssues
276- }
277-
278- // Summary
279198 if ( ! anyAreaHasIssues ) {
280199 console . log ( "✅ All locale files have consistent key ordering!" )
281200 process . exit ( 0 )
282201 } else {
283202 console . log ( "\n❌ Key ordering inconsistencies detected!" )
284- console . log ( "\n💡 To fix ordering issues:" )
285- console . log ( "1. Review the files with ordering mismatches" )
286- console . log ( "2. Reorder keys to match the English locale files" )
287- console . log ( "3. Use MCP sort_i18n_keys tool to fix ordering" )
288- console . log ( "4. Run this linter again to verify fixes" )
203+ console . log ( "\n💡 To fix: Use MCP sort_i18n_keys tool or manually reorder keys to match English files" )
289204 process . exit ( 1 )
290205 }
291206 } catch ( error ) {
292207 console . error ( "Error:" , error . message )
293- console . error ( error . stack )
294208 process . exit ( 1 )
295209 }
296210}
297211
298- // Run the main function
299212lintLocaleKeyOrdering ( )
0 commit comments