From 5b68303e2f41d151afa832ef138daddb53af538a Mon Sep 17 00:00:00 2001 From: userquin Date: Fri, 6 Feb 2026 02:27:02 +0100 Subject: [PATCH 1/7] chore: update `compare-translations.ts` logic --- lunaria/prepare-json-files.ts | 23 +++--- scripts/compare-translations.ts | 133 ++++++++++++++++++++++++++++---- 2 files changed, 131 insertions(+), 25 deletions(-) diff --git a/lunaria/prepare-json-files.ts b/lunaria/prepare-json-files.ts index 0f962dd37..6eaa7cf1d 100644 --- a/lunaria/prepare-json-files.ts +++ b/lunaria/prepare-json-files.ts @@ -37,15 +37,7 @@ export async function prepareJsonFiles() { await Promise.all(currentLocales.map(l => mergeLocale(l))) } -async function loadJsonFile(name: string) { - return JSON.parse(await fs.readFile(path.resolve(`${localesFolder}/${name}`), 'utf8')) -} - -function getFileName(file: string | { path: string }): string { - return typeof file === 'string' ? file : file.path -} - -async function mergeLocale(locale: LocaleObject) { +export async function mergeLocaleObject(locale: LocaleObject) { const files = locale.files ?? [] if (locale.file || files.length === 1) { const json = locale.file ?? (files[0] ? getFileName(files[0]) : undefined) @@ -65,6 +57,19 @@ async function mergeLocale(locale: LocaleObject) { deepCopy(currentSource, source) } + return source +} + +async function loadJsonFile(name: string) { + return JSON.parse(await fs.readFile(path.resolve(`${localesFolder}/${name}`), 'utf8')) +} + +function getFileName(file: string | { path: string }): string { + return typeof file === 'string' ? file : file.path +} + +async function mergeLocale(locale: LocaleObject) { + const source = await mergeLocaleObject(locale) await fs.writeFile( path.resolve(`${destFolder}/${locale.code}.json`), JSON.stringify(source, null, 2), diff --git a/scripts/compare-translations.ts b/scripts/compare-translations.ts index 401d6dcbc..ead7f7c57 100644 --- a/scripts/compare-translations.ts +++ b/scripts/compare-translations.ts @@ -1,8 +1,10 @@ /* eslint-disable no-console */ -import process from 'node:process' +import * as process from 'node:process' import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs' -import { join } from 'node:path' +import { basename, join } from 'node:path' import { fileURLToPath } from 'node:url' +import { countryLocaleVariants, currentLocales } from '../config/i18n.ts' +import { mergeLocaleObject } from '../lunaria/prepare-json-files.ts' const LOCALES_DIRECTORY = fileURLToPath(new URL('../i18n/locales', import.meta.url)) const REFERENCE_FILE_NAME = 'en.json' @@ -17,13 +19,95 @@ const COLORS = { } as const type NestedObject = { [key: string]: unknown } +interface LocaleInfo { + filePath: string + locale: string + lang: string + country?: string + forCountry?: boolean + mergeLocale?: boolean +} + +const contries = new Map>() + +const extractLocalInfo = ( + filePath: string, + forCountry: boolean = false, + mergeLocale: boolean = false, +): LocaleInfo => { + const locale = basename(filePath, '.json') + const [lang, country] = locale.split('-') + return { filePath, locale, lang, country, forCountry, mergeLocale } +} + +const populateLocaleCountries = (): void => { + for (const lang of Object.keys(countryLocaleVariants)) { + const variants = countryLocaleVariants[lang] + for (const variant of variants) { + if (!contries.has(lang)) { + contries.set(lang, new Map()) + } + if (variant.country) { + contries.get(lang)!.set(lang, extractLocalInfo(lang, true)) + contries.get(lang)!.set(variant.code, extractLocalInfo(variant.code, true, true)) + } else { + contries.get(lang)!.set(variant.code, extractLocalInfo(variant.code, false, true)) + } + } + } +} -const loadJson = (filePath: string): NestedObject => { +/** + * We use ISO 639-1 for the language and ISO 3166-1 for the country (e.g. es-ES), we're preventing here: + * using the language as the JSON file name when there is no country variant. + * + * For example, `az.json` is wrong, should be `az-AZ.json` since it is not included at `countryLocaleVariants`. + */ +const checkCountryVariant = (localeInfo: LocaleInfo): void => { + const { locale, lang, country } = localeInfo + const countryVariant = contries.get(lang) + if (countryVariant) { + if (country) { + const found = countryVariant.get(locale) + if (!found) { + console.error( + `${COLORS.red}Error: Invalid locale file "${locale}", it should be included at "countryLocaleVariants" in config/i18n.ts"${COLORS.reset}`, + ) + process.exit(1) + } + localeInfo.forCountry = found.forCountry + localeInfo.mergeLocale = found.mergeLocale + } else { + localeInfo.forCountry = false + localeInfo.mergeLocale = false + } + } else { + if (!country) { + console.error( + `${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}-"${COLORS.reset}`, + ) + process.exit(1) + } + } +} + +const checkJsonName = (filePath: string): LocaleInfo => { + const info = extractLocalInfo(filePath) + checkCountryVariant(info) + return info +} + +const loadJson = async ({ filePath, mergeLocale, locale }: LocaleInfo): Promise => { if (!existsSync(filePath)) { console.error(`${COLORS.red}Error: File not found at ${filePath}${COLORS.reset}`) process.exit(1) } - return JSON.parse(readFileSync(filePath, 'utf-8')) as NestedObject + + if (!mergeLocale) { + return JSON.parse(readFileSync(filePath, 'utf-8')) as NestedObject + } + + return await mergeLocaleObject(currentLocales.find(l => l.code === locale)!) } type SyncStats = { @@ -51,7 +135,13 @@ const syncLocaleData = ( if (isNested(refValue)) { const nextTarget = isNested(target[key]) ? target[key] : {} - result[key] = syncLocaleData(refValue, nextTarget, stats, fix, propertyPath) + const data = syncLocaleData(refValue, nextTarget, stats, fix, propertyPath) + // don't add empty objects: --fix will prevent this + if (Object.keys(data).length === 0) { + delete result[key] + } else { + result[key] = data + } } else { stats.referenceKeys.push(propertyPath) @@ -91,13 +181,14 @@ const logSection = ( keys.forEach(key => console.log(` - ${key}`)) } -const processLocale = ( +const processLocale = async ( localeFile: string, referenceContent: NestedObject, fix = false, -): SyncStats => { +): Promise => { const filePath = join(LOCALES_DIRECTORY, localeFile) - const targetContent = loadJson(filePath) + const localeInfo = checkJsonName(filePath) + const targetContent = await loadJson(localeInfo) const stats: SyncStats = { missing: [], @@ -115,7 +206,11 @@ const processLocale = ( return stats } -const runSingleLocale = (locale: string, referenceContent: NestedObject, fix = false): void => { +const runSingleLocale = async ( + locale: string, + referenceContent: NestedObject, + fix = false, +): Promise => { const localeFile = locale.endsWith('.json') ? locale : `${locale}.json` const filePath = join(LOCALES_DIRECTORY, localeFile) @@ -124,7 +219,7 @@ const runSingleLocale = (locale: string, referenceContent: NestedObject, fix = f process.exit(1) } - const { missing, extra, referenceKeys } = processLocale(localeFile, referenceContent, fix) + const { missing, extra, referenceKeys } = await processLocale(localeFile, referenceContent, fix) console.log( `${COLORS.cyan}=== Missing keys for ${localeFile}${fix ? ' (with --fix)' : ''} ===${COLORS.reset}`, @@ -152,7 +247,7 @@ const runSingleLocale = (locale: string, referenceContent: NestedObject, fix = f console.log('') } -const runAllLocales = (referenceContent: NestedObject, fix = false): void => { +const runAllLocales = async (referenceContent: NestedObject, fix = false): Promise => { const localeFiles = readdirSync(LOCALES_DIRECTORY).filter( file => file.endsWith('.json') && file !== REFERENCE_FILE_NAME, ) @@ -164,7 +259,7 @@ const runAllLocales = (referenceContent: NestedObject, fix = false): void => { let totalAdded = 0 for (const localeFile of localeFiles) { - const stats = processLocale(localeFile, referenceContent, fix) + const stats = await processLocale(localeFile, referenceContent, fix) results.push({ file: localeFile, ...stats, @@ -232,20 +327,26 @@ const runAllLocales = (referenceContent: NestedObject, fix = false): void => { console.log('') } -const run = (): void => { +const run = async (): Promise => { const referenceFilePath = join(LOCALES_DIRECTORY, REFERENCE_FILE_NAME) - const referenceContent = loadJson(referenceFilePath) + const referenceContent = await loadJson({ + filePath: referenceFilePath, + locale: 'en', + lang: 'en', + }) const args = process.argv.slice(2) const fix = args.includes('--fix') const targetLocale = args.find(arg => !arg.startsWith('--')) + populateLocaleCountries() + if (targetLocale) { // Single locale mode - runSingleLocale(targetLocale, referenceContent, fix) + await runSingleLocale(targetLocale, referenceContent, fix) } else { // All locales mode: check all and remove extraneous keys - runAllLocales(referenceContent, fix) + await runAllLocales(referenceContent, fix) } } From e9a84423f7da536b58fa1bb4d73f3599f4f3e12d Mon Sep 17 00:00:00 2001 From: userquin Date: Fri, 6 Feb 2026 03:16:13 +0100 Subject: [PATCH 2/7] chore: fix error for lunaria script --- lunaria/prepare-json-files.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/lunaria/prepare-json-files.ts b/lunaria/prepare-json-files.ts index 6eaa7cf1d..79faecafa 100644 --- a/lunaria/prepare-json-files.ts +++ b/lunaria/prepare-json-files.ts @@ -37,13 +37,19 @@ export async function prepareJsonFiles() { await Promise.all(currentLocales.map(l => mergeLocale(l))) } -export async function mergeLocaleObject(locale: LocaleObject) { +export async function mergeLocaleObject(locale: LocaleObject, copy = false) { const files = locale.files ?? [] if (locale.file || files.length === 1) { - const json = locale.file ?? (files[0] ? getFileName(files[0]) : undefined) + const json = + (locale.file ? getFileName(locale.file) : undefined) ?? + (files[0] ? getFileName(files[0]) : undefined) if (!json) return - await fs.cp(path.resolve(`${localesFolder}/${json}`), path.resolve(`${destFolder}/${json}`)) - return + if (copy) { + await fs.cp(path.resolve(`${localesFolder}/${json}`), path.resolve(`${destFolder}/${json}`)) + return + } else { + return await loadJsonFile(json) + } } const firstFile = files[0] @@ -69,9 +75,14 @@ function getFileName(file: string | { path: string }): string { } async function mergeLocale(locale: LocaleObject) { - const source = await mergeLocaleObject(locale) + const source = await mergeLocaleObject(locale, true) + if (!source) { + return + } + await fs.writeFile( path.resolve(`${destFolder}/${locale.code}.json`), JSON.stringify(source, null, 2), + 'utf-8', ) } From c4e1ec840985146d9469fe8735f2451600cf1f93 Mon Sep 17 00:00:00 2001 From: userquin Date: Fri, 6 Feb 2026 03:17:16 +0100 Subject: [PATCH 3/7] chore: apply coderabbitai suggestions --- scripts/compare-translations.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/scripts/compare-translations.ts b/scripts/compare-translations.ts index ead7f7c57..3c98dc6f6 100644 --- a/scripts/compare-translations.ts +++ b/scripts/compare-translations.ts @@ -107,7 +107,19 @@ const loadJson = async ({ filePath, mergeLocale, locale }: LocaleInfo): Promise< return JSON.parse(readFileSync(filePath, 'utf-8')) as NestedObject } - return await mergeLocaleObject(currentLocales.find(l => l.code === locale)!) + const localeObject = currentLocales.find(l => l.code === locale) + if (!localeObject) { + console.error( + `${COLORS.red}Error: Locale "${locale}" not found in currentLocales${COLORS.reset}`, + ) + process.exit(1) + } + const merged = await mergeLocaleObject(localeObject) + if (!merged) { + console.error(`${COLORS.red}Error: Failed to merge locale "${locale}"${COLORS.reset}`) + process.exit(1) + } + return merged as NestedObject } type SyncStats = { @@ -328,6 +340,7 @@ const runAllLocales = async (referenceContent: NestedObject, fix = false): Promi } const run = async (): Promise => { + populateLocaleCountries() const referenceFilePath = join(LOCALES_DIRECTORY, REFERENCE_FILE_NAME) const referenceContent = await loadJson({ filePath: referenceFilePath, @@ -339,8 +352,6 @@ const run = async (): Promise => { const fix = args.includes('--fix') const targetLocale = args.find(arg => !arg.startsWith('--')) - populateLocaleCountries() - if (targetLocale) { // Single locale mode await runSingleLocale(targetLocale, referenceContent, fix) From d91de6e9390c3ef448db01f40d549b6f642db597 Mon Sep 17 00:00:00 2001 From: userquin Date: Fri, 6 Feb 2026 03:21:39 +0100 Subject: [PATCH 4/7] chore: apply typo for coderabbitai suggestion --- scripts/compare-translations.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/compare-translations.ts b/scripts/compare-translations.ts index 3c98dc6f6..b940cb62e 100644 --- a/scripts/compare-translations.ts +++ b/scripts/compare-translations.ts @@ -28,7 +28,7 @@ interface LocaleInfo { mergeLocale?: boolean } -const contries = new Map>() +const countries = new Map>() const extractLocalInfo = ( filePath: string, @@ -44,14 +44,14 @@ const populateLocaleCountries = (): void => { for (const lang of Object.keys(countryLocaleVariants)) { const variants = countryLocaleVariants[lang] for (const variant of variants) { - if (!contries.has(lang)) { - contries.set(lang, new Map()) + if (!countries.has(lang)) { + countries.set(lang, new Map()) } if (variant.country) { - contries.get(lang)!.set(lang, extractLocalInfo(lang, true)) - contries.get(lang)!.set(variant.code, extractLocalInfo(variant.code, true, true)) + countries.get(lang)!.set(lang, extractLocalInfo(lang, true)) + countries.get(lang)!.set(variant.code, extractLocalInfo(variant.code, true, true)) } else { - contries.get(lang)!.set(variant.code, extractLocalInfo(variant.code, false, true)) + countries.get(lang)!.set(variant.code, extractLocalInfo(variant.code, false, true)) } } } @@ -65,7 +65,7 @@ const populateLocaleCountries = (): void => { */ const checkCountryVariant = (localeInfo: LocaleInfo): void => { const { locale, lang, country } = localeInfo - const countryVariant = contries.get(lang) + const countryVariant = countries.get(lang) if (countryVariant) { if (country) { const found = countryVariant.get(locale) From 120d983d60cc5e3508632a1bd264dee3cf1c7a45 Mon Sep 17 00:00:00 2001 From: userquin Date: Fri, 6 Feb 2026 03:21:53 +0100 Subject: [PATCH 5/7] chore: cleanup --- lunaria/prepare-json-files.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lunaria/prepare-json-files.ts b/lunaria/prepare-json-files.ts index 79faecafa..aacb113c2 100644 --- a/lunaria/prepare-json-files.ts +++ b/lunaria/prepare-json-files.ts @@ -47,9 +47,9 @@ export async function mergeLocaleObject(locale: LocaleObject, copy = false) { if (copy) { await fs.cp(path.resolve(`${localesFolder}/${json}`), path.resolve(`${destFolder}/${json}`)) return - } else { - return await loadJsonFile(json) } + + return await loadJsonFile(json) } const firstFile = files[0] From cbb56e6e99e11ce57687d8c63a911e4a693ae166 Mon Sep 17 00:00:00 2001 From: userquin Date: Fri, 6 Feb 2026 03:40:36 +0100 Subject: [PATCH 6/7] perf: create current locales map --- scripts/compare-translations.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/compare-translations.ts b/scripts/compare-translations.ts index b940cb62e..193459405 100644 --- a/scripts/compare-translations.ts +++ b/scripts/compare-translations.ts @@ -1,4 +1,5 @@ /* eslint-disable no-console */ +import type { LocaleObject } from '@nuxtjs/i18n' import * as process from 'node:process' import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs' import { basename, join } from 'node:path' @@ -29,6 +30,7 @@ interface LocaleInfo { } const countries = new Map>() +const availableLocales = new Map() const extractLocalInfo = ( filePath: string, @@ -55,6 +57,10 @@ const populateLocaleCountries = (): void => { } } } + + for (const localeData of currentLocales) { + availableLocales.set(localeData.code, localeData) + } } /** @@ -107,7 +113,7 @@ const loadJson = async ({ filePath, mergeLocale, locale }: LocaleInfo): Promise< return JSON.parse(readFileSync(filePath, 'utf-8')) as NestedObject } - const localeObject = currentLocales.find(l => l.code === locale) + const localeObject = availableLocales.get(locale) if (!localeObject) { console.error( `${COLORS.red}Error: Locale "${locale}" not found in currentLocales${COLORS.reset}`, From 299dd459f541144bbace8be96f8e0c909a265215 Mon Sep 17 00:00:00 2001 From: userquin Date: Fri, 6 Feb 2026 04:24:33 +0100 Subject: [PATCH 7/7] chore: add explicit return types --- lunaria/prepare-json-files.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lunaria/prepare-json-files.ts b/lunaria/prepare-json-files.ts index aacb113c2..64e19d5f5 100644 --- a/lunaria/prepare-json-files.ts +++ b/lunaria/prepare-json-files.ts @@ -31,13 +31,16 @@ export const locales: [{ label: string; lang: string }, ...{ label: string; lang })), ] -export async function prepareJsonFiles() { +export async function prepareJsonFiles(): Promise { await fs.rm(destFolder, { recursive: true, force: true }) await fs.mkdir(destFolder) await Promise.all(currentLocales.map(l => mergeLocale(l))) } -export async function mergeLocaleObject(locale: LocaleObject, copy = false) { +export async function mergeLocaleObject( + locale: LocaleObject, + copy = false, +): Promise { const files = locale.files ?? [] if (locale.file || files.length === 1) { const json = @@ -66,7 +69,7 @@ export async function mergeLocaleObject(locale: LocaleObject, copy = false) { return source } -async function loadJsonFile(name: string) { +async function loadJsonFile(name: string): Promise { return JSON.parse(await fs.readFile(path.resolve(`${localesFolder}/${name}`), 'utf8')) } @@ -74,7 +77,7 @@ function getFileName(file: string | { path: string }): string { return typeof file === 'string' ? file : file.path } -async function mergeLocale(locale: LocaleObject) { +async function mergeLocale(locale: LocaleObject): Promise { const source = await mergeLocaleObject(locale, true) if (!source) { return