11/* eslint-disable no-console */
2- import process from 'node:process'
2+ import * as process from 'node:process'
33import { existsSync , readdirSync , readFileSync , writeFileSync } from 'node:fs'
4- import { join } from 'node:path'
4+ import { basename , join } from 'node:path'
55import { fileURLToPath } from 'node:url'
6+ import { countryLocaleVariants , currentLocales } from '../config/i18n.ts'
7+ import { mergeLocaleObject } from '../lunaria/prepare-json-files.ts'
68
79const LOCALES_DIRECTORY = fileURLToPath ( new URL ( '../i18n/locales' , import . meta. url ) )
810const REFERENCE_FILE_NAME = 'en.json'
@@ -17,13 +19,95 @@ const COLORS = {
1719} as const
1820
1921type NestedObject = { [ key : string ] : unknown }
22+ interface LocaleInfo {
23+ filePath : string
24+ locale : string
25+ lang : string
26+ country ?: string
27+ forCountry ?: boolean
28+ mergeLocale ?: boolean
29+ }
30+
31+ const contries = new Map < string , Map < string , LocaleInfo > > ( )
32+
33+ const extractLocalInfo = (
34+ filePath : string ,
35+ forCountry : boolean = false ,
36+ mergeLocale : boolean = false ,
37+ ) : LocaleInfo => {
38+ const locale = basename ( filePath , '.json' )
39+ const [ lang , country ] = locale . split ( '-' )
40+ return { filePath, locale, lang, country, forCountry, mergeLocale }
41+ }
42+
43+ const populateLocaleCountries = ( ) : void => {
44+ for ( const lang of Object . keys ( countryLocaleVariants ) ) {
45+ const variants = countryLocaleVariants [ lang ]
46+ for ( const variant of variants ) {
47+ if ( ! contries . has ( lang ) ) {
48+ contries . set ( lang , new Map ( ) )
49+ }
50+ if ( variant . country ) {
51+ contries . get ( lang ) ! . set ( lang , extractLocalInfo ( lang , true ) )
52+ contries . get ( lang ) ! . set ( variant . code , extractLocalInfo ( variant . code , true , true ) )
53+ } else {
54+ contries . get ( lang ) ! . set ( variant . code , extractLocalInfo ( variant . code , false , true ) )
55+ }
56+ }
57+ }
58+ }
2059
21- const loadJson = ( filePath : string ) : NestedObject => {
60+ /**
61+ * We use ISO 639-1 for the language and ISO 3166-1 for the country (e.g. es-ES), we're preventing here:
62+ * using the language as the JSON file name when there is no country variant.
63+ *
64+ * For example, `az.json` is wrong, should be `az-AZ.json` since it is not included at `countryLocaleVariants`.
65+ */
66+ const checkCountryVariant = ( localeInfo : LocaleInfo ) : void => {
67+ const { locale, lang, country } = localeInfo
68+ const countryVariant = contries . get ( lang )
69+ if ( countryVariant ) {
70+ if ( country ) {
71+ const found = countryVariant . get ( locale )
72+ if ( ! found ) {
73+ console . error (
74+ `${ COLORS . red } Error: Invalid locale file "${ locale } ", it should be included at "countryLocaleVariants" in config/i18n.ts"${ COLORS . reset } ` ,
75+ )
76+ process . exit ( 1 )
77+ }
78+ localeInfo . forCountry = found . forCountry
79+ localeInfo . mergeLocale = found . mergeLocale
80+ } else {
81+ localeInfo . forCountry = false
82+ localeInfo . mergeLocale = false
83+ }
84+ } else {
85+ if ( ! country ) {
86+ console . error (
87+ `${ 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 } ` ,
88+ )
89+ process . exit ( 1 )
90+ }
91+ }
92+ }
93+
94+ const checkJsonName = ( filePath : string ) : LocaleInfo => {
95+ const info = extractLocalInfo ( filePath )
96+ checkCountryVariant ( info )
97+ return info
98+ }
99+
100+ const loadJson = async ( { filePath, mergeLocale, locale } : LocaleInfo ) : Promise < NestedObject > => {
22101 if ( ! existsSync ( filePath ) ) {
23102 console . error ( `${ COLORS . red } Error: File not found at ${ filePath } ${ COLORS . reset } ` )
24103 process . exit ( 1 )
25104 }
26- return JSON . parse ( readFileSync ( filePath , 'utf-8' ) ) as NestedObject
105+
106+ if ( ! mergeLocale ) {
107+ return JSON . parse ( readFileSync ( filePath , 'utf-8' ) ) as NestedObject
108+ }
109+
110+ return await mergeLocaleObject ( currentLocales . find ( l => l . code === locale ) ! )
27111}
28112
29113type SyncStats = {
@@ -51,7 +135,13 @@ const syncLocaleData = (
51135
52136 if ( isNested ( refValue ) ) {
53137 const nextTarget = isNested ( target [ key ] ) ? target [ key ] : { }
54- result [ key ] = syncLocaleData ( refValue , nextTarget , stats , fix , propertyPath )
138+ const data = syncLocaleData ( refValue , nextTarget , stats , fix , propertyPath )
139+ // don't add empty objects: --fix will prevent this
140+ if ( Object . keys ( data ) . length === 0 ) {
141+ delete result [ key ]
142+ } else {
143+ result [ key ] = data
144+ }
55145 } else {
56146 stats . referenceKeys . push ( propertyPath )
57147
@@ -91,13 +181,14 @@ const logSection = (
91181 keys . forEach ( key => console . log ( ` - ${ key } ` ) )
92182}
93183
94- const processLocale = (
184+ const processLocale = async (
95185 localeFile : string ,
96186 referenceContent : NestedObject ,
97187 fix = false ,
98- ) : SyncStats => {
188+ ) : Promise < SyncStats > => {
99189 const filePath = join ( LOCALES_DIRECTORY , localeFile )
100- const targetContent = loadJson ( filePath )
190+ const localeInfo = checkJsonName ( filePath )
191+ const targetContent = await loadJson ( localeInfo )
101192
102193 const stats : SyncStats = {
103194 missing : [ ] ,
@@ -115,7 +206,11 @@ const processLocale = (
115206 return stats
116207}
117208
118- const runSingleLocale = ( locale : string , referenceContent : NestedObject , fix = false ) : void => {
209+ const runSingleLocale = async (
210+ locale : string ,
211+ referenceContent : NestedObject ,
212+ fix = false ,
213+ ) : Promise < void > => {
119214 const localeFile = locale . endsWith ( '.json' ) ? locale : `${ locale } .json`
120215 const filePath = join ( LOCALES_DIRECTORY , localeFile )
121216
@@ -124,7 +219,7 @@ const runSingleLocale = (locale: string, referenceContent: NestedObject, fix = f
124219 process . exit ( 1 )
125220 }
126221
127- const { missing, extra, referenceKeys } = processLocale ( localeFile , referenceContent , fix )
222+ const { missing, extra, referenceKeys } = await processLocale ( localeFile , referenceContent , fix )
128223
129224 console . log (
130225 `${ COLORS . cyan } === Missing keys for ${ localeFile } ${ fix ? ' (with --fix)' : '' } ===${ COLORS . reset } ` ,
@@ -152,7 +247,7 @@ const runSingleLocale = (locale: string, referenceContent: NestedObject, fix = f
152247 console . log ( '' )
153248}
154249
155- const runAllLocales = ( referenceContent : NestedObject , fix = false ) : void => {
250+ const runAllLocales = async ( referenceContent : NestedObject , fix = false ) : Promise < void > => {
156251 const localeFiles = readdirSync ( LOCALES_DIRECTORY ) . filter (
157252 file => file . endsWith ( '.json' ) && file !== REFERENCE_FILE_NAME ,
158253 )
@@ -164,7 +259,7 @@ const runAllLocales = (referenceContent: NestedObject, fix = false): void => {
164259 let totalAdded = 0
165260
166261 for ( const localeFile of localeFiles ) {
167- const stats = processLocale ( localeFile , referenceContent , fix )
262+ const stats = await processLocale ( localeFile , referenceContent , fix )
168263 results . push ( {
169264 file : localeFile ,
170265 ...stats ,
@@ -232,20 +327,26 @@ const runAllLocales = (referenceContent: NestedObject, fix = false): void => {
232327 console . log ( '' )
233328}
234329
235- const run = ( ) : void => {
330+ const run = async ( ) : Promise < void > => {
236331 const referenceFilePath = join ( LOCALES_DIRECTORY , REFERENCE_FILE_NAME )
237- const referenceContent = loadJson ( referenceFilePath )
332+ const referenceContent = await loadJson ( {
333+ filePath : referenceFilePath ,
334+ locale : 'en' ,
335+ lang : 'en' ,
336+ } )
238337
239338 const args = process . argv . slice ( 2 )
240339 const fix = args . includes ( '--fix' )
241340 const targetLocale = args . find ( arg => ! arg . startsWith ( '--' ) )
242341
342+ populateLocaleCountries ( )
343+
243344 if ( targetLocale ) {
244345 // Single locale mode
245- runSingleLocale ( targetLocale , referenceContent , fix )
346+ await runSingleLocale ( targetLocale , referenceContent , fix )
246347 } else {
247348 // All locales mode: check all and remove extraneous keys
248- runAllLocales ( referenceContent , fix )
349+ await runAllLocales ( referenceContent , fix )
249350 }
250351}
251352
0 commit comments