import fs from 'node:fs' import path from 'node:path' import { parse } from 'yaml' import type { Plugin } from 'vite' const I18N_SOURCE_DIR = 'public/i18n' const SOURCE_RELATIVE_PATH = `${I18N_SOURCE_DIR}/en.yml` const UNIVERSAL_OUTPUT_RELATIVE_PATH = 'src/universal/types/i18n.d.ts' const RENDERER_OUTPUT_RELATIVE_PATH = 'src/renderer/i18n/i18next.d.ts' function formatKey (key: string) { return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key) } function readTranslationKeys (sourcePath: string) { if (!fs.existsSync(sourcePath)) { return [] } const raw = fs.readFileSync(sourcePath, 'utf8') const data = parse(raw) if (!data || typeof data !== 'object' || Array.isArray(data)) { return [] } return Object.keys(data as Record) } function buildUniversalTypeContent (keys: string[]) { const uniqueKeys = Array.from(new Set(keys)) const lines = uniqueKeys.map((key) => ` ${formatKey(key)}: string`) return `// This file is auto-generated by vite-plugin-i18n-types. Do not edit. interface ILocales { ${lines.join('\n')} } type ILocalesKey = keyof ILocales ` } function buildRendererTypeContent () { return `// This file is auto-generated by vite-plugin-i18n-types. Do not edit. import "i18next" declare module "i18next" { interface CustomTypeOptions { resources: { translation: ILocales } } } ` } function writeFileIfChanged (outputPath: string, nextContent: string) { const previousContent = fs.existsSync(outputPath) ? fs.readFileSync(outputPath, 'utf8') : '' if (previousContent === nextContent) { return } fs.mkdirSync(path.dirname(outputPath), { recursive: true }) fs.writeFileSync(outputPath, nextContent, 'utf8') } function writeTypesFiles (sourcePath: string, universalOutputPath: string, rendererOutputPath: string) { const keys = readTranslationKeys(sourcePath) writeFileIfChanged(universalOutputPath, buildUniversalTypeContent(keys)) writeFileIfChanged(rendererOutputPath, buildRendererTypeContent()) } export function i18nTypesPlugin (): Plugin { let sourcePath = '' let universalOutputPath = '' let rendererOutputPath = '' let projectRoot = '' const generate = () => { try { writeTypesFiles(sourcePath, universalOutputPath, rendererOutputPath) } catch (error) { const message = error instanceof Error ? error.message : String(error) console.error(`[vite-plugin-i18n-types] ${message}`) } } return { name: 'vite-plugin-i18n-types', enforce: 'pre', configResolved (config) { projectRoot = path.resolve(config.root, '../..') sourcePath = path.resolve(projectRoot, SOURCE_RELATIVE_PATH) universalOutputPath = path.resolve(projectRoot, UNIVERSAL_OUTPUT_RELATIVE_PATH) rendererOutputPath = path.resolve(projectRoot, RENDERER_OUTPUT_RELATIVE_PATH) }, buildStart () { generate() }, configureServer (server) { server.watcher.add(path.resolve(projectRoot, I18N_SOURCE_DIR)) }, handleHotUpdate ({ file }) { const normalizedFile = path.resolve(file) const normalizedSourceDir = path.resolve(path.dirname(sourcePath)) if (normalizedFile.startsWith(normalizedSourceDir)) { generate() } } } }