@pantho075/locale 0.1.2 → 0.1.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 +128 -4
- package/dist/index.cjs +31 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -6
- package/dist/index.d.ts +27 -6
- package/dist/index.js +32 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -107,6 +107,115 @@ import ja from "./locales/ja";
|
|
|
107
107
|
setLocales({ en, ja });
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
## Language switching
|
|
111
|
+
|
|
112
|
+
The hooks accept any `string` for `lang`, but for a real app you'll usually want a constrained set of codes, a default fallback, and a UI control to switch between them. Build those once in your app — the package doesn't ship them because they tend to be tied to your auth/session model.
|
|
113
|
+
|
|
114
|
+
### Typing the supported codes
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
// src/i18n/lang.ts
|
|
118
|
+
|
|
119
|
+
/** Matches `AuthUser.lang` from the verify-token response. */
|
|
120
|
+
export type Lang = "en" | "bn" | "ne";
|
|
121
|
+
|
|
122
|
+
export const SUPPORTED_LANGS: readonly Lang[] = ["en", "bn", "ne"] as const;
|
|
123
|
+
export const DEFAULT_LANG: Lang = "en";
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Narrow an arbitrary value (e.g. `useAuthStore((s) => s.user?.lang)`,
|
|
127
|
+
* which is `Lang | undefined`) to one of our `Lang` codes, falling back
|
|
128
|
+
* to `DEFAULT_LANG`. Pass the result to `getTranslation` / `useTranslation`.
|
|
129
|
+
*/
|
|
130
|
+
export function resolveLang(value: unknown): Lang {
|
|
131
|
+
return typeof value === "string" &&
|
|
132
|
+
(SUPPORTED_LANGS as readonly string[]).includes(value)
|
|
133
|
+
? (value as Lang)
|
|
134
|
+
: DEFAULT_LANG;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Reading translations in components
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
// src/components/Greeting.tsx
|
|
142
|
+
import { useTranslation } from "@pantho075/locale";
|
|
143
|
+
import type { Translations } from "@/types/localeTypes";
|
|
144
|
+
import { resolveLang } from "@/i18n/lang";
|
|
145
|
+
import { useAuthStore } from "@/stores/auth";
|
|
146
|
+
|
|
147
|
+
export function Greeting() {
|
|
148
|
+
const lang = useAuthStore((s) => s.user?.lang);
|
|
149
|
+
const t = useTranslation<Translations>(resolveLang(lang));
|
|
150
|
+
return <h1>{t.title}</h1>;
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
…and in non-React / server code:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { getTranslation } from "@pantho075/locale";
|
|
158
|
+
import { resolveLang } from "@/i18n/lang";
|
|
159
|
+
import type { Translations } from "@/types/localeTypes";
|
|
160
|
+
|
|
161
|
+
export async function loadGreeting() {
|
|
162
|
+
const { loading } = getTranslation<Translations>(resolveLang("bn"));
|
|
163
|
+
return loading;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Switching languages at runtime
|
|
168
|
+
|
|
169
|
+
The package ships `switchLocale(lang)` — call it and every component using `useTranslation()` (no argument) re-renders with the new bundle. Combined with `setLocales(...)` at startup, this is the whole switching story.
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
// src/components/LanguageSwitcher.tsx
|
|
173
|
+
import { switchLocale } from "@pantho075/locale";
|
|
174
|
+
import { useAuthStore } from "@/stores/auth";
|
|
175
|
+
import type { Lang } from "@/i18n/lang";
|
|
176
|
+
|
|
177
|
+
const LABELS: Record<Lang, string> = {
|
|
178
|
+
en: "English",
|
|
179
|
+
bn: "বাংলা",
|
|
180
|
+
ne: "नेपाली",
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export function LanguageSwitcher() {
|
|
184
|
+
const user = useAuthStore((s) => s.user);
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<select
|
|
188
|
+
value={user?.lang ?? "en"}
|
|
189
|
+
onChange={(e) => {
|
|
190
|
+
const next = e.target.value as Lang;
|
|
191
|
+
// Persist to your auth store / cookie / localStorage…
|
|
192
|
+
useAuthStore.setState((s) => ({
|
|
193
|
+
user: s.user ? { ...s.user, lang: next } : s.user,
|
|
194
|
+
}));
|
|
195
|
+
// And tell the package. Components using useTranslation() with no
|
|
196
|
+
// argument re-render automatically.
|
|
197
|
+
switchLocale(next);
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
200
|
+
{(Object.keys(LABELS) as Lang[]).map((code) => (
|
|
201
|
+
<option key={code} value={code}>{LABELS[code]}</option>
|
|
202
|
+
))}
|
|
203
|
+
</select>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Components don't need to change. They call `useTranslation()` with no argument:
|
|
209
|
+
|
|
210
|
+
```tsx
|
|
211
|
+
// was: const t = useTranslation<Translations>(lang);
|
|
212
|
+
// now: same hook, no arg → subscribes to the active lang
|
|
213
|
+
const t = useTranslation<Translations>();
|
|
214
|
+
return <h1>{t.title}</h1>;
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`useTranslation` keeps its existing `(lang)` signature for callers that want to pin a language. The two modes coexist — explicit arg wins over the active lang.
|
|
218
|
+
|
|
110
219
|
## API
|
|
111
220
|
|
|
112
221
|
### `getTranslation<T>(lang: string): T`
|
|
@@ -124,17 +233,32 @@ interface English {
|
|
|
124
233
|
const en = getTranslation<English>("en");
|
|
125
234
|
```
|
|
126
235
|
|
|
127
|
-
### `useTranslation<T>(lang
|
|
236
|
+
### `useTranslation<T>(lang?: string): T`
|
|
128
237
|
|
|
129
|
-
React hook wrapping `getTranslation`.
|
|
238
|
+
React hook wrapping `getTranslation`. Two modes:
|
|
239
|
+
|
|
240
|
+
- **Explicit** — `useTranslation('en')` is memoized on `'en'`. Same language returns the same object reference across re-renders.
|
|
241
|
+
- **Active** — `useTranslation()` (no argument) subscribes to the package's currently active language and returns `getTranslation(activeLang)`. Components re-render automatically when `switchLocale(...)` is called.
|
|
130
242
|
|
|
131
243
|
```ts
|
|
132
244
|
import { useTranslation } from "@pantho075/locale";
|
|
133
245
|
|
|
134
|
-
const { title } = useTranslation("en");
|
|
135
|
-
const
|
|
246
|
+
const { title } = useTranslation("en"); // explicit, pinned
|
|
247
|
+
const t = useTranslation<English>(); // active, subscribes
|
|
136
248
|
```
|
|
137
249
|
|
|
250
|
+
### `switchLocale(lang: string): void`
|
|
251
|
+
|
|
252
|
+
Sets the package's active language and notifies every `useTranslation()` subscriber to re-render. Idempotent: calling with the current value is a no-op. Unknown values are accepted and fall back to the empty bundle the same way `getTranslation(unknown)` does.
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
import { switchLocale } from "@pantho075/locale";
|
|
256
|
+
|
|
257
|
+
switchLocale("bn");
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
See [Switching languages at runtime](#switching-languages-at-runtime).
|
|
261
|
+
|
|
138
262
|
### `setLocales<T>(locales: Record<string, T>): void`
|
|
139
263
|
|
|
140
264
|
Replaces the package's internal locale registry. Call once at app startup to swap in your own bundles. See [Using your own translations](#using-your-own-translations).
|
package/dist/index.cjs
CHANGED
|
@@ -45,12 +45,43 @@ function setLocales(next) {
|
|
|
45
45
|
function getTranslation(lang) {
|
|
46
46
|
return locales[lang] ?? EMPTY;
|
|
47
47
|
}
|
|
48
|
+
|
|
49
|
+
// src/switchLocale.ts
|
|
50
|
+
var activeLang;
|
|
51
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
52
|
+
function subscribe(listener) {
|
|
53
|
+
listeners.add(listener);
|
|
54
|
+
return () => {
|
|
55
|
+
listeners.delete(listener);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function getActiveLang() {
|
|
59
|
+
return activeLang;
|
|
60
|
+
}
|
|
61
|
+
function switchLocale(lang) {
|
|
62
|
+
if (lang === activeLang) return;
|
|
63
|
+
activeLang = lang;
|
|
64
|
+
for (const listener of [...listeners]) {
|
|
65
|
+
listener();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/useTranslation.ts
|
|
48
70
|
function useTranslation(lang) {
|
|
71
|
+
if (lang === void 0) {
|
|
72
|
+
const active = react.useSyncExternalStore(
|
|
73
|
+
subscribe,
|
|
74
|
+
getActiveLang,
|
|
75
|
+
() => void 0
|
|
76
|
+
);
|
|
77
|
+
return getTranslation(active ?? "");
|
|
78
|
+
}
|
|
49
79
|
return react.useMemo(() => getTranslation(lang), [lang]);
|
|
50
80
|
}
|
|
51
81
|
|
|
52
82
|
exports.getTranslation = getTranslation;
|
|
53
83
|
exports.setLocales = setLocales;
|
|
84
|
+
exports.switchLocale = switchLocale;
|
|
54
85
|
exports.useTranslation = useTranslation;
|
|
55
86
|
//# sourceMappingURL=index.cjs.map
|
|
56
87
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/locales/bn.ts","../src/locales/en.ts","../src/locales/ne.ts","../src/getTranslation.ts","../src/useTranslation.ts"],"names":["data","useMemo"],"mappings":";;;;;AAAA,IAAM,IAAA,GAAO;AAAA,EACX,KAAA,EAAO,4CAAA;AAAA,EACP,IAAA,EAAM;AAAA,IACJ,MAAA,EAAQ,gCAAA;AAAA,IACR,MAAA,EAAQ;AAAA;AAEZ,CAAA;AAEA,IAAO,UAAA,GAAQ,IAAA;;;ACRf,IAAMA,KAAAA,GAAO;AAAA,EACX,KAAA,EAAO,WAAA;AAAA,EACP,IAAA,EAAM;AAAA,IACJ,MAAA,EAAQ,gBAAA;AAAA,IACR,MAAA,EAAQ;AAAA;AAEZ,CAAA;AAEA,IAAO,UAAA,GAAQA,KAAAA;;;ACRf,IAAMA,KAAAA,GAAO;AAAA,EACX,KAAA,EAAO,WAAA;AAAA,EACP,IAAA,EAAM;AAAA,IACJ,MAAA,EAAQ,gBAAA;AAAA,IACR,MAAA,EAAQ;AAAA;AAEZ,CAAA;AAEA,IAAO,UAAA,GAAQA,KAAAA;;;ACFf,IAAM,KAAA,GAAyB,MAAA,CAAO,MAAA,CAAO,EAAE,CAAA;AAY/C,IAAI,OAAA,GAA2C;AAAA,EAC7C,EAAA,EAAA,UAAA;AAAA,EACA,EAAA,EAAA,UAAA;AAAA,EACA,EAAA,EAAA;AACF,CAAA;AAuBO,SAAS,WACd,IAAA,EACM;AACN,EAAA,OAAA,GAAU,IAAA;AACZ;AASO,SAAS,eACd,IAAA,EACG;AACH,EAAA,OAAQ,OAAA,CAAQ,IAAI,CAAA,IAAK,KAAA;AAC3B;
|
|
1
|
+
{"version":3,"sources":["../src/locales/bn.ts","../src/locales/en.ts","../src/locales/ne.ts","../src/getTranslation.ts","../src/switchLocale.ts","../src/useTranslation.ts"],"names":["data","useSyncExternalStore","useMemo"],"mappings":";;;;;AAAA,IAAM,IAAA,GAAO;AAAA,EACX,KAAA,EAAO,4CAAA;AAAA,EACP,IAAA,EAAM;AAAA,IACJ,MAAA,EAAQ,gCAAA;AAAA,IACR,MAAA,EAAQ;AAAA;AAEZ,CAAA;AAEA,IAAO,UAAA,GAAQ,IAAA;;;ACRf,IAAMA,KAAAA,GAAO;AAAA,EACX,KAAA,EAAO,WAAA;AAAA,EACP,IAAA,EAAM;AAAA,IACJ,MAAA,EAAQ,gBAAA;AAAA,IACR,MAAA,EAAQ;AAAA;AAEZ,CAAA;AAEA,IAAO,UAAA,GAAQA,KAAAA;;;ACRf,IAAMA,KAAAA,GAAO;AAAA,EACX,KAAA,EAAO,WAAA;AAAA,EACP,IAAA,EAAM;AAAA,IACJ,MAAA,EAAQ,gBAAA;AAAA,IACR,MAAA,EAAQ;AAAA;AAEZ,CAAA;AAEA,IAAO,UAAA,GAAQA,KAAAA;;;ACFf,IAAM,KAAA,GAAyB,MAAA,CAAO,MAAA,CAAO,EAAE,CAAA;AAY/C,IAAI,OAAA,GAA2C;AAAA,EAC7C,EAAA,EAAA,UAAA;AAAA,EACA,EAAA,EAAA,UAAA;AAAA,EACA,EAAA,EAAA;AACF,CAAA;AAuBO,SAAS,WACd,IAAA,EACM;AACN,EAAA,OAAA,GAAU,IAAA;AACZ;AASO,SAAS,eACd,IAAA,EACG;AACH,EAAA,OAAQ,OAAA,CAAQ,IAAI,CAAA,IAAK,KAAA;AAC3B;;;ACjDA,IAAI,UAAA;AAEJ,IAAM,SAAA,uBAAgB,GAAA,EAAgB;AAG/B,SAAS,UAAU,QAAA,EAAkC;AAC1D,EAAA,SAAA,CAAU,IAAI,QAAQ,CAAA;AACtB,EAAA,OAAO,MAAM;AACX,IAAA,SAAA,CAAU,OAAO,QAAQ,CAAA;AAAA,EAC3B,CAAA;AACF;AAMO,SAAS,aAAA,GAAoC;AAClD,EAAA,OAAO,UAAA;AACT;AAaO,SAAS,aAAa,IAAA,EAAoB;AAC/C,EAAA,IAAI,SAAS,UAAA,EAAY;AACzB,EAAA,UAAA,GAAa,IAAA;AAGb,EAAA,KAAA,MAAW,QAAA,IAAY,CAAC,GAAG,SAAS,CAAA,EAAG;AACrC,IAAA,QAAA,EAAS;AAAA,EACX;AACF;;;AC7BO,SAAS,eACd,IAAA,EACG;AACH,EAAA,IAAI,SAAS,MAAA,EAAW;AAKtB,IAAA,MAAM,MAAA,GAASC,0BAAA;AAAA,MACb,SAAA;AAAA,MACA,aAAA;AAAA,MACA,MAAM;AAAA,KACR;AACA,IAAA,OAAO,cAAA,CAAkB,UAAU,EAAE,CAAA;AAAA,EACvC;AAEA,EAAA,OAAOC,cAAQ,MAAM,cAAA,CAAkB,IAAI,CAAA,EAAG,CAAC,IAAI,CAAC,CAAA;AACtD","file":"index.cjs","sourcesContent":["const data = {\n title: \"শিরোনাম\",\n home: {\n header: \"হেডার\",\n footer: \"ফুটার\",\n },\n};\n\nexport default data;\n","const data = {\n title: \"the title\",\n home: {\n header: \"this is header\",\n footer: \"this is footer\",\n },\n};\n\nexport default data;\n","const data = {\n title: \"the title\",\n home: {\n header: \"this is header\",\n footer: \"this is footer\",\n },\n};\n\nexport default data;\n","import bn from \"./locales/bn\";\nimport en from \"./locales/en\";\nimport ne from \"./locales/ne\";\nimport type { TranslationData } from \"./types\";\n\n/** Shared empty fallback for unknown languages. Frozen so it can't be mutated. */\nconst EMPTY: TranslationData = Object.freeze({});\n\n/**\n * Locale registry. Adding a new language means:\n * 1. drop a `src/locales/<code>.ts` that exports the locale object as default\n * 2. import it here\n * 3. add it to this map\n *\n * These entries are the default sample bundles. Consumers that need to\n * translate their own data should call `setLocales(...)` once at app\n * startup to replace the entire map — see the README.\n */\nlet locales: Record<string, TranslationData> = {\n en,\n bn,\n ne,\n};\n\n/**\n * Replace the package's locale registry with the consumer's bundles.\n *\n * The new map **replaces** the existing entries wholesale — it does\n * not merge. Pass every locale you want available; anything missing\n * from `next` will fall back to the empty bundle.\n *\n * This is intended as one-shot app startup configuration. It does\n * not invalidate previously memoized `useTranslation` results — if a\n * consumer swaps locales at runtime, they should pass `lang` as\n * reactive state so React re-runs the hook.\n *\n * @example\n * ```ts\n * import { setLocales } from \"@pantho075/locale\";\n * import { en } from \"./locales/en\";\n * import { bn } from \"./locales/bn\";\n *\n * setLocales({ en, bn });\n * ```\n */\nexport function setLocales<T extends object = TranslationData>(\n next: Record<string, T>,\n): void {\n locales = next as unknown as Record<string, TranslationData>;\n}\n\n/**\n * Returns the translation bundle for the given language code.\n * Unknown / undefined / empty values fall back to a stable empty object.\n *\n * Framework-agnostic: works in React, Next.js server components, Vue,\n * Svelte, or vanilla Node scripts.\n */\nexport function getTranslation<T extends object = TranslationData>(\n lang: string,\n): T {\n return (locales[lang] ?? EMPTY) as T;\n}\n","/**\n * Module-scoped active language + listener channel.\n *\n * The package exposes one mutable concept: the currently active language.\n * `switchLocale(...)` mutates it; `subscribe` / `getActiveLang` let React\n * hooks observe changes via `useSyncExternalStore`.\n *\n * SSR safety: `getActiveLang` is called during render and must be\n * deterministic. We keep `activeLang` as a plain `string | undefined`\n * so server and client agree on `undefined` until `switchLocale` fires\n * on the client (which is exactly when re-renders are valid).\n */\n\nlet activeLang: string | undefined;\n\nconst listeners = new Set<() => void>();\n\n/** Subscribe to active-lang changes. Returns an unsubscribe fn. */\nexport function subscribe(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\n/**\n * Read the current active language. Returns `undefined` until\n * `switchLocale` has been called for the first time.\n */\nexport function getActiveLang(): string | undefined {\n return activeLang;\n}\n\n/**\n * Set the package's active language and notify all subscribers.\n *\n * The argument is a key from the map last passed to `setLocales(...)`.\n * Unknown values are accepted — they fall back to the empty bundle the\n * same way `getTranslation(unknown)` does. This keeps the API permissive\n * and matches the rest of the package.\n *\n * Idempotent: calling with the current active value is a no-op (no\n * listener notifications, no re-renders).\n */\nexport function switchLocale(lang: string): void {\n if (lang === activeLang) return;\n activeLang = lang;\n // Snapshot first — listeners may unsubscribe themselves during the\n // notification, and mutating the Set mid-iteration would skip them.\n for (const listener of [...listeners]) {\n listener();\n }\n}\n","import { useMemo, useSyncExternalStore } from \"react\";\nimport { getTranslation } from \"./getTranslation\";\nimport {\n getActiveLang,\n subscribe as subscribeActiveLang,\n} from \"./switchLocale\";\nimport type { TranslationData } from \"./types\";\n\n/**\n * React hook: returns the translation bundle for the given language,\n * or — when called with no argument — for the package's currently\n * active language (set via `switchLocale`).\n *\n * Two modes:\n *\n * - **Explicit** — `useTranslation('en')` is memoized on `'en'`. Same\n * language produces a stable object reference across re-renders.\n * Switching the argument swaps the reference.\n *\n * - **Active** — `useTranslation()` subscribes to the active language\n * and returns `getTranslation(activeLang)`. Components re-render\n * automatically when `switchLocale(...)` is called.\n */\nexport function useTranslation<T extends object = TranslationData>(\n lang?: string,\n): T {\n if (lang === undefined) {\n // `useSyncExternalStore` requires the snapshot getter to return the\n // same value across calls when nothing has changed. Our snapshot is\n // `activeLang ?? ''` so SSR (where activeLang is undefined) and the\n // first client render agree before any `switchLocale` fires.\n const active = useSyncExternalStore(\n subscribeActiveLang,\n getActiveLang,\n () => undefined,\n );\n return getTranslation<T>(active ?? \"\");\n }\n\n return useMemo(() => getTranslation<T>(lang), [lang]);\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -36,12 +36,33 @@ declare function setLocales<T extends object = TranslationData>(next: Record<str
|
|
|
36
36
|
declare function getTranslation<T extends object = TranslationData>(lang: string): T;
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* React hook: returns the translation bundle for the given language
|
|
39
|
+
* React hook: returns the translation bundle for the given language,
|
|
40
|
+
* or — when called with no argument — for the package's currently
|
|
41
|
+
* active language (set via `switchLocale`).
|
|
40
42
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
43
|
+
* Two modes:
|
|
44
|
+
*
|
|
45
|
+
* - **Explicit** — `useTranslation('en')` is memoized on `'en'`. Same
|
|
46
|
+
* language produces a stable object reference across re-renders.
|
|
47
|
+
* Switching the argument swaps the reference.
|
|
48
|
+
*
|
|
49
|
+
* - **Active** — `useTranslation()` subscribes to the active language
|
|
50
|
+
* and returns `getTranslation(activeLang)`. Components re-render
|
|
51
|
+
* automatically when `switchLocale(...)` is called.
|
|
52
|
+
*/
|
|
53
|
+
declare function useTranslation<T extends object = TranslationData>(lang?: string): T;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Set the package's active language and notify all subscribers.
|
|
57
|
+
*
|
|
58
|
+
* The argument is a key from the map last passed to `setLocales(...)`.
|
|
59
|
+
* Unknown values are accepted — they fall back to the empty bundle the
|
|
60
|
+
* same way `getTranslation(unknown)` does. This keeps the API permissive
|
|
61
|
+
* and matches the rest of the package.
|
|
62
|
+
*
|
|
63
|
+
* Idempotent: calling with the current active value is a no-op (no
|
|
64
|
+
* listener notifications, no re-renders).
|
|
44
65
|
*/
|
|
45
|
-
declare function
|
|
66
|
+
declare function switchLocale(lang: string): void;
|
|
46
67
|
|
|
47
|
-
export { type TranslationData, getTranslation, setLocales, useTranslation };
|
|
68
|
+
export { type TranslationData, getTranslation, setLocales, switchLocale, useTranslation };
|
package/dist/index.d.ts
CHANGED
|
@@ -36,12 +36,33 @@ declare function setLocales<T extends object = TranslationData>(next: Record<str
|
|
|
36
36
|
declare function getTranslation<T extends object = TranslationData>(lang: string): T;
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* React hook: returns the translation bundle for the given language
|
|
39
|
+
* React hook: returns the translation bundle for the given language,
|
|
40
|
+
* or — when called with no argument — for the package's currently
|
|
41
|
+
* active language (set via `switchLocale`).
|
|
40
42
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
43
|
+
* Two modes:
|
|
44
|
+
*
|
|
45
|
+
* - **Explicit** — `useTranslation('en')` is memoized on `'en'`. Same
|
|
46
|
+
* language produces a stable object reference across re-renders.
|
|
47
|
+
* Switching the argument swaps the reference.
|
|
48
|
+
*
|
|
49
|
+
* - **Active** — `useTranslation()` subscribes to the active language
|
|
50
|
+
* and returns `getTranslation(activeLang)`. Components re-render
|
|
51
|
+
* automatically when `switchLocale(...)` is called.
|
|
52
|
+
*/
|
|
53
|
+
declare function useTranslation<T extends object = TranslationData>(lang?: string): T;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Set the package's active language and notify all subscribers.
|
|
57
|
+
*
|
|
58
|
+
* The argument is a key from the map last passed to `setLocales(...)`.
|
|
59
|
+
* Unknown values are accepted — they fall back to the empty bundle the
|
|
60
|
+
* same way `getTranslation(unknown)` does. This keeps the API permissive
|
|
61
|
+
* and matches the rest of the package.
|
|
62
|
+
*
|
|
63
|
+
* Idempotent: calling with the current active value is a no-op (no
|
|
64
|
+
* listener notifications, no re-renders).
|
|
44
65
|
*/
|
|
45
|
-
declare function
|
|
66
|
+
declare function switchLocale(lang: string): void;
|
|
46
67
|
|
|
47
|
-
export { type TranslationData, getTranslation, setLocales, useTranslation };
|
|
68
|
+
export { type TranslationData, getTranslation, setLocales, switchLocale, useTranslation };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo } from 'react';
|
|
1
|
+
import { useSyncExternalStore, useMemo } from 'react';
|
|
2
2
|
|
|
3
3
|
// src/locales/bn.ts
|
|
4
4
|
var data = {
|
|
@@ -43,10 +43,40 @@ function setLocales(next) {
|
|
|
43
43
|
function getTranslation(lang) {
|
|
44
44
|
return locales[lang] ?? EMPTY;
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
// src/switchLocale.ts
|
|
48
|
+
var activeLang;
|
|
49
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
50
|
+
function subscribe(listener) {
|
|
51
|
+
listeners.add(listener);
|
|
52
|
+
return () => {
|
|
53
|
+
listeners.delete(listener);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function getActiveLang() {
|
|
57
|
+
return activeLang;
|
|
58
|
+
}
|
|
59
|
+
function switchLocale(lang) {
|
|
60
|
+
if (lang === activeLang) return;
|
|
61
|
+
activeLang = lang;
|
|
62
|
+
for (const listener of [...listeners]) {
|
|
63
|
+
listener();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/useTranslation.ts
|
|
46
68
|
function useTranslation(lang) {
|
|
69
|
+
if (lang === void 0) {
|
|
70
|
+
const active = useSyncExternalStore(
|
|
71
|
+
subscribe,
|
|
72
|
+
getActiveLang,
|
|
73
|
+
() => void 0
|
|
74
|
+
);
|
|
75
|
+
return getTranslation(active ?? "");
|
|
76
|
+
}
|
|
47
77
|
return useMemo(() => getTranslation(lang), [lang]);
|
|
48
78
|
}
|
|
49
79
|
|
|
50
|
-
export { getTranslation, setLocales, useTranslation };
|
|
80
|
+
export { getTranslation, setLocales, switchLocale, useTranslation };
|
|
51
81
|
//# sourceMappingURL=index.js.map
|
|
52
82
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/locales/bn.ts","../src/locales/en.ts","../src/locales/ne.ts","../src/getTranslation.ts","../src/useTranslation.ts"],"names":["data"],"mappings":";;;AAAA,IAAM,IAAA,GAAO;AAAA,EACX,KAAA,EAAO,4CAAA;AAAA,EACP,IAAA,EAAM;AAAA,IACJ,MAAA,EAAQ,gCAAA;AAAA,IACR,MAAA,EAAQ;AAAA;AAEZ,CAAA;AAEA,IAAO,UAAA,GAAQ,IAAA;;;ACRf,IAAMA,KAAAA,GAAO;AAAA,EACX,KAAA,EAAO,WAAA;AAAA,EACP,IAAA,EAAM;AAAA,IACJ,MAAA,EAAQ,gBAAA;AAAA,IACR,MAAA,EAAQ;AAAA;AAEZ,CAAA;AAEA,IAAO,UAAA,GAAQA,KAAAA;;;ACRf,IAAMA,KAAAA,GAAO;AAAA,EACX,KAAA,EAAO,WAAA;AAAA,EACP,IAAA,EAAM;AAAA,IACJ,MAAA,EAAQ,gBAAA;AAAA,IACR,MAAA,EAAQ;AAAA;AAEZ,CAAA;AAEA,IAAO,UAAA,GAAQA,KAAAA;;;ACFf,IAAM,KAAA,GAAyB,MAAA,CAAO,MAAA,CAAO,EAAE,CAAA;AAY/C,IAAI,OAAA,GAA2C;AAAA,EAC7C,EAAA,EAAA,UAAA;AAAA,EACA,EAAA,EAAA,UAAA;AAAA,EACA,EAAA,EAAA;AACF,CAAA;AAuBO,SAAS,WACd,IAAA,EACM;AACN,EAAA,OAAA,GAAU,IAAA;AACZ;AASO,SAAS,eACd,IAAA,EACG;AACH,EAAA,OAAQ,OAAA,CAAQ,IAAI,CAAA,IAAK,KAAA;AAC3B;
|
|
1
|
+
{"version":3,"sources":["../src/locales/bn.ts","../src/locales/en.ts","../src/locales/ne.ts","../src/getTranslation.ts","../src/switchLocale.ts","../src/useTranslation.ts"],"names":["data"],"mappings":";;;AAAA,IAAM,IAAA,GAAO;AAAA,EACX,KAAA,EAAO,4CAAA;AAAA,EACP,IAAA,EAAM;AAAA,IACJ,MAAA,EAAQ,gCAAA;AAAA,IACR,MAAA,EAAQ;AAAA;AAEZ,CAAA;AAEA,IAAO,UAAA,GAAQ,IAAA;;;ACRf,IAAMA,KAAAA,GAAO;AAAA,EACX,KAAA,EAAO,WAAA;AAAA,EACP,IAAA,EAAM;AAAA,IACJ,MAAA,EAAQ,gBAAA;AAAA,IACR,MAAA,EAAQ;AAAA;AAEZ,CAAA;AAEA,IAAO,UAAA,GAAQA,KAAAA;;;ACRf,IAAMA,KAAAA,GAAO;AAAA,EACX,KAAA,EAAO,WAAA;AAAA,EACP,IAAA,EAAM;AAAA,IACJ,MAAA,EAAQ,gBAAA;AAAA,IACR,MAAA,EAAQ;AAAA;AAEZ,CAAA;AAEA,IAAO,UAAA,GAAQA,KAAAA;;;ACFf,IAAM,KAAA,GAAyB,MAAA,CAAO,MAAA,CAAO,EAAE,CAAA;AAY/C,IAAI,OAAA,GAA2C;AAAA,EAC7C,EAAA,EAAA,UAAA;AAAA,EACA,EAAA,EAAA,UAAA;AAAA,EACA,EAAA,EAAA;AACF,CAAA;AAuBO,SAAS,WACd,IAAA,EACM;AACN,EAAA,OAAA,GAAU,IAAA;AACZ;AASO,SAAS,eACd,IAAA,EACG;AACH,EAAA,OAAQ,OAAA,CAAQ,IAAI,CAAA,IAAK,KAAA;AAC3B;;;ACjDA,IAAI,UAAA;AAEJ,IAAM,SAAA,uBAAgB,GAAA,EAAgB;AAG/B,SAAS,UAAU,QAAA,EAAkC;AAC1D,EAAA,SAAA,CAAU,IAAI,QAAQ,CAAA;AACtB,EAAA,OAAO,MAAM;AACX,IAAA,SAAA,CAAU,OAAO,QAAQ,CAAA;AAAA,EAC3B,CAAA;AACF;AAMO,SAAS,aAAA,GAAoC;AAClD,EAAA,OAAO,UAAA;AACT;AAaO,SAAS,aAAa,IAAA,EAAoB;AAC/C,EAAA,IAAI,SAAS,UAAA,EAAY;AACzB,EAAA,UAAA,GAAa,IAAA;AAGb,EAAA,KAAA,MAAW,QAAA,IAAY,CAAC,GAAG,SAAS,CAAA,EAAG;AACrC,IAAA,QAAA,EAAS;AAAA,EACX;AACF;;;AC7BO,SAAS,eACd,IAAA,EACG;AACH,EAAA,IAAI,SAAS,MAAA,EAAW;AAKtB,IAAA,MAAM,MAAA,GAAS,oBAAA;AAAA,MACb,SAAA;AAAA,MACA,aAAA;AAAA,MACA,MAAM;AAAA,KACR;AACA,IAAA,OAAO,cAAA,CAAkB,UAAU,EAAE,CAAA;AAAA,EACvC;AAEA,EAAA,OAAO,QAAQ,MAAM,cAAA,CAAkB,IAAI,CAAA,EAAG,CAAC,IAAI,CAAC,CAAA;AACtD","file":"index.js","sourcesContent":["const data = {\n title: \"শিরোনাম\",\n home: {\n header: \"হেডার\",\n footer: \"ফুটার\",\n },\n};\n\nexport default data;\n","const data = {\n title: \"the title\",\n home: {\n header: \"this is header\",\n footer: \"this is footer\",\n },\n};\n\nexport default data;\n","const data = {\n title: \"the title\",\n home: {\n header: \"this is header\",\n footer: \"this is footer\",\n },\n};\n\nexport default data;\n","import bn from \"./locales/bn\";\nimport en from \"./locales/en\";\nimport ne from \"./locales/ne\";\nimport type { TranslationData } from \"./types\";\n\n/** Shared empty fallback for unknown languages. Frozen so it can't be mutated. */\nconst EMPTY: TranslationData = Object.freeze({});\n\n/**\n * Locale registry. Adding a new language means:\n * 1. drop a `src/locales/<code>.ts` that exports the locale object as default\n * 2. import it here\n * 3. add it to this map\n *\n * These entries are the default sample bundles. Consumers that need to\n * translate their own data should call `setLocales(...)` once at app\n * startup to replace the entire map — see the README.\n */\nlet locales: Record<string, TranslationData> = {\n en,\n bn,\n ne,\n};\n\n/**\n * Replace the package's locale registry with the consumer's bundles.\n *\n * The new map **replaces** the existing entries wholesale — it does\n * not merge. Pass every locale you want available; anything missing\n * from `next` will fall back to the empty bundle.\n *\n * This is intended as one-shot app startup configuration. It does\n * not invalidate previously memoized `useTranslation` results — if a\n * consumer swaps locales at runtime, they should pass `lang` as\n * reactive state so React re-runs the hook.\n *\n * @example\n * ```ts\n * import { setLocales } from \"@pantho075/locale\";\n * import { en } from \"./locales/en\";\n * import { bn } from \"./locales/bn\";\n *\n * setLocales({ en, bn });\n * ```\n */\nexport function setLocales<T extends object = TranslationData>(\n next: Record<string, T>,\n): void {\n locales = next as unknown as Record<string, TranslationData>;\n}\n\n/**\n * Returns the translation bundle for the given language code.\n * Unknown / undefined / empty values fall back to a stable empty object.\n *\n * Framework-agnostic: works in React, Next.js server components, Vue,\n * Svelte, or vanilla Node scripts.\n */\nexport function getTranslation<T extends object = TranslationData>(\n lang: string,\n): T {\n return (locales[lang] ?? EMPTY) as T;\n}\n","/**\n * Module-scoped active language + listener channel.\n *\n * The package exposes one mutable concept: the currently active language.\n * `switchLocale(...)` mutates it; `subscribe` / `getActiveLang` let React\n * hooks observe changes via `useSyncExternalStore`.\n *\n * SSR safety: `getActiveLang` is called during render and must be\n * deterministic. We keep `activeLang` as a plain `string | undefined`\n * so server and client agree on `undefined` until `switchLocale` fires\n * on the client (which is exactly when re-renders are valid).\n */\n\nlet activeLang: string | undefined;\n\nconst listeners = new Set<() => void>();\n\n/** Subscribe to active-lang changes. Returns an unsubscribe fn. */\nexport function subscribe(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\n/**\n * Read the current active language. Returns `undefined` until\n * `switchLocale` has been called for the first time.\n */\nexport function getActiveLang(): string | undefined {\n return activeLang;\n}\n\n/**\n * Set the package's active language and notify all subscribers.\n *\n * The argument is a key from the map last passed to `setLocales(...)`.\n * Unknown values are accepted — they fall back to the empty bundle the\n * same way `getTranslation(unknown)` does. This keeps the API permissive\n * and matches the rest of the package.\n *\n * Idempotent: calling with the current active value is a no-op (no\n * listener notifications, no re-renders).\n */\nexport function switchLocale(lang: string): void {\n if (lang === activeLang) return;\n activeLang = lang;\n // Snapshot first — listeners may unsubscribe themselves during the\n // notification, and mutating the Set mid-iteration would skip them.\n for (const listener of [...listeners]) {\n listener();\n }\n}\n","import { useMemo, useSyncExternalStore } from \"react\";\nimport { getTranslation } from \"./getTranslation\";\nimport {\n getActiveLang,\n subscribe as subscribeActiveLang,\n} from \"./switchLocale\";\nimport type { TranslationData } from \"./types\";\n\n/**\n * React hook: returns the translation bundle for the given language,\n * or — when called with no argument — for the package's currently\n * active language (set via `switchLocale`).\n *\n * Two modes:\n *\n * - **Explicit** — `useTranslation('en')` is memoized on `'en'`. Same\n * language produces a stable object reference across re-renders.\n * Switching the argument swaps the reference.\n *\n * - **Active** — `useTranslation()` subscribes to the active language\n * and returns `getTranslation(activeLang)`. Components re-render\n * automatically when `switchLocale(...)` is called.\n */\nexport function useTranslation<T extends object = TranslationData>(\n lang?: string,\n): T {\n if (lang === undefined) {\n // `useSyncExternalStore` requires the snapshot getter to return the\n // same value across calls when nothing has changed. Our snapshot is\n // `activeLang ?? ''` so SSR (where activeLang is undefined) and the\n // first client render agree before any `switchLocale` fires.\n const active = useSyncExternalStore(\n subscribeActiveLang,\n getActiveLang,\n () => undefined,\n );\n return getTranslation<T>(active ?? \"\");\n }\n\n return useMemo(() => getTranslation<T>(lang), [lang]);\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pantho075/locale",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Tiny framework-agnostic i18n core. Translation data lives in TS, no JSON files, no provider. Works in React, Next.js, Vue, Svelte, and Node.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|