@shoelace-style/localize 1.1.8 → 2.1.2
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/CHANGELOG.md +18 -0
- package/README.md +49 -183
- package/dist/index.d.ts +16 -7
- package/dist/index.js +48 -52
- package/package.json +3 -6
- package/src/index.ts +106 -63
- package/dist/fast.d.ts +0 -6
- package/dist/fast.js +0 -47
- package/dist/lit.d.ts +0 -22
- package/dist/lit.js +0 -62
- package/src/fast.ts +0 -105
- package/src/lit.ts +0 -114
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
## 2.1.2
|
|
5
|
+
|
|
6
|
+
- Removed all dependencies
|
|
7
|
+
|
|
8
|
+
## 2.1.1
|
|
9
|
+
|
|
10
|
+
- Change import to ensure only types get used
|
|
11
|
+
|
|
12
|
+
## 2.1.0
|
|
13
|
+
|
|
14
|
+
- Added relative time method to
|
|
15
|
+
|
|
16
|
+
## 2.0.0
|
|
17
|
+
|
|
18
|
+
- Reworked the library to use the [ReactiveController](https://lit.dev/docs/composition/controllers/) interface
|
package/README.md
CHANGED
|
@@ -1,34 +1,37 @@
|
|
|
1
1
|
# Shoelace: Localize
|
|
2
2
|
|
|
3
|
-
This micro library does not aim to replicate a full-blown localization tool. For that, you should use something like [i18next](https://www.i18next.com/). What this library _does_ do is provide a lightweight, framework-agnostic mechanism for sharing and applying translations across one or more custom elements in a component library.
|
|
3
|
+
This zero-dependency micro library does not aim to replicate a full-blown localization tool. For that, you should use something like [i18next](https://www.i18next.com/). What this library _does_ do is provide a lightweight, framework-agnostic mechanism for sharing and applying translations across one or more custom elements in a component library.
|
|
4
4
|
|
|
5
|
-
Included are methods for translating terms, dates, currencies, and numbers
|
|
5
|
+
Included are methods for translating terms, dates, currencies, and numbers and a [Reactive Controller](https://lit.dev/docs/composition/controllers/) that can be used as a mixin in Lit and other supportive component authoring libraries.
|
|
6
6
|
|
|
7
7
|
## Overview
|
|
8
8
|
|
|
9
|
-
Here's an example of how this library can be used to create a custom element with Lit.
|
|
10
|
-
|
|
9
|
+
Here's an example of how this library can be used to create a localized custom element with Lit.
|
|
11
10
|
|
|
12
11
|
```ts
|
|
13
|
-
import { registerTranslation } from '@shoelace-style/localize';
|
|
14
|
-
|
|
12
|
+
import { LocalizeController, registerTranslation } from '@shoelace-style/localize';
|
|
13
|
+
|
|
14
|
+
// Translations can also be loaded outside of the component and/or on the fly using dynamic imports
|
|
15
15
|
import en from '../translations/en';
|
|
16
16
|
import es from '../translations/es';
|
|
17
17
|
|
|
18
|
-
registerTranslation(en, es);
|
|
18
|
+
registerTranslation(en, es);
|
|
19
19
|
|
|
20
20
|
@customElement('my-element')
|
|
21
|
-
@localize()
|
|
22
21
|
export class MyElement extends LitElement {
|
|
22
|
+
private localize = new LocalizeController(this);
|
|
23
|
+
|
|
24
|
+
@property() lang: string;
|
|
25
|
+
|
|
23
26
|
render() {
|
|
24
27
|
return html`
|
|
25
|
-
<h1>${
|
|
28
|
+
<h1>${this.localize.term('hello_world')}</h1>
|
|
26
29
|
`;
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
32
|
```
|
|
30
33
|
|
|
31
|
-
|
|
34
|
+
To set the page locale, apply the desired `lang` attribute to the `<html>` element.
|
|
32
35
|
|
|
33
36
|
```html
|
|
34
37
|
<html lang="es">
|
|
@@ -36,7 +39,7 @@ Here's how your consumers will change languages.
|
|
|
36
39
|
</html>
|
|
37
40
|
```
|
|
38
41
|
|
|
39
|
-
|
|
42
|
+
Changes to `<html lang>` will trigger an update to all localized components automatically.
|
|
40
43
|
|
|
41
44
|
## Why this instead of an i18n library?
|
|
42
45
|
|
|
@@ -44,7 +47,7 @@ It's not uncommon for a custom element to require localization, but implementing
|
|
|
44
47
|
|
|
45
48
|
```html
|
|
46
49
|
<button type="button" aria-label="Close">
|
|
47
|
-
<svg
|
|
50
|
+
<svg>...</svg>
|
|
48
51
|
</button>
|
|
49
52
|
```
|
|
50
53
|
|
|
@@ -58,39 +61,25 @@ Typically, custom element authors dance around the problem by exposing attribute
|
|
|
58
61
|
|
|
59
62
|
But this approach offloads the problem to the user so they have to provide every term, every time. It also doesn't scale with more complex components that have more than a handful of terms to be translated.
|
|
60
63
|
|
|
61
|
-
This is the use case this library is solving for. It is
|
|
64
|
+
This is the use case this library is solving for. It is not intended to solve localization at the framework level. There are much better tools for that.
|
|
62
65
|
|
|
63
66
|
## How it works
|
|
64
67
|
|
|
65
|
-
To achieve this goal, we lean on HTML’s [`lang`](~https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang~) attribute to determine what
|
|
68
|
+
To achieve this goal, we lean on HTML’s [`lang`](~https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang~) attribute to determine what language should be used. The default locale is specified by `<html lang="...">`, but any localized element can be scoped to a locale by setting its `lang` attribute. This means you can have more than one language per page, if desired.
|
|
66
69
|
|
|
67
70
|
```html
|
|
68
71
|
<html lang="en">
|
|
69
72
|
<body>
|
|
70
|
-
|
|
71
73
|
<my-element>This element will be English</my-element>
|
|
72
|
-
|
|
73
|
-
<
|
|
74
|
-
<my-element>This element will be Spanish</my-element>
|
|
75
|
-
<my-element>This element is also Spanish</my-element>
|
|
76
|
-
</div>
|
|
77
|
-
|
|
74
|
+
<my-element lang="es">This element will be Spanish</my-element>
|
|
75
|
+
<my-element lang="fr">This element will be French</my-element>
|
|
78
76
|
</body>
|
|
79
77
|
</html>
|
|
80
78
|
```
|
|
81
79
|
|
|
82
|
-
This library provides a set of tools to localize dates, currencies, numbers, and terms in your custom element library with a minimal footprint. Reactivity is achieved with a [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) that listens for `lang` changes
|
|
80
|
+
This library provides a set of tools to localize dates, currencies, numbers, and terms in your custom element library with a minimal footprint. Reactivity is achieved with a [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) that listens for `lang` changes on `<html>`.
|
|
83
81
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
When a localized element is connected to the DOM, the library looks for the closest `lang` attribute, moving up through its ancestors and breaking through shadow roots as necessary. The element and its language are then cached internally. When a `lang` attribute changes. the library loops through all connected components, re-detects their language, and tells them to update. When an element is disconnected from the DOM, it is discarded from the map.
|
|
87
|
-
|
|
88
|
-
By caching languages in a map, we're able to limit expensive DOM traversal so it only occurs:
|
|
89
|
-
|
|
90
|
-
1. When the element is connected
|
|
91
|
-
2. When a `lang` attribute changes
|
|
92
|
-
|
|
93
|
-
At this time, there is no easier way to detect the "current language" of an arbitrary element. I consider this a gap in the platform and [I've proposed properties](https://github.com/whatwg/html/issues/7039) to make this lookup more efficient.
|
|
82
|
+
By design, `lang` attributes on ancestor elements are ignored. This is for performance reasons, as there isn't an efficient way to detect the "current language" of an arbitrary element. I consider this a gap in the platform and [I've proposed properties](https://github.com/whatwg/html/issues/7039) to make this lookup less expensive.
|
|
94
83
|
|
|
95
84
|
## Usage
|
|
96
85
|
|
|
@@ -102,13 +91,13 @@ npm install @shoelace-style/localize
|
|
|
102
91
|
|
|
103
92
|
Next, follow these steps to localize your components.
|
|
104
93
|
|
|
105
|
-
1. Create
|
|
106
|
-
2. Register the
|
|
94
|
+
1. Create a translation
|
|
95
|
+
2. Register the translation
|
|
107
96
|
3. Localize your components
|
|
108
97
|
|
|
109
|
-
### Creating
|
|
98
|
+
### Creating a Translation
|
|
110
99
|
|
|
111
|
-
|
|
100
|
+
All translations must extend the `Translation` type and implement the required meta properties (denoted by a `$` prefix). Additional terms can be implemented as show below.
|
|
112
101
|
|
|
113
102
|
```ts
|
|
114
103
|
// en.ts
|
|
@@ -119,11 +108,11 @@ const translation: Translation = {
|
|
|
119
108
|
$name: 'English',
|
|
120
109
|
$dir: 'ltr',
|
|
121
110
|
|
|
122
|
-
//
|
|
111
|
+
// Simple terms
|
|
123
112
|
upload: 'Upload',
|
|
124
113
|
|
|
125
|
-
//
|
|
126
|
-
|
|
114
|
+
// Terms with placeholders
|
|
115
|
+
greet_user: (name: string) => `Hello, ${name}!`,
|
|
127
116
|
|
|
128
117
|
// Plurals
|
|
129
118
|
num_files_selected: (count: number) => {
|
|
@@ -148,13 +137,15 @@ import es from './es';
|
|
|
148
137
|
registerTranslation(en, es);
|
|
149
138
|
```
|
|
150
139
|
|
|
151
|
-
The first translation
|
|
140
|
+
The first translation that's registered will be used as the _fallback_. That is, if a term is missing from the target language, the fallback language will be used instead.
|
|
141
|
+
|
|
142
|
+
Translations registered with country such as `en-GB` are supported. However, your fallback translation must be registered with only a language code (e.g. `en`) to ensure users of unsupported regions will still receive a comprehensible translation.
|
|
152
143
|
|
|
153
|
-
|
|
144
|
+
For example, if you're fallback language is `en-US`, you should register it as `en` so users with unsupported `en-*` country codes will receive it as a fallback. Then you can register country codes such as `en-GB` and `en-AU` to improve the experience for additional regions.
|
|
154
145
|
|
|
155
|
-
|
|
146
|
+
It's important to note that translations _do not_ have to be registered up front. You can register them on demand as the language changes in your app. Upon registration, localized components will update automatically.
|
|
156
147
|
|
|
157
|
-
|
|
148
|
+
Here's a sample function that dynamically loads a translation.
|
|
158
149
|
|
|
159
150
|
```ts
|
|
160
151
|
import { registerTranslation } from '@shoelace-style/localize';
|
|
@@ -169,177 +160,52 @@ async function changeLanguage(lang) {
|
|
|
169
160
|
}
|
|
170
161
|
```
|
|
171
162
|
|
|
172
|
-
###
|
|
163
|
+
### Localizing Components
|
|
173
164
|
|
|
174
|
-
|
|
165
|
+
You can use the `LocalizeController` with any library that supports [Lit's Reactive Controller pattern](https://lit.dev/docs/composition/controllers/). In [Lit](https://lit.dev/), a localized custom element will look something like this.
|
|
175
166
|
|
|
176
167
|
```ts
|
|
177
168
|
import { LitElement } from 'lit';
|
|
178
169
|
import { customElement } from 'lit/decorators.js';
|
|
179
|
-
import {
|
|
170
|
+
import { LocalizeController } from '@shoelace-style/localize/dist/lit.js';
|
|
180
171
|
|
|
181
172
|
@customElement('my-element')
|
|
182
|
-
@localize()
|
|
183
173
|
export class MyElement extends LitElement {
|
|
174
|
+
private localize = new LocalizeController(this);
|
|
175
|
+
|
|
176
|
+
@property() lang: string;
|
|
177
|
+
|
|
184
178
|
render() {
|
|
185
179
|
return html`
|
|
186
180
|
<!-- Term -->
|
|
187
|
-
${
|
|
181
|
+
${this.localize.term('hello')}
|
|
188
182
|
|
|
189
183
|
<!-- Date -->
|
|
190
|
-
${
|
|
184
|
+
${this.localize.date('2021-09-15 14:00:00 ET'), { month: 'long', day: 'numeric', year: 'numeric' }}
|
|
191
185
|
|
|
192
|
-
<!--
|
|
193
|
-
${
|
|
186
|
+
<!-- Number/currency -->
|
|
187
|
+
${this.localize.number(1000, { style: 'currency', currency: 'USD'})}
|
|
194
188
|
`;
|
|
195
189
|
}
|
|
196
190
|
}
|
|
197
191
|
```
|
|
198
192
|
|
|
199
|
-
### FAST
|
|
200
|
-
|
|
201
|
-
If you're using [FAST Element](https://www.fast.design/) to develop components:
|
|
202
|
-
|
|
203
|
-
```ts
|
|
204
|
-
import { FASTElement, customElement } from '@microsoft/fast-element';
|
|
205
|
-
import { localize, translate as t, formatDate as d, formatNumber as n } from '@shoelace-style/localize/dist/lit.js';
|
|
206
|
-
|
|
207
|
-
const template = html<MyElement>`
|
|
208
|
-
<!-- Term -->
|
|
209
|
-
${x => t(x, 'hello')}
|
|
210
|
-
|
|
211
|
-
<!-- Date -->
|
|
212
|
-
${x => d(x, '2021-09-15 14:00:00 ET'), { month: 'long', day: 'numeric', year: 'numeric' }}
|
|
213
|
-
|
|
214
|
-
<!-- Currency -->
|
|
215
|
-
${x => n(x, 1000, { style: 'currency', currency: 'USD'})}
|
|
216
|
-
`;
|
|
217
|
-
|
|
218
|
-
@customElement({
|
|
219
|
-
name: 'my-element',
|
|
220
|
-
template
|
|
221
|
-
})
|
|
222
|
-
export class MyElement extends FASTElement {
|
|
223
|
-
// ...
|
|
224
|
-
}
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
**Note:** This directive requires a context to be passed as the first argument. This is required because of a limitation in FAST's rendering engine that causes inconsistencies when a directive is used as `${t('example')}` vs. `${x => t('example')}`.
|
|
228
|
-
|
|
229
|
-
### No Library (Advanced)
|
|
230
|
-
|
|
231
|
-
To use this without a custom element library, you'll need to follow this pattern.
|
|
232
|
-
|
|
233
|
-
```ts
|
|
234
|
-
import { connectedElements, detectLanguage, forceUpdate } from '@shoelace-style/localize';
|
|
235
|
-
|
|
236
|
-
class MyElement extends HTMLElement {
|
|
237
|
-
connectedCallback() {
|
|
238
|
-
// Register the component when it's connected and cache the current language
|
|
239
|
-
const lang = detectLanguage(this);
|
|
240
|
-
connectedElements.set(this, lang);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
disconnectedCallback() {
|
|
244
|
-
// Remove the element from cache when it disconnects from the DOM
|
|
245
|
-
connectedElements.delete(this);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
updateLocalizedTerms() {
|
|
249
|
-
// Called when the lang changes (this is where you update all localized terms in the element)
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
static get observedAttributes() {
|
|
253
|
-
return ['lang'];
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
attributeChangedCallback(name, oldValue, newValue) {
|
|
257
|
-
// When lang changes, update the cache and trigger an update
|
|
258
|
-
if (name === 'lang') {
|
|
259
|
-
this.updateLocalizedTerms();
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
Three core translation functions are exposed for translating terms, dates, numbers, and currencies. The `translate()` function relies on the translations you provide, while the `formatDate()` and `formatNumber()` functions use the [`Intl` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) to localize dates, numbers, and currencies.
|
|
266
|
-
|
|
267
|
-
These are the functions that get called internally by the Lit directives. Note that `lang` is a required argument. If you use this API, make sure to use the cached language values available in the `connectedElements` map to avoid expensive DOM traversal.
|
|
268
|
-
|
|
269
|
-
```ts
|
|
270
|
-
function translate(lang: string, key: string, args: any) {
|
|
271
|
-
// returns a localized term
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function formatDate(lang: string, date: Date | string, options: Intl.DateTimeFormatOptions) {
|
|
275
|
-
// returns a localized date
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function formatNumber(lang: string, number: number | string, options?: Intl.NumberFormatOptions) {
|
|
279
|
-
// returns a localized number or currency
|
|
280
|
-
}
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
### Typed Arguments
|
|
284
|
-
|
|
285
|
-
As the `Translation` interface is extended by the user, and because terms can have varying arguments, there's no way for TypeScript to automatically know about the terms you've added. This means we won't get strongly typed arguments when calling `translate()` or a translate directive.
|
|
286
|
-
|
|
287
|
-
However, you can optionally extend the `Translation` interface with your own and wrap the translation functions you want to enable strong typings for.
|
|
288
|
-
|
|
289
|
-
```ts
|
|
290
|
-
// translation.ts
|
|
291
|
-
import type { FunctionParams, Translation as BaseTranslation } from '@shoelace-style/localize';
|
|
292
|
-
import { translate as internalTranslate } from '@shoelace-style/localize';
|
|
293
|
-
import { translate as litTranslate } from '@shoelace-style/localize/dist/lit.js';
|
|
294
|
-
|
|
295
|
-
export interface Translation extends BaseTranslation {
|
|
296
|
-
upload: string;
|
|
297
|
-
hello_user: (name: string) => string;
|
|
298
|
-
num_files_selected: (count: number) => string;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Wrap the translate function
|
|
302
|
-
export function translate<T extends keyof Translation>(lang: string, key: T, ...args: FunctionParams<Translation[T]>) {
|
|
303
|
-
return internalTranslate(lang, key, ...args) as unknown;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Wrap the Lit translate directive
|
|
307
|
-
export function translateDirective<T extends keyof Translation>(key: T, ...args: FunctionParams<Translation[T]>) {
|
|
308
|
-
return litTranslate(key, ...args) as unknown;
|
|
309
|
-
}
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
Now, instead of importing from `@shoelace-style/localize`, you can import these functions from your own `translation.ts` module and you'll get strongly typed translation keys and arguments!
|
|
313
|
-
|
|
314
|
-
### Appending Terms
|
|
315
|
-
|
|
316
|
-
Occassionaly, third-party components may want to make use of your localization library. Should you choose to expose this as an option, here's how terms can be added to translations that are already registered.
|
|
317
|
-
|
|
318
|
-
```ts
|
|
319
|
-
import en from './en';
|
|
320
|
-
|
|
321
|
-
en.logout = 'Logout';
|
|
322
|
-
en.goodbye_user = (name: string) => `Goodbye, ${name}`;
|
|
323
|
-
```
|
|
324
|
-
|
|
325
193
|
## Advantages
|
|
326
194
|
|
|
327
195
|
- Extremely lightweight
|
|
328
|
-
|
|
196
|
+
- Zero dependencies
|
|
197
|
+
- Version 2.1 measures 726 bytes (yes, _bytes_) after minify + gzip
|
|
329
198
|
- Uses existing platform features
|
|
330
199
|
- Supports simple terms, plurals, and complex translations
|
|
331
200
|
- Fun fact: some languages have [six plural forms](https://lingohub.com/blog/2019/02/pluralization) and this will support that
|
|
332
201
|
- Supports dates, numbers, and currencies
|
|
333
202
|
- Good DX for custom element authors and consumers
|
|
334
|
-
|
|
335
|
-
|
|
203
|
+
- Intuitive API for custom element authors
|
|
204
|
+
- Consumers only need to load the translations they want and set the `lang` attribute
|
|
336
205
|
- Translations can be loaded up front or on demand
|
|
337
206
|
- Translations can be created by consumers without having to wait for them to get accepted upstream
|
|
338
|
-
- Strong typings when using translations (requires a custom Translation interface and a wrapper function)
|
|
339
207
|
|
|
340
208
|
## Disadvantages
|
|
341
209
|
|
|
342
210
|
- Complex translations require some code, such as conditionals
|
|
343
211
|
- This is arguably no more difficult than, for example, adding them to a [YAML](https://edgeguides.rubyonrails.org/i18n.html#pluralization) or [XLIFF](https://en.wikipedia.org/wiki/XLIFF) file
|
|
344
|
-
|
|
345
|
-
Note that we aren’t aiming to solve localization at the framework level. Many teams already have their own translation libraries, so we need to provide something that will work in tandem with those with a minimal learning curve.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
1
2
|
export declare type FunctionParams<T> = T extends (...args: infer U) => string ? U : never;
|
|
2
3
|
export interface Translation {
|
|
3
4
|
$code: string;
|
|
@@ -5,11 +6,19 @@ export interface Translation {
|
|
|
5
6
|
$dir: 'ltr' | 'rtl';
|
|
6
7
|
[key: string]: any;
|
|
7
8
|
}
|
|
8
|
-
export declare const connectedElements: Map<HTMLElement, string>;
|
|
9
|
-
export declare function getSystemLanguage(): string;
|
|
10
|
-
export declare function detectLanguage(el: HTMLElement): string;
|
|
11
9
|
export declare function registerTranslation(...translation: Translation[]): void;
|
|
12
|
-
export declare function
|
|
13
|
-
export declare function
|
|
14
|
-
export declare function
|
|
15
|
-
export declare function
|
|
10
|
+
export declare function term<K extends keyof Translation>(lang: string, key: K, ...args: FunctionParams<Translation[K]>): any;
|
|
11
|
+
export declare function date(lang: string, dateToFormat: Date | string, options?: Intl.DateTimeFormatOptions): string;
|
|
12
|
+
export declare function number(lang: string, numberToFormat: number | string, options?: Intl.NumberFormatOptions): string;
|
|
13
|
+
export declare function relativeTime(lang: string, value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions): string;
|
|
14
|
+
export declare function updateLocalizedTerms(): void;
|
|
15
|
+
export declare class LocalizeController implements ReactiveController {
|
|
16
|
+
host: ReactiveControllerHost & HTMLElement;
|
|
17
|
+
constructor(host: ReactiveControllerHost & HTMLElement);
|
|
18
|
+
hostConnected(): void;
|
|
19
|
+
hostDisconnected(): void;
|
|
20
|
+
term<K extends keyof Translation>(key: K, ...args: FunctionParams<Translation[K]>): any;
|
|
21
|
+
date(dateToFormat: Date | string, options?: Intl.DateTimeFormatOptions): string;
|
|
22
|
+
number(numberToFormat: number | string, options?: Intl.NumberFormatOptions): string;
|
|
23
|
+
relativeTime(value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions): string;
|
|
24
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,23 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
const documentElementObserver = new MutationObserver(
|
|
1
|
+
const connectedElements = new Set();
|
|
2
|
+
const documentElementObserver = new MutationObserver(updateLocalizedTerms);
|
|
3
3
|
const translations = new Map();
|
|
4
|
+
let documentLanguage = document.documentElement.lang || navigator.language;
|
|
4
5
|
let fallback;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
return next ? next : getNext(el.getRootNode().host);
|
|
11
|
-
}
|
|
12
|
-
return getNext(root);
|
|
13
|
-
}
|
|
14
|
-
export function getSystemLanguage() {
|
|
15
|
-
return document.documentElement.lang || navigator.language;
|
|
16
|
-
}
|
|
17
|
-
export function detectLanguage(el) {
|
|
18
|
-
const closestEl = closest('[lang]', el);
|
|
19
|
-
return (closestEl === null || closestEl === void 0 ? void 0 : closestEl.lang) || getSystemLanguage();
|
|
20
|
-
}
|
|
6
|
+
documentElementObserver.observe(document.documentElement, {
|
|
7
|
+
attributes: true,
|
|
8
|
+
attributeFilter: ['lang']
|
|
9
|
+
});
|
|
21
10
|
export function registerTranslation(...translation) {
|
|
22
11
|
translation.map(t => {
|
|
23
12
|
const code = t.$code.toLowerCase();
|
|
@@ -26,8 +15,9 @@ export function registerTranslation(...translation) {
|
|
|
26
15
|
fallback = t;
|
|
27
16
|
}
|
|
28
17
|
});
|
|
18
|
+
updateLocalizedTerms();
|
|
29
19
|
}
|
|
30
|
-
export function
|
|
20
|
+
export function term(lang, key, ...args) {
|
|
31
21
|
const code = lang.toLowerCase().slice(0, 2);
|
|
32
22
|
const subcode = lang.length > 2 ? lang.toLowerCase() : '';
|
|
33
23
|
const primary = translations.get(subcode);
|
|
@@ -43,7 +33,7 @@ export function translate(lang, key, ...args) {
|
|
|
43
33
|
term = fallback[key];
|
|
44
34
|
}
|
|
45
35
|
else {
|
|
46
|
-
console.error(`
|
|
36
|
+
console.error(`No translation found for: ${key}`);
|
|
47
37
|
return key;
|
|
48
38
|
}
|
|
49
39
|
if (typeof term === 'function') {
|
|
@@ -51,40 +41,46 @@ export function translate(lang, key, ...args) {
|
|
|
51
41
|
}
|
|
52
42
|
return term;
|
|
53
43
|
}
|
|
54
|
-
export function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
result = new Intl.DateTimeFormat(lang, options).format(date);
|
|
59
|
-
}
|
|
60
|
-
catch (_a) {
|
|
61
|
-
console.error(`Invalid language code: "${lang}"`);
|
|
62
|
-
}
|
|
63
|
-
return result;
|
|
44
|
+
export function date(lang, dateToFormat, options) {
|
|
45
|
+
dateToFormat = new Date(dateToFormat);
|
|
46
|
+
return new Intl.DateTimeFormat(lang, options).format(dateToFormat);
|
|
64
47
|
}
|
|
65
|
-
export function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
result = isNaN(number) ? '' : new Intl.NumberFormat(lang, options).format(number);
|
|
70
|
-
}
|
|
71
|
-
catch (_a) {
|
|
72
|
-
console.error(`Invalid language code: "${lang}"`);
|
|
73
|
-
}
|
|
74
|
-
return result;
|
|
48
|
+
export function number(lang, numberToFormat, options) {
|
|
49
|
+
numberToFormat = Number(numberToFormat);
|
|
50
|
+
return isNaN(numberToFormat) ? '' : new Intl.NumberFormat(lang, options).format(numberToFormat);
|
|
75
51
|
}
|
|
76
|
-
export function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
52
|
+
export function relativeTime(lang, value, unit, options) {
|
|
53
|
+
return new Intl.RelativeTimeFormat(lang, options).format(value, unit);
|
|
54
|
+
}
|
|
55
|
+
export function updateLocalizedTerms() {
|
|
56
|
+
documentLanguage = document.documentElement.lang || navigator.language;
|
|
57
|
+
[...connectedElements.keys()].map((el) => {
|
|
58
|
+
if (typeof el.requestUpdate === 'function') {
|
|
59
|
+
el.requestUpdate();
|
|
82
60
|
}
|
|
83
61
|
});
|
|
84
62
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
63
|
+
export class LocalizeController {
|
|
64
|
+
constructor(host) {
|
|
65
|
+
this.host = host;
|
|
66
|
+
this.host.addController(this);
|
|
67
|
+
}
|
|
68
|
+
hostConnected() {
|
|
69
|
+
connectedElements.add(this.host);
|
|
70
|
+
}
|
|
71
|
+
hostDisconnected() {
|
|
72
|
+
connectedElements.delete(this.host);
|
|
73
|
+
}
|
|
74
|
+
term(key, ...args) {
|
|
75
|
+
return term(this.host.lang || documentLanguage, key, ...args);
|
|
76
|
+
}
|
|
77
|
+
date(dateToFormat, options) {
|
|
78
|
+
return date(this.host.lang || documentLanguage, dateToFormat, options);
|
|
79
|
+
}
|
|
80
|
+
number(numberToFormat, options) {
|
|
81
|
+
return number(this.host.lang || documentLanguage, numberToFormat, options);
|
|
82
|
+
}
|
|
83
|
+
relativeTime(value, unit, options) {
|
|
84
|
+
return relativeTime(this.host.lang || documentLanguage, value, unit, options);
|
|
85
|
+
}
|
|
86
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shoelace-style/localize",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "A micro library for localizing custom elements.",
|
|
3
|
+
"version": "2.1.2",
|
|
4
|
+
"description": "A micro library for localizing custom elements using Lit's Reactive Controller model.",
|
|
5
5
|
"main": "/dist/index.js",
|
|
6
6
|
"module": "/dist/index.js",
|
|
7
7
|
"type": "module",
|
|
@@ -34,10 +34,7 @@
|
|
|
34
34
|
"homepage": "https://github.com/shoelace-style/localize#readme",
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"del": "^6.0.0",
|
|
37
|
+
"lit": "^2.0.2",
|
|
37
38
|
"typescript": "^4.4.3"
|
|
38
|
-
},
|
|
39
|
-
"dependencies": {
|
|
40
|
-
"@microsoft/fast-element": "^1.5.1",
|
|
41
|
-
"lit": "*"
|
|
42
39
|
}
|
|
43
40
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,57 +1,53 @@
|
|
|
1
|
+
import type { LitElement, ReactiveController, ReactiveControllerHost } from 'lit';
|
|
2
|
+
|
|
1
3
|
export type FunctionParams<T> = T extends (...args: infer U) => string ? U : never;
|
|
2
4
|
|
|
3
5
|
export interface Translation {
|
|
4
6
|
$code: string; // e.g. en, en-GB
|
|
5
7
|
$name: string; // e.g. English, Español
|
|
6
8
|
$dir: 'ltr' | 'rtl';
|
|
7
|
-
|
|
8
9
|
[key: string]: any;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
const documentElementObserver = new MutationObserver(
|
|
12
|
+
const connectedElements = new Set<HTMLElement>();
|
|
13
|
+
const documentElementObserver = new MutationObserver(updateLocalizedTerms);
|
|
13
14
|
const translations: Map<string, Translation> = new Map();
|
|
15
|
+
let documentLanguage = document.documentElement.lang || navigator.language;
|
|
14
16
|
let fallback: Translation;
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return next ? next : getNext((el.getRootNode() as ShadowRoot).host);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return getNext(root);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function getSystemLanguage() {
|
|
29
|
-
return document.documentElement.lang || navigator.language;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function detectLanguage(el: HTMLElement) {
|
|
33
|
-
const closestEl = closest('[lang]', el) as HTMLElement;
|
|
34
|
-
return closestEl?.lang || getSystemLanguage();
|
|
35
|
-
}
|
|
18
|
+
// Watch for changes on <html lang>
|
|
19
|
+
documentElementObserver.observe(document.documentElement, {
|
|
20
|
+
attributes: true,
|
|
21
|
+
attributeFilter: ['lang']
|
|
22
|
+
});
|
|
36
23
|
|
|
24
|
+
//
|
|
25
|
+
// Registers one or more translations
|
|
26
|
+
//
|
|
37
27
|
export function registerTranslation(...translation: Translation[]) {
|
|
38
28
|
translation.map(t => {
|
|
39
29
|
const code = t.$code.toLowerCase();
|
|
40
30
|
translations.set(code, t);
|
|
41
31
|
|
|
42
|
-
//
|
|
32
|
+
// The first translation that's registered is the fallback
|
|
43
33
|
if (!fallback) {
|
|
44
34
|
fallback = t;
|
|
45
35
|
}
|
|
46
36
|
});
|
|
37
|
+
|
|
38
|
+
updateLocalizedTerms();
|
|
47
39
|
}
|
|
48
40
|
|
|
49
|
-
|
|
41
|
+
//
|
|
42
|
+
// Translates a term using the specified locale. Looks up translations in order of language + country codes (es-PE),
|
|
43
|
+
// language code (es), then the fallback translation.
|
|
44
|
+
//
|
|
45
|
+
export function term<K extends keyof Translation>(lang: string, key: K, ...args: FunctionParams<Translation[K]>) {
|
|
50
46
|
const code = lang.toLowerCase().slice(0, 2); // e.g. en
|
|
51
|
-
const subcode = lang.length > 2 ? lang.toLowerCase() : ''; // e.g. en-
|
|
47
|
+
const subcode = lang.length > 2 ? lang.toLowerCase() : ''; // e.g. en-GB
|
|
52
48
|
const primary = translations.get(subcode);
|
|
53
49
|
const secondary = translations.get(code);
|
|
54
|
-
let term;
|
|
50
|
+
let term: any;
|
|
55
51
|
|
|
56
52
|
// Look for a matching term using subcode, code, then the fallback
|
|
57
53
|
if (primary && primary[key]) {
|
|
@@ -61,58 +57,105 @@ export function translate<K extends keyof Translation>(lang: string, key: K, ...
|
|
|
61
57
|
} else if (fallback && fallback[key]) {
|
|
62
58
|
term = fallback[key];
|
|
63
59
|
} else {
|
|
64
|
-
console.error(`
|
|
60
|
+
console.error(`No translation found for: ${key}`);
|
|
65
61
|
return key;
|
|
66
62
|
}
|
|
67
63
|
|
|
68
64
|
if (typeof term === 'function') {
|
|
69
|
-
return term(...args);
|
|
65
|
+
return term(...args) as string;
|
|
70
66
|
}
|
|
71
67
|
|
|
72
68
|
return term;
|
|
73
69
|
}
|
|
74
70
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
} catch {
|
|
82
|
-
console.error(`Invalid language code: "${lang}"`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return result;
|
|
71
|
+
//
|
|
72
|
+
// Formats a date using the specified locale.
|
|
73
|
+
//
|
|
74
|
+
export function date(lang: string, dateToFormat: Date | string, options?: Intl.DateTimeFormatOptions) {
|
|
75
|
+
dateToFormat = new Date(dateToFormat);
|
|
76
|
+
return new Intl.DateTimeFormat(lang, options).format(dateToFormat);
|
|
86
77
|
}
|
|
87
78
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
console.error(`Invalid language code: "${lang}"`);
|
|
96
|
-
}
|
|
79
|
+
//
|
|
80
|
+
// Formats a number using the specified locale.
|
|
81
|
+
//
|
|
82
|
+
export function number(lang: string, numberToFormat: number | string, options?: Intl.NumberFormatOptions) {
|
|
83
|
+
numberToFormat = Number(numberToFormat);
|
|
84
|
+
return isNaN(numberToFormat) ? '' : new Intl.NumberFormat(lang, options).format(numberToFormat);
|
|
85
|
+
}
|
|
97
86
|
|
|
98
|
-
|
|
87
|
+
//
|
|
88
|
+
// Formats a relative date using the specified locale.
|
|
89
|
+
//
|
|
90
|
+
export function relativeTime(
|
|
91
|
+
lang: string,
|
|
92
|
+
value: number,
|
|
93
|
+
unit: Intl.RelativeTimeFormatUnit,
|
|
94
|
+
options?: Intl.RelativeTimeFormatOptions
|
|
95
|
+
) {
|
|
96
|
+
return new Intl.RelativeTimeFormat(lang, options).format(value, unit);
|
|
99
97
|
}
|
|
100
98
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
//
|
|
100
|
+
// Updates the locale for all localized elements that are currently connected
|
|
101
|
+
//
|
|
102
|
+
export function updateLocalizedTerms() {
|
|
103
|
+
documentLanguage = document.documentElement.lang || navigator.language;
|
|
105
104
|
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
[...connectedElements.keys()].map((el: LitElement) => {
|
|
106
|
+
if (typeof el.requestUpdate === 'function') {
|
|
107
|
+
el.requestUpdate();
|
|
108
108
|
}
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
112
|
+
//
|
|
113
|
+
// Reactive controller
|
|
114
|
+
//
|
|
115
|
+
// To use this controller, import the class and instantiate it in a custom element constructor:
|
|
116
|
+
//
|
|
117
|
+
// private localize = new LocalizeController(this);
|
|
118
|
+
//
|
|
119
|
+
// This will add the element to the set and make it respond to changes to <html lang> automatically. To make it respond
|
|
120
|
+
// to changes to its own lang property, make it a property:
|
|
121
|
+
//
|
|
122
|
+
// @property() lang: string;
|
|
123
|
+
//
|
|
124
|
+
// To use a translation method, call it like this:
|
|
125
|
+
//
|
|
126
|
+
// ${this.localize.term('term_key_here')}
|
|
127
|
+
// ${this.localize.date('2021-12-03')}
|
|
128
|
+
// ${this.localize.number(1000000)}
|
|
129
|
+
//
|
|
130
|
+
export class LocalizeController implements ReactiveController {
|
|
131
|
+
host: ReactiveControllerHost & HTMLElement;
|
|
132
|
+
|
|
133
|
+
constructor(host: ReactiveControllerHost & HTMLElement) {
|
|
134
|
+
this.host = host;
|
|
135
|
+
this.host.addController(this);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
hostConnected() {
|
|
139
|
+
connectedElements.add(this.host);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
hostDisconnected() {
|
|
143
|
+
connectedElements.delete(this.host);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
term<K extends keyof Translation>(key: K, ...args: FunctionParams<Translation[K]>) {
|
|
147
|
+
return term(this.host.lang || documentLanguage, key, ...args);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
date(dateToFormat: Date | string, options?: Intl.DateTimeFormatOptions) {
|
|
151
|
+
return date(this.host.lang || documentLanguage, dateToFormat, options);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
number(numberToFormat: number | string, options?: Intl.NumberFormatOptions) {
|
|
155
|
+
return number(this.host.lang || documentLanguage, numberToFormat, options);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
relativeTime(value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions) {
|
|
159
|
+
return relativeTime(this.host.lang || documentLanguage, value, unit, options);
|
|
160
|
+
}
|
|
161
|
+
}
|
package/dist/fast.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { CaptureType, ExecutionContext, FASTElement } from '@microsoft/fast-element';
|
|
2
|
-
import type { FunctionParams, Translation } from './';
|
|
3
|
-
export declare function localize(): (targetClass: any) => any;
|
|
4
|
-
export declare function translate<TSource, K extends keyof Translation>(contextOrElement: ExecutionContext | FASTElement | HTMLElement, key: K, ...args: FunctionParams<Translation[K]>): CaptureType<TSource>;
|
|
5
|
-
export declare function formatDate<TSource = any>(contextOrElement: ExecutionContext | FASTElement | HTMLElement, date: Date | string, options?: Intl.DateTimeFormatOptions): CaptureType<TSource>;
|
|
6
|
-
export declare function formatNumber<TSource = any>(contextOrElement: ExecutionContext | FASTElement | HTMLElement, number: number | string, options?: Intl.DateTimeFormatOptions): CaptureType<TSource>;
|
package/dist/fast.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { connectedElements, detectLanguage, getSystemLanguage, translate as t, formatDate as d, formatNumber as n } from './';
|
|
2
|
-
export function localize() {
|
|
3
|
-
return (targetClass) => {
|
|
4
|
-
return class extends targetClass {
|
|
5
|
-
constructor() {
|
|
6
|
-
super();
|
|
7
|
-
}
|
|
8
|
-
connectedCallback() {
|
|
9
|
-
super.connectedCallback();
|
|
10
|
-
const lang = detectLanguage(this);
|
|
11
|
-
connectedElements.set(this, lang);
|
|
12
|
-
}
|
|
13
|
-
disconnectedCallback() {
|
|
14
|
-
super.disconnectedCallback();
|
|
15
|
-
connectedElements.delete(this);
|
|
16
|
-
}
|
|
17
|
-
get lang() {
|
|
18
|
-
return this.getAttribute('lang');
|
|
19
|
-
}
|
|
20
|
-
set lang(value) {
|
|
21
|
-
if (value === null || value === undefined) {
|
|
22
|
-
this.removeAttribute('lang');
|
|
23
|
-
}
|
|
24
|
-
else {
|
|
25
|
-
this.setAttribute('lang', value);
|
|
26
|
-
}
|
|
27
|
-
connectedElements.set(this, value);
|
|
28
|
-
this.updateLocalizedTerms();
|
|
29
|
-
}
|
|
30
|
-
updateLocalizedTerms() {
|
|
31
|
-
this.$fastController.renderTemplate(this.$fastController.template);
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
export function translate(contextOrElement, key, ...args) {
|
|
37
|
-
const lang = connectedElements.get(contextOrElement) || getSystemLanguage();
|
|
38
|
-
return t(lang, key, ...args);
|
|
39
|
-
}
|
|
40
|
-
export function formatDate(contextOrElement, date, options) {
|
|
41
|
-
const lang = connectedElements.get(contextOrElement) || getSystemLanguage();
|
|
42
|
-
return d(lang, date, options);
|
|
43
|
-
}
|
|
44
|
-
export function formatNumber(contextOrElement, number, options) {
|
|
45
|
-
const lang = connectedElements.get(contextOrElement) || getSystemLanguage();
|
|
46
|
-
return n(lang, number, options);
|
|
47
|
-
}
|
package/dist/lit.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { Directive, Part } from 'lit/directive.js';
|
|
2
|
-
import type { FunctionParams, Translation } from './';
|
|
3
|
-
export declare function localize(): (targetClass: any) => any;
|
|
4
|
-
declare class TranslateDirective extends Directive {
|
|
5
|
-
host: HTMLElement | undefined;
|
|
6
|
-
constructor(part: Part);
|
|
7
|
-
render<K extends keyof Translation>(key: K, ...args: FunctionParams<Translation[K]>): any;
|
|
8
|
-
}
|
|
9
|
-
export declare function translate<K extends keyof Translation>(key: K, ...args: FunctionParams<Translation[K]>): import("lit-html/directive").DirectiveResult<typeof TranslateDirective>;
|
|
10
|
-
declare class FormatDateDirective extends Directive {
|
|
11
|
-
host: HTMLElement | undefined;
|
|
12
|
-
constructor(part: Part);
|
|
13
|
-
render(date: Date | string, options?: Intl.DateTimeFormatOptions): string;
|
|
14
|
-
}
|
|
15
|
-
export declare const formatDate: (date: string | Date, options?: Intl.DateTimeFormatOptions | undefined) => import("lit-html/directive").DirectiveResult<typeof FormatDateDirective>;
|
|
16
|
-
declare class FormatNumberDirective extends Directive {
|
|
17
|
-
host: HTMLElement | undefined;
|
|
18
|
-
constructor(part: Part);
|
|
19
|
-
render(number: number | string, options?: Intl.NumberFormatOptions): string;
|
|
20
|
-
}
|
|
21
|
-
export declare const formatNumber: (number: string | number, options?: Intl.NumberFormatOptions | undefined) => import("lit-html/directive").DirectiveResult<typeof FormatNumberDirective>;
|
|
22
|
-
export {};
|
package/dist/lit.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { Directive, directive } from 'lit/directive.js';
|
|
2
|
-
import { connectedElements, detectLanguage, getSystemLanguage, formatDate as d, formatNumber as n, translate as t } from './';
|
|
3
|
-
export function localize() {
|
|
4
|
-
return (targetClass) => {
|
|
5
|
-
return class extends targetClass {
|
|
6
|
-
static get properties() {
|
|
7
|
-
return Object.assign({ lang: { type: String } }, targetClass.properties);
|
|
8
|
-
}
|
|
9
|
-
connectedCallback() {
|
|
10
|
-
super.connectedCallback();
|
|
11
|
-
const lang = detectLanguage(this);
|
|
12
|
-
connectedElements.set(this, lang);
|
|
13
|
-
}
|
|
14
|
-
disconnectedCallback() {
|
|
15
|
-
super.disconnectedCallback();
|
|
16
|
-
connectedElements.delete(this);
|
|
17
|
-
}
|
|
18
|
-
updateLocalizedTerms() {
|
|
19
|
-
this.requestUpdate();
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
class TranslateDirective extends Directive {
|
|
25
|
-
constructor(part) {
|
|
26
|
-
var _a;
|
|
27
|
-
super(part);
|
|
28
|
-
this.host = (_a = part.options) === null || _a === void 0 ? void 0 : _a.host;
|
|
29
|
-
}
|
|
30
|
-
render(key, ...args) {
|
|
31
|
-
const lang = connectedElements.get(this.host) || getSystemLanguage();
|
|
32
|
-
return t(lang, key, ...args);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
const litDirective = directive(TranslateDirective);
|
|
36
|
-
export function translate(key, ...args) {
|
|
37
|
-
return litDirective(key, ...args);
|
|
38
|
-
}
|
|
39
|
-
class FormatDateDirective extends Directive {
|
|
40
|
-
constructor(part) {
|
|
41
|
-
var _a;
|
|
42
|
-
super(part);
|
|
43
|
-
this.host = (_a = part.options) === null || _a === void 0 ? void 0 : _a.host;
|
|
44
|
-
}
|
|
45
|
-
render(date, options) {
|
|
46
|
-
const lang = connectedElements.get(this.host) || getSystemLanguage();
|
|
47
|
-
return d(lang, date, options);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
export const formatDate = directive(FormatDateDirective);
|
|
51
|
-
class FormatNumberDirective extends Directive {
|
|
52
|
-
constructor(part) {
|
|
53
|
-
var _a;
|
|
54
|
-
super(part);
|
|
55
|
-
this.host = (_a = part.options) === null || _a === void 0 ? void 0 : _a.host;
|
|
56
|
-
}
|
|
57
|
-
render(number, options) {
|
|
58
|
-
const lang = connectedElements.get(this.host) || getSystemLanguage();
|
|
59
|
-
return n(lang, number, options);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
export const formatNumber = directive(FormatNumberDirective);
|
package/src/fast.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
connectedElements,
|
|
3
|
-
detectLanguage,
|
|
4
|
-
getSystemLanguage,
|
|
5
|
-
translate as t,
|
|
6
|
-
formatDate as d,
|
|
7
|
-
formatNumber as n
|
|
8
|
-
} from './';
|
|
9
|
-
import type { CaptureType, ExecutionContext, FASTElement } from '@microsoft/fast-element';
|
|
10
|
-
import type { FunctionParams, Translation } from './';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* FAST Decorator
|
|
14
|
-
*
|
|
15
|
-
* This class decorator ensures lang is a reactive property and adds and removes the component to and from the
|
|
16
|
-
* connectedElements set.
|
|
17
|
-
*/
|
|
18
|
-
export function localize() {
|
|
19
|
-
return (targetClass: any): typeof targetClass => {
|
|
20
|
-
return class extends targetClass {
|
|
21
|
-
constructor() {
|
|
22
|
-
super();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
connectedCallback() {
|
|
26
|
-
super.connectedCallback();
|
|
27
|
-
|
|
28
|
-
const lang = detectLanguage(this as typeof targetClass);
|
|
29
|
-
connectedElements.set(this as typeof targetClass, lang);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
disconnectedCallback() {
|
|
33
|
-
super.disconnectedCallback();
|
|
34
|
-
connectedElements.delete(this as typeof targetClass);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
get lang() {
|
|
38
|
-
return this.getAttribute('lang');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
set lang(value: string) {
|
|
42
|
-
if (value === null || value === undefined) {
|
|
43
|
-
this.removeAttribute('lang');
|
|
44
|
-
} else {
|
|
45
|
-
this.setAttribute('lang', value);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
connectedElements.set(this as typeof targetClass, value);
|
|
49
|
-
this.updateLocalizedTerms();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
updateLocalizedTerms() {
|
|
53
|
-
//
|
|
54
|
-
// This is a hacky way to force a re-render, but there's no other way to achieve this at the moment. This issue
|
|
55
|
-
// tracks a proposal to introduce signals that will allow external changes to trigger updates.
|
|
56
|
-
//
|
|
57
|
-
// https://github.com/microsoft/fast/issues/5083
|
|
58
|
-
//
|
|
59
|
-
this.$fastController.renderTemplate(this.$fastController.template);
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* FAST format number directive
|
|
67
|
-
*
|
|
68
|
-
* Formats a number using the element's current language.
|
|
69
|
-
*/
|
|
70
|
-
export function translate<TSource, K extends keyof Translation>(
|
|
71
|
-
contextOrElement: ExecutionContext | FASTElement | HTMLElement,
|
|
72
|
-
key: K,
|
|
73
|
-
...args: FunctionParams<Translation[K]>
|
|
74
|
-
): CaptureType<TSource> {
|
|
75
|
-
const lang = connectedElements.get(contextOrElement as HTMLElement) || getSystemLanguage();
|
|
76
|
-
return t(lang, key, ...args);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* FAST format date directive
|
|
81
|
-
*
|
|
82
|
-
* Formats a date using the element's current language.
|
|
83
|
-
*/
|
|
84
|
-
export function formatDate<TSource = any>(
|
|
85
|
-
contextOrElement: ExecutionContext | FASTElement | HTMLElement,
|
|
86
|
-
date: Date | string,
|
|
87
|
-
options?: Intl.DateTimeFormatOptions
|
|
88
|
-
): CaptureType<TSource> {
|
|
89
|
-
const lang = connectedElements.get(contextOrElement as HTMLElement) || getSystemLanguage();
|
|
90
|
-
return d(lang, date, options);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* FAST format number directive
|
|
95
|
-
*
|
|
96
|
-
* Formats a number using the element's current language.
|
|
97
|
-
*/
|
|
98
|
-
export function formatNumber<TSource = any>(
|
|
99
|
-
contextOrElement: ExecutionContext | FASTElement | HTMLElement,
|
|
100
|
-
number: number | string,
|
|
101
|
-
options?: Intl.DateTimeFormatOptions
|
|
102
|
-
): CaptureType<TSource> {
|
|
103
|
-
const lang = connectedElements.get(contextOrElement as HTMLElement) || getSystemLanguage();
|
|
104
|
-
return n(lang, number, options);
|
|
105
|
-
}
|
package/src/lit.ts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { Directive, directive, Part } from 'lit/directive.js';
|
|
2
|
-
import {
|
|
3
|
-
connectedElements,
|
|
4
|
-
detectLanguage,
|
|
5
|
-
getSystemLanguage,
|
|
6
|
-
formatDate as d,
|
|
7
|
-
formatNumber as n,
|
|
8
|
-
translate as t
|
|
9
|
-
} from './';
|
|
10
|
-
|
|
11
|
-
import type { FunctionParams, Translation } from './';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Lit Decorator
|
|
15
|
-
*
|
|
16
|
-
* This class decorator ensures lang is a reactive property and adds and removes the component to and from the
|
|
17
|
-
* connectedElements set.
|
|
18
|
-
*/
|
|
19
|
-
export function localize() {
|
|
20
|
-
return (targetClass: any): typeof targetClass => {
|
|
21
|
-
return class extends targetClass {
|
|
22
|
-
static get properties() {
|
|
23
|
-
return {
|
|
24
|
-
// Ensure lang is a watched property
|
|
25
|
-
lang: { type: String },
|
|
26
|
-
...targetClass.properties
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
connectedCallback() {
|
|
31
|
-
super.connectedCallback();
|
|
32
|
-
|
|
33
|
-
const lang = detectLanguage(this as typeof targetClass);
|
|
34
|
-
connectedElements.set(this as typeof targetClass, lang);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
disconnectedCallback() {
|
|
38
|
-
super.disconnectedCallback();
|
|
39
|
-
connectedElements.delete(this as typeof targetClass);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
updateLocalizedTerms() {
|
|
43
|
-
this.requestUpdate();
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Lit translate directive
|
|
51
|
-
*
|
|
52
|
-
* Translates a term using the element's current language.
|
|
53
|
-
*/
|
|
54
|
-
class TranslateDirective extends Directive {
|
|
55
|
-
host: HTMLElement | undefined;
|
|
56
|
-
|
|
57
|
-
constructor(part: Part) {
|
|
58
|
-
super(part);
|
|
59
|
-
this.host = part.options?.host as HTMLElement;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
render<K extends keyof Translation>(key: K, ...args: FunctionParams<Translation[K]>) {
|
|
63
|
-
const lang = connectedElements.get(this.host as HTMLElement) || getSystemLanguage();
|
|
64
|
-
return t(lang, key, ...args);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const litDirective = directive(TranslateDirective);
|
|
69
|
-
|
|
70
|
-
export function translate<K extends keyof Translation>(key: K, ...args: FunctionParams<Translation[K]>) {
|
|
71
|
-
return litDirective(key, ...args);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Lit format date directive
|
|
76
|
-
*
|
|
77
|
-
* Formats a date using the element's current language.
|
|
78
|
-
*/
|
|
79
|
-
class FormatDateDirective extends Directive {
|
|
80
|
-
host: HTMLElement | undefined;
|
|
81
|
-
|
|
82
|
-
constructor(part: Part) {
|
|
83
|
-
super(part);
|
|
84
|
-
this.host = part.options?.host as HTMLElement;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
render(date: Date | string, options?: Intl.DateTimeFormatOptions) {
|
|
88
|
-
const lang = connectedElements.get(this.host as HTMLElement) || getSystemLanguage();
|
|
89
|
-
return d(lang, date, options);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export const formatDate = directive(FormatDateDirective);
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Lit format number directive
|
|
97
|
-
*
|
|
98
|
-
* Formats a number using the element's current language.
|
|
99
|
-
*/
|
|
100
|
-
class FormatNumberDirective extends Directive {
|
|
101
|
-
host: HTMLElement | undefined;
|
|
102
|
-
|
|
103
|
-
constructor(part: Part) {
|
|
104
|
-
super(part);
|
|
105
|
-
this.host = part.options?.host as HTMLElement;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
render(number: number | string, options?: Intl.NumberFormatOptions) {
|
|
109
|
-
const lang = connectedElements.get(this.host as HTMLElement) || getSystemLanguage();
|
|
110
|
-
return n(lang, number, options);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export const formatNumber = directive(FormatNumberDirective);
|