feat: add settings menu
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m40s

This commit is contained in:
Hewston Fox
2026-03-22 04:08:56 +02:00
parent 2a1115b66f
commit 5e9acffa09
89 changed files with 3412 additions and 216 deletions

View File

@@ -0,0 +1,68 @@
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);
}
});
},
};
}

View File

@@ -0,0 +1,97 @@
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<string, Set<string>>): 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<string, Set<string>>();
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);
}
});
},
};
}