@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.
Files changed (211) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +7 -0
  4. package/jest.config.js +8 -0
  5. package/lib/benchmark/index.d.ts +2 -0
  6. package/lib/benchmark/index.js +417 -0
  7. package/lib/benchmark/index.js.map +1 -0
  8. package/lib/builder/index.d.ts +70 -0
  9. package/lib/builder/index.js +831 -0
  10. package/lib/builder/index.js.map +1 -0
  11. package/lib/cli/index.d.ts +28 -0
  12. package/lib/cli/index.js +182 -0
  13. package/lib/cli/index.js.map +1 -0
  14. package/lib/config/index.d.ts +61 -0
  15. package/lib/config/index.js +255 -0
  16. package/lib/config/index.js.map +1 -0
  17. package/lib/create/index.d.ts +2 -0
  18. package/lib/create/index.js +405 -0
  19. package/lib/create/index.js.map +1 -0
  20. package/lib/datasource/filesystemAdapter.d.ts +44 -0
  21. package/lib/datasource/filesystemAdapter.js +424 -0
  22. package/lib/datasource/filesystemAdapter.js.map +1 -0
  23. package/lib/datasource/index.d.ts +39 -0
  24. package/lib/datasource/index.js +96 -0
  25. package/lib/datasource/index.js.map +1 -0
  26. package/lib/error.d.ts +6 -0
  27. package/lib/error.js +49 -0
  28. package/lib/error.js.map +1 -0
  29. package/lib/evaluate/cli.d.ts +8 -0
  30. package/lib/evaluate/cli.js +179 -0
  31. package/lib/evaluate/cli.js.map +1 -0
  32. package/lib/evaluate/index.d.ts +10 -0
  33. package/lib/evaluate/index.js +131 -0
  34. package/lib/evaluate/index.js.map +1 -0
  35. package/lib/examples/coerceExampleIsoDates.d.ts +12 -0
  36. package/lib/examples/coerceExampleIsoDates.js +81 -0
  37. package/lib/examples/coerceExampleIsoDates.js.map +1 -0
  38. package/lib/examples/index.d.ts +63 -0
  39. package/lib/examples/index.js +713 -0
  40. package/lib/examples/index.js.map +1 -0
  41. package/lib/exporter/index.d.ts +60 -0
  42. package/lib/exporter/index.js +610 -0
  43. package/lib/exporter/index.js.map +1 -0
  44. package/lib/find-duplicates/index.d.ts +41 -0
  45. package/lib/find-duplicates/index.js +297 -0
  46. package/lib/find-duplicates/index.js.map +1 -0
  47. package/lib/generate-code/index.d.ts +11 -0
  48. package/lib/generate-code/index.js +157 -0
  49. package/lib/generate-code/index.js.map +1 -0
  50. package/lib/generate-code/typescript.d.ts +14 -0
  51. package/lib/generate-code/typescript.js +307 -0
  52. package/lib/generate-code/typescript.js.map +1 -0
  53. package/lib/importer/index.d.ts +64 -0
  54. package/lib/importer/index.js +1092 -0
  55. package/lib/importer/index.js.map +1 -0
  56. package/lib/index.d.ts +18 -0
  57. package/lib/index.js +35 -0
  58. package/lib/index.js.map +1 -0
  59. package/lib/info/index.d.ts +17 -0
  60. package/lib/info/index.js +132 -0
  61. package/lib/info/index.js.map +1 -0
  62. package/lib/init/index.d.ts +30 -0
  63. package/lib/init/index.js +348 -0
  64. package/lib/init/index.js.map +1 -0
  65. package/lib/lint/index.d.ts +1 -0
  66. package/lib/lint/index.js +6 -0
  67. package/lib/lint/index.js.map +1 -0
  68. package/lib/linter/attributeSchema.d.ts +7 -0
  69. package/lib/linter/attributeSchema.js +36 -0
  70. package/lib/linter/attributeSchema.js.map +1 -0
  71. package/lib/linter/checkLocaleCircularDependency.d.ts +7 -0
  72. package/lib/linter/checkLocaleCircularDependency.js +42 -0
  73. package/lib/linter/checkLocaleCircularDependency.js.map +1 -0
  74. package/lib/linter/conditionSchema.d.ts +3 -0
  75. package/lib/linter/conditionSchema.js +283 -0
  76. package/lib/linter/conditionSchema.js.map +1 -0
  77. package/lib/linter/formatSchema.d.ts +325 -0
  78. package/lib/linter/formatSchema.js +165 -0
  79. package/lib/linter/formatSchema.js.map +1 -0
  80. package/lib/linter/icuStyleLint.d.ts +6 -0
  81. package/lib/linter/icuStyleLint.js +226 -0
  82. package/lib/linter/icuStyleLint.js.map +1 -0
  83. package/lib/linter/index.d.ts +34 -0
  84. package/lib/linter/index.js +557 -0
  85. package/lib/linter/index.js.map +1 -0
  86. package/lib/linter/localeSchema.d.ts +672 -0
  87. package/lib/linter/localeSchema.js +50 -0
  88. package/lib/linter/localeSchema.js.map +1 -0
  89. package/lib/linter/messageSchema.d.ts +35 -0
  90. package/lib/linter/messageSchema.js +115 -0
  91. package/lib/linter/messageSchema.js.map +1 -0
  92. package/lib/linter/printError.d.ts +8 -0
  93. package/lib/linter/printError.js +41 -0
  94. package/lib/linter/printError.js.map +1 -0
  95. package/lib/linter/schema.d.ts +33 -0
  96. package/lib/linter/schema.js +192 -0
  97. package/lib/linter/schema.js.map +1 -0
  98. package/lib/linter/segmentSchema.d.ts +8 -0
  99. package/lib/linter/segmentSchema.js +18 -0
  100. package/lib/linter/segmentSchema.js.map +1 -0
  101. package/lib/linter/targetSchema.d.ts +337 -0
  102. package/lib/linter/targetSchema.js +39 -0
  103. package/lib/linter/targetSchema.js.map +1 -0
  104. package/lib/linter/testSchema.d.ts +71 -0
  105. package/lib/linter/testSchema.js +165 -0
  106. package/lib/linter/testSchema.js.map +1 -0
  107. package/lib/linter/zodHelpers.d.ts +2 -0
  108. package/lib/linter/zodHelpers.js +15 -0
  109. package/lib/linter/zodHelpers.js.map +1 -0
  110. package/lib/list/index.d.ts +8 -0
  111. package/lib/list/index.js +524 -0
  112. package/lib/list/index.js.map +1 -0
  113. package/lib/matrix.d.ts +4 -0
  114. package/lib/matrix.js +66 -0
  115. package/lib/matrix.js.map +1 -0
  116. package/lib/promoter/index.d.ts +65 -0
  117. package/lib/promoter/index.js +1208 -0
  118. package/lib/promoter/index.js.map +1 -0
  119. package/lib/prune/index.d.ts +37 -0
  120. package/lib/prune/index.js +673 -0
  121. package/lib/prune/index.js.map +1 -0
  122. package/lib/sets.d.ts +10 -0
  123. package/lib/sets.js +120 -0
  124. package/lib/sets.js.map +1 -0
  125. package/lib/tester/cliFormat.d.ts +8 -0
  126. package/lib/tester/cliFormat.js +15 -0
  127. package/lib/tester/cliFormat.js.map +1 -0
  128. package/lib/tester/index.d.ts +35 -0
  129. package/lib/tester/index.js +713 -0
  130. package/lib/tester/index.js.map +1 -0
  131. package/lib/tester/matrix.d.ts +14 -0
  132. package/lib/tester/matrix.js +76 -0
  133. package/lib/tester/matrix.js.map +1 -0
  134. package/lib/tester/prettyDuration.d.ts +1 -0
  135. package/lib/tester/prettyDuration.js +30 -0
  136. package/lib/tester/prettyDuration.js.map +1 -0
  137. package/lib/tester/printTestResult.d.ts +2 -0
  138. package/lib/tester/printTestResult.js +32 -0
  139. package/lib/tester/printTestResult.js.map +1 -0
  140. package/lib/tester/types.d.ts +29 -0
  141. package/lib/tester/types.js +3 -0
  142. package/lib/tester/types.js.map +1 -0
  143. package/package.json +41 -13
  144. package/src/benchmark/index.spec.ts +375 -0
  145. package/src/benchmark/index.ts +433 -0
  146. package/src/builder/index.spec.ts +822 -0
  147. package/src/builder/index.ts +920 -0
  148. package/src/cli/index.spec.ts +54 -0
  149. package/src/cli/index.ts +150 -0
  150. package/src/config/index.spec.ts +70 -0
  151. package/src/config/index.ts +259 -0
  152. package/src/create/index.spec.ts +272 -0
  153. package/src/create/index.ts +295 -0
  154. package/src/datasource/filesystemAdapter.ts +313 -0
  155. package/src/datasource/index.ts +135 -0
  156. package/src/error.ts +33 -0
  157. package/src/evaluate/cli.spec.ts +368 -0
  158. package/src/evaluate/cli.ts +130 -0
  159. package/src/evaluate/index.ts +161 -0
  160. package/src/examples/coerceExampleIsoDates.spec.ts +81 -0
  161. package/src/examples/coerceExampleIsoDates.ts +98 -0
  162. package/src/examples/index.spec.ts +453 -0
  163. package/src/examples/index.ts +854 -0
  164. package/src/exporter/index.spec.ts +443 -0
  165. package/src/exporter/index.ts +643 -0
  166. package/src/find-duplicates/index.spec.ts +289 -0
  167. package/src/find-duplicates/index.ts +314 -0
  168. package/src/generate-code/index.ts +92 -0
  169. package/src/generate-code/typescript.spec.ts +241 -0
  170. package/src/generate-code/typescript.ts +284 -0
  171. package/src/importer/index.spec.ts +1101 -0
  172. package/src/importer/index.ts +1190 -0
  173. package/src/index.ts +18 -0
  174. package/src/info/index.ts +67 -0
  175. package/src/init/index.spec.ts +279 -0
  176. package/src/init/index.ts +292 -0
  177. package/src/lint/index.ts +1 -0
  178. package/src/linter/attributeSchema.ts +38 -0
  179. package/src/linter/checkLocaleCircularDependency.ts +51 -0
  180. package/src/linter/conditionSchema.ts +386 -0
  181. package/src/linter/formatSchema.ts +170 -0
  182. package/src/linter/icuStyleLint.ts +312 -0
  183. package/src/linter/index.spec.ts +824 -0
  184. package/src/linter/index.ts +460 -0
  185. package/src/linter/localeSchema.ts +70 -0
  186. package/src/linter/messageSchema.ts +152 -0
  187. package/src/linter/printError.ts +52 -0
  188. package/src/linter/schema.ts +230 -0
  189. package/src/linter/segmentSchema.ts +15 -0
  190. package/src/linter/targetSchema.ts +50 -0
  191. package/src/linter/testSchema.spec.ts +405 -0
  192. package/src/linter/testSchema.ts +239 -0
  193. package/src/linter/zodHelpers.ts +16 -0
  194. package/src/list/index.spec.ts +431 -0
  195. package/src/list/index.ts +463 -0
  196. package/src/matrix.ts +69 -0
  197. package/src/promoter/index.spec.ts +584 -0
  198. package/src/promoter/index.ts +1267 -0
  199. package/src/prune/index.spec.ts +418 -0
  200. package/src/prune/index.ts +693 -0
  201. package/src/sets.ts +74 -0
  202. package/src/tester/cliFormat.ts +11 -0
  203. package/src/tester/featurevisorIntegration.spec.ts +101 -0
  204. package/src/tester/index.spec.ts +577 -0
  205. package/src/tester/index.ts +679 -0
  206. package/src/tester/matrix.ts +106 -0
  207. package/src/tester/prettyDuration.ts +34 -0
  208. package/src/tester/printTestResult.ts +40 -0
  209. package/src/tester/types.ts +32 -0
  210. package/tsconfig.cjs.json +11 -0
  211. package/tsconfig.typecheck.json +4 -0
@@ -0,0 +1,405 @@
1
+ import { getTestZodSchema } from "./testSchema";
2
+
3
+ describe("getTestZodSchema", function () {
4
+ const schema = getTestZodSchema(
5
+ ["auth.signin", "common.welcome"],
6
+ ["adult", "betaUsers"],
7
+ ["en", "nl"],
8
+ ["web", "mobile"],
9
+ );
10
+
11
+ it("accepts matrix on all supported test assertion kinds", function () {
12
+ expect(() =>
13
+ schema.parse({
14
+ message: "auth.signin",
15
+ assertions: [
16
+ {
17
+ matrix: {
18
+ name: ["Ada", "Sam"],
19
+ enabled: [true],
20
+ },
21
+ locale: "en",
22
+ target: "web",
23
+ description: "Greeting ${{ name }}",
24
+ withFlags: {
25
+ "new-homepage": "${{ enabled }}",
26
+ },
27
+ values: {
28
+ name: "${{ name }}",
29
+ },
30
+ expectedTranslation: "Hello ${{ name }}",
31
+ expectedByRuntime: {
32
+ go: "Hello ${{ name }}",
33
+ },
34
+ },
35
+ ],
36
+ }),
37
+ ).not.toThrow();
38
+
39
+ expect(() =>
40
+ schema.parse({
41
+ segment: "adult",
42
+ assertions: [
43
+ {
44
+ matrix: {
45
+ expected: [true],
46
+ },
47
+ segment: "adult",
48
+ context: {
49
+ age: 21,
50
+ },
51
+ expectedToMatch: "${{ expected }}",
52
+ },
53
+ ],
54
+ }),
55
+ ).not.toThrow();
56
+
57
+ expect(() =>
58
+ schema.parse({
59
+ locale: "en",
60
+ assertions: [
61
+ {
62
+ matrix: {
63
+ target: ["web"],
64
+ amount: [12],
65
+ },
66
+ description: "Locale ${{ target }}",
67
+ target: "${{ target }}",
68
+ expectedFormats: {
69
+ number: {
70
+ money: {
71
+ currency: "USD",
72
+ },
73
+ },
74
+ },
75
+ rawMessage: "{amount, number, money}",
76
+ values: {
77
+ amount: "${{ amount }}",
78
+ },
79
+ expectedTranslation: "$12.00",
80
+ expectedByRuntime: {
81
+ go: "USD12.00",
82
+ },
83
+ },
84
+ ],
85
+ }),
86
+ ).not.toThrow();
87
+
88
+ expect(() =>
89
+ schema.parse({
90
+ target: "web",
91
+ assertions: [
92
+ {
93
+ matrix: {
94
+ currency: ["USD"],
95
+ },
96
+ locale: "en",
97
+ expectedFormats: {
98
+ number: {
99
+ money: {
100
+ currency: "${{ currency }}",
101
+ },
102
+ },
103
+ },
104
+ },
105
+ {
106
+ locale: "en",
107
+ rawMessage: "Total: {amount, number, money}",
108
+ values: {
109
+ amount: 12,
110
+ },
111
+ expectedTranslation: "Total: $12.00",
112
+ expectedByRuntime: {
113
+ go: "Total: USD12.00",
114
+ },
115
+ },
116
+ {
117
+ locale: "en",
118
+ message: "common.welcome",
119
+ expectedTranslation: "Hello Ada",
120
+ },
121
+ ],
122
+ }),
123
+ ).not.toThrow();
124
+ });
125
+
126
+ it("rejects invalid matrix values", function () {
127
+ const result = schema.safeParse({
128
+ message: "auth.signin",
129
+ assertions: [
130
+ {
131
+ matrix: {
132
+ user: {
133
+ name: "Ada",
134
+ },
135
+ },
136
+ locale: "en",
137
+ target: "web",
138
+ expectedTranslation: "Sign in",
139
+ },
140
+ ],
141
+ });
142
+
143
+ expect(result.success).toEqual(false);
144
+ const nestedIssues =
145
+ result.error.issues[0]?.code === "invalid_union"
146
+ ? result.error.issues[0].errors.flat()
147
+ : result.error.issues;
148
+ expect(
149
+ nestedIssues.some(
150
+ (issue) =>
151
+ issue.path.join(".") === "assertions.0.matrix.user" &&
152
+ issue.message.toLowerCase().includes("array"),
153
+ ),
154
+ ).toEqual(true);
155
+ });
156
+
157
+ it("requires runtime-specific expected values to be strings", function () {
158
+ const result = schema.safeParse({
159
+ message: "auth.signin",
160
+ assertions: [
161
+ {
162
+ locale: "en",
163
+ target: "web",
164
+ expectedTranslation: "Sign in",
165
+ expectedByRuntime: {
166
+ go: 12,
167
+ },
168
+ },
169
+ ],
170
+ });
171
+
172
+ expect(result.success).toEqual(false);
173
+ const nestedIssues =
174
+ result.error.issues[0]?.code === "invalid_union"
175
+ ? result.error.issues[0].errors.flat()
176
+ : result.error.issues;
177
+ expect(
178
+ nestedIssues.some(
179
+ (issue) =>
180
+ issue.path.join(".") === "assertions.0.expectedByRuntime.go" &&
181
+ issue.message.toLowerCase().includes("string"),
182
+ ),
183
+ ).toEqual(true);
184
+ });
185
+
186
+ it("keeps key-like fields strict and literal", function () {
187
+ const result = schema.safeParse({
188
+ message: "auth.signin",
189
+ assertions: [
190
+ {
191
+ matrix: {
192
+ locale: ["en"],
193
+ },
194
+ locale: "${{ locale }}",
195
+ target: "web",
196
+ expectedTranslation: "Sign in",
197
+ },
198
+ ],
199
+ });
200
+
201
+ expect(result.success).toEqual(false);
202
+ expect(
203
+ result.error.issues.some(
204
+ (issue) =>
205
+ issue.path.join(".").includes("locale") &&
206
+ issue.message.includes('Unknown locale "${{ locale }}"'),
207
+ ),
208
+ ).toEqual(true);
209
+ });
210
+
211
+ it("accepts locale assertions with only raw-message translation or with both purposes", function () {
212
+ expect(() =>
213
+ schema.parse({
214
+ locale: "en",
215
+ assertions: [
216
+ {
217
+ rawMessage: "Hello {name}",
218
+ values: {
219
+ name: "Ada",
220
+ },
221
+ expectedTranslation: "Hello Ada",
222
+ },
223
+ {
224
+ target: "web",
225
+ expectedFormats: {
226
+ number: {
227
+ money: {
228
+ currency: "USD",
229
+ },
230
+ },
231
+ },
232
+ rawMessage: "{amount, number, money}",
233
+ values: {
234
+ amount: 12,
235
+ },
236
+ expectedTranslation: "$12.00",
237
+ },
238
+ ],
239
+ }),
240
+ ).not.toThrow();
241
+ });
242
+
243
+ it("accepts target assertions with structure checks, raw translation, or message translation", function () {
244
+ expect(() =>
245
+ schema.parse({
246
+ target: "web",
247
+ assertions: [
248
+ {
249
+ locale: "en",
250
+ expectedToIncludeMessages: ["common.welcome"],
251
+ },
252
+ {
253
+ locale: "en",
254
+ rawMessage: "Hello {name}",
255
+ values: {
256
+ name: "Ada",
257
+ },
258
+ expectedTranslation: "Hello Ada",
259
+ },
260
+ {
261
+ locale: "en",
262
+ message: "common.welcome",
263
+ expectedTranslation: "Hello Ada",
264
+ },
265
+ {
266
+ locale: "en",
267
+ expectedFormats: {
268
+ number: {
269
+ money: {
270
+ currency: "USD",
271
+ },
272
+ },
273
+ },
274
+ message: "common.welcome",
275
+ expectedTranslation: "Hello Ada",
276
+ },
277
+ ],
278
+ }),
279
+ ).not.toThrow();
280
+ });
281
+
282
+ it("rejects incomplete or empty locale assertions", function () {
283
+ const missingExpectedTranslation = schema.safeParse({
284
+ locale: "en",
285
+ assertions: [
286
+ {
287
+ rawMessage: "Hello {name}",
288
+ },
289
+ ],
290
+ });
291
+
292
+ expect(missingExpectedTranslation.success).toEqual(false);
293
+ expect(
294
+ missingExpectedTranslation.error.issues.some((issue) =>
295
+ issue.message.includes("`rawMessage` and `expectedTranslation` together"),
296
+ ),
297
+ ).toEqual(true);
298
+
299
+ const missingRawMessage = schema.safeParse({
300
+ locale: "en",
301
+ assertions: [
302
+ {
303
+ expectedTranslation: "Hello Ada",
304
+ },
305
+ ],
306
+ });
307
+
308
+ expect(missingRawMessage.success).toEqual(false);
309
+ expect(
310
+ missingRawMessage.error.issues.some((issue) =>
311
+ issue.message.includes("`rawMessage` and `expectedTranslation` together"),
312
+ ),
313
+ ).toEqual(true);
314
+
315
+ const emptyAssertion = schema.safeParse({
316
+ locale: "en",
317
+ assertions: [
318
+ {
319
+ description: "No-op",
320
+ },
321
+ ],
322
+ });
323
+
324
+ expect(emptyAssertion.success).toEqual(false);
325
+ expect(
326
+ emptyAssertion.error.issues.some((issue) =>
327
+ issue.message.includes("at least one of `expectedFormats` or `rawMessage`"),
328
+ ),
329
+ ).toEqual(true);
330
+ });
331
+
332
+ it("rejects invalid target translation assertion combinations", function () {
333
+ const bothSources = schema.safeParse({
334
+ target: "web",
335
+ assertions: [
336
+ {
337
+ locale: "en",
338
+ rawMessage: "Hello {name}",
339
+ message: "common.welcome",
340
+ expectedTranslation: "Hello Ada",
341
+ },
342
+ ],
343
+ });
344
+
345
+ expect(bothSources.success).toEqual(false);
346
+ expect(
347
+ bothSources.error.issues.some((issue) =>
348
+ issue.message.includes("either `rawMessage` or `message`, not both"),
349
+ ),
350
+ ).toEqual(true);
351
+
352
+ const missingExpectedTranslation = schema.safeParse({
353
+ target: "web",
354
+ assertions: [
355
+ {
356
+ locale: "en",
357
+ rawMessage: "Hello {name}",
358
+ },
359
+ ],
360
+ });
361
+
362
+ expect(missingExpectedTranslation.success).toEqual(false);
363
+ expect(
364
+ missingExpectedTranslation.error.issues.some((issue) =>
365
+ issue.message.includes("must also define `expectedTranslation`"),
366
+ ),
367
+ ).toEqual(true);
368
+
369
+ const orphanedExpectedTranslation = schema.safeParse({
370
+ target: "web",
371
+ assertions: [
372
+ {
373
+ locale: "en",
374
+ expectedTranslation: "Hello Ada",
375
+ },
376
+ ],
377
+ });
378
+
379
+ expect(orphanedExpectedTranslation.success).toEqual(false);
380
+ expect(
381
+ orphanedExpectedTranslation.error.issues.some((issue) =>
382
+ issue.message.includes("must also define `rawMessage` or `message`"),
383
+ ),
384
+ ).toEqual(true);
385
+
386
+ const emptyAssertion = schema.safeParse({
387
+ target: "web",
388
+ assertions: [
389
+ {
390
+ locale: "en",
391
+ description: "No-op",
392
+ },
393
+ ],
394
+ });
395
+
396
+ expect(emptyAssertion.success).toEqual(false);
397
+ expect(
398
+ emptyAssertion.error.issues.some((issue) =>
399
+ issue.message.includes(
400
+ "must define inclusion/exclusion checks, `expectedFormats`, or translation",
401
+ ),
402
+ ),
403
+ ).toEqual(true);
404
+ });
405
+ });
@@ -0,0 +1,239 @@
1
+ import { z } from "zod";
2
+
3
+ import { refineWithMessage } from "./zodHelpers";
4
+
5
+ export function getTestZodSchema(
6
+ messageKeys: string[],
7
+ segmentKeys: string[],
8
+ localeKeys: string[],
9
+ targetKeys: string[],
10
+ ) {
11
+ const matrixZodSchema = z.record(
12
+ z.string(),
13
+ z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])),
14
+ );
15
+ const formatPresets = z.record(z.string(), z.unknown());
16
+ const expectedByRuntime = z.record(z.string(), z.string());
17
+ const targetOrMatrixValue = refineWithMessage(
18
+ z.string(),
19
+ (value) => targetKeys.includes(value) || /^\$\{\{.+\}\}$/.test(value),
20
+ (value) => `Unknown target "${value}"`,
21
+ );
22
+
23
+ const messageAssertion = z
24
+ .object({
25
+ matrix: matrixZodSchema.optional(),
26
+ description: z.string().optional(),
27
+ context: z.record(z.string(), z.unknown()).optional(),
28
+ locale: refineWithMessage(
29
+ z.string(),
30
+ (value) => localeKeys.includes(value),
31
+ (value) => `Unknown locale "${value}"`,
32
+ ),
33
+ target: targetOrMatrixValue.optional(),
34
+ values: z.record(z.string(), z.unknown()).optional(),
35
+ withFlags: z.record(z.string(), z.union([z.boolean(), z.string()])).optional(),
36
+ withVariations: z.record(z.string(), z.string()).optional(),
37
+ currency: z.string().optional(),
38
+ timeZone: z.string().optional(),
39
+ formats: formatPresets.optional(),
40
+ expectedTranslation: z.string(),
41
+ expectedByRuntime: expectedByRuntime.optional(),
42
+ })
43
+ .strict();
44
+
45
+ const segmentAssertion = z
46
+ .object({
47
+ matrix: matrixZodSchema.optional(),
48
+ description: z.string().optional(),
49
+ segment: refineWithMessage(
50
+ z.string(),
51
+ (value) => segmentKeys.includes(value),
52
+ (value) => `Unknown segment "${value}"`,
53
+ ),
54
+ context: z.record(z.string(), z.unknown()).optional(),
55
+ expectedToMatch: z.union([z.boolean(), z.string()]),
56
+ })
57
+ .strict();
58
+
59
+ const localeAssertion = z
60
+ .object({
61
+ matrix: matrixZodSchema.optional(),
62
+ description: z.string().optional(),
63
+ target: targetOrMatrixValue.optional(),
64
+ expectedFormats: formatPresets.optional(),
65
+ rawMessage: z.string().optional(),
66
+ expectedTranslation: z.string().optional(),
67
+ expectedByRuntime: expectedByRuntime.optional(),
68
+ values: z.record(z.string(), z.unknown()).optional(),
69
+ context: z.record(z.string(), z.unknown()).optional(),
70
+ formats: formatPresets.optional(),
71
+ currency: z.string().optional(),
72
+ timeZone: z.string().optional(),
73
+ })
74
+ .strict()
75
+ .superRefine((data, ctx) => {
76
+ const hasExpectedFormats = typeof data.expectedFormats !== "undefined";
77
+ const hasRawMessage = typeof data.rawMessage !== "undefined";
78
+ const hasExpectedTranslation = typeof data.expectedTranslation !== "undefined";
79
+
80
+ if (hasRawMessage !== hasExpectedTranslation) {
81
+ ctx.addIssue({
82
+ code: z.ZodIssueCode.custom,
83
+ message: "Locale assertions must define `rawMessage` and `expectedTranslation` together.",
84
+ path: hasRawMessage ? ["expectedTranslation"] : ["rawMessage"],
85
+ });
86
+ }
87
+
88
+ if (!hasExpectedFormats && !hasRawMessage && !hasExpectedTranslation) {
89
+ ctx.addIssue({
90
+ code: z.ZodIssueCode.custom,
91
+ message:
92
+ "Locale assertions must define at least one of `expectedFormats` or `rawMessage` with `expectedTranslation`.",
93
+ path: ["expectedFormats"],
94
+ });
95
+ }
96
+ });
97
+
98
+ const targetAssertion = z
99
+ .object({
100
+ matrix: matrixZodSchema.optional(),
101
+ description: z.string().optional(),
102
+ locale: refineWithMessage(
103
+ z.string(),
104
+ (value) => localeKeys.includes(value),
105
+ (value) => `Unknown locale "${value}"`,
106
+ ),
107
+ expectedToIncludeMessages: z
108
+ .array(
109
+ refineWithMessage(
110
+ z.string(),
111
+ (value) => messageKeys.includes(value),
112
+ (value) => `Unknown message "${value}"`,
113
+ ),
114
+ )
115
+ .optional(),
116
+ expectedToNotIncludeMessages: z
117
+ .array(
118
+ refineWithMessage(
119
+ z.string(),
120
+ (value) => messageKeys.includes(value),
121
+ (value) => `Unknown message "${value}"`,
122
+ ),
123
+ )
124
+ .optional(),
125
+ expectedFormats: formatPresets.optional(),
126
+ rawMessage: z.string().optional(),
127
+ message: refineWithMessage(
128
+ z.string(),
129
+ (value) => messageKeys.includes(value),
130
+ (value) => `Unknown message "${value}"`,
131
+ ).optional(),
132
+ expectedTranslation: z.string().optional(),
133
+ expectedByRuntime: expectedByRuntime.optional(),
134
+ values: z.record(z.string(), z.unknown()).optional(),
135
+ context: z.record(z.string(), z.unknown()).optional(),
136
+ formats: formatPresets.optional(),
137
+ currency: z.string().optional(),
138
+ timeZone: z.string().optional(),
139
+ })
140
+ .strict()
141
+ .superRefine((data, ctx) => {
142
+ const hasRawMessage = typeof data.rawMessage !== "undefined";
143
+ const hasMessage = typeof data.message !== "undefined";
144
+ const hasExpectedTranslation = typeof data.expectedTranslation !== "undefined";
145
+ const hasStructureChecks =
146
+ typeof data.expectedFormats !== "undefined" ||
147
+ typeof data.expectedToIncludeMessages !== "undefined" ||
148
+ typeof data.expectedToNotIncludeMessages !== "undefined";
149
+
150
+ if (hasRawMessage && hasMessage) {
151
+ ctx.addIssue({
152
+ code: z.ZodIssueCode.custom,
153
+ message: "Target assertions must define either `rawMessage` or `message`, not both.",
154
+ path: ["rawMessage"],
155
+ });
156
+ }
157
+
158
+ if ((hasRawMessage || hasMessage) && !hasExpectedTranslation) {
159
+ ctx.addIssue({
160
+ code: z.ZodIssueCode.custom,
161
+ message:
162
+ "Target assertions that define `rawMessage` or `message` must also define `expectedTranslation`.",
163
+ path: ["expectedTranslation"],
164
+ });
165
+ }
166
+
167
+ if (hasExpectedTranslation && !hasRawMessage && !hasMessage) {
168
+ ctx.addIssue({
169
+ code: z.ZodIssueCode.custom,
170
+ message:
171
+ "Target assertions that define `expectedTranslation` must also define `rawMessage` or `message`.",
172
+ path: ["expectedTranslation"],
173
+ });
174
+ }
175
+
176
+ if (!hasStructureChecks && !hasRawMessage && !hasMessage && !hasExpectedTranslation) {
177
+ ctx.addIssue({
178
+ code: z.ZodIssueCode.custom,
179
+ message:
180
+ "Target assertions must define inclusion/exclusion checks, `expectedFormats`, or translation.",
181
+ path: ["expectedFormats"],
182
+ });
183
+ }
184
+ });
185
+
186
+ const messageTest = z
187
+ .object({
188
+ key: z.string().optional(),
189
+ promotable: z.boolean().optional(),
190
+ message: refineWithMessage(
191
+ z.string(),
192
+ (value) => messageKeys.includes(value),
193
+ (value) => `Unknown message "${value}"`,
194
+ ),
195
+ assertions: z.array(messageAssertion).min(1),
196
+ })
197
+ .strict();
198
+
199
+ const segmentTest = z
200
+ .object({
201
+ key: z.string().optional(),
202
+ promotable: z.boolean().optional(),
203
+ segment: refineWithMessage(
204
+ z.string(),
205
+ (value) => segmentKeys.includes(value),
206
+ (value) => `Unknown segment "${value}"`,
207
+ ),
208
+ assertions: z.array(segmentAssertion).min(1),
209
+ })
210
+ .strict();
211
+
212
+ const localeTest = z
213
+ .object({
214
+ key: z.string().optional(),
215
+ promotable: z.boolean().optional(),
216
+ locale: refineWithMessage(
217
+ z.string(),
218
+ (value) => localeKeys.includes(value),
219
+ (value) => `Unknown locale "${value}"`,
220
+ ),
221
+ assertions: z.array(localeAssertion).min(1),
222
+ })
223
+ .strict();
224
+
225
+ const targetTest = z
226
+ .object({
227
+ key: z.string().optional(),
228
+ promotable: z.boolean().optional(),
229
+ target: refineWithMessage(
230
+ z.string(),
231
+ (value) => targetKeys.includes(value),
232
+ (value) => `Unknown target "${value}"`,
233
+ ),
234
+ assertions: z.array(targetAssertion).min(1),
235
+ })
236
+ .strict();
237
+
238
+ return z.union([messageTest, segmentTest, localeTest, targetTest]);
239
+ }
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+
3
+ export function refineWithMessage<T>(
4
+ schema: z.ZodType<T>,
5
+ predicate: (value: T) => boolean,
6
+ getMessage: (value: T) => string,
7
+ ) {
8
+ return schema.superRefine((value, ctx) => {
9
+ if (!predicate(value)) {
10
+ ctx.addIssue({
11
+ code: z.ZodIssueCode.custom,
12
+ message: getMessage(value),
13
+ });
14
+ }
15
+ });
16
+ }