Skip to content

Commit 754de44

Browse files
feat: json sorting (#435)
* feat: preserve newlines, whitespaces while formatting * fix: fix broken imports * refactor: refactored newline loader * feat(cli): json sorting * test: add test
1 parent 321b98a commit 754de44

6 files changed

Lines changed: 208 additions & 16 deletions

File tree

.changeset/fresh-kings-allow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"lingo.dev": minor
3+
---
4+
5+
preserve newlines, whitespaces while formatting

.changeset/fuzzy-terms-kick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"lingo.dev": minor
3+
---
4+
5+
add json sorting

packages/cli/src/cli/loaders/index.spec.ts

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -655,54 +655,94 @@ user.password=Contraseña
655655
expect(data).toEqual(expectedOutput);
656656
});
657657

658-
it("should save xcode-xcstrings", async () => {
658+
it("should save xcode-xcstrings with alphabetically sorted keys", async () => {
659659
setupFileMocks();
660660

661661
const input = `
662662
{
663663
"sourceLanguage" : "en",
664664
"strings" : {
665-
"greeting" : {
665+
"zebra" : {
666666
"extractionState" : "manual",
667667
"localizations" : {
668668
"en" : {
669669
"stringUnit" : {
670670
"state" : "translated",
671-
"value" : "Hello!",
672-
},
673-
},
674-
},
671+
"value" : "Zebra"
672+
}
673+
}
674+
}
675675
},
676+
"apple" : {
677+
"extractionState" : "manual",
678+
"localizations" : {
679+
"en" : {
680+
"stringUnit" : {
681+
"state" : "translated",
682+
"value" : "Apple"
683+
}
684+
},
685+
"zh" : {
686+
"stringUnit" : {
687+
"state" : "translated",
688+
"value" : "苹果"
689+
}
690+
}
691+
}
692+
}
676693
},
677694
"version" : "1.0"
678-
}
679-
`.trim();
680-
const payload = { greeting: "¡Hola!" };
681-
const expectedOutput = `
682-
{
695+
}`.trim();
696+
const payload = {
697+
zebra: "Cebra",
698+
apple: "Manzana",
699+
};
700+
const expectedOutput = `{
683701
"sourceLanguage" : "en",
684702
"strings" : {
685-
"greeting" : {
703+
"apple" : {
704+
"extractionState" : "manual",
705+
"localizations" : {
706+
"en" : {
707+
"stringUnit" : {
708+
"state" : "translated",
709+
"value" : "Apple"
710+
}
711+
},
712+
"es" : {
713+
"stringUnit" : {
714+
"state" : "translated",
715+
"value" : "Manzana"
716+
}
717+
},
718+
"zh" : {
719+
"stringUnit" : {
720+
"state" : "translated",
721+
"value" : "苹果"
722+
}
723+
}
724+
}
725+
},
726+
"zebra" : {
686727
"extractionState" : "manual",
687728
"localizations" : {
688729
"en" : {
689730
"stringUnit" : {
690731
"state" : "translated",
691-
"value" : "Hello!"
732+
"value" : "Zebra"
692733
}
693734
},
694735
"es" : {
695736
"stringUnit" : {
696737
"state" : "translated",
697-
"value" : "¡Hola!"
738+
"value" : "Cebra"
698739
}
699740
}
700741
}
701742
}
702743
},
703744
"version" : "1.0"
704-
}
705-
`.trim();
745+
}`.trim();
706746

707747
mockFileOperations(input);
708748

packages/cli/src/cli/loaders/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import createVariableLoader from "./variable";
2828
import createSyncLoader from "./sync";
2929
import createPlutilJsonTextLoader from "./plutil-json-loader";
3030
import createNewLineLoader from "./new-line";
31+
import createJsonSortingLoader from "./json-sorting";
3132

3233
export default function createBucketLoader(
3334
bucketType: Z.infer<typeof bucketTypeSchema>,
@@ -123,6 +124,7 @@ export default function createBucketLoader(
123124
createNewLineLoader(),
124125
createPlutilJsonTextLoader(),
125126
createJsonLoader(),
127+
createJsonSortingLoader(),
126128
createXcodeXcstringsLoader(),
127129
createFlatLoader(),
128130
createSyncLoader(),
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { describe, it, expect } from "vitest";
2+
3+
import createJsonSortingLoader from "./json-sorting";
4+
5+
describe("JSON Sorting Loader", () => {
6+
const loader = createJsonSortingLoader();
7+
loader.setDefaultLocale("en");
8+
9+
describe("pull", () => {
10+
it("should return input unchanged", async () => {
11+
const input = { b: 1, a: 2 };
12+
const result = await loader.pull("en", input);
13+
expect(result).toEqual(input);
14+
});
15+
});
16+
17+
describe("push", () => {
18+
it("should sort object keys at root level", async () => {
19+
const input = { zebra: 1, apple: 2, banana: 3 };
20+
const expected = { apple: 2, banana: 3, zebra: 1 };
21+
22+
const result = await loader.push("en", input);
23+
expect(result).toEqual(expected);
24+
});
25+
26+
it("should sort nested object keys", async () => {
27+
const input = {
28+
b: {
29+
z: 1,
30+
y: 2,
31+
x: 3,
32+
},
33+
a: 1,
34+
};
35+
const expected = {
36+
a: 1,
37+
b: {
38+
x: 3,
39+
y: 2,
40+
z: 1,
41+
},
42+
};
43+
44+
const result = await loader.push("en", input);
45+
expect(result).toEqual(expected);
46+
});
47+
48+
it("should handle arrays by sorting their object elements", async () => {
49+
const input = {
50+
items: [
51+
{ b: 1, a: 2 },
52+
{ d: 3, c: 4 },
53+
],
54+
};
55+
const expected = {
56+
items: [
57+
{ a: 2, b: 1 },
58+
{ c: 4, d: 3 },
59+
],
60+
};
61+
62+
const result = await loader.push("en", input);
63+
expect(result).toEqual(expected);
64+
});
65+
66+
it("should handle mixed nested structures", async () => {
67+
const input = {
68+
zebra: [
69+
{ beta: 2, alpha: 1 },
70+
{ delta: 4, gamma: 3 },
71+
],
72+
apple: {
73+
two: 2,
74+
one: 1,
75+
},
76+
};
77+
const expected = {
78+
apple: {
79+
one: 1,
80+
two: 2,
81+
},
82+
zebra: [
83+
{ alpha: 1, beta: 2 },
84+
{ delta: 4, gamma: 3 },
85+
],
86+
};
87+
88+
const result = await loader.push("en", input);
89+
expect(result).toEqual(expected);
90+
});
91+
92+
it("should handle null and primitive values", async () => {
93+
const input = {
94+
c: null,
95+
b: "string",
96+
a: 123,
97+
d: true,
98+
};
99+
const expected = {
100+
a: 123,
101+
b: "string",
102+
c: null,
103+
d: true,
104+
};
105+
106+
const result = await loader.push("en", input);
107+
expect(result).toEqual(expected);
108+
});
109+
});
110+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { ILoader } from "./_types";
2+
import { createLoader } from "./_utils";
3+
4+
export default function createJsonSortingLoader(): ILoader<Record<string, any>, Record<string, any>> {
5+
return createLoader({
6+
async pull(locale, input) {
7+
return input;
8+
},
9+
async push(locale, data, originalInput) {
10+
return sortObjectDeep(data);
11+
},
12+
});
13+
}
14+
15+
function sortObjectDeep(obj: any): any {
16+
if (Array.isArray(obj)) {
17+
return obj.map(sortObjectDeep);
18+
}
19+
20+
if (obj !== null && typeof obj === "object") {
21+
return Object.keys(obj)
22+
.sort()
23+
.reduce((result: any, key) => {
24+
result[key] = sortObjectDeep(obj[key]);
25+
return result;
26+
}, {});
27+
}
28+
29+
return obj;
30+
}

0 commit comments

Comments
 (0)