settings-split-guards.test.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. import fs from "node:fs";
  2. import path from "node:path";
  3. import { describe, expect, test } from "vitest";
  4. const LOCALES = ["zh-CN", "zh-TW", "en", "ja", "ru"] as const;
  5. const CANONICAL: (typeof LOCALES)[number] = "zh-CN";
  6. const SETTINGS_LINE_THRESHOLD = 800;
  7. function isObject(v: unknown): v is Record<string, unknown> {
  8. return Boolean(v) && typeof v === "object" && !Array.isArray(v);
  9. }
  10. function typeTag(v: unknown): string {
  11. if (v === null) return "null";
  12. if (Array.isArray(v)) return "array";
  13. return typeof v;
  14. }
  15. function listJsonFilesRecursive(dir: string): string[] {
  16. const out: string[] = [];
  17. const walk = (d: string) => {
  18. for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
  19. const full = path.join(d, entry.name);
  20. if (entry.isDirectory()) walk(full);
  21. else if (entry.isFile() && entry.name.endsWith(".json")) out.push(full);
  22. }
  23. };
  24. walk(dir);
  25. return out.sort();
  26. }
  27. function loadJson(filePath: string): unknown {
  28. return JSON.parse(fs.readFileSync(filePath, "utf8"));
  29. }
  30. function loadSplitSettings(locale: (typeof LOCALES)[number]): Record<string, unknown> {
  31. const settingsDir = path.join(process.cwd(), "messages", locale, "settings");
  32. const out: Record<string, unknown> = {};
  33. for (const entry of fs.readdirSync(settingsDir, { withFileTypes: true })) {
  34. if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
  35. const name = entry.name.replace(/\.json$/, "");
  36. const v = loadJson(path.join(settingsDir, entry.name)) as Record<string, unknown>;
  37. if (name === "strings") Object.assign(out, v);
  38. else out[name] = v;
  39. }
  40. const providersDir = path.join(settingsDir, "providers");
  41. const providers: Record<string, unknown> = {};
  42. for (const entry of fs.readdirSync(providersDir, { withFileTypes: true })) {
  43. if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
  44. const name = entry.name.replace(/\.json$/, "");
  45. const v = loadJson(path.join(providersDir, entry.name)) as Record<string, unknown>;
  46. if (name === "strings") Object.assign(providers, v);
  47. else providers[name] = v;
  48. }
  49. const formDir = path.join(providersDir, "form");
  50. const form: Record<string, unknown> = {};
  51. for (const entry of fs.readdirSync(formDir, { withFileTypes: true })) {
  52. if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
  53. const name = entry.name.replace(/\.json$/, "");
  54. const v = loadJson(path.join(formDir, entry.name)) as Record<string, unknown>;
  55. if (name === "strings") Object.assign(form, v);
  56. else form[name] = v;
  57. }
  58. providers.form = form;
  59. out.providers = providers;
  60. return out;
  61. }
  62. function flattenLeafTypes(
  63. obj: unknown,
  64. prefix = "",
  65. out: Record<string, string> = {}
  66. ): Record<string, string> {
  67. if (!isObject(obj)) {
  68. if (prefix) out[prefix] = typeTag(obj);
  69. return out;
  70. }
  71. for (const [k, v] of Object.entries(obj)) {
  72. const key = prefix ? `${prefix}.${k}` : k;
  73. if (isObject(v)) flattenLeafTypes(v, key, out);
  74. else out[key] = typeTag(v);
  75. }
  76. return out;
  77. }
  78. describe("i18n settings split guards", () => {
  79. test("split file layout matches canonical and each file <= 800 lines", () => {
  80. const canonicalDir = path.join(process.cwd(), "messages", CANONICAL, "settings");
  81. const canonicalFiles = listJsonFilesRecursive(canonicalDir).map((p) =>
  82. path.relative(canonicalDir, p).replaceAll(path.sep, "/")
  83. );
  84. for (const locale of LOCALES) {
  85. const dir = path.join(process.cwd(), "messages", locale, "settings");
  86. const files = listJsonFilesRecursive(dir).map((p) =>
  87. path.relative(dir, p).replaceAll(path.sep, "/")
  88. );
  89. expect(files).toEqual(canonicalFiles);
  90. for (const file of listJsonFilesRecursive(dir)) {
  91. const lines = fs.readFileSync(file, "utf8").split(/\r?\n/).length;
  92. expect(lines).toBeLessThanOrEqual(SETTINGS_LINE_THRESHOLD);
  93. }
  94. }
  95. });
  96. test("settings key set and leaf types match canonical (zh-CN)", () => {
  97. const canonical = loadSplitSettings(CANONICAL);
  98. const canonicalLeaves = flattenLeafTypes(canonical);
  99. const canonicalKeys = Object.keys(canonicalLeaves).sort();
  100. for (const locale of LOCALES) {
  101. const settings = loadSplitSettings(locale);
  102. const leaves = flattenLeafTypes(settings);
  103. const keys = Object.keys(leaves).sort();
  104. expect(keys).toEqual(canonicalKeys);
  105. for (const k of canonicalKeys) {
  106. expect(leaves[k]).toBe(canonicalLeaves[k]);
  107. }
  108. }
  109. });
  110. });