@messagevisor/core 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/jest.config.js +8 -0
- package/lib/benchmark/index.d.ts +2 -0
- package/lib/benchmark/index.js +417 -0
- package/lib/benchmark/index.js.map +1 -0
- package/lib/builder/index.d.ts +70 -0
- package/lib/builder/index.js +831 -0
- package/lib/builder/index.js.map +1 -0
- package/lib/cli/index.d.ts +28 -0
- package/lib/cli/index.js +182 -0
- package/lib/cli/index.js.map +1 -0
- package/lib/config/index.d.ts +61 -0
- package/lib/config/index.js +255 -0
- package/lib/config/index.js.map +1 -0
- package/lib/create/index.d.ts +2 -0
- package/lib/create/index.js +405 -0
- package/lib/create/index.js.map +1 -0
- package/lib/datasource/filesystemAdapter.d.ts +44 -0
- package/lib/datasource/filesystemAdapter.js +424 -0
- package/lib/datasource/filesystemAdapter.js.map +1 -0
- package/lib/datasource/index.d.ts +39 -0
- package/lib/datasource/index.js +96 -0
- package/lib/datasource/index.js.map +1 -0
- package/lib/error.d.ts +6 -0
- package/lib/error.js +49 -0
- package/lib/error.js.map +1 -0
- package/lib/evaluate/cli.d.ts +8 -0
- package/lib/evaluate/cli.js +179 -0
- package/lib/evaluate/cli.js.map +1 -0
- package/lib/evaluate/index.d.ts +10 -0
- package/lib/evaluate/index.js +131 -0
- package/lib/evaluate/index.js.map +1 -0
- package/lib/examples/coerceExampleIsoDates.d.ts +12 -0
- package/lib/examples/coerceExampleIsoDates.js +81 -0
- package/lib/examples/coerceExampleIsoDates.js.map +1 -0
- package/lib/examples/index.d.ts +63 -0
- package/lib/examples/index.js +713 -0
- package/lib/examples/index.js.map +1 -0
- package/lib/exporter/index.d.ts +60 -0
- package/lib/exporter/index.js +610 -0
- package/lib/exporter/index.js.map +1 -0
- package/lib/find-duplicates/index.d.ts +41 -0
- package/lib/find-duplicates/index.js +297 -0
- package/lib/find-duplicates/index.js.map +1 -0
- package/lib/generate-code/index.d.ts +11 -0
- package/lib/generate-code/index.js +157 -0
- package/lib/generate-code/index.js.map +1 -0
- package/lib/generate-code/typescript.d.ts +14 -0
- package/lib/generate-code/typescript.js +307 -0
- package/lib/generate-code/typescript.js.map +1 -0
- package/lib/importer/index.d.ts +64 -0
- package/lib/importer/index.js +1092 -0
- package/lib/importer/index.js.map +1 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.js +35 -0
- package/lib/index.js.map +1 -0
- package/lib/info/index.d.ts +17 -0
- package/lib/info/index.js +132 -0
- package/lib/info/index.js.map +1 -0
- package/lib/init/index.d.ts +30 -0
- package/lib/init/index.js +348 -0
- package/lib/init/index.js.map +1 -0
- package/lib/lint/index.d.ts +1 -0
- package/lib/lint/index.js +6 -0
- package/lib/lint/index.js.map +1 -0
- package/lib/linter/attributeSchema.d.ts +7 -0
- package/lib/linter/attributeSchema.js +36 -0
- package/lib/linter/attributeSchema.js.map +1 -0
- package/lib/linter/checkLocaleCircularDependency.d.ts +7 -0
- package/lib/linter/checkLocaleCircularDependency.js +42 -0
- package/lib/linter/checkLocaleCircularDependency.js.map +1 -0
- package/lib/linter/conditionSchema.d.ts +3 -0
- package/lib/linter/conditionSchema.js +283 -0
- package/lib/linter/conditionSchema.js.map +1 -0
- package/lib/linter/formatSchema.d.ts +325 -0
- package/lib/linter/formatSchema.js +165 -0
- package/lib/linter/formatSchema.js.map +1 -0
- package/lib/linter/icuStyleLint.d.ts +6 -0
- package/lib/linter/icuStyleLint.js +226 -0
- package/lib/linter/icuStyleLint.js.map +1 -0
- package/lib/linter/index.d.ts +34 -0
- package/lib/linter/index.js +557 -0
- package/lib/linter/index.js.map +1 -0
- package/lib/linter/localeSchema.d.ts +672 -0
- package/lib/linter/localeSchema.js +50 -0
- package/lib/linter/localeSchema.js.map +1 -0
- package/lib/linter/messageSchema.d.ts +35 -0
- package/lib/linter/messageSchema.js +115 -0
- package/lib/linter/messageSchema.js.map +1 -0
- package/lib/linter/printError.d.ts +8 -0
- package/lib/linter/printError.js +41 -0
- package/lib/linter/printError.js.map +1 -0
- package/lib/linter/schema.d.ts +33 -0
- package/lib/linter/schema.js +192 -0
- package/lib/linter/schema.js.map +1 -0
- package/lib/linter/segmentSchema.d.ts +8 -0
- package/lib/linter/segmentSchema.js +18 -0
- package/lib/linter/segmentSchema.js.map +1 -0
- package/lib/linter/targetSchema.d.ts +337 -0
- package/lib/linter/targetSchema.js +39 -0
- package/lib/linter/targetSchema.js.map +1 -0
- package/lib/linter/testSchema.d.ts +71 -0
- package/lib/linter/testSchema.js +165 -0
- package/lib/linter/testSchema.js.map +1 -0
- package/lib/linter/zodHelpers.d.ts +2 -0
- package/lib/linter/zodHelpers.js +15 -0
- package/lib/linter/zodHelpers.js.map +1 -0
- package/lib/list/index.d.ts +8 -0
- package/lib/list/index.js +524 -0
- package/lib/list/index.js.map +1 -0
- package/lib/matrix.d.ts +4 -0
- package/lib/matrix.js +66 -0
- package/lib/matrix.js.map +1 -0
- package/lib/promoter/index.d.ts +65 -0
- package/lib/promoter/index.js +1208 -0
- package/lib/promoter/index.js.map +1 -0
- package/lib/prune/index.d.ts +37 -0
- package/lib/prune/index.js +673 -0
- package/lib/prune/index.js.map +1 -0
- package/lib/sets.d.ts +10 -0
- package/lib/sets.js +120 -0
- package/lib/sets.js.map +1 -0
- package/lib/tester/cliFormat.d.ts +8 -0
- package/lib/tester/cliFormat.js +15 -0
- package/lib/tester/cliFormat.js.map +1 -0
- package/lib/tester/index.d.ts +35 -0
- package/lib/tester/index.js +713 -0
- package/lib/tester/index.js.map +1 -0
- package/lib/tester/matrix.d.ts +14 -0
- package/lib/tester/matrix.js +76 -0
- package/lib/tester/matrix.js.map +1 -0
- package/lib/tester/prettyDuration.d.ts +1 -0
- package/lib/tester/prettyDuration.js +30 -0
- package/lib/tester/prettyDuration.js.map +1 -0
- package/lib/tester/printTestResult.d.ts +2 -0
- package/lib/tester/printTestResult.js +32 -0
- package/lib/tester/printTestResult.js.map +1 -0
- package/lib/tester/types.d.ts +29 -0
- package/lib/tester/types.js +3 -0
- package/lib/tester/types.js.map +1 -0
- package/package.json +41 -13
- package/src/benchmark/index.spec.ts +375 -0
- package/src/benchmark/index.ts +433 -0
- package/src/builder/index.spec.ts +822 -0
- package/src/builder/index.ts +920 -0
- package/src/cli/index.spec.ts +54 -0
- package/src/cli/index.ts +150 -0
- package/src/config/index.spec.ts +70 -0
- package/src/config/index.ts +259 -0
- package/src/create/index.spec.ts +272 -0
- package/src/create/index.ts +295 -0
- package/src/datasource/filesystemAdapter.ts +313 -0
- package/src/datasource/index.ts +135 -0
- package/src/error.ts +33 -0
- package/src/evaluate/cli.spec.ts +368 -0
- package/src/evaluate/cli.ts +130 -0
- package/src/evaluate/index.ts +161 -0
- package/src/examples/coerceExampleIsoDates.spec.ts +81 -0
- package/src/examples/coerceExampleIsoDates.ts +98 -0
- package/src/examples/index.spec.ts +453 -0
- package/src/examples/index.ts +854 -0
- package/src/exporter/index.spec.ts +443 -0
- package/src/exporter/index.ts +643 -0
- package/src/find-duplicates/index.spec.ts +289 -0
- package/src/find-duplicates/index.ts +314 -0
- package/src/generate-code/index.ts +92 -0
- package/src/generate-code/typescript.spec.ts +241 -0
- package/src/generate-code/typescript.ts +284 -0
- package/src/importer/index.spec.ts +1101 -0
- package/src/importer/index.ts +1190 -0
- package/src/index.ts +18 -0
- package/src/info/index.ts +67 -0
- package/src/init/index.spec.ts +279 -0
- package/src/init/index.ts +292 -0
- package/src/lint/index.ts +1 -0
- package/src/linter/attributeSchema.ts +38 -0
- package/src/linter/checkLocaleCircularDependency.ts +51 -0
- package/src/linter/conditionSchema.ts +386 -0
- package/src/linter/formatSchema.ts +170 -0
- package/src/linter/icuStyleLint.ts +312 -0
- package/src/linter/index.spec.ts +824 -0
- package/src/linter/index.ts +460 -0
- package/src/linter/localeSchema.ts +70 -0
- package/src/linter/messageSchema.ts +152 -0
- package/src/linter/printError.ts +52 -0
- package/src/linter/schema.ts +230 -0
- package/src/linter/segmentSchema.ts +15 -0
- package/src/linter/targetSchema.ts +50 -0
- package/src/linter/testSchema.spec.ts +405 -0
- package/src/linter/testSchema.ts +239 -0
- package/src/linter/zodHelpers.ts +16 -0
- package/src/list/index.spec.ts +431 -0
- package/src/list/index.ts +463 -0
- package/src/matrix.ts +69 -0
- package/src/promoter/index.spec.ts +584 -0
- package/src/promoter/index.ts +1267 -0
- package/src/prune/index.spec.ts +418 -0
- package/src/prune/index.ts +693 -0
- package/src/sets.ts +74 -0
- package/src/tester/cliFormat.ts +11 -0
- package/src/tester/featurevisorIntegration.spec.ts +101 -0
- package/src/tester/index.spec.ts +577 -0
- package/src/tester/index.ts +679 -0
- package/src/tester/matrix.ts +106 -0
- package/src/tester/prettyDuration.ts +34 -0
- package/src/tester/printTestResult.ts +40 -0
- package/src/tester/types.ts +32 -0
- package/tsconfig.cjs.json +11 -0
- package/tsconfig.typecheck.json +4 -0
|
@@ -0,0 +1,1101 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
|
|
5
|
+
import { getProjectConfig } from "../config";
|
|
6
|
+
import { Datasource } from "../datasource";
|
|
7
|
+
import { toCsv } from "../exporter";
|
|
8
|
+
import { importPlugin, importProject, importProjectSets } from "./index";
|
|
9
|
+
|
|
10
|
+
async function writeFile(root: string, relativePath: string, content: string) {
|
|
11
|
+
const filePath = path.join(root, relativePath);
|
|
12
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
13
|
+
await fs.promises.writeFile(filePath, content);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function createProject() {
|
|
17
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-import-"));
|
|
18
|
+
|
|
19
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = {};\n");
|
|
20
|
+
await writeFile(root, "locales/en.yml", "description: English\n");
|
|
21
|
+
await writeFile(
|
|
22
|
+
root,
|
|
23
|
+
"locales/en-US.yml",
|
|
24
|
+
"description: English US\ninheritTranslationsFrom: en\n",
|
|
25
|
+
);
|
|
26
|
+
await writeFile(root, "locales/nl.yml", "description: Dutch\n");
|
|
27
|
+
await writeFile(
|
|
28
|
+
root,
|
|
29
|
+
"messages/common/welcome.yml",
|
|
30
|
+
"description: Welcome\ntranslations:\n en: Welcome\n nl: Welkom\noverrides:\n - key: pro\n segments: '*'\n translations:\n en: Welcome pro\n",
|
|
31
|
+
);
|
|
32
|
+
await writeFile(
|
|
33
|
+
root,
|
|
34
|
+
"messages/common/goodbye.yml",
|
|
35
|
+
"description: Goodbye\ntranslations:\n en: Goodbye\n",
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return root;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function createSetsProject() {
|
|
42
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-import-sets-"));
|
|
43
|
+
|
|
44
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = { sets: true };\n");
|
|
45
|
+
|
|
46
|
+
for (const set of ["dev", "production"]) {
|
|
47
|
+
await writeFile(root, `sets/${set}/locales/en.yml`, "description: English\n");
|
|
48
|
+
await writeFile(
|
|
49
|
+
root,
|
|
50
|
+
`sets/${set}/locales/en-US.yml`,
|
|
51
|
+
"description: English US\ninheritTranslationsFrom: en\n",
|
|
52
|
+
);
|
|
53
|
+
await writeFile(root, `sets/${set}/locales/nl.yml`, "description: Dutch\n");
|
|
54
|
+
await writeFile(
|
|
55
|
+
root,
|
|
56
|
+
`sets/${set}/messages/common/welcome.yml`,
|
|
57
|
+
`description: Welcome ${set}\ntranslations:\n en: Welcome ${set}\n`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return root;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getDatasource(root: string) {
|
|
65
|
+
const projectConfig = getProjectConfig(root);
|
|
66
|
+
const datasource = new Datasource(projectConfig, root);
|
|
67
|
+
|
|
68
|
+
return { projectConfig, datasource };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe("importProject", function () {
|
|
72
|
+
it("previews direct and override translations by default and applies only with apply", async function () {
|
|
73
|
+
const root = await createProject();
|
|
74
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
75
|
+
|
|
76
|
+
await writeFile(
|
|
77
|
+
root,
|
|
78
|
+
"imports/nl.csv",
|
|
79
|
+
[
|
|
80
|
+
"messageKey,messageDescription,en-US,en-USStatus,nl,nlStatus",
|
|
81
|
+
"common.welcome,Changed description,Welcome,direct,Welkom bijgewerkt,direct",
|
|
82
|
+
"common.welcome:pro,Changed override,Welcome pro,direct,Welkom pro,direct",
|
|
83
|
+
].join("\n"),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const preview = await importProject(projectConfig, datasource, {
|
|
87
|
+
input: "imports/nl.csv",
|
|
88
|
+
});
|
|
89
|
+
const previewMessage = await datasource.readMessage("common.welcome");
|
|
90
|
+
|
|
91
|
+
expect(preview.apply).toEqual(false);
|
|
92
|
+
expect(preview.summary.changedMessages).toEqual(1);
|
|
93
|
+
expect(preview.summary.changedOverrides).toEqual(1);
|
|
94
|
+
expect(previewMessage.description).toEqual("Welcome");
|
|
95
|
+
expect(previewMessage.translations.nl).toEqual("Welkom");
|
|
96
|
+
expect(previewMessage.translations["en-US"]).toBeUndefined();
|
|
97
|
+
expect(previewMessage.overrides?.[0].translations.nl).toBeUndefined();
|
|
98
|
+
|
|
99
|
+
const result = await importProject(projectConfig, datasource, {
|
|
100
|
+
input: "imports/nl.csv",
|
|
101
|
+
apply: true,
|
|
102
|
+
});
|
|
103
|
+
const message = await datasource.readMessage("common.welcome");
|
|
104
|
+
|
|
105
|
+
expect(result.apply).toEqual(true);
|
|
106
|
+
expect(message.description).toEqual("Welcome");
|
|
107
|
+
expect(message.translations.nl).toEqual("Welkom bijgewerkt");
|
|
108
|
+
expect(message.translations["en-US"]).toBeUndefined();
|
|
109
|
+
expect(message.overrides?.[0].translations.nl).toEqual("Welkom pro");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("imports all locale columns by default", async function () {
|
|
113
|
+
const root = await createProject();
|
|
114
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
115
|
+
|
|
116
|
+
await writeFile(
|
|
117
|
+
root,
|
|
118
|
+
"imports/all-locales.csv",
|
|
119
|
+
["messageKey,en,nl", "common.welcome,Welcome updated,Welkom bijgewerkt"].join("\n"),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const result = await importProject(projectConfig, datasource, {
|
|
123
|
+
input: "imports/all-locales.csv",
|
|
124
|
+
apply: true,
|
|
125
|
+
});
|
|
126
|
+
const message = await datasource.readMessage("common.welcome");
|
|
127
|
+
|
|
128
|
+
expect(result.summary.changedTranslations).toEqual(2);
|
|
129
|
+
expect(message.translations.en).toEqual("Welcome updated");
|
|
130
|
+
expect(message.translations.nl).toEqual("Welkom bijgewerkt");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("imports only requested locale columns", async function () {
|
|
134
|
+
const root = await createProject();
|
|
135
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
136
|
+
|
|
137
|
+
await writeFile(
|
|
138
|
+
root,
|
|
139
|
+
"imports/selected-locale.csv",
|
|
140
|
+
["messageKey,en,nl", "common.welcome,Welcome updated,Welkom bijgewerkt"].join("\n"),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const result = await importProject(projectConfig, datasource, {
|
|
144
|
+
input: "imports/selected-locale.csv",
|
|
145
|
+
locale: "nl",
|
|
146
|
+
apply: true,
|
|
147
|
+
});
|
|
148
|
+
const message = await datasource.readMessage("common.welcome");
|
|
149
|
+
|
|
150
|
+
expect(result.summary.changedTranslations).toEqual(1);
|
|
151
|
+
expect(message.translations.en).toEqual("Welcome");
|
|
152
|
+
expect(message.translations.nl).toEqual("Welkom bijgewerkt");
|
|
153
|
+
expect(result.warnings).toEqual([]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("imports repeated requested locale columns and rejects unknown requested locales", async function () {
|
|
157
|
+
const root = await createProject();
|
|
158
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
159
|
+
|
|
160
|
+
await writeFile(
|
|
161
|
+
root,
|
|
162
|
+
"imports/repeated-locales.csv",
|
|
163
|
+
["messageKey,en,en-US,nl", "common.welcome,Welcome updated,Howdy,Welkom bijgewerkt"].join(
|
|
164
|
+
"\n",
|
|
165
|
+
),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const result = await importProject(projectConfig, datasource, {
|
|
169
|
+
input: "imports/repeated-locales.csv",
|
|
170
|
+
locale: ["en", "nl"],
|
|
171
|
+
apply: true,
|
|
172
|
+
});
|
|
173
|
+
const message = await datasource.readMessage("common.welcome");
|
|
174
|
+
|
|
175
|
+
expect(result.summary.changedTranslations).toEqual(2);
|
|
176
|
+
expect(message.translations.en).toEqual("Welcome updated");
|
|
177
|
+
expect(message.translations.nl).toEqual("Welkom bijgewerkt");
|
|
178
|
+
expect(message.translations["en-US"]).toBeUndefined();
|
|
179
|
+
|
|
180
|
+
await expect(
|
|
181
|
+
importProject(projectConfig, datasource, {
|
|
182
|
+
input: "imports/repeated-locales.csv",
|
|
183
|
+
locale: "fr",
|
|
184
|
+
}),
|
|
185
|
+
).rejects.toThrow('Unknown locale "fr". Available locales: en, en-US, nl.');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("skips empty cells and unchanged inherited values", async function () {
|
|
189
|
+
const root = await createProject();
|
|
190
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
191
|
+
const before = await fs.promises.readFile(
|
|
192
|
+
path.join(root, "messages/common/goodbye.yml"),
|
|
193
|
+
"utf8",
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
await writeFile(
|
|
197
|
+
root,
|
|
198
|
+
"imports/inherited.csv",
|
|
199
|
+
["messageKey,en,en-US,nl", "common.goodbye,,Goodbye,"].join("\n"),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const result = await importProject(projectConfig, datasource, {
|
|
203
|
+
input: "imports/inherited.csv",
|
|
204
|
+
});
|
|
205
|
+
const after = await fs.promises.readFile(
|
|
206
|
+
path.join(root, "messages/common/goodbye.yml"),
|
|
207
|
+
"utf8",
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
expect(result.apply).toEqual(false);
|
|
211
|
+
expect(result.summary.changedTranslations).toEqual(0);
|
|
212
|
+
expect(after).toEqual(before);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("previews inherited value changes and applies direct translations when requested", async function () {
|
|
216
|
+
const root = await createProject();
|
|
217
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
218
|
+
|
|
219
|
+
await writeFile(
|
|
220
|
+
root,
|
|
221
|
+
"imports/inherited-changed.csv",
|
|
222
|
+
["messageKey,en-US", "common.goodbye,Howdy"].join("\n"),
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const preview = await importProject(projectConfig, datasource, {
|
|
226
|
+
input: "imports/inherited-changed.csv",
|
|
227
|
+
});
|
|
228
|
+
const previewMessage = await datasource.readMessage("common.goodbye");
|
|
229
|
+
|
|
230
|
+
expect(preview.apply).toEqual(false);
|
|
231
|
+
expect(preview.summary.changedTranslations).toEqual(1);
|
|
232
|
+
expect(previewMessage.translations["en-US"]).toBeUndefined();
|
|
233
|
+
|
|
234
|
+
const result = await importProject(projectConfig, datasource, {
|
|
235
|
+
input: "imports/inherited-changed.csv",
|
|
236
|
+
apply: true,
|
|
237
|
+
});
|
|
238
|
+
const message = await datasource.readMessage("common.goodbye");
|
|
239
|
+
|
|
240
|
+
expect(result.apply).toEqual(true);
|
|
241
|
+
expect(message.translations["en-US"]).toEqual("Howdy");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("prunes imported translations that duplicate inherited values", async function () {
|
|
245
|
+
const root = await createProject();
|
|
246
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
247
|
+
|
|
248
|
+
await writeFile(
|
|
249
|
+
root,
|
|
250
|
+
"imports/prune-inherited.csv",
|
|
251
|
+
["messageKey,en-US", "common.goodbye,Goodbye"].join("\n"),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const result = await importProject(projectConfig, datasource, {
|
|
255
|
+
input: "imports/prune-inherited.csv",
|
|
256
|
+
prune: true,
|
|
257
|
+
apply: true,
|
|
258
|
+
});
|
|
259
|
+
const message = await datasource.readMessage("common.goodbye");
|
|
260
|
+
|
|
261
|
+
expect(result.summary.changedTranslations).toEqual(0);
|
|
262
|
+
expect(result.summary.prunedTranslations).toEqual(1);
|
|
263
|
+
expect(message.translations.en).toEqual("Goodbye");
|
|
264
|
+
expect(message.translations["en-US"]).toBeUndefined();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("deletes existing direct translations when prune can use inheritance", async function () {
|
|
268
|
+
const root = await createProject();
|
|
269
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
270
|
+
|
|
271
|
+
await writeFile(
|
|
272
|
+
root,
|
|
273
|
+
"messages/common/goodbye.yml",
|
|
274
|
+
"description: Goodbye\ntranslations:\n en: Goodbye\n en-US: Goodbye\n",
|
|
275
|
+
);
|
|
276
|
+
await writeFile(
|
|
277
|
+
root,
|
|
278
|
+
"messages/common/welcome.yml",
|
|
279
|
+
"description: Welcome\ntranslations:\n en: Welcome\n nl: Welkom\noverrides:\n - key: pro\n segments: '*'\n translations:\n en: Welcome pro\n en-US: Welcome pro\n",
|
|
280
|
+
);
|
|
281
|
+
await writeFile(
|
|
282
|
+
root,
|
|
283
|
+
"imports/prune-existing.csv",
|
|
284
|
+
["messageKey,en-US", "common.goodbye,Goodbye", "common.welcome:pro,Welcome pro"].join("\n"),
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
const preview = await importProject(projectConfig, datasource, {
|
|
288
|
+
input: "imports/prune-existing.csv",
|
|
289
|
+
prune: true,
|
|
290
|
+
});
|
|
291
|
+
const previewMessage = await datasource.readMessage("common.goodbye");
|
|
292
|
+
|
|
293
|
+
expect(preview.apply).toEqual(false);
|
|
294
|
+
expect(preview.summary.changedMessages).toEqual(1);
|
|
295
|
+
expect(preview.summary.changedOverrides).toEqual(1);
|
|
296
|
+
expect(preview.summary.changedTranslations).toEqual(0);
|
|
297
|
+
expect(preview.summary.prunedTranslations).toEqual(2);
|
|
298
|
+
expect(previewMessage.translations["en-US"]).toEqual("Goodbye");
|
|
299
|
+
|
|
300
|
+
const result = await importProject(projectConfig, datasource, {
|
|
301
|
+
input: "imports/prune-existing.csv",
|
|
302
|
+
prune: true,
|
|
303
|
+
apply: true,
|
|
304
|
+
});
|
|
305
|
+
const message = await datasource.readMessage("common.goodbye");
|
|
306
|
+
const welcome = await datasource.readMessage("common.welcome");
|
|
307
|
+
|
|
308
|
+
expect(result.summary.prunedTranslations).toEqual(2);
|
|
309
|
+
expect(message.translations["en-US"]).toBeUndefined();
|
|
310
|
+
expect(welcome.overrides?.[0].translations["en-US"]).toBeUndefined();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("keeps direct translations that differ from inherited values with prune enabled", async function () {
|
|
314
|
+
const root = await createProject();
|
|
315
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
316
|
+
|
|
317
|
+
await writeFile(
|
|
318
|
+
root,
|
|
319
|
+
"imports/prune-different.csv",
|
|
320
|
+
["messageKey,en-US", "common.goodbye,Howdy"].join("\n"),
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const result = await importProject(projectConfig, datasource, {
|
|
324
|
+
input: "imports/prune-different.csv",
|
|
325
|
+
prune: true,
|
|
326
|
+
apply: true,
|
|
327
|
+
});
|
|
328
|
+
const message = await datasource.readMessage("common.goodbye");
|
|
329
|
+
|
|
330
|
+
expect(result.summary.changedTranslations).toEqual(1);
|
|
331
|
+
expect(result.summary.prunedTranslations).toEqual(0);
|
|
332
|
+
expect(message.translations["en-US"]).toEqual("Howdy");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("prunes child values against parent values imported in the same CSV", async function () {
|
|
336
|
+
const root = await createProject();
|
|
337
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
338
|
+
|
|
339
|
+
await writeFile(
|
|
340
|
+
root,
|
|
341
|
+
"imports/prune-same-csv.csv",
|
|
342
|
+
["messageKey,en-US,en", "common.goodbye,Hello,Hello"].join("\n"),
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
const result = await importProject(projectConfig, datasource, {
|
|
346
|
+
input: "imports/prune-same-csv.csv",
|
|
347
|
+
prune: true,
|
|
348
|
+
apply: true,
|
|
349
|
+
});
|
|
350
|
+
const message = await datasource.readMessage("common.goodbye");
|
|
351
|
+
|
|
352
|
+
expect(result.summary.changedTranslations).toEqual(1);
|
|
353
|
+
expect(result.summary.prunedTranslations).toEqual(1);
|
|
354
|
+
expect(message.translations.en).toEqual("Hello");
|
|
355
|
+
expect(message.translations["en-US"]).toBeUndefined();
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("warns for unknown entities by default and creates missing messages and overrides when requested", async function () {
|
|
359
|
+
const root = await createProject();
|
|
360
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
361
|
+
|
|
362
|
+
await writeFile(
|
|
363
|
+
root,
|
|
364
|
+
"imports/missing.csv",
|
|
365
|
+
[
|
|
366
|
+
"messageKey,en,nl",
|
|
367
|
+
"common.new,New,Nieuw",
|
|
368
|
+
"common.new:vip,New VIP,Nieuw VIP",
|
|
369
|
+
"common.unknown:vip,Unknown VIP,Onbekend VIP",
|
|
370
|
+
].join("\n"),
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
const preview = await importProject(projectConfig, datasource, {
|
|
374
|
+
input: "imports/missing.csv",
|
|
375
|
+
createMissing: true,
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
await expect(datasource.readMessage("common.new")).rejects.toThrow();
|
|
379
|
+
expect(preview.apply).toEqual(false);
|
|
380
|
+
expect(preview.summary.createdMessages).toEqual(1);
|
|
381
|
+
expect(preview.summary.createdOverrides).toEqual(1);
|
|
382
|
+
|
|
383
|
+
const result = await importProject(projectConfig, datasource, {
|
|
384
|
+
input: "imports/missing.csv",
|
|
385
|
+
apply: true,
|
|
386
|
+
createMissing: true,
|
|
387
|
+
});
|
|
388
|
+
const created = await datasource.readMessage("common.new");
|
|
389
|
+
|
|
390
|
+
expect(result.warnings.join("\n")).toContain("cannot create override");
|
|
391
|
+
expect(created.description).toEqual("");
|
|
392
|
+
expect(created.translations.nl).toEqual("Nieuw");
|
|
393
|
+
expect(created.overrides?.[0]).toEqual({
|
|
394
|
+
key: "vip",
|
|
395
|
+
segments: "*",
|
|
396
|
+
translations: {
|
|
397
|
+
en: "New VIP",
|
|
398
|
+
nl: "Nieuw VIP",
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("imports flat JSON into the selected locale and previews by default", async function () {
|
|
404
|
+
const root = await createProject();
|
|
405
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
406
|
+
|
|
407
|
+
await writeFile(
|
|
408
|
+
root,
|
|
409
|
+
"imports/nl.json",
|
|
410
|
+
JSON.stringify({
|
|
411
|
+
"common.welcome": "Welkom JSON",
|
|
412
|
+
"common.goodbye": "Tot ziens JSON",
|
|
413
|
+
}),
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
const preview = await importProject(projectConfig, datasource, {
|
|
417
|
+
input: "imports/nl.json",
|
|
418
|
+
fromJson: true,
|
|
419
|
+
locale: "nl",
|
|
420
|
+
});
|
|
421
|
+
const previewMessage = await datasource.readMessage("common.welcome");
|
|
422
|
+
|
|
423
|
+
expect(preview.apply).toEqual(false);
|
|
424
|
+
expect(preview.summary.rows).toEqual(2);
|
|
425
|
+
expect(preview.summary.changedTranslations).toEqual(2);
|
|
426
|
+
expect(previewMessage.translations.nl).toEqual("Welkom");
|
|
427
|
+
|
|
428
|
+
const result = await importProject(projectConfig, datasource, {
|
|
429
|
+
input: "imports/nl.json",
|
|
430
|
+
fromJson: true,
|
|
431
|
+
locale: "nl",
|
|
432
|
+
apply: true,
|
|
433
|
+
});
|
|
434
|
+
const welcome = await datasource.readMessage("common.welcome");
|
|
435
|
+
const goodbye = await datasource.readMessage("common.goodbye");
|
|
436
|
+
|
|
437
|
+
expect(result.summary.changedTranslations).toEqual(2);
|
|
438
|
+
expect(welcome.translations.nl).toEqual("Welkom JSON");
|
|
439
|
+
expect(goodbye.translations.nl).toEqual("Tot ziens JSON");
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it("imports JSON from URLs and nested dot paths", async function () {
|
|
443
|
+
const root = await createProject();
|
|
444
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
445
|
+
const url = "https://example.com/translations.json";
|
|
446
|
+
const fetchSpy = jest.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
447
|
+
new Response(
|
|
448
|
+
JSON.stringify({
|
|
449
|
+
data: {
|
|
450
|
+
translations: {
|
|
451
|
+
"common.welcome": "Welkom URL",
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
}),
|
|
455
|
+
{
|
|
456
|
+
status: 200,
|
|
457
|
+
headers: {
|
|
458
|
+
"content-type": "application/json",
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
),
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
const result = await importProject(projectConfig, datasource, {
|
|
466
|
+
input: url,
|
|
467
|
+
fromJson: true,
|
|
468
|
+
jsonPath: "data.translations",
|
|
469
|
+
locale: "nl",
|
|
470
|
+
apply: true,
|
|
471
|
+
});
|
|
472
|
+
const message = await datasource.readMessage("common.welcome");
|
|
473
|
+
|
|
474
|
+
expect(fetchSpy).toHaveBeenCalledWith(url);
|
|
475
|
+
expect(result.inputFilePath).toEqual(url);
|
|
476
|
+
expect(result.summary.changedTranslations).toEqual(1);
|
|
477
|
+
expect(message.translations.nl).toEqual("Welkom URL");
|
|
478
|
+
} finally {
|
|
479
|
+
fetchSpy.mockRestore();
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("rejects non-OK JSON URL responses", async function () {
|
|
484
|
+
const root = await createProject();
|
|
485
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
486
|
+
const url = "https://example.com/not-found.json";
|
|
487
|
+
const fetchSpy = jest.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
488
|
+
new Response(JSON.stringify({ error: "not found" }), {
|
|
489
|
+
status: 404,
|
|
490
|
+
statusText: "Not Found",
|
|
491
|
+
}),
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
await expect(
|
|
496
|
+
importProject(projectConfig, datasource, {
|
|
497
|
+
input: url,
|
|
498
|
+
fromJson: true,
|
|
499
|
+
locale: "nl",
|
|
500
|
+
}),
|
|
501
|
+
).rejects.toThrow(`Unable to fetch JSON from ${url}: 404 Not Found`);
|
|
502
|
+
} finally {
|
|
503
|
+
fetchSpy.mockRestore();
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("imports JSON override keys and can create missing messages", async function () {
|
|
508
|
+
const root = await createProject();
|
|
509
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
510
|
+
|
|
511
|
+
await writeFile(
|
|
512
|
+
root,
|
|
513
|
+
"imports/override.json",
|
|
514
|
+
JSON.stringify({
|
|
515
|
+
"common.welcome:pro": "Welkom pro JSON",
|
|
516
|
+
"common.created": "Nieuw JSON",
|
|
517
|
+
}),
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
const result = await importProject(projectConfig, datasource, {
|
|
521
|
+
input: "imports/override.json",
|
|
522
|
+
fromJson: true,
|
|
523
|
+
locale: "nl",
|
|
524
|
+
createMissing: true,
|
|
525
|
+
apply: true,
|
|
526
|
+
});
|
|
527
|
+
const welcome = await datasource.readMessage("common.welcome");
|
|
528
|
+
const created = await datasource.readMessage("common.created");
|
|
529
|
+
|
|
530
|
+
expect(result.summary.changedOverrides).toEqual(1);
|
|
531
|
+
expect(result.summary.createdMessages).toEqual(1);
|
|
532
|
+
expect(welcome.overrides?.[0].translations.nl).toEqual("Welkom pro JSON");
|
|
533
|
+
expect(created.translations.nl).toEqual("Nieuw JSON");
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it("prunes JSON translations that duplicate inherited values", async function () {
|
|
537
|
+
const root = await createProject();
|
|
538
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
539
|
+
|
|
540
|
+
await writeFile(
|
|
541
|
+
root,
|
|
542
|
+
"messages/common/goodbye.yml",
|
|
543
|
+
"description: Goodbye\ntranslations:\n en: Goodbye\n en-US: Goodbye\n",
|
|
544
|
+
);
|
|
545
|
+
await writeFile(
|
|
546
|
+
root,
|
|
547
|
+
"imports/prune.json",
|
|
548
|
+
JSON.stringify({
|
|
549
|
+
"common.goodbye": "Goodbye",
|
|
550
|
+
}),
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
const result = await importProject(projectConfig, datasource, {
|
|
554
|
+
input: "imports/prune.json",
|
|
555
|
+
fromJson: true,
|
|
556
|
+
locale: "en-US",
|
|
557
|
+
prune: true,
|
|
558
|
+
apply: true,
|
|
559
|
+
});
|
|
560
|
+
const message = await datasource.readMessage("common.goodbye");
|
|
561
|
+
|
|
562
|
+
expect(result.summary.prunedTranslations).toEqual(1);
|
|
563
|
+
expect(message.translations["en-US"]).toBeUndefined();
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it("validates JSON import inputs", async function () {
|
|
567
|
+
const root = await createProject();
|
|
568
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
569
|
+
|
|
570
|
+
await writeFile(root, "imports/invalid.json", "{");
|
|
571
|
+
await writeFile(root, "imports/nested.json", JSON.stringify({ data: { translations: [] } }));
|
|
572
|
+
await writeFile(root, "imports/non-string.json", JSON.stringify({ "common.welcome": 1 }));
|
|
573
|
+
|
|
574
|
+
await expect(
|
|
575
|
+
importProject(projectConfig, datasource, {
|
|
576
|
+
fromJson: true,
|
|
577
|
+
}),
|
|
578
|
+
).rejects.toThrow(
|
|
579
|
+
"Pass a JSON file path or URL: messagevisor import <jsonFilePathOrUrl> --from-json --locale=<locale>.",
|
|
580
|
+
);
|
|
581
|
+
await expect(
|
|
582
|
+
importProject(projectConfig, datasource, {
|
|
583
|
+
input: "imports/invalid.json",
|
|
584
|
+
fromJson: true,
|
|
585
|
+
}),
|
|
586
|
+
).rejects.toThrow("--from-json requires exactly one --locale=<locale>.");
|
|
587
|
+
await expect(
|
|
588
|
+
importProject(projectConfig, datasource, {
|
|
589
|
+
input: "imports/invalid.json",
|
|
590
|
+
fromJson: true,
|
|
591
|
+
locale: ["en", "nl"],
|
|
592
|
+
}),
|
|
593
|
+
).rejects.toThrow("--from-json requires exactly one --locale=<locale>.");
|
|
594
|
+
await expect(
|
|
595
|
+
importProject(projectConfig, datasource, {
|
|
596
|
+
input: "imports/invalid.json",
|
|
597
|
+
fromJson: true,
|
|
598
|
+
locale: "fr",
|
|
599
|
+
}),
|
|
600
|
+
).rejects.toThrow('Unknown locale "fr". Available locales: en, en-US, nl.');
|
|
601
|
+
await expect(
|
|
602
|
+
importProject(projectConfig, datasource, {
|
|
603
|
+
input: "imports/invalid.json",
|
|
604
|
+
fromJson: true,
|
|
605
|
+
locale: "nl",
|
|
606
|
+
}),
|
|
607
|
+
).rejects.toThrow("Invalid JSON: unable to parse input.");
|
|
608
|
+
await expect(
|
|
609
|
+
importProject(projectConfig, datasource, {
|
|
610
|
+
input: "imports/nested.json",
|
|
611
|
+
fromJson: true,
|
|
612
|
+
jsonPath: "data.missing",
|
|
613
|
+
locale: "nl",
|
|
614
|
+
}),
|
|
615
|
+
).rejects.toThrow('JSON path "data.missing" was not found.');
|
|
616
|
+
await expect(
|
|
617
|
+
importProject(projectConfig, datasource, {
|
|
618
|
+
input: "imports/nested.json",
|
|
619
|
+
fromJson: true,
|
|
620
|
+
jsonPath: "data.translations",
|
|
621
|
+
locale: "nl",
|
|
622
|
+
}),
|
|
623
|
+
).rejects.toThrow('JSON path "data.translations" must resolve to a flat object.');
|
|
624
|
+
await expect(
|
|
625
|
+
importProject(projectConfig, datasource, {
|
|
626
|
+
input: "imports/non-string.json",
|
|
627
|
+
fromJson: true,
|
|
628
|
+
locale: "nl",
|
|
629
|
+
}),
|
|
630
|
+
).rejects.toThrow('JSON translation value for "common.welcome" must be a string.');
|
|
631
|
+
await expect(
|
|
632
|
+
importProject(projectConfig, datasource, {
|
|
633
|
+
input: "imports/non-string.json",
|
|
634
|
+
fromJson: true,
|
|
635
|
+
locale: "nl",
|
|
636
|
+
delimiter: ";",
|
|
637
|
+
}),
|
|
638
|
+
).rejects.toThrow("--delimiter can only be used with CSV imports.");
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it("parses custom CSV dialects with BOM, quotes, commas, newlines, and CRLF", async function () {
|
|
642
|
+
const root = await createProject();
|
|
643
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
644
|
+
const csv = ["\uFEFFmessageKey;nl", 'common.goodbye;"Tot ziens, ""Ada""', 'morgen"'].join(
|
|
645
|
+
"\r\n",
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
await writeFile(root, "imports/dialect.csv", csv);
|
|
649
|
+
|
|
650
|
+
const result = await importProject(projectConfig, datasource, {
|
|
651
|
+
input: "imports/dialect.csv",
|
|
652
|
+
apply: true,
|
|
653
|
+
delimiter: ";",
|
|
654
|
+
bom: true,
|
|
655
|
+
});
|
|
656
|
+
const message = await datasource.readMessage("common.goodbye");
|
|
657
|
+
|
|
658
|
+
expect(result.summary.changedTranslations).toEqual(1);
|
|
659
|
+
expect(message.translations.nl).toEqual('Tot ziens, "Ada"\nmorgen');
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it("imports quoted multiline translations with quotes and commas", async function () {
|
|
663
|
+
const root = await createProject();
|
|
664
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
665
|
+
|
|
666
|
+
await writeFile(
|
|
667
|
+
root,
|
|
668
|
+
"imports/multiline.csv",
|
|
669
|
+
["messageKey;nl", 'common.goodbye;"Tot ziens, ""Ada""', 'morgen, graag"'].join("\r\n"),
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
const result = await importProject(projectConfig, datasource, {
|
|
673
|
+
input: "imports/multiline.csv",
|
|
674
|
+
apply: true,
|
|
675
|
+
delimiter: ";",
|
|
676
|
+
});
|
|
677
|
+
const message = await datasource.readMessage("common.goodbye");
|
|
678
|
+
|
|
679
|
+
expect(result.summary.changedTranslations).toEqual(1);
|
|
680
|
+
expect(message.translations.nl).toEqual('Tot ziens, "Ada"\nmorgen, graag');
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it("imports exported multiline CSV back with normalized line breaks", async function () {
|
|
684
|
+
const root = await createProject();
|
|
685
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
686
|
+
const translation = ' Leading\nLine, "quoted"\r\nTrailing ';
|
|
687
|
+
|
|
688
|
+
await writeFile(
|
|
689
|
+
root,
|
|
690
|
+
"imports/exported-multiline.csv",
|
|
691
|
+
toCsv(["messageKey", "nl"], [["common.goodbye", translation]], {
|
|
692
|
+
lineEnding: "crlf",
|
|
693
|
+
}),
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
const result = await importProject(projectConfig, datasource, {
|
|
697
|
+
input: "imports/exported-multiline.csv",
|
|
698
|
+
apply: true,
|
|
699
|
+
});
|
|
700
|
+
const message = await datasource.readMessage("common.goodbye");
|
|
701
|
+
|
|
702
|
+
expect(result.summary.changedTranslations).toEqual(1);
|
|
703
|
+
expect(message.translations.nl).toEqual(' Leading\nLine, "quoted"\nTrailing ');
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it("rejects malformed quotes in unquoted fields and after closing quotes", async function () {
|
|
707
|
+
const root = await createProject();
|
|
708
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
709
|
+
|
|
710
|
+
await writeFile(
|
|
711
|
+
root,
|
|
712
|
+
"imports/bad-unquoted-quote.csv",
|
|
713
|
+
["messageKey,nl", 'common.goodbye,Tot "ziens'].join("\n"),
|
|
714
|
+
);
|
|
715
|
+
await writeFile(
|
|
716
|
+
root,
|
|
717
|
+
"imports/bad-after-quote.csv",
|
|
718
|
+
["messageKey,nl", 'common.goodbye,"Tot ziens"oops'].join("\n"),
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
await expect(
|
|
722
|
+
importProject(projectConfig, datasource, {
|
|
723
|
+
input: "imports/bad-unquoted-quote.csv",
|
|
724
|
+
}),
|
|
725
|
+
).rejects.toThrow("Invalid CSV: unexpected quote in unquoted field.");
|
|
726
|
+
await expect(
|
|
727
|
+
importProject(projectConfig, datasource, {
|
|
728
|
+
input: "imports/bad-after-quote.csv",
|
|
729
|
+
}),
|
|
730
|
+
).rejects.toThrow("Invalid CSV: unexpected character after closing quote.");
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it("rejects extra row cells and accepts fewer row cells as empty", async function () {
|
|
734
|
+
const root = await createProject();
|
|
735
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
736
|
+
|
|
737
|
+
await writeFile(
|
|
738
|
+
root,
|
|
739
|
+
"imports/extra-cells.csv",
|
|
740
|
+
["messageKey,nl", "common.goodbye,Tot ziens,extra"].join("\n"),
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
await expect(
|
|
744
|
+
importProject(projectConfig, datasource, {
|
|
745
|
+
input: "imports/extra-cells.csv",
|
|
746
|
+
}),
|
|
747
|
+
).rejects.toThrow("Invalid CSV: row 2 has 3 cells but only 2 headers.");
|
|
748
|
+
|
|
749
|
+
await writeFile(
|
|
750
|
+
root,
|
|
751
|
+
"imports/fewer-cells.csv",
|
|
752
|
+
["messageKey,en,nl", "common.goodbye,Goodbye updated"].join("\n"),
|
|
753
|
+
);
|
|
754
|
+
|
|
755
|
+
const result = await importProject(projectConfig, datasource, {
|
|
756
|
+
input: "imports/fewer-cells.csv",
|
|
757
|
+
apply: true,
|
|
758
|
+
});
|
|
759
|
+
const message = await datasource.readMessage("common.goodbye");
|
|
760
|
+
|
|
761
|
+
expect(result.summary.changedTranslations).toEqual(1);
|
|
762
|
+
expect(message.translations.en).toEqual("Goodbye updated");
|
|
763
|
+
expect(message.translations.nl).toBeUndefined();
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
describe("importProjectSets", function () {
|
|
768
|
+
it("previews routed set rows by default and applies only selected sets with apply", async function () {
|
|
769
|
+
const root = await createSetsProject();
|
|
770
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
771
|
+
|
|
772
|
+
await writeFile(
|
|
773
|
+
root,
|
|
774
|
+
"imports/sets.csv",
|
|
775
|
+
[
|
|
776
|
+
"set,messageKey,en,nl",
|
|
777
|
+
"dev,common.welcome,Dev updated,Dev NL",
|
|
778
|
+
"production,common.welcome,Production updated,Production NL",
|
|
779
|
+
].join("\n"),
|
|
780
|
+
);
|
|
781
|
+
|
|
782
|
+
const preview = await importProjectSets(projectConfig, datasource, {
|
|
783
|
+
input: "imports/sets.csv",
|
|
784
|
+
set: "production",
|
|
785
|
+
});
|
|
786
|
+
const previewDevMessage = await datasource.forSet("dev").readMessage("common.welcome");
|
|
787
|
+
const previewProductionMessage = await datasource
|
|
788
|
+
.forSet("production")
|
|
789
|
+
.readMessage("common.welcome");
|
|
790
|
+
|
|
791
|
+
expect(preview.apply).toEqual(false);
|
|
792
|
+
expect(preview.summary.sets).toEqual(["production"]);
|
|
793
|
+
expect(previewDevMessage.translations.en).toEqual("Welcome dev");
|
|
794
|
+
expect(previewProductionMessage.translations.en).toEqual("Welcome production");
|
|
795
|
+
expect(previewProductionMessage.translations.nl).toBeUndefined();
|
|
796
|
+
|
|
797
|
+
const result = await importProjectSets(projectConfig, datasource, {
|
|
798
|
+
input: "imports/sets.csv",
|
|
799
|
+
set: "production",
|
|
800
|
+
apply: true,
|
|
801
|
+
});
|
|
802
|
+
const devMessage = await datasource.forSet("dev").readMessage("common.welcome");
|
|
803
|
+
const productionMessage = await datasource.forSet("production").readMessage("common.welcome");
|
|
804
|
+
|
|
805
|
+
expect(result.apply).toEqual(true);
|
|
806
|
+
expect(result.summary.sets).toEqual(["production"]);
|
|
807
|
+
expect(devMessage.translations.en).toEqual("Welcome dev");
|
|
808
|
+
expect(productionMessage.translations.en).toEqual("Production updated");
|
|
809
|
+
expect(productionMessage.translations.nl).toEqual("Production NL");
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
it("requires one set when CSV has no set column", async function () {
|
|
813
|
+
const root = await createSetsProject();
|
|
814
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
815
|
+
|
|
816
|
+
await writeFile(
|
|
817
|
+
root,
|
|
818
|
+
"imports/no-set.csv",
|
|
819
|
+
["messageKey,nl", "common.welcome,Welkom"].join("\n"),
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
await expect(
|
|
823
|
+
importProjectSets(projectConfig, datasource, {
|
|
824
|
+
input: "imports/no-set.csv",
|
|
825
|
+
}),
|
|
826
|
+
).rejects.toThrow('CSV without a "set" column requires exactly one --set=<set>.');
|
|
827
|
+
|
|
828
|
+
const result = await importProjectSets(projectConfig, datasource, {
|
|
829
|
+
input: "imports/no-set.csv",
|
|
830
|
+
set: "dev",
|
|
831
|
+
apply: true,
|
|
832
|
+
});
|
|
833
|
+
const message = await datasource.forSet("dev").readMessage("common.welcome");
|
|
834
|
+
|
|
835
|
+
expect(result.summary.changedTranslations).toEqual(1);
|
|
836
|
+
expect(message.translations.nl).toEqual("Welkom");
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
it("warns and skips unknown sets", async function () {
|
|
840
|
+
const root = await createSetsProject();
|
|
841
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
842
|
+
|
|
843
|
+
await writeFile(
|
|
844
|
+
root,
|
|
845
|
+
"imports/unknown-set.csv",
|
|
846
|
+
["set,messageKey,nl", "qa,common.welcome,Welkom"].join("\n"),
|
|
847
|
+
);
|
|
848
|
+
|
|
849
|
+
const result = await importProjectSets(projectConfig, datasource, {
|
|
850
|
+
input: "imports/unknown-set.csv",
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
expect(result.summary.skippedRows).toEqual(1);
|
|
854
|
+
expect(result.warnings.join("\n")).toContain('unknown set "qa"');
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
it("prunes inherited translations independently per set", async function () {
|
|
858
|
+
const root = await createSetsProject();
|
|
859
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
860
|
+
|
|
861
|
+
await writeFile(
|
|
862
|
+
root,
|
|
863
|
+
"imports/prune-sets.csv",
|
|
864
|
+
[
|
|
865
|
+
"set,messageKey,en-US",
|
|
866
|
+
"dev,common.welcome,Welcome dev",
|
|
867
|
+
"production,common.welcome,Production US",
|
|
868
|
+
].join("\n"),
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
const result = await importProjectSets(projectConfig, datasource, {
|
|
872
|
+
input: "imports/prune-sets.csv",
|
|
873
|
+
prune: true,
|
|
874
|
+
apply: true,
|
|
875
|
+
});
|
|
876
|
+
const devMessage = await datasource.forSet("dev").readMessage("common.welcome");
|
|
877
|
+
const productionMessage = await datasource.forSet("production").readMessage("common.welcome");
|
|
878
|
+
|
|
879
|
+
expect(result.summary.changedTranslations).toEqual(1);
|
|
880
|
+
expect(result.summary.prunedTranslations).toEqual(1);
|
|
881
|
+
expect(devMessage.translations["en-US"]).toBeUndefined();
|
|
882
|
+
expect(productionMessage.translations["en-US"]).toEqual("Production US");
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
it("imports JSON into exactly one requested set", async function () {
|
|
886
|
+
const root = await createSetsProject();
|
|
887
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
888
|
+
|
|
889
|
+
await writeFile(
|
|
890
|
+
root,
|
|
891
|
+
"imports/sets.json",
|
|
892
|
+
JSON.stringify({
|
|
893
|
+
"common.welcome": "Welkom staging JSON",
|
|
894
|
+
}),
|
|
895
|
+
);
|
|
896
|
+
|
|
897
|
+
await expect(
|
|
898
|
+
importProjectSets(projectConfig, datasource, {
|
|
899
|
+
input: "imports/sets.json",
|
|
900
|
+
fromJson: true,
|
|
901
|
+
locale: "nl",
|
|
902
|
+
}),
|
|
903
|
+
).rejects.toThrow(
|
|
904
|
+
"--from-json requires exactly one --set=<set> when `sets: true` is configured.",
|
|
905
|
+
);
|
|
906
|
+
|
|
907
|
+
const result = await importProjectSets(projectConfig, datasource, {
|
|
908
|
+
input: "imports/sets.json",
|
|
909
|
+
fromJson: true,
|
|
910
|
+
set: "dev",
|
|
911
|
+
locale: "nl",
|
|
912
|
+
apply: true,
|
|
913
|
+
});
|
|
914
|
+
const devMessage = await datasource.forSet("dev").readMessage("common.welcome");
|
|
915
|
+
const productionMessage = await datasource.forSet("production").readMessage("common.welcome");
|
|
916
|
+
|
|
917
|
+
expect(result.summary.sets).toEqual(["dev"]);
|
|
918
|
+
expect(result.summary.changedTranslations).toEqual(1);
|
|
919
|
+
expect(devMessage.translations.nl).toEqual("Welkom staging JSON");
|
|
920
|
+
expect(productionMessage.translations.nl).toBeUndefined();
|
|
921
|
+
});
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
describe("importPlugin", function () {
|
|
925
|
+
it("prints expected input errors without throwing", async function () {
|
|
926
|
+
const root = await createProject();
|
|
927
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
928
|
+
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => undefined);
|
|
929
|
+
|
|
930
|
+
try {
|
|
931
|
+
await expect(
|
|
932
|
+
importPlugin.handler({
|
|
933
|
+
projectConfig,
|
|
934
|
+
datasource,
|
|
935
|
+
parsed: {},
|
|
936
|
+
}),
|
|
937
|
+
).resolves.toEqual(false);
|
|
938
|
+
|
|
939
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
940
|
+
"Pass a CSV file path: messagevisor import <csvFilePath>.",
|
|
941
|
+
);
|
|
942
|
+
} finally {
|
|
943
|
+
consoleErrorSpy.mockRestore();
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
it("prints preview and apply modes from the CLI", async function () {
|
|
948
|
+
const root = await createProject();
|
|
949
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
950
|
+
const consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => undefined);
|
|
951
|
+
|
|
952
|
+
await writeFile(
|
|
953
|
+
root,
|
|
954
|
+
"imports/nl.csv",
|
|
955
|
+
["messageKey,nl", "common.welcome,Welkom CLI"].join("\n"),
|
|
956
|
+
);
|
|
957
|
+
|
|
958
|
+
try {
|
|
959
|
+
await importPlugin.handler({
|
|
960
|
+
projectConfig,
|
|
961
|
+
datasource,
|
|
962
|
+
parsed: {
|
|
963
|
+
input: "imports/nl.csv",
|
|
964
|
+
},
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
const previewOutput = consoleLogSpy.mock.calls.flat().join("\n");
|
|
968
|
+
const previewMessage = await datasource.readMessage("common.welcome");
|
|
969
|
+
|
|
970
|
+
expect(previewOutput).toContain("Mode: preview");
|
|
971
|
+
expect(previewOutput).toContain("Import preview complete");
|
|
972
|
+
expect(previewMessage.translations.nl).toEqual("Welkom");
|
|
973
|
+
|
|
974
|
+
consoleLogSpy.mockClear();
|
|
975
|
+
|
|
976
|
+
await importPlugin.handler({
|
|
977
|
+
projectConfig,
|
|
978
|
+
datasource,
|
|
979
|
+
parsed: {
|
|
980
|
+
input: "imports/nl.csv",
|
|
981
|
+
apply: true,
|
|
982
|
+
},
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
const applyOutput = consoleLogSpy.mock.calls.flat().join("\n");
|
|
986
|
+
const appliedMessage = await datasource.readMessage("common.welcome");
|
|
987
|
+
|
|
988
|
+
expect(applyOutput).toContain("Mode: apply");
|
|
989
|
+
expect(applyOutput).toContain("Pruned translations: 0");
|
|
990
|
+
expect(applyOutput).toContain("Import applied");
|
|
991
|
+
expect(appliedMessage.translations.nl).toEqual("Welkom CLI");
|
|
992
|
+
} finally {
|
|
993
|
+
consoleLogSpy.mockRestore();
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
it("passes parsed locale filters from the CLI", async function () {
|
|
998
|
+
const root = await createProject();
|
|
999
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
1000
|
+
const consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => undefined);
|
|
1001
|
+
|
|
1002
|
+
await writeFile(
|
|
1003
|
+
root,
|
|
1004
|
+
"imports/cli-locales.csv",
|
|
1005
|
+
["messageKey,en,nl", "common.welcome,Welcome CLI,Welkom CLI"].join("\n"),
|
|
1006
|
+
);
|
|
1007
|
+
|
|
1008
|
+
try {
|
|
1009
|
+
await importPlugin.handler({
|
|
1010
|
+
projectConfig,
|
|
1011
|
+
datasource,
|
|
1012
|
+
parsed: {
|
|
1013
|
+
input: "imports/cli-locales.csv",
|
|
1014
|
+
locale: "nl",
|
|
1015
|
+
apply: true,
|
|
1016
|
+
},
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
const message = await datasource.readMessage("common.welcome");
|
|
1020
|
+
|
|
1021
|
+
expect(message.translations.en).toEqual("Welcome");
|
|
1022
|
+
expect(message.translations.nl).toEqual("Welkom CLI");
|
|
1023
|
+
} finally {
|
|
1024
|
+
consoleLogSpy.mockRestore();
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
it("passes parsed prune option from the CLI", async function () {
|
|
1029
|
+
const root = await createProject();
|
|
1030
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
1031
|
+
const consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => undefined);
|
|
1032
|
+
|
|
1033
|
+
await writeFile(
|
|
1034
|
+
root,
|
|
1035
|
+
"imports/cli-prune.csv",
|
|
1036
|
+
["messageKey,en-US", "common.goodbye,Goodbye"].join("\n"),
|
|
1037
|
+
);
|
|
1038
|
+
|
|
1039
|
+
try {
|
|
1040
|
+
await importPlugin.handler({
|
|
1041
|
+
projectConfig,
|
|
1042
|
+
datasource,
|
|
1043
|
+
parsed: {
|
|
1044
|
+
input: "imports/cli-prune.csv",
|
|
1045
|
+
prune: true,
|
|
1046
|
+
apply: true,
|
|
1047
|
+
},
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
const output = consoleLogSpy.mock.calls.flat().join("\n");
|
|
1051
|
+
const message = await datasource.readMessage("common.goodbye");
|
|
1052
|
+
|
|
1053
|
+
expect(output).toContain("Pruned translations: 1");
|
|
1054
|
+
expect(message.translations["en-US"]).toBeUndefined();
|
|
1055
|
+
} finally {
|
|
1056
|
+
consoleLogSpy.mockRestore();
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
it("passes parsed JSON import options from the CLI", async function () {
|
|
1061
|
+
const root = await createProject();
|
|
1062
|
+
const { projectConfig, datasource } = getDatasource(root);
|
|
1063
|
+
const consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => undefined);
|
|
1064
|
+
|
|
1065
|
+
await writeFile(
|
|
1066
|
+
root,
|
|
1067
|
+
"imports/cli-json.json",
|
|
1068
|
+
JSON.stringify({
|
|
1069
|
+
payload: {
|
|
1070
|
+
translations: {
|
|
1071
|
+
"common.welcome": "Welkom CLI JSON",
|
|
1072
|
+
},
|
|
1073
|
+
},
|
|
1074
|
+
}),
|
|
1075
|
+
);
|
|
1076
|
+
|
|
1077
|
+
try {
|
|
1078
|
+
await importPlugin.handler({
|
|
1079
|
+
projectConfig,
|
|
1080
|
+
datasource,
|
|
1081
|
+
parsed: {
|
|
1082
|
+
input: "imports/cli-json.json",
|
|
1083
|
+
fromJson: true,
|
|
1084
|
+
jsonPath: "payload.translations",
|
|
1085
|
+
locale: "nl",
|
|
1086
|
+
apply: true,
|
|
1087
|
+
prune: true,
|
|
1088
|
+
createMissing: true,
|
|
1089
|
+
},
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
const output = consoleLogSpy.mock.calls.flat().join("\n");
|
|
1093
|
+
const message = await datasource.readMessage("common.welcome");
|
|
1094
|
+
|
|
1095
|
+
expect(output).toContain("Mode: apply");
|
|
1096
|
+
expect(message.translations.nl).toEqual("Welkom CLI JSON");
|
|
1097
|
+
} finally {
|
|
1098
|
+
consoleLogSpy.mockRestore();
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
});
|