@juit/vue-i18n 0.4.0 → 1.0.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.
package/README.md CHANGED
@@ -1,13 +1,15 @@
1
1
  Juit I18n for Vue
2
2
  =================
3
3
 
4
- The `@juit/vue-i18n` package provides a _minimalistic_ plugin for VueJS (v. 3)
5
- to support basic internationalization (translations, numbers, and date formats).
4
+ The `@juit/vue-i18n` package provides a _minimal_ plugin for Vue 3
5
+ to support basic internationalization (translations, number formatting, and
6
+ date formatting).
6
7
 
7
- It heavily relies on the [`Intl.Locale`][1], [`Intl.NumberFormat`][2], and
8
- [`Intl.DateTimeFormat`][3] global objects widely supported by modern browsers.
8
+ It relies heavily on the [`Intl.Locale`][1], [`Intl.NumberFormat`][2], and
9
+ [`Intl.DateTimeFormat`][3] global objects, which are widely supported by modern
10
+ browsers.
9
11
 
10
- It also deeply integrates with TypeScript to provide compile-time checking on
12
+ It also integrates with TypeScript to provide compile-time checks for
11
13
  required translation languages and translation keys.
12
14
 
13
15
 
@@ -15,6 +17,8 @@ required translation languages and translation keys.
15
17
 
16
18
  - [Installation](#installation)
17
19
  - [Configuration](#configuration)
20
+ - [Date and Time Format Aliases](#date-and-time-format-aliases)
21
+ - [Number Format Aliases](#number-format-aliases)
18
22
  - [Usage](#usage)
19
23
  - [Switching language](#switching-language)
20
24
  - [Translating messages](#translating-messages)
@@ -23,18 +27,21 @@ required translation languages and translation keys.
23
27
  - [Formatting numbers](#formatting-numbers)
24
28
  - [Formatting dates](#formatting-dates)
25
29
  - [Configuring Types](#configuring-types)
30
+ - [Updating Translations](#updating-translations)
31
+ - [Language Matching](#language-matching)
32
+ - [Remarks](#remarks)
26
33
  - [Legal Stuff](#legal-stuff)
27
34
 
28
35
 
29
36
  ## Installation
30
37
 
31
- As usual, install with NPM (or the cool package-manager du jour):
38
+ As usual, install with npm (or the cool package manager du jour):
32
39
 
33
40
  ```bash
34
41
  npm install '@juit/vue-i18n'
35
42
  ```
36
43
 
37
- And add the plugin to your Vue app:
44
+ Then add the plugin to your Vue app:
38
45
 
39
46
  ```typescript
40
47
  import { createApp } from 'vue'
@@ -55,23 +62,23 @@ const app = createApp(MyApp).use(i18n, {
55
62
 
56
63
  ## Configuration
57
64
 
58
- The plugin can be configured with a simple string (the `defaultLanguage`)
59
- described below, or some options:
65
+ The plugin can be configured with a language code string (the default
66
+ language) or an object containing the following options:
60
67
 
61
68
  * `defaultLanguage`: **(required)** the default language to use; all
62
69
  translations should be available in this language.
63
70
  * `defaultTimeZone`: the default time zone to use when formatting dates
64
71
  (defaults to the _local_ time zone).
65
72
  * `translations`: an object containing the translations for the messages to
66
- translate, keyed by its identifier.
67
- * `dateTimeFormats`: date and time format aliases used formatting dates.
68
- * `numberFormats`: number format aliases used formatting numbers.
73
+ translate, keyed by message identifier.
74
+ * `dateTimeFormats`: date and time format aliases used when formatting dates.
75
+ * `numberFormats`: number format aliases used when formatting numbers.
69
76
 
70
77
 
71
- ### Date Time Format Aliases
78
+ ### Date and Time Format Aliases
72
79
 
73
- Date time formatting aliases can be configured keyed by a simple string and
74
- values as [`Intl.DateTimeFormatOptions`][5]
80
+ Date and time format aliases can be configured using string keys and
81
+ [`Intl.DateTimeFormatOptions`][5] values:
75
82
 
76
83
  ```typescript
77
84
  import { createApp } from 'vue'
@@ -93,37 +100,37 @@ const app = createApp(MyApp).use(i18n, {
93
100
  The default (overridable) formats are as follows:
94
101
 
95
102
  ```typescript
96
- {
97
- // used when no alias or date time format is specified
98
- default: { dateStyle: 'medium', timeStyle: 'medium' },
99
-
100
- // generic formats
101
- short: { dateStyle: 'short', timeStyle: 'short' },
102
- medium: { dateStyle: 'medium', timeStyle: 'medium' },
103
- long: { dateStyle: 'long', timeStyle: 'long' },
104
- full: { dateStyle: 'full', timeStyle: 'full' },
105
-
106
- // date only formats
107
- date: { dateStyle: 'medium' },
108
- shortDate: { dateStyle: 'short' },
109
- mediumDate: { dateStyle: 'medium' },
110
- longDate: { dateStyle: 'long' },
111
- fullDate: { dateStyle: 'full' },
112
-
113
- // time only formats
114
- time: { timeStyle: 'medium' },
115
- shortTime: { timeStyle: 'short' },
116
- mediumTime: { timeStyle: 'medium' },
117
- longTime: { timeStyle: 'long' },
118
- fullTime: { timeStyle: 'full' },
103
+ const dateTimeFormats = {
104
+ // used when no alias or date and time format is specified
105
+ default: { dateStyle: 'medium', timeStyle: 'medium' },
106
+
107
+ // generic formats
108
+ short: { dateStyle: 'short', timeStyle: 'short' },
109
+ medium: { dateStyle: 'medium', timeStyle: 'medium' },
110
+ long: { dateStyle: 'long', timeStyle: 'long' },
111
+ full: { dateStyle: 'full', timeStyle: 'full' },
112
+
113
+ // date only formats
114
+ date: { dateStyle: 'medium' },
115
+ shortDate: { dateStyle: 'short' },
116
+ mediumDate: { dateStyle: 'medium' },
117
+ longDate: { dateStyle: 'long' },
118
+ fullDate: { dateStyle: 'full' },
119
+
120
+ // time only formats
121
+ time: { timeStyle: 'medium' },
122
+ shortTime: { timeStyle: 'short' },
123
+ mediumTime: { timeStyle: 'medium' },
124
+ longTime: { timeStyle: 'long' },
125
+ fullTime: { timeStyle: 'full' },
119
126
  }
120
127
  ```
121
128
 
122
129
 
123
130
  ### Number Format Aliases
124
131
 
125
- Similarly to date time, also number formatting aliases can be configured keyed
126
- by a simple string and values as [`Intl.NumberFormatOptions`][4]
132
+ Similarly, number format aliases can be configured using string keys and
133
+ [`Intl.NumberFormatOptions`][4] values:
127
134
 
128
135
  ```typescript
129
136
  import { createApp } from 'vue'
@@ -141,16 +148,16 @@ const app = createApp(MyApp).use(i18n, {
141
148
  })
142
149
  ```
143
150
 
144
- While there is no intrinsic default, each valid ISO-4217 currency code
145
- (e.g. `EUR`, `USD`, ...) can be used as an alias.
151
+ By default, numbers use the locale's standard formatting. Each valid ISO 4217
152
+ currency code (e.g. `EUR`, `USD`, ...) can be used as an alias.
146
153
 
147
- To configure the default number format use the `default` key.
154
+ To configure the default number format, use the `default` key.
148
155
 
149
156
 
150
157
  ## Usage
151
158
 
152
- In any component `setup()` method, you can use the `useTranslator()` function
153
- to get a hold on the `Translator` configured for the current app.
159
+ In any component's `setup()` method, you can use the `useTranslator()` function
160
+ to retrieve the `Translator` configured for the current app.
154
161
 
155
162
  ```typescript
156
163
  import { useTranslator } from '@juit/vue-i18n'
@@ -160,7 +167,7 @@ const translator = useTranslator()
160
167
 
161
168
  ### Switching language
162
169
 
163
- To switch language, simply set the `language`, `region` or `locale` properties
170
+ To switch languages, simply set the `language`, `region`, or `locale` property
164
171
  on the translator instance:
165
172
 
166
173
  ```typescript
@@ -168,8 +175,8 @@ import { useTranslator } from '@juit/vue-i18n'
168
175
 
169
176
  const translator = useTranslator() // assuming the default locale is "en-US"
170
177
 
171
- translator.region = 'CA' // we have switched to Canada, and locale is now "en-CA"
172
- translator.language = 'fr' // we have switched to French, and locale is now "fr-CA"
178
+ translator.region = 'CA' // we have switched to Canada, and the locale is now "en-CA"
179
+ translator.language = 'fr' // we have switched to French, and the locale is now "fr-CA"
173
180
 
174
181
  // or set the full `locale`
175
182
  translator.locale = new Intl.Locale('de-AT')
@@ -180,11 +187,11 @@ Because of reactivity, all translations will be updated to the new locale.
180
187
 
181
188
  ## Translating messages
182
189
 
183
- The base function to translate messages is exposed as `translator.t(...)` or
190
+ The function for translating messages is exposed as `translator.t(...)` or
184
191
  (within components) the `$t(...)` function.
185
192
 
186
- This function takes a translation _key_ (specified in the configuration
187
- phase, see above).
193
+ This function takes a translation _key_ specified during configuration
194
+ (see above).
188
195
 
189
196
  ```typescript
190
197
  import { useTranslator } from '@juit/vue-i18n'
@@ -202,8 +209,8 @@ import { useTranslator } from '@juit/vue-i18n'
202
209
 
203
210
  const translator = useTranslator()
204
211
 
205
- const panagram = translator.t({
206
- en: 'The quick fox jumped over the lazy dog',
212
+ const pangram = translator.t({
213
+ en: 'The quick brown fox jumps over the lazy dog',
207
214
  de: 'Franz jagt im komplett verwahrlosten Taxi quer durch Bayern'
208
215
  })
209
216
  ```
@@ -212,25 +219,40 @@ const panagram = translator.t({
212
219
  ### Parameterizing translations
213
220
 
214
221
  Translations can include parameters by enclosing them in curly braces `{param}`.
222
+ Parameter names are case-sensitive: `{name}` and `{NAME}` refer to different parameters.
223
+
224
+ Whitespace around parameter names is ignored. If a parameter is missing,
225
+ its placeholder is kept in normalized form: `{ name }` becomes `{name}`.
226
+
227
+ Escape a placeholder's opening brace with a backslash to display it literally:
228
+ `\{ name }` becomes `{ name }`, even when no parameter is supplied. In a normal
229
+ JavaScript string, write the backslash as `\\`, or use a `String.raw` template.
230
+ Inserted parameter values are treated as literal text and are never parsed again.
231
+
232
+ Backslashes before placeholders follow the same rule as those before pipes:
233
+ each pair produces one literal backslash, and an odd remaining backslash
234
+ escapes the placeholder. With two actual backslashes before `{name}`, one
235
+ backslash is displayed and `name` is substituted; with three, one backslash
236
+ is displayed and `{name}` stays literal.
215
237
 
216
238
  For example:
217
239
 
218
240
  ```typescript
219
241
  const name = 'John Doe'
220
242
 
221
- const string: translator.t({
222
- en: 'Your name is {name}'
243
+ const string = translator.t({
244
+ en: 'Your name is {name}',
223
245
  de: 'Ihr Name ist {name}'
224
246
  }, { name })
225
247
  // This will result in either "Your name is John Doe" or "Ihr Name ist John Doe"
226
248
  ```
227
249
 
228
- When parameters are numbers, those will be formatted as numbers:
250
+ Numeric parameters are formatted according to the current locale:
229
251
 
230
252
  ```typescript
231
- const string: translator.t({
232
- en: 'Score {points} points'
233
- de: 'Punktestand {points} Punkte'
253
+ const string = translator.t({
254
+ en: 'Score { points } points',
255
+ de: 'Punktestand { points } Punkte'
234
256
  }, { points: 1234.56 })
235
257
  // This will result in "Score 1,234.56 points" or "Punktestand 1.234,56 Punkte"
236
258
  ```
@@ -238,38 +260,62 @@ const string: translator.t({
238
260
 
239
261
  ### Pluralization
240
262
 
241
- The translator supports minimal rules for pluralization by separating
263
+ The translator supports basic pluralization rules by separating
242
264
  translation messages with the `|` (pipe) character.
243
265
 
244
- Messages can contain two variants `singular|plural` or three variants
245
- `zero|singular|plural`, with each variant used when the reference number to
246
- pluralize is either zero, one, or another number:
266
+ Messages can contain two variants, `singular|plural`, or three variants,
267
+ `zero|singular|plural`. The singular variant is used for one, and the plural
268
+ variant for other numbers. If a zero variant is provided, it is used for zero
269
+ instead of the plural variant.
247
270
 
248
- To contextualize the number, either use the `n` parameter, or use the `tc(...)`
249
- function which will take, as a second parameter, the reference number.
271
+ To specify the number used for pluralization, either use the `n` parameter
272
+ or pass the number as the second argument to `tc(...)`.
250
273
 
251
274
  For example:
252
275
 
253
276
  ```typescript
254
- const string: translator.t({
255
- en: 'no cats | one cat | {n} cats'
277
+ const string = translator.t({
278
+ en: 'no cats | one cat | {n} cats',
256
279
  de: 'keine Katzen | eine Katze | {n} Katzen'
257
280
  }, { n })
258
- // This will result in "no cats" or "keine katzen" when "n" is zero,
281
+ // This will result in "no cats" or "keine Katzen" when "n" is zero,
259
282
  // "one cat" or "eine Katze" when "n" is 1, or
260
283
  // "1,234.56 cats" or "1.234,56 Katzen" when "n" is 1234.56
261
284
  ```
262
285
 
263
- Is equivalent to:
286
+ This is equivalent to:
264
287
 
265
288
  ```typescript
266
- const string: translator.tc({
267
- en: 'no cats | one cat | {n} cats'
289
+ const string = translator.tc({
290
+ en: 'no cats | one cat | {n} cats',
268
291
  de: 'keine Katzen | eine Katze | {n} Katzen'
269
292
  }, n)
270
293
  ```
271
294
 
272
295
 
296
+ Messages can also use explicit tuples instead of pipe-delimited strings:
297
+
298
+ ```typescript
299
+ const message = {
300
+ en: ['no cats', 'one cat', '{n} cats'],
301
+ de: ['keine Katzen', 'eine Katze', '{n} Katzen'],
302
+ } as const
303
+
304
+ translator.tc(message, 2) // '2 cats' or '2 Katzen'
305
+ ```
306
+
307
+ A tuple with one element is used for every count. Two elements mean
308
+ `[singular, plural]`, with zero using the plural variant. Three elements mean
309
+ `[zero, singular, plural]`.
310
+
311
+ Tuple elements still support placeholders and escaped placeholders, but skip
312
+ pipe splitting and pipe unescaping entirely: `['A | B']` displays `A | B`.
313
+
314
+ Tuples work in base and regional translations, inline messages, and
315
+ `updateTranslations(...)`. They are copied when stored, so changing the
316
+ original array later cannot change stored messages or cached results.
317
+
318
+
273
319
  ## Formatting numbers
274
320
 
275
321
  The `n(...)` function can be used to format numbers in the current locale:
@@ -280,7 +326,7 @@ import { useTranslator } from '@juit/vue-i18n'
280
326
  const translator = useTranslator()
281
327
 
282
328
  const number = translator.n(1234.5)
283
- // the "number" string will be "1,234.5", "1.234,5" or whatever locale specified
329
+ // the "number" string will be "1,234.5", "1.234,5", etc., depending on the locale
284
330
  ```
285
331
 
286
332
  A currency can be specified as a second parameter for quick formatting:
@@ -291,23 +337,23 @@ import { useTranslator } from '@juit/vue-i18n'
291
337
  const translator = useTranslator()
292
338
 
293
339
  const amount = translator.n(1234.5, 'USD')
294
- // the "amount" will be "$1,234.5", "1.234,5 $" or whatever locale specified
340
+ // the "amount" string will be "$1,234.50" in en-US
295
341
  ```
296
342
 
297
- A full [`Intl.NumberFormatOptions`][4] set of options can also be specified
343
+ An [`Intl.NumberFormatOptions`][4] object can also be specified
298
344
  as a second parameter to fine-tune the formatting.
299
345
 
300
- The default format can be specified when configuring the plugin as the
301
- `formats.numberFormat` option (intentionally, there is no default).
346
+ The default format can be specified using `numberFormats.default` when
347
+ configuring the plugin.
302
348
 
303
349
 
304
350
  ## Formatting dates
305
351
 
306
- The `d(...)` function can be used to format date-and-time values in the current
352
+ The `d(...)` function can be used to format date and time values in the current
307
353
  locale.
308
354
 
309
355
  When the second parameter is a string, it is considered to be one of the
310
- _aliases_ configured when the plugin is setup.
356
+ _aliases_ configured when the plugin is set up.
311
357
 
312
358
  ```typescript
313
359
  import { useTranslator } from '@juit/vue-i18n'
@@ -316,15 +362,15 @@ const translator = useTranslator()
316
362
 
317
363
  const dateTime = translator.d(new Date()) // e.g. '03.02.2025, 18:08:05' in de-DE
318
364
  const dateOnly = translator.d(new Date(), 'date') // e.g. '03.02.2025' in de-DE
319
- const dateTime = translator.d(new Date(), 'time') // e.g. '18:08:05' in de-DE
365
+ const timeOnly = translator.d(new Date(), 'time') // e.g. '18:08:05' in de-DE
320
366
  ```
321
367
 
322
- A full [`Intl.DateTimeFormatOptions`][5] set of options can also be specified
368
+ An [`Intl.DateTimeFormatOptions`][5] object can also be specified
323
369
  as a second parameter to fine-tune the formatting.
324
370
 
325
- The third parameter, if specified, can be used to force the time zone used
371
+ The third parameter, if specified, can be used to override the time zone used
326
372
  when formatting the date. This is useful when time zones are specified in
327
- the definition of date format _aliases_ (see above):
373
+ the definitions of date and time format _aliases_ (see above):
328
374
 
329
375
  ```typescript
330
376
  import { useTranslator } from '@juit/vue-i18n'
@@ -336,17 +382,16 @@ translator.d(new Date(), 'full', 'Europe/Berlin')
336
382
  ```
337
383
 
338
384
 
339
-
340
385
  ## Configuring Types
341
386
 
342
- One of the keys to this package is to provide compile-time safety for all
343
- translation languages (we don't want to forget to translate a message in
387
+ One of the key features of this package is compile-time safety for all
388
+ translation languages (we don't want to forget to translate a message into
344
389
  a new language) and translation keys (we don't want to mistype a translation
345
390
  key by accident).
346
391
 
347
392
  To do so, we can _merge_ the `I18nConfiguration` interface of this package
348
- with our specific configurations. Two properties are expected to be defined
349
- in the configuration:
393
+ with our application-specific configuration. The following properties can
394
+ be defined:
350
395
 
351
396
  * `languages`: the list of supported languages for the application. These
352
397
  are ISO 639-1 language codes, and when specified, _every_
@@ -355,9 +400,9 @@ in the configuration:
355
400
  These are the arbitrary keys used to identify the
356
401
  messages to be translated with the `t` and `tc`
357
402
  methods of `Translator`.
358
- * `dateTimeFormats`: the date and time formats _aliases_ used by the
403
+ * `dateTimeFormats`: the date and time format _aliases_ used by the
359
404
  application.
360
- * `numberFormats`: the number formats _aliases_ used by the application.
405
+ * `numberFormats`: the number format _aliases_ used by the application.
361
406
 
362
407
  To configure the types, follow the example below:
363
408
 
@@ -403,11 +448,105 @@ const app = createApp(MyApp).use(i18n, {
403
448
  In the example above, if any of the translation objects in our app is missing
404
449
  a language (either `en` or `de`), TypeScript will complain.
405
450
 
406
- In the same way, if we pass any other string but `hello` to `t(...)` or
407
- `tc(...)`, TypeScript will report the wrong key.
451
+ In the same way, if we pass any string other than `hello` to `t(...)` or
452
+ `tc(...)`, TypeScript will report an invalid key.
453
+
454
+ Date, time, and number format alias types will also be augmented using the
455
+ customizations specified in `dateTimeFormats` and `numberFormats`.
456
+
457
+
458
+ ## Updating Translations
459
+
460
+ Use `translator.utils.updateTranslations(...)` to add or change translations
461
+ after the plugin has been configured, for example when loading messages from
462
+ a server. Pass an object keyed by translation key, with language codes and
463
+ their translated messages as values:
464
+
465
+ ```typescript
466
+ import { useTranslator } from '@juit/vue-i18n'
467
+
468
+ const translator = useTranslator()
469
+
470
+ translator.utils.updateTranslations({
471
+ hello: {
472
+ en: 'Hello again!',
473
+ },
474
+ })
475
+
476
+ translator.language = 'en'
477
+ translator.t('hello') // 'Hello again!'
478
+ ```
479
+
480
+ Updates are merged into the existing translations: only the supplied
481
+ key-language pairs are overwritten. In this example, the German translation
482
+ of `hello` is preserved. New keys and language variants can also be added.
483
+
484
+ Both translation keys and languages are optional in an update, so you do not
485
+ need to supply every configured language. If you have configured
486
+ `I18nConfiguration`, TypeScript checks the supplied keys and languages against
487
+ those types.
488
+
489
+ Empty strings and `undefined` values are ignored; they cannot be used to
490
+ delete an existing translation. The method returns nothing and clears the
491
+ translation cache when an update is applied, so subsequent calls to `t(...)`
492
+ and `tc(...)` use the updated messages. Updating translations alone does not
493
+ trigger a Vue component re-render.
494
+
495
+
496
+ ## Language Matching
497
+
498
+ The `LanguageMatcher` class selects a supported language from a user's
499
+ preferences. It can be used independently of the Vue plugin, for example to
500
+ choose the initial language from the browser's `navigator.languages`:
501
+
502
+ ```typescript
503
+ import { LanguageMatcher } from '@juit/vue-i18n'
504
+
505
+ const matcher = new LanguageMatcher([ 'en', 'de', 'ja' ])
506
+
507
+ matcher.defaultLanguage // 'en'
508
+ matcher.availableLanguages // [ 'en', 'de', 'ja' ]
509
+
510
+ const app = createApp(MyApp).use(i18n, {
511
+ defaultLanguage: matcher.match([ ...navigator.languages ]),
512
+ translations,
513
+ })
514
+ ```
515
+
516
+ The constructor accepts a single ISO 639-1 language code (such as `'en'`) or
517
+ a non-empty array of codes. The first valid language is the default, used
518
+ whenever no preference matches.
519
+
520
+ The `match(...)` method accepts a string, an array of strings in preference
521
+ order, `null`, or `undefined`. It returns the **first supported preference**,
522
+ regardless of the order of the available languages:
523
+
524
+ ```typescript
525
+ matcher.match('de') // 'de'
526
+ matcher.match('JA-JP') // 'ja'
527
+ matcher.match('de_AT') // 'de'
528
+ matcher.match([ 'fr', 'ja-JP', 'de' ]) // 'ja'
529
+ matcher.match('fr') // 'en' (default)
530
+ matcher.match([]) // 'en' (default)
531
+ matcher.match(null) // 'en' (default)
532
+ matcher.match(undefined) // 'en' (default)
533
+
534
+ const englishOnly = new LanguageMatcher('en')
535
+ englishOnly.match('de') // 'en'
536
+ ```
537
+
538
+
539
+ ## Remarks
540
+
541
+ To keep the package small, the translator supports only the **language** and
542
+ optional **region** of a locale. Both `defaultLanguage` and assignments to
543
+ `translator.locale` discard script subtags and Unicode extensions, including
544
+ calendar and numbering-system preferences. For example,
545
+ `zh-Hant-TW-u-nu-hanidec` becomes `zh-TW`.
408
546
 
409
- Also, date time format aliases will be augumented using the customizations
410
- specified in `dateTimeFormats` and `numberFormats`.
547
+ Changing `translator.language` preserves the current region: switching from
548
+ `en-CA` to `fr` produces `fr-CA`. Changing `translator.region` preserves the
549
+ language; setting it to `undefined` removes the region.
411
550
 
412
551
 
413
552
  ## Legal Stuff