-
-
Notifications
You must be signed in to change notification settings - Fork 220
chore: update compare-translations.ts logic
#1063
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5b68303
e9a8442
c4e1ec8
d91de6e
120d983
cbb56e6
299dd45
53fc33f
88b6713
d767faa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,32 +31,36 @@ export const locales: [{ label: string; lang: string }, ...{ label: string; lang | |
| })), | ||
| ] | ||
|
|
||
| export async function prepareJsonFiles() { | ||
| export async function prepareJsonFiles(): Promise<void> { | ||
| await fs.rm(destFolder, { recursive: true, force: true }) | ||
| await fs.mkdir(destFolder) | ||
| 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 | ||
| } | ||
| type NestedObject = Record<string, unknown> | ||
|
|
||
| async function mergeLocale(locale: LocaleObject) { | ||
| export async function mergeLocaleObject( | ||
| locale: LocaleObject, | ||
| options: { copy?: boolean } = {}, | ||
| ): Promise<NestedObject | undefined> { | ||
| const { copy = false } = options | ||
| const files = locale.files ?? [] | ||
| if (locale.file || files.length === 1) { | ||
| const json = locale.file ?? (files[0] ? getFileName(files[0]) : undefined) | ||
| if (!json) return | ||
| await fs.cp(path.resolve(`${localesFolder}/${json}`), path.resolve(`${destFolder}/${json}`)) | ||
| return | ||
| const json = | ||
| (locale.file ? getFileName(locale.file) : undefined) ?? | ||
| (files[0] ? getFileName(files[0]) : undefined) | ||
| if (!json) return undefined | ||
| if (copy) { | ||
| await fs.cp(path.resolve(`${localesFolder}/${json}`), path.resolve(`${destFolder}/${json}`)) | ||
| return undefined | ||
| } | ||
|
|
||
| return await loadJsonFile<NestedObject>(json) | ||
| } | ||
|
|
||
| const firstFile = files[0] | ||
| if (!firstFile) return | ||
| const source = await loadJsonFile(getFileName(firstFile)) | ||
| if (!firstFile) return undefined | ||
| const source = await loadJsonFile<NestedObject>(getFileName(firstFile)) | ||
| let currentSource: unknown | ||
| for (let i = 1; i < files.length; i++) { | ||
| const file = files[i] | ||
|
|
@@ -65,8 +69,26 @@ async function mergeLocale(locale: LocaleObject) { | |
| deepCopy(currentSource, source) | ||
| } | ||
|
|
||
| return source | ||
| } | ||
|
|
||
| async function loadJsonFile<T = unknown>(name: string): Promise<T> { | ||
| 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): Promise<void> { | ||
| const source = await mergeLocaleObject(locale, { copy: true }) | ||
| if (!source) { | ||
| return | ||
| } | ||
|
|
||
| await fs.writeFile( | ||
| path.resolve(`${destFolder}/${locale.code}.json`), | ||
| JSON.stringify(source, null, 2), | ||
| 'utf-8', | ||
|
Comment on lines
89
to
+92
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preserve trailing newline to avoid dirty 🛠️ Suggested fix- JSON.stringify(source, null, 2),
+ JSON.stringify(source, null, 2) + '\n',
'utf-8', |
||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,21 +1,139 @@ | ||
| /* eslint-disable no-console */ | ||
| import process from 'node:process' | ||
| import type { LocaleObject } from '@nuxtjs/i18n' | ||
| 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' | ||
| import { COLORS } from './utils.ts' | ||
|
|
||
| const LOCALES_DIRECTORY = fileURLToPath(new URL('../i18n/locales', import.meta.url)) | ||
| const REFERENCE_FILE_NAME = 'en.json' | ||
|
|
||
| type NestedObject = { [key: string]: unknown } | ||
| interface LocaleInfo { | ||
| filePath: string | ||
| locale: string | ||
| lang: string | ||
| country?: string | ||
| forCountry?: boolean | ||
| mergeLocale?: boolean | ||
| } | ||
|
|
||
| const countries = new Map<string, Map<string, LocaleInfo>>() | ||
| const availableLocales = new Map<string, LocaleObject>() | ||
|
|
||
| function extractLocalInfo(filePath: string): LocaleInfo { | ||
| const locale = basename(filePath, '.json') | ||
| const [lang, country] = locale.split('-') | ||
| return { filePath, locale, lang, country } | ||
| } | ||
|
|
||
| function createVariantInfo( | ||
| code: string, | ||
| options: { forCountry: boolean; mergeLocale: boolean }, | ||
| ): LocaleInfo { | ||
| const [lang, country] = code.split('-') | ||
| return { filePath: '', locale: code, lang, country, ...options } | ||
| } | ||
|
|
||
| const loadJson = (filePath: string): NestedObject => { | ||
| const populateLocaleCountries = (): void => { | ||
| for (const lang of Object.keys(countryLocaleVariants)) { | ||
| const variants = countryLocaleVariants[lang] | ||
| for (const variant of variants) { | ||
| if (!countries.has(lang)) { | ||
| countries.set(lang, new Map()) | ||
| } | ||
| if (variant.country) { | ||
| countries | ||
| .get(lang)! | ||
| .set(lang, createVariantInfo(lang, { forCountry: true, mergeLocale: false })) | ||
| countries | ||
| .get(lang)! | ||
| .set( | ||
| variant.code, | ||
| createVariantInfo(variant.code, { forCountry: true, mergeLocale: true }), | ||
| ) | ||
| } else { | ||
| countries | ||
| .get(lang)! | ||
| .set( | ||
| variant.code, | ||
| createVariantInfo(variant.code, { forCountry: false, mergeLocale: true }), | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| for (const localeData of currentLocales) { | ||
| availableLocales.set(localeData.code, localeData) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * 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 = countries.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}-<country-name>"${COLORS.reset}`, | ||
| ) | ||
| process.exit(1) | ||
|
Comment on lines
+80
to
+103
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove the stray quote in the error message. 🛠️ Suggested fix- `${COLORS.red}Error: Invalid locale file "${locale}", it should be included at "countryLocaleVariants" in config/i18n.ts"${COLORS.reset}`,
+ `${COLORS.red}Error: Invalid locale file "${locale}", it should be included at "countryLocaleVariants" in config/i18n.ts${COLORS.reset}`, |
||
| } | ||
| } | ||
| } | ||
|
|
||
| const checkJsonName = (filePath: string): LocaleInfo => { | ||
| const info = extractLocalInfo(filePath) | ||
| checkCountryVariant(info) | ||
| return info | ||
| } | ||
|
|
||
| const loadJson = async ({ filePath, mergeLocale, locale }: LocaleInfo): Promise<NestedObject> => { | ||
| 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 | ||
| } | ||
|
|
||
| const localeObject = availableLocales.get(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 | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| type SyncStats = { | ||
|
|
@@ -43,7 +161,14 @@ 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) | ||
| // When fixing, empty objects won't occur since missing keys get placeholders. | ||
| // Without --fix, keep empty objects to preserve structural parity with the reference. | ||
| if (fix && Object.keys(data).length === 0) { | ||
| delete result[key] | ||
| } else { | ||
| result[key] = data | ||
| } | ||
| } else { | ||
| stats.referenceKeys.push(propertyPath) | ||
|
|
||
|
|
@@ -83,13 +208,14 @@ const logSection = ( | |
| keys.forEach(key => console.log(` - ${key}`)) | ||
| } | ||
|
|
||
| const processLocale = ( | ||
| const processLocale = async ( | ||
| localeFile: string, | ||
| referenceContent: NestedObject, | ||
| fix = false, | ||
| ): SyncStats => { | ||
| ): Promise<SyncStats> => { | ||
| const filePath = join(LOCALES_DIRECTORY, localeFile) | ||
| const targetContent = loadJson(filePath) | ||
| const localeInfo = checkJsonName(filePath) | ||
| const targetContent = await loadJson(localeInfo) | ||
|
|
||
| const stats: SyncStats = { | ||
| missing: [], | ||
|
|
@@ -107,7 +233,11 @@ const processLocale = ( | |
| return stats | ||
| } | ||
|
|
||
| const runSingleLocale = (locale: string, referenceContent: NestedObject, fix = false): void => { | ||
| const runSingleLocale = async ( | ||
| locale: string, | ||
| referenceContent: NestedObject, | ||
| fix = false, | ||
| ): Promise<void> => { | ||
| const localeFile = locale.endsWith('.json') ? locale : `${locale}.json` | ||
| const filePath = join(LOCALES_DIRECTORY, localeFile) | ||
|
|
||
|
|
@@ -116,7 +246,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}`, | ||
|
|
@@ -144,7 +274,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<void> => { | ||
| const localeFiles = readdirSync(LOCALES_DIRECTORY).filter( | ||
| file => file.endsWith('.json') && file !== REFERENCE_FILE_NAME, | ||
| ) | ||
|
|
@@ -156,7 +286,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, | ||
|
|
@@ -224,21 +354,26 @@ const runAllLocales = (referenceContent: NestedObject, fix = false): void => { | |
| console.log('') | ||
| } | ||
|
|
||
| const run = (): void => { | ||
| const run = async (): Promise<void> => { | ||
| populateLocaleCountries() | ||
| 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('--')) | ||
|
|
||
| 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) | ||
| } | ||
| } | ||
|
|
||
| run() | ||
| await run() | ||
Uh oh!
There was an error while loading. Please reload this page.