@inlang/sdk 0.35.4 → 0.35.6

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 (111) hide show
  1. package/dist/adapter/solidAdapter.test.js +1 -1
  2. package/dist/api.d.ts +2 -13
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/createMessagesQuery.d.ts.map +1 -1
  5. package/dist/createMessagesQuery.js +68 -82
  6. package/dist/createNewProject.test.js +1 -3
  7. package/dist/loadProject.d.ts.map +1 -1
  8. package/dist/loadProject.js +67 -32
  9. package/dist/loadProject.test.js +6 -2
  10. package/dist/persistence/batchedIO.d.ts +11 -0
  11. package/dist/persistence/batchedIO.d.ts.map +1 -0
  12. package/dist/persistence/batchedIO.js +49 -0
  13. package/dist/persistence/batchedIO.test.d.ts +2 -0
  14. package/dist/persistence/batchedIO.test.d.ts.map +1 -0
  15. package/dist/persistence/batchedIO.test.js +56 -0
  16. package/dist/persistence/filelock/acquireFileLock.d.ts.map +1 -1
  17. package/dist/persistence/filelock/acquireFileLock.js +3 -1
  18. package/dist/persistence/filelock/releaseLock.d.ts.map +1 -1
  19. package/dist/persistence/filelock/releaseLock.js +2 -1
  20. package/dist/persistence/store.d.ts +107 -0
  21. package/dist/persistence/store.d.ts.map +1 -0
  22. package/dist/persistence/store.js +99 -0
  23. package/dist/persistence/store.test.d.ts +2 -0
  24. package/dist/persistence/store.test.d.ts.map +1 -0
  25. package/dist/persistence/store.test.js +79 -0
  26. package/dist/persistence/storeApi.d.ts +22 -0
  27. package/dist/persistence/storeApi.d.ts.map +1 -0
  28. package/dist/persistence/storeApi.js +1 -0
  29. package/dist/reactivity/solid.test.js +1 -6
  30. package/dist/resolve-modules/plugins/resolvePlugins.d.ts.map +1 -1
  31. package/dist/resolve-modules/plugins/resolvePlugins.js +3 -10
  32. package/dist/test-utilities/sleep.d.ts +4 -0
  33. package/dist/test-utilities/sleep.d.ts.map +1 -0
  34. package/dist/test-utilities/sleep.js +9 -0
  35. package/dist/v2/helper.d.ts +131 -0
  36. package/dist/v2/helper.d.ts.map +1 -0
  37. package/dist/v2/helper.js +75 -0
  38. package/dist/v2/helper.test.d.ts +2 -0
  39. package/dist/v2/helper.test.d.ts.map +1 -0
  40. package/dist/v2/{createMessageBundle.test.js → helper.test.js} +1 -1
  41. package/dist/v2/index.d.ts +2 -0
  42. package/dist/v2/index.d.ts.map +1 -1
  43. package/dist/v2/index.js +2 -1
  44. package/dist/v2/mocks/index.d.ts +3 -0
  45. package/dist/v2/mocks/index.d.ts.map +1 -0
  46. package/dist/v2/mocks/index.js +2 -0
  47. package/dist/v2/mocks/multipleMatcher/bundle.d.ts +3 -0
  48. package/dist/v2/mocks/multipleMatcher/bundle.d.ts.map +1 -0
  49. package/dist/v2/mocks/multipleMatcher/bundle.js +194 -0
  50. package/dist/v2/mocks/multipleMatcher/bundle.test.d.ts +2 -0
  51. package/dist/v2/mocks/multipleMatcher/bundle.test.d.ts.map +1 -0
  52. package/dist/v2/mocks/multipleMatcher/bundle.test.js +10 -0
  53. package/dist/v2/mocks/plural/bundle.d.ts +1 -1
  54. package/dist/v2/mocks/plural/bundle.d.ts.map +1 -1
  55. package/dist/v2/mocks/plural/bundle.js +1 -1
  56. package/dist/v2/mocks/plural/bundle.test.js +6 -6
  57. package/dist/v2/shim.d.ts +12 -0
  58. package/dist/v2/shim.d.ts.map +1 -0
  59. package/dist/v2/shim.js +151 -0
  60. package/dist/v2/shim.test.d.ts +2 -0
  61. package/dist/v2/shim.test.d.ts.map +1 -0
  62. package/dist/v2/shim.test.js +49 -0
  63. package/dist/v2/stubQueryApi.d.ts +9 -0
  64. package/dist/v2/stubQueryApi.d.ts.map +1 -0
  65. package/dist/v2/stubQueryApi.js +38 -0
  66. package/dist/v2/types.d.ts +110 -0
  67. package/dist/v2/types.d.ts.map +1 -1
  68. package/dist/v2/types.js +9 -0
  69. package/package.json +9 -8
  70. package/src/adapter/solidAdapter.test.ts +1 -1
  71. package/src/api.ts +2 -13
  72. package/src/createMessagesQuery.ts +80 -99
  73. package/src/createNewProject.test.ts +1 -4
  74. package/src/loadProject.test.ts +6 -2
  75. package/src/loadProject.ts +86 -45
  76. package/src/persistence/batchedIO.test.ts +63 -0
  77. package/src/persistence/batchedIO.ts +64 -0
  78. package/src/persistence/filelock/acquireFileLock.ts +5 -2
  79. package/src/persistence/filelock/releaseLock.ts +2 -1
  80. package/src/persistence/store.test.ts +102 -0
  81. package/src/persistence/store.ts +119 -0
  82. package/src/persistence/storeApi.ts +19 -0
  83. package/src/reactivity/solid.test.ts +1 -8
  84. package/src/resolve-modules/plugins/resolvePlugins.ts +4 -13
  85. package/src/test-utilities/sleep.ts +11 -0
  86. package/src/v2/{createMessageBundle.test.ts → helper.test.ts} +1 -1
  87. package/src/v2/helper.ts +98 -0
  88. package/src/v2/index.ts +2 -0
  89. package/src/v2/mocks/index.ts +2 -0
  90. package/src/v2/mocks/multipleMatcher/bundle.test.ts +11 -0
  91. package/src/v2/mocks/multipleMatcher/bundle.ts +196 -0
  92. package/src/v2/mocks/plural/bundle.test.ts +6 -6
  93. package/src/v2/mocks/plural/bundle.ts +1 -1
  94. package/src/v2/shim.test.ts +56 -0
  95. package/src/v2/shim.ts +173 -0
  96. package/src/v2/stubQueryApi.ts +43 -0
  97. package/src/v2/types.ts +17 -0
  98. package/dist/persistence/plugin.d.ts +0 -31
  99. package/dist/persistence/plugin.d.ts.map +0 -1
  100. package/dist/persistence/plugin.js +0 -42
  101. package/dist/persistence/plugin.test.d.ts +0 -2
  102. package/dist/persistence/plugin.test.d.ts.map +0 -1
  103. package/dist/persistence/plugin.test.js +0 -49
  104. package/dist/v2/createMessageBundle.d.ts +0 -25
  105. package/dist/v2/createMessageBundle.d.ts.map +0 -1
  106. package/dist/v2/createMessageBundle.js +0 -36
  107. package/dist/v2/createMessageBundle.test.d.ts +0 -2
  108. package/dist/v2/createMessageBundle.test.d.ts.map +0 -1
  109. package/src/persistence/plugin.test.ts +0 -60
  110. package/src/persistence/plugin.ts +0 -56
  111. package/src/v2/createMessageBundle.ts +0 -43
@@ -21,16 +21,19 @@ export async function acquireFileLock(
21
21
  try {
22
22
  debug(lockOrigin + " tries to acquire a lockfile Retry Nr.: " + tryCount)
23
23
  await fs.mkdir(lockDirPath)
24
- const stats = await fs.stat(lockDirPath)
24
+ // NOTE: fs.stat does not need to be atomic since mkdir would crash atomically - if we are here its save to consider the lock held by this process
25
+ const stats = await fs.stat(lockDirPath)
25
26
  debug(lockOrigin + " acquired a lockfile Retry Nr.: " + tryCount)
27
+
26
28
  return stats.mtimeMs
27
29
  } catch (error: any) {
28
30
  if (error.code !== "EEXIST") {
29
- // we only expect the error that the file exists already (locked by other process)
31
+ // NOTE in case we have an EEXIST - this is an expected error: the folder existed - another process already acquired the lock. Rethrow all other errors
30
32
  throw error
31
33
  }
32
34
  }
33
35
 
36
+ // land here if the lockDirPath already exists => lock is held by other process
34
37
  let currentLockTime: number
35
38
 
36
39
  try {
@@ -11,8 +11,9 @@ export async function releaseLock(
11
11
  debug(lockOrigin + " releasing the lock ")
12
12
  try {
13
13
  const stats = await fs.stat(lockDirPath)
14
+ // I believe this check associates the lock with the aquirer
14
15
  if (stats.mtimeMs === lockTime) {
15
- // this can be corrupt as welll since the last getStat and the current a modification could have occured :-/
16
+ // NOTE: since we have to use a timeout for stale detection (uTimes is not exposed via mermoryfs) the check for the locktime is not sufficient and can fail in rare cases when another process accuires a lock that was identifiert as tale between call to fs.state and rmDir
16
17
  await fs.rmdir(lockDirPath)
17
18
  }
18
19
  } catch (statError: any) {
@@ -0,0 +1,102 @@
1
+ import { test, expect } from "vitest"
2
+ import { createNodeishMemoryFs } from "../test-utilities/index.js"
3
+ import type { MessageBundle } from "../v2/types.js"
4
+ import { createMessageBundle, createMessage, injectJSONNewlines, addSlots } from "../v2/helper.js"
5
+ import { openStore, readJSON, writeJSON } from "./store.js"
6
+
7
+ const locales = ["en", "de"]
8
+
9
+ const mockMessages: MessageBundle[] = [
10
+ createMessageBundle({
11
+ id: "first_message",
12
+ messages: [
13
+ createMessage({
14
+ locale: "en",
15
+ text: "If this fails I will be sad",
16
+ }),
17
+ ],
18
+ }),
19
+ createMessageBundle({
20
+ id: "second_message",
21
+ messages: [
22
+ createMessage({ locale: "en", text: "Let's see if this works" }),
23
+ createMessage({ locale: "de", text: "Mal sehen ob das funktioniert" }),
24
+ ],
25
+ }),
26
+ ]
27
+
28
+ test("roundtrip readJSON/writeJSON", async () => {
29
+ const nodeishFs = createNodeishMemoryFs()
30
+ const projectPath = "/test/project.inlang"
31
+ const filePath = projectPath + "/messages.json"
32
+ const persistedMessages = injectJSONNewlines(
33
+ JSON.stringify(mockMessages.map((bundle) => addSlots(bundle, locales)))
34
+ )
35
+
36
+ await nodeishFs.mkdir(projectPath, { recursive: true })
37
+ await nodeishFs.writeFile(filePath, persistedMessages)
38
+
39
+ const firstMessageLoad = await readJSON({
40
+ filePath,
41
+ nodeishFs: nodeishFs,
42
+ })
43
+
44
+ expect(firstMessageLoad).toStrictEqual(mockMessages)
45
+
46
+ await writeJSON({
47
+ filePath,
48
+ nodeishFs,
49
+ messages: firstMessageLoad,
50
+ locales,
51
+ })
52
+
53
+ const afterRoundtrip = await nodeishFs.readFile(filePath, { encoding: "utf-8" })
54
+
55
+ expect(afterRoundtrip).toStrictEqual(persistedMessages)
56
+
57
+ const messagesAfterRoundtrip = await readJSON({
58
+ filePath,
59
+ nodeishFs,
60
+ })
61
+
62
+ expect(messagesAfterRoundtrip).toStrictEqual(firstMessageLoad)
63
+ })
64
+
65
+ test("openStore does minimal CRUD on messageBundles", async () => {
66
+ const nodeishFs = createNodeishMemoryFs()
67
+ const projectPath = "/test/project.inlang"
68
+ const filePath = projectPath + "/messages.json"
69
+ const persistedMessages = JSON.stringify(mockMessages)
70
+
71
+ await nodeishFs.mkdir(projectPath, { recursive: true })
72
+ await nodeishFs.writeFile(filePath, persistedMessages)
73
+
74
+ const store = await openStore({ projectPath, nodeishFs, locales })
75
+
76
+ const messages = await store.messageBundles.getAll()
77
+ expect(messages).toStrictEqual(mockMessages)
78
+
79
+ const firstMessageBundle = await store.messageBundles.get({ id: "first_message" })
80
+ expect(firstMessageBundle).toStrictEqual(mockMessages[0])
81
+
82
+ const modifedMessageBundle = structuredClone(firstMessageBundle) as MessageBundle
83
+ const newMessage = createMessage({ locale: "de", text: "Wenn dies schiefläuft, bin ich sauer" })
84
+ modifedMessageBundle.messages.push(newMessage)
85
+ await store.messageBundles.set({ data: modifedMessageBundle })
86
+
87
+ const setMessageBundle = await store.messageBundles.get({ id: "first_message" })
88
+ expect(setMessageBundle).toStrictEqual(modifedMessageBundle)
89
+
90
+ // no need to sleep here
91
+ // with batchedIO, the set should await until the write is done
92
+ const messagesAfterRoundtrip = await readJSON({
93
+ filePath,
94
+ nodeishFs,
95
+ })
96
+ const expected = [setMessageBundle, ...mockMessages.slice(1)]
97
+ expect(messagesAfterRoundtrip).toStrictEqual(expected)
98
+
99
+ await store.messageBundles.delete({ id: "first_message" })
100
+ const messagesAfterDelete = await store.messageBundles.getAll()
101
+ expect(messagesAfterDelete).toStrictEqual(mockMessages.slice(1))
102
+ })
@@ -0,0 +1,119 @@
1
+ import type { MessageBundle } from "../v2/types.js"
2
+ import { addSlots, removeSlots, injectJSONNewlines } from "../v2/helper.js"
3
+ import { getDirname, type NodeishFilesystem } from "@lix-js/fs"
4
+ import { acquireFileLock } from "./filelock/acquireFileLock.js"
5
+ import { releaseLock } from "./filelock/releaseLock.js"
6
+ import { batchedIO } from "./batchedIO.js"
7
+ import type { StoreApi } from "./storeApi.js"
8
+
9
+ import _debug from "debug"
10
+ const debug = _debug("sdk:store")
11
+
12
+ export async function openStore(args: {
13
+ projectPath: string
14
+ nodeishFs: NodeishFilesystem
15
+ locales: string[]
16
+ }): Promise<StoreApi> {
17
+ const nodeishFs = args.nodeishFs
18
+ const filePath = args.projectPath + "/messages.json"
19
+ const lockDirPath = args.projectPath + "/messagelock"
20
+
21
+ // the index holds the in-memory state
22
+ // TODO: reload when file changes on disk
23
+ // https://github.com/opral/inlang-message-sdk/issues/80
24
+ let index = await load()
25
+
26
+ const batchedSave = batchedIO(acquireSaveLock, releaseSaveLock, save)
27
+
28
+ return {
29
+ messageBundles: {
30
+ reload: async () => {
31
+ index.clear()
32
+ index = await load()
33
+ },
34
+ get: async (args: { id: string }) => {
35
+ return index.get(args.id)
36
+ },
37
+ set: async (args: { data: MessageBundle }) => {
38
+ index.set(args.data.id, args.data)
39
+ await batchedSave(args.data.id)
40
+ },
41
+ delete: async (args: { id: string }) => {
42
+ index.delete(args.id)
43
+ await batchedSave(args.id)
44
+ },
45
+ getAll: async () => {
46
+ return [...index.values()]
47
+ },
48
+ },
49
+ }
50
+
51
+ // load and save messages from file system atomically
52
+ // using a lock file to prevent partial reads and writes
53
+ async function load() {
54
+ const lockTime = await acquireFileLock(nodeishFs, lockDirPath, "load")
55
+ const messages = await readJSON({ filePath, nodeishFs: nodeishFs })
56
+ const index = new Map<string, MessageBundle>(messages.map((message) => [message.id, message]))
57
+ await releaseLock(nodeishFs, lockDirPath, "load", lockTime)
58
+ return index
59
+ }
60
+ async function acquireSaveLock() {
61
+ return await acquireFileLock(nodeishFs, lockDirPath, "save")
62
+ }
63
+ async function releaseSaveLock(lock: number) {
64
+ return await releaseLock(nodeishFs, lockDirPath, "save", lock)
65
+ }
66
+ async function save() {
67
+ await writeJSON({
68
+ filePath,
69
+ nodeishFs: nodeishFs,
70
+ messages: [...index.values()],
71
+ locales: args.locales,
72
+ })
73
+ }
74
+ }
75
+
76
+ export async function readJSON(args: { filePath: string; nodeishFs: NodeishFilesystem }) {
77
+ let result: MessageBundle[] = []
78
+
79
+ debug("loadAll", args.filePath)
80
+ try {
81
+ const file = await args.nodeishFs.readFile(args.filePath, { encoding: "utf-8" })
82
+ result = JSON.parse(file)
83
+ } catch (error) {
84
+ if ((error as any)?.code !== "ENOENT") {
85
+ debug("loadMessages", error)
86
+ throw error
87
+ }
88
+ }
89
+ return result.map(removeSlots)
90
+ }
91
+
92
+ export async function writeJSON(args: {
93
+ filePath: string
94
+ nodeishFs: NodeishFilesystem
95
+ messages: MessageBundle[]
96
+ locales: string[]
97
+ }) {
98
+ debug("saveall", args.filePath)
99
+ try {
100
+ await createDirectoryIfNotExits(getDirname(args.filePath), args.nodeishFs)
101
+ const output = injectJSONNewlines(
102
+ JSON.stringify(args.messages.map((bundle) => addSlots(bundle, args.locales)))
103
+ )
104
+ await args.nodeishFs.writeFile(args.filePath, output)
105
+ } catch (error) {
106
+ debug("saveMessages", error)
107
+ throw error
108
+ }
109
+ }
110
+
111
+ async function createDirectoryIfNotExits(path: string, nodeishFs: NodeishFilesystem) {
112
+ try {
113
+ await nodeishFs.mkdir(path, { recursive: true })
114
+ } catch (error: any) {
115
+ if (error.code !== "EEXIST") {
116
+ throw error
117
+ }
118
+ }
119
+ }
@@ -0,0 +1,19 @@
1
+ import type * as V2 from "../v2/types.js"
2
+
3
+ /**
4
+ * WIP async V2 store interface
5
+ * E.g. `await project.store.messageBundles.get({ id: "..." })`
6
+ **/
7
+ export type StoreApi = {
8
+ messageBundles: Store<V2.MessageBundle>
9
+ }
10
+
11
+ export interface Store<T> {
12
+ // TODO: remove reload when fs.watch can trigger auto-invalidation
13
+ reload: () => Promise<void>
14
+
15
+ get: (args: { id: string }) => Promise<T | undefined>
16
+ set: (args: { data: T }) => Promise<void>
17
+ delete: (args: { id: string }) => Promise<void>
18
+ getAll: () => Promise<T[]>
19
+ }
@@ -1,4 +1,5 @@
1
1
  import { describe, it, expect } from "vitest"
2
+ import { sleep, delay } from "../test-utilities/sleep.js"
2
3
  import {
3
4
  createRoot,
4
5
  createSignal,
@@ -9,14 +10,6 @@ import {
9
10
  } from "./solid.js"
10
11
  import { ReactiveMap } from "./map.js"
11
12
 
12
- function sleep(ms: number) {
13
- return new Promise((resolve) => setTimeout(resolve, ms))
14
- }
15
-
16
- function delay(v: unknown, ms: number) {
17
- return new Promise((resolve) => setTimeout(() => resolve(v), ms))
18
- }
19
-
20
13
  describe("vitest", () => {
21
14
  it("waits for the result of an async function", async () => {
22
15
  const rval = await delay(42, 1)
@@ -1,10 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
2
  import type { ResolvePluginsFunction } from "./types.js"
3
3
  import { Plugin } from "@inlang/plugin"
4
- import {
5
- loadMessages as sdkLoadMessages,
6
- saveMessages as sdkSaveMessages,
7
- } from "../../persistence/plugin.js"
8
4
  import {
9
5
  PluginReturnedInvalidCustomApiError,
10
6
  PluginLoadMessagesFunctionAlreadyDefinedError,
@@ -121,15 +117,10 @@ export const resolvePlugins: ResolvePluginsFunction = async (args) => {
121
117
 
122
118
  // --- LOADMESSAGE / SAVEMESSAGE NOT DEFINED ---
123
119
 
124
- if (experimentalPersistence) {
125
- debug("Override load/save for experimental persistence")
126
- // @ts-ignore - type mismatch error
127
- result.data.loadMessages = sdkLoadMessages
128
- // @ts-ignore - type mismatch error
129
- result.data.saveMessages = sdkSaveMessages
130
- } else if (
131
- typeof result.data.loadMessages !== "function" ||
132
- typeof result.data.saveMessages !== "function"
120
+ if (
121
+ !experimentalPersistence &&
122
+ (typeof result.data.loadMessages !== "function" ||
123
+ typeof result.data.saveMessages !== "function")
133
124
  ) {
134
125
  result.errors.push(new PluginsDoNotProvideLoadOrSaveMessagesError())
135
126
  }
@@ -0,0 +1,11 @@
1
+ export function sleep(ms: number) {
2
+ return new Promise((resolve) => setTimeout(resolve, ms))
3
+ }
4
+
5
+ export function delay(v: unknown, ms: number) {
6
+ return new Promise((resolve) => setTimeout(() => resolve(v), ms))
7
+ }
8
+
9
+ export function fail(ms: number) {
10
+ return new Promise((resolve, reject) => setTimeout(() => reject(new Error("fail")), ms))
11
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from "vitest"
2
- import { createMessageBundle, createMessage } from "./createMessageBundle.js"
2
+ import { createMessageBundle, createMessage } from "./helper.js"
3
3
  import { MessageBundle } from "./types.js"
4
4
  import { Value } from "@sinclair/typebox/value"
5
5
 
@@ -0,0 +1,98 @@
1
+ import {
2
+ LanguageTag,
3
+ MessageBundle,
4
+ MessageBundleWithSlots,
5
+ Message,
6
+ MessageSlot,
7
+ Text,
8
+ } from "./types.js"
9
+
10
+ /**
11
+ * create v2 MessageBundle
12
+ * @example createMessageBundle({
13
+ * id: "greeting",
14
+ * messages: [
15
+ * createMessage({locale: "en", text: "Hello world!"})
16
+ * createMessage({locale: "de", text: "Hallo Welt!"})
17
+ * ]
18
+ * })
19
+ */
20
+ export function createMessageBundle(args: {
21
+ id: string
22
+ messages: Message[]
23
+ alias?: MessageBundle["alias"]
24
+ }): MessageBundle {
25
+ return {
26
+ id: args.id,
27
+ alias: args.alias ?? {},
28
+ messages: args.messages,
29
+ }
30
+ }
31
+
32
+ /**
33
+ * create v2 Messsage AST with text-only pattern
34
+ * @example createMessage({locale: "en", text: "Hello world"})
35
+ */
36
+ export function createMessage(args: {
37
+ locale: LanguageTag
38
+ text: string
39
+ match?: Array<string>
40
+ }): Message {
41
+ return {
42
+ locale: args.locale,
43
+ declarations: [],
44
+ selectors: [],
45
+ variants: [{ match: args.match ? args.match : [], pattern: [toTextElement(args.text ?? "")] }],
46
+ }
47
+ }
48
+
49
+ export function toTextElement(text: string): Text {
50
+ return {
51
+ type: "text",
52
+ value: text,
53
+ }
54
+ }
55
+
56
+ // ****************************
57
+ // WIP experimental persistence
58
+ // ****************************
59
+
60
+ /**
61
+ * create MessageSlot for a locale (only used for persistence)
62
+ */
63
+ export function createMessageSlot(locale: LanguageTag): MessageSlot {
64
+ return {
65
+ locale,
66
+ slot: true,
67
+ }
68
+ }
69
+
70
+ /**
71
+ * return structuredClone with message slots for all locales not yet present
72
+ */
73
+ export function addSlots(messageBundle: MessageBundle, locales: string[]): MessageBundleWithSlots {
74
+ const bundle = structuredClone(messageBundle) as MessageBundleWithSlots
75
+ bundle.messages = locales.map((locale) => {
76
+ return bundle.messages.find((message) => message.locale === locale) ?? createMessageSlot(locale)
77
+ })
78
+ return bundle
79
+ }
80
+
81
+ /**
82
+ * remove empty message slots without first creating a structured clone
83
+ */
84
+ export function removeSlots(messageBundle: MessageBundleWithSlots) {
85
+ messageBundle.messages = messageBundle.messages.filter((message) => !("slot" in message))
86
+ return messageBundle as MessageBundle
87
+ }
88
+
89
+ /**
90
+ * Add newlines between bundles and messages to avoid merge conflicts
91
+ */
92
+ export function injectJSONNewlines(json: string): string {
93
+ return json
94
+ .replace(/\{"id":"/g, '\n\n\n\n{"id":"')
95
+ .replace(/"messages":\[\{"locale":"/g, '"messages":[\n\n\n\n{"locale":"')
96
+ .replace(/\}\]\}\]\},\{"locale":"/g, '}]}]},\n\n\n\n{"locale":"')
97
+ .replace(/"slot":true\},\{"locale":/g, '"slot":true},\n\n\n\n{"locale":')
98
+ }
package/src/v2/index.ts CHANGED
@@ -1 +1,3 @@
1
1
  export type * from "./types.js"
2
+ export * from "./helper.js"
3
+ export * from "./shim.js"
@@ -0,0 +1,2 @@
1
+ export * from "./plural/bundle.js"
2
+ export * from "./multipleMatcher/bundle.js"
@@ -0,0 +1,11 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { multipleMatcherBundle } from "./bundle.js"
3
+ import { MessageBundle } from "../../types.js"
4
+ import { Value } from "@sinclair/typebox/value"
5
+
6
+ describe("mock plural messageBundle", () => {
7
+ it("is valid", () => {
8
+ const messageBundle: unknown = multipleMatcherBundle
9
+ expect(Value.Check(MessageBundle, messageBundle)).toBe(true)
10
+ })
11
+ })
@@ -0,0 +1,196 @@
1
+ import type { MessageBundle } from "../../types.js"
2
+
3
+ export const multipleMatcherBundle: MessageBundle = {
4
+ id: "mock-bundle-human-id",
5
+ alias: {
6
+ default: "mock-bundle-alias",
7
+ },
8
+ messages: [
9
+ {
10
+ locale: "en",
11
+ declarations: [],
12
+ selectors: [
13
+ {
14
+ type: "expression",
15
+ arg: {
16
+ type: "variable",
17
+ name: "count",
18
+ },
19
+ annotation: { type: "function", name: "plural", options: [] },
20
+ },
21
+ ],
22
+ variants: [
23
+ {
24
+ match: ["one"],
25
+ pattern: [
26
+ {
27
+ type: "text",
28
+ value: "Show ",
29
+ },
30
+ {
31
+ type: "expression",
32
+ arg: {
33
+ type: "variable",
34
+ name: "count",
35
+ },
36
+ },
37
+ {
38
+ type: "text",
39
+ value: " message.",
40
+ },
41
+ ],
42
+ },
43
+ {
44
+ match: ["many"],
45
+ pattern: [
46
+ {
47
+ type: "text",
48
+ value: "Show ",
49
+ },
50
+ {
51
+ type: "expression",
52
+ arg: {
53
+ type: "variable",
54
+ name: "count",
55
+ },
56
+ },
57
+ {
58
+ type: "text",
59
+ value: " messages.",
60
+ },
61
+ ],
62
+ },
63
+ {
64
+ match: ["*"],
65
+ pattern: [
66
+ {
67
+ type: "text",
68
+ value: "Show ",
69
+ },
70
+ {
71
+ type: "expression",
72
+ arg: {
73
+ type: "variable",
74
+ name: "count",
75
+ },
76
+ },
77
+ ],
78
+ },
79
+ ],
80
+ },
81
+ {
82
+ locale: "de",
83
+ declarations: [],
84
+ selectors: [
85
+ {
86
+ type: "expression",
87
+ arg: {
88
+ type: "variable",
89
+ name: "count",
90
+ },
91
+ annotation: { type: "function", name: "plural", options: [] },
92
+ },
93
+ {
94
+ type: "expression",
95
+ arg: {
96
+ type: "variable",
97
+ name: "formal",
98
+ },
99
+ annotation: { type: "function", name: "bool", options: [] },
100
+ },
101
+ ],
102
+ variants: [
103
+ {
104
+ match: ["one", "formal"],
105
+ pattern: [
106
+ {
107
+ type: "text",
108
+ value: "Zeigen Sie bitte Ihre Nachricht.",
109
+ },
110
+ ],
111
+ },
112
+ {
113
+ match: ["one", "informal"],
114
+ pattern: [
115
+ {
116
+ type: "text",
117
+ value: "Zeigen Deine Nachricht.",
118
+ },
119
+ ],
120
+ },
121
+ {
122
+ match: ["many", "formal"],
123
+ pattern: [
124
+ {
125
+ type: "text",
126
+ value: "Zeigen Sie bitte Ihre ",
127
+ },
128
+ {
129
+ type: "expression",
130
+ arg: {
131
+ type: "variable",
132
+ name: "count",
133
+ },
134
+ },
135
+ {
136
+ type: "text",
137
+ value: " Nachrichten.",
138
+ },
139
+ ],
140
+ },
141
+ {
142
+ match: ["many", "informal"],
143
+ pattern: [
144
+ {
145
+ type: "text",
146
+ value: "Zeigen Deine ",
147
+ },
148
+ {
149
+ type: "expression",
150
+ arg: {
151
+ type: "variable",
152
+ name: "count",
153
+ },
154
+ },
155
+ {
156
+ type: "text",
157
+ value: " Nachrichten.",
158
+ },
159
+ ],
160
+ },
161
+ {
162
+ match: ["many", "*"],
163
+ pattern: [
164
+ {
165
+ type: "expression",
166
+ arg: {
167
+ type: "variable",
168
+ name: "count",
169
+ },
170
+ },
171
+ {
172
+ type: "text",
173
+ value: " Nachrichten zeigen.",
174
+ },
175
+ ],
176
+ },
177
+ {
178
+ match: ["*"],
179
+ pattern: [
180
+ {
181
+ type: "text",
182
+ value: "Zeige ",
183
+ },
184
+ {
185
+ type: "expression",
186
+ arg: {
187
+ type: "variable",
188
+ name: "count",
189
+ },
190
+ },
191
+ ],
192
+ },
193
+ ],
194
+ },
195
+ ],
196
+ }
@@ -1,18 +1,18 @@
1
1
  /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
2
 
3
3
  import { describe, it, expect } from "vitest"
4
- import { bundle } from "./bundle.js"
4
+ import { pluralBundle } from "./bundle.js"
5
5
  import { MessageBundle } from "../../types.js"
6
6
  import { Value } from "@sinclair/typebox/value"
7
7
 
8
8
  describe("mock plural messageBundle", () => {
9
9
  it("is valid", () => {
10
- const messageBundle: unknown = bundle
10
+ const messageBundle: unknown = pluralBundle
11
11
  expect(Value.Check(MessageBundle, messageBundle)).toBe(true)
12
12
 
13
- expect(bundle.messages.length).toBe(2)
14
- expect(bundle.messages[0]!.declarations.length).toBe(1)
15
- expect(bundle.messages[0]!.selectors.length).toBe(1)
16
- expect(bundle.messages[0]!.variants.length).toBe(3)
13
+ expect(pluralBundle.messages.length).toBe(2)
14
+ expect(pluralBundle.messages[0]!.declarations.length).toBe(1)
15
+ expect(pluralBundle.messages[0]!.selectors.length).toBe(1)
16
+ expect(pluralBundle.messages[0]!.variants.length).toBe(3)
17
17
  })
18
18
  })