Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 32 additions & 13 deletions lunaria/prepare-json-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,27 +31,28 @@ 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
}

async function mergeLocale(locale: LocaleObject) {
export async function mergeLocaleObject(
locale: LocaleObject,
copy = false,
): Promise<void | unknown> {
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
}

return await loadJsonFile(json)
}

const firstFile = files[0]
Expand All @@ -65,8 +66,26 @@ async function mergeLocale(locale: LocaleObject) {
deepCopy(currentSource, source)
}

return source
}

async function loadJsonFile(name: string): Promise<unknown> {
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, true)
if (!source) {
return
}

await fs.writeFile(
path.resolve(`${destFolder}/${locale.code}.json`),
JSON.stringify(source, null, 2),
'utf-8',
)
}
150 changes: 134 additions & 16 deletions scripts/compare-translations.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,123 @@
/* 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>()

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 (!countries.has(lang)) {
countries.set(lang, new Map())
}
if (variant.country) {
countries.get(lang)!.set(lang, extractLocalInfo(lang, true))
countries.get(lang)!.set(variant.code, extractLocalInfo(variant.code, true, true))
} else {
countries.get(lang)!.set(variant.code, extractLocalInfo(variant.code, false, 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)
}
}
}

const loadJson = (filePath: string): NestedObject => {
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 as NestedObject
}

type SyncStats = {
Expand Down Expand Up @@ -43,7 +145,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)

Expand Down Expand Up @@ -83,13 +191,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: [],
Expand All @@ -107,7 +216,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)

Expand All @@ -116,7 +229,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}`,
Expand Down Expand Up @@ -144,7 +257,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,
)
Expand All @@ -156,7 +269,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,
Expand Down Expand Up @@ -224,20 +337,25 @@ 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)
}
}

Expand Down
Loading