@inlang/sdk 0.34.4 → 0.34.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.
@@ -3,23 +3,19 @@ import type {
3
3
  InstalledMessageLintRule,
4
4
  MessageLintReportsQueryApi,
5
5
  MessageQueryApi,
6
+ MessageQueryDelegate,
6
7
  } from "./api.js"
7
8
  import type { ProjectSettings } from "@inlang/project-settings"
8
9
  import type { resolveModules } from "./resolve-modules/index.js"
9
10
  import type { MessageLintReport, Message } from "./versionedInterfaces.js"
10
11
  import { lintSingleMessage } from "./lint/index.js"
11
- import { createRoot, createEffect } from "./reactivity/solid.js"
12
-
13
- import { throttle } from "throttle-debounce"
14
- import _debug from "debug"
15
- const debug = _debug("sdk:lintReports")
16
12
 
17
13
  function sleep(ms: number) {
18
14
  return new Promise((resolve) => setTimeout(resolve, ms))
19
15
  }
20
16
 
21
17
  /**
22
- * Creates a reactive query API for messages.
18
+ * Creates a ~~reactive~~ query API for lint reports.
23
19
  */
24
20
  export function createMessageLintReportsQuery(
25
21
  messagesQuery: MessageQueryApi,
@@ -42,72 +38,54 @@ export function createMessageLintReportsQuery(
42
38
  }
43
39
  }
44
40
 
45
- const messages = messagesQuery.getAll() as Message[]
46
-
47
- const trackedMessages: Map<string, () => void> = new Map()
48
-
49
- debug(`createMessageLintReportsQuery ${rulesArray?.length} rules, ${messages.length} messages`)
50
-
51
- // TODO: don't throttle when no debug
52
- let lintMessageCount = 0
53
- const throttledLogLintMessage = throttle(2000, (messageId) => {
54
- debug(`lintSingleMessage: ${lintMessageCount} id: ${messageId}`)
55
- })
56
-
57
- createEffect(() => {
58
- const currentMessageIds = new Set(messagesQuery.includedMessageIds())
59
-
60
- const deletedTrackedMessages = [...trackedMessages].filter(
61
- (tracked) => !currentMessageIds.has(tracked[0])
62
- )
63
-
64
- if (rulesArray) {
65
- for (const messageId of currentMessageIds) {
66
- if (!trackedMessages.has(messageId)) {
67
- createRoot((dispose) => {
68
- createEffect(() => {
69
- const message = messagesQuery.get({ where: { id: messageId } })
70
- if (!message) {
71
- return
72
- }
73
- if (!trackedMessages?.has(messageId)) {
74
- // initial effect execution - add dispose function
75
- trackedMessages?.set(messageId, dispose)
76
- }
41
+ const lintMessage = (message: Message, messages: Message[]) => {
42
+ if (!rulesArray) {
43
+ return
44
+ }
77
45
 
78
- lintSingleMessage({
79
- rules: rulesArray,
80
- settings: settingsObject(),
81
- messages: messages,
82
- message: message,
83
- }).then((report) => {
84
- lintMessageCount++
85
- throttledLogLintMessage(messageId)
86
- if (report.errors.length === 0 && index.get(messageId) !== report.data) {
87
- // console.log("lintSingleMessage", messageId, report.data.length)
88
- index.set(messageId, report.data)
89
- }
90
- })
91
- })
92
- })
93
- }
46
+ // TODO unhandled promise rejection (as before the refactor) but won't tackle this in this pr
47
+ lintSingleMessage({
48
+ rules: rulesArray,
49
+ settings: settingsObject(),
50
+ messages: messages,
51
+ message: message,
52
+ }).then((report) => {
53
+ if (report.errors.length === 0 && index.get(message.id) !== report.data) {
54
+ // console.log("lintSingleMessage", messageId, report.data.length)
55
+ index.set(message.id, report.data)
94
56
  }
57
+ })
58
+ }
95
59
 
96
- for (const deletedMessage of deletedTrackedMessages) {
97
- const deletedMessageId = deletedMessage[0]
60
+ const messages = messagesQuery.getAll() as Message[]
61
+ // load report for all messages once
62
+ for (const message of messages) {
63
+ // NOTE: this potentually creates thousands of promisses we could create a promise that batches linting
64
+ lintMessage(message, messages)
65
+ }
98
66
 
99
- // call dispose to cleanup the effect
100
- const messageEffectDisposeFunction = trackedMessages.get(deletedMessageId)
101
- if (messageEffectDisposeFunction) {
102
- messageEffectDisposeFunction()
103
- trackedMessages.delete(deletedMessageId)
104
- // remove lint report result
105
- index.delete(deletedMessageId)
106
- debug(`delete lint message id: ${deletedMessageId}`)
107
- }
67
+ const messageQueryChangeDelegate: MessageQueryDelegate = {
68
+ onCleanup: () => {
69
+ // NOTE: we could cancel all running lint rules - but results get overritten anyway
70
+ index.clear()
71
+ },
72
+ onLoaded: (messages: Message[]) => {
73
+ for (const message of messages) {
74
+ lintMessage(message, messages)
108
75
  }
109
- }
110
- })
76
+ },
77
+ onMessageCreate: (messageId: string, message: Message) => {
78
+ lintMessage(message, messages)
79
+ },
80
+ onMessageUpdate: (messageId: string, message: Message) => {
81
+ lintMessage(message, messages)
82
+ },
83
+ onMessageDelete: (messageId: string) => {
84
+ index.delete(messageId)
85
+ },
86
+ }
87
+
88
+ messagesQuery.setDelegate(messageQueryChangeDelegate)
111
89
 
112
90
  return {
113
91
  getAll: async () => {
@@ -2,7 +2,7 @@ import type { Message } from "@inlang/message"
2
2
  import { ReactiveMap } from "./reactivity/map.js"
3
3
  import { createEffect } from "./reactivity/solid.js"
4
4
  import { createSubscribable } from "./loadProject.js"
5
- import type { InlangProject, MessageQueryApi } from "./api.js"
5
+ import type { InlangProject, MessageQueryApi, MessageQueryDelegate } from "./api.js"
6
6
  import type { ResolvedPluginApi } from "./resolve-modules/plugins/types.js"
7
7
  import type { resolveModules } from "./resolve-modules/resolveModules.js"
8
8
  import { createNodeishFsWithWatcher } from "./createNodeishFsWithWatcher.js"
@@ -17,6 +17,10 @@ import { PluginLoadMessagesError, PluginSaveMessagesError } from "./errors.js"
17
17
  import { humanIdHash } from "./storage/human-id/human-readable-id.js"
18
18
  const debug = _debug("sdk:createMessagesQuery")
19
19
 
20
+ function sleep(ms: number) {
21
+ return new Promise((resolve) => setTimeout(resolve, ms))
22
+ }
23
+
20
24
  type MessageState = {
21
25
  messageDirtyFlags: {
22
26
  [messageId: string]: boolean
@@ -62,6 +66,12 @@ export function createMessagesQuery({
62
66
  // filepath for the lock folder
63
67
  const messageLockDirPath = projectPath + "/messagelock"
64
68
 
69
+ let delegate: MessageQueryDelegate | undefined = undefined
70
+
71
+ const setDelegate = (newDelegate: MessageQueryDelegate) => {
72
+ delegate = newDelegate
73
+ }
74
+
65
75
  // Map default alias to message
66
76
  // Assumes that aliases are only created and deleted, not updated
67
77
  // TODO #2346 - handle updates to aliases
@@ -98,6 +108,7 @@ export function createMessagesQuery({
98
108
  onCleanup(() => {
99
109
  // stop listening on fs events
100
110
  abortController.abort()
111
+ delegate?.onCleanup()
101
112
  })
102
113
 
103
114
  const fsWithWatcher = createNodeishFsWithWatcher({
@@ -111,6 +122,7 @@ export function createMessagesQuery({
111
122
  messageLockDirPath,
112
123
  messageStates,
113
124
  index,
125
+ delegate,
114
126
  _settings, // NOTE we bang here - we don't expect the settings to become null during the livetime of a project
115
127
  resolvedPluginApi
116
128
  )
@@ -133,6 +145,7 @@ export function createMessagesQuery({
133
145
  messageLockDirPath,
134
146
  messageStates,
135
147
  index,
148
+ delegate,
136
149
  _settings, // NOTE we bang here - we don't expect the settings to become null during the livetime of a project
137
150
  resolvedPluginApi
138
151
  )
@@ -142,6 +155,7 @@ export function createMessagesQuery({
142
155
  })
143
156
  .then(() => {
144
157
  onInitialMessageLoadResult()
158
+ delegate?.onLoaded([...index.values()])
145
159
  })
146
160
  })
147
161
 
@@ -165,6 +179,7 @@ export function createMessagesQuery({
165
179
  messageLockDirPath,
166
180
  messageStates,
167
181
  index,
182
+ delegate,
168
183
  _settings, // NOTE we bang here - we don't expect the settings to become null during the livetime of a project
169
184
  resolvedPluginApi
170
185
  )
@@ -181,6 +196,7 @@ export function createMessagesQuery({
181
196
  }
182
197
 
183
198
  return {
199
+ setDelegate,
184
200
  create: ({ data }): boolean => {
185
201
  if (index.has(data.id)) return false
186
202
  index.set(data.id, data)
@@ -189,6 +205,7 @@ export function createMessagesQuery({
189
205
  }
190
206
 
191
207
  messageStates.messageDirtyFlags[data.id] = true
208
+ delegate?.onMessageCreate(data.id, index.get(data.id))
192
209
  scheduleSave()
193
210
  return true
194
211
  },
@@ -215,6 +232,7 @@ export function createMessagesQuery({
215
232
  if (message === undefined) return false
216
233
  index.set(where.id, { ...message, ...data })
217
234
  messageStates.messageDirtyFlags[where.id] = true
235
+ delegate?.onMessageCreate(where.id, index.get(data.id))
218
236
  scheduleSave()
219
237
  return true
220
238
  },
@@ -225,10 +243,13 @@ export function createMessagesQuery({
225
243
  if ("default" in data.alias) {
226
244
  defaultAliasIndex.set(data.alias.default, data)
227
245
  }
246
+ messageStates.messageDirtyFlags[where.id] = true
247
+ delegate?.onMessageCreate(data.id, index.get(data.id))
228
248
  } else {
229
249
  index.set(where.id, { ...message, ...data })
250
+ messageStates.messageDirtyFlags[where.id] = true
251
+ delegate?.onMessageUpdate(data.id, index.get(data.id))
230
252
  }
231
- messageStates.messageDirtyFlags[where.id] = true
232
253
  scheduleSave()
233
254
  return true
234
255
  },
@@ -240,6 +261,7 @@ export function createMessagesQuery({
240
261
  }
241
262
  index.delete(where.id)
242
263
  messageStates.messageDirtyFlags[where.id] = true
264
+ delegate?.onMessageDelete(where.id)
243
265
  scheduleSave()
244
266
  return true
245
267
  },
@@ -253,6 +275,8 @@ export function createMessagesQuery({
253
275
  // - saving a message in two different languages would lead to a write in de.json first
254
276
  // - This will leads to a load of the messages and since en.json has not been saved yet the english variant in the message would get overritten with the old state again
255
277
 
278
+ const maxMessagesPerTick = 500
279
+
256
280
  /**
257
281
  * Messsage that loads messages from a plugin - this method synchronizes with the saveMessage funciton.
258
282
  * If a save is in progress loading will wait until saving is done. If another load kicks in during this load it will queue the
@@ -271,6 +295,7 @@ async function loadMessagesViaPlugin(
271
295
  lockDirPath: string,
272
296
  messageState: MessageState,
273
297
  messages: Map<string, Message>,
298
+ delegate: MessageQueryDelegate | undefined,
274
299
  settingsValue: ProjectSettings,
275
300
  resolvedPluginApi: ResolvedPluginApi
276
301
  ) {
@@ -298,6 +323,8 @@ async function loadMessagesViaPlugin(
298
323
  })
299
324
  )
300
325
 
326
+ let loadedMessageCount = 0
327
+
301
328
  for (const loadedMessage of loadedMessages) {
302
329
  const loadedMessageClone = structuredClone(loadedMessage)
303
330
 
@@ -339,6 +366,8 @@ async function loadMessagesViaPlugin(
339
366
  messages.set(loadedMessageClone.id, loadedMessageClone)
340
367
  // NOTE could use hash instead of the whole object JSON to save memory...
341
368
  messageState.messageLoadHash[loadedMessageClone.id] = importedEnecoded
369
+ delegate?.onMessageUpdate(loadedMessageClone.id, loadedMessageClone)
370
+ loadedMessageCount++
342
371
  } else {
343
372
  // message with the given alias does not exist so far
344
373
  loadedMessageClone.alias = {} as any
@@ -365,6 +394,15 @@ async function loadMessagesViaPlugin(
365
394
  // we don't have to check - done before hand if (messages.has(loadedMessageClone.id)) return false
366
395
  messages.set(loadedMessageClone.id, loadedMessageClone)
367
396
  messageState.messageLoadHash[loadedMessageClone.id] = importedEnecoded
397
+ delegate?.onMessageUpdate(loadedMessageClone.id, loadedMessageClone)
398
+ loadedMessageCount++
399
+ }
400
+ if (loadedMessageCount > maxMessagesPerTick) {
401
+ // move loading of the next messages to the next ticks to allow solid to cleanup resources
402
+ // solid needs some time to settle and clean up
403
+ // https://github.com/solidjs-community/solid-primitives/blob/9ca76a47ffa2172770e075a90695cf933da0ff48/packages/trigger/src/index.ts#L64
404
+ await sleep(0)
405
+ loadedMessageCount = 0
368
406
  }
369
407
  }
370
408
  await releaseLock(fs as NodeishFilesystem, lockDirPath, "loadMessage", lockTime)
@@ -388,7 +426,15 @@ async function loadMessagesViaPlugin(
388
426
  messageState.sheduledLoadMessagesViaPlugin = undefined
389
427
 
390
428
  // recall load unawaited to allow stack to pop
391
- loadMessagesViaPlugin(fs, lockDirPath, messageState, messages, settingsValue, resolvedPluginApi)
429
+ loadMessagesViaPlugin(
430
+ fs,
431
+ lockDirPath,
432
+ messageState,
433
+ messages,
434
+ delegate,
435
+ settingsValue,
436
+ resolvedPluginApi
437
+ )
392
438
  .then(() => {
393
439
  // resolve the scheduled load message promise
394
440
  executingScheduledMessages.resolve()
@@ -405,6 +451,7 @@ async function saveMessagesViaPlugin(
405
451
  lockDirPath: string,
406
452
  messageState: MessageState,
407
453
  messages: Map<string, Message>,
454
+ delegate: MessageQueryDelegate | undefined,
408
455
  settingsValue: ProjectSettings,
409
456
  resolvedPluginApi: ResolvedPluginApi
410
457
  ): Promise<void> {
@@ -491,6 +538,7 @@ async function saveMessagesViaPlugin(
491
538
  lockDirPath,
492
539
  messageState,
493
540
  messages,
541
+ delegate,
494
542
  settingsValue,
495
543
  resolvedPluginApi
496
544
  )
@@ -530,7 +578,15 @@ async function saveMessagesViaPlugin(
530
578
  const executingSheduledSaveMessages = messageState.sheduledSaveMessages
531
579
  messageState.sheduledSaveMessages = undefined
532
580
 
533
- saveMessagesViaPlugin(fs, lockDirPath, messageState, messages, settingsValue, resolvedPluginApi)
581
+ saveMessagesViaPlugin(
582
+ fs,
583
+ lockDirPath,
584
+ messageState,
585
+ messages,
586
+ delegate,
587
+ settingsValue,
588
+ resolvedPluginApi
589
+ )
534
590
  .then(() => {
535
591
  executingSheduledSaveMessages.resolve()
536
592
  })
@@ -33,7 +33,6 @@ import { identifyProject } from "./telemetry/groupIdentify.js"
33
33
  import _debug from "debug"
34
34
  const debug = _debug("sdk:loadProject")
35
35
 
36
-
37
36
  const settingsCompiler = TypeCompiler.Compile(ProjectSettings)
38
37
 
39
38
  /**
@@ -0,0 +1 @@
1
+ export type * from "./types.js"
@@ -0,0 +1,142 @@
1
+ import { Type, type Static } from "@sinclair/typebox"
2
+
3
+ /**
4
+ * Follows the IETF BCP 47 language tag schema.
5
+ *
6
+ * @see https://www.ietf.org/rfc/bcp/bcp47.txt
7
+ * @see https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
8
+ */
9
+ export type LanguageTag = Static<typeof LanguageTag>
10
+ /**
11
+ * Follows the IETF BCP 47 language tag schema with modifications.
12
+ * @see REAMDE.md file for more information on the validation.
13
+ */
14
+
15
+ export const pattern =
16
+ "^((?<grandfathered>(en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang))|((?<language>([A-Za-z]{2,3}(-(?<extlang>[A-Za-z]{3}(-[A-Za-z]{3}){0,2}))?))(-(?<script>[A-Za-z]{4}))?(-(?<region>[A-Za-z]{2}|[0-9]{3}))?(-(?<variant>[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*))$"
17
+
18
+ export const LanguageTag = Type.String({
19
+ pattern: pattern,
20
+ description: "The language tag must be a valid IETF BCP 47 language tag.",
21
+ examples: ["en", "de", "en-US", "zh-Hans", "es-419"],
22
+ })
23
+
24
+ export type Literal = Static<typeof Literal>
25
+ export const Literal = Type.Object({
26
+ type: Type.Literal("literal"),
27
+ value: Type.String(),
28
+ })
29
+
30
+ /**
31
+ * A (text) element that is translatable and rendered to the UI.
32
+ */
33
+ export type Text = Static<typeof Text>
34
+ export const Text = Type.Object({
35
+ type: Type.Literal("text"),
36
+ value: Type.String(),
37
+ })
38
+
39
+ export type VariableReference = Static<typeof VariableReference>
40
+ export const VariableReference = Type.Object({
41
+ type: Type.Literal("variable"),
42
+ name: Type.String(),
43
+ })
44
+
45
+ export type Option = Static<typeof Option>
46
+ export const Option = Type.Object({
47
+ name: Type.String(),
48
+ value: Type.Union([Literal, VariableReference]),
49
+ })
50
+
51
+ export type FunctionAnnotation = Static<typeof FunctionAnnotation>
52
+ export const FunctionAnnotation = Type.Object({
53
+ type: Type.Literal("function"),
54
+ name: Type.String(),
55
+ options: Type.Array(Option),
56
+ })
57
+
58
+ /**
59
+ * An expression is a reference to a variable or a function.
60
+ *
61
+ * Think of expressions as elements that are rendered to a
62
+ * text value during runtime.
63
+ */
64
+ export type Expression = Static<typeof Expression>
65
+ export const Expression = Type.Object({
66
+ type: Type.Literal("expression"),
67
+ arg: Type.Union([Literal, VariableReference]),
68
+ annotation: Type.Optional(FunctionAnnotation),
69
+ })
70
+
71
+ // export type FunctionReference = {
72
+ // type: "function"
73
+ // name: string
74
+ // operand?: Text | VariableReference
75
+ // options?: Option[]
76
+ // }
77
+
78
+ /**
79
+ * A pattern is a sequence of elements that comprise
80
+ * a message that is rendered to the UI.
81
+ */
82
+ export type Pattern = Static<typeof Pattern>
83
+ export const Pattern = Type.Array(Type.Union([Text, Expression]))
84
+
85
+ /**
86
+ * A variant contains a pattern that is rendered to the UI.
87
+ */
88
+ export type Variant = Static<typeof Variant>
89
+ export const Variant = Type.Object({
90
+ /**
91
+ * The number of keys in each variant match MUST equal the number of expressions in the selectors.
92
+ *
93
+ * Inspired by: https://github.com/unicode-org/message-format-wg/blob/main/spec/formatting.md#pattern-selection
94
+ */
95
+ // a match can always only be string-based because a string is what is rendered to the UI
96
+ match: Type.Array(Type.String()),
97
+ pattern: Pattern,
98
+ })
99
+
100
+ export type InputDeclaration = Static<typeof InputDeclaration>
101
+ export const InputDeclaration = Type.Object({
102
+ type: Type.Literal("input"),
103
+ name: Type.String(),
104
+
105
+ //TODO make this generic so that only Variable-Ref Expressions are allowed
106
+ value: Expression,
107
+ })
108
+
109
+ // local declarations are not supported.
110
+ // Will only add when required. See discussion:
111
+ // https://github.com/opral/monorepo/pull/2700#discussion_r1591070701
112
+ //
113
+ // export type LocalDeclaration = Static<typeof InputDeclaration>
114
+ // export const LocalDeclaration = Type.Object({
115
+ // type: Type.Literal("local"),
116
+ // name: Type.String(),
117
+ // value: Expression,
118
+ // })
119
+ //
120
+ // export type Declaration = Static<typeof Declaration>
121
+ // export const Declaration = Type.Union([LocalDeclaration, InputDeclaration])
122
+
123
+ export type Declaration = Static<typeof Declaration>
124
+ export const Declaration = Type.Union([InputDeclaration])
125
+
126
+ export type Message = Static<typeof Message>
127
+ export const Message = Type.Object({
128
+ locale: LanguageTag,
129
+ declarations: Type.Array(Declaration),
130
+ /**
131
+ * The order in which the selectors are placed determines the precedence of patterns.
132
+ */
133
+ selectors: Type.Array(Expression),
134
+ variants: Type.Array(Variant),
135
+ })
136
+
137
+ export type MessageBundle = Static<typeof MessageBundle>
138
+ export const MessageBundle = Type.Object({
139
+ id: Type.String(),
140
+ alias: Type.Record(Type.String(), Type.String()),
141
+ messages: Type.Array(Message),
142
+ })