locl-js 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +518 -0
- package/dist/LocL-fN9Yari8.d.mts +437 -0
- package/dist/LocL-fN9Yari8.d.ts +437 -0
- package/dist/compat.d.mts +44 -0
- package/dist/compat.d.ts +44 -0
- package/dist/compat.js +137 -0
- package/dist/compat.mjs +111 -0
- package/dist/index.d.mts +20 -341
- package/dist/index.d.ts +20 -341
- package/dist/index.js +298 -84
- package/dist/index.mjs +293 -81
- package/dist/react.d.mts +120 -0
- package/dist/react.d.ts +120 -0
- package/dist/react.js +228 -0
- package/dist/react.mjs +188 -0
- package/package.json +28 -5
- package/readme.md +0 -319
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Sztrida Sámuel
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Sztrida Sámuel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
# LocL.js 🌐
|
|
2
|
+
|
|
3
|
+
[](https://github.com/sztsam/LocL.js) [](https://www.typescriptlang.org/) [](https://opensource.org/licenses/MIT) [](https://www.npmjs.com/package/locl-js)
|
|
4
|
+
|
|
5
|
+
**LocL** is an ultra-fast, lightweight, and fully-featured TypeScript internationalization (i18n) library. Designed for modern web applications, backend services, and multi-framework setups, it provides compile-time type-safety, automatic pluralization, ICU-style select branching, built-in formatters, and reactive subscriptions with **zero external dependencies**.
|
|
6
|
+
|
|
7
|
+
It is inspired by libraries like `i18next` and `react-i18next`, but engineered with a focus on strict TypeScript type checking, sub-millisecond execution, clean APIs, and 100% test coverage.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Features ✨
|
|
12
|
+
|
|
13
|
+
- **💪 Strict Compile-Time Safety**: Auto-extracts parameters (`{name}`, `{{name}}`, `{gender, select, ...}`) and enforces required arguments at compile time with IDE autocomplete.
|
|
14
|
+
- **🏷️ `defineResources` Helper**: Preserves literal types automatically without having to append `as const` manually.
|
|
15
|
+
- **⚛️ First-Class React Integration**: Official React bindings (`<LocLProvider>`, `useTranslation`, `useLocL`, `<Trans />`) via `locl-js/react` with two type-safe workflows:
|
|
16
|
+
- **`createLocLReact` Hook Factory**: Zero-declaration, pre-bound React suite.
|
|
17
|
+
- **`LocLRegister` Declaration Merging**: Ambient global autocomplete for standalone imports.
|
|
18
|
+
- **📦 Zero Dependencies & Lightweight**: Pure TypeScript with no runtime dependencies.
|
|
19
|
+
- **🔄 Drop-in i18next Compat**: Swap existing `i18next` codebases effortlessly using `locl-js/compat` without rewriting translation call sites. Supports colon namespaces (`auth:login`), default fallbacks, and event listener cleanup.
|
|
20
|
+
- **🌐 Multi-Framework Reactive Store**: Implements the standard reactive store contract via `translator.subscribe()`, working out of the box in Svelte, Vue, Solid, and Node.js.
|
|
21
|
+
- **🔢 Automatic Plurals**: Handles object plurals (`{ zero, one, other }`) and suffix plurals (`_zero`, `_one`, `_other`) directly via `t()` or dedicated `plural()`.
|
|
22
|
+
- **🔀 ICU Select Support**: Conditional branching (`{gender, select, male {He} female {She} other {They}}`) with nested parameter interpolation (`{gender, select, male {He has {count} items}}`).
|
|
23
|
+
- **🔧 Built-in & Custom Formatters**: Format dates, numbers, currencies, strings, and booleans inline via pipe syntax (`{val | number}`, `{date | date:short}`).
|
|
24
|
+
- **⚡ High Performance Caching**: Granular scope-based translation caching and multi-proxy instance reuse.
|
|
25
|
+
- **🛡️ Prototype-Safe Dynamic Resources**: Safely merge resources dynamically at runtime with built-in prototype pollution guards.
|
|
26
|
+
- **💯 100% Test Coverage**: Fully verified across every line, branch, function, and edge case.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install locl-js
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { initLocL, defineResources } from "locl-js";
|
|
42
|
+
|
|
43
|
+
// defineResources automatically infers literal types for compile-time safety
|
|
44
|
+
const resources = defineResources({
|
|
45
|
+
en: {
|
|
46
|
+
hello: "Hello, {name}!",
|
|
47
|
+
messages: {
|
|
48
|
+
one: "You have one message.",
|
|
49
|
+
other: "You have {count} messages.",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
fr: {
|
|
53
|
+
hello: "Bonjour, {name}!",
|
|
54
|
+
messages: {
|
|
55
|
+
one: "Vous avez un message.",
|
|
56
|
+
other: "Vous avez {count} messages.",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const translator = initLocL({
|
|
62
|
+
resources,
|
|
63
|
+
fallbackLanguage: "en",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Interpolation
|
|
67
|
+
console.log(translator.t("hello", { name: "Alice" })); // "Hello, Alice!"
|
|
68
|
+
|
|
69
|
+
// Plurals via t() or plural()
|
|
70
|
+
console.log(translator.t("messages", { count: 1 })); // "You have one message."
|
|
71
|
+
console.log(translator.plural("messages", { count: 5 })); // "You have 5 messages."
|
|
72
|
+
|
|
73
|
+
// Switch language
|
|
74
|
+
translator.changeLanguage("fr");
|
|
75
|
+
console.log(translator.t("hello", { name: "Alice" })); // "Bonjour, Alice!"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Strict Parameter Type-Checking
|
|
81
|
+
|
|
82
|
+
When defining resources with `defineResources` (or `as const`), `LocL` infers parameter names and types at compile time:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { defineResources, initLocL } from "locl-js";
|
|
86
|
+
|
|
87
|
+
const resources = defineResources({
|
|
88
|
+
en: {
|
|
89
|
+
welcome: "Welcome back, {name}!",
|
|
90
|
+
notification: "Hello {{user}}, you have {count} items.",
|
|
91
|
+
staticNotice: "Server maintenance scheduled for tonight.",
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const translator = initLocL({ resources, fallbackLanguage: "en" });
|
|
96
|
+
|
|
97
|
+
// ✅ TypeScript validates parameters and provides IDE autocomplete:
|
|
98
|
+
translator.t("welcome", { name: "Alice" });
|
|
99
|
+
translator.t("notification", { user: "Bob", count: 3 });
|
|
100
|
+
|
|
101
|
+
// ❌ TypeScript errors on missing or invalid parameters:
|
|
102
|
+
// translator.t("welcome"); // Error: Expected 2 arguments, got 1
|
|
103
|
+
// translator.t("welcome", { wrongKey: "Alice" }); // Error: Property 'name' is missing
|
|
104
|
+
|
|
105
|
+
// ✅ Static templates allow omitting options:
|
|
106
|
+
translator.t("staticNotice");
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## React Integration (`locl-js/react`)
|
|
112
|
+
|
|
113
|
+
`LocL` provides flicker-free React bindings powered by `useSyncExternalStore` and an XSS-safe `<Trans />` component. You can choose between two type-safe workflows:
|
|
114
|
+
|
|
115
|
+
### Workflow A: Zero-Declaration Factory (`createLocLReact`) ⭐ *Recommended*
|
|
116
|
+
|
|
117
|
+
No `.d.ts` declaration merging required. Bind your translator once and export typed components and hooks:
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
// src/i18n-react.ts
|
|
121
|
+
import { initLocL, defineResources } from "locl-js";
|
|
122
|
+
import { createLocLReact } from "locl-js/react";
|
|
123
|
+
|
|
124
|
+
export const translator = initLocL({
|
|
125
|
+
resources: defineResources({
|
|
126
|
+
en: {
|
|
127
|
+
welcome: "Welcome, {name}!",
|
|
128
|
+
user: { title: "Profile", greeting: "Hello, {name}!" },
|
|
129
|
+
},
|
|
130
|
+
}),
|
|
131
|
+
fallbackLanguage: "en",
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
export const { LocLProvider, useTranslation, useLocL, Trans } = createLocLReact(translator);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
In your application components:
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
import React from "react";
|
|
141
|
+
import { LocLProvider, useTranslation, Trans } from "./i18n-react";
|
|
142
|
+
|
|
143
|
+
export function App() {
|
|
144
|
+
return (
|
|
145
|
+
<LocLProvider>
|
|
146
|
+
<Header />
|
|
147
|
+
<Profile />
|
|
148
|
+
</LocLProvider>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function Header() {
|
|
153
|
+
// 100% Autocomplete across root keys!
|
|
154
|
+
const { t, language, changeLanguage } = useTranslation();
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<header>
|
|
158
|
+
<h1>{t("welcome", { name: "Alice" })}</h1>
|
|
159
|
+
<button onClick={() => changeLanguage("de")}>Deutsch ({language})</button>
|
|
160
|
+
</header>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function Profile() {
|
|
165
|
+
// Scoped autocomplete: keys are restricted to the "user" namespace!
|
|
166
|
+
const { t } = useTranslation("user");
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<section>
|
|
170
|
+
<h2>{t("title")}</h2>
|
|
171
|
+
<Trans
|
|
172
|
+
scope="user"
|
|
173
|
+
i18nKey="greeting"
|
|
174
|
+
values={{ name: "Alice" }}
|
|
175
|
+
components={{ bold: <strong className="font-bold" /> }}
|
|
176
|
+
/>
|
|
177
|
+
</section>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
### Workflow B: Global Type Registry (`LocLRegister`)
|
|
185
|
+
|
|
186
|
+
If you prefer importing directly from `"locl-js/react"`, augment `LocLRegister` once in your project (e.g. `src/i18n.d.ts`):
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
import { translator } from "./i18n";
|
|
190
|
+
|
|
191
|
+
declare module "locl-js/react" {
|
|
192
|
+
interface LocLRegister {
|
|
193
|
+
translator: typeof translator;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Now, standalone imports automatically have full autocomplete everywhere:
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
import { useTranslation, Trans } from "locl-js/react";
|
|
202
|
+
|
|
203
|
+
function UserProfile() {
|
|
204
|
+
const { t } = useTranslation("user"); // ✅ Scopes and keys autocompleted globally!
|
|
205
|
+
return <h1>{t("title")}</h1>;
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
### The `<Trans />` Component
|
|
212
|
+
|
|
213
|
+
Renders complex translations containing formatting tags (`<bold>read</bold>`, `<link>terms</link>`) into React elements safely without `dangerouslySetInnerHTML`:
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
<Trans
|
|
217
|
+
i18nKey="notice" // "Please <bold>read</bold> our <link>terms</link>."
|
|
218
|
+
values={{ name: "Alice" }}
|
|
219
|
+
components={{
|
|
220
|
+
bold: <strong className="text-primary font-bold" />,
|
|
221
|
+
link: <a href="/terms" className="underline" target="_blank" rel="noopener noreferrer" />,
|
|
222
|
+
}}
|
|
223
|
+
fallback="Please read our terms."
|
|
224
|
+
/>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
- **Tag element replacement**: Replaces tags with JSX elements (e.g. `<strong />`) while preserving inner text as children.
|
|
228
|
+
- **Function renderers**: Supports custom render functions: `(content) => <span style={{ fontWeight: "bold" }}>{content}</span>`.
|
|
229
|
+
- **Reactive updates**: Subscribes to `useSyncExternalStore` so language changes re-render smoothly.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Drop-in `i18next` Compatibility (`locl-js/compat`)
|
|
234
|
+
|
|
235
|
+
Migrate existing `i18next` codebases effortlessly without refactoring callsites:
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
import { initLocL } from "locl-js";
|
|
239
|
+
import { toI18next, createI18nextCompat } from "locl-js/compat";
|
|
240
|
+
|
|
241
|
+
const locl = initLocL({ resources, fallbackLanguage: "en" });
|
|
242
|
+
export const i18n = toI18next(locl);
|
|
243
|
+
|
|
244
|
+
// Standard i18next patterns supported seamlessly:
|
|
245
|
+
i18n.t("common:login", { name: "Alice" }); // Colon namespaces
|
|
246
|
+
i18n.t("common.login"); // Dot namespaces
|
|
247
|
+
i18n.t("missing.key", "Default String"); // Default values
|
|
248
|
+
i18n.exists("common:login"); // Key existence checks
|
|
249
|
+
|
|
250
|
+
// Plurals via count:
|
|
251
|
+
i18n.t("apple", { count: 5 }); // "5 apples"
|
|
252
|
+
|
|
253
|
+
// Dynamic resource bundle management:
|
|
254
|
+
i18n.addResourceBundle("en", "dynamic", { title: "Title" });
|
|
255
|
+
i18n.hasResourceBundle("en", "dynamic"); // true
|
|
256
|
+
i18n.getResourceBundle("en", "dynamic"); // { title: "Title" }
|
|
257
|
+
|
|
258
|
+
// Event listeners with unbind cleanup:
|
|
259
|
+
const unbind = i18n.on("languageChanged", (lng) => {
|
|
260
|
+
console.log("Language changed to", lng);
|
|
261
|
+
});
|
|
262
|
+
unbind(); // Unsubscribes listener
|
|
263
|
+
|
|
264
|
+
await i18n.changeLanguage("es");
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Pluralization
|
|
270
|
+
|
|
271
|
+
`LocL` supports pluralization directly inside `translator.t()` and through `translator.plural()`. Both approaches automatically resolve CLDR plural rules (`zero`, `one`, `two`, `few`, `many`, `other`).
|
|
272
|
+
|
|
273
|
+
### 1. Object Pluralization
|
|
274
|
+
|
|
275
|
+
Define plurals as structured objects:
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
const en = {
|
|
279
|
+
items: {
|
|
280
|
+
zero: "No items",
|
|
281
|
+
one: "1 item",
|
|
282
|
+
other: "{count} items",
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
translator.t("items", { count: 0 }); // "No items"
|
|
287
|
+
translator.t("items", { count: 1 }); // "1 item"
|
|
288
|
+
translator.t("items", { count: 5 }); // "5 items"
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### 2. Suffix Pluralization
|
|
292
|
+
|
|
293
|
+
Define plural forms using category suffixes:
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
const en = {
|
|
297
|
+
apple_zero: "Zero apples",
|
|
298
|
+
apple_one: "1 apple",
|
|
299
|
+
apple_other: "{count} apples",
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
translator.t("apple", { count: 1 }); // "1 apple"
|
|
303
|
+
translator.t("apple", { count: 4 }); // "4 apples"
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
> **Note**: Both single `{var}` and double `{{var}}` placeholders are supported across all templates.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## ICU Select Branching
|
|
311
|
+
|
|
312
|
+
Support gender or contextual choice branching with ICU `select` syntax, including **nested placeholders** inside branch bodies:
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
const en = {
|
|
316
|
+
feedback: "{gender, select, male {He liked your photo} female {She liked your photo} other {They liked your photo}}.",
|
|
317
|
+
items: "{gender, select, male {He has {count} items} female {She has {count} items} other {They have {count} items}}",
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
translator.t("feedback", { gender: "female" });
|
|
321
|
+
// "She liked your photo."
|
|
322
|
+
|
|
323
|
+
// Nested parameters (gender AND count) are strictly typed!
|
|
324
|
+
translator.t("items", { gender: "male", count: 42 });
|
|
325
|
+
// "He has 42 items"
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## Built-in & Custom Formatters
|
|
331
|
+
|
|
332
|
+
Use pipe syntax (`{variable | formatter[:args]}`) inside your translations:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
const en = {
|
|
336
|
+
profile: "Member since {joined | date:short}. Balance: {balance | currency:USD}.",
|
|
337
|
+
user: "Username: {name | upper | truncate:10, ...}",
|
|
338
|
+
};
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Built-in Formatters
|
|
342
|
+
|
|
343
|
+
| Formatter | Description | Example |
|
|
344
|
+
| :--- | :--- | :--- |
|
|
345
|
+
| `upper` | Uppercase string | `{name \| upper}` |
|
|
346
|
+
| `lower` | Lowercase string | `{name \| lower}` |
|
|
347
|
+
| `capitalize` | Capitalize first letter | `{name \| capitalize}` |
|
|
348
|
+
| `trim` | Strip leading/trailing whitespace | `{name \| trim}` |
|
|
349
|
+
| `truncate` | Truncate string with suffix | `{text \| truncate:10, ...}` |
|
|
350
|
+
| `number` | `Intl.NumberFormat` decimal | `{val \| number:en-US}` |
|
|
351
|
+
| `currency` | `Intl.NumberFormat` currency | `{val \| currency:EUR, de-DE}` |
|
|
352
|
+
| `date` | `Intl.DateTimeFormat` date | `{val \| date:en-US, short}` |
|
|
353
|
+
| `relativeDate` | Formats relative time | `{val \| relativeDate:en-US}` |
|
|
354
|
+
| `json` | Converts object to JSON string | `{data \| json}` |
|
|
355
|
+
| `yesNo` | Returns "Yes" / "No" or custom labels | `{flag \| yesNo:Y, N}` |
|
|
356
|
+
| `boolean` | Returns "true" / "false" or custom labels | `{flag \| boolean:Active, Inactive}` |
|
|
357
|
+
| `padStart` | Left pad string | `{code \| padStart:5, 0}` |
|
|
358
|
+
| `padEnd` | Right pad string | `{code \| padEnd:5, _}` |
|
|
359
|
+
|
|
360
|
+
### Custom Formatters
|
|
361
|
+
|
|
362
|
+
Add custom formatters during initialization:
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
const translator = initLocL({
|
|
366
|
+
resources,
|
|
367
|
+
fallbackLanguage: "en",
|
|
368
|
+
formatters: {
|
|
369
|
+
reverse: (val) => String(val).split("").reverse().join(""),
|
|
370
|
+
highlight: (val) => `***${val}***`,
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
translator.t("greeting", { name: "Alice" }, { formatter: "highlight" });
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## Multi-Framework Reactive Store (Svelte, Vue, Solid, Node.js)
|
|
380
|
+
|
|
381
|
+
`LocL` implements the standard reactive store contract (`subscribe`):
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
const unsubscribe = translator.subscribe((newLang, prevLang) => {
|
|
385
|
+
console.log(`Language updated from ${prevLang} to ${newLang}`);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
translator.changeLanguage("es");
|
|
389
|
+
unsubscribe();
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
- **Svelte**: Reference directly as an auto-subscribing readable store (`$translator.t("key")`).
|
|
393
|
+
- **Vue**: Bind to a `shallowRef` inside `translator.subscribe()`.
|
|
394
|
+
- **Node.js**: Cleanly isolate translations per request or per guild.
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Rich Tag Interpolation (`translator.rich`)
|
|
399
|
+
|
|
400
|
+
Interpolate custom inline components, terminal styling, or HTML strings using `<tag>content</tag>`:
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
const en = {
|
|
404
|
+
notice: "Click <link>here</link> to view details or <bold>read more</bold>.",
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const elements = translator.rich("notice", {
|
|
408
|
+
link: (text) => `<a href="/details">${text}</a>`,
|
|
409
|
+
bold: (text) => `<b>${text}</b>`,
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
console.log(elements.join(""));
|
|
413
|
+
// "Click <a href="/details">here</a> to view details or <b>read more</b>."
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## Dynamic Resources
|
|
419
|
+
|
|
420
|
+
Add or deep-merge translation bundles at runtime safely with built-in prototype pollution guards:
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
// Add a single dotted key
|
|
424
|
+
translator.addResource("en", "dashboard.title", "Analytics Dashboard");
|
|
425
|
+
|
|
426
|
+
// Deep-merge an entire namespace
|
|
427
|
+
translator.addResources("en", {
|
|
428
|
+
checkout: {
|
|
429
|
+
pay: "Pay Now",
|
|
430
|
+
cancel: "Cancel",
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## Scopes & Proxies (`withConfig`)
|
|
438
|
+
|
|
439
|
+
Create lightweight, isolated translator instances for specific namespaces or fixed languages without duplicating resources:
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
const resources = defineResources({
|
|
443
|
+
en: {
|
|
444
|
+
auth: { login: "Log In", register: "Sign Up" },
|
|
445
|
+
dashboard: { welcome: "Welcome, {name}" },
|
|
446
|
+
},
|
|
447
|
+
de: {
|
|
448
|
+
auth: { login: "Anmelden", register: "Registrieren" },
|
|
449
|
+
dashboard: { welcome: "Willkommen, {name}" },
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
const translator = initLocL({ resources, fallbackLanguage: "en" });
|
|
454
|
+
|
|
455
|
+
// 1. Scoped proxy (inherits active language dynamically)
|
|
456
|
+
const authT = translator.withConfig({ scope: "auth" });
|
|
457
|
+
console.log(authT.t("login")); // "Log In"
|
|
458
|
+
|
|
459
|
+
// 2. Fixed-language proxy (pinned to German regardless of global language)
|
|
460
|
+
const deDashboardT = translator.withConfig({ scope: "dashboard", language: "de" });
|
|
461
|
+
console.log(deDashboardT.t("welcome", { name: "Hans" })); // "Willkommen, Hans"
|
|
462
|
+
|
|
463
|
+
// 3. Array scope (combines multiple namespaces)
|
|
464
|
+
const multiT = translator.withConfig({ scope: ["auth", "dashboard"] });
|
|
465
|
+
console.log(multiT.t("dashboard.welcome", { name: "Alice" })); // "Welcome, Alice"
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## API Reference
|
|
471
|
+
|
|
472
|
+
### Top-Level Exports (`locl-js`)
|
|
473
|
+
|
|
474
|
+
- **`defineResources(resources)`**: Identity helper using `<const T>` to enforce compile-time literal types without writing `as const`.
|
|
475
|
+
- **`initLocL(config)`**: Creates and returns a new `LocL` translator instance.
|
|
476
|
+
|
|
477
|
+
### React Exports (`locl-js/react`)
|
|
478
|
+
|
|
479
|
+
- **`createLocLReact(translator)`**: Returns a pre-bound React suite (`LocLProvider`, `useTranslation`, `useLocL`, `Trans`).
|
|
480
|
+
- **`LocLProvider`**: React Context Provider for the active translator.
|
|
481
|
+
- **`useTranslation(scope?, customTranslator?)`**: Translation hook with scope support.
|
|
482
|
+
- **`useLocL(customTranslator?)`**: Hook returning translator instance and reactive language state.
|
|
483
|
+
- **`<Trans />`**: Rich JSX tag interpolation component.
|
|
484
|
+
- **`LocLRegister`**: Ambient interface for global type registration.
|
|
485
|
+
|
|
486
|
+
### Compat Exports (`locl-js/compat`)
|
|
487
|
+
|
|
488
|
+
- **`toI18next(translator)`**: Wraps a LocL instance into an `i18next`-compatible API.
|
|
489
|
+
- **`createI18nextCompat(translator)`**: Alias of `toI18next`.
|
|
490
|
+
|
|
491
|
+
### `LocL` Instance Methods
|
|
492
|
+
|
|
493
|
+
- **`t(key, values?, format?)`**: Translates a key, handles interpolation, formatters, and plurals.
|
|
494
|
+
- **`plural(key, values, format?)`**: Translates a key with pluralization.
|
|
495
|
+
- **`rich(key, tagRenderers, values?)`**: Replaces `<tag>` wrappers using custom renderer functions.
|
|
496
|
+
- **`changeLanguage(lang)`**: Updates current language and notifies all subscribers.
|
|
497
|
+
- **`getLanguage()`**: Returns the currently active language code.
|
|
498
|
+
- **`subscribe(listener)`**: Subscribes to language changes. Returns an unsubscribe function.
|
|
499
|
+
- **`withConfig(config)`**: Returns a cached proxy translator instance with scoped configuration.
|
|
500
|
+
- **`clone(language?, scope?)`**: Clones the translator instance with optional language/scope overrides.
|
|
501
|
+
- **`addResource(lang, key, value)`**: Adds or overrides a single translation key at runtime.
|
|
502
|
+
- **`addResources(lang, bundle)`**: Deep-merges a dictionary into the target language.
|
|
503
|
+
- **`format(value, formatterName, args?)`**: Executes a formatter function directly.
|
|
504
|
+
- **`get(key?)`**: Returns the raw translation value or the entire active scoped dictionary.
|
|
505
|
+
- **`getObj(key)`**: Returns a typed nested object from the translation tree.
|
|
506
|
+
- **`tt(key?, values?, format?)`**: Untyped translation alias.
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
## Contributing
|
|
511
|
+
|
|
512
|
+
Contributions, feedback, and pull requests are warmly welcomed! Please feel free to open an issue or submit a pull request on GitHub.
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
## License
|
|
517
|
+
|
|
518
|
+
This project is licensed under the [MIT License](LICENSE).
|