Skip to content

Commit d731543

Browse files
userquindanielroe
andauthored
chore: update compare-translations.ts logic (#1063)
Co-authored-by: Daniel Roe <daniel@roe.dev>
1 parent f4e7f6f commit d731543

File tree

2 files changed

+189
-32
lines changed

2 files changed

+189
-32
lines changed

lunaria/prepare-json-files.ts

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,32 +31,36 @@ export const locales: [{ label: string; lang: string }, ...{ label: string; lang
3131
})),
3232
]
3333

34-
export async function prepareJsonFiles() {
34+
export async function prepareJsonFiles(): Promise<void> {
3535
await fs.rm(destFolder, { recursive: true, force: true })
3636
await fs.mkdir(destFolder)
3737
await Promise.all(currentLocales.map(l => mergeLocale(l)))
3838
}
3939

40-
async function loadJsonFile(name: string) {
41-
return JSON.parse(await fs.readFile(path.resolve(`${localesFolder}/${name}`), 'utf8'))
42-
}
43-
44-
function getFileName(file: string | { path: string }): string {
45-
return typeof file === 'string' ? file : file.path
46-
}
40+
type NestedObject = Record<string, unknown>
4741

48-
async function mergeLocale(locale: LocaleObject) {
42+
export async function mergeLocaleObject(
43+
locale: LocaleObject,
44+
options: { copy?: boolean } = {},
45+
): Promise<NestedObject | undefined> {
46+
const { copy = false } = options
4947
const files = locale.files ?? []
5048
if (locale.file || files.length === 1) {
51-
const json = locale.file ?? (files[0] ? getFileName(files[0]) : undefined)
52-
if (!json) return
53-
await fs.cp(path.resolve(`${localesFolder}/${json}`), path.resolve(`${destFolder}/${json}`))
54-
return
49+
const json =
50+
(locale.file ? getFileName(locale.file) : undefined) ??
51+
(files[0] ? getFileName(files[0]) : undefined)
52+
if (!json) return undefined
53+
if (copy) {
54+
await fs.cp(path.resolve(`${localesFolder}/${json}`), path.resolve(`${destFolder}/${json}`))
55+
return undefined
56+
}
57+
58+
return await loadJsonFile<NestedObject>(json)
5559
}
5660

5761
const firstFile = files[0]
58-
if (!firstFile) return
59-
const source = await loadJsonFile(getFileName(firstFile))
62+
if (!firstFile) return undefined
63+
const source = await loadJsonFile<NestedObject>(getFileName(firstFile))
6064
let currentSource: unknown
6165
for (let i = 1; i < files.length; i++) {
6266
const file = files[i]
@@ -65,8 +69,26 @@ async function mergeLocale(locale: LocaleObject) {
6569
deepCopy(currentSource, source)
6670
}
6771

72+
return source
73+
}
74+
75+
async function loadJsonFile<T = unknown>(name: string): Promise<T> {
76+
return JSON.parse(await fs.readFile(path.resolve(`${localesFolder}/${name}`), 'utf8'))
77+
}
78+
79+
function getFileName(file: string | { path: string }): string {
80+
return typeof file === 'string' ? file : file.path
81+
}
82+
83+
async function mergeLocale(locale: LocaleObject): Promise<void> {
84+
const source = await mergeLocaleObject(locale, { copy: true })
85+
if (!source) {
86+
return
87+
}
88+
6889
await fs.writeFile(
6990
path.resolve(`${destFolder}/${locale.code}.json`),
7091
JSON.stringify(source, null, 2),
92+
'utf-8',
7193
)
7294
}

scripts/compare-translations.ts

Lines changed: 152 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,139 @@
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'
34
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
4-
import { join } from 'node:path'
5+
import { basename, join } from 'node:path'
56
import { fileURLToPath } from 'node:url'
7+
import { countryLocaleVariants, currentLocales } from '../config/i18n.ts'
8+
import { mergeLocaleObject } from '../lunaria/prepare-json-files.ts'
69
import { COLORS } from './utils.ts'
710

811
const LOCALES_DIRECTORY = fileURLToPath(new URL('../i18n/locales', import.meta.url))
912
const REFERENCE_FILE_NAME = 'en.json'
1013

1114
type 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

21139
type 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

Comments
 (0)