@inlang/sdk 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 (170) hide show
  1. package/README.md +25 -0
  2. package/dist/adapter/solidAdapter.d.ts +32 -0
  3. package/dist/adapter/solidAdapter.d.ts.map +1 -0
  4. package/dist/adapter/solidAdapter.js +39 -0
  5. package/dist/adapter/solidAdapter.test.d.ts +2 -0
  6. package/dist/adapter/solidAdapter.test.d.ts.map +1 -0
  7. package/dist/adapter/solidAdapter.test.js +284 -0
  8. package/dist/api.d.ts +88 -0
  9. package/dist/api.d.ts.map +1 -0
  10. package/dist/api.js +1 -0
  11. package/dist/createMessageLintReportsQuery.d.ts +9 -0
  12. package/dist/createMessageLintReportsQuery.d.ts.map +1 -0
  13. package/dist/createMessageLintReportsQuery.js +48 -0
  14. package/dist/createMessagesQuery.d.ts +7 -0
  15. package/dist/createMessagesQuery.d.ts.map +1 -0
  16. package/dist/createMessagesQuery.js +57 -0
  17. package/dist/createMessagesQuery.test.d.ts +2 -0
  18. package/dist/createMessagesQuery.test.d.ts.map +1 -0
  19. package/dist/createMessagesQuery.test.js +304 -0
  20. package/dist/errors.d.ts +22 -0
  21. package/dist/errors.d.ts.map +1 -0
  22. package/dist/errors.js +39 -0
  23. package/dist/index.d.ts +15 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +13 -0
  26. package/dist/lint/index.d.ts +3 -0
  27. package/dist/lint/index.d.ts.map +1 -0
  28. package/dist/lint/index.js +2 -0
  29. package/dist/lint/message/errors.d.ts +7 -0
  30. package/dist/lint/message/errors.d.ts.map +1 -0
  31. package/dist/lint/message/errors.js +9 -0
  32. package/dist/lint/message/lintMessages.d.ts +17 -0
  33. package/dist/lint/message/lintMessages.d.ts.map +1 -0
  34. package/dist/lint/message/lintMessages.js +12 -0
  35. package/dist/lint/message/lintMessages.test.d.ts +2 -0
  36. package/dist/lint/message/lintMessages.test.d.ts.map +1 -0
  37. package/dist/lint/message/lintMessages.test.js +105 -0
  38. package/dist/lint/message/lintSingleMessage.d.ts +23 -0
  39. package/dist/lint/message/lintSingleMessage.d.ts.map +1 -0
  40. package/dist/lint/message/lintSingleMessage.js +36 -0
  41. package/dist/lint/message/lintSingleMessage.test.d.ts +2 -0
  42. package/dist/lint/message/lintSingleMessage.test.d.ts.map +1 -0
  43. package/dist/lint/message/lintSingleMessage.test.js +155 -0
  44. package/dist/messages/errors.d.ts +13 -0
  45. package/dist/messages/errors.d.ts.map +1 -0
  46. package/dist/messages/errors.js +18 -0
  47. package/dist/messages/index.d.ts +3 -0
  48. package/dist/messages/index.d.ts.map +1 -0
  49. package/dist/messages/index.js +2 -0
  50. package/dist/messages/variant.d.ts +46 -0
  51. package/dist/messages/variant.d.ts.map +1 -0
  52. package/dist/messages/variant.js +177 -0
  53. package/dist/messages/variant.test.d.ts +2 -0
  54. package/dist/messages/variant.test.d.ts.map +1 -0
  55. package/dist/messages/variant.test.js +379 -0
  56. package/dist/openInlangProject.d.ts +18 -0
  57. package/dist/openInlangProject.d.ts.map +1 -0
  58. package/dist/openInlangProject.js +226 -0
  59. package/dist/openInlangProject.test.d.ts +2 -0
  60. package/dist/openInlangProject.test.d.ts.map +1 -0
  61. package/dist/openInlangProject.test.js +627 -0
  62. package/dist/parseConfig.d.ts +8 -0
  63. package/dist/parseConfig.d.ts.map +1 -0
  64. package/dist/parseConfig.js +26 -0
  65. package/dist/reactivity/map.d.ts +66 -0
  66. package/dist/reactivity/map.d.ts.map +1 -0
  67. package/dist/reactivity/map.js +143 -0
  68. package/dist/reactivity/solid.d.ts +12 -0
  69. package/dist/reactivity/solid.d.ts.map +1 -0
  70. package/dist/reactivity/solid.js +13 -0
  71. package/dist/reactivity/trigger.d.ts +11 -0
  72. package/dist/reactivity/trigger.d.ts.map +1 -0
  73. package/dist/reactivity/trigger.js +46 -0
  74. package/dist/resolve-modules/errors.d.ts +34 -0
  75. package/dist/resolve-modules/errors.d.ts.map +1 -0
  76. package/dist/resolve-modules/errors.js +35 -0
  77. package/dist/resolve-modules/import.d.ts +35 -0
  78. package/dist/resolve-modules/import.d.ts.map +1 -0
  79. package/dist/resolve-modules/import.js +40 -0
  80. package/dist/resolve-modules/import.test.d.ts +2 -0
  81. package/dist/resolve-modules/import.test.d.ts.map +1 -0
  82. package/dist/resolve-modules/import.test.js +45 -0
  83. package/dist/resolve-modules/index.d.ts +3 -0
  84. package/dist/resolve-modules/index.d.ts.map +1 -0
  85. package/dist/resolve-modules/index.js +2 -0
  86. package/dist/resolve-modules/message-lint-rules/errors.d.ts +8 -0
  87. package/dist/resolve-modules/message-lint-rules/errors.d.ts.map +1 -0
  88. package/dist/resolve-modules/message-lint-rules/errors.js +8 -0
  89. package/dist/resolve-modules/message-lint-rules/resolveMessageLintRules.d.ts +9 -0
  90. package/dist/resolve-modules/message-lint-rules/resolveMessageLintRules.d.ts.map +1 -0
  91. package/dist/resolve-modules/message-lint-rules/resolveMessageLintRules.js +21 -0
  92. package/dist/resolve-modules/plugins/errors.d.ts +28 -0
  93. package/dist/resolve-modules/plugins/errors.d.ts.map +1 -0
  94. package/dist/resolve-modules/plugins/errors.js +44 -0
  95. package/dist/resolve-modules/plugins/resolvePlugins.d.ts +3 -0
  96. package/dist/resolve-modules/plugins/resolvePlugins.d.ts.map +1 -0
  97. package/dist/resolve-modules/plugins/resolvePlugins.js +108 -0
  98. package/dist/resolve-modules/plugins/resolvePlugins.test.d.ts +2 -0
  99. package/dist/resolve-modules/plugins/resolvePlugins.test.d.ts.map +1 -0
  100. package/dist/resolve-modules/plugins/resolvePlugins.test.js +289 -0
  101. package/dist/resolve-modules/plugins/types.d.ts +60 -0
  102. package/dist/resolve-modules/plugins/types.d.ts.map +1 -0
  103. package/dist/resolve-modules/plugins/types.js +1 -0
  104. package/dist/resolve-modules/plugins/types.test.d.ts +2 -0
  105. package/dist/resolve-modules/plugins/types.test.d.ts.map +1 -0
  106. package/dist/resolve-modules/plugins/types.test.js +49 -0
  107. package/dist/resolve-modules/resolveModules.d.ts +3 -0
  108. package/dist/resolve-modules/resolveModules.d.ts.map +1 -0
  109. package/dist/resolve-modules/resolveModules.js +70 -0
  110. package/dist/resolve-modules/resolveModules.test.d.ts +2 -0
  111. package/dist/resolve-modules/resolveModules.test.d.ts.map +1 -0
  112. package/dist/resolve-modules/resolveModules.test.js +143 -0
  113. package/dist/resolve-modules/types.d.ts +62 -0
  114. package/dist/resolve-modules/types.d.ts.map +1 -0
  115. package/dist/resolve-modules/types.js +1 -0
  116. package/dist/test-utilities/createMessage.d.ts +17 -0
  117. package/dist/test-utilities/createMessage.d.ts.map +1 -0
  118. package/dist/test-utilities/createMessage.js +16 -0
  119. package/dist/test-utilities/createMessage.test.d.ts +2 -0
  120. package/dist/test-utilities/createMessage.test.d.ts.map +1 -0
  121. package/dist/test-utilities/createMessage.test.js +91 -0
  122. package/dist/test-utilities/index.d.ts +2 -0
  123. package/dist/test-utilities/index.d.ts.map +1 -0
  124. package/dist/test-utilities/index.js +1 -0
  125. package/dist/versionedInterfaces.d.ts +8 -0
  126. package/dist/versionedInterfaces.d.ts.map +1 -0
  127. package/dist/versionedInterfaces.js +8 -0
  128. package/package.json +58 -0
  129. package/src/adapter/solidAdapter.test.ts +363 -0
  130. package/src/adapter/solidAdapter.ts +77 -0
  131. package/src/api.ts +86 -0
  132. package/src/createMessageLintReportsQuery.ts +77 -0
  133. package/src/createMessagesQuery.test.ts +435 -0
  134. package/src/createMessagesQuery.ts +64 -0
  135. package/src/errors.ts +46 -0
  136. package/src/index.ts +29 -0
  137. package/src/lint/index.ts +2 -0
  138. package/src/lint/message/errors.ts +9 -0
  139. package/src/lint/message/lintMessages.test.ts +122 -0
  140. package/src/lint/message/lintMessages.ts +33 -0
  141. package/src/lint/message/lintSingleMessage.test.ts +183 -0
  142. package/src/lint/message/lintSingleMessage.ts +62 -0
  143. package/src/messages/errors.ts +25 -0
  144. package/src/messages/index.ts +2 -0
  145. package/src/messages/variant.test.ts +444 -0
  146. package/src/messages/variant.ts +242 -0
  147. package/src/openInlangProject.test.ts +734 -0
  148. package/src/openInlangProject.ts +337 -0
  149. package/src/parseConfig.ts +33 -0
  150. package/src/reactivity/map.ts +135 -0
  151. package/src/reactivity/solid.ts +36 -0
  152. package/src/reactivity/trigger.ts +46 -0
  153. package/src/resolve-modules/errors.ts +39 -0
  154. package/src/resolve-modules/import.test.ts +58 -0
  155. package/src/resolve-modules/import.ts +69 -0
  156. package/src/resolve-modules/index.ts +2 -0
  157. package/src/resolve-modules/message-lint-rules/errors.ts +9 -0
  158. package/src/resolve-modules/message-lint-rules/resolveMessageLintRules.ts +24 -0
  159. package/src/resolve-modules/plugins/errors.ts +57 -0
  160. package/src/resolve-modules/plugins/resolvePlugins.test.ts +340 -0
  161. package/src/resolve-modules/plugins/resolvePlugins.ts +170 -0
  162. package/src/resolve-modules/plugins/types.test.ts +57 -0
  163. package/src/resolve-modules/plugins/types.ts +77 -0
  164. package/src/resolve-modules/resolveModules.test.ts +176 -0
  165. package/src/resolve-modules/resolveModules.ts +97 -0
  166. package/src/resolve-modules/types.ts +71 -0
  167. package/src/test-utilities/createMessage.test.ts +100 -0
  168. package/src/test-utilities/createMessage.ts +20 -0
  169. package/src/test-utilities/index.ts +1 -0
  170. package/src/versionedInterfaces.ts +9 -0
@@ -0,0 +1,444 @@
1
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
+ import { createVariant, getVariant, updateVariantPattern } from "./variant.js"
3
+ import { describe, test, expect } from "vitest"
4
+ import {
5
+ MessagePatternsForLanguageTagDoNotExistError,
6
+ MessageVariantAlreadyExistsError,
7
+ MessageVariantDoesNotExistError,
8
+ } from "./errors.js"
9
+ import type { Message, Variant } from "../versionedInterfaces.js"
10
+
11
+ describe("getVariant", () => {
12
+ test("should return the correct variant of a message", () => {
13
+ const mockMessage: Message = getMockMessage()
14
+
15
+ const variant = getVariant(mockMessage, {
16
+ where: {
17
+ languageTag: "en",
18
+ selectors: { gender: "female", guestOther: "1" },
19
+ },
20
+ })
21
+
22
+ expect(variant?.pattern[0]).toStrictEqual({
23
+ type: "Text",
24
+ value: "{$hostName} invites {$guestName} to her party.",
25
+ })
26
+ })
27
+
28
+ test("should return the correct fallback because the exact match does not exist", () => {
29
+ const mockMessage: Message = getMockMessage()
30
+
31
+ const variant = getVariant(mockMessage, {
32
+ where: {
33
+ languageTag: "en",
34
+ selectors: { gender: "female", guestOther: "0" },
35
+ },
36
+ })
37
+ expect(variant?.pattern[0]).toStrictEqual({
38
+ type: "Text",
39
+ value: "{$hostName} invites {$guestName} and {$guestsOther} other people to her party.",
40
+ })
41
+ })
42
+
43
+ test("it should return undefined (but never throw an error) if selectors is an empty array", () => {
44
+ const mockMessage: Message = {
45
+ id: "mockMessage",
46
+ selectors: [],
47
+ variants: [
48
+ {
49
+ languageTag: "en",
50
+ pattern: [{ type: "Text", value: "Gender male" }],
51
+ match: {
52
+ gender: "male",
53
+ },
54
+ },
55
+ {
56
+ languageTag: "de",
57
+ pattern: [{ type: "Text", value: "Veraltete Übersetzung" }],
58
+ match: {},
59
+ },
60
+ ],
61
+ }
62
+ const variant = getVariant(mockMessage, {
63
+ where: {
64
+ languageTag: "fr",
65
+ selectors: {},
66
+ },
67
+ })
68
+ expect(variant).toBeUndefined()
69
+ })
70
+
71
+ test("the matcher should resolve not existing selectors to '*'", () => {
72
+ const mockMessage: Message = getMockMessage()
73
+
74
+ const variant = getVariant(mockMessage, {
75
+ where: {
76
+ languageTag: "en",
77
+ selectors: { guestOther: "0" },
78
+ },
79
+ })
80
+ expect(variant?.pattern[0]).toStrictEqual({
81
+ type: "Text",
82
+ value: "{$hostName} does not give a party.",
83
+ })
84
+ })
85
+
86
+ test("empty selector should result in '*','*'", () => {
87
+ const mockMessage: Message = getMockMessage()
88
+
89
+ const variant = getVariant(mockMessage, {
90
+ where: {
91
+ languageTag: "en",
92
+ selectors: {},
93
+ },
94
+ })
95
+ expect(variant?.pattern[0]).toStrictEqual({
96
+ type: "Text",
97
+ value: "{$hostName} invites {$guestName} and {$guestsOther} other people to their party.",
98
+ })
99
+ })
100
+
101
+ test("non existing selector values should be resolved", () => {
102
+ const mockMessage: Message = getMockMessage()
103
+
104
+ const variant = getVariant(mockMessage, {
105
+ where: {
106
+ languageTag: "en",
107
+ selectors: { gender: "trans", guestOther: "2" },
108
+ },
109
+ })
110
+ expect(variant?.pattern[0]).toStrictEqual({
111
+ type: "Text",
112
+ value: "{$hostName} invites {$guestName} and one other person to their party.",
113
+ })
114
+
115
+ const variant2 = getVariant(mockMessage, {
116
+ where: {
117
+ languageTag: "en",
118
+ selectors: { gender: "male", guestOther: "8" },
119
+ },
120
+ })
121
+ expect(variant2?.pattern[0]).toStrictEqual({
122
+ type: "Text",
123
+ value: "{$hostName} invites {$guestName} and {$guestsOther} other people to his party.",
124
+ })
125
+ })
126
+
127
+ test("should return undefined of no variant matches", () => {
128
+ const mockMessage: Message = getMockMessage()
129
+ mockMessage.variants = [
130
+ ...mockMessage.variants!.filter(
131
+ (v) => v.languageTag === "en" && (v.match.gender !== "*" || v.match.guestOther !== "*"),
132
+ ),
133
+ ]
134
+
135
+ const variant = getVariant(mockMessage, {
136
+ where: {
137
+ languageTag: "en",
138
+ selectors: {},
139
+ },
140
+ })
141
+ expect(variant).toBeUndefined()
142
+ })
143
+
144
+ test("should return undefined if a variant for specific language does not exist", () => {
145
+ const mockMessage: Message = getMockMessage()
146
+
147
+ const variant = getVariant(mockMessage, {
148
+ where: {
149
+ languageTag: "de",
150
+ selectors: { gender: "female", guestOther: "1" },
151
+ },
152
+ })
153
+ expect(variant).toBeUndefined()
154
+ })
155
+
156
+ test("should return the catch all variant if no selector defined", () => {
157
+ const mockMessage: Message = {} as any
158
+ mockMessage.variants = [
159
+ {
160
+ languageTag: "en",
161
+ match: {},
162
+ pattern: [
163
+ {
164
+ type: "Text",
165
+ value: "test",
166
+ },
167
+ ],
168
+ },
169
+ ]
170
+
171
+ const variant = getVariant(mockMessage, {
172
+ where: {
173
+ languageTag: "en",
174
+ selectors: {},
175
+ },
176
+ })
177
+ // should return the female variant
178
+ expect(variant?.pattern[0]).toStrictEqual({
179
+ type: "Text",
180
+ value: "test",
181
+ })
182
+ })
183
+ })
184
+
185
+ describe("createVariant", () => {
186
+ test("should create a variant for a message", () => {
187
+ const mockMessage: Message = getMockMessage()
188
+
189
+ const newVariant: Variant = {
190
+ languageTag: "en",
191
+ match: { gender: "female", guestOther: "0" },
192
+ pattern: [],
193
+ }
194
+ const message = createVariant(mockMessage, {
195
+ data: newVariant,
196
+ })
197
+ // should return the female variant
198
+ expect(
199
+ message.data!.variants.find(
200
+ (v) => v.languageTag === "en" && v.match.gender === "female" && v.match.guestOther === "0",
201
+ )?.pattern,
202
+ ).toStrictEqual([])
203
+ })
204
+
205
+ test("should create a variant, also if matcher are not full defined", () => {
206
+ const mockMessage: Message = getMockMessage()
207
+ mockMessage.variants = [
208
+ ...mockMessage.variants!.filter(
209
+ (v) => v.languageTag === "en" && (v.match.gender !== "*" || v.match.guestOther !== "*"),
210
+ ),
211
+ ]
212
+
213
+ const message = createVariant(mockMessage, {
214
+ data: {
215
+ languageTag: "en",
216
+ match: {},
217
+ pattern: [],
218
+ },
219
+ })
220
+ // should return the female variant
221
+ expect(
222
+ message.data!.variants.find(
223
+ (v) => v.languageTag === "en" && v.match.gender === "*" && v.match.guestOther === "*",
224
+ )?.pattern,
225
+ ).toStrictEqual([])
226
+ })
227
+
228
+ test("should return error if variant matches", () => {
229
+ const mockMessage: Message = getMockMessage()
230
+
231
+ const variant = createVariant(mockMessage, {
232
+ data: {
233
+ languageTag: "en",
234
+ match: { gender: "male", guestOther: "1" },
235
+ pattern: [],
236
+ },
237
+ })
238
+ // should return the female variant
239
+ expect(variant.data).toBeUndefined()
240
+ expect(variant.error).toBeInstanceOf(MessageVariantAlreadyExistsError)
241
+ })
242
+
243
+ test("should not return error if set of variants for specific language does not exist", () => {
244
+ const mockMessage: Message = getMockMessage()
245
+
246
+ const variant = createVariant(mockMessage, {
247
+ data: {
248
+ languageTag: "de",
249
+ match: { gender: "female", guestOther: "1" },
250
+ pattern: [],
251
+ },
252
+ })
253
+ // should return the female variant
254
+ expect(variant.data).toBeDefined()
255
+ expect(variant.error).toBeUndefined()
256
+ })
257
+ })
258
+
259
+ describe("updateVariant", () => {
260
+ test("should update a variant of a message", () => {
261
+ const mockMessage: Message = getMockMessage()
262
+
263
+ const message = updateVariantPattern(mockMessage, {
264
+ where: {
265
+ languageTag: "en",
266
+ selectors: { gender: "female", guestOther: "1" },
267
+ },
268
+ data: [],
269
+ })
270
+ // should return the female variant
271
+ expect(
272
+ message.data!.variants.find(
273
+ (v) => v.languageTag === "en" && v.match.gender === "female" && v.match.guestOther === "1",
274
+ )?.pattern,
275
+ ).toStrictEqual([])
276
+ })
277
+
278
+ test("should update a variant, also if matcher are not full defined", () => {
279
+ const mockMessage: Message = getMockMessage()
280
+
281
+ const message = updateVariantPattern(mockMessage, {
282
+ where: {
283
+ languageTag: "en",
284
+ selectors: {},
285
+ },
286
+ data: [],
287
+ })
288
+ // should return the female variant
289
+ expect(
290
+ message.data!.variants.find(
291
+ (v) => v.languageTag === "en" && v.match.gender === "*" && v.match.guestOther === "*",
292
+ )?.pattern,
293
+ ).toStrictEqual([])
294
+ })
295
+
296
+ test("should return error if no variant matches", () => {
297
+ const mockMessage: Message = getMockMessage()
298
+
299
+ mockMessage.variants = [
300
+ ...mockMessage.variants!.filter(
301
+ (v) => v.languageTag === "en" && (v.match.gender !== "*" || v.match.guestOther !== "*"),
302
+ ),
303
+ ]
304
+
305
+ const variant = updateVariantPattern(mockMessage, {
306
+ where: {
307
+ languageTag: "en",
308
+ selectors: {},
309
+ },
310
+ data: [],
311
+ })
312
+ // should return the female variant
313
+ expect(variant.data).toBeUndefined()
314
+ expect(variant.error).toBeInstanceOf(MessageVariantDoesNotExistError)
315
+ })
316
+
317
+ test("should return error if set of variants for specific language does not exist", () => {
318
+ const mockMessage: Message = getMockMessage()
319
+
320
+ const variant = updateVariantPattern(mockMessage, {
321
+ where: {
322
+ languageTag: "de",
323
+ selectors: {},
324
+ },
325
+ data: [],
326
+ })
327
+ // should return the female variant
328
+ expect(variant.data).toBeUndefined()
329
+ expect(variant.error).toBeInstanceOf(MessagePatternsForLanguageTagDoNotExistError)
330
+ })
331
+ })
332
+
333
+ const getMockMessage = (): Message => {
334
+ return {
335
+ id: "first-message",
336
+ selectors: [
337
+ { type: "VariableReference", name: "gender" },
338
+ { type: "VariableReference", name: "guestOther" },
339
+ ],
340
+ variants: [
341
+ {
342
+ languageTag: "en",
343
+ match: { gender: "female", guestOther: "1" },
344
+ pattern: [
345
+ {
346
+ type: "Text",
347
+ value: "{$hostName} invites {$guestName} to her party.",
348
+ },
349
+ ],
350
+ },
351
+ {
352
+ languageTag: "en",
353
+ match: { gender: "female", guestOther: "2" },
354
+ pattern: [
355
+ {
356
+ type: "Text",
357
+ value: "{$hostName} invites {$guestName} and one other person to her party.",
358
+ },
359
+ ],
360
+ },
361
+ {
362
+ languageTag: "en",
363
+ match: { gender: "female", guestOther: "*" },
364
+ pattern: [
365
+ {
366
+ type: "Text",
367
+ value: "{$hostName} invites {$guestName} and {$guestsOther} other people to her party.",
368
+ },
369
+ ],
370
+ },
371
+ {
372
+ languageTag: "en",
373
+ match: { gender: "male", guestOther: "1" },
374
+ pattern: [
375
+ {
376
+ type: "Text",
377
+ value: "{$hostName} invites {$guestName} to his party.",
378
+ },
379
+ ],
380
+ },
381
+ {
382
+ languageTag: "en",
383
+ match: { gender: "male", guestOther: "2" },
384
+ pattern: [
385
+ {
386
+ type: "Text",
387
+ value: "{$hostName} invites {$guestName} and one other person to his party.",
388
+ },
389
+ ],
390
+ },
391
+ {
392
+ languageTag: "en",
393
+ match: { gender: "male", guestOther: "*" },
394
+ pattern: [
395
+ {
396
+ type: "Text",
397
+ value: "{$hostName} invites {$guestName} and {$guestsOther} other people to his party.",
398
+ },
399
+ ],
400
+ },
401
+ {
402
+ languageTag: "en",
403
+ match: { gender: "*", guestOther: "0" },
404
+ pattern: [
405
+ {
406
+ type: "Text",
407
+ value: "{$hostName} does not give a party.",
408
+ },
409
+ ],
410
+ },
411
+ {
412
+ languageTag: "en",
413
+ match: { gender: "*", guestOther: "1" },
414
+ pattern: [
415
+ {
416
+ type: "Text",
417
+ value: "{$hostName} invites {$guestName} to their party.",
418
+ },
419
+ ],
420
+ },
421
+ {
422
+ languageTag: "en",
423
+ match: { gender: "*", guestOther: "2" },
424
+ pattern: [
425
+ {
426
+ type: "Text",
427
+ value: "{$hostName} invites {$guestName} and one other person to their party.",
428
+ },
429
+ ],
430
+ },
431
+ {
432
+ languageTag: "en",
433
+ match: { gender: "*", guestOther: "*" },
434
+ pattern: [
435
+ {
436
+ type: "Text",
437
+ value:
438
+ "{$hostName} invites {$guestName} and {$guestsOther} other people to their party.",
439
+ },
440
+ ],
441
+ },
442
+ ],
443
+ }
444
+ }
@@ -0,0 +1,242 @@
1
+ import type { LanguageTag, Message, Variant } from "../versionedInterfaces.js"
2
+ import type { Result } from "@inlang/result"
3
+ import {
4
+ MessagePatternsForLanguageTagDoNotExistError,
5
+ MessageVariantAlreadyExistsError,
6
+ MessageVariantDoesNotExistError,
7
+ } from "./errors.js"
8
+
9
+ /**
10
+ * Tries to match the most specific variant of a message.
11
+ *
12
+ * The selectors determine the specificity of a variant. If no selectors are provided,
13
+ * or if the selectors do not match any variant, the catch all variant is returned
14
+ * (if it exists).
15
+ *
16
+ * @example
17
+ * const variant = getVariant(message, { where: { languageTag: "en", selectors: { gender: "male" }}});
18
+ */
19
+ export function getVariant(
20
+ message: Message,
21
+ args: {
22
+ where: {
23
+ languageTag: LanguageTag
24
+ selectors?: Variant["match"]
25
+ }
26
+ },
27
+ ): Variant | undefined {
28
+ const variant = matchMostSpecificVariant(message, args.where.languageTag, args.where.selectors)
29
+ if (variant) {
30
+ //! do not return a reference to the message in a resource
31
+ //! modifications to the returned message will leak into the
32
+ //! resource which is considered to be immutable.
33
+ return structuredClone(variant)
34
+ }
35
+ return undefined
36
+ }
37
+
38
+ /**
39
+ * Create a variant for a message
40
+ *
41
+ * All actions are immutable.
42
+ *
43
+ * @example
44
+ * const message = createVariant(message, { languageTag: "en", data: variant })
45
+ */
46
+ export function createVariant(
47
+ message: Message,
48
+ args: {
49
+ data: Variant
50
+ },
51
+ ): Result<Message, MessageVariantAlreadyExistsError> {
52
+ const copy = structuredClone(message)
53
+
54
+ // check if variant already exists
55
+ if (matchVariant(copy, args.data.languageTag, args.data.match)) {
56
+ return { error: new MessageVariantAlreadyExistsError(message.id, args.data.languageTag) }
57
+ }
58
+
59
+ // need to resolve selectors to match length and order of message selectors
60
+ copy.variants.push({
61
+ ...args.data,
62
+ match: resolveSelector(copy.selectors, args.data.match),
63
+ })
64
+ return { data: copy }
65
+ }
66
+
67
+ /**
68
+ * Update a variant of a message
69
+ *
70
+ * All actions are immutable.
71
+ *
72
+ * @example
73
+ * const message = updateVariant(message, { languageTag: "en", selectors: { gender: "male" }, pattern: []})
74
+ */
75
+ export function updateVariantPattern(
76
+ message: Message,
77
+ args: {
78
+ where: {
79
+ languageTag: LanguageTag
80
+ selectors: Record<string, string>
81
+ }
82
+ data: Variant["pattern"]
83
+ },
84
+ ): Result<Message, MessageVariantDoesNotExistError | MessagePatternsForLanguageTagDoNotExistError> {
85
+ const copy = structuredClone(message)
86
+
87
+ const containsLanguageTag = message.variants.some(
88
+ (variant) => variant.languageTag === args.where.languageTag,
89
+ )
90
+ if (!containsLanguageTag) {
91
+ return {
92
+ error: new MessagePatternsForLanguageTagDoNotExistError(message.id, args.where.languageTag),
93
+ }
94
+ }
95
+
96
+ const variant = matchVariant(copy, args.where.languageTag, args.where.selectors)
97
+ if (variant === undefined) {
98
+ return { error: new MessageVariantDoesNotExistError(message.id, args.where.languageTag) }
99
+ }
100
+ if (variant) {
101
+ variant.pattern = args.data
102
+ return { data: copy }
103
+ }
104
+ return { error: new MessageVariantDoesNotExistError(message.id, args.where.languageTag) }
105
+ }
106
+
107
+ /**
108
+ * Returns the specific variant defined by selectors or undefined
109
+ *
110
+ * @example
111
+ * const variant = matchVariant(message, languageTag: "en", selectors: { gender: "male" })
112
+ */
113
+ const matchVariant = (
114
+ message: Message,
115
+ languageTag: LanguageTag,
116
+ selectors: Record<string, string>,
117
+ ): Variant | undefined => {
118
+ // resolve preferenceSelectors to match length and order of message selectors
119
+ const resolvedSelectors = resolveSelector(message.selectors, selectors)
120
+
121
+ const languageVariants = message.variants.filter((variant) => variant.languageTag === languageTag)
122
+ if (languageVariants.length === 0) return undefined
123
+
124
+ for (const variant of languageVariants) {
125
+ let isMatch = true
126
+ //check if vaiant is a match
127
+ for (const [key, value] of Object.entries(variant.match)) {
128
+ if (resolvedSelectors[key] !== value) {
129
+ isMatch = false
130
+ }
131
+ }
132
+ if (isMatch) {
133
+ return variant
134
+ }
135
+ }
136
+ return undefined
137
+ }
138
+
139
+ /**
140
+ * Returns the most specific variant of a message.
141
+ *
142
+ * @example
143
+ * const variant = matchMostSpecificVariant(message, languageTag: "en", selectors: { gender: "male" })
144
+ */
145
+ const matchMostSpecificVariant = (
146
+ message: Message,
147
+ languageTag: LanguageTag,
148
+ selectors?: Record<string, string>,
149
+ ): Variant | undefined => {
150
+ // make selector undefined if empty object
151
+ selectors = JSON.stringify(selectors) === "{}" ? undefined : selectors
152
+
153
+ // resolve preferenceSelectors to match length and order of message selectors
154
+ const resolvedSelectors = resolveSelector(message.selectors, selectors)
155
+ const index: Record<string, any> = {}
156
+
157
+ const languageVariants = message.variants.filter((variant) => variant.languageTag === languageTag)
158
+ if (languageVariants.length === 0) return undefined
159
+
160
+ for (const variant of languageVariants) {
161
+ let isMatch = true
162
+
163
+ //check if variant is a match
164
+ for (const [key, value] of Object.entries(variant.match)) {
165
+ if (resolvedSelectors[key] !== value && value !== "*") {
166
+ isMatch = false
167
+ }
168
+ }
169
+ if (isMatch && selectors) {
170
+ // add variant to nested index
171
+ // eslint-disable-next-line no-inner-declarations
172
+ function recursiveAddToIndex(
173
+ currentIndex: Record<string, any>,
174
+ currentKeys: Message["selectors"],
175
+ variant: Variant,
176
+ ) {
177
+ if (currentKeys[0]?.name) {
178
+ const key = variant.match[currentKeys[0].name]
179
+ if (key) {
180
+ if (currentKeys.length === 1) {
181
+ currentIndex[key] = variant
182
+ } else {
183
+ if (!currentIndex[key]) {
184
+ currentIndex[key] = {}
185
+ }
186
+ recursiveAddToIndex(currentIndex[key], currentKeys.slice(1), variant)
187
+ }
188
+ }
189
+ }
190
+ }
191
+ recursiveAddToIndex(index, message.selectors, variant)
192
+ } else if (isMatch && !selectors) {
193
+ return variant
194
+ }
195
+ }
196
+
197
+ //find the most specific variant
198
+ const findOptimalMatch = (
199
+ index: Record<string, any>,
200
+ selectors: string[],
201
+ ): Variant | undefined => {
202
+ const keys = Object.keys(index)
203
+
204
+ for (const key of keys) {
205
+ if (key === selectors[0] || key === "*") {
206
+ const nextOptimal = selectors.slice(1)
207
+
208
+ if (nextOptimal.length === 0) {
209
+ return (index[key] as Variant) || undefined
210
+ }
211
+
212
+ const match = findOptimalMatch(index[key] as Record<string, any>, nextOptimal)
213
+
214
+ if (match !== undefined) {
215
+ return match
216
+ }
217
+ }
218
+ }
219
+ return undefined
220
+ }
221
+
222
+ return findOptimalMatch(index, Object.values(resolvedSelectors))
223
+ }
224
+
225
+ /**
226
+ * Returns resolved selector.
227
+ * -> Adds all possible selectors, if not defined it adds '*'. Order is determined by messageSelectors
228
+ *
229
+ * @example
230
+ * const variant = resolveSelector(["gender","count"], selector: {count: "2"})
231
+ */
232
+ const resolveSelector = (
233
+ messageSelectors: Message["selectors"],
234
+ selectors?: Record<string, string>,
235
+ ): Record<string, string> => {
236
+ const resolvedSelectors: Record<string, string> = {}
237
+ if (!selectors) return {}
238
+ for (const messageSelector of messageSelectors) {
239
+ resolvedSelectors[messageSelector.name] = selectors[messageSelector.name] ?? "*"
240
+ }
241
+ return resolvedSelectors
242
+ }