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 +33 -0
- package/README.md +81 -20
- package/dist/core.d.mts +46 -0
- package/dist/core.d.ts +46 -0
- package/dist/core.js +352 -0
- package/dist/core.mjs +322 -0
- package/dist/index.js +129 -7
- package/dist/react.d.mts +19 -0
- package/dist/react.d.ts +19 -0
- package/dist/react.js +328 -0
- package/dist/react.mjs +300 -0
- package/dist/resources-BwFIzg3m.d.mts +52 -0
- package/dist/resources-BwFIzg3m.d.ts +52 -0
- package/package.json +24 -7
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
|
|
10
|
-
|
|
11
|
-
|
|
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**:
|
|
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
|
-
{/*
|
|
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('
|
|
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('
|
|
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
|
-
`
|
|
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` —
|
|
753
|
-
- `dist/
|
|
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
|
|
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
|
|
781
|
-
|
|
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.
|
package/dist/core.d.mts
ADDED
|
@@ -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
|
+
});
|