import { readFileSync, readdirSync, writeFileSync } from "node:fs"; import { join, resolve } from "node:path"; import { normalizePath } from "vite"; import type { Plugin, ResolvedConfig } from "vite"; interface Options { sourceDir: string; destination: string; } type JsonValue = string | number | boolean | null | JsonObject | JsonValue[]; type JsonObject = { [key: string]: JsonValue }; function flattenKeys(obj: JsonObject, prefix: string, result: Map>): void { for (const [key, value] of Object.entries(obj)) { const fullKey = prefix ? `${prefix}.${key}` : key; if (value !== null && typeof value === "object" && !Array.isArray(value)) { flattenKeys(value as JsonObject, fullKey, result); } else if (typeof value === "string") { if (!result.has(fullKey)) { result.set(fullKey, new Set()); } result.get(fullKey)!.add(value); } } } function generateTypes(sourceDir: string, destination: string): void { const files = readdirSync(sourceDir).filter((f) => f.endsWith(".json")); const keyValues = new Map>(); for (const file of files) { const content = JSON.parse(readFileSync(join(sourceDir, file), "utf-8")) as JsonObject; flattenKeys(content, "", keyValues); } const sortedKeys = Array.from(keyValues.keys()).sort(); const lines: string[] = [ "// Auto-generated by i18nextTypesPlugin — do not edit manually", "declare const resources: {", ]; for (const key of sortedKeys) { const values = Array.from(keyValues.get(key)!); const union = values.map((v) => JSON.stringify(v)).join(" | "); lines.push(` ${JSON.stringify(key)}: ${union};`); } lines.push("};"); lines.push("export default resources;"); lines.push(""); writeFileSync(destination, lines.join("\n"), "utf-8"); } export function i18nextTypesPlugin(options: Options): Plugin { let resolvedSourceDir: string; let resolvedDestination: string; return { name: "i18next-types", configResolved(config: ResolvedConfig) { resolvedSourceDir = normalizePath(resolve(config.root, options.sourceDir)); resolvedDestination = normalizePath(resolve(config.root, options.destination)); }, buildStart() { generateTypes(resolvedSourceDir, resolvedDestination); }, configureServer(server) { server.watcher.add(resolvedSourceDir); server.watcher.on("change", (file: string) => { if (file.startsWith(resolvedSourceDir) && file.endsWith(".json")) { generateTypes(resolvedSourceDir, resolvedDestination); } }); server.watcher.on("add", (file: string) => { if (file.startsWith(resolvedSourceDir) && file.endsWith(".json")) { generateTypes(resolvedSourceDir, resolvedDestination); } }); server.watcher.on("unlink", (file: string) => { if (file.startsWith(resolvedSourceDir) && file.endsWith(".json")) { generateTypes(resolvedSourceDir, resolvedDestination); } }); }, }; }