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; } type JsonValue = string | number | boolean | null | JsonObject | JsonValue[]; type JsonObject = { [key: string]: JsonValue }; function deepSortKeys(obj: JsonObject): JsonObject { const sorted: JsonObject = {}; for (const key of Object.keys(obj).sort()) { const value = obj[key]; if (value !== null && typeof value === "object" && !Array.isArray(value)) { sorted[key] = deepSortKeys(value as JsonObject); } else { sorted[key] = value; } } return sorted; } function sortFile(filePath: string): void { const raw = readFileSync(filePath, "utf-8"); const parsed = JSON.parse(raw) as JsonObject; const sorted = deepSortKeys(parsed); const output = JSON.stringify(sorted, null, "\t") + "\n"; if (raw !== output) { writeFileSync(filePath, output, "utf-8"); } } function sortAllFiles(sourceDir: string): void { const files = readdirSync(sourceDir).filter((f) => f.endsWith(".json")); for (const file of files) { sortFile(join(sourceDir, file)); } } export function i18nextSortPlugin(options: Options): Plugin { let resolvedSourceDir: string; return { name: "i18next-sort", configResolved(config: ResolvedConfig) { resolvedSourceDir = normalizePath(resolve(config.root, options.sourceDir)); }, buildStart() { sortAllFiles(resolvedSourceDir); }, configureServer(server) { server.watcher.add(resolvedSourceDir); server.watcher.on("change", (file: string) => { if (file.startsWith(resolvedSourceDir) && file.endsWith(".json")) { sortFile(file); } }); }, }; }