@svelstack/translator 0.9.0 → 0.9.1

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/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # TypeSafe Internationalization
2
+
3
+ Features:
4
+ - Converting translations from YAML to .ts files
5
+ - Loading translations from .ts files
6
+ - Variable interpolation
7
+ - Typechecking for keys and domains
8
+ - Typechecking for variable interpolation
9
+ - Pluralization support
10
+ - Svelte5 plugin
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm i @svelstack/translator
16
+ npm i @svelstack/translator-svelte # optional
17
+ ```
18
+
19
+ ## Standalone usage
20
+
21
+ First, create a YAML file with translations in `translations/domain.en.yaml`:
22
+ ```yaml
23
+ hello: 'Hello!'
24
+ advanced:
25
+ hello: 'Hello, {name}!'
26
+ ```
27
+
28
+ Generate dictionaries, d.ts files:
29
+
30
+ ```bash
31
+ npx @svelstack/translator-extractor translations output
32
+ ```
33
+
34
+ The output will be:
35
+ ```
36
+ - dictionary
37
+ - en.ts
38
+ - types.d.ts
39
+ - dictionaries.ts
40
+ ```
41
+
42
+ Create instance of Translator:
43
+
44
+ ```typescript
45
+ import { Translator } from '@svelstack/translator';
46
+ import { dictionaries } from './output/dictionaries';
47
+
48
+ const translator = new Translator({
49
+ language: 'en',
50
+ fallbackLanguage: 'en',
51
+ dictionaries,
52
+ });
53
+
54
+ // first argument is a domain (from filename), second is a key
55
+ console.log(translator.trans('domain', 'hello')); // Hello!
56
+ console.log(translator.trans('domain', 'advanced.hello', { name: 'John' })); // Hello, John!
57
+ ```
58
+
59
+ ## Typechecking
60
+
61
+ ```typescript
62
+ import { Translator } from '@svelstack/translator';
63
+ import { dictionaries } from './output/dictionaries';
64
+ import type { MappingForTranslator } from './output/types';
65
+
66
+ export type AppTranslator = TypesafeTranslator<MappingForTranslator>;
67
+
68
+ const translator = new Translator({
69
+ language: 'en',
70
+ fallbackLanguage: 'en',
71
+ dictionaries,
72
+ }) as AppTranslator;
73
+
74
+ translator.trans('domain', 'hello'); // IDE will suggest domains, keys and variables
75
+
76
+ translator.trans('messages', 'hello'); // Error: messages is not a valid domain
77
+ translator.trans('domain', 'hello', {}); // Error: hello does not accept variables
78
+ translator.trans('domain', 'advanced.hello'); // Error: advanced.hello requires variables
79
+ translator.trans('domain', 'advanced.hello', { name: 'John', extra: '' }); // Error: unexpected variable extra
80
+
81
+ ```
82
+
83
+ ## Svelte
84
+
85
+ ```sveltehtml
86
+ <script lang="ts">
87
+ import { SvelteTranslator, type TypesafeSvelteTranslatorConstructor } from '@svelstack/translator-svelte';
88
+ import { dictionaries } from './output/dictionaries';
89
+ import type { MappingForTranslator } from './output/types';
90
+ import { setContext } from 'svelte';
91
+
92
+ export const AppTranslator = SvelteTranslator as TypesafeSvelteTranslatorConstructor<MappingForTranslator>;
93
+
94
+ const translator = new AppTranslator({
95
+ language: 'en',
96
+ fallbackLanguage: 'en',
97
+ dictionaries,
98
+ });
99
+
100
+ setContext(AppTranslator, translator);
101
+ </script>
102
+
103
+ <!-- Translations are loaded asynchronously -->
104
+ {#await translator.wait()}
105
+ Loading translations...
106
+ {:then _}
107
+ ....
108
+ {/await}
109
+
110
+ {translator.trans('domain', 'hello')} <!-- Fully reactive -->
111
+ {translator.language} <!-- Fully reactive -->
112
+
113
+ <button onclick={() => translator.changeLanguage('de')}>Change language</button>
114
+ ```
115
+
116
+ Another component:
117
+ ```sveltehtml
118
+ <script lang="ts">
119
+ const translator = AppTranslator.of();
120
+ </script>
121
+
122
+ {translator.trans('domain', 'hello')}
123
+ ```
package/dist/index.js CHANGED
@@ -257,6 +257,28 @@ var PluralMessageFormatter = class {
257
257
  }
258
258
  };
259
259
  var ParameterMessageFormatter = class {
260
+ /** @type {RegExp} */
261
+ parameterRegex;
262
+ /** @type {string} */
263
+ parameterPlaceholderStart;
264
+ /** @type {string} */
265
+ parameterPlaceholderEnd;
266
+ /**
267
+ * @param {{ parameterPlaceholder: { start: string, end: string } }} settings
268
+ */
269
+ constructor(settings) {
270
+ this.parameterPlaceholderStart = settings.parameterPlaceholder.start;
271
+ this.parameterPlaceholderEnd = settings.parameterPlaceholder.end;
272
+ if (typeof this.parameterPlaceholderStart !== "string") {
273
+ throw new Error("ParameterMessageFormatter: parameterPlaceholder.start must be a string.");
274
+ }
275
+ if (typeof this.parameterPlaceholderEnd !== "string") {
276
+ throw new Error("ParameterMessageFormatter: parameterPlaceholder.end must be a string.");
277
+ }
278
+ const quotedStart = escapeRegExp(this.parameterPlaceholderStart);
279
+ const quotedEnd = escapeRegExp(this.parameterPlaceholderEnd);
280
+ this.parameterRegex = new RegExp(`${quotedStart}\\s*(.*?)\\s*${quotedEnd}`, "g");
281
+ }
260
282
  /**
261
283
  * Formats the message by replacing placeholders with parameter values.
262
284
  * @param {string} message - The message to format.
@@ -268,15 +290,18 @@ var ParameterMessageFormatter = class {
268
290
  if (parameters === void 0) {
269
291
  return message;
270
292
  }
271
- return message.replace(/\{\s*(.*?)\s*}/g, (_, key) => {
293
+ return message.replace(this.parameterRegex, (_, key) => {
272
294
  const val = parameters[key];
273
295
  if (val == null) {
274
- return `{${key}}`;
296
+ return `${this.parameterPlaceholderStart}${key}${this.parameterPlaceholderEnd}`;
275
297
  }
276
298
  return val.toString();
277
299
  });
278
300
  }
279
301
  };
302
+ function escapeRegExp(str) {
303
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
304
+ }
280
305
 
281
306
  // src/index.js
282
307
  var Translator = class {
@@ -304,10 +329,7 @@ var Translator = class {
304
329
  * @private
305
330
  * @type {ChainMessageFormatter}
306
331
  */
307
- formatter = new ChainMessageFormatter([
308
- new PluralMessageFormatter(),
309
- new ParameterMessageFormatter()
310
- ]);
332
+ formatter;
311
333
  /**
312
334
  * @param {import('./types.js').TranslatorOptions} options - Configuration options for the Translator.
313
335
  * @throws {Error} If the fallback language is not in the dictionaries.
@@ -320,6 +342,14 @@ var Translator = class {
320
342
  this._language = this.getLanguage(this.options.language);
321
343
  this.report = this.options.report || function() {
322
344
  };
345
+ if (Array.isArray(options.formatters)) {
346
+ this.formatter = new ChainMessageFormatter(options.formatters);
347
+ } else {
348
+ this.formatter = new ChainMessageFormatter([
349
+ new PluralMessageFormatter(),
350
+ new ParameterMessageFormatter({ parameterPlaceholder: { start: "{", end: "}" } })
351
+ ]);
352
+ }
323
353
  this.load(this._language);
324
354
  }
325
355
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelstack/translator",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "scripts": {
5
5
  "build": "tsup && npm run prepack",
6
6
  "prepack": "publint",
package/src/types.d.ts CHANGED
@@ -8,6 +8,7 @@ export type TranslatorOptions = {
8
8
  dictionaries: {
9
9
  [lang: string]: (() => Promise<Dictionary>) | Dictionary;
10
10
  },
11
+ formatters?: MessageFormatter[];
11
12
  report?: (error: any) => void;
12
13
  };
13
14