locl-js 1.0.2 → 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/README.md ADDED
@@ -0,0 +1,518 @@
1
+ # LocL.js 🌐
2
+
3
+ [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/sztsam/LocL.js) [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue)](https://www.typescriptlang.org/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](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).