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 +50 -0
- package/README.md +116 -13
- 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/react.d.mts +27 -0
- package/dist/react.d.ts +27 -0
- package/dist/react.js +334 -0
- package/dist/react.mjs +305 -0
- package/dist/resources-BwFIzg3m.d.mts +52 -0
- package/dist/resources-BwFIzg3m.d.ts +52 -0
- package/package.json +24 -7
- package/skills/locon-sync/SKILL.md +14 -1
- package/skills/locon-sync/scripts/check-locon-assets.mjs +16 -7
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
|
|
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.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**:
|
|
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
|
-
`
|
|
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` —
|
|
749
|
-
- `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.
|
|
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
|
|
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
|
|
777
|
-
|
|
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.
|
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
|
+
});
|