locon 1.5.1 → 1.6.1

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,50 @@
1
+ # Changelog
2
+
3
+ ## 1.6.1
4
+
5
+ ### Added
6
+
7
+ - `L` and `LProps` from `locon/react`: localized source-language text without
8
+ extra DOM elements, including use inside SVG `<text>`.
9
+ - `L` supports interpolation, CLDR plurals, per-call locale overrides, fallback,
10
+ and optional `assetKey` disambiguation. React escapes the translated text.
11
+ - The asset checker recognizes `<L>` alongside `<LText>`, including static
12
+ string-expression children used with placeholders.
13
+
14
+ ### Documentation
15
+
16
+ - Source-phrase examples for React web and Node; phrase-indexed JSON examples
17
+ preserve lazy loading without preloading the source locale.
18
+ - Updated website examples and release instructions for 1.6.1.
19
+
20
+ ## 1.6.0
21
+
22
+ ### Added
23
+
24
+ - `locon/core`: React-free and React Native-free ESM/CommonJS entry for Node and
25
+ plain TypeScript, exporting `createTranslator`, `intlLocale`, `createLocon`
26
+ and `matchLocale` with types.
27
+ - `locon/react`: React 19+ adapter with `LoconProvider` and `useLocon`, without
28
+ importing React Native.
29
+ - Lazy per-locale resource loaders, JSON-module support, successful-load cache,
30
+ parallel request deduplication and ordered locale preference matching.
31
+ - Async locale switching with last-selection-wins behavior, loading/error state,
32
+ preservation of the current locale on failure and retry support.
33
+
34
+ ### Compatibility
35
+
36
+ - The existing `locon` React Native entry, provider, hook and `LText` API remain
37
+ available. Native device language detection and RTL behavior are unchanged.
38
+ - React and React Native peers are optional at package installation time; each
39
+ adapter still requires its runtime. Node core needs neither framework.
40
+ - Lazy lookups use only already loaded resources for fallback. Prefer stable
41
+ keys when the source-language resource has not been loaded. Source-phrase
42
+ reverse lookup continues to work when that resource is available.
43
+ - Clean the output directory once before building all entries to avoid parallel
44
+ builds removing each other's output.
45
+
46
+ ### Verification
47
+
48
+ - Existing native tests and built-entry smoke tests are retained.
49
+ - Added coverage for lazy loads, caching, concurrent switching, failed load
50
+ 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.1:** the React web `<L>` component. Version 1.6 introduced 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`
@@ -448,7 +466,7 @@ no React tree: generating a PDF or spreadsheet, composing an e-mail, or
448
466
  building a notification from a background task.
449
467
 
450
468
  ```ts
451
- import { createTranslator } from 'locon'
469
+ import { createTranslator } from 'locon/core'
452
470
  import de from './assets/i18n/de.json'
453
471
  import tr from './assets/i18n/tr.json'
454
472
 
@@ -465,9 +483,13 @@ l('Arbeitszeit') // → 'Çalışma süresi'
465
483
  This is what lets an app export a report in a language the interface is not
466
484
  currently in — a user browsing in German can still produce a Turkish report.
467
485
 
468
- #### `resolveLocale()` — the provider's locale precedence
486
+ #### `resolveLocale()` — the native provider's locale precedence
469
487
 
470
- `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
471
493
  for you. `resolveLocale()` answers that question with the exact precedence
472
494
  `<Locon />` uses internally: an explicit choice, else the device language, else
473
495
  the default.
@@ -745,8 +767,13 @@ raw text inside `<LText>`.
745
767
 
746
768
  The published package contains:
747
769
 
748
- - `dist/index.js` — one bundled CommonJS file
749
- - `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.
750
777
 
751
778
  So you can import from `locon` in both JS and TS projects and get type support
752
779
  for every export. Public component/configuration types are importable too:
@@ -765,7 +792,7 @@ import type {
765
792
  } from 'locon'
766
793
  ```
767
794
 
768
- 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
769
796
  dependencies by statically reading literal `require()` calls, and esbuild
770
797
  rewrites those into its own shim when emitting ESM — so an ESM build registered
771
798
  nothing and even `react-native` failed to resolve at runtime. Bundling matters
@@ -773,11 +800,87 @@ for the same reason: an unbundled tree emitted extensionless relative imports,
773
800
  which Node resolves to `.js` but Metro tries `.mjs` for first, loading the
774
801
  library twice. Two instances mean two contexts — the provider fills one, every
775
802
  `useLocon()` reads the other, and consumers get untranslated screens with no
776
- error at all. Metro consumes CJS natively, so a second format buys a React
777
- Native library nothing.
803
+ error at all. Metro consumes CJS natively. React web and Node have their own
804
+ independent ESM/CommonJS entries.
778
805
 
779
806
  ---
780
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. Write source-language text in code.
837
+ For lazy apps, use the source phrases as JSON keys so translating them does not
838
+ require downloading the source locale first:
839
+
840
+ ```json
841
+ // en.json
842
+ { "Überraschung öffnen": "Open your surprise" }
843
+ ```
844
+
845
+ ```json
846
+ // de.json
847
+ { "Überraschung öffnen": "Überraschung öffnen" }
848
+ ```
849
+
850
+ Set `projectLocale: 'de'` for German source strings. Reverse lookup against
851
+ separate technical keys still needs the source resource loaded.
852
+
853
+ #### Localized React text (`L`, 1.6.1+)
854
+
855
+ ```tsx
856
+ import { L, useLocon } from 'locon/react'
857
+
858
+ function Card({ name }: { name: string }) {
859
+ const { l } = useLocon()
860
+ return <>
861
+ <button><L>Überraschung öffnen</L></button>
862
+ <h2><L params={{ name }}>{'Buh, {name}!'}</L></h2>
863
+ <input placeholder={l('Name eingeben')} />
864
+ </>
865
+ }
866
+ ```
867
+
868
+ `L` renders a text fragment without a wrapping span. Use it inside HTML or SVG
869
+ text elements. It updates when the provider's locale changes. Pass one source
870
+ string as children; put dynamic values in `params` instead of JSX concatenation
871
+ or nested markup. `count`, `locale`, and `fallback` have the same meaning as in
872
+ `l()`. An optional `assetKey` can disambiguate identical source phrases; missing
873
+ translations fall back to the children unless `fallback` is explicitly set.
874
+ Keep `l()` for string props, document metadata, and other non-rendered values.
875
+ The existing React Native `LText` API is unchanged.
876
+
877
+ In Node, create an instance per request, pass the request's language preferences
878
+ explicitly, and await `setLocale` before rendering. For resources already loaded
879
+ in memory, use `createTranslator({ assets, locale })` from `locon/core` directly.
880
+ On SSR, use a separate instance per request and hydrate with the same preloaded
881
+ resources/locale; never share a mutable locale store between requests.
882
+
883
+
781
884
  ### License
782
885
 
783
886
  `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
+ });