@itrocks/translate 0.2.2 → 0.2.3
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 +33 -1
- package/cjs/translate.d.ts +10 -5
- package/cjs/translate.js +68 -21
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -29,6 +29,7 @@ You typically use it to:
|
|
|
29
29
|
|
|
30
30
|
- declare the current UI language with `trInit()`,
|
|
31
31
|
- load translation keys from a CSV file with `trLoad()`,
|
|
32
|
+
- isolate concurrent request languages with `trWithLanguage()`,
|
|
32
33
|
- translate strings at runtime with `tr()`,
|
|
33
34
|
- optionally inspect or extend the in-memory `translations` map.
|
|
34
35
|
|
|
@@ -103,6 +104,24 @@ async function initTranslations() {
|
|
|
103
104
|
initTranslations().catch(console.error)
|
|
104
105
|
```
|
|
105
106
|
|
|
107
|
+
### Concurrent request languages
|
|
108
|
+
|
|
109
|
+
Load each catalog once, then run request work in its own asynchronous language context.
|
|
110
|
+
The context is preserved through promises without changing other concurrent requests:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
trInit('en-US')
|
|
114
|
+
await trLoad('locales/fr-FR.csv', { language: 'fr-FR' })
|
|
115
|
+
|
|
116
|
+
await trWithLanguage('fr-FR', async () => {
|
|
117
|
+
console.log(tr('Hello')) // Bonjour
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`trLoad(file, { language, reverse: true })` loads the second CSV column as the
|
|
122
|
+
source and the first as the target. This is useful while migrating templates that
|
|
123
|
+
temporarily contain literals in both the source and target languages.
|
|
124
|
+
|
|
106
125
|
> **Note**
|
|
107
126
|
> This package is intentionally minimal: it does not manage locales, fallbacks,
|
|
108
127
|
> or pluralization rules on its own. Those concerns are expected to be handled
|
|
@@ -240,7 +259,7 @@ active language and want to reload translation data.
|
|
|
240
259
|
### `trLoad()`
|
|
241
260
|
|
|
242
261
|
```ts
|
|
243
|
-
async function trLoad(file: string): Promise<void | unknown>
|
|
262
|
+
async function trLoad(file: string, options?: LoadOptions): Promise<void | unknown>
|
|
244
263
|
```
|
|
245
264
|
|
|
246
265
|
Loads translations from a **semicolon‑separated CSV file** at the given path.
|
|
@@ -253,6 +272,9 @@ Behavior:
|
|
|
253
272
|
delimiter.
|
|
254
273
|
- Each row is expected to have at least two columns: `row[0]` is the source
|
|
255
274
|
string, `row[1]` is the translated string. Extra columns are ignored.
|
|
275
|
+
- `options.language` selects the catalog to populate; it defaults to the language
|
|
276
|
+
selected by `trInit()`.
|
|
277
|
+
- `options.reverse` swaps the source and translated columns while loading.
|
|
256
278
|
- For each row, the pair is stored in `translations`.
|
|
257
279
|
- If `row[0]` contains a placeholder like `$1`, an expression `RegExp` is
|
|
258
280
|
created and added to `expressions` to support dynamic matching in `tr()`.
|
|
@@ -265,6 +287,16 @@ hello;Hello
|
|
|
265
287
|
"You have $1 new messages";"You have $1 new messages"
|
|
266
288
|
```
|
|
267
289
|
|
|
290
|
+
### `trWithLanguage()`
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
function trWithLanguage<T>(language: string, callback: () => T): T
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Runs `callback` in an asynchronous language context. `lang()` and `tr()` use that
|
|
297
|
+
language for the callback and all asynchronous work it starts. Concurrent contexts
|
|
298
|
+
remain isolated.
|
|
299
|
+
|
|
268
300
|
## Typical use cases
|
|
269
301
|
|
|
270
302
|
Here are some scenarios where `@itrocks/translate` is a good fit:
|
package/cjs/translate.d.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
export type LoadOptions = {
|
|
2
|
+
language?: string;
|
|
3
|
+
reverse?: boolean;
|
|
4
|
+
};
|
|
5
|
+
export type Options = {
|
|
6
|
+
ucFirst?: boolean;
|
|
7
|
+
};
|
|
1
8
|
export declare const DefaultOptions: Options;
|
|
2
9
|
export declare const expressions: Set<RegExp>;
|
|
3
10
|
export declare const translations: Map<string, string>;
|
|
4
11
|
export declare function lang(): string;
|
|
5
|
-
export type Options = {
|
|
6
|
-
ucFirst?: boolean;
|
|
7
|
-
};
|
|
8
12
|
export declare function tr(text: string, options: Options): string;
|
|
9
13
|
export declare function tr(text: string, parts?: string[], options?: Options): string;
|
|
10
|
-
export declare function trInit(
|
|
11
|
-
export declare function trLoad(file: string): Promise<void>;
|
|
14
|
+
export declare function trInit(language: string): void;
|
|
15
|
+
export declare function trLoad(file: string, options?: LoadOptions): Promise<void>;
|
|
16
|
+
export declare function trWithLanguage<T>(language: string, callback: () => T): T;
|
package/cjs/translate.js
CHANGED
|
@@ -5,6 +5,8 @@ exports.lang = lang;
|
|
|
5
5
|
exports.tr = tr;
|
|
6
6
|
exports.trInit = trInit;
|
|
7
7
|
exports.trLoad = trLoad;
|
|
8
|
+
exports.trWithLanguage = trWithLanguage;
|
|
9
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
8
10
|
const promises_1 = require("node:fs/promises");
|
|
9
11
|
const promises_2 = require("node:fs/promises");
|
|
10
12
|
const parseCsv = require('papaparse').parse;
|
|
@@ -13,9 +15,21 @@ exports.DefaultOptions = {
|
|
|
13
15
|
};
|
|
14
16
|
exports.expressions = new Set;
|
|
15
17
|
exports.translations = new Map;
|
|
16
|
-
|
|
18
|
+
const catalogs = new Map;
|
|
19
|
+
const expressionData = new WeakMap;
|
|
20
|
+
const languageScope = new node_async_hooks_1.AsyncLocalStorage();
|
|
21
|
+
let defaultLanguage = 'en-US';
|
|
22
|
+
catalogs.set(defaultLanguage, { expressions: exports.expressions, translations: exports.translations });
|
|
23
|
+
function catalog(language = lang()) {
|
|
24
|
+
let value = catalogs.get(language);
|
|
25
|
+
if (!value) {
|
|
26
|
+
value = { expressions: new Set, translations: new Map };
|
|
27
|
+
catalogs.set(language, value);
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
17
31
|
function lang() {
|
|
18
|
-
return
|
|
32
|
+
return languageScope.getStore() ?? defaultLanguage;
|
|
19
33
|
}
|
|
20
34
|
function tr(text, parts, options) {
|
|
21
35
|
if (!Array.isArray(parts)) {
|
|
@@ -27,10 +41,11 @@ function tr(text, parts, options) {
|
|
|
27
41
|
let partsCount = parts.length;
|
|
28
42
|
const firstChar = text[0];
|
|
29
43
|
const ucFirst = (options?.ucFirst ?? exports.DefaultOptions.ucFirst) && (firstChar >= 'A') && (firstChar <= 'Z');
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
??
|
|
33
|
-
??
|
|
44
|
+
const active = catalog();
|
|
45
|
+
let translated = active.translations.get(text)
|
|
46
|
+
?? (ucFirst ? active.translations.get(firstChar.toLocaleLowerCase() + text.slice(1)) : undefined)
|
|
47
|
+
?? active.translations.get(text.toLocaleLowerCase())
|
|
48
|
+
?? trMatch(text, parts, active);
|
|
34
49
|
if (!translated) {
|
|
35
50
|
const separator = (text.length > 1)
|
|
36
51
|
? ['.', '?', '!', ';', ':', ',', '(', ')'].find(c => text.includes(c))
|
|
@@ -49,42 +64,74 @@ function tr(text, parts, options) {
|
|
|
49
64
|
}
|
|
50
65
|
return firstSpaces + translated + lastSpaces;
|
|
51
66
|
}
|
|
52
|
-
function trInit(
|
|
53
|
-
|
|
67
|
+
function trInit(language) {
|
|
68
|
+
defaultLanguage = language;
|
|
69
|
+
catalogs.clear();
|
|
54
70
|
exports.expressions.clear();
|
|
55
71
|
exports.translations.clear();
|
|
72
|
+
catalogs.set(language, { expressions: exports.expressions, translations: exports.translations });
|
|
56
73
|
}
|
|
57
|
-
async function trLoad(file) {
|
|
74
|
+
async function trLoad(file, options = {}) {
|
|
58
75
|
try {
|
|
59
76
|
await (0, promises_1.access)(file);
|
|
60
77
|
}
|
|
61
78
|
catch {
|
|
62
79
|
return;
|
|
63
80
|
}
|
|
81
|
+
const active = catalog(options.language ?? defaultLanguage);
|
|
64
82
|
return (0, promises_2.readFile)(file, 'utf-8')
|
|
65
83
|
.then((data) => parseCsv(data, { delimiter: ';' }).data)
|
|
66
84
|
.then(data => data.forEach(row => {
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
|
|
85
|
+
const [source, target] = options.reverse ? [row[1], row[0]] : row;
|
|
86
|
+
if ((typeof source !== 'string') || (typeof target !== 'string'))
|
|
87
|
+
return;
|
|
88
|
+
active.translations.set(source, target);
|
|
89
|
+
if (source.includes('$')) {
|
|
90
|
+
const indexes = [];
|
|
91
|
+
let last = 0;
|
|
92
|
+
let pattern = '^';
|
|
93
|
+
for (const match of source.matchAll(/\$([1-9][0-9]*)/g)) {
|
|
94
|
+
pattern += escapeRegExp(source.slice(last, match.index)) + '(.*?)';
|
|
95
|
+
indexes.push(Number(match[1]));
|
|
96
|
+
last = match.index + match[0].length;
|
|
97
|
+
}
|
|
98
|
+
pattern += escapeRegExp(source.slice(last)) + '$';
|
|
99
|
+
const expression = RegExp(pattern);
|
|
100
|
+
expressionData.set(expression, { indexes, source });
|
|
101
|
+
active.expressions.add(expression);
|
|
70
102
|
}
|
|
71
103
|
}));
|
|
72
104
|
}
|
|
73
|
-
function
|
|
74
|
-
|
|
105
|
+
function trWithLanguage(language, callback) {
|
|
106
|
+
return languageScope.run(language, callback);
|
|
107
|
+
}
|
|
108
|
+
function trMatch(text, parts, active) {
|
|
109
|
+
for (const expression of active.expressions) {
|
|
75
110
|
const match = text.match(expression);
|
|
76
111
|
if (!match)
|
|
77
112
|
continue;
|
|
78
|
-
|
|
79
|
-
|
|
113
|
+
const data = expressionData.get(expression);
|
|
114
|
+
let counter = 0;
|
|
115
|
+
const source = data?.source
|
|
116
|
+
?? expression.source.slice(1, -1).replace(/\(\.\*\??\)/g, () => '$' + ++counter);
|
|
117
|
+
const translated = active.translations.get(source);
|
|
118
|
+
if (!translated)
|
|
119
|
+
continue;
|
|
80
120
|
const trParts = [...parts];
|
|
81
|
-
for (const part of match.slice(1)) {
|
|
121
|
+
for (const [offset, part] of match.slice(1).entries()) {
|
|
82
122
|
const translatedPart = tr(part);
|
|
83
|
-
trParts
|
|
84
|
-
|
|
123
|
+
trParts[data?.indexes[offset] ?? (offset + 1)] = (translatedPart && (translatedPart !== part))
|
|
124
|
+
? translatedPart[0].toLocaleLowerCase() + translatedPart.slice(1)
|
|
125
|
+
: translatedPart;
|
|
126
|
+
}
|
|
127
|
+
let result = translated;
|
|
128
|
+
for (let index = trParts.length - 1; index > 0; index--) {
|
|
129
|
+
result = result.replaceAll('$' + index, trParts[index] ?? '');
|
|
85
130
|
}
|
|
86
|
-
|
|
87
|
-
return tr(trText, trParts);
|
|
131
|
+
return result;
|
|
88
132
|
}
|
|
89
133
|
}
|
|
134
|
+
function escapeRegExp(text) {
|
|
135
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
136
|
+
}
|
|
90
137
|
//# sourceMappingURL=translate.js.map
|
package/package.json
CHANGED