#!/usr/bin/env node import { promises as fs } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const ROOT_DIR = path.resolve(__dirname, '..') const LOCALE_DIR = path.resolve(ROOT_DIR, 'src/locales/en') const KEY_OUTPUT = path.resolve(ROOT_DIR, 'src/types/generated/i18n-keys.ts') const RESOURCE_OUTPUT = path.resolve( ROOT_DIR, 'src/types/generated/i18n-resources.ts', ) const GENERATED_HEADER_LINES = [ '// This file is auto-generated by scripts/generate-i18n-keys.mjs', '// Do not edit this file manually.', ] const IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/ const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value) const getIndent = (size) => ' '.repeat(size) const formatStringLiteral = (value) => `'${JSON.stringify(value).slice(1, -1).replaceAll("'", "\\'")}'` const formatPropertyKey = (key) => IDENTIFIER_PATTERN.test(key) ? key : formatStringLiteral(key) const buildGeneratedFile = (bodyLines) => [...GENERATED_HEADER_LINES, '', ...bodyLines, ''].join('\n') const flattenKeys = (data, prefix = '') => { const keys = [] for (const [key, value] of Object.entries(data)) { const nextPrefix = prefix ? `${prefix}.${key}` : key if (isPlainObject(value)) { keys.push(...flattenKeys(value, nextPrefix)) } else { keys.push(nextPrefix) } } return keys } const buildType = (data, indent = 0) => { if (!isPlainObject(data)) { return 'string' } const entries = Object.entries(data).sort(([a], [b]) => a.localeCompare(b)) const pad = getIndent(indent) const inner = entries .map(([key, value]) => { const typeStr = buildType(value, indent + 2) return `${getIndent(indent + 2)}${formatPropertyKey(key)}: ${typeStr}` }) .join('\n') return entries.length ? `{ ${inner} ${pad}}` : '{}' } const loadNamespaceJson = async () => { const dirents = await fs.readdir(LOCALE_DIR, { withFileTypes: true }) const namespaces = [] for (const dirent of dirents) { if (!dirent.isFile() || !dirent.name.endsWith('.json')) continue const name = dirent.name.replace(/\.json$/, '') const filePath = path.join(LOCALE_DIR, dirent.name) const raw = await fs.readFile(filePath, 'utf8') const json = JSON.parse(raw) namespaces.push({ name, json }) } namespaces.sort((a, b) => a.name.localeCompare(b.name)) return namespaces } const buildKeysFile = (keys) => { const keyLines = keys.map( (key) => `${getIndent(2)}${formatStringLiteral(key)},`, ) return buildGeneratedFile([ 'export const translationKeys = [', ...keyLines, '] as const', '', 'export type TranslationKey = (typeof translationKeys)[number]', ]) } const buildResourcesFile = (namespaces) => { const namespaceLines = namespaces.map(({ name, json }) => { const typeStr = buildType(json, 4) return `${getIndent(4)}${formatPropertyKey(name)}: ${typeStr}` }) return buildGeneratedFile([ 'export interface TranslationResources {', ' translation: {', ...namespaceLines, ' }', '}', ]) } const main = async () => { const namespaces = await loadNamespaceJson() const keys = namespaces.flatMap(({ name, json }) => flattenKeys(json, name)) const keysContent = buildKeysFile(keys) const resourcesContent = buildResourcesFile(namespaces) await fs.mkdir(path.dirname(KEY_OUTPUT), { recursive: true }) await fs.writeFile(KEY_OUTPUT, keysContent, 'utf8') await fs.writeFile(RESOURCE_OUTPUT, resourcesContent, 'utf8') console.log(`Generated ${keys.length} translation keys.`) } main().catch((error) => { console.error('Failed to generate i18n metadata:', error) process.exitCode = 1 })