@genrwork/laravel-i18next 0.1.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.
Potentially problematic release.
This version of @genrwork/laravel-i18next might be problematic. Click here for more details.
- package/README.md +367 -0
- package/client.d.ts +12 -0
- package/dist/index.cjs +12 -0
- package/dist/index.mjs +2 -0
- package/dist/react.cjs +40 -0
- package/dist/react.mjs +38 -0
- package/dist/shared/create-i18n-BSEwKsCX.mjs +641 -0
- package/dist/shared/create-i18n-WsDK4Z8L.cjs +647 -0
- package/dist/svelte.cjs +77 -0
- package/dist/svelte.mjs +74 -0
- package/dist/types/backend.d.ts +29 -0
- package/dist/types/contrib/get-plural-index.d.ts +15 -0
- package/dist/types/format.d.ts +43 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/interfaces/locale-file.d.ts +8 -0
- package/dist/types/interfaces/options.d.ts +19 -0
- package/dist/types/interfaces/replacements.d.ts +6 -0
- package/dist/types/plugin/helper.d.ts +6 -0
- package/dist/types/plugin/locale.d.ts +11 -0
- package/dist/types/plugin/parser.d.ts +22 -0
- package/dist/types/plugin/sources.d.ts +53 -0
- package/dist/types/react/i18n-provider-props.d.ts +23 -0
- package/dist/types/react/index.d.ts +3 -0
- package/dist/types/react/provider.d.ts +10 -0
- package/dist/types/shared/create-i18n.d.ts +26 -0
- package/dist/types/svelte/index.d.ts +48 -0
- package/dist/types/utils/pluralization.d.ts +9 -0
- package/dist/types/utils/recognizer.d.ts +29 -0
- package/dist/types/utils/replacer.d.ts +9 -0
- package/dist/types/utils/resolver.d.ts +23 -0
- package/dist/types/utils/sources.d.ts +14 -0
- package/dist/types/vite.d.ts +25 -0
- package/dist/types/vue/index.d.ts +21 -0
- package/dist/vite.cjs +315 -0
- package/dist/vite.mjs +310 -0
- package/dist/vue.cjs +39 -0
- package/dist/vue.mjs +34 -0
- package/package.json +155 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var i18next = require('i18next');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Normalize a `LocaleFileSources` (a single `LocaleFiles` map, or an ordered
|
|
7
|
+
* array of them, highest priority first) into an array, always -- a bare
|
|
8
|
+
* map behaves like a single-entry array. Non-object entries (e.g. `null`,
|
|
9
|
+
* defensively) are dropped rather than throwing, so a hand-built array with a
|
|
10
|
+
* gap does not crash the whole app.
|
|
11
|
+
*
|
|
12
|
+
* `Array.isArray` is the discriminator: `LocaleFiles` is itself a plain
|
|
13
|
+
* `Record<string, unknown>`, which is why every consumer of `files` must call
|
|
14
|
+
* this before treating it as a map of maps -- `Object.keys()` on a raw array
|
|
15
|
+
* would otherwise yield `'0'`, `'1'`, ... instead of file paths.
|
|
16
|
+
*/ function toSources(files) {
|
|
17
|
+
const list = Array.isArray(files) ? files : [
|
|
18
|
+
files
|
|
19
|
+
];
|
|
20
|
+
return list.filter((source)=>typeof source === 'object' && source !== null);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The i18next namespace of a flat `lang/{locale}.json` file (i18next's own default).
|
|
25
|
+
*/ const DEFAULT_NAMESPACE = 'translation';
|
|
26
|
+
/**
|
|
27
|
+
* A directory segment is treated as a locale when it looks like a locale code
|
|
28
|
+
* (`en`, `id`, `pt_BR`, `zh-CN`...), so namespace detection works regardless of
|
|
29
|
+
* how deep the configured lang directory itself is nested.
|
|
30
|
+
*/ const LOCALE_PATTERN = /^[a-z]{2,3}([_-][a-z]{2,4})?$/i;
|
|
31
|
+
/**
|
|
32
|
+
* Classify one file path into a `(locale, namespace)` pair, or `null` when it
|
|
33
|
+
* is not a recognizable translation file.
|
|
34
|
+
*
|
|
35
|
+
* `lang/{locale}.json` is the default namespace. `lang/{locale}/{ns}.json`
|
|
36
|
+
* (hand-written, or generated by the Vite plugin from `{ns}.php`) is a named
|
|
37
|
+
* namespace one level deep. A namespace may nest to any depth --
|
|
38
|
+
* `lang/{locale}/{ns1}/{ns2}.json` and deeper -- by walking the directory
|
|
39
|
+
* segments backward (closest to the file first) until one of them looks like
|
|
40
|
+
* a locale; every segment walked past becomes part of the namespace, in
|
|
41
|
+
* top-down order, e.g. `es/reports/teams/x.json` -> locale `es`, namespace
|
|
42
|
+
* `reports/teams/x`. A one-level path finds its locale on the very first
|
|
43
|
+
* (only) segment checked, so `lang/{locale}/{ns}.json` needs no walking.
|
|
44
|
+
*
|
|
45
|
+
* Known limitation: this is a purely lexical scan with no knowledge of where
|
|
46
|
+
* a source's own root directory ends (a bare file->module record carries no
|
|
47
|
+
* such information, including a hand-rolled `import.meta.glob()` per the
|
|
48
|
+
* README's manual-fallback path). It picks the NEAREST directory segment that
|
|
49
|
+
* merely looks like a locale code, so a namespace segment that itself looks
|
|
50
|
+
* like one (e.g. `es/reports/en/x.json`, intended as locale `es` / namespace
|
|
51
|
+
* `reports/en/x`) is misread as locale `en` / namespace `x`. No namespace in
|
|
52
|
+
* a typical app looks like a locale code, so this is accepted and documented
|
|
53
|
+
* rather than solved.
|
|
54
|
+
*/ function classify(file) {
|
|
55
|
+
const segments = file.split('/').filter(Boolean);
|
|
56
|
+
const last = segments.at(-1);
|
|
57
|
+
const match = last === null || last === void 0 ? void 0 : last.match(/^(.+)\.json$/);
|
|
58
|
+
if (!match) return null;
|
|
59
|
+
const basename = match[1];
|
|
60
|
+
const dirSegments = segments.slice(0, -1);
|
|
61
|
+
const nsParts = [];
|
|
62
|
+
for(let i = dirSegments.length - 1; i >= 0; i -= 1){
|
|
63
|
+
const segment = dirSegments[i];
|
|
64
|
+
if (LOCALE_PATTERN.test(segment)) {
|
|
65
|
+
return {
|
|
66
|
+
locale: segment,
|
|
67
|
+
namespace: [
|
|
68
|
+
...nsParts.reverse(),
|
|
69
|
+
basename
|
|
70
|
+
].join('/')
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
nsParts.push(segment);
|
|
74
|
+
}
|
|
75
|
+
if (LOCALE_PATTERN.test(basename)) {
|
|
76
|
+
return {
|
|
77
|
+
locale: basename,
|
|
78
|
+
namespace: DEFAULT_NAMESPACE
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Recognize the locales and namespaces available from one or more
|
|
85
|
+
* `LocaleFileSources`, highest priority first.
|
|
86
|
+
*
|
|
87
|
+
* @param files
|
|
88
|
+
*/ function recognizer(files) {
|
|
89
|
+
const index = new Map();
|
|
90
|
+
const namespaces = new Set();
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
toSources(files).forEach((source, rank)=>{
|
|
93
|
+
Object.keys(source).forEach((file)=>{
|
|
94
|
+
var _a;
|
|
95
|
+
// Two sources overlapping on the exact same path (e.g. two `sources`
|
|
96
|
+
// patterns matching the same file) keep only the higher-priority one.
|
|
97
|
+
if (seen.has(file)) return;
|
|
98
|
+
const classified = classify(file);
|
|
99
|
+
if (!classified) return;
|
|
100
|
+
seen.add(file);
|
|
101
|
+
namespaces.add(classified.namespace);
|
|
102
|
+
if (!index.has(classified.locale)) index.set(classified.locale, new Map());
|
|
103
|
+
const byNamespace = index.get(classified.locale);
|
|
104
|
+
if (!byNamespace) return;
|
|
105
|
+
const candidates = (_a = byNamespace.get(classified.namespace)) !== null && _a !== void 0 ? _a : [];
|
|
106
|
+
candidates.push({
|
|
107
|
+
source: rank,
|
|
108
|
+
file
|
|
109
|
+
});
|
|
110
|
+
byNamespace.set(classified.namespace, candidates);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
// Sources are visited in rank order above, so each namespace's candidate
|
|
114
|
+
// list is already highest-priority-first -- no sort needed here.
|
|
115
|
+
const locales = Array.from(index.keys()).sort();
|
|
116
|
+
return {
|
|
117
|
+
isLocale: (locale)=>index.has(locale),
|
|
118
|
+
getLocales: ()=>locales,
|
|
119
|
+
getNamespaces: (locale)=>{
|
|
120
|
+
var _a, _b;
|
|
121
|
+
return Array.from((_b = (_a = index.get(locale)) === null || _a === void 0 ? void 0 : _a.keys()) !== null && _b !== void 0 ? _b : []).sort();
|
|
122
|
+
},
|
|
123
|
+
getAllNamespaces: ()=>Array.from(namespaces).sort(),
|
|
124
|
+
/** The single highest-priority file, or `undefined` when none exists. */ getFile: (locale, namespace)=>{
|
|
125
|
+
var _a, _b, _c;
|
|
126
|
+
return (_c = (_b = (_a = index.get(locale)) === null || _a === void 0 ? void 0 : _a.get(namespace)) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.file;
|
|
127
|
+
},
|
|
128
|
+
/** Every candidate file for the pair, highest priority first. */ getCandidates: (locale, namespace)=>{
|
|
129
|
+
var _a, _b;
|
|
130
|
+
return (_b = (_a = index.get(locale)) === null || _a === void 0 ? void 0 : _a.get(namespace)) !== null && _b !== void 0 ? _b : [];
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Turn one `files` entry into a module or a promise of one. Lazy files
|
|
137
|
+
* (`import.meta.glob()`) are loader functions resolving to a promise of a
|
|
138
|
+
* module; eager files (`import.meta.glob(..., { eager: true })`) resolve to a
|
|
139
|
+
* module directly; a plain object (hand-built maps, tests) is wrapped as a
|
|
140
|
+
* module's `default`.
|
|
141
|
+
*/ function toModule(entry) {
|
|
142
|
+
const type = Object.prototype.toString.call(entry);
|
|
143
|
+
if (type === '[object Promise]' || type === '[object Module]') {
|
|
144
|
+
return entry;
|
|
145
|
+
}
|
|
146
|
+
if (type === '[object Object]') {
|
|
147
|
+
return {
|
|
148
|
+
default: entry
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (type === '[object Function]') {
|
|
152
|
+
return entry();
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Resolve every candidate language file of a locale's namespace, highest
|
|
158
|
+
* priority first (see `recognizer().getCandidates()`). Each candidate keeps
|
|
159
|
+
* its own shape -- eager candidates stay synchronous, lazy ones stay a
|
|
160
|
+
* promise -- so a caller merging them can still take an all-synchronous path
|
|
161
|
+
* when every candidate happens to be eager.
|
|
162
|
+
*
|
|
163
|
+
* An empty array means the locale has no file at all for that namespace, in
|
|
164
|
+
* any source.
|
|
165
|
+
*
|
|
166
|
+
* @param files
|
|
167
|
+
* @param locale
|
|
168
|
+
* @param namespace
|
|
169
|
+
*/ function resolver(files, locale, namespace) {
|
|
170
|
+
const sources = toSources(files);
|
|
171
|
+
const candidates = recognizer(sources).getCandidates(locale, namespace);
|
|
172
|
+
return candidates.reduce((resolved, candidate)=>{
|
|
173
|
+
var _a;
|
|
174
|
+
const entry = (_a = sources[candidate.source]) === null || _a === void 0 ? void 0 : _a[candidate.file];
|
|
175
|
+
const module = toModule(entry);
|
|
176
|
+
if (module !== undefined) resolved.push(module);
|
|
177
|
+
return resolved;
|
|
178
|
+
}, []);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function isPromiseLike(value) {
|
|
182
|
+
return typeof (value === null || value === void 0 ? void 0 : value.then) === 'function';
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Merge resolved modules into one resource object, LOWEST priority first, so
|
|
186
|
+
* a higher-priority source's key wins. Every key is already flat/dotted (the
|
|
187
|
+
* Vite plugin flattens PHP arrays before writing JSON), so this shallow merge
|
|
188
|
+
* is exactly the deep merge Laravel itself does over the original nested PHP
|
|
189
|
+
* arrays -- no recursive merge is needed.
|
|
190
|
+
*
|
|
191
|
+
* Starts from a fresh object rather than the first module's `default`, so no
|
|
192
|
+
* source module is ever mutated. A candidate whose `default` is missing or
|
|
193
|
+
* not an object (e.g. a disabled module stubbed to `export default {}` by the
|
|
194
|
+
* consumer's build) contributes nothing rather than throwing.
|
|
195
|
+
*/ function merge(modules) {
|
|
196
|
+
var _a;
|
|
197
|
+
const resources = {};
|
|
198
|
+
for(let i = modules.length - 1; i >= 0; i -= 1){
|
|
199
|
+
const translations = (_a = modules[i]) === null || _a === void 0 ? void 0 : _a.default;
|
|
200
|
+
if (translations && typeof translations === 'object') Object.assign(resources, translations);
|
|
201
|
+
}
|
|
202
|
+
return resources;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* i18next backend loading Laravel language files as separate namespaces from
|
|
206
|
+
* one or more sources, highest priority first: the default namespace from
|
|
207
|
+
* `{source}/{locale}.json`, and one namespace per PHP file from
|
|
208
|
+
* `{source}/{locale}/{namespace}.json` (generated by the Vite plugin from
|
|
209
|
+
* `{namespace}.php`, or hand-written) -- a namespace may also nest to any
|
|
210
|
+
* depth, see `src/utils/recognizer.ts`.
|
|
211
|
+
*
|
|
212
|
+
* When several sources have a file for the same `(locale, namespace)`, they
|
|
213
|
+
* are merged per key: a higher-priority source's key overrides a
|
|
214
|
+
* lower-priority one, but a key present only in a lower-priority source still
|
|
215
|
+
* resolves. This is what lets a namespace migrate from PHP to hand-written
|
|
216
|
+
* JSON one string at a time.
|
|
217
|
+
*
|
|
218
|
+
* Eager files are read synchronously, lazy files asynchronously -- and the
|
|
219
|
+
* whole read stays synchronous as long as every candidate for the pair is
|
|
220
|
+
* eager, even when several sources contribute, which SSR depends on
|
|
221
|
+
* (`initAsync: false` + `preload`). A (locale, namespace) pair without a
|
|
222
|
+
* matching file in any source gets no resources.
|
|
223
|
+
*/ class LaravelBackend {
|
|
224
|
+
init(_services, backendOptions) {
|
|
225
|
+
var _a;
|
|
226
|
+
this.files = (_a = backendOptions === null || backendOptions === void 0 ? void 0 : backendOptions.files) !== null && _a !== void 0 ? _a : {};
|
|
227
|
+
}
|
|
228
|
+
read(language, namespace, callback) {
|
|
229
|
+
const candidates = resolver(this.files, language, namespace);
|
|
230
|
+
if (candidates.length === 0) {
|
|
231
|
+
callback(null, null);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (candidates.some(isPromiseLike)) {
|
|
235
|
+
Promise.all(candidates).then((modules)=>callback(null, merge(modules)), (error)=>callback(error, false));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
callback(null, merge(candidates));
|
|
239
|
+
}
|
|
240
|
+
constructor(){
|
|
241
|
+
this.type = 'backend';
|
|
242
|
+
this.files = {};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
LaravelBackend.type = 'backend';
|
|
246
|
+
|
|
247
|
+
/* eslint-disable */ /**
|
|
248
|
+
* Get the index to use for pluralization.
|
|
249
|
+
* The plural rules are derived from code of the Zend Framework.
|
|
250
|
+
*
|
|
251
|
+
* @category Zend
|
|
252
|
+
* @package Zend_Locale
|
|
253
|
+
* @public https://github.com/zendframework/zf1/blob/master/library/Zend/Translate/Plural.php
|
|
254
|
+
* @copyright 2005-2015 Zend Technologies USA Inc. http://www.zend.com
|
|
255
|
+
* @license http://framework.zend.com/license New BSD License
|
|
256
|
+
*
|
|
257
|
+
* @param {String} locale
|
|
258
|
+
* @param {Number} number
|
|
259
|
+
* @return {Number}
|
|
260
|
+
*/ function getPluralIndex(locale, number) {
|
|
261
|
+
locale = locale.replace('-', '_');
|
|
262
|
+
if (locale === 'pt_BR') {
|
|
263
|
+
// temporary set a locale for brazilian
|
|
264
|
+
locale = 'xbr';
|
|
265
|
+
}
|
|
266
|
+
if (locale.length > 3) {
|
|
267
|
+
locale = locale.substring(0, locale.lastIndexOf('_'));
|
|
268
|
+
}
|
|
269
|
+
switch(locale){
|
|
270
|
+
case 'az':
|
|
271
|
+
case 'bo':
|
|
272
|
+
case 'dz':
|
|
273
|
+
case 'id':
|
|
274
|
+
case 'ja':
|
|
275
|
+
case 'jv':
|
|
276
|
+
case 'ka':
|
|
277
|
+
case 'km':
|
|
278
|
+
case 'kn':
|
|
279
|
+
case 'ko':
|
|
280
|
+
case 'ms':
|
|
281
|
+
case 'th':
|
|
282
|
+
case 'tr':
|
|
283
|
+
case 'vi':
|
|
284
|
+
case 'zh':
|
|
285
|
+
return 0;
|
|
286
|
+
case 'af':
|
|
287
|
+
case 'bn':
|
|
288
|
+
case 'bg':
|
|
289
|
+
case 'ca':
|
|
290
|
+
case 'da':
|
|
291
|
+
case 'de':
|
|
292
|
+
case 'el':
|
|
293
|
+
case 'en':
|
|
294
|
+
case 'eo':
|
|
295
|
+
case 'es':
|
|
296
|
+
case 'et':
|
|
297
|
+
case 'eu':
|
|
298
|
+
case 'fa':
|
|
299
|
+
case 'fi':
|
|
300
|
+
case 'fo':
|
|
301
|
+
case 'fur':
|
|
302
|
+
case 'fy':
|
|
303
|
+
case 'gl':
|
|
304
|
+
case 'gu':
|
|
305
|
+
case 'ha':
|
|
306
|
+
case 'he':
|
|
307
|
+
case 'hu':
|
|
308
|
+
case 'is':
|
|
309
|
+
case 'it':
|
|
310
|
+
case 'ku':
|
|
311
|
+
case 'lb':
|
|
312
|
+
case 'ml':
|
|
313
|
+
case 'mn':
|
|
314
|
+
case 'mr':
|
|
315
|
+
case 'nah':
|
|
316
|
+
case 'nb':
|
|
317
|
+
case 'ne':
|
|
318
|
+
case 'nl':
|
|
319
|
+
case 'nn':
|
|
320
|
+
case 'no':
|
|
321
|
+
case 'om':
|
|
322
|
+
case 'or':
|
|
323
|
+
case 'pa':
|
|
324
|
+
case 'pap':
|
|
325
|
+
case 'ps':
|
|
326
|
+
case 'pt':
|
|
327
|
+
case 'so':
|
|
328
|
+
case 'sq':
|
|
329
|
+
case 'sv':
|
|
330
|
+
case 'sw':
|
|
331
|
+
case 'ta':
|
|
332
|
+
case 'te':
|
|
333
|
+
case 'tk':
|
|
334
|
+
case 'ur':
|
|
335
|
+
case 'zu':
|
|
336
|
+
return number === 1 ? 0 : 1;
|
|
337
|
+
case 'am':
|
|
338
|
+
case 'bh':
|
|
339
|
+
case 'fil':
|
|
340
|
+
case 'fr':
|
|
341
|
+
case 'gun':
|
|
342
|
+
case 'hi':
|
|
343
|
+
case 'ln':
|
|
344
|
+
case 'mg':
|
|
345
|
+
case 'nso':
|
|
346
|
+
case 'xbr':
|
|
347
|
+
case 'ti':
|
|
348
|
+
case 'wa':
|
|
349
|
+
return number === 0 || number === 1 ? 0 : 1;
|
|
350
|
+
case 'be':
|
|
351
|
+
case 'bs':
|
|
352
|
+
case 'hr':
|
|
353
|
+
case 'ru':
|
|
354
|
+
case 'sr':
|
|
355
|
+
case 'uk':
|
|
356
|
+
return number % 10 === 1 && number % 100 !== 11 ? 0 : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 10 || number % 100 >= 20) ? 1 : 2;
|
|
357
|
+
case 'cs':
|
|
358
|
+
case 'sk':
|
|
359
|
+
return number === 1 ? 0 : number >= 2 && number <= 4 ? 1 : 2;
|
|
360
|
+
case 'ga':
|
|
361
|
+
return number === 1 ? 0 : number === 2 ? 1 : 2;
|
|
362
|
+
case 'lt':
|
|
363
|
+
return number % 10 === 1 && number % 100 !== 11 ? 0 : number % 10 >= 2 && (number % 100 < 10 || number % 100 >= 20) ? 1 : 2;
|
|
364
|
+
case 'sl':
|
|
365
|
+
return number % 100 === 1 ? 0 : number % 100 === 2 ? 1 : number % 100 === 3 || number % 100 === 4 ? 2 : 3;
|
|
366
|
+
case 'mk':
|
|
367
|
+
return number % 10 === 1 ? 0 : 1;
|
|
368
|
+
case 'mt':
|
|
369
|
+
return number === 1 ? 0 : number === 0 || number % 100 > 1 && number % 100 < 11 ? 1 : number % 100 > 10 && number % 100 < 20 ? 2 : 3;
|
|
370
|
+
case 'lv':
|
|
371
|
+
return number === 0 ? 0 : number % 10 === 1 && number % 100 !== 11 ? 1 : 2;
|
|
372
|
+
case 'pl':
|
|
373
|
+
return number === 1 ? 0 : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 12 || number % 100 > 14) ? 1 : 2;
|
|
374
|
+
case 'cy':
|
|
375
|
+
return number === 1 ? 0 : number === 2 ? 1 : number === 8 || number === 11 ? 2 : 3;
|
|
376
|
+
case 'ro':
|
|
377
|
+
return number === 1 ? 0 : number === 0 || number % 100 > 0 && number % 100 < 20 ? 1 : 2;
|
|
378
|
+
case 'ar':
|
|
379
|
+
return number === 0 ? 0 : number === 1 ? 1 : number === 2 ? 2 : number >= 3 && number <= 10 ? 3 : number >= 11 && number <= 99 ? 4 : 5;
|
|
380
|
+
default:
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Select a proper translation string based on the given number,
|
|
387
|
+
* using the same rules as Laravel's `trans_choice()`.
|
|
388
|
+
*
|
|
389
|
+
* @param message
|
|
390
|
+
* @param number
|
|
391
|
+
* @param locale
|
|
392
|
+
*/ function pluralization(message, number, locale) {
|
|
393
|
+
let segments = message.split('|');
|
|
394
|
+
const extracted = extract(segments, number);
|
|
395
|
+
if (extracted !== null) {
|
|
396
|
+
return extracted.trim();
|
|
397
|
+
}
|
|
398
|
+
segments = stripConditions(segments);
|
|
399
|
+
const pluralIndex = getPluralIndex(locale, number);
|
|
400
|
+
return segments.length === 1 || !segments[pluralIndex] ? segments[0] : segments[pluralIndex];
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Extract a translation string using inline conditions.
|
|
404
|
+
*
|
|
405
|
+
* @param segments
|
|
406
|
+
* @param number
|
|
407
|
+
*/ function extract(segments, number) {
|
|
408
|
+
for (const segment of segments){
|
|
409
|
+
const result = extractFromString(segment, number);
|
|
410
|
+
if (result !== null) return result;
|
|
411
|
+
}
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Get the translation string if the condition matches.
|
|
416
|
+
*
|
|
417
|
+
* @param part
|
|
418
|
+
* @param number
|
|
419
|
+
*/ function extractFromString(part, number) {
|
|
420
|
+
const matches = part.match(/^[{[]([^,{}[\]]*),?([^{}[\]]*)[}\]]([\s\S]*)/);
|
|
421
|
+
if (!matches) return null;
|
|
422
|
+
const [, from, to, value] = matches;
|
|
423
|
+
if ((from === '*' || number >= parseFloat(from)) && (to === '*' || number <= parseFloat(to))) {
|
|
424
|
+
return value;
|
|
425
|
+
}
|
|
426
|
+
return from && parseFloat(from) === number ? value : null;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Strip the inline conditions from each segment, just leaving the text.
|
|
430
|
+
*
|
|
431
|
+
* @param segments
|
|
432
|
+
*/ function stripConditions(segments) {
|
|
433
|
+
return segments.map((part)=>part.replace(/^[{[]([^[\]{}]*)[}\]]/, ''));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Make the place-holder replacements on a line, the Laravel way:
|
|
438
|
+
* `:name` as is, `:NAME` upper-cased and `:Name` capitalized.
|
|
439
|
+
*
|
|
440
|
+
* @param message
|
|
441
|
+
* @param replacements
|
|
442
|
+
*/ function replacer(message, replacements) {
|
|
443
|
+
if (!replacements) return message;
|
|
444
|
+
const patterns = Object.entries(replacements).flatMap(([key, value])=>[
|
|
445
|
+
{
|
|
446
|
+
pattern: new RegExp(`:${key}`, 'g'),
|
|
447
|
+
replacement: value.toString()
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
pattern: new RegExp(`:${key.toUpperCase()}`, 'g'),
|
|
451
|
+
replacement: value.toString().toUpperCase()
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
pattern: new RegExp(`:${capitalize(key)}`, 'g'),
|
|
455
|
+
replacement: capitalize(value.toString())
|
|
456
|
+
}
|
|
457
|
+
]);
|
|
458
|
+
return patterns.reduce((result, { pattern, replacement })=>result.replace(pattern, replacement), message);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Capitalizing string.
|
|
462
|
+
*
|
|
463
|
+
* @param str
|
|
464
|
+
*/ function capitalize(str) {
|
|
465
|
+
return str ? str[0].toUpperCase() + str.slice(1) : '';
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* i18next `t()` options that are not Laravel replacements. Use the `replace`
|
|
470
|
+
* option to pass replacements having one of these names.
|
|
471
|
+
*/ const I18NEXT_OPTIONS = new Set([
|
|
472
|
+
'appendNamespaceToMissingKey',
|
|
473
|
+
'applyPostProcessor',
|
|
474
|
+
'context',
|
|
475
|
+
'fallbackLng',
|
|
476
|
+
'i18nResolved',
|
|
477
|
+
'interpolation',
|
|
478
|
+
'joinArrays',
|
|
479
|
+
'keyPrefix',
|
|
480
|
+
'keySeparator',
|
|
481
|
+
'lng',
|
|
482
|
+
'lngs',
|
|
483
|
+
'missingKeyNoValueFallbackToKey',
|
|
484
|
+
'ns',
|
|
485
|
+
'nsSeparator',
|
|
486
|
+
'nest',
|
|
487
|
+
'ordinal',
|
|
488
|
+
'postProcess',
|
|
489
|
+
'replace',
|
|
490
|
+
'returnDetails',
|
|
491
|
+
'returnObjects',
|
|
492
|
+
'scopeNs',
|
|
493
|
+
'skipInterpolation'
|
|
494
|
+
]);
|
|
495
|
+
function getReplacements(options) {
|
|
496
|
+
const fromReplace = options.replace !== null && typeof options.replace === 'object';
|
|
497
|
+
const source = fromReplace ? options.replace : options;
|
|
498
|
+
const replacements = {};
|
|
499
|
+
Object.entries(source).forEach(([key, value])=>{
|
|
500
|
+
if (typeof value !== 'string' && typeof value !== 'number') return;
|
|
501
|
+
// A numeric count selects the plural form, it is replaced after pluralization.
|
|
502
|
+
if (key === 'count' && typeof value === 'number') return;
|
|
503
|
+
if (!fromReplace && (I18NEXT_OPTIONS.has(key) || key.startsWith('defaultValue'))) return;
|
|
504
|
+
replacements[key] = value;
|
|
505
|
+
});
|
|
506
|
+
return replacements;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* i18next format plugin translating the way Laravel does:
|
|
510
|
+
*
|
|
511
|
+
* - flat keys (`auth.failed`, `Welcome, :name!`), no key or namespace separators;
|
|
512
|
+
* - `:name`, `:NAME` and `:Name` replacements instead of `{{name}}` interpolation and `$t()` nesting;
|
|
513
|
+
* - a numeric `count` selects the message with Laravel's `trans_choice()` rules
|
|
514
|
+
* (`one|many`, `{0} none|[1,19] some|[20,*] many`) instead of `_one`/`_other` suffixed keys;
|
|
515
|
+
* - empty messages fall back to the next language.
|
|
516
|
+
*/ class LaravelFormat {
|
|
517
|
+
init(i18next) {
|
|
518
|
+
this.i18next = i18next;
|
|
519
|
+
// Laravel keys contain dots and colons: disable the separators, unless given to `init()`.
|
|
520
|
+
const options = i18next.options;
|
|
521
|
+
if (options.userDefinedKeySeparator === undefined) options.keySeparator = false;
|
|
522
|
+
if (options.userDefinedNsSeparator === undefined) options.nsSeparator = false;
|
|
523
|
+
}
|
|
524
|
+
getResource(language, namespace, key) {
|
|
525
|
+
var _a;
|
|
526
|
+
const value = (_a = this.i18next) === null || _a === void 0 ? void 0 : _a.getResource(language, namespace, key, {
|
|
527
|
+
keySeparator: false,
|
|
528
|
+
ignoreJSONStructure: false
|
|
529
|
+
});
|
|
530
|
+
return value || undefined;
|
|
531
|
+
}
|
|
532
|
+
addLookupKeys(finalKeys) {
|
|
533
|
+
return finalKeys;
|
|
534
|
+
}
|
|
535
|
+
parse(message, options, language, namespace, _key, info) {
|
|
536
|
+
if (typeof message !== 'string') return message;
|
|
537
|
+
const replacements = getReplacements(options);
|
|
538
|
+
const translated = replacer(message, replacements);
|
|
539
|
+
const count = options.count;
|
|
540
|
+
if (typeof count !== 'number') return translated;
|
|
541
|
+
return replacer(pluralization(translated, count, this.getPluralLocale(language, namespace, info)), {
|
|
542
|
+
...replacements,
|
|
543
|
+
count: count.toString()
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* The language the message actually resolved in, so pluralization always
|
|
548
|
+
* matches the text being pluralized -- never merely a language that has
|
|
549
|
+
* translations for the namespace, which is not the same thing once several
|
|
550
|
+
* sources can contribute to one namespace (see LaravelBackend): a single
|
|
551
|
+
* unrelated key from a higher-priority source is enough to make
|
|
552
|
+
* `hasResourceBundle(language, namespace)` true for a namespace whose
|
|
553
|
+
* actual message still came from the fallback language.
|
|
554
|
+
*
|
|
555
|
+
* i18next passes the language the lookup succeeded at as `resolved.usedLng`
|
|
556
|
+
* in `parse()`'s 6th argument (`extendTranslation()` in i18next's own
|
|
557
|
+
* `translator.js`); prefer it. Older i18next in the `>=24` peer range that
|
|
558
|
+
* does not pass it falls back to a heuristic: the current language when it
|
|
559
|
+
* has translations for the namespace, else the fallback language.
|
|
560
|
+
*/ getPluralLocale(language, namespace, info) {
|
|
561
|
+
var _a;
|
|
562
|
+
const usedLng = (_a = info === null || info === void 0 ? void 0 : info.resolved) === null || _a === void 0 ? void 0 : _a.usedLng;
|
|
563
|
+
if (usedLng) return usedLng;
|
|
564
|
+
if (!this.i18next || !language || this.i18next.hasResourceBundle(language, namespace)) return language;
|
|
565
|
+
const [fallbackLanguage] = this.i18next.services.languageUtils.getFallbackCodes(this.i18next.options.fallbackLng, language);
|
|
566
|
+
return fallbackLanguage !== null && fallbackLanguage !== void 0 ? fallbackLanguage : language;
|
|
567
|
+
}
|
|
568
|
+
constructor(){
|
|
569
|
+
this.type = 'i18nFormat';
|
|
570
|
+
/**
|
|
571
|
+
* Non-string translations (e.g. an empty PHP array) are returned as is.
|
|
572
|
+
*/ this.handleAsObject = false;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
LaravelFormat.type = 'i18nFormat';
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Locale of the `<html lang="">` attribute (set by Laravel's `app.blade.php`), or `en`.
|
|
579
|
+
*/ function documentLocale() {
|
|
580
|
+
var _a, _b;
|
|
581
|
+
return typeof document !== 'undefined' && ((_b = (_a = document.documentElement) === null || _a === void 0 ? void 0 : _a.lang) === null || _b === void 0 ? void 0 : _b.replace('-', '_')) || 'en';
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* One source is eager (`import.meta.glob(..., { eager: true })`) when every
|
|
585
|
+
* entry is already a resolved module, lazy (`import.meta.glob(...)`) when
|
|
586
|
+
* they are loader functions.
|
|
587
|
+
*/ function isEagerSource(source) {
|
|
588
|
+
return Object.values(source).every((value)=>typeof value === 'object' && value !== null);
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* All-or-nothing across every non-empty source: the Vite plugin's `sources`
|
|
592
|
+
* option never mixes eager and lazy sources, so a hand-rolled config that
|
|
593
|
+
* does is the only way to end up here with a mixed result -- `preload` is
|
|
594
|
+
* then skipped and the backend's async path still produces correct results,
|
|
595
|
+
* just not synchronously. An empty source (e.g. a module with no
|
|
596
|
+
* translations) is ignored rather than short-circuiting eagerness to false.
|
|
597
|
+
*/ function isEagerSources(sources) {
|
|
598
|
+
return sources.filter((source)=>Object.keys(source).length > 0).every(isEagerSource);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Set the `<html lang="">` attribute to the instance's current language, and keep it
|
|
602
|
+
* in sync whenever the language changes afterwards.
|
|
603
|
+
*/ function syncDocumentLang(instance) {
|
|
604
|
+
const setDocumentLang = (language)=>{
|
|
605
|
+
// When setting the HTML lang attribute, hyphen must be used instead of underscore.
|
|
606
|
+
document.documentElement.setAttribute('lang', language.replace('_', '-'));
|
|
607
|
+
};
|
|
608
|
+
if (typeof document !== 'undefined' && instance.language) setDocumentLang(instance.language);
|
|
609
|
+
instance.on('languageChanged', setDocumentLang);
|
|
610
|
+
return ()=>instance.off('languageChanged', setDocumentLang);
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Create an i18next instance translating Laravel language files: the default
|
|
614
|
+
* namespace from `lang/{locale}.json`, and one namespace per `lang/{locale}/{namespace}.json`
|
|
615
|
+
* file (generated by the Vite plugin, or hand-written).
|
|
616
|
+
*
|
|
617
|
+
* With eager files, every known namespace of the locale and fallback locale is
|
|
618
|
+
* preloaded synchronously, so a first render never has to wait for translations.
|
|
619
|
+
* With lazy files, namespaces are loaded on demand as components request them.
|
|
620
|
+
*/ function createI18n({ files, locale, fallbackLocale }) {
|
|
621
|
+
const instance = i18next.createInstance();
|
|
622
|
+
const sources = toSources(files);
|
|
623
|
+
const namespaces = recognizer(sources).getAllNamespaces();
|
|
624
|
+
const resolvedLocale = locale || documentLocale();
|
|
625
|
+
const resolvedFallbackLocale = fallbackLocale || documentLocale();
|
|
626
|
+
const eager = isEagerSources(sources);
|
|
627
|
+
instance.use(LaravelBackend).use(LaravelFormat).init({
|
|
628
|
+
lng: resolvedLocale,
|
|
629
|
+
fallbackLng: resolvedFallbackLocale,
|
|
630
|
+
initAsync: false,
|
|
631
|
+
ns: namespaces.length > 0 ? namespaces : undefined,
|
|
632
|
+
preload: eager ? Array.from(new Set([
|
|
633
|
+
resolvedLocale,
|
|
634
|
+
resolvedFallbackLocale
|
|
635
|
+
])) : undefined,
|
|
636
|
+
backend: {
|
|
637
|
+
files: sources
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
return instance;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
exports.LaravelBackend = LaravelBackend;
|
|
644
|
+
exports.LaravelFormat = LaravelFormat;
|
|
645
|
+
exports.createI18n = createI18n;
|
|
646
|
+
exports.documentLocale = documentLocale;
|
|
647
|
+
exports.syncDocumentLang = syncDocumentLang;
|