localize-react 1.7.1 → 2.0.0-next.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 CHANGED
@@ -1,307 +1,208 @@
1
- [![CircleCI](https://circleci.com/gh/yankouskia/localize-react.svg?style=shield)](https://circleci.com/gh/yankouskia/localize-react) [![Codecov Coverage](https://img.shields.io/codecov/c/github/yankouskia/localize-react/master.svg?style=flat-square)](https://codecov.io/gh/yankouskia/localize-react/) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/yankouskia/localize-react/pulls) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/yankouskia/localize-react/blob/master/LICENSE) ![GitHub stars](https://img.shields.io/github/stars/yankouskia/localize-react.svg?style=social)
1
+ <div align="center">
2
2
 
3
- [![NPM](https://nodei.co/npm/localize-react.png?downloads=true)](https://www.npmjs.com/package/localize-react)
3
+ <a href="https://yankouskia.github.io/localize-react">
4
+ <img src="./website/static/img/logo.svg" alt="localize-react" width="96" height="96" />
5
+ </a>
4
6
 
5
7
  # localize-react
6
8
 
7
- ✈️ Lightweight React Localization Library 🇺🇸
9
+ ### **React i18n, without the weight.**
8
10
 
9
- ## Motivation
11
+ Tiny, type-safe React i18n built on Context and hooks.<br/>
12
+ **< 1 kB brotli · 0 runtime deps · dual ESM + CJS · React 19 ready.**
10
13
 
11
- Creating really simple lightweight library for localization in React applications without any dependencies, which is built on top of new [React Context Api](https://reactjs.org/docs/context.html)
14
+ [**Docs**](https://yankouskia.github.io/localize-react) ·
15
+ [**Quickstart**](https://yankouskia.github.io/localize-react/docs/quickstart) ·
16
+ [**API**](https://yankouskia.github.io/localize-react/docs/api) ·
17
+ [**Recipes**](https://yankouskia.github.io/localize-react/docs/recipes) ·
18
+ [**Migration v1 → v2**](https://yankouskia.github.io/localize-react/docs/migration-v2)
12
19
 
13
- Library has just **737 Bytes** gzipped size
20
+ [![npm](https://img.shields.io/npm/v/localize-react?style=flat-square&color=cb3837&logo=npm&label=npm)](https://www.npmjs.com/package/localize-react)
21
+ [![downloads](https://img.shields.io/npm/dm/localize-react?style=flat-square&color=cb3837)](https://www.npmjs.com/package/localize-react)
22
+ [![CI](https://img.shields.io/github/actions/workflow/status/yankouskia/localize-react/ci.yml?branch=master&style=flat-square&logo=github&label=CI)](https://github.com/yankouskia/localize-react/actions/workflows/ci.yml)
23
+ [![coverage](https://img.shields.io/codecov/c/github/yankouskia/localize-react?style=flat-square&logo=codecov)](https://codecov.io/gh/yankouskia/localize-react)
24
+ [![bundle](https://img.shields.io/bundlejs/size/localize-react?style=flat-square&color=4c1&label=brotli)](https://bundlejs.com/?q=localize-react)
25
+ [![types](https://img.shields.io/npm/types/localize-react?style=flat-square&logo=typescript)](https://github.com/yankouskia/localize-react/blob/master/src/types.ts)
26
+ [![license](https://img.shields.io/npm/l/localize-react?style=flat-square&color=blue)](LICENSE)
14
27
 
28
+ </div>
15
29
 
16
- ## Installation
30
+ ---
17
31
 
18
- npm:
32
+ ```tsx
33
+ import { LocalizationProvider, useLocalize } from 'localize-react';
19
34
 
20
- ```sh
21
- npm install localize-react --save
22
- ```
35
+ const translations = {
36
+ en: { hello: 'Hi {{name}}!' },
37
+ es: { hello: '¡Hola {{name}}!' },
38
+ ja: { hello: '{{name}}さん、こんにちは!' },
39
+ };
23
40
 
24
- yarn:
41
+ function Greeting() {
42
+ const { translate } = useLocalize();
43
+ return <h1>{translate('hello', { name: 'Alex' })}</h1>;
44
+ }
25
45
 
26
- ```sh
27
- yarn add localize-react
46
+ export default function App() {
47
+ return (
48
+ <LocalizationProvider locale="en" translations={translations}>
49
+ <Greeting />
50
+ </LocalizationProvider>
51
+ );
52
+ }
28
53
  ```
29
54
 
30
- ## API
31
-
32
- ### Provider & Consumer
55
+ That's the whole API. Three exports, no plugins, no extraction toolchain. Ship it.
33
56
 
34
- `LocalizationProvider` is used to provide data for translations into React context. The root application component should be wrapped into `LocalizationProvider`. Component has the next props:
35
- - `children` - children to render
36
- - `locale` - [OPTIONAL] locale to be used for translations. If locale is not specified regular translations object will be used as map of `{ key: translations }`
37
- - `translations` - object with translations
38
- - `disableCache` - boolean variable to disable cache on runtime (`false` by default). Setting this to `true` could affect runtime performance, but could be useful for development.
57
+ ---
39
58
 
40
- Example:
59
+ ## ✨ Why?
41
60
 
42
- ```js
43
- import React from 'react';
44
- import ReactDOM from 'react-dom';
45
- import { LocalizationConsumer, LocalizationProvider } from 'localize-react';
46
-
47
- const TRANSLATIONS = {
48
- en: {
49
- name: 'Alex',
50
- },
51
- };
52
-
53
- const App = () => (
54
- <LocalizationProvider
55
- disableCache
56
- locale="en"
57
- translations={TRANSLATIONS}
58
- >
59
- <LocalizationConsumer>
60
- {({ translate }) => translate('name')}
61
- </LocalizationConsumer>
62
- </LocalizationProvider>
63
- );
64
-
65
- ReactDOM.render(<App />, node); // "Alex" will be rendered
66
- ```
61
+ Most React i18n libraries are 30–80 kB and bring opinions about plural rules, ICU MessageFormat, async loading, and TMS workflows. **`localize-react` is the smallest thing that works.**
67
62
 
68
- ### Message
63
+ It does exactly what a frontend most often needs:
69
64
 
70
- `Message` component is used to provide translated message by specified key, which should be passed via props. Component has the next props:
71
- - `descriptor` - translation key (descriptor)
72
- - `defaultMessage` - message to be used in case translation is not provided (values object are applied to default message as well)
73
- - `values` - possible values to use with template string (Template should be passed in next format: `Hello {{name}}`)
65
+ - A nested translations tree, keyed by locale.
66
+ - Dot-path lookups (`'cart.summary'`).
67
+ - `{{name}}`-style interpolation.
68
+ - A graceful fallback when keys are missing.
74
69
 
75
- Example:
70
+ For everything else (plurals, currency, dates) — reach for [the platform](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl): `Intl.PluralRules`, `Intl.NumberFormat`, `Intl.DateTimeFormat`. Free, fast, already in the browser. See the [Intl formatters recipe](https://yankouskia.github.io/localize-react/docs/recipes/intl-formatters).
76
71
 
77
- ```js
78
- import React from 'react';
79
- import ReactDOM from 'react-dom';
80
- import { LocalizationProvider, Message } from 'localize-react';
72
+ ## 📦 Specs
81
73
 
82
- const TRANSLATIONS = {
83
- en: {
84
- name: 'Alex',
85
- },
86
- };
74
+ | Property | Value |
75
+ | -------------------- | ------------------------------------------------------ |
76
+ | Bundle (brotli) | **916 B** ESM · 989 B CJS |
77
+ | Runtime dependencies | **0** |
78
+ | Source | Strict **TypeScript 6** |
79
+ | Module formats | ESM + CJS with proper `exports`/`types` conditions |
80
+ | Tree-shaking | `sideEffects: false` |
81
+ | Peer range | React `>= 16.8 < 20` (tested in CI through React 19) |
82
+ | Node | `>= 20.19` (CI on 20, 22, 24 × Linux/macOS/Windows) |
83
+ | Test coverage | **100 %** statements · 100 % functions · 98 % branches |
84
+ | Type-checked exports | Validated by `publint` + `@arethetypeswrong/cli` in CI |
87
85
 
88
- const App = () => (
89
- <LocalizationProvider
90
- locale="en"
91
- translations={TRANSLATIONS}
92
- >
93
- <Message descriptor="name" />
94
- </LocalizationProvider>
95
- );
86
+ ## 🚀 Install
96
87
 
97
- ReactDOM.render(<App />, node); // "Alex" will be rendered
88
+ ```sh
89
+ npm install localize-react
90
+ # or: pnpm add localize-react · yarn add localize-react · bun add localize-react
98
91
  ```
99
92
 
100
- To use with templates:
93
+ ## ⚡️ Quickstart
101
94
 
102
- ```js
103
- import React from 'react';
104
- import ReactDOM from 'react-dom';
105
- import { LocalizationProvider, Message } from 'localize-react';
95
+ ### 1. Define translations
106
96
 
107
- const TRANSLATIONS = {
97
+ ```ts
98
+ export const translations = {
108
99
  en: {
109
- name: 'Hello, {{name}}!',
100
+ greeting: { hello: 'Hi {{name}}!' },
101
+ cart: { summary: '{{count}} items, {{total}} total' },
110
102
  },
111
- };
112
-
113
- const App = () => (
114
- <LocalizationProvider
115
- locale="en"
116
- translations={TRANSLATIONS}
117
- >
118
- <Message descriptor="name" values={{ name: 'Alex' }} />
119
- </LocalizationProvider>
120
- );
121
-
122
- ReactDOM.render(<App />, node); // "Alex" will be rendered
123
- ```
124
-
125
- To use with default message:
126
-
127
- ```js
128
- import React from 'react';
129
- import ReactDOM from 'react-dom';
130
- import { LocalizationProvider, Message } from 'localize-react';
131
-
132
- const TRANSLATIONS = {
133
- en: {},
134
- };
135
-
136
- const App = () => (
137
- <LocalizationProvider
138
- locale="en"
139
- translations={TRANSLATIONS}
140
- >
141
- <Message
142
- descriptor="name"
143
- defaultMessage="Hello, {{name}}!"
144
- values={{ name: 'Alex' }}
145
- />
146
- </LocalizationProvider>
147
- );
148
-
149
- ReactDOM.render(<App />, node); // "Alex" will be rendered
150
- ```
151
-
152
- ### useLocalize
153
-
154
- `useLocalize` hook is used to provide localization context, which can be used for translation.
155
-
156
- **NOTE**
157
-
158
- Keep in mind, that hooks are not supported in class components!
159
-
160
- Example:
161
-
162
- ```js
163
- import React from 'react';
164
- import ReactDOM from 'react-dom';
165
- import { LocalizationProvider, useLocalize } from 'localize-react';
166
-
167
- const TRANSLATIONS = {
168
- en: {
169
- name: 'Alex',
103
+ es: {
104
+ greeting: { hello: '¡Hola {{name}}!' },
105
+ cart: { summary: '{{count}} artículos, {{total}} total' },
170
106
  },
171
- };
107
+ } as const;
108
+ ```
172
109
 
173
- function Test() {
174
- const { translate } = useLocalize();
110
+ ### 2. Mount the provider
175
111
 
176
- return translate('name');
177
- }
178
-
179
- const App = () => {
112
+ ```tsx
113
+ import { LocalizationProvider } from 'localize-react';
114
+ import { translations } from './i18n/translations';
180
115
 
116
+ export function App() {
181
117
  return (
182
- <LocalizationProvider
183
- locale="en"
184
- translations={TRANSLATIONS}
185
- >
186
- <Test />
118
+ <LocalizationProvider locale="en" translations={translations}>
119
+ <Shell />
187
120
  </LocalizationProvider>
188
121
  );
189
122
  }
190
-
191
- ReactDOM.render(<App />, node); // "Alex" will be rendered
192
123
  ```
193
124
 
194
- ### Templates
125
+ ### 3. Translate, two ways
195
126
 
196
- It's possible to use templates inside translation strings with highlighting templates using double curly braces. To pass correpospondent values:
127
+ ```tsx
128
+ import { Message, useLocalize } from 'localize-react';
197
129
 
198
- ```js
199
- const translation = translate('My name is {{name}}. I am {{age}}', { name: 'Alex', age: 25 });
200
- ```
201
-
202
- Or with React component:
203
-
204
- ```js
205
- <Message descriptor="My name is {{name}}. I am {{age}}" values={{ name: 'Alex', age: 25 }} />
206
- ```
207
-
208
- ### contextType
209
-
210
- Alternative way of usage inside class components:
211
-
212
- ```js
213
- import React from 'react';
214
- import { LocalizationContext, LocalizationProvider } from 'localize-react';
215
-
216
- const TRANSLATIONS = {
217
- en: {
218
- name: 'Alex',
219
- },
220
- };
221
-
222
-
223
- class Translation extends React.PureComponent {
224
- render() {
225
- return (
226
- <span>
227
- {this.context.translate('name')}
228
- </span>
229
- )
230
- }
130
+ // Hook
131
+ function Cart() {
132
+ const { translate } = useLocalize();
133
+ return <p>{translate('cart.summary', { count: 3, total: '$42.00' })}</p>;
231
134
  }
232
135
 
233
- Translation.contextType = LocalizationContext;
234
-
235
- const App = () => {
136
+ // Component
137
+ function CartHeader() {
236
138
  return (
237
- <LocalizationProvider
238
- locale="en"
239
- translations={TRANSLATIONS}
240
- >
241
- <Translation />
242
- </LocalizationProvider>
139
+ <h1>
140
+ <Message descriptor="greeting.hello" values={{ name: 'Alex' }} />
141
+ </h1>
243
142
  );
244
143
  }
245
-
246
- ReactDOM.render(<App />, node); // "Alex" will be rendered
247
144
  ```
248
145
 
249
- ### locale
250
- Locale could be passed in short or long option.
251
-
146
+ That's the whole story. Full docs at **[yankouskia.github.io/localize-react](https://yankouskia.github.io/localize-react)**.
252
147
 
253
- Valid examples:
148
+ ## 🧠 Concept in one screen
254
149
 
255
- ```
256
- en-us
257
- EN_US
258
- en
259
- eN-uS
260
- ```
150
+ | Operation | API |
151
+ | ------------------------ | -------------------------------------------------------- |
152
+ | Mount translations | `<LocalizationProvider locale translations>` |
153
+ | Translate (hook) | `useLocalize().translate(descriptor, values?, default?)` |
154
+ | Translate (component) | `<Message descriptor values? defaultMessage? />` |
155
+ | Switch locale at runtime | Re-render with a new `locale` prop |
156
+ | Missing key | Renders `defaultMessage ?? descriptor` (never throws) |
157
+ | Nested lookup | `translate('a.b.c')` walks the tree |
158
+ | Interpolation | `{{token}}` — literal replacement, safe with regex chars |
159
+ | Locale normalization | `En-US` → `en_us` → `en` |
261
160
 
262
- ### translations
263
- Translations could be passed in any object form (plain or with deep properties)
161
+ ## 📚 Recipes
264
162
 
265
- Valid examples:
163
+ Real-world patterns, fully documented on the site:
266
164
 
267
- ```js
268
- const translations = {
269
- n: {
270
- a: {
271
- m: {
272
- e: 'Alex',
273
- },
274
- },
275
- },
276
- },
277
- ```
165
+ - [Switching locales (URL / cookie / localStorage)](https://yankouskia.github.io/localize-react/docs/recipes/switching-locales)
166
+ - [Lazy-loading translation chunks with `React.use()`](https://yankouskia.github.io/localize-react/docs/recipes/lazy-loading)
167
+ - [Next.js (App Router) — `[locale]` segments + middleware](https://yankouskia.github.io/localize-react/docs/recipes/nextjs)
168
+ - [Vite + React Router — `import.meta.glob`](https://yankouskia.github.io/localize-react/docs/recipes/vite)
169
+ - [Testing — RTL render helper, descriptor coverage](https://yankouskia.github.io/localize-react/docs/recipes/testing)
170
+ - [Intl formatters — plurals, currency, dates, lists](https://yankouskia.github.io/localize-react/docs/recipes/intl-formatters)
278
171
 
279
- You could use key with dot delimiter to access that property:
172
+ ## 🥊 How it compares
280
173
 
281
- ```js
282
- <Message descriptor="n.a.m.e" /> // will print "Alex"
283
- ```
284
-
285
- If there is no exact match in translations, then the value of locale will be sanitized and formatted to **lower_case_separate_by_underscore**. Make sure you provide translations object with keys in this format. If translations for long locale will not be found, and translations will be found for shorten alternative - that version will be used
174
+ | | **localize-react** | react-i18next | react-intl | lingui |
175
+ | -------------------- | ------------------ | ------------- | ---------- | --------- |
176
+ | Bundle (brotli) | **< 1 kB** | ~17 kB | ~38 kB | ~9 kB |
177
+ | Runtime deps | **0** | several | several | one macro |
178
+ | Pluralization (CLDR) | Use `Intl` | ✅ | (ICU) | (ICU) |
179
+ | Number / date format | Use `Intl` | Optional | ✅ | ✅ |
180
+ | ICU MessageFormat | ❌ | ✅ | ✅ | ✅ |
181
+ | Lazy locale loading | DIY | ✅ | ✅ | ✅ |
182
+ | Auto extraction | ❌ | ✅ | CLI | CLI |
183
+ | TypeScript-first | ✅ | ✅ | ✅ | ✅ |
184
+ | Learning curve | **Tiny** | Medium | Medium | Medium |
286
185
 
287
- ## Restriction
186
+ Use `localize-react` when you want a hook + a tag. Reach for the others when CLDR plurals, ICU MessageFormat, or a TMS workflow matter — they're all great at what they do.
288
187
 
289
- At least `React 16.8.0` is required to use this library, because new React Context API & React Hooks
188
+ ## 🛡 Production-ready
290
189
 
291
- ## Contributing
190
+ - **Types ship inside the package** — no `@types/localize-react` to chase.
191
+ - **Provenance attestation** on every published version (npm OIDC trusted publishing).
192
+ - **CodeQL** runs on every PR; CI matrix exercises Node 20/22/24 × Linux/macOS/Windows.
193
+ - **Size budget enforced** — < 2 kB ESM, < 2.5 kB CJS, checked on every PR with [`size-limit`](https://github.com/ai/size-limit).
194
+ - **No dynamic require, no eval, no regex from user input** — interpolation is literal `replaceAll`.
292
195
 
293
- `localize-react` is open-source library, opened for contributions
196
+ ## 📖 v1 v2
294
197
 
295
- ### Tests
198
+ The runtime API is unchanged. v2 modernizes the toolchain (strict TS 6, dual ESM+CJS, React 19 peer, GitHub Actions + Changesets). One soft TypeScript regression in `exactOptionalPropertyTypes` mode — see the [migration guide](https://yankouskia.github.io/localize-react/docs/migration-v2).
296
199
 
297
- **Current test coverage is 100%**
200
+ ## 🤝 Contributing
298
201
 
299
- `jest` is used for tests. To run tests:
202
+ PRs welcome. See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the setup + release flow. Security reports: please open a private security advisory rather than a public issue.
300
203
 
301
- ```sh
302
- yarn test
303
- ```
204
+ If you'd like to support the project, [sponsoring](https://github.com/sponsors/yankouskia) helps a lot.
304
205
 
305
- ### License
206
+ ## 📄 License
306
207
 
307
- localize-react is [MIT licensed](https://github.com/yankouskia/localize-react/blob/master/LICENSE)
208
+ [MIT](./LICENSE) © Aliaksandr Yankouski
package/dist/index.cjs ADDED
@@ -0,0 +1,133 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ // src/Provider.tsx
7
+
8
+ // src/helpers.ts
9
+ var NO_TRANSLATION_WARNING_MESSAGE = "[LOCALIZE-REACT]: There are no translations for specified locale";
10
+ var NO_TEMPLATE_VALUE_MESSAGE = "[LOCALIZE-REACT] Looks like template is being used, but no value passed for ";
11
+ var PARSE_TEMPLATE_REGEXP = /\{\{([^{}]+)\}\}/g;
12
+ var translationCache = /* @__PURE__ */ Object.create(null);
13
+ function sanitizeLocale(locale, translations) {
14
+ if (!locale) return null;
15
+ if (typeof translations[locale] === "object") return locale;
16
+ const normalized = locale.toLowerCase().replaceAll("-", "_");
17
+ if (typeof translations[normalized] === "object") return normalized;
18
+ const short = normalized.split("_")[0];
19
+ if (short && typeof translations[short] === "object") return short;
20
+ console.warn(NO_TRANSLATION_WARNING_MESSAGE, locale);
21
+ return locale;
22
+ }
23
+ function memoize(fn) {
24
+ return (descriptor, values, defaultMessage) => {
25
+ const cacheKey = values ? JSON.stringify(values) + descriptor + (defaultMessage ?? "") : descriptor + (defaultMessage ?? "");
26
+ const cached = translationCache[cacheKey];
27
+ if (cached !== void 0) return cached;
28
+ const output = fn(descriptor, values, defaultMessage);
29
+ translationCache[cacheKey] = output;
30
+ return output;
31
+ };
32
+ }
33
+ function clearCache() {
34
+ translationCache = /* @__PURE__ */ Object.create(null);
35
+ }
36
+ function transformToPairs(templates, values) {
37
+ return templates.map((token) => {
38
+ const placeholderKey = token.slice(2, -2);
39
+ if (Object.hasOwn(values, placeholderKey)) {
40
+ return [token, values[placeholderKey]];
41
+ }
42
+ console.warn(NO_TEMPLATE_VALUE_MESSAGE, token);
43
+ return [token, token];
44
+ });
45
+ }
46
+ function buildTranslation(source, values) {
47
+ if (!values) return source;
48
+ if (Object.keys(values).length === 0) return source;
49
+ const templates = source.match(PARSE_TEMPLATE_REGEXP);
50
+ if (!templates || templates.length === 0) return source;
51
+ const pairs = transformToPairs(templates, values);
52
+ let result = source;
53
+ for (const [token, value] of pairs) {
54
+ result = result.replaceAll(token, String(value));
55
+ }
56
+ return result;
57
+ }
58
+ var DEFAULT_CONTEXT_VALUE = {
59
+ locale: void 0,
60
+ translate: (descriptor, _values, defaultMessage) => defaultMessage ?? descriptor,
61
+ translations: {}
62
+ };
63
+ var LocalizationContext = react.createContext(
64
+ DEFAULT_CONTEXT_VALUE
65
+ );
66
+ LocalizationContext.displayName = "LocalizationContext";
67
+ function LocalizationProvider({
68
+ children,
69
+ disableCache = false,
70
+ locale,
71
+ translations = {}
72
+ }) {
73
+ const pureTranslations = react.useMemo(() => {
74
+ const sanitizedLocale = sanitizeLocale(locale, translations);
75
+ const localeTranslations = sanitizedLocale === null ? translations : translations[sanitizedLocale];
76
+ return localeTranslations && typeof localeTranslations === "object" ? localeTranslations : {};
77
+ }, [locale, translations]);
78
+ react.useEffect(() => {
79
+ clearCache();
80
+ }, [locale, translations]);
81
+ const translate = react.useMemo(() => {
82
+ const pureTranslate = (descriptor, values, defaultMessage) => {
83
+ if (!descriptor) return defaultMessage ?? descriptor;
84
+ const fallback = typeof defaultMessage === "string" ? defaultMessage : descriptor;
85
+ const direct = pureTranslations[descriptor];
86
+ if (typeof direct === "string") {
87
+ return values ? buildTranslation(direct, values) : direct;
88
+ }
89
+ const segments = descriptor.split(".");
90
+ if (segments.length === 1) {
91
+ return buildTranslation(fallback, values);
92
+ }
93
+ const resolved = resolveNestedKey(pureTranslations, segments);
94
+ return typeof resolved === "string" ? buildTranslation(resolved, values) : buildTranslation(fallback, values);
95
+ };
96
+ return disableCache ? pureTranslate : memoize(pureTranslate);
97
+ }, [disableCache, pureTranslations]);
98
+ const value = react.useMemo(
99
+ () => ({ locale, translate, translations }),
100
+ [locale, translate, translations]
101
+ );
102
+ return /* @__PURE__ */ jsxRuntime.jsx(LocalizationContext.Provider, { value, children });
103
+ }
104
+ var LocalizationConsumer = LocalizationContext.Consumer;
105
+ function resolveNestedKey(tree, segments) {
106
+ let cursor = tree;
107
+ for (const segment of segments) {
108
+ if (cursor === void 0 || typeof cursor === "string") return void 0;
109
+ cursor = cursor[segment];
110
+ }
111
+ return typeof cursor === "string" ? cursor : void 0;
112
+ }
113
+ function useLocalize() {
114
+ return react.useContext(LocalizationContext);
115
+ }
116
+
117
+ // src/Message.tsx
118
+ function Message({
119
+ defaultMessage,
120
+ descriptor,
121
+ values
122
+ }) {
123
+ const { translate } = useLocalize();
124
+ return translate(descriptor, values, defaultMessage);
125
+ }
126
+
127
+ exports.LocalizationConsumer = LocalizationConsumer;
128
+ exports.LocalizationContext = LocalizationContext;
129
+ exports.LocalizationProvider = LocalizationProvider;
130
+ exports.Message = Message;
131
+ exports.useLocalize = useLocalize;
132
+ //# sourceMappingURL=index.cjs.map
133
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/helpers.ts","../src/Provider.tsx","../src/use-localize.ts","../src/Message.tsx"],"names":["createContext","useMemo","useEffect","jsx","useContext"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { TemplateValues, Translate, Translations } from './types.js';\n\n/** Warning logged when no locale entry matches the requested locale. */\nexport const NO_TRANSLATION_WARNING_MESSAGE =\n '[LOCALIZE-REACT]: There are no translations for specified locale';\n\n/** Warning logged when a `{{placeholder}}` has no matching value. */\nexport const NO_TEMPLATE_VALUE_MESSAGE =\n '[LOCALIZE-REACT] Looks like template is being used, but no value passed for ';\n\n/**\n * Pattern matching `{{name}}`-style mustaches. Anchored to avoid\n * consuming adjacent braces (`{{{a}}}` only matches `{{a}}`).\n */\nexport const PARSE_TEMPLATE_REGEXP = /\\{\\{([^{}]+)\\}\\}/g;\n\n/**\n * Module-scoped translation cache. Kept module-scoped (rather than\n * provider-scoped) for behavioural parity with v1.x; see ADR-008.\n */\nlet translationCache: Record<string, string> = Object.create(null) as Record<\n string,\n string\n>;\n\n/**\n * Normalize a locale string against the available translation keys.\n *\n * - Returns `null` if no locale was supplied.\n * - Returns the input as-is if `translations[locale]` is an object.\n * - Lowercases and replaces dashes with underscores, then retries.\n * - Falls back to the leading segment (`en_us` → `en`).\n * - If nothing matches, logs a warning and returns the original input.\n */\nexport function sanitizeLocale(\n locale: string | undefined,\n translations: Translations,\n): string | null {\n if (!locale) return null;\n\n if (typeof translations[locale] === 'object') return locale;\n\n const normalized = locale.toLowerCase().replaceAll('-', '_');\n if (typeof translations[normalized] === 'object') return normalized;\n\n const short = normalized.split('_')[0];\n if (short && typeof translations[short] === 'object') return short;\n\n console.warn(NO_TRANSLATION_WARNING_MESSAGE, locale);\n return locale;\n}\n\n/**\n * Returns a memoizing wrapper around {@link fn}. The cache key combines\n * the descriptor, the JSON-serialized values, and the default message.\n */\nexport function memoize(fn: Translate): Translate {\n return (descriptor, values, defaultMessage) => {\n const cacheKey = values\n ? JSON.stringify(values) + descriptor + (defaultMessage ?? '')\n : descriptor + (defaultMessage ?? '');\n\n const cached = translationCache[cacheKey];\n if (cached !== undefined) return cached;\n\n const output = fn(descriptor, values, defaultMessage);\n translationCache[cacheKey] = output;\n return output;\n };\n}\n\n/** Reset the module-level translation cache. */\nexport function clearCache(): void {\n translationCache = Object.create(null) as Record<string, string>;\n}\n\n/**\n * Pair each `{{key}}` template token with the matching value from\n * {@link values}. Tokens with no value are paired with themselves and\n * logged.\n */\nexport function transformToPairs(\n templates: readonly string[],\n values: TemplateValues,\n): readonly (readonly [string, string | number])[] {\n return templates.map((token) => {\n const placeholderKey = token.slice(2, -2);\n if (Object.hasOwn(values, placeholderKey)) {\n return [token, values[placeholderKey]!] as const;\n }\n console.warn(NO_TEMPLATE_VALUE_MESSAGE, token);\n return [token, token] as const;\n });\n}\n\n/**\n * Replace every `{{placeholder}}` token in {@link source} with its\n * corresponding entry from {@link values}. Uses literal string\n * replacement (not `new RegExp(token)`) so values containing regex\n * meta-characters are safe — see MIGRATION_PLAN.md risk R4.\n */\nexport function buildTranslation(\n source: string,\n values?: TemplateValues,\n): string {\n if (!values) return source;\n if (Object.keys(values).length === 0) return source;\n\n const templates = source.match(PARSE_TEMPLATE_REGEXP);\n if (!templates || templates.length === 0) return source;\n\n const pairs = transformToPairs(templates, values);\n\n let result = source;\n for (const [token, value] of pairs) {\n result = result.replaceAll(token, String(value));\n }\n return result;\n}\n","import { createContext, useEffect, useMemo } from 'react';\nimport type { JSX } from 'react';\n\nimport {\n buildTranslation,\n clearCache,\n memoize,\n sanitizeLocale,\n} from './helpers.js';\nimport type {\n LocalizationContextValue,\n LocalizationProviderProps,\n TemplateValues,\n Translate,\n Translations,\n} from './types.js';\n\nconst DEFAULT_CONTEXT_VALUE: LocalizationContextValue = {\n locale: undefined,\n translate: (descriptor, _values, defaultMessage) =>\n defaultMessage ?? descriptor,\n translations: {},\n};\n\n/**\n * React context carrying the active translate function. Prefer\n * {@link useLocalize} or {@link LocalizationConsumer} in new code;\n * `static contextType = LocalizationContext` is supported for legacy\n * class components.\n */\nexport const LocalizationContext = createContext<LocalizationContextValue>(\n DEFAULT_CONTEXT_VALUE,\n);\n\nLocalizationContext.displayName = 'LocalizationContext';\n\n/**\n * Provides translation data to descendants. Wrap the root of your app\n * (or the subtree that needs translations) with this component.\n *\n * @example\n * ```tsx\n * <LocalizationProvider locale=\"en\" translations={messages}>\n * <App />\n * </LocalizationProvider>\n * ```\n */\nexport function LocalizationProvider({\n children,\n disableCache = false,\n locale,\n translations = {},\n}: LocalizationProviderProps): JSX.Element {\n const pureTranslations = useMemo<Translations>(() => {\n const sanitizedLocale = sanitizeLocale(locale, translations);\n const localeTranslations: Translations | string | undefined =\n sanitizedLocale === null ? translations : translations[sanitizedLocale];\n return localeTranslations && typeof localeTranslations === 'object'\n ? localeTranslations\n : {};\n }, [locale, translations]);\n\n useEffect(() => {\n clearCache();\n }, [locale, translations]);\n\n const translate = useMemo<Translate>(() => {\n const pureTranslate: Translate = (descriptor, values, defaultMessage) => {\n if (!descriptor) return defaultMessage ?? descriptor;\n\n const fallback =\n typeof defaultMessage === 'string' ? defaultMessage : descriptor;\n\n const direct = pureTranslations[descriptor];\n if (typeof direct === 'string') {\n return values ? buildTranslation(direct, values) : direct;\n }\n\n const segments = descriptor.split('.');\n if (segments.length === 1) {\n return buildTranslation(fallback, values);\n }\n\n const resolved = resolveNestedKey(pureTranslations, segments);\n return typeof resolved === 'string'\n ? buildTranslation(resolved, values)\n : buildTranslation(fallback, values);\n };\n\n return disableCache ? pureTranslate : memoize(pureTranslate);\n }, [disableCache, pureTranslations]);\n\n const value = useMemo<LocalizationContextValue>(\n () => ({ locale, translate, translations }),\n [locale, translate, translations],\n );\n\n return (\n <LocalizationContext.Provider value={value}>\n {children}\n </LocalizationContext.Provider>\n );\n}\n\n/**\n * Render-prop consumer for {@link LocalizationContext}. Prefer\n * {@link useLocalize} in function components.\n */\nexport const LocalizationConsumer = LocalizationContext.Consumer;\n\nfunction resolveNestedKey(\n tree: Translations,\n segments: readonly string[],\n): string | undefined {\n let cursor: string | Translations | undefined = tree;\n for (const segment of segments) {\n if (cursor === undefined || typeof cursor === 'string') return undefined;\n cursor = cursor[segment];\n }\n return typeof cursor === 'string' ? cursor : undefined;\n}\n\n// `TemplateValues` re-export keeps the file self-contained for the\n// snapshot of the public type surface; do not remove without updating\n// `src/index.ts`.\nexport type { TemplateValues };\n","import { useContext } from 'react';\n\nimport { LocalizationContext } from './Provider.js';\nimport type { LocalizationContextValue } from './types.js';\n\n/**\n * Read the current {@link LocalizationContextValue} from the nearest\n * {@link LocalizationProvider}. Throws nothing if there is no provider —\n * a no-op `translate` (returns the descriptor) is used instead.\n *\n * @example\n * ```tsx\n * function Greeting() {\n * const { translate } = useLocalize();\n * return <h1>{translate('greeting.hello', { name: 'World' })}</h1>;\n * }\n * ```\n */\nexport function useLocalize(): LocalizationContextValue {\n return useContext(LocalizationContext);\n}\n","import type { MessageProps } from './types.js';\nimport { useLocalize } from './use-localize.js';\n\n/**\n * Render a translated string by descriptor.\n *\n * Equivalent to inlining `useLocalize().translate(descriptor, values,\n * defaultMessage)`. Use this when you want a component instead of a\n * hook call (e.g. inside `<button title={...} />` is not possible, but\n * `<button title=\"...\"><Message .../></button>` reads fine in JSX).\n *\n * @example\n * ```tsx\n * <Message descriptor=\"greeting.hello\" values={{ name: 'Alex' }} />\n * ```\n */\nexport function Message({\n defaultMessage,\n descriptor,\n values,\n}: MessageProps): string {\n const { translate } = useLocalize();\n return translate(descriptor, values, defaultMessage);\n}\n"]}