frontend/scripts/sort-i18n-keys.test.mjs
Rama main · réplica de lectura del repositorio original.
// @vitest-environment node
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, test } from "vitest";
import {
checkLocaleFiles,
normalizeLocaleFile,
sortLocaleFiles,
sortObjectKeys,
} from "./sort-i18n-keys.mjs";
let tempDir = "";
afterEach(() => {
if (tempDir) {
rmSync(tempDir, { recursive: true, force: true });
tempDir = "";
}
});
describe("sortObjectKeys", () => {
test("sorts nested object keys recursively and preserves arrays", () => {
expect(
sortObjectKeys({
z: "last",
a: {
d: "delta",
b: "bravo",
nested: {
c: "charlie",
a: "alpha",
},
},
m: ["keep", { y: 2, x: 1 }],
})
).toEqual({
a: {
b: "bravo",
d: "delta",
nested: {
a: "alpha",
c: "charlie",
},
},
m: ["keep", { y: 2, x: 1 }],
z: "last",
});
});
});
describe("normalizeLocaleFile", () => {
test("rewrites files with stable 2-space JSON and trailing newline", () => {
tempDir = mkdtempSync(join(tmpdir(), "locale-sorter-"));
const filePath = join(tempDir, "en-US.json");
writeFileSync(filePath, '{"z":1,"a":{"d":4,"b":2},"m":[{"y":2,"x":1}]}');
expect(normalizeLocaleFile(filePath)).toBe(true);
expect(readFileSync(filePath, "utf-8")).toBe(
[
"{",
' "a": {',
' "b": 2,',
' "d": 4',
" },",
' "m": [',
" {",
' "y": 2,',
' "x": 1',
" }",
" ],",
' "z": 1',
"}",
"",
].join("\n")
);
});
test("does not rewrite already normalized files and fails fast on invalid JSON", () => {
tempDir = mkdtempSync(join(tmpdir(), "locale-sorter-"));
const validPath = join(tempDir, "ja-JP.json");
const invalidPath = join(tempDir, "broken.json");
const normalized = [
"{",
' "a": {',
' "b": 2,',
' "d": 4',
" },",
' "z": 1',
"}",
"",
].join("\n");
writeFileSync(validPath, normalized);
writeFileSync(invalidPath, '{"a":');
expect(normalizeLocaleFile(validPath)).toBe(false);
expect(readFileSync(validPath, "utf-8")).toBe(normalized);
expect(() => normalizeLocaleFile(invalidPath)).toThrow(
/Failed to parse locale file .*broken\.json/
);
});
test("sortLocaleFiles validates every file before writing any change", () => {
const validPath = "/tmp/locale-sorter-valid.json";
const invalidPath = "/tmp/locale-sorter-invalid.json";
const writes = [];
const readStub = (filePath) => {
if (filePath === validPath) {
return '{"z":1,"a":2}';
}
if (filePath === invalidPath) {
return '{"a":';
}
throw new Error(`Unexpected file read: ${String(filePath)}`);
};
const writeStub = (...args) => {
writes.push(args);
};
expect(
() =>
sortLocaleFiles([validPath, invalidPath], {
readFileSync: readStub,
writeFileSync: writeStub,
})
).toThrow(/Failed to parse locale file .*locale-sorter-invalid\.json/);
expect(writes).toEqual([]);
});
test("checkLocaleFiles reports unsorted files without writing", () => {
const sortedPath = "/tmp/locale-sorter-sorted.json";
const unsortedPath = "/tmp/locale-sorter-unsorted.json";
const writes = [];
const readStub = (filePath) => {
if (filePath === sortedPath) {
return '{\n "a": 1,\n "b": 2\n}\n';
}
if (filePath === unsortedPath) {
return '{"b":2,"a":1}';
}
throw new Error(`Unexpected file read: ${String(filePath)}`);
};
const writeStub = (...args) => {
writes.push(args);
};
expect(
checkLocaleFiles([sortedPath, unsortedPath], {
readFileSync: readStub,
writeFileSync: writeStub,
})
).toEqual([unsortedPath]);
expect(writes).toEqual([]);
});
test("sortLocaleFiles rolls back earlier writes when a later write fails", () => {
const firstPath = "/tmp/locale-sorter-first.json";
const secondPath = "/tmp/locale-sorter-second.json";
const contents = new Map([
[firstPath, '{"z":1,"a":2}'],
[secondPath, '{"y":1,"b":2}'],
]);
const writes = [];
const readStub = (filePath) => {
const content = contents.get(filePath);
if (content === undefined) {
throw new Error(`Unexpected file read: ${String(filePath)}`);
}
return content;
};
const writeStub = (filePath, content) => {
writes.push([filePath, content]);
if (filePath === secondPath && content.includes('"b": 2')) {
throw new Error("disk full");
}
contents.set(filePath, content);
};
expect(
() =>
sortLocaleFiles([firstPath, secondPath], {
readFileSync: readStub,
writeFileSync: writeStub,
})
).toThrow(
/Failed to write locale file .*locale-sorter-second\.json: disk full\. Rolled back 1 earlier file\(s\) and restored the current file\./
);
expect(contents.get(firstPath)).toBe('{"z":1,"a":2}');
expect(contents.get(secondPath)).toBe('{"y":1,"b":2}');
expect(writes).toEqual([
[firstPath, '{\n "a": 2,\n "z": 1\n}\n'],
[secondPath, '{\n "b": 2,\n "y": 1\n}\n'],
[firstPath, '{"z":1,"a":2}'],
[secondPath, '{"y":1,"b":2}'],
]);
});
test("sortLocaleFiles restores the current file after a partial write failure", () => {
const firstPath = "/tmp/locale-sorter-first.json";
const secondPath = "/tmp/locale-sorter-second.json";
const contents = new Map([
[firstPath, '{"z":1,"a":2}'],
[secondPath, '{"y":1,"b":2}'],
]);
const writes = [];
const readStub = (filePath) => {
const content = contents.get(filePath);
if (content === undefined) {
throw new Error(`Unexpected file read: ${String(filePath)}`);
}
return content;
};
const writeStub = (filePath, content) => {
writes.push([filePath, content]);
if (filePath === secondPath && content.includes('"b": 2')) {
contents.set(filePath, '{"y":1');
throw new Error("write interrupted after partial flush");
}
contents.set(filePath, content);
};
expect(
() =>
sortLocaleFiles([firstPath, secondPath], {
readFileSync: readStub,
writeFileSync: writeStub,
})
).toThrow(
/Failed to write locale file .*locale-sorter-second\.json: write interrupted after partial flush\. Rolled back 1 earlier file\(s\) and restored the current file\./
);
expect(contents.get(firstPath)).toBe('{"z":1,"a":2}');
expect(contents.get(secondPath)).toBe('{"y":1,"b":2}');
expect(writes).toEqual([
[firstPath, '{\n "a": 2,\n "z": 1\n}\n'],
[secondPath, '{\n "b": 2,\n "y": 1\n}\n'],
[firstPath, '{"z":1,"a":2}'],
[secondPath, '{"y":1,"b":2}'],
]);
});
});