@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.

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