locon 1.5.1 → 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/dist/react.mjs ADDED
@@ -0,0 +1,300 @@
1
+ // src/react/index.tsx
2
+ import { createContext, useContext, useSyncExternalStore } from "react";
3
+
4
+ // src/locon/utils/pluralCategory.ts
5
+ var between = (x, from, to) => x >= from && x <= to;
6
+ var oneIfIntegerOne = ({ i, v }) => i === 1 && v === 0 ? "one" : "other";
7
+ var oneIfOne = ({ n }) => n === 1 ? "one" : "other";
8
+ var oneIfZeroOrOne = ({ i }) => i === 0 || i === 1 ? "one" : "other";
9
+ var onlyOther = () => "other";
10
+ var eastSlavic = ({ i, v }) => {
11
+ if (v !== 0) return "other";
12
+ if (i % 10 === 1 && i % 100 !== 11) return "one";
13
+ if (between(i % 10, 2, 4) && !between(i % 100, 12, 14)) return "few";
14
+ return "many";
15
+ };
16
+ var southSlavic = ({ i, v }) => {
17
+ if (v === 0 && i % 10 === 1 && i % 100 !== 11) return "one";
18
+ if (v === 0 && between(i % 10, 2, 4) && !between(i % 100, 12, 14)) return "few";
19
+ return "other";
20
+ };
21
+ var westSlavic = ({ i, v }) => {
22
+ if (i === 1 && v === 0) return "one";
23
+ if (between(i, 2, 4) && v === 0) return "few";
24
+ return v !== 0 ? "many" : "other";
25
+ };
26
+ var RULES = {
27
+ ru: eastSlavic,
28
+ uk: eastSlavic,
29
+ be: eastSlavic,
30
+ pl: ({ i, v }) => {
31
+ if (v !== 0) return "other";
32
+ if (i === 1) return "one";
33
+ if (between(i % 10, 2, 4) && !between(i % 100, 12, 14)) return "few";
34
+ return "many";
35
+ },
36
+ cs: westSlavic,
37
+ sk: westSlavic,
38
+ hr: southSlavic,
39
+ sr: southSlavic,
40
+ bs: southSlavic,
41
+ sl: ({ i, v }) => {
42
+ if (v !== 0) return "few";
43
+ if (i % 100 === 1) return "one";
44
+ if (i % 100 === 2) return "two";
45
+ if (between(i % 100, 3, 4)) return "few";
46
+ return "other";
47
+ },
48
+ lt: ({ n, v }) => {
49
+ if (v !== 0) return "many";
50
+ if (n % 10 === 1 && !between(n % 100, 11, 19)) return "one";
51
+ if (between(n % 10, 2, 9) && !between(n % 100, 11, 19)) return "few";
52
+ return "other";
53
+ },
54
+ lv: ({ n, v }) => {
55
+ if (v === 0 && (n % 10 === 0 || between(n % 100, 11, 19))) return "zero";
56
+ if (v === 0 && n % 10 === 1 && n % 100 !== 11) return "one";
57
+ return "other";
58
+ },
59
+ ro: ({ n, i, v }) => {
60
+ if (i === 1 && v === 0) return "one";
61
+ if (v !== 0 || n === 0 || n !== 1 && between(n % 100, 1, 19)) return "few";
62
+ return "other";
63
+ },
64
+ ar: ({ n, v }) => {
65
+ if (v !== 0) return "other";
66
+ if (n === 0) return "zero";
67
+ if (n === 1) return "one";
68
+ if (n === 2) return "two";
69
+ if (between(n % 100, 3, 10)) return "few";
70
+ if (between(n % 100, 11, 99)) return "many";
71
+ return "other";
72
+ },
73
+ he: ({ i, v }) => {
74
+ if (i === 1 && v === 0 || i === 0 && v !== 0) return "one";
75
+ if (i === 2 && v === 0) return "two";
76
+ return "other";
77
+ },
78
+ ga: ({ n, v }) => {
79
+ if (v !== 0) return "other";
80
+ if (n === 1) return "one";
81
+ if (n === 2) return "two";
82
+ if (between(n, 3, 6)) return "few";
83
+ if (between(n, 7, 10)) return "many";
84
+ return "other";
85
+ },
86
+ fr: oneIfZeroOrOne,
87
+ pt: oneIfZeroOrOne,
88
+ hi: ({ n, i }) => i === 0 || n === 1 ? "one" : "other",
89
+ bn: ({ n, i }) => i === 0 || n === 1 ? "one" : "other",
90
+ fa: ({ n, i }) => i === 0 || n === 1 ? "one" : "other",
91
+ es: oneIfOne,
92
+ tr: oneIfOne,
93
+ el: oneIfOne,
94
+ hu: oneIfOne,
95
+ ka: oneIfOne,
96
+ kk: oneIfOne,
97
+ az: oneIfOne,
98
+ uz: oneIfOne,
99
+ bg: oneIfOne,
100
+ sq: oneIfOne,
101
+ ja: onlyOther,
102
+ zh: onlyOther,
103
+ ko: onlyOther,
104
+ vi: onlyOther,
105
+ th: onlyOther,
106
+ id: onlyOther,
107
+ ms: onlyOther,
108
+ my: onlyOther,
109
+ km: onlyOther,
110
+ lo: onlyOther
111
+ };
112
+ function operands(count) {
113
+ const n = Math.abs(count);
114
+ const fraction = String(n).split(".")[1] ?? "";
115
+ return { n, i: Math.floor(n), v: fraction.length };
116
+ }
117
+ function fallbackCategory(count, tag) {
118
+ const language = tag.split("-")[0]?.toLowerCase() ?? "";
119
+ const rule = RULES[language] ?? oneIfIntegerOne;
120
+ return rule(operands(count));
121
+ }
122
+ function pluralCategory(count, locale) {
123
+ const tag = locale.replace(/_/g, "-");
124
+ if (typeof Intl !== "undefined" && typeof Intl.PluralRules === "function") {
125
+ try {
126
+ return new Intl.PluralRules(tag).select(count);
127
+ } catch {
128
+ }
129
+ }
130
+ return fallbackCategory(count, tag);
131
+ }
132
+
133
+ // src/locon/utils/createTranslator.ts
134
+ var reverseIndexCache = /* @__PURE__ */ new WeakMap();
135
+ function getReverseIndex(assets, locale) {
136
+ let perLocale = reverseIndexCache.get(assets);
137
+ if (!perLocale) {
138
+ perLocale = /* @__PURE__ */ new Map();
139
+ reverseIndexCache.set(assets, perLocale);
140
+ }
141
+ const cached = perLocale.get(locale);
142
+ if (cached) {
143
+ return cached;
144
+ }
145
+ const index = /* @__PURE__ */ new Map();
146
+ const localeAssets = assets[locale];
147
+ if (localeAssets) {
148
+ for (const key of Object.keys(localeAssets)) {
149
+ const value = localeAssets[key];
150
+ if (value !== void 0 && !index.has(value)) {
151
+ index.set(value, key);
152
+ }
153
+ }
154
+ }
155
+ perLocale.set(locale, index);
156
+ return index;
157
+ }
158
+ function interpolate(value, params) {
159
+ if (!params) {
160
+ return value;
161
+ }
162
+ return value.replace(
163
+ /\{(\w+)\}/g,
164
+ (match, token) => params[token] === void 0 ? match : String(params[token])
165
+ );
166
+ }
167
+ var PLURAL_SUFFIX = /_(zero|one|two|few|many|other)$/;
168
+ function createTranslator({ assets, locale, defaultLocale = "en", projectLocale }) {
169
+ const resolvedProjectLocale = projectLocale || defaultLocale;
170
+ const read = (fromLocale, key) => {
171
+ const value = assets[fromLocale]?.[key];
172
+ return value === void 0 || value === "" ? void 0 : value;
173
+ };
174
+ const keyFor = (input, targetLocale) => {
175
+ if (assets[targetLocale]?.[input] !== void 0 || assets[defaultLocale]?.[input] !== void 0 || assets[resolvedProjectLocale]?.[input] !== void 0) {
176
+ return input;
177
+ }
178
+ return getReverseIndex(assets, resolvedProjectLocale).get(input) ?? getReverseIndex(assets, defaultLocale).get(input) ?? input;
179
+ };
180
+ return function l(assetKey, options) {
181
+ if (!assetKey) {
182
+ return "";
183
+ }
184
+ const targetLocale = options?.locale || locale;
185
+ const resolvedKey = keyFor(assetKey, targetLocale);
186
+ const key = options?.count === void 0 ? resolvedKey : resolvedKey.replace(PLURAL_SUFFIX, "");
187
+ const params = options?.count === void 0 ? options?.params : { ...options.params, count: options.count };
188
+ for (const candidateLocale of [targetLocale, defaultLocale, resolvedProjectLocale]) {
189
+ const candidateKeys = options?.count === void 0 ? [key] : [`${key}_${pluralCategory(options.count, candidateLocale)}`, `${key}_other`, key];
190
+ for (const candidateKey of candidateKeys) {
191
+ const value = read(candidateLocale, candidateKey);
192
+ if (value !== void 0) {
193
+ return interpolate(value, params);
194
+ }
195
+ }
196
+ }
197
+ return interpolate(options?.fallback ?? assetKey, params);
198
+ };
199
+ }
200
+ var createTranslator_default = createTranslator;
201
+
202
+ // src/core/resources.ts
203
+ var normalized = (value) => value.replace(/_/g, "-").toLowerCase();
204
+ function matchLocale(languages, supported, fallback) {
205
+ for (const language of languages) {
206
+ let candidate = normalized(language);
207
+ while (candidate) {
208
+ const exact = supported.find((locale) => normalized(locale) === candidate);
209
+ if (exact) return exact;
210
+ const next = candidate.lastIndexOf("-");
211
+ if (next < 0) break;
212
+ candidate = candidate.slice(0, next);
213
+ }
214
+ const base = normalized(language).split("-")[0];
215
+ const variant = supported.find((locale) => normalized(locale).split("-")[0] === base);
216
+ if (variant) return variant;
217
+ }
218
+ return fallback;
219
+ }
220
+ function createLocon(options) {
221
+ const locales = Object.keys(options.loaders);
222
+ if (!locales.includes(options.defaultLocale)) throw new Error("[locon] defaultLocale needs a loader");
223
+ const initial = matchLocale(options.locale ? [options.locale] : options.languages ?? [], locales, options.defaultLocale);
224
+ let snapshot = { currentLocale: initial, loading: false, ready: false, error: null };
225
+ let assets = {};
226
+ const pending = /* @__PURE__ */ new Map();
227
+ const listeners = /* @__PURE__ */ new Set();
228
+ let selection = 0;
229
+ const notify = (next) => {
230
+ snapshot = next;
231
+ listeners.forEach((listener) => listener());
232
+ };
233
+ async function loadLocale(locale) {
234
+ if (!locales.includes(locale)) throw new Error(`[locon] Unsupported locale: ${locale}`);
235
+ if (assets[locale]) return assets[locale];
236
+ let request = pending.get(locale);
237
+ if (!request) {
238
+ request = Promise.resolve().then(options.loaders[locale]).then((module) => {
239
+ const resource = typeof module.default === "object" ? module.default : module;
240
+ if (!resource || typeof resource !== "object" || Array.isArray(resource) || Object.values(resource).some((value) => typeof value !== "string")) {
241
+ throw new Error(`[locon] Invalid resources: ${locale}`);
242
+ }
243
+ assets = { ...assets, [locale]: { ...resource } };
244
+ return assets[locale];
245
+ }).finally(() => pending.delete(locale));
246
+ pending.set(locale, request);
247
+ }
248
+ return request;
249
+ }
250
+ async function setLocale(locale) {
251
+ const target = locale === null ? matchLocale(options.languages ?? [], locales, options.defaultLocale) : locale;
252
+ if (!locales.includes(target)) throw new Error(`[locon] Unsupported locale: ${target}`);
253
+ const version = ++selection;
254
+ notify({ ...snapshot, loading: true, error: null });
255
+ try {
256
+ await loadLocale(target);
257
+ if (version !== selection) return false;
258
+ notify({ currentLocale: target, loading: false, ready: true, error: null });
259
+ return true;
260
+ } catch (cause) {
261
+ if (version === selection) notify({ ...snapshot, loading: false, error: cause instanceof Error ? cause : new Error("[locon] Loading failed") });
262
+ throw cause;
263
+ }
264
+ }
265
+ function l(key, translateOptions) {
266
+ return createTranslator_default({ assets, locale: snapshot.currentLocale, defaultLocale: options.defaultLocale, projectLocale: options.projectLocale })(key, translateOptions);
267
+ }
268
+ return {
269
+ locales,
270
+ l,
271
+ loadLocale,
272
+ setLocale,
273
+ getSnapshot: () => snapshot,
274
+ subscribe(listener) {
275
+ listeners.add(listener);
276
+ return () => {
277
+ listeners.delete(listener);
278
+ };
279
+ }
280
+ };
281
+ }
282
+
283
+ // src/react/index.tsx
284
+ import { jsx } from "react/jsx-runtime";
285
+ var Context = createContext(null);
286
+ function LoconProvider({ instance, children }) {
287
+ return /* @__PURE__ */ jsx(Context, { value: instance, children });
288
+ }
289
+ function useLocon() {
290
+ const instance = useContext(Context);
291
+ if (!instance) throw new Error("[locon/react] useLocon needs LoconProvider");
292
+ const snapshot = useSyncExternalStore(instance.subscribe, instance.getSnapshot, instance.getSnapshot);
293
+ return { ...snapshot, locales: instance.locales, l: instance.l, setLocale: instance.setLocale };
294
+ }
295
+ export {
296
+ LoconProvider,
297
+ createLocon,
298
+ matchLocale,
299
+ useLocon
300
+ };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Per-call options for `l()`, `lIn()` and `<LText>`.
3
+ *
4
+ * All four are optional and independent: `params` interpolates, `count`
5
+ * selects a plural form, and `locale` renders one call in a language other
6
+ * than the current one (used when an app exports a document in a language
7
+ * the interface is not currently showing). `fallback` is returned only when
8
+ * no locale resolves the input.
9
+ */
10
+ interface TranslateOptions {
11
+ /** Values for `{token}` placeholders inside the resolved string. */
12
+ params?: Record<string, string | number>;
13
+ /** Selects `key_one` / `key_few` / `key_other` … via `Intl.PluralRules`. */
14
+ count?: number;
15
+ /** Resolve against this locale instead of the current one. */
16
+ locale?: string;
17
+ /** Human-readable fallback when no locale resolves the key/value. */
18
+ fallback?: string;
19
+ }
20
+
21
+ type Resource = Record<string, string>;
22
+ type ResourceLoader = () => Promise<Resource | {
23
+ default: Resource;
24
+ }>;
25
+ interface ResourceOptions {
26
+ loaders: Record<string, ResourceLoader>;
27
+ defaultLocale: string;
28
+ projectLocale?: string;
29
+ locale?: string | null;
30
+ /** Ordered browser/device preferences, or an explicit list in Node. */
31
+ languages?: readonly string[];
32
+ }
33
+ interface LocaleSnapshot {
34
+ currentLocale: string;
35
+ loading: boolean;
36
+ ready: boolean;
37
+ error: Error | null;
38
+ }
39
+ /** First matching preference wins; regional variants can use a base resource. */
40
+ declare function matchLocale(languages: readonly string[], supported: readonly string[], fallback: string): string;
41
+ /** A per-app/per-request store. Importing it touches neither React nor a platform API. */
42
+ declare function createLocon(options: ResourceOptions): {
43
+ locales: string[];
44
+ l: (key: string, translateOptions?: TranslateOptions) => string;
45
+ loadLocale: (locale: string) => Promise<Resource>;
46
+ setLocale: (locale: string | null) => Promise<boolean>;
47
+ getSnapshot: () => LocaleSnapshot;
48
+ subscribe(listener: () => void): () => void;
49
+ };
50
+ type LoconInstance = ReturnType<typeof createLocon>;
51
+
52
+ export { type LocaleSnapshot as L, type ResourceLoader as R, type TranslateOptions as T, type LoconInstance as a, type ResourceOptions as b, createLocon as c, matchLocale as m };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Per-call options for `l()`, `lIn()` and `<LText>`.
3
+ *
4
+ * All four are optional and independent: `params` interpolates, `count`
5
+ * selects a plural form, and `locale` renders one call in a language other
6
+ * than the current one (used when an app exports a document in a language
7
+ * the interface is not currently showing). `fallback` is returned only when
8
+ * no locale resolves the input.
9
+ */
10
+ interface TranslateOptions {
11
+ /** Values for `{token}` placeholders inside the resolved string. */
12
+ params?: Record<string, string | number>;
13
+ /** Selects `key_one` / `key_few` / `key_other` … via `Intl.PluralRules`. */
14
+ count?: number;
15
+ /** Resolve against this locale instead of the current one. */
16
+ locale?: string;
17
+ /** Human-readable fallback when no locale resolves the key/value. */
18
+ fallback?: string;
19
+ }
20
+
21
+ type Resource = Record<string, string>;
22
+ type ResourceLoader = () => Promise<Resource | {
23
+ default: Resource;
24
+ }>;
25
+ interface ResourceOptions {
26
+ loaders: Record<string, ResourceLoader>;
27
+ defaultLocale: string;
28
+ projectLocale?: string;
29
+ locale?: string | null;
30
+ /** Ordered browser/device preferences, or an explicit list in Node. */
31
+ languages?: readonly string[];
32
+ }
33
+ interface LocaleSnapshot {
34
+ currentLocale: string;
35
+ loading: boolean;
36
+ ready: boolean;
37
+ error: Error | null;
38
+ }
39
+ /** First matching preference wins; regional variants can use a base resource. */
40
+ declare function matchLocale(languages: readonly string[], supported: readonly string[], fallback: string): string;
41
+ /** A per-app/per-request store. Importing it touches neither React nor a platform API. */
42
+ declare function createLocon(options: ResourceOptions): {
43
+ locales: string[];
44
+ l: (key: string, translateOptions?: TranslateOptions) => string;
45
+ loadLocale: (locale: string) => Promise<Resource>;
46
+ setLocale: (locale: string | null) => Promise<boolean>;
47
+ getSnapshot: () => LocaleSnapshot;
48
+ subscribe(listener: () => void): () => void;
49
+ };
50
+ type LoconInstance = ReturnType<typeof createLocon>;
51
+
52
+ export { type LocaleSnapshot as L, type ResourceLoader as R, type TranslateOptions as T, type LoconInstance as a, type ResourceOptions as b, createLocon as c, matchLocale as m };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "locon",
3
- "version": "1.5.1",
4
- "description": "locon is a simple and lightweight library for React Native that provides a simple way to manage translations in your app.",
3
+ "version": "1.6.0",
4
+ "description": "Lightweight localization for React Native, React and Node with lazy locale resources.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
@@ -10,17 +10,28 @@
10
10
  "default": "./dist/index.js"
11
11
  },
12
12
  "./skills/*": "./skills/*",
13
- "./package.json": "./package.json"
13
+ "./package.json": "./package.json",
14
+ "./core": {
15
+ "types": "./dist/core.d.ts",
16
+ "import": "./dist/core.mjs",
17
+ "require": "./dist/core.js"
18
+ },
19
+ "./react": {
20
+ "types": "./dist/react.d.ts",
21
+ "import": "./dist/react.mjs",
22
+ "require": "./dist/react.js"
23
+ }
14
24
  },
15
25
  "files": [
16
26
  "dist",
17
- "skills"
27
+ "skills",
28
+ "CHANGELOG.md"
18
29
  ],
19
30
  "scripts": {
20
- "build": "tsup",
31
+ "build": "tsup && tsup --config tsup.web.config.ts",
21
32
  "lint": "eslint src",
22
- "prepare": "tsup",
23
- "pretest": "tsup",
33
+ "prepare": "npm run build",
34
+ "pretest": "npm run build",
24
35
  "test": "jest"
25
36
  },
26
37
  "keywords": [
@@ -59,6 +70,12 @@
59
70
  },
60
71
  "react-native-localize": {
61
72
  "optional": true
73
+ },
74
+ "react": {
75
+ "optional": true
76
+ },
77
+ "react-native": {
78
+ "optional": true
62
79
  }
63
80
  },
64
81
  "devDependencies": {