// cSpell:ignore Tolgee import * as fs from 'node:fs/promises'; import * as path from 'node:path'; import { format } from 'prettier'; import { getAllProjectLanguages, getRemoteTranslations } from './api.js'; import type { TranslationRes } from './utils.js'; import { flattenTranslation } from './utils.js'; const INDENT = 2; const RES_DIR = path.resolve(process.cwd(), 'src', 'resources'); const countKeys = (obj: TranslationRes | null) => { if (!obj) { return 0; } let count = 0; // eslint-disable-next-line @typescript-eslint/no-unused-vars Object.entries(obj).forEach(([_, value]) => { if (typeof value === 'string') { count++; } else { count += countKeys(value); } }); return count; }; const getBaseTranslations = async (baseLanguage: { tag: string }) => { try { const baseTranslationsStr = await fs.readFile( path.resolve(RES_DIR, `${baseLanguage.tag}.json`), { encoding: 'utf8' } ); const baseTranslations = JSON.parse(baseTranslationsStr); return baseTranslations; } catch (e) { console.error('base language:', JSON.stringify(baseLanguage)); console.error('Failed to read base language', e); const translations = await getRemoteTranslations(baseLanguage.tag); await fs.writeFile( path.resolve(RES_DIR, `${baseLanguage.tag}.json`), JSON.stringify(translations, null, 4) ); } }; const main = async () => { try { await fs.access(RES_DIR); } catch (error) { fs.mkdir(RES_DIR).catch(console.error); console.log('Create directory', RES_DIR); } console.log('Loading project languages...'); const languages = await getAllProjectLanguages(); const baseLanguage = languages.find(language => language.base); if (!baseLanguage) { console.error(JSON.stringify(languages)); throw new Error('Could not find base language'); } console.log(`Loading ${baseLanguage.tag} languages translations as base...`); const baseTranslations = await getBaseTranslations(baseLanguage); const baseKeyNum = countKeys(baseTranslations); const languagesWithTranslations = await Promise.all( languages.map(async language => { console.log(`Loading ${language.tag} translations...`); const translations = await getRemoteTranslations(language.tag); const keyNum = countKeys(translations); const completeRate = Number((keyNum / baseKeyNum).toFixed(3)); console.log( `Load ${language.name} ${ completeRate * 100 }, %(${keyNum}/${baseKeyNum}) complete` ); return { ...language, translations, completeRate, }; }) ); const availableLanguages = languagesWithTranslations.filter( language => language.completeRate > 0.2 ); for (const language of availableLanguages // skip base language .filter(i => !i.base)) { await fs.writeFile( path.resolve(RES_DIR, `${language.tag}.json`), JSON.stringify( { '// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.': '', ...flattenTranslation(language.translations), }, null, INDENT ) + '\n' ); } console.log('Generating meta data...'); const code = `// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. // Run \`yarn run download-resources\` to regenerate. // If you need to update the code, please edit \`i18n/src/scripts/download.ts\` inside your project. ${availableLanguages .map( language => `import ${language.tag.replaceAll('-', '_')} from './${ language.tag }.json'` ) .sort() .join('\n')} export const LOCALES = [ ${availableLanguages // eslint-disable-next-line @typescript-eslint/no-unused-vars -- omit key .map(({ translations, ...language }) => JSON.stringify({ ...language, // a trick to generate a string without quotation marks res: '__RES_PLACEHOLDER', }).replace( '"__RES_PLACEHOLDER"', language.tag.replaceAll('-', '_') ) ) .join(',\n')} ] as const; `; await fs.writeFile( path.resolve(RES_DIR, 'index.ts'), await format(code, { parser: 'typescript', singleQuote: true, trailingComma: 'es5', tabWidth: INDENT, arrowParens: 'avoid', }) ); console.log('Done'); }; main().catch(e => { console.error(e); process.exit(1); });