intl-template 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +138 -58
- package/intl.cjs +172 -1
- package/intl.d.ts +11 -8
- package/intl.js +114 -67
- package/package.json +13 -6
- package/intl.test.d.ts +0 -1
- package/intl.test.js +0 -132
package/README.md
CHANGED
|
@@ -1,19 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# intl-template
|
|
2
2
|
|
|
3
|
-
A tiny i18n/l10n
|
|
3
|
+
A tiny i18n/l10n helper built on JavaScript Tagged Template Literals.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- [intel-template](#intel-template)
|
|
8
|
-
- [Table of Contents](#table-of-contents)
|
|
9
|
-
- [Installation](#installation)
|
|
10
|
-
- [Usage](#usage)
|
|
11
|
-
- [Use browser specifies locale](#use-browser-specifies-locale)
|
|
12
|
-
- [Use with React](#use-with-react)
|
|
13
|
-
- [Specify slot order](#specify-slot-order)
|
|
14
|
-
- [Nested](#nested)
|
|
15
|
-
- [Function slot](#function-slot)
|
|
16
|
-
- [Call as function](#call-as-function)
|
|
5
|
+
`intl-template` lets source strings stay close to your UI code while translations live in a simple locale-indexed template map.
|
|
17
6
|
|
|
18
7
|
## Installation
|
|
19
8
|
|
|
@@ -21,96 +10,187 @@ A tiny i18n/l10n TTL(Tagged Template Literals) function
|
|
|
21
10
|
npm install intl-template
|
|
22
11
|
```
|
|
23
12
|
|
|
24
|
-
##
|
|
13
|
+
## Quick Start
|
|
25
14
|
|
|
26
|
-
```
|
|
27
|
-
import translation from "intl-template"
|
|
15
|
+
```javascript
|
|
16
|
+
import translation, { l10n } from "intl-template"
|
|
28
17
|
|
|
18
|
+
translation.locale = "es-ES"
|
|
29
19
|
translation.templates["es-ES"] = {
|
|
30
|
-
"hello {}": "hola {}"
|
|
20
|
+
"hello {}": "hola {}",
|
|
31
21
|
}
|
|
32
22
|
|
|
33
|
-
const
|
|
23
|
+
const name = "Willow"
|
|
34
24
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
console.log(l10n`hello ${name}`)
|
|
38
|
-
// => hola willow
|
|
25
|
+
console.log(l10n`hello ${name}`.toString())
|
|
26
|
+
// => hola Willow
|
|
39
27
|
```
|
|
40
28
|
|
|
41
|
-
|
|
29
|
+
Every interpolation in a tagged template becomes `{}` in the translation key. For example, `l10n`hello ${name}`` looks up the key `"hello {}"`.
|
|
30
|
+
|
|
31
|
+
By default, translations return a `Runes` array so React nodes and other non-string values can pass through unchanged. Call `.toString()` when you need plain text, or create a `Translation` instance in `"string"` mode.
|
|
32
|
+
|
|
33
|
+
## Templates
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
translation.templates["en-US"] = {
|
|
37
|
+
"hello {}": "hello {}",
|
|
38
|
+
"{} invited {}": "{} invited {}",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
translation.templates["de-DE"] = {
|
|
42
|
+
"hello {}": "hallo {}",
|
|
43
|
+
"{} invited {}": "{1} wurde von {0} eingeladen",
|
|
44
|
+
}
|
|
42
45
|
```
|
|
46
|
+
|
|
47
|
+
Use `{}` to keep the original slot order. Use `{0}`, `{1}`, and later indexes when a locale needs a different order.
|
|
48
|
+
|
|
49
|
+
## Locale
|
|
50
|
+
|
|
51
|
+
The shared `translation` instance uses `navigator.language` when it is available, then falls back to `"en"`. You can also set the locale explicitly.
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
43
54
|
import translation, { l10n } from "intl-template"
|
|
44
55
|
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
const browserLocale = navigator.language
|
|
57
|
+
|
|
58
|
+
translation.locale = browserLocale
|
|
59
|
+
translation.templates[browserLocale] = {
|
|
60
|
+
"hello {}": "hola {}",
|
|
47
61
|
}
|
|
48
62
|
|
|
49
|
-
|
|
63
|
+
console.log(l10n`hello ${"Willow"}`.toString())
|
|
64
|
+
// => hola Willow
|
|
65
|
+
```
|
|
50
66
|
|
|
51
|
-
|
|
67
|
+
## Examples
|
|
52
68
|
|
|
53
|
-
|
|
54
|
-
// => hola willow
|
|
55
|
-
```
|
|
69
|
+
### React
|
|
56
70
|
|
|
57
|
-
|
|
71
|
+
The default `"react"` mode keeps interpolated values as values, so JSX can be used inside a translation.
|
|
58
72
|
|
|
59
|
-
```
|
|
73
|
+
```jsx
|
|
74
|
+
import translation, { l10n } from "intl-template"
|
|
60
75
|
|
|
61
|
-
|
|
76
|
+
translation.locale = "es-ES"
|
|
77
|
+
translation.templates["es-ES"] = {
|
|
78
|
+
"hello {}": "hola {}",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function Greeting({ name }) {
|
|
62
82
|
return (
|
|
63
|
-
<
|
|
64
|
-
{l10n`hello ${<
|
|
65
|
-
</
|
|
83
|
+
<p>
|
|
84
|
+
{l10n`hello ${<strong key="name">{name}</strong>}`}
|
|
85
|
+
</p>
|
|
66
86
|
)
|
|
67
87
|
}
|
|
68
88
|
```
|
|
69
89
|
|
|
70
|
-
###
|
|
90
|
+
### Slot Order
|
|
71
91
|
|
|
72
|
-
```
|
|
92
|
+
```javascript
|
|
93
|
+
translation.locale = "de-DE"
|
|
73
94
|
translation.templates["de-DE"] = {
|
|
74
|
-
"
|
|
95
|
+
"{} invited {}": "{1} wurde von {0} eingeladen",
|
|
75
96
|
}
|
|
76
97
|
|
|
77
|
-
const
|
|
98
|
+
const inviter = "Willow"
|
|
99
|
+
const guest = "Jack"
|
|
100
|
+
|
|
101
|
+
console.log(l10n`${inviter} invited ${guest}`.toString())
|
|
102
|
+
// => Jack wurde von Willow eingeladen
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Nested Translations
|
|
78
106
|
|
|
79
|
-
|
|
80
|
-
|
|
107
|
+
```javascript
|
|
108
|
+
translation.locale = "de-DE"
|
|
109
|
+
translation.templates["de-DE"] = {
|
|
110
|
+
"Bill": "Schmidt",
|
|
111
|
+
"hello {}": "hallo {}",
|
|
112
|
+
}
|
|
81
113
|
|
|
82
|
-
console.log(l10n`hello ${
|
|
83
|
-
// =>
|
|
114
|
+
console.log(l10n`hello ${l10n`Bill`}`.toString())
|
|
115
|
+
// => hallo Schmidt
|
|
84
116
|
```
|
|
85
117
|
|
|
86
|
-
###
|
|
118
|
+
### Function Slots
|
|
119
|
+
|
|
120
|
+
When a slot is a function, it receives the active locale.
|
|
87
121
|
|
|
88
122
|
```javascript
|
|
123
|
+
translation.locale = "de-DE"
|
|
89
124
|
translation.templates["de-DE"] = {
|
|
90
|
-
"
|
|
91
|
-
"hello {}": "hallo {1}"
|
|
125
|
+
"current locale: {}": "aktuelle Sprache: {}",
|
|
92
126
|
}
|
|
93
127
|
|
|
94
|
-
|
|
128
|
+
console.log(l10n`current locale: ${locale => locale}`.toString())
|
|
129
|
+
// => aktuelle Sprache: de-DE
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Call as a Function
|
|
133
|
+
|
|
134
|
+
Tagged templates and function calls use the same placeholder format.
|
|
95
135
|
|
|
96
|
-
|
|
136
|
+
```javascript
|
|
137
|
+
translation.locale = "es-ES"
|
|
138
|
+
translation.templates["es-ES"] = {
|
|
139
|
+
"hello {}": "hola {}",
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log(l10n("hello {}", "Willow").toString())
|
|
143
|
+
// => hola Willow
|
|
97
144
|
```
|
|
98
145
|
|
|
99
|
-
###
|
|
146
|
+
### String Mode
|
|
100
147
|
|
|
101
148
|
```javascript
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
149
|
+
import { Translation } from "intl-template"
|
|
150
|
+
|
|
151
|
+
const translation = new Translation("es-ES", "string")
|
|
152
|
+
|
|
153
|
+
translation.templates["es-ES"] = {
|
|
154
|
+
"hello {}": "hola {}",
|
|
105
155
|
}
|
|
106
156
|
|
|
107
|
-
const
|
|
157
|
+
const t = translation.translate
|
|
108
158
|
|
|
109
|
-
|
|
159
|
+
console.log(t`hello ${"Willow"}`)
|
|
160
|
+
// => hola Willow
|
|
110
161
|
```
|
|
111
162
|
|
|
112
|
-
|
|
163
|
+
## API
|
|
164
|
+
|
|
165
|
+
### `translation`
|
|
166
|
+
|
|
167
|
+
The default shared `Translation` instance.
|
|
168
|
+
|
|
169
|
+
### `l10n`
|
|
170
|
+
|
|
171
|
+
A shorthand for `translation.translate`.
|
|
172
|
+
|
|
173
|
+
### `new Translation(defaultLocale, mode)`
|
|
174
|
+
|
|
175
|
+
Creates an isolated translation instance.
|
|
176
|
+
|
|
177
|
+
- `defaultLocale`: locale used by this instance.
|
|
178
|
+
- `mode`: `"react"` by default, or `"string"` for plain string output.
|
|
179
|
+
|
|
180
|
+
### `translation.locale`
|
|
181
|
+
|
|
182
|
+
The active locale used by `translate`.
|
|
183
|
+
|
|
184
|
+
### `translation.templates`
|
|
185
|
+
|
|
186
|
+
Locale-indexed translation templates.
|
|
113
187
|
|
|
114
188
|
```javascript
|
|
115
|
-
|
|
189
|
+
translation.templates["es-ES"] = {
|
|
190
|
+
"source {}": "translated {}",
|
|
191
|
+
}
|
|
116
192
|
```
|
|
193
|
+
|
|
194
|
+
### `translation.translate(strings, ...parts)`
|
|
195
|
+
|
|
196
|
+
Translates a tagged template or a string with `{}` placeholders.
|
package/intl.cjs
CHANGED
|
@@ -1 +1,172 @@
|
|
|
1
|
-
var
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __moduleCache = /* @__PURE__ */ new WeakMap;
|
|
6
|
+
var __toCommonJS = (from) => {
|
|
7
|
+
var entry = __moduleCache.get(from), desc;
|
|
8
|
+
if (entry)
|
|
9
|
+
return entry;
|
|
10
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function")
|
|
12
|
+
__getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
|
|
13
|
+
get: () => from[key],
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
}));
|
|
16
|
+
__moduleCache.set(from, entry);
|
|
17
|
+
return entry;
|
|
18
|
+
};
|
|
19
|
+
var __export = (target, all) => {
|
|
20
|
+
for (var name in all)
|
|
21
|
+
__defProp(target, name, {
|
|
22
|
+
get: all[name],
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
set: (newValue) => all[name] = () => newValue
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// intl.js
|
|
30
|
+
var exports_intl = {};
|
|
31
|
+
__export(exports_intl, {
|
|
32
|
+
translation: () => translation,
|
|
33
|
+
parseTemplate: () => parseTemplate,
|
|
34
|
+
l10n: () => l10n,
|
|
35
|
+
default: () => intl_default,
|
|
36
|
+
Translation: () => Translation,
|
|
37
|
+
Runes: () => Runes
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(exports_intl);
|
|
40
|
+
|
|
41
|
+
class Runes extends Array {
|
|
42
|
+
toString() {
|
|
43
|
+
return this.join("");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
var SLOT_RE = /\{(\d*)\}/g;
|
|
47
|
+
var TEMPLATE_KEY_CACHE = new WeakMap;
|
|
48
|
+
function parseTemplate(templateString) {
|
|
49
|
+
const template = [];
|
|
50
|
+
const order = [];
|
|
51
|
+
let lastIndex = 0;
|
|
52
|
+
let match;
|
|
53
|
+
SLOT_RE.lastIndex = 0;
|
|
54
|
+
while ((match = SLOT_RE.exec(templateString)) !== null) {
|
|
55
|
+
template.push(templateString.slice(lastIndex, match.index));
|
|
56
|
+
order.push(match[1] === "" ? order.length : +match[1]);
|
|
57
|
+
lastIndex = SLOT_RE.lastIndex;
|
|
58
|
+
}
|
|
59
|
+
template.push(templateString.slice(lastIndex));
|
|
60
|
+
return { template, order };
|
|
61
|
+
}
|
|
62
|
+
function compileRegionTemplates(regionTemplates) {
|
|
63
|
+
const region = {};
|
|
64
|
+
for (const key in regionTemplates) {
|
|
65
|
+
region[key] = parseTemplate(regionTemplates[key]);
|
|
66
|
+
}
|
|
67
|
+
return region;
|
|
68
|
+
}
|
|
69
|
+
function getTemplateKey(strings) {
|
|
70
|
+
if (!strings.raw) {
|
|
71
|
+
return strings.join("{}");
|
|
72
|
+
}
|
|
73
|
+
let key = TEMPLATE_KEY_CACHE.get(strings);
|
|
74
|
+
if (key === undefined) {
|
|
75
|
+
key = strings.join("{}");
|
|
76
|
+
TEMPLATE_KEY_CACHE.set(strings, key);
|
|
77
|
+
}
|
|
78
|
+
return key;
|
|
79
|
+
}
|
|
80
|
+
function toTemplateString(value) {
|
|
81
|
+
return value == null ? "" : value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
class Translation {
|
|
85
|
+
mode = "react";
|
|
86
|
+
locale = "";
|
|
87
|
+
constructor(defaultLocale, mode = "react") {
|
|
88
|
+
this.mode = mode;
|
|
89
|
+
this.locale = defaultLocale || globalThis?.navigator?.language || "en";
|
|
90
|
+
}
|
|
91
|
+
#regions = {};
|
|
92
|
+
#regionProxies = {};
|
|
93
|
+
#templates = new Proxy(this.#regions, {
|
|
94
|
+
get: (regions, locale) => {
|
|
95
|
+
const region = regions[locale];
|
|
96
|
+
if (!region) {
|
|
97
|
+
return new Proxy({}, REGION_HANDLER);
|
|
98
|
+
}
|
|
99
|
+
let proxy = this.#regionProxies[locale];
|
|
100
|
+
if (!proxy) {
|
|
101
|
+
proxy = new Proxy(region, REGION_HANDLER);
|
|
102
|
+
this.#regionProxies[locale] = proxy;
|
|
103
|
+
}
|
|
104
|
+
return proxy;
|
|
105
|
+
},
|
|
106
|
+
set: (regions, locale, regionTemplates) => {
|
|
107
|
+
const region = compileRegionTemplates(regionTemplates);
|
|
108
|
+
regions[locale] = region;
|
|
109
|
+
this.#regionProxies[locale] = new Proxy(region, REGION_HANDLER);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
get templates() {
|
|
114
|
+
return this.#templates;
|
|
115
|
+
}
|
|
116
|
+
set templates(value) {
|
|
117
|
+
for (const locale in value) {
|
|
118
|
+
this.#templates[locale] = value[locale];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
translate = (strings, ...parts) => {
|
|
122
|
+
const locale = this.locale;
|
|
123
|
+
const isStringInput = typeof strings === "string";
|
|
124
|
+
const key = isStringInput ? strings : getTemplateKey(strings);
|
|
125
|
+
const compiled = this.#regions[locale]?.[key];
|
|
126
|
+
let template;
|
|
127
|
+
let order;
|
|
128
|
+
if (compiled) {
|
|
129
|
+
template = compiled.template;
|
|
130
|
+
order = compiled.order;
|
|
131
|
+
} else {
|
|
132
|
+
template = isStringInput ? strings.split("{}") : strings.slice();
|
|
133
|
+
}
|
|
134
|
+
if (parts.length !== template.length - 1) {
|
|
135
|
+
throw new Error(`translate template parts length does not match. locale: ${locale}, key: ${key}`);
|
|
136
|
+
}
|
|
137
|
+
const len = template.length;
|
|
138
|
+
if (this.mode !== "react") {
|
|
139
|
+
let result = "";
|
|
140
|
+
for (let idx = 0;idx < len; idx++) {
|
|
141
|
+
result += template[idx];
|
|
142
|
+
if (idx < parts.length) {
|
|
143
|
+
const part = parts[order ? order[idx] : idx];
|
|
144
|
+
result += toTemplateString(typeof part === "function" ? part(locale) : part);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
const runes = new Runes(len + parts.length);
|
|
150
|
+
let runeIdx = 0;
|
|
151
|
+
for (let idx = 0;idx < len; idx++) {
|
|
152
|
+
runes[runeIdx++] = template[idx];
|
|
153
|
+
if (idx < parts.length) {
|
|
154
|
+
const part = parts[order ? order[idx] : idx];
|
|
155
|
+
runes[runeIdx++] = typeof part === "function" ? part(locale) : part;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return runes;
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
var REGION_HANDLER = {
|
|
162
|
+
set(region, key, value) {
|
|
163
|
+
if (typeof value !== "string") {
|
|
164
|
+
throw new Error("Template must be a string.");
|
|
165
|
+
}
|
|
166
|
+
region[key] = parseTemplate(value);
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
var translation = new Translation;
|
|
171
|
+
var intl_default = translation;
|
|
172
|
+
var l10n = translation.translate;
|
package/intl.d.ts
CHANGED
|
@@ -16,7 +16,11 @@ export class Runes extends Array<any> {
|
|
|
16
16
|
* Represents a Translation object that handles string translation based on locale and templates.
|
|
17
17
|
*/
|
|
18
18
|
export class Translation {
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} defaultLocale
|
|
21
|
+
* @param {"string" | "react"} mode
|
|
22
|
+
*/
|
|
23
|
+
constructor(defaultLocale: string, mode?: "string" | "react");
|
|
20
24
|
/** @type {"string" | "react"} */
|
|
21
25
|
mode: "string" | "react";
|
|
22
26
|
/**
|
|
@@ -24,20 +28,19 @@ export class Translation {
|
|
|
24
28
|
* @type {string}
|
|
25
29
|
**/
|
|
26
30
|
locale: string;
|
|
27
|
-
set templates(value:
|
|
28
|
-
get templates():
|
|
31
|
+
set templates(value: any);
|
|
32
|
+
get templates(): any;
|
|
29
33
|
/**
|
|
30
34
|
* Translates a string based on the provided locale and strings.
|
|
31
35
|
*
|
|
32
|
-
* @param {string} locale - The locale to use for translation.
|
|
33
36
|
* @param {TemplateStringsArray | string} strings - The string or array of strings to be translated.
|
|
34
37
|
* @param {...any} parts - The dynamic parts to be inserted into the translated string.
|
|
35
|
-
* @returns {Runes} - The translated string with dynamic parts inserted.
|
|
38
|
+
* @returns {Runes | string} - The translated string with dynamic parts inserted.
|
|
36
39
|
* @throws {Error} - If the length of the template parts does not match the length of the template.
|
|
37
40
|
*/
|
|
38
|
-
translate: (
|
|
41
|
+
translate: (strings: TemplateStringsArray | string, ...parts: any[]) => Runes | string;
|
|
39
42
|
#private;
|
|
40
43
|
}
|
|
44
|
+
export const translation: Translation;
|
|
41
45
|
export default translation;
|
|
42
|
-
export function l10n(strings:
|
|
43
|
-
declare const translation: Translation;
|
|
46
|
+
export function l10n(strings: TemplateStringsArray | string, ...parts: any[]): Runes | string;
|
package/intl.js
CHANGED
|
@@ -4,31 +4,59 @@ export class Runes extends Array {
|
|
|
4
4
|
}
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
const SLOT_RE = /\{(\d*)\}/g
|
|
8
|
+
const TEMPLATE_KEY_CACHE = new WeakMap()
|
|
9
|
+
|
|
7
10
|
/**
|
|
8
11
|
* Parses a template string and extracts the template parts and order of slots.
|
|
9
12
|
* @param {string} templateString - The template string to parse.
|
|
10
13
|
* @returns {{ template: string[], order: number[] }}
|
|
11
14
|
*/
|
|
12
15
|
export function parseTemplate(templateString) {
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
})
|
|
16
|
+
const template = []
|
|
17
|
+
const order = []
|
|
18
|
+
let lastIndex = 0
|
|
19
|
+
let match
|
|
20
|
+
|
|
21
|
+
SLOT_RE.lastIndex = 0
|
|
22
|
+
while ((match = SLOT_RE.exec(templateString)) !== null) {
|
|
23
|
+
template.push(templateString.slice(lastIndex, match.index))
|
|
24
|
+
// `{}` keeps original auto-numbering: index = current order length
|
|
25
|
+
order.push(match[1] === "" ? order.length : +match[1])
|
|
26
|
+
lastIndex = SLOT_RE.lastIndex
|
|
27
|
+
}
|
|
28
|
+
template.push(templateString.slice(lastIndex))
|
|
28
29
|
|
|
29
30
|
return { template, order }
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
function compileRegionTemplates(regionTemplates) {
|
|
34
|
+
const region = {}
|
|
35
|
+
for (const key in regionTemplates) {
|
|
36
|
+
region[key] = parseTemplate(regionTemplates[key])
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return region
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getTemplateKey(strings) {
|
|
43
|
+
if (!strings.raw) {
|
|
44
|
+
return strings.join("{}")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let key = TEMPLATE_KEY_CACHE.get(strings)
|
|
48
|
+
if (key === undefined) {
|
|
49
|
+
key = strings.join("{}")
|
|
50
|
+
TEMPLATE_KEY_CACHE.set(strings, key)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return key
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toTemplateString(value) {
|
|
57
|
+
return value == null ? "" : value
|
|
58
|
+
}
|
|
59
|
+
|
|
32
60
|
/**
|
|
33
61
|
* Represents a Translation object that handles string translation based on locale and templates.
|
|
34
62
|
*/
|
|
@@ -42,6 +70,10 @@ export class Translation {
|
|
|
42
70
|
**/
|
|
43
71
|
locale = ""
|
|
44
72
|
|
|
73
|
+
/**
|
|
74
|
+
* @param {string} defaultLocale
|
|
75
|
+
* @param {"string" | "react"} mode
|
|
76
|
+
*/
|
|
45
77
|
constructor(defaultLocale, mode = "react") {
|
|
46
78
|
this.mode = mode
|
|
47
79
|
this.locale = defaultLocale || globalThis?.navigator?.language || "en"
|
|
@@ -51,26 +83,29 @@ export class Translation {
|
|
|
51
83
|
* Templates object that stores the translation templates for each locale.
|
|
52
84
|
* @type {Proxy}
|
|
53
85
|
*/
|
|
54
|
-
#
|
|
55
|
-
get(templates, locale) {
|
|
56
|
-
return new Proxy(templates[locale] || {}, {
|
|
57
|
-
set(region, key, value) {
|
|
58
|
-
if (typeof value !== "string") {
|
|
59
|
-
throw new Error("Template must be a string.")
|
|
60
|
-
}
|
|
86
|
+
#regions = {}
|
|
61
87
|
|
|
62
|
-
|
|
88
|
+
#regionProxies = {}
|
|
63
89
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
90
|
+
#templates = new Proxy(this.#regions, {
|
|
91
|
+
get: (regions, locale) => {
|
|
92
|
+
const region = regions[locale]
|
|
93
|
+
if (!region) {
|
|
94
|
+
return new Proxy({}, REGION_HANDLER)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let proxy = this.#regionProxies[locale]
|
|
98
|
+
if (!proxy) {
|
|
99
|
+
proxy = new Proxy(region, REGION_HANDLER)
|
|
100
|
+
this.#regionProxies[locale] = proxy
|
|
101
|
+
}
|
|
71
102
|
|
|
72
|
-
|
|
73
|
-
|
|
103
|
+
return proxy
|
|
104
|
+
},
|
|
105
|
+
set: (regions, locale, regionTemplates) => {
|
|
106
|
+
const region = compileRegionTemplates(regionTemplates)
|
|
107
|
+
regions[locale] = region
|
|
108
|
+
this.#regionProxies[locale] = new Proxy(region, REGION_HANDLER)
|
|
74
109
|
|
|
75
110
|
return true
|
|
76
111
|
}
|
|
@@ -81,66 +116,78 @@ export class Translation {
|
|
|
81
116
|
}
|
|
82
117
|
|
|
83
118
|
set templates(value) {
|
|
84
|
-
|
|
85
|
-
this.#templates[locale] =
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return true
|
|
119
|
+
for (const locale in value) {
|
|
120
|
+
this.#templates[locale] = value[locale]
|
|
121
|
+
}
|
|
89
122
|
}
|
|
90
123
|
|
|
91
124
|
/**
|
|
92
125
|
* Translates a string based on the provided locale and strings.
|
|
93
126
|
*
|
|
94
|
-
* @param {string} locale - The locale to use for translation.
|
|
95
127
|
* @param {TemplateStringsArray | string} strings - The string or array of strings to be translated.
|
|
96
128
|
* @param {...any} parts - The dynamic parts to be inserted into the translated string.
|
|
97
|
-
* @returns {Runes} - The translated string with dynamic parts inserted.
|
|
129
|
+
* @returns {Runes | string} - The translated string with dynamic parts inserted.
|
|
98
130
|
* @throws {Error} - If the length of the template parts does not match the length of the template.
|
|
99
131
|
*/
|
|
100
|
-
translate = (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
let
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
132
|
+
translate = (strings, ...parts) => {
|
|
133
|
+
const locale = this.locale
|
|
134
|
+
const isStringInput = typeof strings === "string"
|
|
135
|
+
const key = isStringInput ? strings : getTemplateKey(strings)
|
|
136
|
+
|
|
137
|
+
const compiled = this.#regions[locale]?.[key]
|
|
138
|
+
let template
|
|
139
|
+
let order
|
|
140
|
+
if (compiled) {
|
|
141
|
+
template = compiled.template
|
|
142
|
+
order = compiled.order
|
|
143
|
+
} else {
|
|
144
|
+
template = isStringInput ? strings.split("{}") : strings.slice()
|
|
112
145
|
}
|
|
113
146
|
|
|
114
147
|
if (parts.length !== template.length - 1) {
|
|
115
148
|
throw new Error(`translate template parts length does not match. locale: ${locale}, key: ${key}`)
|
|
116
149
|
}
|
|
117
150
|
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
} else {
|
|
127
|
-
runes.push(part)
|
|
151
|
+
const len = template.length
|
|
152
|
+
if (this.mode !== "react") {
|
|
153
|
+
let result = ""
|
|
154
|
+
for (let idx = 0; idx < len; idx++) {
|
|
155
|
+
result += template[idx]
|
|
156
|
+
if (idx < parts.length) {
|
|
157
|
+
const part = parts[order ? order[idx] : idx]
|
|
158
|
+
result += toTemplateString(typeof part === "function" ? part(locale) : part)
|
|
128
159
|
}
|
|
129
160
|
}
|
|
130
161
|
|
|
131
|
-
return
|
|
132
|
-
}
|
|
162
|
+
return result
|
|
163
|
+
}
|
|
133
164
|
|
|
134
|
-
|
|
135
|
-
|
|
165
|
+
const runes = new Runes(len + parts.length)
|
|
166
|
+
let runeIdx = 0
|
|
167
|
+
for (let idx = 0; idx < len; idx++) {
|
|
168
|
+
runes[runeIdx++] = template[idx]
|
|
169
|
+
if (idx < parts.length) {
|
|
170
|
+
const part = parts[order ? order[idx] : idx]
|
|
171
|
+
runes[runeIdx++] = typeof part === "function" ? part(locale) : part
|
|
172
|
+
}
|
|
136
173
|
}
|
|
137
174
|
|
|
138
175
|
return runes
|
|
139
176
|
}
|
|
140
177
|
}
|
|
141
178
|
|
|
142
|
-
const
|
|
179
|
+
const REGION_HANDLER = {
|
|
180
|
+
set(region, key, value) {
|
|
181
|
+
if (typeof value !== "string") {
|
|
182
|
+
throw new Error("Template must be a string.")
|
|
183
|
+
}
|
|
184
|
+
region[key] = parseTemplate(value)
|
|
185
|
+
return true
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const translation = new Translation()
|
|
143
190
|
|
|
144
191
|
export default translation
|
|
145
192
|
|
|
146
|
-
export const l10n =
|
|
193
|
+
export const l10n = translation.translate
|
package/package.json
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "intl-template",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "l10n tagged template literals",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "intl.cjs",
|
|
7
|
-
"module": "intl.js",
|
|
6
|
+
"main": "./intl.cjs",
|
|
7
|
+
"module": "./intl.js",
|
|
8
|
+
"types": "./intl.d.ts",
|
|
8
9
|
"repository": "github:performonkey/intl-template",
|
|
10
|
+
"files": [
|
|
11
|
+
"README.md",
|
|
12
|
+
"intl.js",
|
|
13
|
+
"intl.cjs",
|
|
14
|
+
"intl.d.ts"
|
|
15
|
+
],
|
|
9
16
|
"scripts": {
|
|
10
|
-
"build": "bun build --outfile intl.cjs --
|
|
11
|
-
"
|
|
12
|
-
"
|
|
17
|
+
"build": "bun build --outfile intl.cjs --format cjs intl.js",
|
|
18
|
+
"perf": "node --test intl.perf.test.js",
|
|
19
|
+
"postbuild": "rm *.d.ts && tsc --emitDeclarationOnly --allowJs -d --outDir . intl.js intl.test.js"
|
|
13
20
|
},
|
|
14
21
|
"keywords": [
|
|
15
22
|
"i18n",
|
package/intl.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/intl.test.js
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert"
|
|
2
|
-
import test from "node:test"
|
|
3
|
-
import { Translation, Runes } from "./intl.js"
|
|
4
|
-
|
|
5
|
-
test("template", async (t) => {
|
|
6
|
-
await t.test("multiple locale template parse", () => {
|
|
7
|
-
const translation = new Translation()
|
|
8
|
-
translation.templates = {
|
|
9
|
-
"es-ES": {
|
|
10
|
-
"{}matched{}": "{}{}uno",
|
|
11
|
-
},
|
|
12
|
-
"zh-CN": {
|
|
13
|
-
"{}matched{}": "{}{}丁戊卯",
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
assert.deepEqual(
|
|
18
|
-
translation.templates["zh-CN"],
|
|
19
|
-
{
|
|
20
|
-
"{}matched{}": {
|
|
21
|
-
template: ["", "", "丁戊卯"],
|
|
22
|
-
order: [0, 1]
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
)
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
await t.test("template slot parse", () => {
|
|
29
|
-
const translation = new Translation()
|
|
30
|
-
const locale = "zh-CN"
|
|
31
|
-
translation.templates[locale] = {
|
|
32
|
-
"{}matched{}": "{}{}丁戊卯",
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
Object.assign(translation.templates[locale], {
|
|
36
|
-
"assign{}": "123{}"
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
assert.deepEqual(
|
|
40
|
-
translation.templates[locale],
|
|
41
|
-
{
|
|
42
|
-
"{}matched{}": {
|
|
43
|
-
template: ["", "", "丁戊卯"],
|
|
44
|
-
order: [0, 1]
|
|
45
|
-
},
|
|
46
|
-
"assign{}": {
|
|
47
|
-
template: ["123", ""],
|
|
48
|
-
order: [0]
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
await t.test("template slot order parse", () => {
|
|
55
|
-
const translation = new Translation()
|
|
56
|
-
const locale = "zh-CN"
|
|
57
|
-
const key = "{}matched{}"
|
|
58
|
-
translation.templates[locale] = {
|
|
59
|
-
[key]: "{1}{0}丁戊卯",
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
assert.deepEqual(
|
|
63
|
-
translation.templates[locale][key],
|
|
64
|
-
{
|
|
65
|
-
template: ["", "", "丁戊卯"],
|
|
66
|
-
order: [1, 0]
|
|
67
|
-
}
|
|
68
|
-
)
|
|
69
|
-
})
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
test("translation.translate", async (t) => {
|
|
73
|
-
await t.test("apply template", () => {
|
|
74
|
-
const translation = new Translation()
|
|
75
|
-
translation.templates["zh-CN"] = {
|
|
76
|
-
"abc {} def {}": "甲乙丙 {} {} 丁戊卯",
|
|
77
|
-
}
|
|
78
|
-
const l10n = translation.translate.bind(null, "zh-CN")
|
|
79
|
-
assert.deepEqual(l10n`abc ${123} def ${345}`, ["甲乙丙 ", 123, " ", 345, " 丁戊卯"])
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
await t.test("change slot order", () => {
|
|
83
|
-
const translation = new Translation()
|
|
84
|
-
translation.templates["zh-CN"] = {
|
|
85
|
-
"abc {} def {}": "甲乙丙 {1} {0} 丁戊卯",
|
|
86
|
-
}
|
|
87
|
-
const l10n = translation.translate.bind(null, "zh-CN")
|
|
88
|
-
assert.equal(l10n`abc ${123} def ${345}`.toString(), "甲乙丙 345 123 丁戊卯")
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
await t.test("slot function", () => {
|
|
92
|
-
const translation = new Translation()
|
|
93
|
-
translation.templates["zh-CN"] = {
|
|
94
|
-
"{} def {}": "{1} {0} 丁戊卯",
|
|
95
|
-
}
|
|
96
|
-
const l10n = translation.translate.bind(null, "zh-CN")
|
|
97
|
-
assert.equal(l10n`${locale => locale} def ${345}`.toString(), "345 zh-CN 丁戊卯")
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
await t.test("slot count match", () => {
|
|
101
|
-
const translation = new Translation()
|
|
102
|
-
translation.templates["zh-CN"] = {
|
|
103
|
-
"{} def {}": "{} 丁戊卯",
|
|
104
|
-
"{}matched{}": "{}{}丁戊卯",
|
|
105
|
-
}
|
|
106
|
-
const l10n = translation.translate.bind(null, "zh-CN")
|
|
107
|
-
try {
|
|
108
|
-
l10n`${locale => locale} def ${345}`
|
|
109
|
-
} catch (err) {
|
|
110
|
-
assert.equal(
|
|
111
|
-
err.message,
|
|
112
|
-
`translate template parts length does not match. locale: zh-CN, key: {} def {}`
|
|
113
|
-
)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
assert.equal(l10n`${locale => locale}matched${345}`.toString(), `zh-CN345丁戊卯`)
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
await t.test("call as function", () => {
|
|
120
|
-
const translation = new Translation()
|
|
121
|
-
translation.templates["zh-CN"] = {}
|
|
122
|
-
const l10n = translation.translate.bind(null, "zh-CN")
|
|
123
|
-
assert.equal(l10n("{} def {} {}", 1, "a", "b").toString(), '1 def a b')
|
|
124
|
-
})
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
test("Runes", async (t) => {
|
|
128
|
-
assert.equal(
|
|
129
|
-
new Runes('a', ' b ', 1, 2).toString(),
|
|
130
|
-
'a b 12'
|
|
131
|
-
)
|
|
132
|
-
})
|