@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,81 @@
1
+ import { coerceExampleValuesIsoDates } from "./coerceExampleIsoDates";
2
+
3
+ describe("coerceExampleValuesIsoDates", function () {
4
+ it("converts ISO 8601 date and datetime strings to Date", function () {
5
+ const out = coerceExampleValuesIsoDates({
6
+ d: "2026-11-26",
7
+ when: "2026-11-26T15:05:00.000Z",
8
+ deadline: "2026-12-24T23:59:00.000Z",
9
+ event: "2026-06-01T14:00:00.000Z",
10
+ });
11
+
12
+ expect(out?.d).toBeInstanceOf(Date);
13
+ expect((out?.d as Date).toISOString().slice(0, 10)).toEqual("2026-11-26");
14
+ expect(out?.when).toBeInstanceOf(Date);
15
+ expect((out?.when as Date).toISOString()).toEqual("2026-11-26T15:05:00.000Z");
16
+ expect(out?.deadline).toBeInstanceOf(Date);
17
+ expect(out?.event).toBeInstanceOf(Date);
18
+ });
19
+
20
+ it("accepts flexible ISO-like forms without fractional seconds and with space / lowercase", function () {
21
+ const out = coerceExampleValuesIsoDates({
22
+ noMsZ: "2026-07-04T12:30:00Z",
23
+ noMsLowerZ: "2026-07-04T12:30:00z",
24
+ hourMinuteZ: "2026-07-04T12:30Z",
25
+ spaceSep: "2026-07-04 12:30:45",
26
+ offsetNoMs: "2026-07-04T12:30:00+02:00",
27
+ offsetShort: "2026-07-04T12:30:00+02",
28
+ trimmed: " 2026-07-04T00:00:00Z ",
29
+ });
30
+
31
+ expect(out?.noMsZ).toBeInstanceOf(Date);
32
+ expect((out?.noMsZ as Date).toISOString()).toEqual("2026-07-04T12:30:00.000Z");
33
+ expect(out?.noMsLowerZ).toBeInstanceOf(Date);
34
+ expect((out?.noMsLowerZ as Date).toISOString()).toEqual("2026-07-04T12:30:00.000Z");
35
+ expect(out?.hourMinuteZ).toBeInstanceOf(Date);
36
+ expect(out?.spaceSep).toBeInstanceOf(Date);
37
+ expect(out?.offsetNoMs).toBeInstanceOf(Date);
38
+ expect(out?.offsetShort).toBeInstanceOf(Date);
39
+ expect(out?.trimmed).toBeInstanceOf(Date);
40
+ expect((out?.trimmed as Date).toISOString()).toEqual("2026-07-04T00:00:00.000Z");
41
+ });
42
+
43
+ it("accepts comma as fractional-second separator", function () {
44
+ const out = coerceExampleValuesIsoDates({
45
+ t: "2026-01-02T03:04:05,678Z",
46
+ });
47
+
48
+ expect(out?.t).toBeInstanceOf(Date);
49
+ expect((out?.t as Date).toISOString()).toEqual("2026-01-02T03:04:05.678Z");
50
+ });
51
+
52
+ it("does not convert non-ISO strings or numbers", function () {
53
+ const out = coerceExampleValuesIsoDates({
54
+ name: "Ada",
55
+ amount: 10,
56
+ loose: "12/25/2026",
57
+ yearOnly: "2026",
58
+ });
59
+
60
+ expect(out?.name).toEqual("Ada");
61
+ expect(out?.amount).toEqual(10);
62
+ expect(out?.loose).toEqual("12/25/2026");
63
+ expect(out?.yearOnly).toEqual("2026");
64
+ });
65
+
66
+ it("walks nested objects and arrays", function () {
67
+ const out = coerceExampleValuesIsoDates({
68
+ range: { start: "2026-01-01T00:00:00.000Z", end: "2026-01-02T00:00:00.000Z" },
69
+ slots: ["2026-06-01T10:00:00Z", "plain"],
70
+ });
71
+
72
+ expect((out?.range as Record<string, unknown>).start).toBeInstanceOf(Date);
73
+ expect((out?.range as Record<string, unknown>).end).toBeInstanceOf(Date);
74
+ expect((out?.slots as unknown[])[0]).toBeInstanceOf(Date);
75
+ expect((out?.slots as unknown[])[1]).toEqual("plain");
76
+ });
77
+
78
+ it("returns undefined for undefined input", function () {
79
+ expect(coerceExampleValuesIsoDates(undefined)).toBeUndefined();
80
+ });
81
+ });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Coerce ISO 8601-like strings in example `values` to `Date` for CLI evaluation (ICU date/time formatters).
3
+ *
4
+ * Accepted shapes (non-exhaustive; `Date.parse` must still succeed):
5
+ * - Calendar: `YYYY-MM-DD`
6
+ * - Datetime: `T` or space between date and time; `t` / `z` case-insensitive where applicable
7
+ * - Time: `HH:MM` or `HH:MM:SS`, optional fractional seconds (`.` or `,`), optional `Z` or numeric offset (`±HH`, `±HH:MM`, `±HHMM`)
8
+ */
9
+
10
+ const ISO_8601_LIKE =
11
+ /^\d{4}-\d{2}-\d{2}(?:[Tt ]\d{2}:\d{2}(?::\d{2})?(?:[.,]\d{1,9})?(?:[Zz]|[+-]\d{2}(?::?\d{2})?)?)?$/;
12
+
13
+ function isIso8601TimestampString(value: string): boolean {
14
+ const trimmed = value.trim();
15
+ if (!ISO_8601_LIKE.test(trimmed)) {
16
+ return false;
17
+ }
18
+
19
+ return !Number.isNaN(Date.parse(coerceParseableIsoLike(trimmed)));
20
+ }
21
+
22
+ /**
23
+ * Normalize a few ISO variants so `Date.parse` behaves consistently (comma decimals, lowercase z).
24
+ */
25
+ function normalizeIsoLikeForParse(value: string): string {
26
+ let s = value.trim();
27
+ if (s.includes(",") && /\d{2}:\d{2}(?::\d{2})?,/.test(s)) {
28
+ s = s.replace(",", ".");
29
+ }
30
+ return s;
31
+ }
32
+
33
+ /**
34
+ * If `Date.parse` rejects a valid-looking ISO string, apply small normalizations (hour-only offset).
35
+ */
36
+ function coerceParseableIsoLike(trimmed: string): string {
37
+ const commaNormalized = normalizeIsoLikeForParse(trimmed);
38
+ if (!Number.isNaN(Date.parse(commaNormalized))) {
39
+ return commaNormalized;
40
+ }
41
+
42
+ const s = commaNormalized;
43
+ if (/[+-]\d{2}:\d{2}$/.test(s) || /[+-]\d{4}$/.test(s)) {
44
+ return s;
45
+ }
46
+
47
+ const hourOnlyOffset = s.match(/^(.+)([+-]\d{2})$/);
48
+ if (hourOnlyOffset) {
49
+ const withMinutes = `${hourOnlyOffset[1]}${hourOnlyOffset[2]}:00`;
50
+ if (!Number.isNaN(Date.parse(withMinutes))) {
51
+ return withMinutes;
52
+ }
53
+ }
54
+
55
+ return s;
56
+ }
57
+
58
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
59
+ return (
60
+ typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date)
61
+ );
62
+ }
63
+
64
+ function coerceLeaf(value: unknown): unknown {
65
+ if (typeof value === "string" && isIso8601TimestampString(value)) {
66
+ const trimmed = value.trim();
67
+ return new Date(Date.parse(coerceParseableIsoLike(trimmed)));
68
+ }
69
+
70
+ if (isPlainObject(value)) {
71
+ return coerceExampleValuesIsoDates(value);
72
+ }
73
+
74
+ if (Array.isArray(value)) {
75
+ return value.map((entry) => coerceLeaf(entry));
76
+ }
77
+
78
+ return value;
79
+ }
80
+
81
+ /**
82
+ * Returns a shallow copy with coerced values (nested plain objects and arrays are walked).
83
+ */
84
+ export function coerceExampleValuesIsoDates(
85
+ values: Record<string, unknown> | undefined,
86
+ ): Record<string, unknown> | undefined {
87
+ if (typeof values === "undefined") {
88
+ return undefined;
89
+ }
90
+
91
+ const result: Record<string, unknown> = {};
92
+
93
+ for (const key of Object.keys(values)) {
94
+ result[key] = coerceLeaf(values[key]);
95
+ }
96
+
97
+ return result;
98
+ }
@@ -0,0 +1,453 @@
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 { examplesPlugin, resolveExamples } 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
+ async function createProject(configContent = "module.exports = {};\n") {
16
+ const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-examples-"));
17
+
18
+ await writeFile(root, "messagevisor.config.js", configContent);
19
+ await writeFile(
20
+ root,
21
+ "locales/en.yml",
22
+ [
23
+ "description: English",
24
+ "formats:",
25
+ " number:",
26
+ " decimal:",
27
+ " minimumFractionDigits: 2",
28
+ " maximumFractionDigits: 2",
29
+ "examples:",
30
+ " - description: Greeting raw",
31
+ " rawMessage: Hello {name}",
32
+ " values:",
33
+ " name: Ada",
34
+ " formats:",
35
+ " number:",
36
+ " adHoc:",
37
+ " maximumFractionDigits: 1",
38
+ " - matrix:",
39
+ " name: [John, Jane]",
40
+ " age: [30, 25]",
41
+ " description: Matrix ${{ name }}",
42
+ " rawMessage: Hello {name}! You are {age} years old.",
43
+ " values:",
44
+ " name: ${{ name }}",
45
+ " age: ${{ age }}",
46
+ " - description: Referenced message",
47
+ " message: auth.signin",
48
+ " context:",
49
+ " age: 21",
50
+ "",
51
+ ].join("\n"),
52
+ );
53
+ await writeFile(
54
+ root,
55
+ "locales/en-US.yml",
56
+ [
57
+ "description: English US",
58
+ "inheritFormatsFrom: en",
59
+ "inheritTranslationsFrom: en",
60
+ "mergeExamplesFrom: en",
61
+ "examples:",
62
+ " - description: Local US example",
63
+ " rawMessage: Welcome {name}",
64
+ " values:",
65
+ " name: Sam",
66
+ "",
67
+ ].join("\n"),
68
+ );
69
+ await writeFile(
70
+ root,
71
+ "messages/auth/signin.yml",
72
+ [
73
+ "description: Sign in",
74
+ "examples:",
75
+ " - description: Default signin",
76
+ " locale: en",
77
+ " - matrix:",
78
+ " locale: [en, en-US]",
79
+ " age: [17, 21]",
80
+ " description: Signin for ${{ locale }} age ${{ age }}",
81
+ " locale: ${{ locale }}",
82
+ " context:",
83
+ " age: ${{ age }}",
84
+ "translations:",
85
+ " en: Sign in",
86
+ "overrides:",
87
+ " - key: adult",
88
+ " segments: adult",
89
+ " translations:",
90
+ " en: Adult sign in",
91
+ "",
92
+ ].join("\n"),
93
+ );
94
+ await writeFile(
95
+ root,
96
+ "segments/adult.yml",
97
+ [
98
+ "description: Adult",
99
+ "conditions:",
100
+ " - attribute: age",
101
+ " operator: greaterThanOrEquals",
102
+ " value: 18",
103
+ "",
104
+ ].join("\n"),
105
+ );
106
+
107
+ return root;
108
+ }
109
+
110
+ async function createSetsProject() {
111
+ const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "messagevisor-examples-sets-"));
112
+ await writeFile(root, "messagevisor.config.js", "module.exports = { sets: true };\n");
113
+
114
+ for (const set of ["dev", "production"]) {
115
+ await writeFile(
116
+ root,
117
+ `sets/${set}/locales/en.yml`,
118
+ [
119
+ "description: English",
120
+ "examples:",
121
+ ` - description: ${set} raw`,
122
+ ` rawMessage: Hello from ${set}`,
123
+ "",
124
+ ].join("\n"),
125
+ );
126
+ }
127
+
128
+ return root;
129
+ }
130
+
131
+ function getDatasource(root: string) {
132
+ const projectConfig = getProjectConfig(root);
133
+ const datasource = new Datasource(projectConfig, root);
134
+
135
+ return { projectConfig, datasource };
136
+ }
137
+
138
+ describe("examplesPlugin", function () {
139
+ it("lists rich examples in JSON and evaluates raw and message examples", async function () {
140
+ const interpolationModulePath = path.join(
141
+ path.resolve(__dirname, "../../../.."),
142
+ "packages/module-interpolation/src/index.ts",
143
+ );
144
+ const root = await createProject(
145
+ [
146
+ `const { createInterpolationModule } = require(${JSON.stringify(interpolationModulePath)});`,
147
+ "module.exports = {",
148
+ " modules: [createInterpolationModule()],",
149
+ "};",
150
+ "",
151
+ ].join("\n"),
152
+ );
153
+ const { projectConfig, datasource } = getDatasource(root);
154
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
155
+
156
+ await examplesPlugin.handler({
157
+ projectConfig,
158
+ datasource,
159
+ parsed: { json: true, pretty: true },
160
+ } as any);
161
+
162
+ const result = JSON.parse(String(logSpy.mock.calls[0][0]));
163
+ expect(Array.isArray(result.locales)).toEqual(true);
164
+ expect(Array.isArray(result.messages)).toEqual(true);
165
+ expect(
166
+ result.locales.some(
167
+ (entry: any) =>
168
+ entry.rawMessage === "Hello {name}" && entry.evaluatedTranslation === "Hello Ada",
169
+ ),
170
+ ).toEqual(true);
171
+ expect(
172
+ result.locales.some(
173
+ (entry: any) =>
174
+ entry.message === "auth.signin" &&
175
+ entry.evaluatedTranslation === "Adult sign in" &&
176
+ entry.sourceLocale === "en",
177
+ ),
178
+ ).toEqual(true);
179
+ expect(
180
+ result.locales.filter(
181
+ (entry: any) => entry.description && entry.description.startsWith("Matrix "),
182
+ ),
183
+ ).toHaveLength(8);
184
+ expect(
185
+ result.locales.some(
186
+ (entry: any) =>
187
+ entry.locale === "en-US" &&
188
+ entry.sourceLocale === "en" &&
189
+ entry.description === "Matrix John",
190
+ ),
191
+ ).toEqual(true);
192
+ expect(
193
+ result.messages.some(
194
+ (entry: any) =>
195
+ entry.message === "auth.signin" &&
196
+ entry.locale === "en" &&
197
+ entry.description === "Default signin" &&
198
+ entry.evaluatedTranslation === "Sign in",
199
+ ),
200
+ ).toEqual(true);
201
+ expect(
202
+ result.messages.some(
203
+ (entry: any) =>
204
+ entry.message === "auth.signin" &&
205
+ entry.locale === "en-US" &&
206
+ entry.description === "Signin for en-US age 21" &&
207
+ entry.evaluatedTranslation === "Adult sign in",
208
+ ),
209
+ ).toEqual(true);
210
+ logSpy.mockRestore();
211
+ });
212
+
213
+ it("merges inherited examples before local examples for a locale", async function () {
214
+ const root = await createProject();
215
+ const { projectConfig, datasource } = getDatasource(root);
216
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
217
+
218
+ await examplesPlugin.handler({
219
+ projectConfig,
220
+ datasource,
221
+ parsed: { locale: "en-US", json: true },
222
+ } as any);
223
+
224
+ const result = JSON.parse(String(logSpy.mock.calls[0][0]));
225
+ expect(result.locales[0].sourceLocale).toEqual("en");
226
+ expect(result.locales[result.locales.length - 1].sourceLocale).toEqual("en-US");
227
+ logSpy.mockRestore();
228
+ });
229
+
230
+ it("can include evaluation input for external runtime conformance", async function () {
231
+ const root = await createProject();
232
+ const { projectConfig, datasource } = getDatasource(root);
233
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
234
+
235
+ await examplesPlugin.handler({
236
+ projectConfig,
237
+ datasource,
238
+ parsed: { json: true, includeEvaluationInput: true },
239
+ } as any);
240
+
241
+ const result = JSON.parse(String(logSpy.mock.calls[0][0]));
242
+ const rawExample = result.locales.find((entry: any) => entry.description === "Greeting raw");
243
+ const localeMessageExample = result.locales.find(
244
+ (entry: any) => entry.description === "Referenced message",
245
+ );
246
+ const messageExample = result.messages.find(
247
+ (entry: any) => entry.description === "Default signin",
248
+ );
249
+
250
+ expect(rawExample.evaluationInput.defaultFormats.en.number.decimal).toEqual({
251
+ minimumFractionDigits: 2,
252
+ maximumFractionDigits: 2,
253
+ });
254
+ expect(rawExample.evaluationInput.formats.number.adHoc).toEqual({
255
+ maximumFractionDigits: 1,
256
+ });
257
+ expect(localeMessageExample.evaluationInput.datafile.messages["auth.signin"]).toBeDefined();
258
+ expect(Object.keys(localeMessageExample.evaluationInput.datafile.messages)).toEqual([
259
+ "auth.signin",
260
+ ]);
261
+ expect(messageExample.evaluationInput.datafile.messages["auth.signin"]).toBeDefined();
262
+
263
+ logSpy.mockRestore();
264
+ });
265
+
266
+ it("lists all sets by default and supports set filtering", async function () {
267
+ const root = await createSetsProject();
268
+ const { projectConfig, datasource } = getDatasource(root);
269
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
270
+
271
+ await examplesPlugin.handler({
272
+ projectConfig,
273
+ datasource,
274
+ parsed: { json: true },
275
+ } as any);
276
+
277
+ const allResults = JSON.parse(String(logSpy.mock.calls[0][0]));
278
+ expect(allResults.locales.map((entry: any) => entry.set).sort()).toEqual(["dev", "production"]);
279
+ expect(allResults.messages).toEqual([]);
280
+
281
+ logSpy.mockClear();
282
+
283
+ await examplesPlugin.handler({
284
+ projectConfig,
285
+ datasource,
286
+ parsed: { set: "dev", json: true },
287
+ } as any);
288
+
289
+ const devResults = JSON.parse(String(logSpy.mock.calls[0][0]));
290
+ expect(devResults.locales).toHaveLength(1);
291
+ expect(devResults.locales[0].set).toEqual("dev");
292
+ logSpy.mockRestore();
293
+ });
294
+
295
+ it("supports example and matrix index filtering", async function () {
296
+ const root = await createProject();
297
+ const { projectConfig, datasource } = getDatasource(root);
298
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
299
+
300
+ await examplesPlugin.handler({
301
+ projectConfig,
302
+ datasource,
303
+ parsed: { exampleIndex: 2, matrixIndex: 2, json: true },
304
+ } as any);
305
+
306
+ const result = JSON.parse(String(logSpy.mock.calls[0][0]));
307
+ expect(result.locales).toHaveLength(2);
308
+ expect(
309
+ result.locales.every((entry: any) => entry.exampleIndex === 1 && entry.matrixIndex === 1),
310
+ ).toEqual(true);
311
+ expect(result.locales.every((entry: any) => entry.description === "Matrix John")).toEqual(true);
312
+ expect(result.messages).toHaveLength(1);
313
+ expect(result.messages[0].description).toEqual("Signin for en age 21");
314
+ logSpy.mockRestore();
315
+ });
316
+
317
+ it("supports description and evaluated translation pattern filtering", async function () {
318
+ const root = await createProject();
319
+ const { projectConfig, datasource } = getDatasource(root);
320
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
321
+
322
+ await examplesPlugin.handler({
323
+ projectConfig,
324
+ datasource,
325
+ parsed: { locale: "en-US", descriptionPattern: "local us", json: true },
326
+ } as any);
327
+
328
+ const descriptionResult = JSON.parse(String(logSpy.mock.calls[0][0]));
329
+ expect(descriptionResult.locales).toHaveLength(1);
330
+ expect(descriptionResult.locales[0].description).toEqual("Local US example");
331
+ expect(descriptionResult.messages).toEqual([]);
332
+
333
+ logSpy.mockClear();
334
+
335
+ await examplesPlugin.handler({
336
+ projectConfig,
337
+ datasource,
338
+ parsed: { locale: "en-US", translationPattern: "adult sign in", json: true },
339
+ } as any);
340
+
341
+ const translationResult = JSON.parse(String(logSpy.mock.calls[0][0]));
342
+ expect(translationResult.locales).toHaveLength(1);
343
+ expect(translationResult.locales[0].evaluatedTranslation).toEqual("Adult sign in");
344
+ expect(translationResult.messages).toHaveLength(1);
345
+ expect(translationResult.messages[0].evaluatedTranslation).toEqual("Adult sign in");
346
+ logSpy.mockRestore();
347
+ });
348
+
349
+ it("supports message example filtering by example and matrix index", async function () {
350
+ const root = await createProject();
351
+ const { projectConfig, datasource } = getDatasource(root);
352
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
353
+
354
+ await examplesPlugin.handler({
355
+ projectConfig,
356
+ datasource,
357
+ parsed: { locale: "en-US", exampleIndex: 2, matrixIndex: 4, json: true },
358
+ } as any);
359
+
360
+ const result = JSON.parse(String(logSpy.mock.calls[0][0]));
361
+ expect(result.locales).toHaveLength(1);
362
+ expect(result.locales.every((entry: any) => entry.description === "Matrix Jane")).toEqual(true);
363
+ expect(result.messages).toHaveLength(1);
364
+ expect(result.messages[0].description).toEqual("Signin for en-US age 21");
365
+ logSpy.mockRestore();
366
+ });
367
+
368
+ it("supports resolving examples for a specific message", async function () {
369
+ const root = await createProject();
370
+ const { projectConfig, datasource } = getDatasource(root);
371
+
372
+ const result = await resolveExamples(projectConfig, datasource, {
373
+ onlyMessages: true,
374
+ message: "auth.signin",
375
+ });
376
+
377
+ expect(result.locales).toEqual([]);
378
+ expect(result.messages.length).toBeGreaterThan(0);
379
+ expect(result.messages.every((entry) => entry.message === "auth.signin")).toEqual(true);
380
+ });
381
+
382
+ it("supports onlyMessages and onlyLocales output selection", async function () {
383
+ const root = await createProject();
384
+ const { projectConfig, datasource } = getDatasource(root);
385
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
386
+
387
+ await examplesPlugin.handler({
388
+ projectConfig,
389
+ datasource,
390
+ parsed: { onlyMessages: true, json: true },
391
+ } as any);
392
+
393
+ const onlyMessagesResult = JSON.parse(String(logSpy.mock.calls[0][0]));
394
+ expect(onlyMessagesResult.locales).toEqual([]);
395
+ expect(onlyMessagesResult.messages.length).toBeGreaterThan(0);
396
+
397
+ logSpy.mockClear();
398
+
399
+ await examplesPlugin.handler({
400
+ projectConfig,
401
+ datasource,
402
+ parsed: { onlyLocales: true, json: true },
403
+ } as any);
404
+
405
+ const onlyLocalesResult = JSON.parse(String(logSpy.mock.calls[0][0]));
406
+ expect(onlyLocalesResult.locales.length).toBeGreaterThan(0);
407
+ expect(onlyLocalesResult.messages).toEqual([]);
408
+ logSpy.mockRestore();
409
+ });
410
+
411
+ it("fails when onlyMessages and onlyLocales are both requested", async function () {
412
+ const root = await createProject();
413
+ const { projectConfig, datasource } = getDatasource(root);
414
+ const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
415
+
416
+ try {
417
+ await expect(
418
+ examplesPlugin.handler({
419
+ projectConfig,
420
+ datasource,
421
+ parsed: { onlyMessages: true, onlyLocales: true },
422
+ } as any),
423
+ ).resolves.toEqual(false);
424
+
425
+ expect(errorSpy).toHaveBeenCalledWith(
426
+ "Pass either --onlyLocales or --onlyMessages, not both.",
427
+ );
428
+ } finally {
429
+ errorSpy.mockRestore();
430
+ }
431
+ });
432
+
433
+ it("prints grouped plain output", async function () {
434
+ const root = await createProject();
435
+ const { projectConfig, datasource } = getDatasource(root);
436
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
437
+
438
+ await examplesPlugin.handler({
439
+ projectConfig,
440
+ datasource,
441
+ parsed: {},
442
+ } as any);
443
+
444
+ const output = logSpy.mock.calls.map((call) => String(call[0])).join("\n");
445
+ expect(output).toContain("Locales");
446
+ expect(output).toContain("Messages");
447
+ expect(output).toContain('Locale "en":');
448
+ expect(output).toContain('Message "auth.signin":');
449
+ expect(output).toContain("Evaluated translation:");
450
+ expect(output).toContain("Found ");
451
+ logSpy.mockRestore();
452
+ });
453
+ });