@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,824 @@
|
|
|
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 { lintProject } from "./index";
|
|
8
|
+
|
|
9
|
+
async function writeFile(root: string, relativePath: string, content: string) {
|
|
10
|
+
const filePath = path.join(root, relativePath);
|
|
11
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
12
|
+
await fs.promises.writeFile(filePath, content);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("lintProject", function () {
|
|
16
|
+
it("finds missing locales", async function () {
|
|
17
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-"));
|
|
18
|
+
|
|
19
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = {};\n");
|
|
20
|
+
|
|
21
|
+
const projectConfig = getProjectConfig(root);
|
|
22
|
+
const datasource = new Datasource(projectConfig, root);
|
|
23
|
+
const result = await lintProject(projectConfig, datasource);
|
|
24
|
+
|
|
25
|
+
expect(result.errors.map((error) => error.message)).toContain(
|
|
26
|
+
"At least one locale is required",
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("disables ICU skeleton syntax by default", async function () {
|
|
31
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-"));
|
|
32
|
+
|
|
33
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = {};\n");
|
|
34
|
+
|
|
35
|
+
const projectConfig = getProjectConfig(root);
|
|
36
|
+
|
|
37
|
+
expect(projectConfig.icuSkeleton).toEqual(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("reports strict schema errors", async function () {
|
|
41
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-"));
|
|
42
|
+
|
|
43
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = {};\n");
|
|
44
|
+
await writeFile(root, "locales/en.yml", "description: English\nunknown: true\n");
|
|
45
|
+
|
|
46
|
+
const projectConfig = getProjectConfig(root);
|
|
47
|
+
const datasource = new Datasource(projectConfig, root);
|
|
48
|
+
const result = await lintProject(projectConfig, datasource);
|
|
49
|
+
|
|
50
|
+
expect(result.hasError).toEqual(true);
|
|
51
|
+
expect(
|
|
52
|
+
result.errors.some(
|
|
53
|
+
(error) =>
|
|
54
|
+
error.entityType === "locale" &&
|
|
55
|
+
error.code === "unrecognized_keys" &&
|
|
56
|
+
error.message.includes("unknown"),
|
|
57
|
+
),
|
|
58
|
+
).toEqual(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("rejects non-boolean promotable values", async function () {
|
|
62
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-"));
|
|
63
|
+
|
|
64
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = {};\n");
|
|
65
|
+
await writeFile(root, "locales/en.yml", "description: English\npromotable: nope\n");
|
|
66
|
+
|
|
67
|
+
const projectConfig = getProjectConfig(root);
|
|
68
|
+
const datasource = new Datasource(projectConfig, root);
|
|
69
|
+
const result = await lintProject(projectConfig, datasource);
|
|
70
|
+
|
|
71
|
+
expect(
|
|
72
|
+
result.errors.some(
|
|
73
|
+
(error) =>
|
|
74
|
+
error.entityType === "locale" &&
|
|
75
|
+
error.path.join(".") === "promotable" &&
|
|
76
|
+
error.message.toLowerCase().includes("boolean"),
|
|
77
|
+
),
|
|
78
|
+
).toEqual(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("rejects non-object message meta values", async function () {
|
|
82
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-"));
|
|
83
|
+
|
|
84
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = {};\n");
|
|
85
|
+
await writeFile(root, "locales/en.yml", "description: English\n");
|
|
86
|
+
await writeFile(
|
|
87
|
+
root,
|
|
88
|
+
"messages/auth/signin.yml",
|
|
89
|
+
["description: Sign in", "meta: nope", "translations:", " en: Sign in", ""].join("\n"),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const projectConfig = getProjectConfig(root);
|
|
93
|
+
const datasource = new Datasource(projectConfig, root);
|
|
94
|
+
const result = await lintProject(projectConfig, datasource);
|
|
95
|
+
|
|
96
|
+
expect(
|
|
97
|
+
result.errors.some(
|
|
98
|
+
(error) =>
|
|
99
|
+
error.entityType === "message" &&
|
|
100
|
+
error.path.join(".") === "meta" &&
|
|
101
|
+
error.message.toLowerCase().includes("record"),
|
|
102
|
+
),
|
|
103
|
+
).toEqual(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("validates message examples strictly", async function () {
|
|
107
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-"));
|
|
108
|
+
|
|
109
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = {};\n");
|
|
110
|
+
await writeFile(root, "locales/en.yml", "description: English\n");
|
|
111
|
+
await writeFile(
|
|
112
|
+
root,
|
|
113
|
+
"messages/auth/signin.yml",
|
|
114
|
+
[
|
|
115
|
+
"description: Sign in",
|
|
116
|
+
"examples:",
|
|
117
|
+
" - locale: en",
|
|
118
|
+
" - locale: missing",
|
|
119
|
+
" - matrix:",
|
|
120
|
+
" locale: [en]",
|
|
121
|
+
" locale: ${{ locale }}",
|
|
122
|
+
" - matrix:",
|
|
123
|
+
" user:",
|
|
124
|
+
" name: Ada",
|
|
125
|
+
" locale: en",
|
|
126
|
+
"translations:",
|
|
127
|
+
" en: Sign in",
|
|
128
|
+
"",
|
|
129
|
+
].join("\n"),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const projectConfig = getProjectConfig(root);
|
|
133
|
+
const datasource = new Datasource(projectConfig, root);
|
|
134
|
+
const result = await lintProject(projectConfig, datasource);
|
|
135
|
+
|
|
136
|
+
expect(
|
|
137
|
+
result.errors.some(
|
|
138
|
+
(error) =>
|
|
139
|
+
error.entityType === "message" &&
|
|
140
|
+
error.path.join(".") === "examples.1.locale" &&
|
|
141
|
+
error.message.includes('Unknown locale "missing"'),
|
|
142
|
+
),
|
|
143
|
+
).toEqual(true);
|
|
144
|
+
expect(
|
|
145
|
+
result.errors.some(
|
|
146
|
+
(error) =>
|
|
147
|
+
error.path.join(".") === "examples.3.matrix.user" &&
|
|
148
|
+
error.message.toLowerCase().includes("array"),
|
|
149
|
+
),
|
|
150
|
+
).toEqual(true);
|
|
151
|
+
expect(
|
|
152
|
+
result.errors.some(
|
|
153
|
+
(error) => error.entityType === "message" && error.path.join(".") === "examples.2.locale",
|
|
154
|
+
),
|
|
155
|
+
).toEqual(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("reports circular locale dependencies for translations, formats, and examples", async function () {
|
|
159
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-"));
|
|
160
|
+
|
|
161
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = {};\n");
|
|
162
|
+
await writeFile(
|
|
163
|
+
root,
|
|
164
|
+
"messages/auth/signin.yml",
|
|
165
|
+
"description: Sign in\ntranslations:\n en: Sign in\n",
|
|
166
|
+
);
|
|
167
|
+
await writeFile(
|
|
168
|
+
root,
|
|
169
|
+
"locales/en.yml",
|
|
170
|
+
"description: English\ninheritTranslationsFrom: nl\nmergeExamplesFrom: fr\nexamples:\n - rawMessage: Hello\n",
|
|
171
|
+
);
|
|
172
|
+
await writeFile(
|
|
173
|
+
root,
|
|
174
|
+
"locales/nl.yml",
|
|
175
|
+
"description: Dutch\ninheritTranslationsFrom: en\ninheritFormatsFrom: fr\n",
|
|
176
|
+
);
|
|
177
|
+
await writeFile(
|
|
178
|
+
root,
|
|
179
|
+
"locales/fr.yml",
|
|
180
|
+
"description: French\ninheritFormatsFrom: nl\nmergeExamplesFrom: en\nexamples:\n - message: auth.signin\n",
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const projectConfig = getProjectConfig(root);
|
|
184
|
+
const datasource = new Datasource(projectConfig, root);
|
|
185
|
+
const result = await lintProject(projectConfig, datasource);
|
|
186
|
+
|
|
187
|
+
const circularErrors = result.errors.filter(
|
|
188
|
+
(error) => error.code === "circular_locale_dependency",
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
expect(circularErrors).toHaveLength(3);
|
|
192
|
+
expect(circularErrors.map((error) => error.path[0]).sort()).toEqual([
|
|
193
|
+
"inheritFormatsFrom",
|
|
194
|
+
"inheritTranslationsFrom",
|
|
195
|
+
"mergeExamplesFrom",
|
|
196
|
+
]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("validates locale examples strictly", async function () {
|
|
200
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-"));
|
|
201
|
+
|
|
202
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = {};\n");
|
|
203
|
+
await writeFile(
|
|
204
|
+
root,
|
|
205
|
+
"messages/auth/signin.yml",
|
|
206
|
+
"description: Sign in\ntranslations:\n en: Sign in\n",
|
|
207
|
+
);
|
|
208
|
+
await writeFile(
|
|
209
|
+
root,
|
|
210
|
+
"locales/en.yml",
|
|
211
|
+
[
|
|
212
|
+
"description: English",
|
|
213
|
+
"examples:",
|
|
214
|
+
" - rawMessage: Hello",
|
|
215
|
+
" message: auth.signin",
|
|
216
|
+
" - description: Missing both",
|
|
217
|
+
" - message: missing.key",
|
|
218
|
+
" - matrix:",
|
|
219
|
+
" user:",
|
|
220
|
+
" name: Ada",
|
|
221
|
+
"",
|
|
222
|
+
].join("\n"),
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const projectConfig = getProjectConfig(root);
|
|
226
|
+
const datasource = new Datasource(projectConfig, root);
|
|
227
|
+
const result = await lintProject(projectConfig, datasource);
|
|
228
|
+
const messages = result.errors.map((error) => error.message);
|
|
229
|
+
|
|
230
|
+
expect(messages).toContain("Example must define exactly one of `rawMessage` or `message`.");
|
|
231
|
+
expect(messages).toContain('Unknown message "missing.key"');
|
|
232
|
+
expect(
|
|
233
|
+
result.errors.some(
|
|
234
|
+
(error) =>
|
|
235
|
+
error.path.join(".") === "examples.3.matrix.user" &&
|
|
236
|
+
error.message.toLowerCase().includes("array"),
|
|
237
|
+
),
|
|
238
|
+
).toEqual(true);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("accepts expanded Intl-backed locale format preset options", async function () {
|
|
242
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-"));
|
|
243
|
+
|
|
244
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = {};\n");
|
|
245
|
+
await writeFile(
|
|
246
|
+
root,
|
|
247
|
+
"locales/en.yml",
|
|
248
|
+
[
|
|
249
|
+
"description: English",
|
|
250
|
+
"formats:",
|
|
251
|
+
" number:",
|
|
252
|
+
" compactShort:",
|
|
253
|
+
" notation: compact",
|
|
254
|
+
" compactDisplay: short",
|
|
255
|
+
" unitDistance:",
|
|
256
|
+
" style: unit",
|
|
257
|
+
" unit: kilometer",
|
|
258
|
+
" unitDisplay: short",
|
|
259
|
+
" signNegative:",
|
|
260
|
+
" signDisplay: negative",
|
|
261
|
+
" priceName:",
|
|
262
|
+
" style: currency",
|
|
263
|
+
" currency: USD",
|
|
264
|
+
" currencyDisplay: name",
|
|
265
|
+
" runtimeMoney:",
|
|
266
|
+
" style: currency",
|
|
267
|
+
" currencyDisplay: code",
|
|
268
|
+
" rounded:",
|
|
269
|
+
" maximumFractionDigits: 2",
|
|
270
|
+
" roundingMode: halfExpand",
|
|
271
|
+
" roundingPriority: lessPrecision",
|
|
272
|
+
" trailingZeroDisplay: stripIfInteger",
|
|
273
|
+
" numberingSystem: latn",
|
|
274
|
+
" date:",
|
|
275
|
+
" fullStyle:",
|
|
276
|
+
" dateStyle: full",
|
|
277
|
+
" calendar: gregory",
|
|
278
|
+
" arabicNumeric:",
|
|
279
|
+
" year: numeric",
|
|
280
|
+
" month: 2-digit",
|
|
281
|
+
" day: 2-digit",
|
|
282
|
+
" numberingSystem: arab",
|
|
283
|
+
" time:",
|
|
284
|
+
" fullStyle:",
|
|
285
|
+
" timeStyle: full",
|
|
286
|
+
" timeZone: UTC",
|
|
287
|
+
" period:",
|
|
288
|
+
" hour: numeric",
|
|
289
|
+
" dayPeriod: long",
|
|
290
|
+
" hour12: true",
|
|
291
|
+
" dateTimeRange:",
|
|
292
|
+
" fullStyle:",
|
|
293
|
+
" dateStyle: full",
|
|
294
|
+
" timeStyle: short",
|
|
295
|
+
" timeZone: UTC",
|
|
296
|
+
"",
|
|
297
|
+
].join("\n"),
|
|
298
|
+
);
|
|
299
|
+
await writeFile(
|
|
300
|
+
root,
|
|
301
|
+
"targets/web.yml",
|
|
302
|
+
[
|
|
303
|
+
"description: Web",
|
|
304
|
+
"includeMessages:",
|
|
305
|
+
" - '*'",
|
|
306
|
+
"locales:",
|
|
307
|
+
" - en",
|
|
308
|
+
"formats:",
|
|
309
|
+
" en:",
|
|
310
|
+
" number:",
|
|
311
|
+
" runtimeMoney:",
|
|
312
|
+
" style: currency",
|
|
313
|
+
" currencyDisplay: symbol",
|
|
314
|
+
"",
|
|
315
|
+
].join("\n"),
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
const projectConfig = getProjectConfig(root);
|
|
319
|
+
const datasource = new Datasource(projectConfig, root);
|
|
320
|
+
const result = await lintProject(projectConfig, datasource);
|
|
321
|
+
|
|
322
|
+
expect(result.errors).toHaveLength(0);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("validates target-level datafile build options", async function () {
|
|
326
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-"));
|
|
327
|
+
|
|
328
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = {};\n");
|
|
329
|
+
await writeFile(root, "locales/en.yml", "description: English\n");
|
|
330
|
+
await writeFile(
|
|
331
|
+
root,
|
|
332
|
+
"targets/web.yml",
|
|
333
|
+
[
|
|
334
|
+
"description: Web",
|
|
335
|
+
"includeMessages:",
|
|
336
|
+
" - '*'",
|
|
337
|
+
"locales:",
|
|
338
|
+
" - en",
|
|
339
|
+
"stringify: false",
|
|
340
|
+
"pretty: true",
|
|
341
|
+
"revisionFromHash: true",
|
|
342
|
+
"",
|
|
343
|
+
].join("\n"),
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
const projectConfig = getProjectConfig(root);
|
|
347
|
+
const datasource = new Datasource(projectConfig, root);
|
|
348
|
+
const result = await lintProject(projectConfig, datasource);
|
|
349
|
+
|
|
350
|
+
expect(result.errors).toHaveLength(0);
|
|
351
|
+
|
|
352
|
+
await writeFile(
|
|
353
|
+
root,
|
|
354
|
+
"targets/web.yml",
|
|
355
|
+
[
|
|
356
|
+
"description: Web",
|
|
357
|
+
"includeMessages:",
|
|
358
|
+
" - '*'",
|
|
359
|
+
"locales:",
|
|
360
|
+
" - en",
|
|
361
|
+
"stringify: no",
|
|
362
|
+
"pretty: yes",
|
|
363
|
+
"revisionFromHash: sometimes",
|
|
364
|
+
"",
|
|
365
|
+
].join("\n"),
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const invalidResult = await lintProject(projectConfig, datasource);
|
|
369
|
+
const paths = invalidResult.errors.map((error) => error.path.join("."));
|
|
370
|
+
|
|
371
|
+
expect(paths).toContain("stringify");
|
|
372
|
+
expect(paths).toContain("pretty");
|
|
373
|
+
expect(paths).toContain("revisionFromHash");
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("rejects invalid locale format option combinations and ICU-only pseudo-styles", async function () {
|
|
377
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-"));
|
|
378
|
+
|
|
379
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = {};\n");
|
|
380
|
+
await writeFile(
|
|
381
|
+
root,
|
|
382
|
+
"locales/en.yml",
|
|
383
|
+
[
|
|
384
|
+
"description: English",
|
|
385
|
+
"formats:",
|
|
386
|
+
" number:",
|
|
387
|
+
" badUnit:",
|
|
388
|
+
" style: unit",
|
|
389
|
+
" badCompact:",
|
|
390
|
+
" compactDisplay: short",
|
|
391
|
+
" badSpellout:",
|
|
392
|
+
" style: spellout",
|
|
393
|
+
" date:",
|
|
394
|
+
" badDateStyle:",
|
|
395
|
+
" dateStyle: full",
|
|
396
|
+
" year: numeric",
|
|
397
|
+
"",
|
|
398
|
+
].join("\n"),
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const projectConfig = getProjectConfig(root);
|
|
402
|
+
const datasource = new Datasource(projectConfig, root);
|
|
403
|
+
const result = await lintProject(projectConfig, datasource);
|
|
404
|
+
const messages = result.errors.map((error) => error.message);
|
|
405
|
+
|
|
406
|
+
expect(messages).toContain("Unit number formats must define `unit`.");
|
|
407
|
+
expect(messages).toContain('`compactDisplay` can only be used when `notation` is "compact".');
|
|
408
|
+
expect(messages).toContain(
|
|
409
|
+
"`dateStyle` / `timeStyle` cannot be combined with granular date/time component fields.",
|
|
410
|
+
);
|
|
411
|
+
expect(
|
|
412
|
+
result.errors.some((error) => error.path.join(".") === "formats.number.badSpellout.style"),
|
|
413
|
+
).toEqual(true);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("validates test assertion matrix usage", async function () {
|
|
417
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-"));
|
|
418
|
+
|
|
419
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = {};\n");
|
|
420
|
+
await writeFile(root, "locales/en.yml", "description: English\n");
|
|
421
|
+
await writeFile(
|
|
422
|
+
root,
|
|
423
|
+
"targets/web.yml",
|
|
424
|
+
"description: Web\nincludeMessages:\n - '*'\nlocales:\n - en\n",
|
|
425
|
+
);
|
|
426
|
+
await writeFile(root, "segments/adult.yml", "conditions: '*'\ndescription: Adult\n");
|
|
427
|
+
await writeFile(
|
|
428
|
+
root,
|
|
429
|
+
"messages/auth/signin.yml",
|
|
430
|
+
"description: Sign in\ntranslations:\n en: Sign in\n",
|
|
431
|
+
);
|
|
432
|
+
await writeFile(
|
|
433
|
+
root,
|
|
434
|
+
"tests/messages/auth/signin.spec.yml",
|
|
435
|
+
[
|
|
436
|
+
"message: auth.signin",
|
|
437
|
+
"assertions:",
|
|
438
|
+
" - matrix:",
|
|
439
|
+
" name: [Ada]",
|
|
440
|
+
" enabled: [true]",
|
|
441
|
+
" locale: en",
|
|
442
|
+
" target: web",
|
|
443
|
+
" description: Hello ${{ name }}",
|
|
444
|
+
" withFlags:",
|
|
445
|
+
" new-homepage: ${{ enabled }}",
|
|
446
|
+
" values:",
|
|
447
|
+
" name: ${{ name }}",
|
|
448
|
+
" expectedTranslation: Sign in",
|
|
449
|
+
"",
|
|
450
|
+
].join("\n"),
|
|
451
|
+
);
|
|
452
|
+
await writeFile(
|
|
453
|
+
root,
|
|
454
|
+
"tests/segments/adult.spec.yml",
|
|
455
|
+
[
|
|
456
|
+
"segment: adult",
|
|
457
|
+
"assertions:",
|
|
458
|
+
" - matrix:",
|
|
459
|
+
" shouldMatch: [true]",
|
|
460
|
+
" segment: adult",
|
|
461
|
+
" context:",
|
|
462
|
+
" plan: pro",
|
|
463
|
+
" expectedToMatch: ${{ shouldMatch }}",
|
|
464
|
+
"",
|
|
465
|
+
].join("\n"),
|
|
466
|
+
);
|
|
467
|
+
await writeFile(
|
|
468
|
+
root,
|
|
469
|
+
"tests/locales/en.spec.yml",
|
|
470
|
+
[
|
|
471
|
+
"locale: en",
|
|
472
|
+
"assertions:",
|
|
473
|
+
" - matrix:",
|
|
474
|
+
" currency: [USD]",
|
|
475
|
+
" expectedFormats:",
|
|
476
|
+
" number:",
|
|
477
|
+
" money:",
|
|
478
|
+
" currency: ${{ currency }}",
|
|
479
|
+
"",
|
|
480
|
+
].join("\n"),
|
|
481
|
+
);
|
|
482
|
+
await writeFile(
|
|
483
|
+
root,
|
|
484
|
+
"tests/targets/web.spec.yml",
|
|
485
|
+
[
|
|
486
|
+
"target: web",
|
|
487
|
+
"assertions:",
|
|
488
|
+
" - matrix:",
|
|
489
|
+
" currency: [USD]",
|
|
490
|
+
" locale: en",
|
|
491
|
+
" expectedFormats:",
|
|
492
|
+
" number:",
|
|
493
|
+
" money:",
|
|
494
|
+
" currency: ${{ currency }}",
|
|
495
|
+
"",
|
|
496
|
+
].join("\n"),
|
|
497
|
+
);
|
|
498
|
+
const projectConfig = getProjectConfig(root);
|
|
499
|
+
const datasource = new Datasource(projectConfig, root);
|
|
500
|
+
const result = await lintProject(projectConfig, datasource);
|
|
501
|
+
|
|
502
|
+
expect(result.hasError).toEqual(false);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("validates feature and experiment condition operators precisely", async function () {
|
|
506
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-"));
|
|
507
|
+
|
|
508
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = {};\n");
|
|
509
|
+
await writeFile(root, "locales/en.yml", "description: English\n");
|
|
510
|
+
await writeFile(
|
|
511
|
+
root,
|
|
512
|
+
"messages/checkout/banner.yml",
|
|
513
|
+
[
|
|
514
|
+
"description: Checkout banner",
|
|
515
|
+
"translations:",
|
|
516
|
+
" en: Default",
|
|
517
|
+
"overrides:",
|
|
518
|
+
" - key: feature",
|
|
519
|
+
" conditions:",
|
|
520
|
+
" feature: new-checkout",
|
|
521
|
+
" operator: equals",
|
|
522
|
+
" value: true",
|
|
523
|
+
" translations:",
|
|
524
|
+
" en: Feature",
|
|
525
|
+
" - key: experiment",
|
|
526
|
+
" conditions:",
|
|
527
|
+
" experiment: checkout-copy",
|
|
528
|
+
" operator: isEnabled",
|
|
529
|
+
" translations:",
|
|
530
|
+
" en: Experiment",
|
|
531
|
+
"",
|
|
532
|
+
].join("\n"),
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
const projectConfig = getProjectConfig(root);
|
|
536
|
+
const datasource = new Datasource(projectConfig, root);
|
|
537
|
+
const result = await lintProject(projectConfig, datasource);
|
|
538
|
+
const messages = result.errors.map((error) => error.message);
|
|
539
|
+
|
|
540
|
+
expect(messages).toContain(
|
|
541
|
+
'Feature conditions only support operators "isEnabled" and "isDisabled".',
|
|
542
|
+
);
|
|
543
|
+
expect(messages).toContain(
|
|
544
|
+
"Feature conditions must not define `value`; the flag state comes from resolveFlag.",
|
|
545
|
+
);
|
|
546
|
+
expect(messages).toContain('Experiment conditions only support operator "hasVariation".');
|
|
547
|
+
expect(messages).toContain(
|
|
548
|
+
"Experiment conditions must define `value` with the expected variation.",
|
|
549
|
+
);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it("requires unique override keys", async function () {
|
|
553
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-"));
|
|
554
|
+
|
|
555
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = {};\n");
|
|
556
|
+
await writeFile(root, "locales/en.yml", "description: English\n");
|
|
557
|
+
await writeFile(
|
|
558
|
+
root,
|
|
559
|
+
"messages/dashboard/welcome.yml",
|
|
560
|
+
[
|
|
561
|
+
"description: Dashboard welcome",
|
|
562
|
+
"translations:",
|
|
563
|
+
" en: Welcome",
|
|
564
|
+
"overrides:",
|
|
565
|
+
" - key: pro",
|
|
566
|
+
' conditions: "*"',
|
|
567
|
+
" translations:",
|
|
568
|
+
" en: Welcome pro",
|
|
569
|
+
" - key: pro",
|
|
570
|
+
' conditions: "*"',
|
|
571
|
+
" translations:",
|
|
572
|
+
" en: Welcome again",
|
|
573
|
+
"",
|
|
574
|
+
].join("\n"),
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
const projectConfig = getProjectConfig(root);
|
|
578
|
+
const datasource = new Datasource(projectConfig, root);
|
|
579
|
+
const result = await lintProject(projectConfig, datasource);
|
|
580
|
+
|
|
581
|
+
expect(result.errors.map((error) => error.message)).toContain(
|
|
582
|
+
'Duplicate override key "pro". Override keys must be unique within a message.',
|
|
583
|
+
);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it("rejects override keys containing namespace or export separator characters", async function () {
|
|
587
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-"));
|
|
588
|
+
|
|
589
|
+
await writeFile(
|
|
590
|
+
root,
|
|
591
|
+
"messagevisor.config.js",
|
|
592
|
+
'module.exports = { namespaceCharacter: "_", exportOverrideKeySeparator: "#" };\n',
|
|
593
|
+
);
|
|
594
|
+
await writeFile(root, "locales/en.yml", "description: English\n");
|
|
595
|
+
await writeFile(
|
|
596
|
+
root,
|
|
597
|
+
"messages/dashboard/welcome.yml",
|
|
598
|
+
[
|
|
599
|
+
"description: Dashboard welcome",
|
|
600
|
+
"translations:",
|
|
601
|
+
" en: Welcome",
|
|
602
|
+
"overrides:",
|
|
603
|
+
" - key: plan_pro",
|
|
604
|
+
' conditions: "*"',
|
|
605
|
+
" translations:",
|
|
606
|
+
" en: Welcome pro",
|
|
607
|
+
" - key: plan#enterprise",
|
|
608
|
+
' conditions: "*"',
|
|
609
|
+
" translations:",
|
|
610
|
+
" en: Welcome enterprise",
|
|
611
|
+
"",
|
|
612
|
+
].join("\n"),
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
const projectConfig = getProjectConfig(root);
|
|
616
|
+
const datasource = new Datasource(projectConfig, root);
|
|
617
|
+
const result = await lintProject(projectConfig, datasource);
|
|
618
|
+
|
|
619
|
+
expect(result.errors.map((error) => error.message)).toEqual(
|
|
620
|
+
expect.arrayContaining([
|
|
621
|
+
'Override key "plan_pro" must not include namespaceCharacter "_".',
|
|
622
|
+
'Override key "plan#enterprise" must not include exportOverrideKeySeparator "#".',
|
|
623
|
+
]),
|
|
624
|
+
);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it("accepts ICU styles that exist in inherited locale formats", async function () {
|
|
628
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-"));
|
|
629
|
+
|
|
630
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = {};\n");
|
|
631
|
+
await writeFile(
|
|
632
|
+
root,
|
|
633
|
+
"locales/en.yml",
|
|
634
|
+
[
|
|
635
|
+
"description: English",
|
|
636
|
+
"formats:",
|
|
637
|
+
" number:",
|
|
638
|
+
" money:",
|
|
639
|
+
" style: currency",
|
|
640
|
+
" currency: USD",
|
|
641
|
+
" date:",
|
|
642
|
+
" long:",
|
|
643
|
+
" year: numeric",
|
|
644
|
+
" month: long",
|
|
645
|
+
" day: numeric",
|
|
646
|
+
"",
|
|
647
|
+
].join("\n"),
|
|
648
|
+
);
|
|
649
|
+
await writeFile(
|
|
650
|
+
root,
|
|
651
|
+
"locales/en-US.yml",
|
|
652
|
+
["description: English (US)", "inheritFormatsFrom: en", ""].join("\n"),
|
|
653
|
+
);
|
|
654
|
+
await writeFile(
|
|
655
|
+
root,
|
|
656
|
+
"messages/billing/summary.yml",
|
|
657
|
+
[
|
|
658
|
+
"description: Billing summary",
|
|
659
|
+
"translations:",
|
|
660
|
+
' en-US: "Total {amount, number, money} on {createdAt, date, long}"',
|
|
661
|
+
"",
|
|
662
|
+
].join("\n"),
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
const projectConfig = getProjectConfig(root);
|
|
666
|
+
const datasource = new Datasource(projectConfig, root);
|
|
667
|
+
const result = await lintProject(projectConfig, datasource);
|
|
668
|
+
|
|
669
|
+
expect(result.errors.filter((error) => error.code === "missing_icu_format_style")).toHaveLength(
|
|
670
|
+
0,
|
|
671
|
+
);
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it("reports missing ICU styles for the target locale format primitive", async function () {
|
|
675
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-"));
|
|
676
|
+
|
|
677
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = {};\n");
|
|
678
|
+
await writeFile(
|
|
679
|
+
root,
|
|
680
|
+
"locales/en.yml",
|
|
681
|
+
[
|
|
682
|
+
"description: English",
|
|
683
|
+
"formats:",
|
|
684
|
+
" number:",
|
|
685
|
+
" money:",
|
|
686
|
+
" style: currency",
|
|
687
|
+
" currency: USD",
|
|
688
|
+
"",
|
|
689
|
+
].join("\n"),
|
|
690
|
+
);
|
|
691
|
+
await writeFile(
|
|
692
|
+
root,
|
|
693
|
+
"locales/en-US.yml",
|
|
694
|
+
["description: English (US)", "inheritFormatsFrom: en", ""].join("\n"),
|
|
695
|
+
);
|
|
696
|
+
await writeFile(
|
|
697
|
+
root,
|
|
698
|
+
"messages/billing/summary.yml",
|
|
699
|
+
[
|
|
700
|
+
"description: Billing summary",
|
|
701
|
+
"translations:",
|
|
702
|
+
' en-US: "Total {amount, number, money} at {createdAt, time, short}"',
|
|
703
|
+
"",
|
|
704
|
+
].join("\n"),
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
const projectConfig = getProjectConfig(root);
|
|
708
|
+
const datasource = new Datasource(projectConfig, root);
|
|
709
|
+
const result = await lintProject(projectConfig, datasource);
|
|
710
|
+
const styleErrors = result.errors.filter((error) => error.code === "missing_icu_format_style");
|
|
711
|
+
|
|
712
|
+
expect(styleErrors).toHaveLength(1);
|
|
713
|
+
expect(styleErrors[0].filePath.endsWith("messages/billing/summary.yml")).toEqual(true);
|
|
714
|
+
expect(styleErrors[0].path).toEqual(["translations", "en-US"]);
|
|
715
|
+
expect(styleErrors[0].message).toEqual(
|
|
716
|
+
'Missing ICU time format style "short" for locale "en-US" in message "billing.summary". Add formats.time.short to locale "en-US" or one of its inheritFormatsFrom ancestors.',
|
|
717
|
+
);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
it("reports missing ICU styles in override translations and allows skeletons by default", async function () {
|
|
721
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-"));
|
|
722
|
+
|
|
723
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = {};\n");
|
|
724
|
+
await writeFile(
|
|
725
|
+
root,
|
|
726
|
+
"locales/en.yml",
|
|
727
|
+
[
|
|
728
|
+
"description: English",
|
|
729
|
+
"formats:",
|
|
730
|
+
" number:",
|
|
731
|
+
" money:",
|
|
732
|
+
" style: currency",
|
|
733
|
+
" currency: USD",
|
|
734
|
+
"",
|
|
735
|
+
].join("\n"),
|
|
736
|
+
);
|
|
737
|
+
await writeFile(
|
|
738
|
+
root,
|
|
739
|
+
"messages/billing/discount.yml",
|
|
740
|
+
[
|
|
741
|
+
"description: Billing discount",
|
|
742
|
+
"translations:",
|
|
743
|
+
' en: "Total {amount, number, ::currency/USD}"',
|
|
744
|
+
"overrides:",
|
|
745
|
+
" - key: vip",
|
|
746
|
+
' conditions: "*"',
|
|
747
|
+
" translations:",
|
|
748
|
+
' en: "VIP total {amount, number, vipMoney}"',
|
|
749
|
+
"",
|
|
750
|
+
].join("\n"),
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
const projectConfig = getProjectConfig(root);
|
|
754
|
+
const datasource = new Datasource(projectConfig, root);
|
|
755
|
+
const result = await lintProject(projectConfig, datasource);
|
|
756
|
+
const styleErrors = result.errors.filter((error) => error.code === "missing_icu_format_style");
|
|
757
|
+
|
|
758
|
+
expect(styleErrors).toHaveLength(1);
|
|
759
|
+
expect(styleErrors[0].path).toEqual(["overrides", 0, "translations", "en"]);
|
|
760
|
+
expect(styleErrors[0].message).toEqual(
|
|
761
|
+
'Missing ICU number format style "vipMoney" for locale "en" in message "billing.discount". Add formats.number.vipMoney to locale "en" or one of its inheritFormatsFrom ancestors.',
|
|
762
|
+
);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it("reports ICU skeleton styles when they are disabled in project config", async function () {
|
|
766
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-"));
|
|
767
|
+
|
|
768
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = { icuSkeleton: false };\n");
|
|
769
|
+
await writeFile(root, "locales/en.yml", "description: English\n");
|
|
770
|
+
await writeFile(
|
|
771
|
+
root,
|
|
772
|
+
"messages/billing/total.yml",
|
|
773
|
+
[
|
|
774
|
+
"description: Billing total",
|
|
775
|
+
"translations:",
|
|
776
|
+
' en: "Total {amount, number, ::currency/USD}"',
|
|
777
|
+
"",
|
|
778
|
+
].join("\n"),
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
const projectConfig = getProjectConfig(root);
|
|
782
|
+
const datasource = new Datasource(projectConfig, root);
|
|
783
|
+
const result = await lintProject(projectConfig, datasource);
|
|
784
|
+
const skeletonErrors = result.errors.filter(
|
|
785
|
+
(error) => error.code === "icu_skeleton_not_allowed",
|
|
786
|
+
);
|
|
787
|
+
|
|
788
|
+
expect(projectConfig.icuSkeleton).toEqual(false);
|
|
789
|
+
expect(skeletonErrors).toHaveLength(1);
|
|
790
|
+
expect(skeletonErrors[0].path).toEqual(["translations", "en"]);
|
|
791
|
+
expect(skeletonErrors[0].message).toEqual(
|
|
792
|
+
'ICU skeleton style "::currency/USD" is not allowed for locale "en" in message "billing.total" because messagevisor.config.js has icuSkeleton set to false. Use a named formats.number preset instead, or enable icuSkeleton.',
|
|
793
|
+
);
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
it("reports structurally invalid ICU syntax before datafiles are built", async function () {
|
|
797
|
+
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-"));
|
|
798
|
+
|
|
799
|
+
await writeFile(root, "messagevisor.config.js", "module.exports = {};\n");
|
|
800
|
+
await writeFile(root, "locales/en.yml", "description: English\n");
|
|
801
|
+
await writeFile(
|
|
802
|
+
root,
|
|
803
|
+
"messages/cart/count.yml",
|
|
804
|
+
[
|
|
805
|
+
"description: Cart count",
|
|
806
|
+
"translations:",
|
|
807
|
+
' en: "{count, plural, one {One item} other {# items}"',
|
|
808
|
+
"",
|
|
809
|
+
].join("\n"),
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
const projectConfig = getProjectConfig(root);
|
|
813
|
+
const datasource = new Datasource(projectConfig, root);
|
|
814
|
+
const result = await lintProject(projectConfig, datasource);
|
|
815
|
+
const syntaxErrors = result.errors.filter((error) => error.code === "invalid_icu_syntax");
|
|
816
|
+
|
|
817
|
+
expect(syntaxErrors).toHaveLength(1);
|
|
818
|
+
expect(syntaxErrors[0].path).toEqual(["translations", "en"]);
|
|
819
|
+
expect(syntaxErrors[0].message).toContain(
|
|
820
|
+
'Invalid ICU syntax for locale "en" in message "cart.count".',
|
|
821
|
+
);
|
|
822
|
+
expect(syntaxErrors[0].message).toContain("Parser reported:");
|
|
823
|
+
});
|
|
824
|
+
});
|