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 CHANGED
@@ -1,19 +1,8 @@
1
- # intel-template
1
+ # intl-template
2
2
 
3
- A tiny i18n/l10n TTL(Tagged Template Literals) function
3
+ A tiny i18n/l10n helper built on JavaScript Tagged Template Literals.
4
4
 
5
- ## Table of Contents
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
- ## Usage
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 l10n = translation.translate.bind(null, "es-ES")
23
+ const name = "Willow"
34
24
 
35
- const name = "willow";
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
- ### Use browser specifies locale
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
- translation.templates["es-ES"] = {
46
- "hello {}": "hola {}"
56
+ const browserLocale = navigator.language
57
+
58
+ translation.locale = browserLocale
59
+ translation.templates[browserLocale] = {
60
+ "hello {}": "hola {}",
47
61
  }
48
62
 
49
- // l10n = translation.translate.bind(null, navigator,language)
63
+ console.log(l10n`hello ${"Willow"}`.toString())
64
+ // => hola Willow
65
+ ```
50
66
 
51
- const name = "willow";
67
+ ## Examples
52
68
 
53
- console.log(l10n`hello ${name}`)
54
- // => hola willow
55
- ```
69
+ ### React
56
70
 
57
- ### Use with React
71
+ The default `"react"` mode keeps interpolated values as values, so JSX can be used inside a translation.
58
72
 
59
- ```javascript
73
+ ```jsx
74
+ import translation, { l10n } from "intl-template"
60
75
 
61
- function SomeComponent({ name }) {
76
+ translation.locale = "es-ES"
77
+ translation.templates["es-ES"] = {
78
+ "hello {}": "hola {}",
79
+ }
80
+
81
+ function Greeting({ name }) {
62
82
  return (
63
- <div>
64
- {l10n`hello ${<b>{name}</b>}`}
65
- </div>
83
+ <p>
84
+ {l10n`hello ${<strong key="name">{name}</strong>}`}
85
+ </p>
66
86
  )
67
87
  }
68
88
  ```
69
89
 
70
- ### Specify slot order
90
+ ### Slot Order
71
91
 
72
- ```
92
+ ```javascript
93
+ translation.locale = "de-DE"
73
94
  translation.templates["de-DE"] = {
74
- "hello {} and {}": "hallo {1} und {0}"
95
+ "{} invited {}": "{1} wurde von {0} eingeladen",
75
96
  }
76
97
 
77
- const l10n = translation.translate.bind(null, "de-DE")
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
- const name1 = "willow"
80
- const name2 = "jack"
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 ${name1} and ${name2}`)
83
- // => holla jack und willow
114
+ console.log(l10n`hello ${l10n`Bill`}`.toString())
115
+ // => hallo Schmidt
84
116
  ```
85
117
 
86
- ### Nested
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
- "bill": "schmidt",
91
- "hello {}": "hallo {1}"
125
+ "current locale: {}": "aktuelle Sprache: {}",
92
126
  }
93
127
 
94
- const l10n = translation.translate.bind(null, "de-DE")
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
- l10n`hello ${l10n`bill`}` // => hallo schmidt
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
- ### Function slot
146
+ ### String Mode
100
147
 
101
148
  ```javascript
102
- translation.templates["de-DE"] = {
103
- "bill": "schmidt",
104
- "hello {}": "hallo {1}"
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 l10n = translation.translate.bind(null, "de-DE")
157
+ const t = translation.translate
108
158
 
109
- l10n`hello ${(locale) => 123}` // => hallo 123
159
+ console.log(t`hello ${"Willow"}`)
160
+ // => hola Willow
110
161
  ```
111
162
 
112
- ### Call as function
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
- l10n("hello {}", name)
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{defineProperty:J,getOwnPropertyNames:X,getOwnPropertyDescriptor:Y}=Object,Z=Object.prototype.hasOwnProperty;var V=new WeakMap,_=(q)=>{var j=V.get(q),z;if(j)return j;if(j=J({},"__esModule",{value:!0}),q&&typeof q==="object"||typeof q==="function")X(q).map((B)=>!Z.call(j,B)&&J(j,B,{get:()=>q[B],enumerable:!(z=Y(q,B))||z.enumerable}));return V.set(q,j),j};var $=(q,j)=>{for(var z in j)J(q,z,{get:j[z],enumerable:!0,configurable:!0,set:(B)=>j[z]=()=>B})};var E={};$(E,{parseTemplate:()=>K,l10n:()=>A,default:()=>w,Translation:()=>O,Runes:()=>N});module.exports=_(E);class N extends Array{toString(){return this.join("")}}function K(q){let j=[],z=[];return q.split(/({\d*})/).forEach((C)=>{if(C.match(/^{\d*}$/))if(C==="{}")j.push(j.length);else j.push(parseInt(C.slice(1,-1),10));else z.push(C)}),{template:z,order:j}}class O{mode="react";locale="";constructor(q,j="react"){this.mode=j,this.locale=q||globalThis?.navigator?.language||"en"}#j=new Proxy({},{get(q,j){return new Proxy(q[j]||{},{set(z,B,C){if(typeof C!=="string")throw Error("Template must be a string.");return z[B]=K(C),!0}})},set(q,j,z){return q[j]=Object.entries(z).reduce((B,[C,D])=>{return B[C]=K(D),B},{}),!0}});get templates(){return this.#j}set templates(q){return Object.entries(q).forEach(([j,z])=>{this.#j[j]=z}),!0}translate=(q,j,...z)=>{if(typeof j==="string")j=j.split("{}");let B=j.join("{}"),C=this.#j?.[q],{template:D,order:P}=C?.[B]||{};if(!D)console.debug(`[intl-template]not match translate key, ${B}`,{translation:C,locale:q,strings:j,parts:z}),D=j.slice(),P=z.map((F,G)=>G);if(z.length!==D.length-1)throw Error(`translate template parts length does not match. locale: ${q}, key: ${B}`);let Q=D.reduce((F,G,W)=>{F.push(G);let U=P[W];if(U>=0){let H=z[U];if(typeof H==="function")F.push(H(q));else F.push(H)}return F},new N);if(this.mode!=="react")return Q.toString();return Q}}var M=new O,w=M,A=(q,...j)=>M.translate(M.locale,q,...j);
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
- constructor(defaultLocale: any, mode?: string);
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: Proxy);
28
- get templates(): Proxy;
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: (locale: string, strings: TemplateStringsArray | string, ...parts: any[]) => Runes;
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: any, ...parts: any[]): Runes;
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 order = [];
14
- const template = [];
15
- const parts = templateString.split(/({\d*})/)
16
- parts.forEach((part) => {
17
- if (part.match(/^{\d*}$/)) {
18
- if (part === '{}') {
19
- order.push(order.length)
20
- } else {
21
- // slot with order, e.g. `{2}abc{1}def{0}`
22
- order.push(parseInt(part.slice(1, -1), 10))
23
- }
24
- } else {
25
- template.push(part)
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
- #templates = new Proxy({}, {
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
- region[key] = parseTemplate(value)
88
+ #regionProxies = {}
63
89
 
64
- return true
65
- }
66
- })
67
- },
68
- set(templates, locale, regionTemplates) {
69
- templates[locale] = Object.entries(regionTemplates).reduce((region, [key, value]) => {
70
- region[key] = parseTemplate(value)
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
- return region
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
- Object.entries(value).forEach(([locale, regionTemplates]) => {
85
- this.#templates[locale] = regionTemplates
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 = (locale, strings, ...parts) => {
101
- if (typeof strings === "string") {
102
- strings = strings.split("{}")
103
- }
104
-
105
- const key = strings.join("{}")
106
- const translation = this.#templates?.[locale]
107
- let { template, order } = translation?.[key] || {}
108
- if (!template) {
109
- console.debug(`[intl-template]not match translate key, ${key}`, { translation, locale, strings, parts })
110
- template = strings.slice()
111
- order = parts.map((_, i) => i)
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 runes = template.reduce((runes, template, idx) => {
119
- runes.push(template)
120
-
121
- const orderIdx = order[idx]
122
- if (orderIdx >= 0) {
123
- const part = parts[orderIdx]
124
- if (typeof part === "function") {
125
- runes.push(part(locale))
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 runes
132
- }, new Runes())
162
+ return result
163
+ }
133
164
 
134
- if (this.mode !== "react") {
135
- return runes.toString()
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 translation = new Translation()
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 = (strings, ...parts) => translation.translate(translation.locale, strings, ...parts);
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.5",
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 --production --format cjs intl.js",
11
- "postbuild": "tsc --emitDeclarationOnly --allowJs -d --outDir . *.js",
12
- "postpublish": "rm -rf *.d.ts *.cjs"
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
- })