@genrwork/laravel-i18next 0.1.0 → 0.1.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.
@@ -1,6 +1,26 @@
1
1
  'use strict';
2
2
 
3
3
  var i18next = require('i18next');
4
+ var demand = require('./demand-sbjTXVnl.cjs');
5
+
6
+ /**
7
+ * The modules lazy loaders (`import.meta.glob()` functions) have already
8
+ * resolved, keyed by the loader itself. Loading a language file is the one
9
+ * asynchronous step of translating; once a loader has been awaited, its
10
+ * module never changes, so every later read of it can be synchronous. That is
11
+ * what lets `preloadI18n()` load translations during page resolution --
12
+ * before anything renders -- and lets `createI18n()` and the backend read
13
+ * them without waiting, so a render never has to suspend.
14
+ *
15
+ * Module scoped on purpose, so it is shared by every i18next instance of the
16
+ * process: a server keeps one copy of each catalog across requests.
17
+ */ const modules = new WeakMap();
18
+ function getCachedModule(loader) {
19
+ return modules.get(loader);
20
+ }
21
+ function setCachedModule(loader, module) {
22
+ modules.set(loader, module);
23
+ }
4
24
 
5
25
  /**
6
26
  * Normalize a `LocaleFileSources` (a single `LocaleFiles` map, or an ordered
@@ -138,6 +158,9 @@ var i18next = require('i18next');
138
158
  * module; eager files (`import.meta.glob(..., { eager: true })`) resolve to a
139
159
  * module directly; a plain object (hand-built maps, tests) is wrapped as a
140
160
  * module's `default`.
161
+ *
162
+ * A loader that was awaited before resolves to its module directly, from the
163
+ * cache (see `src/utils/cache.ts`): reading it is synchronous from then on.
141
164
  */ function toModule(entry) {
142
165
  const type = Object.prototype.toString.call(entry);
143
166
  if (type === '[object Promise]' || type === '[object Module]') {
@@ -148,8 +171,14 @@ var i18next = require('i18next');
148
171
  default: entry
149
172
  };
150
173
  }
151
- if (type === '[object Function]') {
152
- return entry();
174
+ if (typeof entry === 'function') {
175
+ const loader = entry;
176
+ const cached = getCachedModule(loader);
177
+ if (cached) return cached;
178
+ return Promise.resolve(loader()).then((module)=>{
179
+ setCachedModule(loader, module);
180
+ return module;
181
+ });
153
182
  }
154
183
  return undefined;
155
184
  }
@@ -244,335 +273,36 @@ function isPromiseLike(value) {
244
273
  }
245
274
  LaravelBackend.type = 'backend';
246
275
 
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
276
  /**
386
- * Select a proper translation string based on the given number,
387
- * using the same rules as Laravel's `trans_choice()`.
277
+ * The i18next options Laravel translations need. `createI18n()` applies them;
278
+ * pass them to `init()` yourself when you set i18next up by hand.
388
279
  *
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();
280
+ * - no key or namespace separator: Laravel keys are flat and contain dots
281
+ * and colons (`auth.failed`, `Email:`);
282
+ * - an empty message counts as missing and falls back to the next language;
283
+ * - no escaping of interpolated values: React, Vue and Svelte already escape
284
+ * what they render, and Laravel's `:name` replacements were never escaped.
285
+ */ const laravelOptions = {
286
+ keySeparator: false,
287
+ nsSeparator: false,
288
+ returnEmptyString: false,
289
+ interpolation: {
290
+ escapeValue: false
397
291
  }
398
- segments = stripConditions(segments);
399
- const pluralIndex = getPluralIndex(locale, number);
400
- return segments.length === 1 || !segments[pluralIndex] ? segments[0] : segments[pluralIndex];
401
- }
292
+ };
402
293
  /**
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;
294
+ * Register the `uppercase`, `lowercase` and `capitalize` interpolation formats.
295
+ * The Vite plugin writes Laravel's `:NAME` as `{{name, uppercase}}` and `:Name`
296
+ * as `{{name, capitalize}}`; call this once the instance is initialized.
297
+ */ function registerCaseFormats(instance) {
298
+ const formatter = instance.services.formatter;
299
+ formatter === null || formatter === void 0 ? void 0 : formatter.add('uppercase', (value)=>String(value).toUpperCase());
300
+ formatter === null || formatter === void 0 ? void 0 : formatter.add('lowercase', (value)=>String(value).toLowerCase());
301
+ formatter === null || formatter === void 0 ? void 0 : formatter.add('capitalize', (value)=>{
302
+ const text = String(value);
303
+ return text ? text[0].toUpperCase() + text.slice(1) : text;
505
304
  });
506
- return replacements;
507
305
  }
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
306
 
577
307
  /**
578
308
  * Locale of the `<html lang="">` attribute (set by Laravel's `app.blade.php`), or `en`.
@@ -610,26 +340,39 @@ LaravelFormat.type = 'i18nFormat';
610
340
  return ()=>instance.off('languageChanged', setDocumentLang);
611
341
  }
612
342
  /**
613
- * Create an i18next instance translating Laravel language files: the default
343
+ * Create an i18next instance reading Laravel language files as i18next JSON: the default
614
344
  * namespace from `lang/{locale}.json`, and one namespace per `lang/{locale}/{namespace}.json`
615
- * file (generated by the Vite plugin, or hand-written).
345
+ * file (converted from PHP by the Vite plugin, or hand-written the i18next way).
616
346
  *
617
347
  * With eager files, every known namespace of the locale and fallback locale is
618
348
  * 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.
349
+ *
350
+ * With lazy files, only the namespaces the evaluated modules declared are loaded
351
+ * up front (see `declareNamespaces()`), plus the default one; any other is loaded
352
+ * when a component first asks for it, which suspends until it arrives. Loaded
353
+ * beforehand with `preloadI18n()` -- or `withI18nPreload()` around the page
354
+ * resolver -- the up front ones are already in memory, so the instance is ready
355
+ * synchronously and neither the first render nor a later page ever waits.
620
356
  */ function createI18n({ files, locale, fallbackLocale }) {
621
357
  const instance = i18next.createInstance();
622
358
  const sources = toSources(files);
623
- const namespaces = recognizer(sources).getAllNamespaces();
359
+ const known = recognizer(sources).getAllNamespaces();
624
360
  const resolvedLocale = locale || documentLocale();
625
361
  const resolvedFallbackLocale = fallbackLocale || documentLocale();
626
362
  const eager = isEagerSources(sources);
627
- instance.use(LaravelBackend).use(LaravelFormat).init({
363
+ const namespaces = eager ? known : Array.from(new Set([
364
+ DEFAULT_NAMESPACE,
365
+ ...demand.getDeclaredNamespaces()
366
+ ])).filter((namespace)=>known.includes(namespace));
367
+ instance.use(LaravelBackend).init({
368
+ ...laravelOptions,
628
369
  lng: resolvedLocale,
629
370
  fallbackLng: resolvedFallbackLocale,
630
371
  initAsync: false,
631
- ns: namespaces.length > 0 ? namespaces : undefined,
632
- preload: eager ? Array.from(new Set([
372
+ ...namespaces.length > 0 ? {
373
+ ns: namespaces
374
+ } : {},
375
+ preload: namespaces.length > 0 ? Array.from(new Set([
633
376
  resolvedLocale,
634
377
  resolvedFallbackLocale
635
378
  ])) : undefined,
@@ -637,11 +380,18 @@ LaravelFormat.type = 'i18nFormat';
637
380
  files: sources
638
381
  }
639
382
  });
383
+ registerCaseFormats(instance);
384
+ demand.trackInstance(instance);
640
385
  return instance;
641
386
  }
642
387
 
388
+ exports.DEFAULT_NAMESPACE = DEFAULT_NAMESPACE;
643
389
  exports.LaravelBackend = LaravelBackend;
644
- exports.LaravelFormat = LaravelFormat;
645
390
  exports.createI18n = createI18n;
646
391
  exports.documentLocale = documentLocale;
392
+ exports.laravelOptions = laravelOptions;
393
+ exports.recognizer = recognizer;
394
+ exports.registerCaseFormats = registerCaseFormats;
395
+ exports.resolver = resolver;
647
396
  exports.syncDocumentLang = syncDocumentLang;
397
+ exports.toSources = toSources;