/** * This file contains a script that can be used to update the following files: * * - `src/locale/.ts` * - `src/locales//index.ts` * - `src/locales///index.ts` * - `src/docs/guide/localization.md` * * If you wish to edit all/specific locale data files you can do so using the * `updateLocaleFileHook()` method. * Please remember to not commit your temporary update code. * * Run this script using `pnpm run generate:locales` */ import { existsSync, lstatSync, readdirSync, readFileSync, writeFileSync, } from 'node:fs'; import { resolve } from 'node:path'; import type { Options } from 'prettier'; import { format } from 'prettier'; import options from '../.prettierrc.cjs'; import type { LocaleDefinition, MetadataDefinition } from '../src/definitions'; // Constants const pathRoot = resolve(__dirname, '..'); const pathLocale = resolve(pathRoot, 'src', 'locale'); const pathLocales = resolve(pathRoot, 'src', 'locales'); const pathLocaleIndex = resolve(pathLocale, 'index.ts'); const pathLocalesIndex = resolve(pathLocales, 'index.ts'); const pathDocsGuideLocalization = resolve( pathRoot, 'docs', 'guide', 'localization.md' ); // Workaround for nameOf type PascalCase = S extends `${infer P1}_${infer P2}` ? `${Capitalize}${PascalCase}` : Capitalize; type DefinitionType = { [key in keyof LocaleDefinition]-?: PascalCase<`${key}Definition`>; }; /** * The types of the definitions. */ const definitionsTypes: DefinitionType = { airline: 'AirlineDefinition', animal: 'AnimalDefinition', color: 'ColorDefinition', commerce: 'CommerceDefinition', company: 'CompanyDefinition', database: 'DatabaseDefinition', date: 'DateDefinition', finance: 'FinanceDefinition', hacker: 'HackerDefinition', internet: 'InternetDefinition', location: 'LocationDefinition', lorem: 'LoremDefinition', metadata: 'MetadataDefinition', music: 'MusicDefinition', person: 'PersonDefinition', phone_number: 'PhoneNumberDefinition', science: 'ScienceDefinition', system: 'SystemDefinition', vehicle: 'VehicleDefinition', word: 'WordDefinition', }; const prettierTsOptions: Options = { ...options, parser: 'typescript' }; const prettierMdOptions: Options = { ...options, parser: 'markdown' }; const scriptCommand = 'pnpm run generate:locales'; const autoGeneratedCommentHeader = `/* * This file is automatically generated. * Run '${scriptCommand}' to update. */`; // Helper functions function removeIndexTs(files: string[]): string[] { const index = files.indexOf('index.ts'); if (index !== -1) { files.splice(index, 1); } return files; } function removeTsSuffix(files: string[]): string[] { return files.map((file) => file.replace('.ts', '')); } function escapeImport(parent: string, module: string): string { if (['name', 'type', 'switch', parent].includes(module)) { return `${module}_`; } return module; } function escapeField(parent: string, module: string): string { if (['name', 'type', 'switch', parent].includes(module)) { return `${module}: ${module}_`; } return module; } function generateLocaleFile(locale: string): void { const parts = locale.split('_'); const locales = [locale]; for (let i = parts.length - 1; i > 0; i--) { const fallback = parts.slice(0, i).join('_'); if (existsSync(resolve(pathLocales, fallback))) { locales.push(fallback); } } // TODO @Shinigami92 2023-03-07: Remove 'en' fallback in a separate PR if (locales[locales.length - 1] !== 'en' && locale !== 'base') { locales.push('en'); } if (locales[locales.length - 1] !== 'base') { locales.push('base'); } let content = ` ${autoGeneratedCommentHeader} import { Faker } from '../faker'; ${locales .map((imp) => `import ${imp} from '../locales/${imp}';`) .join('\n')} export const faker = new Faker({ locale: ${ locales.length === 1 ? locales[0] : `[${locales.join(', ')}]` }, }); `; content = format(content, prettierTsOptions); writeFileSync(resolve(pathLocale, `${locale}.ts`), content); } function generateLocalesIndexFile( path: string, name: string, type: string, depth: number ): void { let modules = readdirSync(path); modules = removeIndexTs(modules); modules = removeTsSuffix(modules); modules.sort(); const content = [autoGeneratedCommentHeader]; let fieldType = ''; if (type !== 'any') { fieldType = `: ${type}`; content.push( `import type { ${type.replace(/\[.*/, '')} } from '..${'/..'.repeat( depth )}';` ); } content.push( ...modules.map( (module) => `import ${escapeImport(name, module)} from './${module}';` ) ); content.push(`\nconst ${name}${fieldType} = { ${modules.map((module) => `${escapeField(name, module)},`).join('\n')} };\n`); content.push(`export default ${name};`); writeFileSync( resolve(path, 'index.ts'), format(content.join('\n'), prettierTsOptions) ); } function generateRecursiveModuleIndexes( path: string, name: string, definition: string, depth: number ): void { generateLocalesIndexFile(path, name, definition, depth); let submodules = readdirSync(path); submodules = removeIndexTs(submodules); for (const submodule of submodules) { const pathModule = resolve(path, submodule); updateLocaleFile(pathModule); // Only process sub folders recursively if (lstatSync(pathModule).isDirectory()) { let moduleDefinition = definition === 'any' ? 'any' : `${definition}['${submodule}']`; // Overwrite types of src/locales///index.ts for known definition types if (depth === 1) { moduleDefinition = definitionsTypes[submodule] ?? 'any'; } // Recursive generateRecursiveModuleIndexes( pathModule, submodule, moduleDefinition, depth + 1 ); } } } /** * Intermediate helper function to allow selectively updating locale data files. * Use the `updateLocaleFileHook()` method to temporarily add your custom per file processing/update logic. * * @param filePath The full file path to the file. */ function updateLocaleFile(filePath: string): void { if (lstatSync(filePath).isFile()) { const pathParts = filePath .substring(pathLocales.length + 1, filePath.length - 3) .split(/[\\\/]/); const locale = pathParts[0]; pathParts.splice(0, 1); updateLocaleFileHook(filePath, locale, pathParts); } } /** * Use this hook method to selectively update locale data files (not for index.ts files). * This method is intended to be temporarily overwritten for one-time updates. * * @param filePath The full file path to the file. * @param locale The locale for that file. * @param localePath The locale path parts (after the locale). */ function updateLocaleFileHook( filePath: string, locale: string, localePath: string[] ): void { if (filePath === 'never') { console.log(`${filePath} <-> ${locale} @ ${localePath.join(' -> ')}`); } } // Start of actual logic const locales = readdirSync(pathLocales); removeIndexTs(locales); let localeIndexImports = ''; let localeIndexExportsIndividual = ''; let localeIndexExportsGrouped = ''; let localesIndexExports = ''; let localizationLocales = '| Locale | Name | Faker |\n| :--- | :--- | :--- |\n'; for (const locale of locales) { const pathModules = resolve(pathLocales, locale); const pathMetadata = resolve(pathModules, 'metadata.ts'); let localeTitle = 'No title found'; try { // eslint-disable-next-line @typescript-eslint/no-var-requires const metadata: MetadataDefinition = require(pathMetadata).default; const { title } = metadata; if (!title) { throw new Error(`No title property found on ${JSON.stringify(metadata)}`); } localeTitle = title; } catch (e) { console.error( `Failed to load ${pathMetadata}. Please make sure the file exists and exports a MetadataDefinition.` ); console.error(e); } const localizedFaker = `faker${locale.replace(/^([a-z]+)/, (part) => part.toUpperCase() )}`; localeIndexImports += `import { faker as ${localizedFaker} } from './${locale}';\n`; localeIndexExportsIndividual += ` ${localizedFaker},\n`; localeIndexExportsGrouped += ` ${locale}: ${localizedFaker},\n`; localesIndexExports += `export { default as ${locale} } from './${locale}';\n`; localizationLocales += `| \`${locale}\` | ${localeTitle} | \`${localizedFaker}\` |\n`; // src/locale/.ts generateLocaleFile(locale); // src/locales/**/index.ts generateRecursiveModuleIndexes(pathModules, locale, 'LocaleDefinition', 1); } // src/locale/index.ts let localeIndexContent = ` ${autoGeneratedCommentHeader} ${localeIndexImports} export { ${localeIndexExportsIndividual} }; export const allFakers = { ${localeIndexExportsGrouped} } as const; `; localeIndexContent = format(localeIndexContent, prettierTsOptions); writeFileSync(pathLocaleIndex, localeIndexContent); // src/locales/index.ts let localesIndexContent = ` ${autoGeneratedCommentHeader} ${localesIndexExports} `; localesIndexContent = format(localesIndexContent, prettierTsOptions); writeFileSync(pathLocalesIndex, localesIndexContent); // docs/guide/localization.md localizationLocales = format(localizationLocales, prettierMdOptions); let localizationContent = readFileSync(pathDocsGuideLocalization, 'utf-8'); localizationContent = localizationContent.replace( /(^$).*(^$)/gms, `$1\n\n\n\n${localizationLocales}\n$2` ); writeFileSync(pathDocsGuideLocalization, localizationContent);