@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,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;