@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.
- package/dist/adapter/solidAdapter.test.js +1 -1
- package/dist/api.d.ts +2 -13
- package/dist/api.d.ts.map +1 -1
- package/dist/createMessagesQuery.d.ts.map +1 -1
- package/dist/createMessagesQuery.js +68 -82
- package/dist/createNewProject.test.js +1 -3
- package/dist/loadProject.d.ts.map +1 -1
- package/dist/loadProject.js +67 -32
- package/dist/loadProject.test.js +6 -2
- package/dist/persistence/batchedIO.d.ts +11 -0
- package/dist/persistence/batchedIO.d.ts.map +1 -0
- package/dist/persistence/batchedIO.js +49 -0
- package/dist/persistence/batchedIO.test.d.ts +2 -0
- package/dist/persistence/batchedIO.test.d.ts.map +1 -0
- package/dist/persistence/batchedIO.test.js +56 -0
- package/dist/persistence/filelock/acquireFileLock.d.ts.map +1 -1
- package/dist/persistence/filelock/acquireFileLock.js +3 -1
- package/dist/persistence/filelock/releaseLock.d.ts.map +1 -1
- package/dist/persistence/filelock/releaseLock.js +2 -1
- package/dist/persistence/store.d.ts +107 -0
- package/dist/persistence/store.d.ts.map +1 -0
- package/dist/persistence/store.js +99 -0
- package/dist/persistence/store.test.d.ts +2 -0
- package/dist/persistence/store.test.d.ts.map +1 -0
- package/dist/persistence/store.test.js +79 -0
- package/dist/persistence/storeApi.d.ts +22 -0
- package/dist/persistence/storeApi.d.ts.map +1 -0
- package/dist/persistence/storeApi.js +1 -0
- package/dist/reactivity/solid.test.js +1 -6
- package/dist/resolve-modules/plugins/resolvePlugins.d.ts.map +1 -1
- package/dist/resolve-modules/plugins/resolvePlugins.js +3 -10
- package/dist/test-utilities/sleep.d.ts +4 -0
- package/dist/test-utilities/sleep.d.ts.map +1 -0
- package/dist/test-utilities/sleep.js +9 -0
- package/dist/v2/helper.d.ts +131 -0
- package/dist/v2/helper.d.ts.map +1 -0
- package/dist/v2/helper.js +75 -0
- package/dist/v2/helper.test.d.ts +2 -0
- package/dist/v2/helper.test.d.ts.map +1 -0
- package/dist/v2/{createMessageBundle.test.js → helper.test.js} +1 -1
- package/dist/v2/index.d.ts +2 -0
- package/dist/v2/index.d.ts.map +1 -1
- package/dist/v2/index.js +2 -1
- package/dist/v2/mocks/index.d.ts +3 -0
- package/dist/v2/mocks/index.d.ts.map +1 -0
- package/dist/v2/mocks/index.js +2 -0
- package/dist/v2/mocks/multipleMatcher/bundle.d.ts +3 -0
- package/dist/v2/mocks/multipleMatcher/bundle.d.ts.map +1 -0
- package/dist/v2/mocks/multipleMatcher/bundle.js +194 -0
- package/dist/v2/mocks/multipleMatcher/bundle.test.d.ts +2 -0
- package/dist/v2/mocks/multipleMatcher/bundle.test.d.ts.map +1 -0
- package/dist/v2/mocks/multipleMatcher/bundle.test.js +10 -0
- package/dist/v2/mocks/plural/bundle.d.ts +1 -1
- package/dist/v2/mocks/plural/bundle.d.ts.map +1 -1
- package/dist/v2/mocks/plural/bundle.js +1 -1
- package/dist/v2/mocks/plural/bundle.test.js +6 -6
- package/dist/v2/shim.d.ts +12 -0
- package/dist/v2/shim.d.ts.map +1 -0
- package/dist/v2/shim.js +151 -0
- package/dist/v2/shim.test.d.ts +2 -0
- package/dist/v2/shim.test.d.ts.map +1 -0
- package/dist/v2/shim.test.js +49 -0
- package/dist/v2/stubQueryApi.d.ts +9 -0
- package/dist/v2/stubQueryApi.d.ts.map +1 -0
- package/dist/v2/stubQueryApi.js +38 -0
- package/dist/v2/types.d.ts +110 -0
- package/dist/v2/types.d.ts.map +1 -1
- package/dist/v2/types.js +9 -0
- package/package.json +9 -8
- package/src/adapter/solidAdapter.test.ts +1 -1
- package/src/api.ts +2 -13
- package/src/createMessagesQuery.ts +80 -99
- package/src/createNewProject.test.ts +1 -4
- package/src/loadProject.test.ts +6 -2
- package/src/loadProject.ts +86 -45
- package/src/persistence/batchedIO.test.ts +63 -0
- package/src/persistence/batchedIO.ts +64 -0
- package/src/persistence/filelock/acquireFileLock.ts +5 -2
- package/src/persistence/filelock/releaseLock.ts +2 -1
- package/src/persistence/store.test.ts +102 -0
- package/src/persistence/store.ts +119 -0
- package/src/persistence/storeApi.ts +19 -0
- package/src/reactivity/solid.test.ts +1 -8
- package/src/resolve-modules/plugins/resolvePlugins.ts +4 -13
- package/src/test-utilities/sleep.ts +11 -0
- package/src/v2/{createMessageBundle.test.ts → helper.test.ts} +1 -1
- package/src/v2/helper.ts +98 -0
- package/src/v2/index.ts +2 -0
- package/src/v2/mocks/index.ts +2 -0
- package/src/v2/mocks/multipleMatcher/bundle.test.ts +11 -0
- package/src/v2/mocks/multipleMatcher/bundle.ts +196 -0
- package/src/v2/mocks/plural/bundle.test.ts +6 -6
- package/src/v2/mocks/plural/bundle.ts +1 -1
- package/src/v2/shim.test.ts +56 -0
- package/src/v2/shim.ts +173 -0
- package/src/v2/stubQueryApi.ts +43 -0
- package/src/v2/types.ts +17 -0
- package/dist/persistence/plugin.d.ts +0 -31
- package/dist/persistence/plugin.d.ts.map +0 -1
- package/dist/persistence/plugin.js +0 -42
- package/dist/persistence/plugin.test.d.ts +0 -2
- package/dist/persistence/plugin.test.d.ts.map +0 -1
- package/dist/persistence/plugin.test.js +0 -49
- package/dist/v2/createMessageBundle.d.ts +0 -25
- package/dist/v2/createMessageBundle.d.ts.map +0 -1
- package/dist/v2/createMessageBundle.js +0 -36
- package/dist/v2/createMessageBundle.test.d.ts +0 -2
- package/dist/v2/createMessageBundle.test.d.ts.map +0 -1
- package/src/persistence/plugin.test.ts +0 -60
- package/src/persistence/plugin.ts +0 -56
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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 "./
|
|
2
|
+
import { createMessageBundle, createMessage } from "./helper.js"
|
|
3
3
|
import { MessageBundle } from "./types.js"
|
|
4
4
|
import { Value } from "@sinclair/typebox/value"
|
|
5
5
|
|
package/src/v2/helper.ts
ADDED
|
@@ -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
|
@@ -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 {
|
|
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 =
|
|
10
|
+
const messageBundle: unknown = pluralBundle
|
|
11
11
|
expect(Value.Check(MessageBundle, messageBundle)).toBe(true)
|
|
12
12
|
|
|
13
|
-
expect(
|
|
14
|
-
expect(
|
|
15
|
-
expect(
|
|
16
|
-
expect(
|
|
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
|
})
|