@shoelace-style/localize 2.2.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +47 -18
- package/dist/index.d.ts +4 -6
- package/dist/index.js +34 -41
- package/package.json +3 -3
- package/src/index.ts +46 -69
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
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
|
+
|
|
3
12
|
## 2.2.0
|
|
4
13
|
|
|
5
14
|
- Added `dir()` method to return the target element's directionality
|
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,19 +176,23 @@ 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'})}
|
|
188
192
|
|
|
193
|
+
<!-- Determining language -->
|
|
194
|
+
${this.localize.lang()}
|
|
195
|
+
|
|
189
196
|
<!-- Determining directionality, e.g. 'ltr' or 'rtl' -->
|
|
190
197
|
${this.localize.dir()}
|
|
191
198
|
`;
|
|
@@ -193,20 +200,42 @@ export class MyElement extends LitElement {
|
|
|
193
200
|
}
|
|
194
201
|
```
|
|
195
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
|
+
|
|
196
228
|
## Advantages
|
|
197
229
|
|
|
230
|
+
- Zero dependencies
|
|
198
231
|
- Extremely lightweight
|
|
199
|
-
- Zero dependencies
|
|
200
|
-
- Version 2.1 measures 726 bytes (yes, _bytes_) after minify + gzip
|
|
201
|
-
- Uses existing platform features
|
|
202
232
|
- Supports simple terms, plurals, and complex translations
|
|
203
|
-
- Fun fact: some languages have [six plural forms](https://lingohub.com/blog/2019/02/pluralization) and this
|
|
204
|
-
- 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)
|
|
205
235
|
- Good DX for custom element authors and consumers
|
|
206
236
|
- Intuitive API for custom element authors
|
|
207
237
|
- Consumers only need to load the translations they want and set the `lang` attribute
|
|
208
238
|
- Translations can be loaded up front or on demand
|
|
209
|
-
- Translations can be created by consumers without having to wait for them to get accepted upstream
|
|
210
239
|
|
|
211
240
|
## Disadvantages
|
|
212
241
|
|
package/dist/index.d.ts
CHANGED
|
@@ -4,22 +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;
|
|
21
19
|
lang(): string;
|
|
22
|
-
term<K extends keyof
|
|
20
|
+
term<K extends keyof UserTranslation>(key: K, ...args: FunctionParams<UserTranslation[K]>): any;
|
|
23
21
|
date(dateToFormat: Date | string, options?: Intl.DateTimeFormatOptions): string;
|
|
24
22
|
number(numberToFormat: number | string, options?: Intl.NumberFormatOptions): string;
|
|
25
23
|
relativeTime(value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions): string;
|
package/dist/index.js
CHANGED
|
@@ -6,53 +6,23 @@ let documentLanguage = document.documentElement.lang || navigator.language;
|
|
|
6
6
|
let fallback;
|
|
7
7
|
documentElementObserver.observe(document.documentElement, {
|
|
8
8
|
attributes: true,
|
|
9
|
-
attributeFilter: ['lang']
|
|
9
|
+
attributeFilter: ['dir', 'lang']
|
|
10
10
|
});
|
|
11
11
|
export function registerTranslation(...translation) {
|
|
12
12
|
translation.map(t => {
|
|
13
13
|
const code = t.$code.toLowerCase();
|
|
14
|
-
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
|
+
}
|
|
15
20
|
if (!fallback) {
|
|
16
21
|
fallback = t;
|
|
17
22
|
}
|
|
18
23
|
});
|
|
19
24
|
update();
|
|
20
25
|
}
|
|
21
|
-
export function term(lang, key, ...args) {
|
|
22
|
-
const code = lang.toLowerCase().slice(0, 2);
|
|
23
|
-
const subcode = lang.length > 2 ? lang.toLowerCase() : '';
|
|
24
|
-
const primary = translations.get(subcode);
|
|
25
|
-
const secondary = translations.get(code);
|
|
26
|
-
let term;
|
|
27
|
-
if (primary && primary[key]) {
|
|
28
|
-
term = primary[key];
|
|
29
|
-
}
|
|
30
|
-
else if (secondary && secondary[key]) {
|
|
31
|
-
term = secondary[key];
|
|
32
|
-
}
|
|
33
|
-
else if (fallback && fallback[key]) {
|
|
34
|
-
term = fallback[key];
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
console.error(`No translation found for: ${key}`);
|
|
38
|
-
return key;
|
|
39
|
-
}
|
|
40
|
-
if (typeof term === 'function') {
|
|
41
|
-
return term(...args);
|
|
42
|
-
}
|
|
43
|
-
return term;
|
|
44
|
-
}
|
|
45
|
-
export function date(lang, dateToFormat, options) {
|
|
46
|
-
dateToFormat = new Date(dateToFormat);
|
|
47
|
-
return new Intl.DateTimeFormat(lang, options).format(dateToFormat);
|
|
48
|
-
}
|
|
49
|
-
export function number(lang, numberToFormat, options) {
|
|
50
|
-
numberToFormat = Number(numberToFormat);
|
|
51
|
-
return isNaN(numberToFormat) ? '' : new Intl.NumberFormat(lang, options).format(numberToFormat);
|
|
52
|
-
}
|
|
53
|
-
export function relativeTime(lang, value, unit, options) {
|
|
54
|
-
return new Intl.RelativeTimeFormat(lang, options).format(value, unit);
|
|
55
|
-
}
|
|
56
26
|
export function update() {
|
|
57
27
|
documentDirection = document.documentElement.dir || 'ltr';
|
|
58
28
|
documentLanguage = document.documentElement.lang || navigator.language;
|
|
@@ -80,15 +50,38 @@ export class LocalizeController {
|
|
|
80
50
|
return `${this.host.lang || documentLanguage}`.toLowerCase();
|
|
81
51
|
}
|
|
82
52
|
term(key, ...args) {
|
|
83
|
-
|
|
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;
|
|
84
75
|
}
|
|
85
76
|
date(dateToFormat, options) {
|
|
86
|
-
|
|
77
|
+
dateToFormat = new Date(dateToFormat);
|
|
78
|
+
return new Intl.DateTimeFormat(this.lang(), options).format(dateToFormat);
|
|
87
79
|
}
|
|
88
80
|
number(numberToFormat, options) {
|
|
89
|
-
|
|
81
|
+
numberToFormat = Number(numberToFormat);
|
|
82
|
+
return isNaN(numberToFormat) ? '' : new Intl.NumberFormat(this.lang(), options).format(numberToFormat);
|
|
90
83
|
}
|
|
91
84
|
relativeTime(value, unit, options) {
|
|
92
|
-
return
|
|
85
|
+
return new Intl.RelativeTimeFormat(this.lang(), options).format(value, unit);
|
|
93
86
|
}
|
|
94
87
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shoelace-style/localize",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "A micro library for localizing custom elements using Lit's Reactive Controller model.",
|
|
5
|
-
"main": "
|
|
6
|
-
"module": "
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
9
9
|
"scripts": {
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,9 @@ 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
|
|
|
@@ -19,7 +22,7 @@ let fallback: Translation;
|
|
|
19
22
|
// Watch for changes on <html lang>
|
|
20
23
|
documentElementObserver.observe(document.documentElement, {
|
|
21
24
|
attributes: true,
|
|
22
|
-
attributeFilter: ['lang']
|
|
25
|
+
attributeFilter: ['dir', 'lang']
|
|
23
26
|
});
|
|
24
27
|
|
|
25
28
|
//
|
|
@@ -28,7 +31,13 @@ documentElementObserver.observe(document.documentElement, {
|
|
|
28
31
|
export function registerTranslation(...translation: Translation[]) {
|
|
29
32
|
translation.map(t => {
|
|
30
33
|
const code = t.$code.toLowerCase();
|
|
31
|
-
|
|
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
|
+
}
|
|
32
41
|
|
|
33
42
|
// The first translation that's registered is the fallback
|
|
34
43
|
if (!fallback) {
|
|
@@ -39,64 +48,6 @@ export function registerTranslation(...translation: Translation[]) {
|
|
|
39
48
|
update();
|
|
40
49
|
}
|
|
41
50
|
|
|
42
|
-
//
|
|
43
|
-
// Translates a term using the specified locale. Looks up translations in order of language + country codes (es-PE),
|
|
44
|
-
// language code (es), then the fallback translation.
|
|
45
|
-
//
|
|
46
|
-
export function term<K extends keyof Translation>(lang: string, key: K, ...args: FunctionParams<Translation[K]>) {
|
|
47
|
-
const code = lang.toLowerCase().slice(0, 2); // e.g. en
|
|
48
|
-
const subcode = lang.length > 2 ? lang.toLowerCase() : ''; // e.g. en-GB
|
|
49
|
-
const primary = translations.get(subcode);
|
|
50
|
-
const secondary = translations.get(code);
|
|
51
|
-
let term: any;
|
|
52
|
-
|
|
53
|
-
// Look for a matching term using subcode, code, then the fallback
|
|
54
|
-
if (primary && primary[key]) {
|
|
55
|
-
term = primary[key];
|
|
56
|
-
} else if (secondary && secondary[key]) {
|
|
57
|
-
term = secondary[key];
|
|
58
|
-
} else if (fallback && fallback[key]) {
|
|
59
|
-
term = fallback[key];
|
|
60
|
-
} else {
|
|
61
|
-
console.error(`No translation found for: ${key}`);
|
|
62
|
-
return key;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (typeof term === 'function') {
|
|
66
|
-
return term(...args) as string;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return term;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
//
|
|
73
|
-
// Formats a date using the specified locale.
|
|
74
|
-
//
|
|
75
|
-
export function date(lang: string, dateToFormat: Date | string, options?: Intl.DateTimeFormatOptions) {
|
|
76
|
-
dateToFormat = new Date(dateToFormat);
|
|
77
|
-
return new Intl.DateTimeFormat(lang, options).format(dateToFormat);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
// Formats a number using the specified locale.
|
|
82
|
-
//
|
|
83
|
-
export function number(lang: string, numberToFormat: number | string, options?: Intl.NumberFormatOptions) {
|
|
84
|
-
numberToFormat = Number(numberToFormat);
|
|
85
|
-
return isNaN(numberToFormat) ? '' : new Intl.NumberFormat(lang, options).format(numberToFormat);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
//
|
|
89
|
-
// Formats a relative date using the specified locale.
|
|
90
|
-
//
|
|
91
|
-
export function relativeTime(
|
|
92
|
-
lang: string,
|
|
93
|
-
value: number,
|
|
94
|
-
unit: Intl.RelativeTimeFormatUnit,
|
|
95
|
-
options?: Intl.RelativeTimeFormatOptions
|
|
96
|
-
) {
|
|
97
|
-
return new Intl.RelativeTimeFormat(lang, options).format(value, unit);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
51
|
//
|
|
101
52
|
// Updates all localized elements that are currently connected
|
|
102
53
|
//
|
|
@@ -118,9 +69,10 @@ export function update() {
|
|
|
118
69
|
//
|
|
119
70
|
// private localize = new LocalizeController(this);
|
|
120
71
|
//
|
|
121
|
-
// This will add the element to the set and make it respond to changes to <html lang> automatically. To make it
|
|
122
|
-
// 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:
|
|
123
74
|
//
|
|
75
|
+
// @property() dir: string;
|
|
124
76
|
// @property() lang: string;
|
|
125
77
|
//
|
|
126
78
|
// To use a translation method, call it like this:
|
|
@@ -129,7 +81,9 @@ export function update() {
|
|
|
129
81
|
// ${this.localize.date('2021-12-03')}
|
|
130
82
|
// ${this.localize.number(1000000)}
|
|
131
83
|
//
|
|
132
|
-
export class LocalizeController
|
|
84
|
+
export class LocalizeController<UserTranslation extends Translation = DefaultTranslation>
|
|
85
|
+
implements ReactiveController
|
|
86
|
+
{
|
|
133
87
|
host: ReactiveControllerHost & HTMLElement;
|
|
134
88
|
|
|
135
89
|
constructor(host: ReactiveControllerHost & HTMLElement) {
|
|
@@ -161,23 +115,46 @@ export class LocalizeController implements ReactiveController {
|
|
|
161
115
|
return `${this.host.lang || documentLanguage}`.toLowerCase();
|
|
162
116
|
}
|
|
163
117
|
|
|
164
|
-
term<K extends keyof
|
|
165
|
-
|
|
166
|
-
|
|
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;
|
|
167
142
|
}
|
|
168
143
|
|
|
169
144
|
/** Outputs a localized date in the specified format. */
|
|
170
145
|
date(dateToFormat: Date | string, options?: Intl.DateTimeFormatOptions) {
|
|
171
|
-
|
|
146
|
+
dateToFormat = new Date(dateToFormat);
|
|
147
|
+
return new Intl.DateTimeFormat(this.lang(), options).format(dateToFormat);
|
|
172
148
|
}
|
|
173
149
|
|
|
174
150
|
/** Outputs a localized number in the specified format. */
|
|
175
151
|
number(numberToFormat: number | string, options?: Intl.NumberFormatOptions) {
|
|
176
|
-
|
|
152
|
+
numberToFormat = Number(numberToFormat);
|
|
153
|
+
return isNaN(numberToFormat) ? '' : new Intl.NumberFormat(this.lang(), options).format(numberToFormat);
|
|
177
154
|
}
|
|
178
155
|
|
|
179
156
|
/** Outputs a localized time in relative format. */
|
|
180
157
|
relativeTime(value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions) {
|
|
181
|
-
return
|
|
158
|
+
return new Intl.RelativeTimeFormat(this.lang(), options).format(value, unit);
|
|
182
159
|
}
|
|
183
160
|
}
|