11/* eslint-disable no-console */
2- import process from 'node:process'
2+ import type { LocaleObject } from '@nuxtjs/i18n'
3+ import * as process from 'node:process'
34import { existsSync , readdirSync , readFileSync , writeFileSync } from 'node:fs'
4- import { join } from 'node:path'
5+ import { basename , join } from 'node:path'
56import { fileURLToPath } from 'node:url'
7+ import { countryLocaleVariants , currentLocales } from '../config/i18n.ts'
8+ import { mergeLocaleObject } from '../lunaria/prepare-json-files.ts'
69import { COLORS } from './utils.ts'
710
811const LOCALES_DIRECTORY = fileURLToPath ( new URL ( '../i18n/locales' , import . meta. url ) )
912const REFERENCE_FILE_NAME = 'en.json'
1013
1114type NestedObject = { [ key : string ] : unknown }
15+ interface LocaleInfo {
16+ filePath : string
17+ locale : string
18+ lang : string
19+ country ?: string
20+ forCountry ?: boolean
21+ mergeLocale ?: boolean
22+ }
23+
24+ const countries = new Map < string , Map < string , LocaleInfo > > ( )
25+ const availableLocales = new Map < string , LocaleObject > ( )
26+
27+ function extractLocalInfo ( filePath : string ) : LocaleInfo {
28+ const locale = basename ( filePath , '.json' )
29+ const [ lang , country ] = locale . split ( '-' )
30+ return { filePath, locale, lang, country }
31+ }
32+
33+ function createVariantInfo (
34+ code : string ,
35+ options : { forCountry : boolean ; mergeLocale : boolean } ,
36+ ) : LocaleInfo {
37+ const [ lang , country ] = code . split ( '-' )
38+ return { filePath : '' , locale : code , lang, country, ...options }
39+ }
1240
13- const loadJson = ( filePath : string ) : NestedObject => {
41+ const populateLocaleCountries = ( ) : void => {
42+ for ( const lang of Object . keys ( countryLocaleVariants ) ) {
43+ const variants = countryLocaleVariants [ lang ]
44+ for ( const variant of variants ) {
45+ if ( ! countries . has ( lang ) ) {
46+ countries . set ( lang , new Map ( ) )
47+ }
48+ if ( variant . country ) {
49+ countries
50+ . get ( lang ) !
51+ . set ( lang , createVariantInfo ( lang , { forCountry : true , mergeLocale : false } ) )
52+ countries
53+ . get ( lang ) !
54+ . set (
55+ variant . code ,
56+ createVariantInfo ( variant . code , { forCountry : true , mergeLocale : true } ) ,
57+ )
58+ } else {
59+ countries
60+ . get ( lang ) !
61+ . set (
62+ variant . code ,
63+ createVariantInfo ( variant . code , { forCountry : false , mergeLocale : true } ) ,
64+ )
65+ }
66+ }
67+ }
68+
69+ for ( const localeData of currentLocales ) {
70+ availableLocales . set ( localeData . code , localeData )
71+ }
72+ }
73+
74+ /**
75+ * We use ISO 639-1 for the language and ISO 3166-1 for the country (e.g. es-ES), we're preventing here:
76+ * using the language as the JSON file name when there is no country variant.
77+ *
78+ * For example, `az.json` is wrong, should be `az-AZ.json` since it is not included at `countryLocaleVariants`.
79+ */
80+ const checkCountryVariant = ( localeInfo : LocaleInfo ) : void => {
81+ const { locale, lang, country } = localeInfo
82+ const countryVariant = countries . get ( lang )
83+ if ( countryVariant ) {
84+ if ( country ) {
85+ const found = countryVariant . get ( locale )
86+ if ( ! found ) {
87+ console . error (
88+ `${ COLORS . red } Error: Invalid locale file "${ locale } ", it should be included at "countryLocaleVariants" in config/i18n.ts"${ COLORS . reset } ` ,
89+ )
90+ process . exit ( 1 )
91+ }
92+ localeInfo . forCountry = found . forCountry
93+ localeInfo . mergeLocale = found . mergeLocale
94+ } else {
95+ localeInfo . forCountry = false
96+ localeInfo . mergeLocale = false
97+ }
98+ } else {
99+ if ( ! country ) {
100+ console . error (
101+ `${ COLORS . red } Error: Invalid locale file "${ locale } ", it should be included at "countryLocaleVariants" in config/i18n.ts, or change the name to include country name "${ lang } -<country-name>"${ COLORS . reset } ` ,
102+ )
103+ process . exit ( 1 )
104+ }
105+ }
106+ }
107+
108+ const checkJsonName = ( filePath : string ) : LocaleInfo => {
109+ const info = extractLocalInfo ( filePath )
110+ checkCountryVariant ( info )
111+ return info
112+ }
113+
114+ const loadJson = async ( { filePath, mergeLocale, locale } : LocaleInfo ) : Promise < NestedObject > => {
14115 if ( ! existsSync ( filePath ) ) {
15116 console . error ( `${ COLORS . red } Error: File not found at ${ filePath } ${ COLORS . reset } ` )
16117 process . exit ( 1 )
17118 }
18- return JSON . parse ( readFileSync ( filePath , 'utf-8' ) ) as NestedObject
119+
120+ if ( ! mergeLocale ) {
121+ return JSON . parse ( readFileSync ( filePath , 'utf-8' ) ) as NestedObject
122+ }
123+
124+ const localeObject = availableLocales . get ( locale )
125+ if ( ! localeObject ) {
126+ console . error (
127+ `${ COLORS . red } Error: Locale "${ locale } " not found in currentLocales${ COLORS . reset } ` ,
128+ )
129+ process . exit ( 1 )
130+ }
131+ const merged = await mergeLocaleObject ( localeObject )
132+ if ( ! merged ) {
133+ console . error ( `${ COLORS . red } Error: Failed to merge locale "${ locale } "${ COLORS . reset } ` )
134+ process . exit ( 1 )
135+ }
136+ return merged
19137}
20138
21139type SyncStats = {
@@ -43,7 +161,14 @@ const syncLocaleData = (
43161
44162 if ( isNested ( refValue ) ) {
45163 const nextTarget = isNested ( target [ key ] ) ? target [ key ] : { }
46- result [ key ] = syncLocaleData ( refValue , nextTarget , stats , fix , propertyPath )
164+ const data = syncLocaleData ( refValue , nextTarget , stats , fix , propertyPath )
165+ // When fixing, empty objects won't occur since missing keys get placeholders.
166+ // Without --fix, keep empty objects to preserve structural parity with the reference.
167+ if ( fix && Object . keys ( data ) . length === 0 ) {
168+ delete result [ key ]
169+ } else {
170+ result [ key ] = data
171+ }
47172 } else {
48173 stats . referenceKeys . push ( propertyPath )
49174
@@ -83,13 +208,14 @@ const logSection = (
83208 keys . forEach ( key => console . log ( ` - ${ key } ` ) )
84209}
85210
86- const processLocale = (
211+ const processLocale = async (
87212 localeFile : string ,
88213 referenceContent : NestedObject ,
89214 fix = false ,
90- ) : SyncStats => {
215+ ) : Promise < SyncStats > => {
91216 const filePath = join ( LOCALES_DIRECTORY , localeFile )
92- const targetContent = loadJson ( filePath )
217+ const localeInfo = checkJsonName ( filePath )
218+ const targetContent = await loadJson ( localeInfo )
93219
94220 const stats : SyncStats = {
95221 missing : [ ] ,
@@ -107,7 +233,11 @@ const processLocale = (
107233 return stats
108234}
109235
110- const runSingleLocale = ( locale : string , referenceContent : NestedObject , fix = false ) : void => {
236+ const runSingleLocale = async (
237+ locale : string ,
238+ referenceContent : NestedObject ,
239+ fix = false ,
240+ ) : Promise < void > => {
111241 const localeFile = locale . endsWith ( '.json' ) ? locale : `${ locale } .json`
112242 const filePath = join ( LOCALES_DIRECTORY , localeFile )
113243
@@ -116,7 +246,7 @@ const runSingleLocale = (locale: string, referenceContent: NestedObject, fix = f
116246 process . exit ( 1 )
117247 }
118248
119- const { missing, extra, referenceKeys } = processLocale ( localeFile , referenceContent , fix )
249+ const { missing, extra, referenceKeys } = await processLocale ( localeFile , referenceContent , fix )
120250
121251 console . log (
122252 `${ COLORS . cyan } === Missing keys for ${ localeFile } ${ fix ? ' (with --fix)' : '' } ===${ COLORS . reset } ` ,
@@ -144,7 +274,7 @@ const runSingleLocale = (locale: string, referenceContent: NestedObject, fix = f
144274 console . log ( '' )
145275}
146276
147- const runAllLocales = ( referenceContent : NestedObject , fix = false ) : void => {
277+ const runAllLocales = async ( referenceContent : NestedObject , fix = false ) : Promise < void > => {
148278 const localeFiles = readdirSync ( LOCALES_DIRECTORY ) . filter (
149279 file => file . endsWith ( '.json' ) && file !== REFERENCE_FILE_NAME ,
150280 )
@@ -156,7 +286,7 @@ const runAllLocales = (referenceContent: NestedObject, fix = false): void => {
156286 let totalAdded = 0
157287
158288 for ( const localeFile of localeFiles ) {
159- const stats = processLocale ( localeFile , referenceContent , fix )
289+ const stats = await processLocale ( localeFile , referenceContent , fix )
160290 results . push ( {
161291 file : localeFile ,
162292 ...stats ,
@@ -224,21 +354,26 @@ const runAllLocales = (referenceContent: NestedObject, fix = false): void => {
224354 console . log ( '' )
225355}
226356
227- const run = ( ) : void => {
357+ const run = async ( ) : Promise < void > => {
358+ populateLocaleCountries ( )
228359 const referenceFilePath = join ( LOCALES_DIRECTORY , REFERENCE_FILE_NAME )
229- const referenceContent = loadJson ( referenceFilePath )
360+ const referenceContent = await loadJson ( {
361+ filePath : referenceFilePath ,
362+ locale : 'en' ,
363+ lang : 'en' ,
364+ } )
230365
231366 const args = process . argv . slice ( 2 )
232367 const fix = args . includes ( '--fix' )
233368 const targetLocale = args . find ( arg => ! arg . startsWith ( '--' ) )
234369
235370 if ( targetLocale ) {
236371 // Single locale mode
237- runSingleLocale ( targetLocale , referenceContent , fix )
372+ await runSingleLocale ( targetLocale , referenceContent , fix )
238373 } else {
239374 // All locales mode: check all and remove extraneous keys
240- runAllLocales ( referenceContent , fix )
375+ await runAllLocales ( referenceContent , fix )
241376 }
242377}
243378
244- run ( )
379+ await run ( )
0 commit comments