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/dist/react.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
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/react/index.tsx
|
|
21
|
+
var react_exports = {};
|
|
22
|
+
__export(react_exports, {
|
|
23
|
+
L: () => L,
|
|
24
|
+
LoconProvider: () => LoconProvider,
|
|
25
|
+
createLocon: () => createLocon,
|
|
26
|
+
matchLocale: () => matchLocale,
|
|
27
|
+
useLocon: () => useLocon
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(react_exports);
|
|
30
|
+
var import_react = require("react");
|
|
31
|
+
|
|
32
|
+
// src/locon/utils/pluralCategory.ts
|
|
33
|
+
var between = (x, from, to) => x >= from && x <= to;
|
|
34
|
+
var oneIfIntegerOne = ({ i, v }) => i === 1 && v === 0 ? "one" : "other";
|
|
35
|
+
var oneIfOne = ({ n }) => n === 1 ? "one" : "other";
|
|
36
|
+
var oneIfZeroOrOne = ({ i }) => i === 0 || i === 1 ? "one" : "other";
|
|
37
|
+
var onlyOther = () => "other";
|
|
38
|
+
var eastSlavic = ({ i, v }) => {
|
|
39
|
+
if (v !== 0) return "other";
|
|
40
|
+
if (i % 10 === 1 && i % 100 !== 11) return "one";
|
|
41
|
+
if (between(i % 10, 2, 4) && !between(i % 100, 12, 14)) return "few";
|
|
42
|
+
return "many";
|
|
43
|
+
};
|
|
44
|
+
var southSlavic = ({ i, v }) => {
|
|
45
|
+
if (v === 0 && i % 10 === 1 && i % 100 !== 11) return "one";
|
|
46
|
+
if (v === 0 && between(i % 10, 2, 4) && !between(i % 100, 12, 14)) return "few";
|
|
47
|
+
return "other";
|
|
48
|
+
};
|
|
49
|
+
var westSlavic = ({ i, v }) => {
|
|
50
|
+
if (i === 1 && v === 0) return "one";
|
|
51
|
+
if (between(i, 2, 4) && v === 0) return "few";
|
|
52
|
+
return v !== 0 ? "many" : "other";
|
|
53
|
+
};
|
|
54
|
+
var RULES = {
|
|
55
|
+
ru: eastSlavic,
|
|
56
|
+
uk: eastSlavic,
|
|
57
|
+
be: eastSlavic,
|
|
58
|
+
pl: ({ i, v }) => {
|
|
59
|
+
if (v !== 0) return "other";
|
|
60
|
+
if (i === 1) return "one";
|
|
61
|
+
if (between(i % 10, 2, 4) && !between(i % 100, 12, 14)) return "few";
|
|
62
|
+
return "many";
|
|
63
|
+
},
|
|
64
|
+
cs: westSlavic,
|
|
65
|
+
sk: westSlavic,
|
|
66
|
+
hr: southSlavic,
|
|
67
|
+
sr: southSlavic,
|
|
68
|
+
bs: southSlavic,
|
|
69
|
+
sl: ({ i, v }) => {
|
|
70
|
+
if (v !== 0) return "few";
|
|
71
|
+
if (i % 100 === 1) return "one";
|
|
72
|
+
if (i % 100 === 2) return "two";
|
|
73
|
+
if (between(i % 100, 3, 4)) return "few";
|
|
74
|
+
return "other";
|
|
75
|
+
},
|
|
76
|
+
lt: ({ n, v }) => {
|
|
77
|
+
if (v !== 0) return "many";
|
|
78
|
+
if (n % 10 === 1 && !between(n % 100, 11, 19)) return "one";
|
|
79
|
+
if (between(n % 10, 2, 9) && !between(n % 100, 11, 19)) return "few";
|
|
80
|
+
return "other";
|
|
81
|
+
},
|
|
82
|
+
lv: ({ n, v }) => {
|
|
83
|
+
if (v === 0 && (n % 10 === 0 || between(n % 100, 11, 19))) return "zero";
|
|
84
|
+
if (v === 0 && n % 10 === 1 && n % 100 !== 11) return "one";
|
|
85
|
+
return "other";
|
|
86
|
+
},
|
|
87
|
+
ro: ({ n, i, v }) => {
|
|
88
|
+
if (i === 1 && v === 0) return "one";
|
|
89
|
+
if (v !== 0 || n === 0 || n !== 1 && between(n % 100, 1, 19)) return "few";
|
|
90
|
+
return "other";
|
|
91
|
+
},
|
|
92
|
+
ar: ({ n, v }) => {
|
|
93
|
+
if (v !== 0) return "other";
|
|
94
|
+
if (n === 0) return "zero";
|
|
95
|
+
if (n === 1) return "one";
|
|
96
|
+
if (n === 2) return "two";
|
|
97
|
+
if (between(n % 100, 3, 10)) return "few";
|
|
98
|
+
if (between(n % 100, 11, 99)) return "many";
|
|
99
|
+
return "other";
|
|
100
|
+
},
|
|
101
|
+
he: ({ i, v }) => {
|
|
102
|
+
if (i === 1 && v === 0 || i === 0 && v !== 0) return "one";
|
|
103
|
+
if (i === 2 && v === 0) return "two";
|
|
104
|
+
return "other";
|
|
105
|
+
},
|
|
106
|
+
ga: ({ n, v }) => {
|
|
107
|
+
if (v !== 0) return "other";
|
|
108
|
+
if (n === 1) return "one";
|
|
109
|
+
if (n === 2) return "two";
|
|
110
|
+
if (between(n, 3, 6)) return "few";
|
|
111
|
+
if (between(n, 7, 10)) return "many";
|
|
112
|
+
return "other";
|
|
113
|
+
},
|
|
114
|
+
fr: oneIfZeroOrOne,
|
|
115
|
+
pt: oneIfZeroOrOne,
|
|
116
|
+
hi: ({ n, i }) => i === 0 || n === 1 ? "one" : "other",
|
|
117
|
+
bn: ({ n, i }) => i === 0 || n === 1 ? "one" : "other",
|
|
118
|
+
fa: ({ n, i }) => i === 0 || n === 1 ? "one" : "other",
|
|
119
|
+
es: oneIfOne,
|
|
120
|
+
tr: oneIfOne,
|
|
121
|
+
el: oneIfOne,
|
|
122
|
+
hu: oneIfOne,
|
|
123
|
+
ka: oneIfOne,
|
|
124
|
+
kk: oneIfOne,
|
|
125
|
+
az: oneIfOne,
|
|
126
|
+
uz: oneIfOne,
|
|
127
|
+
bg: oneIfOne,
|
|
128
|
+
sq: oneIfOne,
|
|
129
|
+
ja: onlyOther,
|
|
130
|
+
zh: onlyOther,
|
|
131
|
+
ko: onlyOther,
|
|
132
|
+
vi: onlyOther,
|
|
133
|
+
th: onlyOther,
|
|
134
|
+
id: onlyOther,
|
|
135
|
+
ms: onlyOther,
|
|
136
|
+
my: onlyOther,
|
|
137
|
+
km: onlyOther,
|
|
138
|
+
lo: onlyOther
|
|
139
|
+
};
|
|
140
|
+
function operands(count) {
|
|
141
|
+
const n = Math.abs(count);
|
|
142
|
+
const fraction = String(n).split(".")[1] ?? "";
|
|
143
|
+
return { n, i: Math.floor(n), v: fraction.length };
|
|
144
|
+
}
|
|
145
|
+
function fallbackCategory(count, tag) {
|
|
146
|
+
const language = tag.split("-")[0]?.toLowerCase() ?? "";
|
|
147
|
+
const rule = RULES[language] ?? oneIfIntegerOne;
|
|
148
|
+
return rule(operands(count));
|
|
149
|
+
}
|
|
150
|
+
function pluralCategory(count, locale) {
|
|
151
|
+
const tag = locale.replace(/_/g, "-");
|
|
152
|
+
if (typeof Intl !== "undefined" && typeof Intl.PluralRules === "function") {
|
|
153
|
+
try {
|
|
154
|
+
return new Intl.PluralRules(tag).select(count);
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return fallbackCategory(count, tag);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/locon/utils/createTranslator.ts
|
|
162
|
+
var reverseIndexCache = /* @__PURE__ */ new WeakMap();
|
|
163
|
+
function getReverseIndex(assets, locale) {
|
|
164
|
+
let perLocale = reverseIndexCache.get(assets);
|
|
165
|
+
if (!perLocale) {
|
|
166
|
+
perLocale = /* @__PURE__ */ new Map();
|
|
167
|
+
reverseIndexCache.set(assets, perLocale);
|
|
168
|
+
}
|
|
169
|
+
const cached = perLocale.get(locale);
|
|
170
|
+
if (cached) {
|
|
171
|
+
return cached;
|
|
172
|
+
}
|
|
173
|
+
const index = /* @__PURE__ */ new Map();
|
|
174
|
+
const localeAssets = assets[locale];
|
|
175
|
+
if (localeAssets) {
|
|
176
|
+
for (const key of Object.keys(localeAssets)) {
|
|
177
|
+
const value = localeAssets[key];
|
|
178
|
+
if (value !== void 0 && !index.has(value)) {
|
|
179
|
+
index.set(value, key);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
perLocale.set(locale, index);
|
|
184
|
+
return index;
|
|
185
|
+
}
|
|
186
|
+
function interpolate(value, params) {
|
|
187
|
+
if (!params) {
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
return value.replace(
|
|
191
|
+
/\{(\w+)\}/g,
|
|
192
|
+
(match, token) => params[token] === void 0 ? match : String(params[token])
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
var PLURAL_SUFFIX = /_(zero|one|two|few|many|other)$/;
|
|
196
|
+
function createTranslator({ assets, locale, defaultLocale = "en", projectLocale }) {
|
|
197
|
+
const resolvedProjectLocale = projectLocale || defaultLocale;
|
|
198
|
+
const read = (fromLocale, key) => {
|
|
199
|
+
const value = assets[fromLocale]?.[key];
|
|
200
|
+
return value === void 0 || value === "" ? void 0 : value;
|
|
201
|
+
};
|
|
202
|
+
const keyFor = (input, targetLocale) => {
|
|
203
|
+
if (assets[targetLocale]?.[input] !== void 0 || assets[defaultLocale]?.[input] !== void 0 || assets[resolvedProjectLocale]?.[input] !== void 0) {
|
|
204
|
+
return input;
|
|
205
|
+
}
|
|
206
|
+
return getReverseIndex(assets, resolvedProjectLocale).get(input) ?? getReverseIndex(assets, defaultLocale).get(input) ?? input;
|
|
207
|
+
};
|
|
208
|
+
return function l(assetKey, options) {
|
|
209
|
+
if (!assetKey) {
|
|
210
|
+
return "";
|
|
211
|
+
}
|
|
212
|
+
const targetLocale = options?.locale || locale;
|
|
213
|
+
const resolvedKey = keyFor(assetKey, targetLocale);
|
|
214
|
+
const key = options?.count === void 0 ? resolvedKey : resolvedKey.replace(PLURAL_SUFFIX, "");
|
|
215
|
+
const params = options?.count === void 0 ? options?.params : { ...options.params, count: options.count };
|
|
216
|
+
for (const candidateLocale of [targetLocale, defaultLocale, resolvedProjectLocale]) {
|
|
217
|
+
const candidateKeys = options?.count === void 0 ? [key] : [`${key}_${pluralCategory(options.count, candidateLocale)}`, `${key}_other`, key];
|
|
218
|
+
for (const candidateKey of candidateKeys) {
|
|
219
|
+
const value = read(candidateLocale, candidateKey);
|
|
220
|
+
if (value !== void 0) {
|
|
221
|
+
return interpolate(value, params);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return interpolate(options?.fallback ?? assetKey, params);
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
var createTranslator_default = createTranslator;
|
|
229
|
+
|
|
230
|
+
// src/core/resources.ts
|
|
231
|
+
var normalized = (value) => value.replace(/_/g, "-").toLowerCase();
|
|
232
|
+
function matchLocale(languages, supported, fallback) {
|
|
233
|
+
for (const language of languages) {
|
|
234
|
+
let candidate = normalized(language);
|
|
235
|
+
while (candidate) {
|
|
236
|
+
const exact = supported.find((locale) => normalized(locale) === candidate);
|
|
237
|
+
if (exact) return exact;
|
|
238
|
+
const next = candidate.lastIndexOf("-");
|
|
239
|
+
if (next < 0) break;
|
|
240
|
+
candidate = candidate.slice(0, next);
|
|
241
|
+
}
|
|
242
|
+
const base = normalized(language).split("-")[0];
|
|
243
|
+
const variant = supported.find((locale) => normalized(locale).split("-")[0] === base);
|
|
244
|
+
if (variant) return variant;
|
|
245
|
+
}
|
|
246
|
+
return fallback;
|
|
247
|
+
}
|
|
248
|
+
function createLocon(options) {
|
|
249
|
+
const locales = Object.keys(options.loaders);
|
|
250
|
+
if (!locales.includes(options.defaultLocale)) throw new Error("[locon] defaultLocale needs a loader");
|
|
251
|
+
const initial = matchLocale(options.locale ? [options.locale] : options.languages ?? [], locales, options.defaultLocale);
|
|
252
|
+
let snapshot = { currentLocale: initial, loading: false, ready: false, error: null };
|
|
253
|
+
let assets = {};
|
|
254
|
+
const pending = /* @__PURE__ */ new Map();
|
|
255
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
256
|
+
let selection = 0;
|
|
257
|
+
const notify = (next) => {
|
|
258
|
+
snapshot = next;
|
|
259
|
+
listeners.forEach((listener) => listener());
|
|
260
|
+
};
|
|
261
|
+
async function loadLocale(locale) {
|
|
262
|
+
if (!locales.includes(locale)) throw new Error(`[locon] Unsupported locale: ${locale}`);
|
|
263
|
+
if (assets[locale]) return assets[locale];
|
|
264
|
+
let request = pending.get(locale);
|
|
265
|
+
if (!request) {
|
|
266
|
+
request = Promise.resolve().then(options.loaders[locale]).then((module2) => {
|
|
267
|
+
const resource = typeof module2.default === "object" ? module2.default : module2;
|
|
268
|
+
if (!resource || typeof resource !== "object" || Array.isArray(resource) || Object.values(resource).some((value) => typeof value !== "string")) {
|
|
269
|
+
throw new Error(`[locon] Invalid resources: ${locale}`);
|
|
270
|
+
}
|
|
271
|
+
assets = { ...assets, [locale]: { ...resource } };
|
|
272
|
+
return assets[locale];
|
|
273
|
+
}).finally(() => pending.delete(locale));
|
|
274
|
+
pending.set(locale, request);
|
|
275
|
+
}
|
|
276
|
+
return request;
|
|
277
|
+
}
|
|
278
|
+
async function setLocale(locale) {
|
|
279
|
+
const target = locale === null ? matchLocale(options.languages ?? [], locales, options.defaultLocale) : locale;
|
|
280
|
+
if (!locales.includes(target)) throw new Error(`[locon] Unsupported locale: ${target}`);
|
|
281
|
+
const version = ++selection;
|
|
282
|
+
notify({ ...snapshot, loading: true, error: null });
|
|
283
|
+
try {
|
|
284
|
+
await loadLocale(target);
|
|
285
|
+
if (version !== selection) return false;
|
|
286
|
+
notify({ currentLocale: target, loading: false, ready: true, error: null });
|
|
287
|
+
return true;
|
|
288
|
+
} catch (cause) {
|
|
289
|
+
if (version === selection) notify({ ...snapshot, loading: false, error: cause instanceof Error ? cause : new Error("[locon] Loading failed") });
|
|
290
|
+
throw cause;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function l(key, translateOptions) {
|
|
294
|
+
return createTranslator_default({ assets, locale: snapshot.currentLocale, defaultLocale: options.defaultLocale, projectLocale: options.projectLocale })(key, translateOptions);
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
locales,
|
|
298
|
+
l,
|
|
299
|
+
loadLocale,
|
|
300
|
+
setLocale,
|
|
301
|
+
getSnapshot: () => snapshot,
|
|
302
|
+
subscribe(listener) {
|
|
303
|
+
listeners.add(listener);
|
|
304
|
+
return () => {
|
|
305
|
+
listeners.delete(listener);
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/react/index.tsx
|
|
312
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
313
|
+
var Context = (0, import_react.createContext)(null);
|
|
314
|
+
function LoconProvider({ instance, children }) {
|
|
315
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Context, { value: instance, children });
|
|
316
|
+
}
|
|
317
|
+
function useLocon() {
|
|
318
|
+
const instance = (0, import_react.useContext)(Context);
|
|
319
|
+
if (!instance) throw new Error("[locon/react] useLocon needs LoconProvider");
|
|
320
|
+
const snapshot = (0, import_react.useSyncExternalStore)(instance.subscribe, instance.getSnapshot, instance.getSnapshot);
|
|
321
|
+
return { ...snapshot, locales: instance.locales, l: instance.l, setLocale: instance.setLocale };
|
|
322
|
+
}
|
|
323
|
+
function L({ children, assetKey, ...options }) {
|
|
324
|
+
const { l } = useLocon();
|
|
325
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: l(assetKey ?? children, { ...options, fallback: options.fallback ?? children }) });
|
|
326
|
+
}
|
|
327
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
328
|
+
0 && (module.exports = {
|
|
329
|
+
L,
|
|
330
|
+
LoconProvider,
|
|
331
|
+
createLocon,
|
|
332
|
+
matchLocale,
|
|
333
|
+
useLocon
|
|
334
|
+
});
|
package/dist/react.mjs
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
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 { Fragment, 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
|
+
function L({ children, assetKey, ...options }) {
|
|
296
|
+
const { l } = useLocon();
|
|
297
|
+
return /* @__PURE__ */ jsx(Fragment, { children: l(assetKey ?? children, { ...options, fallback: options.fallback ?? children }) });
|
|
298
|
+
}
|
|
299
|
+
export {
|
|
300
|
+
L,
|
|
301
|
+
LoconProvider,
|
|
302
|
+
createLocon,
|
|
303
|
+
matchLocale,
|
|
304
|
+
useLocon
|
|
305
|
+
};
|
|
@@ -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 };
|