@hua-labs/i18n-core 2.2.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present HUA Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @hua-labs/i18n-core
2
2
 
3
- Lightweight, production-ready i18n library for React. Delivers zero-flicker language transitions through intelligent caching, SSR-first hydration handling, and built-in state management integration. ~6.5KB gzipped with zero dependencies (React only).
3
+ Lightweight, production-ready i18n library for React. Delivers zero-flicker language transitions through intelligent caching, SSR-first hydration handling, and built-in state management integration. ~7KB gzipped with zero dependencies (React only).
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@hua-labs/i18n-core.svg)](https://www.npmjs.com/package/@hua-labs/i18n-core)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/@hua-labs/i18n-core.svg)](https://www.npmjs.com/package/@hua-labs/i18n-core)
@@ -18,7 +18,7 @@ Lightweight, production-ready i18n library for React. Delivers zero-flicker lang
18
18
  - **State management — First-class Zustand support via i18n-core-zustand**
19
19
  - **Automatic retry — Exponential backoff for API loader failures**
20
20
  - **React Native — Works in Expo and bare RN projects via CJS/ESM dual format**
21
- - **~6.5KB gzipped — Zero external dependencies**
21
+ - **~7KB gzipped — Zero external dependencies**
22
22
  - **Cross-platform — I18nPlatformAdapter for Web, React Native, Flutter bridge**
23
23
  - **Generic getRawValue<T>() — type-safe raw value access without casting**
24
24
 
@@ -68,8 +68,8 @@ function Welcome() {
68
68
  | `Translator` | class | Core translator class (for manual instantiation) |
69
69
  | `ssrTranslate` | function | Server-side translation function (no React needed) |
70
70
  | `serverTranslate` | function | Server-side translate with full config |
71
- | `webPlatformAdapter` | function | Default web adapter — navigator.language detection + window event language sync |
72
- | `headlessPlatformAdapter` | function | No-op adapter for SSR, testing, and Flutter bridge scenarios |
71
+ | `webPlatformAdapter` | adapter | Default web adapter — navigator.language detection + window event language sync |
72
+ | `headlessPlatformAdapter` | adapter | No-op adapter for SSR, testing, and Flutter bridge scenarios |
73
73
  | `I18nConfig` | type | |
74
74
  | `I18nContextType` | type | |
75
75
  | `TranslationParams` | type | |
@@ -1,7 +1,15 @@
1
1
  // src/types/index.ts
2
- var PLURAL_CATEGORIES = /* @__PURE__ */ new Set(["zero", "one", "two", "few", "many", "other"]);
2
+ var PLURAL_CATEGORIES = /* @__PURE__ */ new Set([
3
+ "zero",
4
+ "one",
5
+ "two",
6
+ "few",
7
+ "many",
8
+ "other"
9
+ ]);
3
10
  function isPluralValue(value) {
4
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
11
+ if (typeof value !== "object" || value === null || Array.isArray(value))
12
+ return false;
5
13
  const obj = value;
6
14
  const keys = Object.keys(obj);
7
15
  return keys.length > 0 && keys.every((k) => PLURAL_CATEGORIES.has(k)) && Object.values(obj).every((v) => typeof v === "string") && typeof obj.other === "string";
@@ -99,7 +107,9 @@ var Translator = class {
99
107
  this.currentLang = config.defaultLanguage;
100
108
  if (config.initialTranslations) {
101
109
  this.allTranslations = config.initialTranslations;
102
- for (const [language, namespaces] of Object.entries(config.initialTranslations)) {
110
+ for (const [language, namespaces] of Object.entries(
111
+ config.initialTranslations
112
+ )) {
103
113
  for (const namespace of Object.keys(namespaces)) {
104
114
  this.loadedNamespaces.add(`${language}:${namespace}`);
105
115
  }
@@ -197,9 +207,15 @@ var Translator = class {
197
207
  }
198
208
  }
199
209
  if (this.config.debug) {
200
- console.log("\u{1F30D} [TRANSLATOR] Initializing translator with languages:", languages);
210
+ console.log(
211
+ "\u{1F30D} [TRANSLATOR] Initializing translator with languages:",
212
+ languages
213
+ );
201
214
  console.log("\u{1F4CD} [TRANSLATOR] Current language:", this.currentLang);
202
- console.log("\u{1F4E6} [TRANSLATOR] Config namespaces:", this.config.namespaces);
215
+ console.log(
216
+ "\u{1F4E6} [TRANSLATOR] Config namespaces:",
217
+ this.config.namespaces
218
+ );
203
219
  }
204
220
  for (const language of languages) {
205
221
  if (this.config.debug) {
@@ -212,12 +228,23 @@ var Translator = class {
212
228
  const cacheKey = `${language}:${namespace}`;
213
229
  if (skipNamespaces.has(cacheKey)) {
214
230
  if (this.config.debug) {
215
- console.log("\u23ED\uFE0F [TRANSLATOR] Skipping", namespace, "for", language, "(already loaded from SSR)");
231
+ console.log(
232
+ "\u23ED\uFE0F [TRANSLATOR] Skipping",
233
+ namespace,
234
+ "for",
235
+ language,
236
+ "(already loaded from SSR)"
237
+ );
216
238
  }
217
239
  continue;
218
240
  }
219
241
  if (this.config.debug) {
220
- console.log("Loading namespace:", namespace, "for language:", language);
242
+ console.log(
243
+ "Loading namespace:",
244
+ namespace,
245
+ "for language:",
246
+ language
247
+ );
221
248
  }
222
249
  try {
223
250
  const data = await this.safeLoadTranslations(language, namespace);
@@ -237,7 +264,10 @@ var Translator = class {
237
264
  if (isRecoverableError(translationError)) {
238
265
  if (language !== this.config.fallbackLanguage) {
239
266
  try {
240
- const fallbackData = await this.safeLoadTranslations(this.config.fallbackLanguage || "en", namespace);
267
+ const fallbackData = await this.safeLoadTranslations(
268
+ this.config.fallbackLanguage || "en",
269
+ namespace
270
+ );
241
271
  this.allTranslations[language][namespace] = fallbackData;
242
272
  this.loadedNamespaces.add(`${language}:${namespace}`);
243
273
  if (this.config.debug) {
@@ -275,7 +305,9 @@ var Translator = class {
275
305
  this.logError(this.initializationError);
276
306
  this.isInitialized = true;
277
307
  if (this.config.debug) {
278
- console.warn("Translator initialized with errors, using fallback translations");
308
+ console.warn(
309
+ "Translator initialized with errors, using fallback translations"
310
+ );
279
311
  }
280
312
  }
281
313
  }
@@ -290,7 +322,10 @@ var Translator = class {
290
322
  const result = this.findInNamespace(namespace, actualKey, targetLang);
291
323
  if (result) {
292
324
  if (this.config.debug) {
293
- console.log(`\u2705 [TRANSLATOR] Found fallback translation from initialTranslations:`, result);
325
+ console.log(
326
+ `\u2705 [TRANSLATOR] Found fallback translation from initialTranslations:`,
327
+ result
328
+ );
294
329
  }
295
330
  return result;
296
331
  }
@@ -354,10 +389,17 @@ var Translator = class {
354
389
  }
355
390
  if (!this.isInitialized) {
356
391
  const raw = this.translateBeforeInitialized(key, targetLang);
392
+ if ((!raw || raw === key) && params && typeof params === "object" && "defaultValue" in params && typeof params.defaultValue === "string") {
393
+ return this.interpolate(params.defaultValue, params);
394
+ }
357
395
  return params ? this.interpolate(raw, params) : raw;
358
396
  }
359
397
  const { namespace, key: actualKey } = this.parseKey(key);
360
- let result = this.findInNamespace(namespace, actualKey, targetLang);
398
+ let result = this.findInNamespace(
399
+ namespace,
400
+ actualKey,
401
+ targetLang
402
+ );
361
403
  if (result) {
362
404
  this.cacheStats.hits++;
363
405
  return params ? this.interpolate(result, params) : result;
@@ -371,6 +413,9 @@ var Translator = class {
371
413
  return params ? this.interpolate(result, params) : result;
372
414
  }
373
415
  this.cacheStats.misses++;
416
+ if (params && typeof params === "object" && "defaultValue" in params && typeof params.defaultValue === "string") {
417
+ return this.interpolate(params.defaultValue, params);
418
+ }
374
419
  if (this.config.debug) {
375
420
  const missing = this.config.missingKeyHandler?.(key, targetLang, namespace) || key;
376
421
  return params ? this.interpolate(missing, params) : missing;
@@ -387,11 +432,16 @@ var Translator = class {
387
432
  if (!this.loadedNamespaces.has(cacheKey) && !this.loadingPromises.has(cacheKey)) {
388
433
  this.loadTranslationData(language, namespace).catch((error) => {
389
434
  if (this.config.debug) {
390
- console.warn(`\u26A0\uFE0F [TRANSLATOR] Auto-load failed for ${language}/${namespace}:`, error);
435
+ console.warn(
436
+ `\u26A0\uFE0F [TRANSLATOR] Auto-load failed for ${language}/${namespace}:`,
437
+ error
438
+ );
391
439
  }
392
440
  });
393
441
  if (this.config.debug) {
394
- console.warn(`\u274C [TRANSLATOR] No translations found for ${language}/${namespace}, attempting auto-load...`);
442
+ console.warn(
443
+ `\u274C [TRANSLATOR] No translations found for ${language}/${namespace}, attempting auto-load...`
444
+ );
395
445
  }
396
446
  }
397
447
  return "";
@@ -411,7 +461,9 @@ var Translator = class {
411
461
  return nestedValue[Math.floor(Math.random() * nestedValue.length)];
412
462
  }
413
463
  if (this.config.debug) {
414
- console.warn(`\u274C [TRANSLATOR] No match found for key: ${key} in ${language}/${namespace}`);
464
+ console.warn(
465
+ `\u274C [TRANSLATOR] No match found for key: ${key} in ${language}/${namespace}`
466
+ );
415
467
  }
416
468
  return "";
417
469
  }
@@ -477,7 +529,10 @@ var Translator = class {
477
529
  if (actualKey in fallbackTranslations) {
478
530
  return fallbackTranslations[actualKey];
479
531
  }
480
- const fallbackNestedValue = this.getNestedValue(fallbackTranslations, actualKey);
532
+ const fallbackNestedValue = this.getNestedValue(
533
+ fallbackTranslations,
534
+ actualKey
535
+ );
481
536
  if (fallbackNestedValue !== void 0) {
482
537
  return fallbackNestedValue;
483
538
  }
@@ -523,7 +578,9 @@ var Translator = class {
523
578
  const raw = this.getRawValue(key, targetLang);
524
579
  const mergedParams = { count, ...params };
525
580
  if (isPluralValue(raw)) {
526
- const category = this.getPluralRules(targetLang).select(count);
581
+ const category = this.getPluralRules(targetLang).select(
582
+ count
583
+ );
527
584
  const text = raw[category] ?? raw.other;
528
585
  return this.interpolate(text, mergedParams);
529
586
  }
@@ -566,7 +623,9 @@ var Translator = class {
566
623
  });
567
624
  }
568
625
  if (this.config.debug) {
569
- console.log(`\u{1F310} [TRANSLATOR] Language changed: ${previousLanguage} -> ${language}`);
626
+ console.log(
627
+ `\u{1F310} [TRANSLATOR] Language changed: ${previousLanguage} -> ${language}`
628
+ );
570
629
  }
571
630
  }
572
631
  /**
@@ -678,7 +737,11 @@ var Translator = class {
678
737
  */
679
738
  logError(error) {
680
739
  if (this.config.errorHandler) {
681
- this.config.errorHandler(error, error.language || "", error.namespace || "");
740
+ this.config.errorHandler(
741
+ error,
742
+ error.language || "",
743
+ error.namespace || ""
744
+ );
682
745
  }
683
746
  }
684
747
  /**
@@ -701,7 +764,9 @@ var Translator = class {
701
764
  if (attempt === maxRetries) {
702
765
  break;
703
766
  }
704
- await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 1e3));
767
+ await new Promise(
768
+ (resolve) => setTimeout(resolve, Math.pow(2, attempt) * 1e3)
769
+ );
705
770
  }
706
771
  }
707
772
  throw lastError;
@@ -711,21 +776,29 @@ var Translator = class {
711
776
  */
712
777
  async safeLoadTranslations(language, namespace) {
713
778
  if (this.config.debug) {
714
- console.log(`\u{1F4E5} [TRANSLATOR] safeLoadTranslations called:`, { language, namespace });
779
+ console.log(`\u{1F4E5} [TRANSLATOR] safeLoadTranslations called:`, {
780
+ language,
781
+ namespace
782
+ });
715
783
  }
716
784
  const loadOperation = async () => {
717
785
  if (!this.config.loadTranslations) {
718
786
  throw new Error("No translation loader configured");
719
787
  }
720
788
  if (this.config.debug) {
721
- console.log(`\u{1F504} [TRANSLATOR] Calling loadTranslations for:`, { language, namespace });
789
+ console.log(`\u{1F504} [TRANSLATOR] Calling loadTranslations for:`, {
790
+ language,
791
+ namespace
792
+ });
722
793
  }
723
794
  const data = await this.config.loadTranslations(language, namespace);
724
795
  if (this.config.debug) {
725
796
  console.log(`\u{1F4E6} [TRANSLATOR] loadTranslations returned:`, data);
726
797
  }
727
798
  if (!isTranslationNamespace(data)) {
728
- throw new Error(`Invalid translation data for ${language}:${namespace}`);
799
+ throw new Error(
800
+ `Invalid translation data for ${language}:${namespace}`
801
+ );
729
802
  }
730
803
  return data;
731
804
  };
@@ -738,7 +811,10 @@ var Translator = class {
738
811
  language,
739
812
  namespace
740
813
  );
741
- return this.retryOperation(loadOperation, translationError, { language, namespace });
814
+ return this.retryOperation(loadOperation, translationError, {
815
+ language,
816
+ namespace
817
+ });
742
818
  }
743
819
  }
744
820
  /**
@@ -775,35 +851,30 @@ var Translator = class {
775
851
  if (!this.isInitialized) {
776
852
  await this.initialize();
777
853
  }
778
- const translated = this.translate(key);
779
- if (!params) {
780
- return translated;
781
- }
782
- return this.interpolate(translated, params);
854
+ return this.translate(key, params);
783
855
  }
784
856
  /**
785
857
  * 동기 번역 (고급 기능)
786
858
  */
787
859
  translateSync(key, params) {
788
860
  if (!this.isInitialized) {
861
+ if (params && typeof params === "object" && "defaultValue" in params && typeof params.defaultValue === "string") {
862
+ return this.interpolate(params.defaultValue, params);
863
+ }
789
864
  if (this.config.debug) {
790
865
  console.warn("Translator not initialized for sync translation");
791
866
  }
792
867
  const { namespace } = this.parseKey(key);
793
868
  return this.config.missingKeyHandler?.(key, this.currentLang, namespace) || key;
794
869
  }
795
- const translated = this.translate(key);
796
- if (!params) {
797
- return translated;
798
- }
799
- return this.interpolate(translated, params);
870
+ return this.translate(key, params);
800
871
  }
801
872
  /**
802
873
  * 키 파싱 (네임스페이스:키 형식)
803
- *
874
+ *
804
875
  * - 콜론(:)만 네임스페이스 구분자로 사용
805
876
  * - 점(.)은 키 이름의 일부로 취급 (중첩 객체 접근용)
806
- *
877
+ *
807
878
  * @example
808
879
  * parseKey("home:hero.badge") → { namespace: "home", key: "hero.badge" }
809
880
  * parseKey("hero.badge") → { namespace: "common", key: "hero.badge" }
@@ -812,7 +883,10 @@ var Translator = class {
812
883
  parseKey(key) {
813
884
  const colonIndex = key.indexOf(":");
814
885
  if (colonIndex !== -1) {
815
- return { namespace: key.substring(0, colonIndex), key: key.substring(colonIndex + 1) };
886
+ return {
887
+ namespace: key.substring(0, colonIndex),
888
+ key: key.substring(colonIndex + 1)
889
+ };
816
890
  }
817
891
  return { namespace: "common", key };
818
892
  }
@@ -851,7 +925,9 @@ var Translator = class {
851
925
  this.loadedNamespaces.add(cacheKey);
852
926
  this.setCacheEntry(cacheKey, data);
853
927
  if (this.config.debug) {
854
- console.log(`\u2705 [TRANSLATOR] Auto-loaded and saved ${language}/${namespace}`);
928
+ console.log(
929
+ `\u2705 [TRANSLATOR] Auto-loaded and saved ${language}/${namespace}`
930
+ );
855
931
  }
856
932
  this.notifyTranslationLoaded(language, namespace);
857
933
  return data;
@@ -869,7 +945,9 @@ var Translator = class {
869
945
  try {
870
946
  const data = await this.config.loadTranslations(language, namespace);
871
947
  if (!isTranslationNamespace(data)) {
872
- throw new Error(`Invalid translation data for ${language}:${namespace}`);
948
+ throw new Error(
949
+ `Invalid translation data for ${language}:${namespace}`
950
+ );
873
951
  }
874
952
  return data;
875
953
  } catch (error) {
@@ -892,12 +970,20 @@ function ssrTranslate({
892
970
  missingKeyHandler = (key2) => key2
893
971
  }) {
894
972
  const { namespace, key: actualKey } = parseKey(key);
895
- let result = ssrFindInNamespace(translations, namespace, actualKey, language);
973
+ let result = ssrFindInNamespace(
974
+ translations,
975
+ namespace,
976
+ actualKey,
977
+ language);
896
978
  if (result) {
897
979
  return result;
898
980
  }
899
981
  if (language !== fallbackLanguage) {
900
- result = ssrFindInNamespace(translations, namespace, actualKey, fallbackLanguage);
982
+ result = ssrFindInNamespace(
983
+ translations,
984
+ namespace,
985
+ actualKey,
986
+ fallbackLanguage);
901
987
  if (result) {
902
988
  return result;
903
989
  }
@@ -936,7 +1022,10 @@ function isStringValue(value) {
936
1022
  function parseKey(key) {
937
1023
  const colonIndex = key.indexOf(":");
938
1024
  if (colonIndex !== -1) {
939
- return { namespace: key.substring(0, colonIndex), key: key.substring(colonIndex + 1) };
1025
+ return {
1026
+ namespace: key.substring(0, colonIndex),
1027
+ key: key.substring(colonIndex + 1)
1028
+ };
940
1029
  }
941
1030
  return { namespace: "common", key };
942
1031
  }
@@ -958,7 +1047,11 @@ function serverTranslate({
958
1047
  return cached;
959
1048
  }
960
1049
  }
961
- const result = findInTranslations(translations, key, language, fallbackLanguage);
1050
+ const result = findInTranslations(
1051
+ translations,
1052
+ key,
1053
+ language,
1054
+ fallbackLanguage);
962
1055
  if (cache && result) {
963
1056
  const cacheKey = `${language}:${key}`;
964
1057
  cache.set(cacheKey, result);
@@ -974,7 +1067,12 @@ function findInTranslations(translations, key, language, fallbackLanguage, missi
974
1067
  return result;
975
1068
  }
976
1069
  if (language !== fallbackLanguage) {
977
- result = findInNamespace(translations, namespace, actualKey, fallbackLanguage);
1070
+ result = findInNamespace(
1071
+ translations,
1072
+ namespace,
1073
+ actualKey,
1074
+ fallbackLanguage
1075
+ );
978
1076
  if (result) {
979
1077
  return result;
980
1078
  }
@@ -1002,5 +1100,5 @@ function findInNamespace(translations, namespace, key, language) {
1002
1100
  }
1003
1101
 
1004
1102
  export { Translator, headlessPlatformAdapter, serverTranslate, ssrTranslate, validateI18nConfig, webPlatformAdapter };
1005
- //# sourceMappingURL=chunk-7ZYOSEMW.mjs.map
1006
- //# sourceMappingURL=chunk-7ZYOSEMW.mjs.map
1103
+ //# sourceMappingURL=chunk-4IYWT7MS.mjs.map
1104
+ //# sourceMappingURL=chunk-4IYWT7MS.mjs.map