locon 1.5.0 → 1.6.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/CHANGELOG.md ADDED
@@ -0,0 +1,33 @@
1
+ # Changelog
2
+
3
+ ## 1.6.0
4
+
5
+ ### Added
6
+
7
+ - `locon/core`: React-free and React Native-free ESM/CommonJS entry for Node and
8
+ plain TypeScript, exporting `createTranslator`, `intlLocale`, `createLocon`
9
+ and `matchLocale` with types.
10
+ - `locon/react`: React 19+ adapter with `LoconProvider` and `useLocon`, without
11
+ importing React Native.
12
+ - Lazy per-locale resource loaders, JSON-module support, successful-load cache,
13
+ parallel request deduplication and ordered locale preference matching.
14
+ - Async locale switching with last-selection-wins behavior, loading/error state,
15
+ preservation of the current locale on failure and retry support.
16
+
17
+ ### Compatibility
18
+
19
+ - The existing `locon` React Native entry, provider, hook and `LText` API remain
20
+ available. Native device language detection and RTL behavior are unchanged.
21
+ - React and React Native peers are optional at package installation time; each
22
+ adapter still requires its runtime. Node core needs neither framework.
23
+ - Lazy lookups use only already loaded resources for fallback. Prefer stable
24
+ keys when the source-language resource has not been loaded. Source-phrase
25
+ reverse lookup continues to work when that resource is available.
26
+ - Clean the output directory once before building all entries to avoid parallel
27
+ builds removing each other's output.
28
+
29
+ ### Verification
30
+
31
+ - Existing native tests and built-entry smoke tests are retained.
32
+ - Added coverage for lazy loads, caching, concurrent switching, failed load
33
+ retry, platform-free Node imports and React updates after resource loading.
package/README.md CHANGED
@@ -6,9 +6,19 @@
6
6
 
7
7
  [Website](https://locon.dev) · [npm](https://www.npmjs.com/package/locon) · [GitHub](https://github.com/loconapp/locon)
8
8
 
9
- `locon` is a small, typed i18n helper for React Native.
10
- It provides a context provider, a hook, and a simple text component to keep
11
- your translations flat, explicit, and easy to use.
9
+ `locon` is a small, typed localization library for **React Native, React and Node**.
10
+ Keep flat JSON resources, translate with interpolation and CLDR plurals, and
11
+ load browser languages only when they are selected.
12
+
13
+ | Runtime | Import | Resources |
14
+ | --- | --- | --- |
15
+ | React Native / Expo | `locon` | Existing provider, hook and `<LText>` API |
16
+ | React web (React 19+) | `locon/react` | Async loaders, provider and reactive hook |
17
+ | Node / plain TypeScript | `locon/core` | Standalone translator and lazy resource store |
18
+
19
+ **New in 1.6:** platform-specific entries and lazy resources. See
20
+ [React (web), Node and lazy resources](#react-web-node-and-lazy-resources).
21
+ The existing React Native entry remains compatible.
12
22
 
13
23
  Basic usage: use `<LText>` instead of `<Text>`. Put plain text in your project language (or the default language) inside and it will be localized automatically.
14
24
 
@@ -24,7 +34,9 @@ function Example() {
24
34
 
25
35
  ### Features
26
36
 
27
- - **React Native–first**: tiny API, no global singletons
37
+ - **React Native–first**: existing native API, plus independent React web and Node entries
38
+ - **Lazy locale resources** for web: load the selected language, cache successful loads, deduplicate parallel requests
39
+ - **Safe async switching**: the last selection wins; a failed load preserves the current language
28
40
  - **Expo-ready**: detects the device language through `expo-localization`, no native linking
29
41
  - **Context provider** with pluggable assets (per-locale JSON)
30
42
  - **Device language auto-detection** (optional)
@@ -54,7 +66,13 @@ yarn add locon
54
66
  bun add locon
55
67
  ```
56
68
 
57
- **Peer dependencies**
69
+ **Peer dependencies by entry**
70
+
71
+ `locon/core` requires neither React nor React Native. `locon/react` requires
72
+ React 19+; add `react-dom` to your web application as usual. The package marks
73
+ React and React Native peers optional so Node users do not install them.
74
+
75
+ For the existing React Native entry, your app supplies:
58
76
 
59
77
  - `react >= 19.0.0`
60
78
  - `react-native >= 0.70.0`
@@ -208,11 +226,7 @@ function LanguageSwitcher() {
208
226
 
209
227
  return (
210
228
  <View>
211
- {/* Localised value by key */}
212
- <Text>
213
- {l('current_language')}: {currentLocale}
214
- </Text>
215
- {/* or by value in project- or default language */}
229
+ {/* Resolve by the value written in the project language. */}
216
230
  <Text>
217
231
  {l('Current language')}: {currentLocale}
218
232
  </Text>
@@ -349,7 +363,7 @@ Unknown tokens are left as-is rather than blanked:
349
363
  ```
350
364
 
351
365
  ```tsx
352
- l('saved_radius', { params: { m: 150 } }) // → '150 m radius · saved'
366
+ l('{m} m radius · saved', { params: { m: 150 } }) // → '150 m radius · saved'
353
367
  ```
354
368
 
355
369
  **Fallback** — `fallback` is returned only when no locale resolves the input.
@@ -411,7 +425,7 @@ fallback when a locale-specific category is absent.
411
425
  ```
412
426
 
413
427
  ```tsx
414
- l('day', { count: 5 }) // en → '5 days' · ru → '5 дней'
428
+ l('{count} days', { count: 5 }) // en → '5 days' · ru → '5 дней'
415
429
  ```
416
430
 
417
431
  There is deliberately no generic `_plural` suffix: most languages do not have
@@ -452,7 +466,7 @@ no React tree: generating a PDF or spreadsheet, composing an e-mail, or
452
466
  building a notification from a background task.
453
467
 
454
468
  ```ts
455
- import { createTranslator } from 'locon'
469
+ import { createTranslator } from 'locon/core'
456
470
  import de from './assets/i18n/de.json'
457
471
  import tr from './assets/i18n/tr.json'
458
472
 
@@ -469,9 +483,13 @@ l('Arbeitszeit') // → 'Çalışma süresi'
469
483
  This is what lets an app export a report in a language the interface is not
470
484
  currently in — a user browsing in German can still produce a Turkish report.
471
485
 
472
- #### `resolveLocale()` — the provider's locale precedence
486
+ #### `resolveLocale()` — the native provider's locale precedence
473
487
 
474
- `createTranslator()` needs a locale, and outside React nothing has picked one
488
+ `resolveLocale()` is a helper for the native entry. In Node and React web, use
489
+ `matchLocale(preferences, supportedLocales, defaultLocale)` from `locon/core`
490
+ instead, or pass `languages` to `createLocon`.
491
+
492
+ `createTranslator()` needs a locale, and outside a provider nothing has picked one
475
493
  for you. `resolveLocale()` answers that question with the exact precedence
476
494
  `<Locon />` uses internally: an explicit choice, else the device language, else
477
495
  the default.
@@ -749,8 +767,13 @@ raw text inside `<LText>`.
749
767
 
750
768
  The published package contains:
751
769
 
752
- - `dist/index.js` — one bundled CommonJS file
753
- - `dist/index.d.ts` — types
770
+ - `dist/index.js`, `dist/index.d.ts` — the existing bundled CommonJS native entry
771
+ - `dist/react.js`, `dist/react.mjs`, and declarations — React web, CommonJS and ESM
772
+ - `dist/core.js`, `dist/core.mjs`, and declarations — Node/plain TypeScript, CommonJS and ESM
773
+
774
+ Use package entry names (`locon`, `locon/react`, `locon/core`) instead of importing
775
+ `dist` files directly. The entry-specific exports keep native dependencies out
776
+ of browser and server bundles.
754
777
 
755
778
  So you can import from `locon` in both JS and TS projects and get type support
756
779
  for every export. Public component/configuration types are importable too:
@@ -769,7 +792,7 @@ import type {
769
792
  } from 'locon'
770
793
  ```
771
794
 
772
- The build is deliberately **CommonJS only, and bundled**. Metro finds optional
795
+ The **React Native entry** is deliberately CommonJS only and bundled. Metro finds optional
773
796
  dependencies by statically reading literal `require()` calls, and esbuild
774
797
  rewrites those into its own shim when emitting ESM — so an ESM build registered
775
798
  nothing and even `react-native` failed to resolve at runtime. Bundling matters
@@ -777,11 +800,49 @@ for the same reason: an unbundled tree emitted extensionless relative imports,
777
800
  which Node resolves to `.js` but Metro tries `.mjs` for first, loading the
778
801
  library twice. Two instances mean two contexts — the provider fills one, every
779
802
  `useLocon()` reads the other, and consumers get untranslated screens with no
780
- error at all. Metro consumes CJS natively, so a second format buys a React
781
- Native library nothing.
803
+ error at all. Metro consumes CJS natively. React web and Node have their own
804
+ independent ESM/CommonJS entries.
782
805
 
783
806
  ---
784
807
 
808
+ ### React (web), Node and lazy resources
809
+
810
+ The existing `locon` entry remains the React Native API. Use `locon/react` in
811
+ React web apps and `locon/core` in Node. These entries never import React Native;
812
+ `locon/core` does not import React either. React 19+ is required by the adapter.
813
+
814
+ ```tsx
815
+ import { createLocon, LoconProvider, useLocon } from 'locon/react'
816
+
817
+ const i18n = createLocon({
818
+ defaultLocale: 'en',
819
+ languages: navigator.languages,
820
+ loaders: {
821
+ en: () => import('./locales/en.json'),
822
+ de: () => import('./locales/de.json'),
823
+ },
824
+ })
825
+ await i18n.setLocale(null) // load just the browser language, before mounting
826
+ // <LoconProvider instance={i18n}><App /></LoconProvider>
827
+ // const { l, currentLocale, loading, error, setLocale } = useLocon()
828
+ // await setLocale('de') // fetch once; successful resources stay cached
829
+ ```
830
+
831
+ `setLocale` resolves `true` when that selection was applied, `false` when a later
832
+ selection won. Failed loads reject, preserve the displayed language and can be
833
+ retried. Parallel loads are deduplicated. Catch rejections in event handlers and
834
+ offer a retry. Local storage and document `lang` remain application decisions.
835
+ Only already loaded resources participate in per-key fallback: the store never
836
+ silently downloads every fallback language. Prefer stable translation keys in
837
+ lazy apps; source-phrase reverse lookup needs the source resources loaded.
838
+
839
+ In Node, create an instance per request, pass the request's language preferences
840
+ explicitly, and await `setLocale` before rendering. For resources already loaded
841
+ in memory, use `createTranslator({ assets, locale })` from `locon/core` directly.
842
+ On SSR, use a separate instance per request and hydrate with the same preloaded
843
+ resources/locale; never share a mutable locale store between requests.
844
+
845
+
785
846
  ### License
786
847
 
787
848
  `locon` is released under the **ISC** license.
@@ -0,0 +1,46 @@
1
+ import { T as TranslateOptions } from './resources-BwFIzg3m.mjs';
2
+ export { L as LocaleSnapshot, a as LoconInstance, R as ResourceLoader, b as ResourceOptions, c as createLocon, m as matchLocale } from './resources-BwFIzg3m.mjs';
3
+
4
+ type Assets = Record<string, Record<string, string>>;
5
+
6
+ /**
7
+ * The resolver behind `l()`, usable without React.
8
+ *
9
+ * The provider builds one of these per render; export pipelines (PDF, XLSX,
10
+ * e-mail) build their own with an explicit locale, so a document can be
11
+ * produced in a language the interface is not currently showing.
12
+ */
13
+
14
+ interface TranslatorConfig {
15
+ assets: Assets;
16
+ locale: string;
17
+ defaultLocale?: string;
18
+ projectLocale?: string;
19
+ }
20
+ type Translator = (assetKey: string, options?: TranslateOptions) => string;
21
+ declare function createTranslator({ assets, locale, defaultLocale, projectLocale }: TranslatorConfig): Translator;
22
+
23
+ /**
24
+ * Builds a BCP-47 tag carrying Unicode extensions, for handing to `Intl`.
25
+ *
26
+ * `locon` has no opinion on which calendar or numbering system your app
27
+ * should use — a Persian journal app wants Jalali dates, a Persian payroll
28
+ * export usually does not. That is a product decision. What the library can
29
+ * do is get the tag syntax right, which naive concatenation does not:
30
+ * appending `-u-ca-gregory` to a locale that already reads `de-u-nu-latn`
31
+ * yields a tag with two `-u-` singletons, and `Intl` throws on it.
32
+ */
33
+ interface IntlExtensions {
34
+ /** Calendar system, e.g. `gregory`, `islamic`, `persian`. */
35
+ calendar?: string;
36
+ /** Numbering system, e.g. `latn`, `arab`, `deva`. */
37
+ numbering?: string;
38
+ }
39
+ /**
40
+ * Returns `locale` with the given Unicode extensions applied, merging into an
41
+ * existing `-u-` section rather than adding a second one. Keys already present
42
+ * in the tag are left alone, so an explicit choice by the caller wins.
43
+ */
44
+ declare function intlLocale(locale: string, extensions?: IntlExtensions): string;
45
+
46
+ export { type Assets, TranslateOptions, type Translator, type TranslatorConfig, createTranslator, intlLocale };
package/dist/core.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { T as TranslateOptions } from './resources-BwFIzg3m.js';
2
+ export { L as LocaleSnapshot, a as LoconInstance, R as ResourceLoader, b as ResourceOptions, c as createLocon, m as matchLocale } from './resources-BwFIzg3m.js';
3
+
4
+ type Assets = Record<string, Record<string, string>>;
5
+
6
+ /**
7
+ * The resolver behind `l()`, usable without React.
8
+ *
9
+ * The provider builds one of these per render; export pipelines (PDF, XLSX,
10
+ * e-mail) build their own with an explicit locale, so a document can be
11
+ * produced in a language the interface is not currently showing.
12
+ */
13
+
14
+ interface TranslatorConfig {
15
+ assets: Assets;
16
+ locale: string;
17
+ defaultLocale?: string;
18
+ projectLocale?: string;
19
+ }
20
+ type Translator = (assetKey: string, options?: TranslateOptions) => string;
21
+ declare function createTranslator({ assets, locale, defaultLocale, projectLocale }: TranslatorConfig): Translator;
22
+
23
+ /**
24
+ * Builds a BCP-47 tag carrying Unicode extensions, for handing to `Intl`.
25
+ *
26
+ * `locon` has no opinion on which calendar or numbering system your app
27
+ * should use — a Persian journal app wants Jalali dates, a Persian payroll
28
+ * export usually does not. That is a product decision. What the library can
29
+ * do is get the tag syntax right, which naive concatenation does not:
30
+ * appending `-u-ca-gregory` to a locale that already reads `de-u-nu-latn`
31
+ * yields a tag with two `-u-` singletons, and `Intl` throws on it.
32
+ */
33
+ interface IntlExtensions {
34
+ /** Calendar system, e.g. `gregory`, `islamic`, `persian`. */
35
+ calendar?: string;
36
+ /** Numbering system, e.g. `latn`, `arab`, `deva`. */
37
+ numbering?: string;
38
+ }
39
+ /**
40
+ * Returns `locale` with the given Unicode extensions applied, merging into an
41
+ * existing `-u-` section rather than adding a second one. Keys already present
42
+ * in the tag are left alone, so an explicit choice by the caller wins.
43
+ */
44
+ declare function intlLocale(locale: string, extensions?: IntlExtensions): string;
45
+
46
+ export { type Assets, TranslateOptions, type Translator, type TranslatorConfig, createTranslator, intlLocale };
package/dist/core.js ADDED
@@ -0,0 +1,352 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/core/index.ts
21
+ var core_exports = {};
22
+ __export(core_exports, {
23
+ createLocon: () => createLocon,
24
+ createTranslator: () => createTranslator_default,
25
+ intlLocale: () => intlLocale_default,
26
+ matchLocale: () => matchLocale
27
+ });
28
+ module.exports = __toCommonJS(core_exports);
29
+
30
+ // src/locon/utils/pluralCategory.ts
31
+ var between = (x, from, to) => x >= from && x <= to;
32
+ var oneIfIntegerOne = ({ i, v }) => i === 1 && v === 0 ? "one" : "other";
33
+ var oneIfOne = ({ n }) => n === 1 ? "one" : "other";
34
+ var oneIfZeroOrOne = ({ i }) => i === 0 || i === 1 ? "one" : "other";
35
+ var onlyOther = () => "other";
36
+ var eastSlavic = ({ i, v }) => {
37
+ if (v !== 0) return "other";
38
+ if (i % 10 === 1 && i % 100 !== 11) return "one";
39
+ if (between(i % 10, 2, 4) && !between(i % 100, 12, 14)) return "few";
40
+ return "many";
41
+ };
42
+ var southSlavic = ({ i, v }) => {
43
+ if (v === 0 && i % 10 === 1 && i % 100 !== 11) return "one";
44
+ if (v === 0 && between(i % 10, 2, 4) && !between(i % 100, 12, 14)) return "few";
45
+ return "other";
46
+ };
47
+ var westSlavic = ({ i, v }) => {
48
+ if (i === 1 && v === 0) return "one";
49
+ if (between(i, 2, 4) && v === 0) return "few";
50
+ return v !== 0 ? "many" : "other";
51
+ };
52
+ var RULES = {
53
+ ru: eastSlavic,
54
+ uk: eastSlavic,
55
+ be: eastSlavic,
56
+ pl: ({ i, v }) => {
57
+ if (v !== 0) return "other";
58
+ if (i === 1) return "one";
59
+ if (between(i % 10, 2, 4) && !between(i % 100, 12, 14)) return "few";
60
+ return "many";
61
+ },
62
+ cs: westSlavic,
63
+ sk: westSlavic,
64
+ hr: southSlavic,
65
+ sr: southSlavic,
66
+ bs: southSlavic,
67
+ sl: ({ i, v }) => {
68
+ if (v !== 0) return "few";
69
+ if (i % 100 === 1) return "one";
70
+ if (i % 100 === 2) return "two";
71
+ if (between(i % 100, 3, 4)) return "few";
72
+ return "other";
73
+ },
74
+ lt: ({ n, v }) => {
75
+ if (v !== 0) return "many";
76
+ if (n % 10 === 1 && !between(n % 100, 11, 19)) return "one";
77
+ if (between(n % 10, 2, 9) && !between(n % 100, 11, 19)) return "few";
78
+ return "other";
79
+ },
80
+ lv: ({ n, v }) => {
81
+ if (v === 0 && (n % 10 === 0 || between(n % 100, 11, 19))) return "zero";
82
+ if (v === 0 && n % 10 === 1 && n % 100 !== 11) return "one";
83
+ return "other";
84
+ },
85
+ ro: ({ n, i, v }) => {
86
+ if (i === 1 && v === 0) return "one";
87
+ if (v !== 0 || n === 0 || n !== 1 && between(n % 100, 1, 19)) return "few";
88
+ return "other";
89
+ },
90
+ ar: ({ n, v }) => {
91
+ if (v !== 0) return "other";
92
+ if (n === 0) return "zero";
93
+ if (n === 1) return "one";
94
+ if (n === 2) return "two";
95
+ if (between(n % 100, 3, 10)) return "few";
96
+ if (between(n % 100, 11, 99)) return "many";
97
+ return "other";
98
+ },
99
+ he: ({ i, v }) => {
100
+ if (i === 1 && v === 0 || i === 0 && v !== 0) return "one";
101
+ if (i === 2 && v === 0) return "two";
102
+ return "other";
103
+ },
104
+ ga: ({ n, v }) => {
105
+ if (v !== 0) return "other";
106
+ if (n === 1) return "one";
107
+ if (n === 2) return "two";
108
+ if (between(n, 3, 6)) return "few";
109
+ if (between(n, 7, 10)) return "many";
110
+ return "other";
111
+ },
112
+ fr: oneIfZeroOrOne,
113
+ pt: oneIfZeroOrOne,
114
+ hi: ({ n, i }) => i === 0 || n === 1 ? "one" : "other",
115
+ bn: ({ n, i }) => i === 0 || n === 1 ? "one" : "other",
116
+ fa: ({ n, i }) => i === 0 || n === 1 ? "one" : "other",
117
+ es: oneIfOne,
118
+ tr: oneIfOne,
119
+ el: oneIfOne,
120
+ hu: oneIfOne,
121
+ ka: oneIfOne,
122
+ kk: oneIfOne,
123
+ az: oneIfOne,
124
+ uz: oneIfOne,
125
+ bg: oneIfOne,
126
+ sq: oneIfOne,
127
+ ja: onlyOther,
128
+ zh: onlyOther,
129
+ ko: onlyOther,
130
+ vi: onlyOther,
131
+ th: onlyOther,
132
+ id: onlyOther,
133
+ ms: onlyOther,
134
+ my: onlyOther,
135
+ km: onlyOther,
136
+ lo: onlyOther
137
+ };
138
+ function operands(count) {
139
+ const n = Math.abs(count);
140
+ const fraction = String(n).split(".")[1] ?? "";
141
+ return { n, i: Math.floor(n), v: fraction.length };
142
+ }
143
+ function fallbackCategory(count, tag) {
144
+ const language = tag.split("-")[0]?.toLowerCase() ?? "";
145
+ const rule = RULES[language] ?? oneIfIntegerOne;
146
+ return rule(operands(count));
147
+ }
148
+ function pluralCategory(count, locale) {
149
+ const tag = locale.replace(/_/g, "-");
150
+ if (typeof Intl !== "undefined" && typeof Intl.PluralRules === "function") {
151
+ try {
152
+ return new Intl.PluralRules(tag).select(count);
153
+ } catch {
154
+ }
155
+ }
156
+ return fallbackCategory(count, tag);
157
+ }
158
+
159
+ // src/locon/utils/createTranslator.ts
160
+ var reverseIndexCache = /* @__PURE__ */ new WeakMap();
161
+ function getReverseIndex(assets, locale) {
162
+ let perLocale = reverseIndexCache.get(assets);
163
+ if (!perLocale) {
164
+ perLocale = /* @__PURE__ */ new Map();
165
+ reverseIndexCache.set(assets, perLocale);
166
+ }
167
+ const cached = perLocale.get(locale);
168
+ if (cached) {
169
+ return cached;
170
+ }
171
+ const index = /* @__PURE__ */ new Map();
172
+ const localeAssets = assets[locale];
173
+ if (localeAssets) {
174
+ for (const key of Object.keys(localeAssets)) {
175
+ const value = localeAssets[key];
176
+ if (value !== void 0 && !index.has(value)) {
177
+ index.set(value, key);
178
+ }
179
+ }
180
+ }
181
+ perLocale.set(locale, index);
182
+ return index;
183
+ }
184
+ function interpolate(value, params) {
185
+ if (!params) {
186
+ return value;
187
+ }
188
+ return value.replace(
189
+ /\{(\w+)\}/g,
190
+ (match, token) => params[token] === void 0 ? match : String(params[token])
191
+ );
192
+ }
193
+ var PLURAL_SUFFIX = /_(zero|one|two|few|many|other)$/;
194
+ function createTranslator({ assets, locale, defaultLocale = "en", projectLocale }) {
195
+ const resolvedProjectLocale = projectLocale || defaultLocale;
196
+ const read = (fromLocale, key) => {
197
+ const value = assets[fromLocale]?.[key];
198
+ return value === void 0 || value === "" ? void 0 : value;
199
+ };
200
+ const keyFor = (input, targetLocale) => {
201
+ if (assets[targetLocale]?.[input] !== void 0 || assets[defaultLocale]?.[input] !== void 0 || assets[resolvedProjectLocale]?.[input] !== void 0) {
202
+ return input;
203
+ }
204
+ return getReverseIndex(assets, resolvedProjectLocale).get(input) ?? getReverseIndex(assets, defaultLocale).get(input) ?? input;
205
+ };
206
+ return function l(assetKey, options) {
207
+ if (!assetKey) {
208
+ return "";
209
+ }
210
+ const targetLocale = options?.locale || locale;
211
+ const resolvedKey = keyFor(assetKey, targetLocale);
212
+ const key = options?.count === void 0 ? resolvedKey : resolvedKey.replace(PLURAL_SUFFIX, "");
213
+ const params = options?.count === void 0 ? options?.params : { ...options.params, count: options.count };
214
+ for (const candidateLocale of [targetLocale, defaultLocale, resolvedProjectLocale]) {
215
+ const candidateKeys = options?.count === void 0 ? [key] : [`${key}_${pluralCategory(options.count, candidateLocale)}`, `${key}_other`, key];
216
+ for (const candidateKey of candidateKeys) {
217
+ const value = read(candidateLocale, candidateKey);
218
+ if (value !== void 0) {
219
+ return interpolate(value, params);
220
+ }
221
+ }
222
+ }
223
+ return interpolate(options?.fallback ?? assetKey, params);
224
+ };
225
+ }
226
+ var createTranslator_default = createTranslator;
227
+
228
+ // src/locon/utils/intlLocale.ts
229
+ function intlLocale(locale, extensions = {}) {
230
+ const normalizedLocale = locale.replace(/_/g, "-");
231
+ const requested = [];
232
+ if (extensions.calendar) {
233
+ requested.push(["ca", extensions.calendar]);
234
+ }
235
+ if (extensions.numbering) {
236
+ requested.push(["nu", extensions.numbering]);
237
+ }
238
+ if (!requested.length) {
239
+ return normalizedLocale;
240
+ }
241
+ const subtags = normalizedLocale.split("-");
242
+ const isSingleton = (subtag) => subtag.length === 1;
243
+ const privateUseIndex = subtags.findIndex((subtag, index) => index > 0 && subtag.toLowerCase() === "x");
244
+ const unicodeIndex = subtags.findIndex(
245
+ (subtag, index) => index > 0 && (privateUseIndex < 0 || index < privateUseIndex) && subtag.toLowerCase() === "u"
246
+ );
247
+ if (unicodeIndex >= 0) {
248
+ const nextSingleton = subtags.findIndex((subtag, index) => index > unicodeIndex && isSingleton(subtag));
249
+ const unicodeEnd = nextSingleton < 0 ? subtags.length : nextSingleton;
250
+ const existingKeys = new Set(
251
+ subtags.slice(unicodeIndex + 1, unicodeEnd).filter((subtag) => subtag.length === 2).map((subtag) => subtag.toLowerCase())
252
+ );
253
+ const additions = requested.flatMap(([key, value]) => existingKeys.has(key) ? [] : [key, value]);
254
+ if (!additions.length) {
255
+ return normalizedLocale;
256
+ }
257
+ subtags.splice(unicodeEnd, 0, ...additions);
258
+ return subtags.join("-");
259
+ }
260
+ const insertionIndex = privateUseIndex < 0 ? subtags.length : privateUseIndex;
261
+ subtags.splice(insertionIndex, 0, "u", ...requested.flat());
262
+ return subtags.join("-");
263
+ }
264
+ var intlLocale_default = intlLocale;
265
+
266
+ // src/core/resources.ts
267
+ var normalized = (value) => value.replace(/_/g, "-").toLowerCase();
268
+ function matchLocale(languages, supported, fallback) {
269
+ for (const language of languages) {
270
+ let candidate = normalized(language);
271
+ while (candidate) {
272
+ const exact = supported.find((locale) => normalized(locale) === candidate);
273
+ if (exact) return exact;
274
+ const next = candidate.lastIndexOf("-");
275
+ if (next < 0) break;
276
+ candidate = candidate.slice(0, next);
277
+ }
278
+ const base = normalized(language).split("-")[0];
279
+ const variant = supported.find((locale) => normalized(locale).split("-")[0] === base);
280
+ if (variant) return variant;
281
+ }
282
+ return fallback;
283
+ }
284
+ function createLocon(options) {
285
+ const locales = Object.keys(options.loaders);
286
+ if (!locales.includes(options.defaultLocale)) throw new Error("[locon] defaultLocale needs a loader");
287
+ const initial = matchLocale(options.locale ? [options.locale] : options.languages ?? [], locales, options.defaultLocale);
288
+ let snapshot = { currentLocale: initial, loading: false, ready: false, error: null };
289
+ let assets = {};
290
+ const pending = /* @__PURE__ */ new Map();
291
+ const listeners = /* @__PURE__ */ new Set();
292
+ let selection = 0;
293
+ const notify = (next) => {
294
+ snapshot = next;
295
+ listeners.forEach((listener) => listener());
296
+ };
297
+ async function loadLocale(locale) {
298
+ if (!locales.includes(locale)) throw new Error(`[locon] Unsupported locale: ${locale}`);
299
+ if (assets[locale]) return assets[locale];
300
+ let request = pending.get(locale);
301
+ if (!request) {
302
+ request = Promise.resolve().then(options.loaders[locale]).then((module2) => {
303
+ const resource = typeof module2.default === "object" ? module2.default : module2;
304
+ if (!resource || typeof resource !== "object" || Array.isArray(resource) || Object.values(resource).some((value) => typeof value !== "string")) {
305
+ throw new Error(`[locon] Invalid resources: ${locale}`);
306
+ }
307
+ assets = { ...assets, [locale]: { ...resource } };
308
+ return assets[locale];
309
+ }).finally(() => pending.delete(locale));
310
+ pending.set(locale, request);
311
+ }
312
+ return request;
313
+ }
314
+ async function setLocale(locale) {
315
+ const target = locale === null ? matchLocale(options.languages ?? [], locales, options.defaultLocale) : locale;
316
+ if (!locales.includes(target)) throw new Error(`[locon] Unsupported locale: ${target}`);
317
+ const version = ++selection;
318
+ notify({ ...snapshot, loading: true, error: null });
319
+ try {
320
+ await loadLocale(target);
321
+ if (version !== selection) return false;
322
+ notify({ currentLocale: target, loading: false, ready: true, error: null });
323
+ return true;
324
+ } catch (cause) {
325
+ if (version === selection) notify({ ...snapshot, loading: false, error: cause instanceof Error ? cause : new Error("[locon] Loading failed") });
326
+ throw cause;
327
+ }
328
+ }
329
+ function l(key, translateOptions) {
330
+ return createTranslator_default({ assets, locale: snapshot.currentLocale, defaultLocale: options.defaultLocale, projectLocale: options.projectLocale })(key, translateOptions);
331
+ }
332
+ return {
333
+ locales,
334
+ l,
335
+ loadLocale,
336
+ setLocale,
337
+ getSnapshot: () => snapshot,
338
+ subscribe(listener) {
339
+ listeners.add(listener);
340
+ return () => {
341
+ listeners.delete(listener);
342
+ };
343
+ }
344
+ };
345
+ }
346
+ // Annotate the CommonJS export names for ESM import in node:
347
+ 0 && (module.exports = {
348
+ createLocon,
349
+ createTranslator,
350
+ intlLocale,
351
+ matchLocale
352
+ });