@shoelace-style/localize 2.1.3 → 3.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/CHANGELOG.md +14 -0
- package/README.md +50 -18
- package/dist/index.d.ts +6 -6
- package/dist/index.js +42 -41
- package/package.json +1 -1
- package/src/index.ts +67 -68
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.0.0
|
|
4
|
+
|
|
5
|
+
- 🚨BREAKING: Removed top level `term()`, `date()`, `number()`, and `relativeTime()` functions
|
|
6
|
+
- Refactored `LocalizeController.term()` to allow strong typings by extending the controller and default translation (see "Typed Translations and Arguments" in the readme for details)
|
|
7
|
+
|
|
8
|
+
## 2.2.1
|
|
9
|
+
|
|
10
|
+
- Fixed a bug that prevented updates from happening when `<html dir>` changed
|
|
11
|
+
|
|
12
|
+
## 2.2.0
|
|
13
|
+
|
|
14
|
+
- Added `dir()` method to return the target element's directionality
|
|
15
|
+
- Added `lang()` method to return the target element's language
|
|
16
|
+
|
|
3
17
|
## 2.1.3
|
|
4
18
|
|
|
5
19
|
- Renamed `updateLocalizedTerms()` to `update()` (forgive me SemVer, but nobody was using this I promise)
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Shoelace: Localize
|
|
2
2
|
|
|
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,
|
|
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, [Reactive Controller](https://lit.dev/docs/composition/controllers/) for sharing and applying translations across one or more custom elements in a component library.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Reactive Controllers are supported by Lit 2 out of the box, but they're designed to be generic so other libraries can elect to support them either natively or through an adapter. If you're favorite custom element authoring library doesn't support Reactive Controllers yet, consider asking the maintainers to add support for them!
|
|
6
6
|
|
|
7
7
|
## Overview
|
|
8
8
|
|
|
@@ -11,7 +11,7 @@ Here's an example of how this library can be used to create a localized custom e
|
|
|
11
11
|
```ts
|
|
12
12
|
import { LocalizeController, registerTranslation } from '@shoelace-style/localize';
|
|
13
13
|
|
|
14
|
-
//
|
|
14
|
+
// Note: translations can also be lazy loaded (see "Registering Translations" below)
|
|
15
15
|
import en from '../translations/en';
|
|
16
16
|
import es from '../translations/es';
|
|
17
17
|
|
|
@@ -46,9 +46,10 @@ Changes to `<html lang>` will trigger an update to all localized components auto
|
|
|
46
46
|
It's not uncommon for a custom element to require localization, but implementing it at the component level is challenging. For example, how should we provide a translation for this close button that exists in a custom element's shadow root?
|
|
47
47
|
|
|
48
48
|
```html
|
|
49
|
-
|
|
50
|
-
<
|
|
51
|
-
|
|
49
|
+
#shadow-root
|
|
50
|
+
<button type="button" aria-label="Close">
|
|
51
|
+
<svg>...</svg>
|
|
52
|
+
</button>
|
|
52
53
|
```
|
|
53
54
|
|
|
54
55
|
Typically, custom element authors dance around the problem by exposing attributes or properties for such purposes.
|
|
@@ -81,6 +82,8 @@ This library provides a set of tools to localize dates, currencies, numbers, and
|
|
|
81
82
|
|
|
82
83
|
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.
|
|
83
84
|
|
|
85
|
+
Fortunately, the majority of use cases appear to favor a single language per page. However, multiple languages per page are also supported, but you'll need to explicitly set the `lang` attribute on all components whose language differs from the one set in `<html lang>`.
|
|
86
|
+
|
|
84
87
|
## Usage
|
|
85
88
|
|
|
86
89
|
First, install the library.
|
|
@@ -112,10 +115,10 @@ const translation: Translation = {
|
|
|
112
115
|
upload: 'Upload',
|
|
113
116
|
|
|
114
117
|
// Terms with placeholders
|
|
115
|
-
|
|
118
|
+
greetUser: (name: string) => `Hello, ${name}!`,
|
|
116
119
|
|
|
117
120
|
// Plurals
|
|
118
|
-
|
|
121
|
+
numFilesSelected: (count: number) => {
|
|
119
122
|
if (count === 0) return 'No files selected';
|
|
120
123
|
if (count === 1) return '1 file selected';
|
|
121
124
|
return `${count} files selected`;
|
|
@@ -162,7 +165,7 @@ async function changeLanguage(lang) {
|
|
|
162
165
|
|
|
163
166
|
### Localizing Components
|
|
164
167
|
|
|
165
|
-
You can use the `LocalizeController` with any library that supports [Lit's Reactive Controller pattern](https://lit.dev/docs/composition/controllers/). In
|
|
168
|
+
You can use the `LocalizeController` with any library that supports [Lit's Reactive Controller pattern](https://lit.dev/docs/composition/controllers/). In Lit, a localized custom element will look something like this.
|
|
166
169
|
|
|
167
170
|
```ts
|
|
168
171
|
import { LitElement } from 'lit';
|
|
@@ -173,37 +176,66 @@ import { LocalizeController } from '@shoelace-style/localize/dist/lit.js';
|
|
|
173
176
|
export class MyElement extends LitElement {
|
|
174
177
|
private localize = new LocalizeController(this);
|
|
175
178
|
|
|
179
|
+
// Make sure to define `lang` so the component will respond to changes to its own lang attribute
|
|
176
180
|
@property() lang: string;
|
|
177
181
|
|
|
178
182
|
render() {
|
|
179
183
|
return html`
|
|
180
|
-
<!--
|
|
184
|
+
<!-- Terms -->
|
|
181
185
|
${this.localize.term('hello')}
|
|
182
186
|
|
|
183
|
-
<!--
|
|
187
|
+
<!-- Dates -->
|
|
184
188
|
${this.localize.date('2021-09-15 14:00:00 ET'), { month: 'long', day: 'numeric', year: 'numeric' }}
|
|
185
189
|
|
|
186
|
-
<!--
|
|
190
|
+
<!-- Numbers/currency -->
|
|
187
191
|
${this.localize.number(1000, { style: 'currency', currency: 'USD'})}
|
|
192
|
+
|
|
193
|
+
<!-- Determining language -->
|
|
194
|
+
${this.localize.lang()}
|
|
195
|
+
|
|
196
|
+
<!-- Determining directionality, e.g. 'ltr' or 'rtl' -->
|
|
197
|
+
${this.localize.dir()}
|
|
188
198
|
`;
|
|
189
199
|
}
|
|
190
200
|
}
|
|
191
201
|
```
|
|
192
202
|
|
|
203
|
+
## Typed Translations and Arguments
|
|
204
|
+
|
|
205
|
+
Because translations are defined by the user, there's no way for TypeScript to automatically know about the terms you've defined. This means you won't get strongly typed arguments when calling `this.localize.term()`. However, you can solve this by extending `Translation` and `LocalizeController`.
|
|
206
|
+
|
|
207
|
+
In a separate file, e.g. `my-localize.ts`, add the following code.
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
import { LocalizeController as DefaultLocalizeController } from '@shoelace-style/localize';
|
|
211
|
+
|
|
212
|
+
// Extend the default controller with your custom translation
|
|
213
|
+
export class LocalizeController extends DefaultLocalizeController<MyTranslation> {}
|
|
214
|
+
|
|
215
|
+
// Export `registerTranslation` so you can import everything from this file
|
|
216
|
+
export { registerTranslation } from '@shoelace-style/localize';
|
|
217
|
+
|
|
218
|
+
// Define your translation terms here
|
|
219
|
+
export interface MyTranslation extends Translation {
|
|
220
|
+
myTerm: string;
|
|
221
|
+
myOtherTerm: string;
|
|
222
|
+
myTermWithArgs: (count: string) => string;
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Now you can import `MyLocalizeController` and get strongly typed translations when you use `this.localize.term()`!
|
|
227
|
+
|
|
193
228
|
## Advantages
|
|
194
229
|
|
|
230
|
+
- Zero dependencies
|
|
195
231
|
- Extremely lightweight
|
|
196
|
-
- Zero dependencies
|
|
197
|
-
- Version 2.1 measures 726 bytes (yes, _bytes_) after minify + gzip
|
|
198
|
-
- Uses existing platform features
|
|
199
232
|
- Supports simple terms, plurals, and complex translations
|
|
200
|
-
- Fun fact: some languages have [six plural forms](https://lingohub.com/blog/2019/02/pluralization) and this
|
|
201
|
-
- Supports dates, numbers, and currencies
|
|
233
|
+
- Fun fact: some languages have [six plural forms](https://lingohub.com/blog/2019/02/pluralization) and this utility supports that
|
|
234
|
+
- Supports dates, numbers, and currencies using built-in [`Intl` APIs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl)
|
|
202
235
|
- Good DX for custom element authors and consumers
|
|
203
236
|
- Intuitive API for custom element authors
|
|
204
237
|
- Consumers only need to load the translations they want and set the `lang` attribute
|
|
205
238
|
- Translations can be loaded up front or on demand
|
|
206
|
-
- Translations can be created by consumers without having to wait for them to get accepted upstream
|
|
207
239
|
|
|
208
240
|
## Disadvantages
|
|
209
241
|
|
package/dist/index.d.ts
CHANGED
|
@@ -4,20 +4,20 @@ export interface Translation {
|
|
|
4
4
|
$code: string;
|
|
5
5
|
$name: string;
|
|
6
6
|
$dir: 'ltr' | 'rtl';
|
|
7
|
+
}
|
|
8
|
+
export interface DefaultTranslation extends Translation {
|
|
7
9
|
[key: string]: any;
|
|
8
10
|
}
|
|
9
11
|
export declare function registerTranslation(...translation: Translation[]): void;
|
|
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
12
|
export declare function update(): void;
|
|
15
|
-
export declare class LocalizeController implements ReactiveController {
|
|
13
|
+
export declare class LocalizeController<UserTranslation extends Translation = DefaultTranslation> implements ReactiveController {
|
|
16
14
|
host: ReactiveControllerHost & HTMLElement;
|
|
17
15
|
constructor(host: ReactiveControllerHost & HTMLElement);
|
|
18
16
|
hostConnected(): void;
|
|
19
17
|
hostDisconnected(): void;
|
|
20
|
-
|
|
18
|
+
dir(): string;
|
|
19
|
+
lang(): string;
|
|
20
|
+
term<K extends keyof UserTranslation>(key: K, ...args: FunctionParams<UserTranslation[K]>): any;
|
|
21
21
|
date(dateToFormat: Date | string, options?: Intl.DateTimeFormatOptions): string;
|
|
22
22
|
number(numberToFormat: number | string, options?: Intl.NumberFormatOptions): string;
|
|
23
23
|
relativeTime(value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions): string;
|
package/dist/index.js
CHANGED
|
@@ -1,58 +1,30 @@
|
|
|
1
1
|
const connectedElements = new Set();
|
|
2
2
|
const documentElementObserver = new MutationObserver(update);
|
|
3
3
|
const translations = new Map();
|
|
4
|
+
let documentDirection = document.documentElement.dir || 'ltr';
|
|
4
5
|
let documentLanguage = document.documentElement.lang || navigator.language;
|
|
5
6
|
let fallback;
|
|
6
7
|
documentElementObserver.observe(document.documentElement, {
|
|
7
8
|
attributes: true,
|
|
8
|
-
attributeFilter: ['lang']
|
|
9
|
+
attributeFilter: ['dir', 'lang']
|
|
9
10
|
});
|
|
10
11
|
export function registerTranslation(...translation) {
|
|
11
12
|
translation.map(t => {
|
|
12
13
|
const code = t.$code.toLowerCase();
|
|
13
|
-
translations.
|
|
14
|
+
if (translations.has(code)) {
|
|
15
|
+
translations.set(code, Object.assign(Object.assign({}, translations.get(code)), t));
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
translations.set(code, t);
|
|
19
|
+
}
|
|
14
20
|
if (!fallback) {
|
|
15
21
|
fallback = t;
|
|
16
22
|
}
|
|
17
23
|
});
|
|
18
24
|
update();
|
|
19
25
|
}
|
|
20
|
-
export function term(lang, key, ...args) {
|
|
21
|
-
const code = lang.toLowerCase().slice(0, 2);
|
|
22
|
-
const subcode = lang.length > 2 ? lang.toLowerCase() : '';
|
|
23
|
-
const primary = translations.get(subcode);
|
|
24
|
-
const secondary = translations.get(code);
|
|
25
|
-
let term;
|
|
26
|
-
if (primary && primary[key]) {
|
|
27
|
-
term = primary[key];
|
|
28
|
-
}
|
|
29
|
-
else if (secondary && secondary[key]) {
|
|
30
|
-
term = secondary[key];
|
|
31
|
-
}
|
|
32
|
-
else if (fallback && fallback[key]) {
|
|
33
|
-
term = fallback[key];
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
console.error(`No translation found for: ${key}`);
|
|
37
|
-
return key;
|
|
38
|
-
}
|
|
39
|
-
if (typeof term === 'function') {
|
|
40
|
-
return term(...args);
|
|
41
|
-
}
|
|
42
|
-
return term;
|
|
43
|
-
}
|
|
44
|
-
export function date(lang, dateToFormat, options) {
|
|
45
|
-
dateToFormat = new Date(dateToFormat);
|
|
46
|
-
return new Intl.DateTimeFormat(lang, options).format(dateToFormat);
|
|
47
|
-
}
|
|
48
|
-
export function number(lang, numberToFormat, options) {
|
|
49
|
-
numberToFormat = Number(numberToFormat);
|
|
50
|
-
return isNaN(numberToFormat) ? '' : new Intl.NumberFormat(lang, options).format(numberToFormat);
|
|
51
|
-
}
|
|
52
|
-
export function relativeTime(lang, value, unit, options) {
|
|
53
|
-
return new Intl.RelativeTimeFormat(lang, options).format(value, unit);
|
|
54
|
-
}
|
|
55
26
|
export function update() {
|
|
27
|
+
documentDirection = document.documentElement.dir || 'ltr';
|
|
56
28
|
documentLanguage = document.documentElement.lang || navigator.language;
|
|
57
29
|
[...connectedElements.keys()].map((el) => {
|
|
58
30
|
if (typeof el.requestUpdate === 'function') {
|
|
@@ -71,16 +43,45 @@ export class LocalizeController {
|
|
|
71
43
|
hostDisconnected() {
|
|
72
44
|
connectedElements.delete(this.host);
|
|
73
45
|
}
|
|
46
|
+
dir() {
|
|
47
|
+
return `${this.host.dir || documentDirection}`.toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
lang() {
|
|
50
|
+
return `${this.host.lang || documentLanguage}`.toLowerCase();
|
|
51
|
+
}
|
|
74
52
|
term(key, ...args) {
|
|
75
|
-
|
|
53
|
+
const code = this.lang().toLowerCase().slice(0, 2);
|
|
54
|
+
const regionCode = this.lang().length > 2 ? this.lang().toLowerCase() : '';
|
|
55
|
+
const primary = translations.get(regionCode);
|
|
56
|
+
const secondary = translations.get(code);
|
|
57
|
+
let term;
|
|
58
|
+
if (primary && primary[key]) {
|
|
59
|
+
term = primary[key];
|
|
60
|
+
}
|
|
61
|
+
else if (secondary && secondary[key]) {
|
|
62
|
+
term = secondary[key];
|
|
63
|
+
}
|
|
64
|
+
else if (fallback && fallback[key]) {
|
|
65
|
+
term = fallback[key];
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
console.error(`No translation found for: ${String(key)}`);
|
|
69
|
+
return key;
|
|
70
|
+
}
|
|
71
|
+
if (typeof term === 'function') {
|
|
72
|
+
return term(...args);
|
|
73
|
+
}
|
|
74
|
+
return term;
|
|
76
75
|
}
|
|
77
76
|
date(dateToFormat, options) {
|
|
78
|
-
|
|
77
|
+
dateToFormat = new Date(dateToFormat);
|
|
78
|
+
return new Intl.DateTimeFormat(this.lang(), options).format(dateToFormat);
|
|
79
79
|
}
|
|
80
80
|
number(numberToFormat, options) {
|
|
81
|
-
|
|
81
|
+
numberToFormat = Number(numberToFormat);
|
|
82
|
+
return isNaN(numberToFormat) ? '' : new Intl.NumberFormat(this.lang(), options).format(numberToFormat);
|
|
82
83
|
}
|
|
83
84
|
relativeTime(value, unit, options) {
|
|
84
|
-
return
|
|
85
|
+
return new Intl.RelativeTimeFormat(this.lang(), options).format(value, unit);
|
|
85
86
|
}
|
|
86
87
|
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -6,19 +6,23 @@ export interface Translation {
|
|
|
6
6
|
$code: string; // e.g. en, en-GB
|
|
7
7
|
$name: string; // e.g. English, Español
|
|
8
8
|
$dir: 'ltr' | 'rtl';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DefaultTranslation extends Translation {
|
|
9
12
|
[key: string]: any;
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
const connectedElements = new Set<HTMLElement>();
|
|
13
16
|
const documentElementObserver = new MutationObserver(update);
|
|
14
17
|
const translations: Map<string, Translation> = new Map();
|
|
18
|
+
let documentDirection = document.documentElement.dir || 'ltr';
|
|
15
19
|
let documentLanguage = document.documentElement.lang || navigator.language;
|
|
16
20
|
let fallback: Translation;
|
|
17
21
|
|
|
18
22
|
// Watch for changes on <html lang>
|
|
19
23
|
documentElementObserver.observe(document.documentElement, {
|
|
20
24
|
attributes: true,
|
|
21
|
-
attributeFilter: ['lang']
|
|
25
|
+
attributeFilter: ['dir', 'lang']
|
|
22
26
|
});
|
|
23
27
|
|
|
24
28
|
//
|
|
@@ -27,7 +31,13 @@ documentElementObserver.observe(document.documentElement, {
|
|
|
27
31
|
export function registerTranslation(...translation: Translation[]) {
|
|
28
32
|
translation.map(t => {
|
|
29
33
|
const code = t.$code.toLowerCase();
|
|
30
|
-
|
|
34
|
+
|
|
35
|
+
if (translations.has(code)) {
|
|
36
|
+
// Merge translations that share the same language code
|
|
37
|
+
translations.set(code, { ...translations.get(code), ...t });
|
|
38
|
+
} else {
|
|
39
|
+
translations.set(code, t);
|
|
40
|
+
}
|
|
31
41
|
|
|
32
42
|
// The first translation that's registered is the fallback
|
|
33
43
|
if (!fallback) {
|
|
@@ -38,68 +48,11 @@ export function registerTranslation(...translation: Translation[]) {
|
|
|
38
48
|
update();
|
|
39
49
|
}
|
|
40
50
|
|
|
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]>) {
|
|
46
|
-
const code = lang.toLowerCase().slice(0, 2); // e.g. en
|
|
47
|
-
const subcode = lang.length > 2 ? lang.toLowerCase() : ''; // e.g. en-GB
|
|
48
|
-
const primary = translations.get(subcode);
|
|
49
|
-
const secondary = translations.get(code);
|
|
50
|
-
let term: any;
|
|
51
|
-
|
|
52
|
-
// Look for a matching term using subcode, code, then the fallback
|
|
53
|
-
if (primary && primary[key]) {
|
|
54
|
-
term = primary[key];
|
|
55
|
-
} else if (secondary && secondary[key]) {
|
|
56
|
-
term = secondary[key];
|
|
57
|
-
} else if (fallback && fallback[key]) {
|
|
58
|
-
term = fallback[key];
|
|
59
|
-
} else {
|
|
60
|
-
console.error(`No translation found for: ${key}`);
|
|
61
|
-
return key;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (typeof term === 'function') {
|
|
65
|
-
return term(...args) as string;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return term;
|
|
69
|
-
}
|
|
70
|
-
|
|
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);
|
|
77
|
-
}
|
|
78
|
-
|
|
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
|
-
}
|
|
86
|
-
|
|
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);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
51
|
//
|
|
100
52
|
// Updates all localized elements that are currently connected
|
|
101
53
|
//
|
|
102
54
|
export function update() {
|
|
55
|
+
documentDirection = document.documentElement.dir || 'ltr';
|
|
103
56
|
documentLanguage = document.documentElement.lang || navigator.language;
|
|
104
57
|
|
|
105
58
|
[...connectedElements.keys()].map((el: LitElement) => {
|
|
@@ -116,9 +69,10 @@ export function update() {
|
|
|
116
69
|
//
|
|
117
70
|
// private localize = new LocalizeController(this);
|
|
118
71
|
//
|
|
119
|
-
// This will add the element to the set and make it respond to changes to <html lang> automatically. To make it
|
|
120
|
-
// to changes to its own lang
|
|
72
|
+
// This will add the element to the set and make it respond to changes to <html dir|lang> automatically. To make it
|
|
73
|
+
// respond to changes to its own dir|lang properties, make it a property:
|
|
121
74
|
//
|
|
75
|
+
// @property() dir: string;
|
|
122
76
|
// @property() lang: string;
|
|
123
77
|
//
|
|
124
78
|
// To use a translation method, call it like this:
|
|
@@ -127,7 +81,9 @@ export function update() {
|
|
|
127
81
|
// ${this.localize.date('2021-12-03')}
|
|
128
82
|
// ${this.localize.number(1000000)}
|
|
129
83
|
//
|
|
130
|
-
export class LocalizeController
|
|
84
|
+
export class LocalizeController<UserTranslation extends Translation = DefaultTranslation>
|
|
85
|
+
implements ReactiveController
|
|
86
|
+
{
|
|
131
87
|
host: ReactiveControllerHost & HTMLElement;
|
|
132
88
|
|
|
133
89
|
constructor(host: ReactiveControllerHost & HTMLElement) {
|
|
@@ -143,19 +99,62 @@ export class LocalizeController implements ReactiveController {
|
|
|
143
99
|
connectedElements.delete(this.host);
|
|
144
100
|
}
|
|
145
101
|
|
|
146
|
-
|
|
147
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Gets the host element's directionality as determined by the `dir` attribute. The return value is transformed to
|
|
104
|
+
* lowercase.
|
|
105
|
+
*/
|
|
106
|
+
dir() {
|
|
107
|
+
return `${this.host.dir || documentDirection}`.toLowerCase();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Gets the host element's language as determined by the `lang` attribute. The return value is transformed to
|
|
112
|
+
* lowercase.
|
|
113
|
+
*/
|
|
114
|
+
lang() {
|
|
115
|
+
return `${this.host.lang || documentLanguage}`.toLowerCase();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
term<K extends keyof UserTranslation>(key: K, ...args: FunctionParams<UserTranslation[K]>) {
|
|
119
|
+
const code = this.lang().toLowerCase().slice(0, 2); // e.g. en
|
|
120
|
+
const regionCode = this.lang().length > 2 ? this.lang().toLowerCase() : ''; // e.g. en-gb
|
|
121
|
+
const primary = <UserTranslation>translations.get(regionCode);
|
|
122
|
+
const secondary = <UserTranslation>translations.get(code);
|
|
123
|
+
let term: any;
|
|
124
|
+
|
|
125
|
+
// Look for a matching term using regionCode, code, then the fallback
|
|
126
|
+
if (primary && primary[key]) {
|
|
127
|
+
term = primary[key];
|
|
128
|
+
} else if (secondary && secondary[key]) {
|
|
129
|
+
term = secondary[key];
|
|
130
|
+
} else if (fallback && fallback[key as keyof Translation]) {
|
|
131
|
+
term = fallback[key as keyof Translation];
|
|
132
|
+
} else {
|
|
133
|
+
console.error(`No translation found for: ${String(key)}`);
|
|
134
|
+
return key;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (typeof term === 'function') {
|
|
138
|
+
return term(...args) as string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return term;
|
|
148
142
|
}
|
|
149
143
|
|
|
144
|
+
/** Outputs a localized date in the specified format. */
|
|
150
145
|
date(dateToFormat: Date | string, options?: Intl.DateTimeFormatOptions) {
|
|
151
|
-
|
|
146
|
+
dateToFormat = new Date(dateToFormat);
|
|
147
|
+
return new Intl.DateTimeFormat(this.lang(), options).format(dateToFormat);
|
|
152
148
|
}
|
|
153
149
|
|
|
150
|
+
/** Outputs a localized number in the specified format. */
|
|
154
151
|
number(numberToFormat: number | string, options?: Intl.NumberFormatOptions) {
|
|
155
|
-
|
|
152
|
+
numberToFormat = Number(numberToFormat);
|
|
153
|
+
return isNaN(numberToFormat) ? '' : new Intl.NumberFormat(this.lang(), options).format(numberToFormat);
|
|
156
154
|
}
|
|
157
155
|
|
|
156
|
+
/** Outputs a localized time in relative format. */
|
|
158
157
|
relativeTime(value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions) {
|
|
159
|
-
return
|
|
158
|
+
return new Intl.RelativeTimeFormat(this.lang(), options).format(value, unit);
|
|
160
159
|
}
|
|
161
160
|
}
|