react-native-i18njs 0.0.2 → 0.0.3

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,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2024-present, wangws
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
  - 🛡️ **极致类型安全**:完全 TypeScript 编写,提供从 Key 到插值参数的完整类型推导。
16
16
  - 📱 **自动跟随系统**:基于 `react-native-localize`,自动检测并响应设备语言变更。
17
17
  - ⚡ **高性能**:基于 `i18n-js` 核心,轻量高效,无多余运行时开销。
18
- - 🔌 **灵活 API**:同时支持 Hook (`useI18n`)、高阶组件 (`withTranslation`) 和全局函数 (`t`)。
18
+ - 🔌 **灵活 API**:同时支持 Hook (`useI18n`)、高阶组件 (`withI18n`) 和全局函数 (`t`)。
19
19
  - 📝 **富文本支持**:`Trans` 组件轻松处理嵌套样式和组件插值。
20
20
  - 🌍 **格式化内置**:开箱即用的数字、货币、日期格式化支持。
21
21
 
@@ -129,13 +129,13 @@ const message = t('errors.network_timeout');
129
129
 
130
130
  #### 进阶:监听语言变化
131
131
 
132
- 如果你需要在组件外监听语言变更(例如同步更新全局状态),可以使用默认导出的实例:
132
+ 如果你需要在组件外监听语言变更(例如同步更新全局状态),可以使用顶层 `subscribe` 函数:
133
133
 
134
134
  ```ts
135
- import i18n from 'react-native-i18njs';
135
+ import { subscribe } from 'react-native-i18njs';
136
136
 
137
137
  // 订阅语言变更
138
- const unsubscribe = i18n.subscribe((locale) => {
138
+ const unsubscribe = subscribe((locale) => {
139
139
  console.log('Language changed to:', locale);
140
140
  // 更新 API 默认 Header 或其他全局状态
141
141
  });
@@ -144,6 +144,27 @@ const unsubscribe = i18n.subscribe((locale) => {
144
144
  // unsubscribe();
145
145
  ```
146
146
 
147
+ #### 进阶:重置为跟随系统
148
+
149
+ 用户手动调用 `setLocale` 后会锁定语言,不再自动跟随系统。如果需要恢复跟随系统语言:
150
+
151
+ ```ts
152
+ import { resetToSystem } from 'react-native-i18njs';
153
+
154
+ // 撤销用户锁定,重新跟随系统语言
155
+ resetToSystem();
156
+ ```
157
+
158
+ #### 进阶:RTL 检测
159
+
160
+ ```ts
161
+ import { isRTL } from 'react-native-i18njs';
162
+
163
+ if (isRTL()) {
164
+ // 当前为从右到左语言(如阿拉伯语、希伯来语)
165
+ }
166
+ ```
167
+
147
168
  #### 实战示例:Axios 拦截器
148
169
 
149
170
  ```ts
@@ -192,7 +213,7 @@ async function loadFrench() {
192
213
 
193
214
  ### 4. 格式化工具
194
215
 
195
- 利用 `Intl` 标准进行格式化:
216
+ 利用 `Intl` 标准进行格式化,在组件中通过 Hook 使用:
196
217
 
197
218
  ```ts
198
219
  const { formatNumber, formatCurrency, formatDate } = useI18n();
@@ -207,6 +228,16 @@ formatCurrency(99.99, 'USD'); // "$99.99"
207
228
  formatDate(new Date(), { dateStyle: 'full' }); // "Tuesday, October 10, 2023"
208
229
  ```
209
230
 
231
+ 在非组件环境中,也可以直接使用顶层导出:
232
+
233
+ ```ts
234
+ import { formatNumber, formatCurrency, formatDate } from 'react-native-i18njs';
235
+
236
+ formatNumber(1234.56);
237
+ formatCurrency(99.99, 'USD');
238
+ formatDate(new Date());
239
+ ```
240
+
210
241
  ## ⚙️ 配置选项 (I18nOptions)
211
242
 
212
243
  `initI18n` 接受的第二个参数对象:
package/dist/index.d.mts CHANGED
@@ -35,6 +35,8 @@ interface I18nEngine {
35
35
  formatDate(date: Date | number, options?: Intl.DateTimeFormatOptions): string;
36
36
  subscribe(listener: Listener): () => void;
37
37
  isRTL(): boolean;
38
+ /** 重置为跟随系统语言,撤销 setLocale 的用户锁定 */
39
+ resetToSystem(): void;
38
40
  ready(): Promise<void>;
39
41
  isReady(): boolean;
40
42
  }
@@ -55,12 +57,18 @@ declare class DefaultI18nEngine implements I18nEngine {
55
57
  private version;
56
58
  private readyPromise;
57
59
  private resolveReady?;
60
+ /** Intl 格式化器缓存,key = locale + JSON(options) */
61
+ private numberFormatCache;
62
+ private dateFormatCache;
63
+ /** locale chain 缓存,locale 或 fallbackLocales 变化时失效 */
64
+ private localeChainCache;
58
65
  constructor();
59
66
  init(translations: Translations, options?: I18nOptions): void;
60
67
  loadTranslations(translations: Translations): void;
61
68
  updateLocale(): void;
62
69
  setLocale(locale: string): void;
63
70
  getLocale(): string;
71
+ resetToSystem(): void;
64
72
  t(scope: Scope, options?: TranslateOptions): string;
65
73
  formatNumber(n: number, options?: Intl.NumberFormatOptions): string;
66
74
  formatCurrency(n: number, currency: string, options?: Intl.NumberFormatOptions): string;
@@ -69,11 +77,16 @@ declare class DefaultI18nEngine implements I18nEngine {
69
77
  isRTL(): boolean;
70
78
  ready(): Promise<void>;
71
79
  isReady(): boolean;
80
+ private invalidateCaches;
81
+ private getCachedNumberFormat;
82
+ private getCachedDateFormat;
72
83
  private handleRTL;
73
84
  private notifyListeners;
74
85
  private setLocaleFromSystem;
75
86
  private applyLocale;
76
87
  private translateAtLocale;
88
+ /** 将 locale 及其自动降级(如 en-US → en)追加到 chain 中 */
89
+ private pushWithDegradation;
77
90
  private getLocaleChain;
78
91
  private hasTranslation;
79
92
  private normalizeTranslateResult;
@@ -126,7 +139,14 @@ declare const loadTranslations: (translations: Translations) => void;
126
139
  declare const setLocale: (locale: string) => void;
127
140
  declare const getLocale: () => string;
128
141
  declare const t: (scope: Scope, options?: TranslateOptions) => string;
142
+ declare const formatNumber: (n: number, options?: Intl.NumberFormatOptions) => string;
143
+ declare const formatCurrency: (n: number, currency: string, options?: Intl.NumberFormatOptions) => string;
144
+ declare const formatDate: (date: Date | number, options?: Intl.DateTimeFormatOptions) => string;
145
+ declare const subscribe: (listener: Listener) => (() => void);
146
+ declare const isRTL: () => boolean;
147
+ /** 重置为跟随系统语言,撤销 setLocale 的用户锁定 */
148
+ declare const resetToSystem: () => void;
129
149
  declare const readyI18n: () => Promise<void>;
130
150
  declare const isI18nReady: () => boolean;
131
151
 
132
- export { I18nContext, type I18nContextType, type I18nOptions, I18nProvider, type I18nProviderProps, type Path, Trans, type Translations, i18nService as default, getLocale, initI18n, isI18nReady, loadTranslations, readyI18n, setLocale, t, useI18n, withI18n };
152
+ export { I18nContext, type I18nContextType, type I18nOptions, I18nProvider, type I18nProviderProps, type Listener, type Path, Trans, type Translations, i18nService as default, formatCurrency, formatDate, formatNumber, getLocale, initI18n, isI18nReady, isRTL, loadTranslations, readyI18n, resetToSystem, setLocale, subscribe, t, useI18n, withI18n };
package/dist/index.d.ts CHANGED
@@ -35,6 +35,8 @@ interface I18nEngine {
35
35
  formatDate(date: Date | number, options?: Intl.DateTimeFormatOptions): string;
36
36
  subscribe(listener: Listener): () => void;
37
37
  isRTL(): boolean;
38
+ /** 重置为跟随系统语言,撤销 setLocale 的用户锁定 */
39
+ resetToSystem(): void;
38
40
  ready(): Promise<void>;
39
41
  isReady(): boolean;
40
42
  }
@@ -55,12 +57,18 @@ declare class DefaultI18nEngine implements I18nEngine {
55
57
  private version;
56
58
  private readyPromise;
57
59
  private resolveReady?;
60
+ /** Intl 格式化器缓存,key = locale + JSON(options) */
61
+ private numberFormatCache;
62
+ private dateFormatCache;
63
+ /** locale chain 缓存,locale 或 fallbackLocales 变化时失效 */
64
+ private localeChainCache;
58
65
  constructor();
59
66
  init(translations: Translations, options?: I18nOptions): void;
60
67
  loadTranslations(translations: Translations): void;
61
68
  updateLocale(): void;
62
69
  setLocale(locale: string): void;
63
70
  getLocale(): string;
71
+ resetToSystem(): void;
64
72
  t(scope: Scope, options?: TranslateOptions): string;
65
73
  formatNumber(n: number, options?: Intl.NumberFormatOptions): string;
66
74
  formatCurrency(n: number, currency: string, options?: Intl.NumberFormatOptions): string;
@@ -69,11 +77,16 @@ declare class DefaultI18nEngine implements I18nEngine {
69
77
  isRTL(): boolean;
70
78
  ready(): Promise<void>;
71
79
  isReady(): boolean;
80
+ private invalidateCaches;
81
+ private getCachedNumberFormat;
82
+ private getCachedDateFormat;
72
83
  private handleRTL;
73
84
  private notifyListeners;
74
85
  private setLocaleFromSystem;
75
86
  private applyLocale;
76
87
  private translateAtLocale;
88
+ /** 将 locale 及其自动降级(如 en-US → en)追加到 chain 中 */
89
+ private pushWithDegradation;
77
90
  private getLocaleChain;
78
91
  private hasTranslation;
79
92
  private normalizeTranslateResult;
@@ -126,7 +139,14 @@ declare const loadTranslations: (translations: Translations) => void;
126
139
  declare const setLocale: (locale: string) => void;
127
140
  declare const getLocale: () => string;
128
141
  declare const t: (scope: Scope, options?: TranslateOptions) => string;
142
+ declare const formatNumber: (n: number, options?: Intl.NumberFormatOptions) => string;
143
+ declare const formatCurrency: (n: number, currency: string, options?: Intl.NumberFormatOptions) => string;
144
+ declare const formatDate: (date: Date | number, options?: Intl.DateTimeFormatOptions) => string;
145
+ declare const subscribe: (listener: Listener) => (() => void);
146
+ declare const isRTL: () => boolean;
147
+ /** 重置为跟随系统语言,撤销 setLocale 的用户锁定 */
148
+ declare const resetToSystem: () => void;
129
149
  declare const readyI18n: () => Promise<void>;
130
150
  declare const isI18nReady: () => boolean;
131
151
 
132
- export { I18nContext, type I18nContextType, type I18nOptions, I18nProvider, type I18nProviderProps, type Path, Trans, type Translations, i18nService as default, getLocale, initI18n, isI18nReady, loadTranslations, readyI18n, setLocale, t, useI18n, withI18n };
152
+ export { I18nContext, type I18nContextType, type I18nOptions, I18nProvider, type I18nProviderProps, type Listener, type Path, Trans, type Translations, i18nService as default, formatCurrency, formatDate, formatNumber, getLocale, initI18n, isI18nReady, isRTL, loadTranslations, readyI18n, resetToSystem, setLocale, subscribe, t, useI18n, withI18n };
package/dist/index.js CHANGED
@@ -73,6 +73,12 @@ var DefaultI18nEngine = class {
73
73
  __publicField(this, "version");
74
74
  __publicField(this, "readyPromise");
75
75
  __publicField(this, "resolveReady");
76
+ // --- 性能缓存 ---
77
+ /** Intl 格式化器缓存,key = locale + JSON(options) */
78
+ __publicField(this, "numberFormatCache", /* @__PURE__ */ new Map());
79
+ __publicField(this, "dateFormatCache", /* @__PURE__ */ new Map());
80
+ /** locale chain 缓存,locale 或 fallbackLocales 变化时失效 */
81
+ __publicField(this, "localeChainCache", /* @__PURE__ */ new Map());
76
82
  this.i18n = new i18nJs.I18n();
77
83
  this.listeners = /* @__PURE__ */ new Set();
78
84
  this.i18n.enableFallback = false;
@@ -107,6 +113,7 @@ var DefaultI18nEngine = class {
107
113
  this.localeSource = "system";
108
114
  this.missingBehavior = missingBehavior;
109
115
  this.onMissingKey = onMissingKey;
116
+ this.invalidateCaches();
110
117
  if (this.followSystem) this.updateLocale();
111
118
  this.version += 1;
112
119
  this.notifyListeners({ type: "translations", version: this.version });
@@ -142,6 +149,10 @@ var DefaultI18nEngine = class {
142
149
  getLocale() {
143
150
  return this.i18n.locale;
144
151
  }
152
+ resetToSystem() {
153
+ this.localeSource = "system";
154
+ this.updateLocale();
155
+ }
145
156
  t(scope, options) {
146
157
  var _a, _b, _c;
147
158
  if (typeof scope !== "string") {
@@ -175,7 +186,7 @@ var DefaultI18nEngine = class {
175
186
  return String(n);
176
187
  }
177
188
  try {
178
- return new Intl.NumberFormat(this.i18n.locale, options).format(n);
189
+ return this.getCachedNumberFormat(this.i18n.locale, options).format(n);
179
190
  } catch {
180
191
  return String(n);
181
192
  }
@@ -185,7 +196,7 @@ var DefaultI18nEngine = class {
185
196
  return `${n} ${currency}`;
186
197
  }
187
198
  try {
188
- return new Intl.NumberFormat(this.i18n.locale, {
199
+ return this.getCachedNumberFormat(this.i18n.locale, {
189
200
  ...options,
190
201
  style: "currency",
191
202
  currency
@@ -201,7 +212,7 @@ var DefaultI18nEngine = class {
201
212
  return d.toISOString();
202
213
  }
203
214
  try {
204
- return new Intl.DateTimeFormat(this.i18n.locale, options).format(d);
215
+ return this.getCachedDateFormat(this.i18n.locale, options).format(d);
205
216
  } catch {
206
217
  return d.toISOString();
207
218
  }
@@ -222,15 +233,40 @@ var DefaultI18nEngine = class {
222
233
  isReady() {
223
234
  return this.initialized;
224
235
  }
236
+ // ─── 缓存管理 ────────────────────────────────────────────
237
+ invalidateCaches() {
238
+ this.numberFormatCache.clear();
239
+ this.dateFormatCache.clear();
240
+ this.localeChainCache.clear();
241
+ }
242
+ getCachedNumberFormat(locale, options) {
243
+ const cacheKey = locale + (options ? JSON.stringify(options) : "");
244
+ let fmt = this.numberFormatCache.get(cacheKey);
245
+ if (!fmt) {
246
+ fmt = new Intl.NumberFormat(locale, options);
247
+ this.numberFormatCache.set(cacheKey, fmt);
248
+ }
249
+ return fmt;
250
+ }
251
+ getCachedDateFormat(locale, options) {
252
+ const cacheKey = locale + (options ? JSON.stringify(options) : "");
253
+ let fmt = this.dateFormatCache.get(cacheKey);
254
+ if (!fmt) {
255
+ fmt = new Intl.DateTimeFormat(locale, options);
256
+ this.dateFormatCache.set(cacheKey, fmt);
257
+ }
258
+ return fmt;
259
+ }
260
+ // ─── 私有方法 ────────────────────────────────────────────
225
261
  handleRTL(locale) {
226
262
  var _a, _b;
227
- const isRTL = this.isRTLLocale(locale);
263
+ const isRTL2 = this.isRTLLocale(locale);
228
264
  if (typeof ((_a = reactNative.I18nManager) == null ? void 0 : _a.allowRTL) !== "function" || typeof ((_b = reactNative.I18nManager) == null ? void 0 : _b.forceRTL) !== "function") {
229
265
  return;
230
266
  }
231
- if (reactNative.I18nManager.isRTL !== isRTL) {
232
- reactNative.I18nManager.allowRTL(isRTL);
233
- reactNative.I18nManager.forceRTL(isRTL);
267
+ if (reactNative.I18nManager.isRTL !== isRTL2) {
268
+ reactNative.I18nManager.allowRTL(isRTL2);
269
+ reactNative.I18nManager.forceRTL(isRTL2);
234
270
  }
235
271
  }
236
272
  notifyListeners(change) {
@@ -245,6 +281,7 @@ var DefaultI18nEngine = class {
245
281
  const normalizedLocale = this.normalizeLocaleTag(locale);
246
282
  if (this.i18n.locale !== normalizedLocale) {
247
283
  this.i18n.locale = normalizedLocale;
284
+ this.invalidateCaches();
248
285
  this.handleRTL(normalizedLocale);
249
286
  this.version += 1;
250
287
  this.notifyListeners({ type: "locale", version: this.version });
@@ -260,39 +297,36 @@ var DefaultI18nEngine = class {
260
297
  this.i18n.locale = prevLocale;
261
298
  }
262
299
  }
263
- getLocaleChain(locale) {
264
- const chain = [];
265
- const normalizedLocale = this.normalizeLocaleTag(locale);
266
- chain.push(normalizedLocale);
267
- const parts = normalizedLocale.split("-").filter(Boolean);
300
+ /** 将 locale 及其自动降级(如 en-US → en)追加到 chain 中 */
301
+ pushWithDegradation(chain, locale) {
302
+ const normalized = this.normalizeLocaleTag(locale);
303
+ chain.push(normalized);
304
+ const parts = normalized.split("-").filter(Boolean);
268
305
  for (let i = parts.length - 1; i >= 1; i -= 1) {
269
306
  chain.push(parts.slice(0, i).join("-"));
270
307
  }
308
+ }
309
+ getLocaleChain(locale) {
310
+ const cached = this.localeChainCache.get(locale);
311
+ if (cached) return cached;
312
+ const chain = [];
313
+ this.pushWithDegradation(chain, locale);
314
+ const normalizedLocale = this.normalizeLocaleTag(locale);
271
315
  const extra = this.fallbackLocales ? typeof this.fallbackLocales === "function" ? this.fallbackLocales(normalizedLocale) : this.fallbackLocales : [];
272
316
  for (const l of extra) {
273
- const normalizedFallback = this.normalizeLocaleTag(l);
274
- chain.push(normalizedFallback);
275
- const fallbackParts = normalizedFallback.split("-").filter(Boolean);
276
- for (let i = fallbackParts.length - 1; i >= 1; i -= 1) {
277
- chain.push(fallbackParts.slice(0, i).join("-"));
278
- }
317
+ this.pushWithDegradation(chain, l);
279
318
  }
280
319
  if (this.i18n.defaultLocale) {
281
- const normalizedDefault = this.normalizeLocaleTag(this.i18n.defaultLocale);
282
- chain.push(normalizedDefault);
283
- const defaultParts = normalizedDefault.split("-").filter(Boolean);
284
- for (let i = defaultParts.length - 1; i >= 1; i -= 1) {
285
- chain.push(defaultParts.slice(0, i).join("-"));
286
- }
320
+ this.pushWithDegradation(chain, this.i18n.defaultLocale);
287
321
  }
288
322
  const seen = /* @__PURE__ */ new Set();
289
- return chain.filter((l) => {
290
- if (!l) return false;
291
- const normalized = l;
292
- if (seen.has(normalized)) return false;
293
- seen.add(normalized);
323
+ const result = chain.filter((l) => {
324
+ if (!l || seen.has(l)) return false;
325
+ seen.add(l);
294
326
  return true;
295
327
  });
328
+ this.localeChainCache.set(locale, result);
329
+ return result;
296
330
  }
297
331
  hasTranslation(locale, key) {
298
332
  var _a;
@@ -327,7 +361,7 @@ var DefaultI18nEngine = class {
327
361
  }
328
362
  isRTLLocale(locale) {
329
363
  var _a;
330
- const normalized = locale.replace(/_/g, "-");
364
+ const normalized = this.normalizeLocaleTag(locale);
331
365
  const parts = normalized.split("-").filter(Boolean);
332
366
  const languageCode = (_a = parts[0]) == null ? void 0 : _a.toLowerCase();
333
367
  if (!languageCode) return false;
@@ -427,13 +461,13 @@ function useI18n() {
427
461
  }, [context]);
428
462
  }
429
463
  var escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
464
+ var TAG_REGEX = /<(\/?)([A-Za-z][\w-]*)(?:\s[^>]*?)?(\/?)>/g;
430
465
  var parseString = (input) => {
431
466
  var _a;
432
467
  const root = { type: "tag", name: "root", children: [] };
433
468
  const stack = [root];
434
- const tagRegex = /<(\/?)([A-Za-z][\w-]*)(?:\s[^>]*?)?(\/?)>/g;
435
469
  let lastIndex = 0;
436
- for (const match of input.matchAll(tagRegex)) {
470
+ for (const match of input.matchAll(TAG_REGEX)) {
437
471
  const fullMatch = match[0];
438
472
  const isClosing = match[1] === "/";
439
473
  const tagName = match[2];
@@ -463,51 +497,57 @@ var parseString = (input) => {
463
497
  }
464
498
  return root.children;
465
499
  };
500
+ var renderAST = (nodes, keyPrefix, components, placeholderToElement) => {
501
+ return nodes.map((node, index) => {
502
+ const key = `${keyPrefix}-${index}`;
503
+ if (node.type === "text") {
504
+ const placeholders = Object.keys(placeholderToElement);
505
+ if (placeholders.length === 0) return node.content;
506
+ const pattern = new RegExp(`(${placeholders.map(escapeRegExp).join("|")})`, "g");
507
+ const parts = node.content.split(pattern);
508
+ if (parts.length === 1) return node.content;
509
+ return /* @__PURE__ */ React__default.default.createElement(React.Fragment, { key }, parts.map((part, i) => {
510
+ if (placeholderToElement[part]) {
511
+ return React.cloneElement(placeholderToElement[part], { key: `${key}-${i}` });
512
+ }
513
+ return part;
514
+ }));
515
+ } else if (node.type === "tag") {
516
+ const Component = components == null ? void 0 : components[node.name];
517
+ const children = renderAST(node.children, key, components, placeholderToElement);
518
+ if (React.isValidElement(Component)) {
519
+ return React.cloneElement(Component, { key }, children.length > 0 ? children : void 0);
520
+ }
521
+ return /* @__PURE__ */ React__default.default.createElement(React.Fragment, { key }, children);
522
+ }
523
+ return null;
524
+ });
525
+ };
466
526
  var Trans = ({ i18nKey, values, components, ...props }) => {
467
527
  const { t: t2 } = useI18n();
468
- const stringValues = {};
469
- const placeholderToElement = {};
470
- if (values) {
471
- Object.keys(values).forEach((key) => {
472
- const value = values[key];
473
- if (React.isValidElement(value)) {
474
- const placeholder = `__ELEMENT_${key}__`;
475
- placeholderToElement[placeholder] = value;
476
- stringValues[key] = placeholder;
477
- } else {
478
- stringValues[key] = value == null ? "" : String(value);
479
- }
480
- });
481
- }
482
- const translatedText = t2(i18nKey, stringValues);
483
- const ast = parseString(translatedText);
484
- const renderAST = (nodes, keyPrefix) => {
485
- return nodes.map((node, index) => {
486
- const key = `${keyPrefix}-${index}`;
487
- if (node.type === "text") {
488
- const placeholders = Object.keys(placeholderToElement);
489
- if (placeholders.length === 0) return node.content;
490
- const pattern = new RegExp(`(${placeholders.map(escapeRegExp).join("|")})`, "g");
491
- const parts = node.content.split(pattern);
492
- if (parts.length === 1) return node.content;
493
- return /* @__PURE__ */ React__default.default.createElement(React.Fragment, { key }, parts.map((part, i) => {
494
- if (placeholderToElement[part]) {
495
- return React.cloneElement(placeholderToElement[part], { key: `${key}-${i}` });
496
- }
497
- return part;
498
- }));
499
- } else if (node.type === "tag") {
500
- const Component = components == null ? void 0 : components[node.name];
501
- const children2 = renderAST(node.children, key);
502
- if (React.isValidElement(Component)) {
503
- return React.cloneElement(Component, { key }, children2.length > 0 ? children2 : void 0);
528
+ const { stringValues, placeholderToElement } = React.useMemo(() => {
529
+ const sv = {};
530
+ const pte = {};
531
+ if (values) {
532
+ for (const key of Object.keys(values)) {
533
+ const value = values[key];
534
+ if (React.isValidElement(value)) {
535
+ const placeholder = `__ELEMENT_${key}__`;
536
+ pte[placeholder] = value;
537
+ sv[key] = placeholder;
538
+ } else {
539
+ sv[key] = value == null ? "" : String(value);
504
540
  }
505
- return /* @__PURE__ */ React__default.default.createElement(React.Fragment, { key }, children2);
506
541
  }
507
- return null;
508
- });
509
- };
510
- const children = renderAST(ast, "trans");
542
+ }
543
+ return { stringValues: sv, placeholderToElement: pte };
544
+ }, [values]);
545
+ const translatedText = t2(i18nKey, stringValues);
546
+ const ast = React.useMemo(() => parseString(translatedText), [translatedText]);
547
+ const children = React.useMemo(
548
+ () => renderAST(ast, "trans", components, placeholderToElement),
549
+ [ast, components, placeholderToElement]
550
+ );
511
551
  return /* @__PURE__ */ React__default.default.createElement(reactNative.Text, { ...props }, children);
512
552
  };
513
553
 
@@ -517,6 +557,12 @@ var loadTranslations = (translations) => i18nService.loadTranslations(translatio
517
557
  var setLocale = (locale) => i18nService.setLocale(locale);
518
558
  var getLocale = () => i18nService.getLocale();
519
559
  var t = (scope, options) => i18nService.t(scope, options);
560
+ var formatNumber = (n, options) => i18nService.formatNumber(n, options);
561
+ var formatCurrency = (n, currency, options) => i18nService.formatCurrency(n, currency, options);
562
+ var formatDate = (date, options) => i18nService.formatDate(date, options);
563
+ var subscribe = (listener) => i18nService.subscribe(listener);
564
+ var isRTL = () => i18nService.isRTL();
565
+ var resetToSystem = () => i18nService.resetToSystem();
520
566
  var readyI18n = () => i18nService.ready();
521
567
  var isI18nReady = () => i18nService.isReady();
522
568
  var index_default = i18nService;
@@ -525,12 +571,18 @@ exports.I18nContext = I18nContext;
525
571
  exports.I18nProvider = I18nProvider;
526
572
  exports.Trans = Trans;
527
573
  exports.default = index_default;
574
+ exports.formatCurrency = formatCurrency;
575
+ exports.formatDate = formatDate;
576
+ exports.formatNumber = formatNumber;
528
577
  exports.getLocale = getLocale;
529
578
  exports.initI18n = initI18n;
530
579
  exports.isI18nReady = isI18nReady;
580
+ exports.isRTL = isRTL;
531
581
  exports.loadTranslations = loadTranslations;
532
582
  exports.readyI18n = readyI18n;
583
+ exports.resetToSystem = resetToSystem;
533
584
  exports.setLocale = setLocale;
585
+ exports.subscribe = subscribe;
534
586
  exports.t = t;
535
587
  exports.useI18n = useI18n;
536
588
  exports.withI18n = withI18n;
package/dist/index.mjs CHANGED
@@ -46,6 +46,12 @@ var DefaultI18nEngine = class {
46
46
  __publicField(this, "version");
47
47
  __publicField(this, "readyPromise");
48
48
  __publicField(this, "resolveReady");
49
+ // --- 性能缓存 ---
50
+ /** Intl 格式化器缓存,key = locale + JSON(options) */
51
+ __publicField(this, "numberFormatCache", /* @__PURE__ */ new Map());
52
+ __publicField(this, "dateFormatCache", /* @__PURE__ */ new Map());
53
+ /** locale chain 缓存,locale 或 fallbackLocales 变化时失效 */
54
+ __publicField(this, "localeChainCache", /* @__PURE__ */ new Map());
49
55
  this.i18n = new I18n();
50
56
  this.listeners = /* @__PURE__ */ new Set();
51
57
  this.i18n.enableFallback = false;
@@ -80,6 +86,7 @@ var DefaultI18nEngine = class {
80
86
  this.localeSource = "system";
81
87
  this.missingBehavior = missingBehavior;
82
88
  this.onMissingKey = onMissingKey;
89
+ this.invalidateCaches();
83
90
  if (this.followSystem) this.updateLocale();
84
91
  this.version += 1;
85
92
  this.notifyListeners({ type: "translations", version: this.version });
@@ -115,6 +122,10 @@ var DefaultI18nEngine = class {
115
122
  getLocale() {
116
123
  return this.i18n.locale;
117
124
  }
125
+ resetToSystem() {
126
+ this.localeSource = "system";
127
+ this.updateLocale();
128
+ }
118
129
  t(scope, options) {
119
130
  var _a, _b, _c;
120
131
  if (typeof scope !== "string") {
@@ -148,7 +159,7 @@ var DefaultI18nEngine = class {
148
159
  return String(n);
149
160
  }
150
161
  try {
151
- return new Intl.NumberFormat(this.i18n.locale, options).format(n);
162
+ return this.getCachedNumberFormat(this.i18n.locale, options).format(n);
152
163
  } catch {
153
164
  return String(n);
154
165
  }
@@ -158,7 +169,7 @@ var DefaultI18nEngine = class {
158
169
  return `${n} ${currency}`;
159
170
  }
160
171
  try {
161
- return new Intl.NumberFormat(this.i18n.locale, {
172
+ return this.getCachedNumberFormat(this.i18n.locale, {
162
173
  ...options,
163
174
  style: "currency",
164
175
  currency
@@ -174,7 +185,7 @@ var DefaultI18nEngine = class {
174
185
  return d.toISOString();
175
186
  }
176
187
  try {
177
- return new Intl.DateTimeFormat(this.i18n.locale, options).format(d);
188
+ return this.getCachedDateFormat(this.i18n.locale, options).format(d);
178
189
  } catch {
179
190
  return d.toISOString();
180
191
  }
@@ -195,15 +206,40 @@ var DefaultI18nEngine = class {
195
206
  isReady() {
196
207
  return this.initialized;
197
208
  }
209
+ // ─── 缓存管理 ────────────────────────────────────────────
210
+ invalidateCaches() {
211
+ this.numberFormatCache.clear();
212
+ this.dateFormatCache.clear();
213
+ this.localeChainCache.clear();
214
+ }
215
+ getCachedNumberFormat(locale, options) {
216
+ const cacheKey = locale + (options ? JSON.stringify(options) : "");
217
+ let fmt = this.numberFormatCache.get(cacheKey);
218
+ if (!fmt) {
219
+ fmt = new Intl.NumberFormat(locale, options);
220
+ this.numberFormatCache.set(cacheKey, fmt);
221
+ }
222
+ return fmt;
223
+ }
224
+ getCachedDateFormat(locale, options) {
225
+ const cacheKey = locale + (options ? JSON.stringify(options) : "");
226
+ let fmt = this.dateFormatCache.get(cacheKey);
227
+ if (!fmt) {
228
+ fmt = new Intl.DateTimeFormat(locale, options);
229
+ this.dateFormatCache.set(cacheKey, fmt);
230
+ }
231
+ return fmt;
232
+ }
233
+ // ─── 私有方法 ────────────────────────────────────────────
198
234
  handleRTL(locale) {
199
235
  var _a, _b;
200
- const isRTL = this.isRTLLocale(locale);
236
+ const isRTL2 = this.isRTLLocale(locale);
201
237
  if (typeof ((_a = I18nManager) == null ? void 0 : _a.allowRTL) !== "function" || typeof ((_b = I18nManager) == null ? void 0 : _b.forceRTL) !== "function") {
202
238
  return;
203
239
  }
204
- if (I18nManager.isRTL !== isRTL) {
205
- I18nManager.allowRTL(isRTL);
206
- I18nManager.forceRTL(isRTL);
240
+ if (I18nManager.isRTL !== isRTL2) {
241
+ I18nManager.allowRTL(isRTL2);
242
+ I18nManager.forceRTL(isRTL2);
207
243
  }
208
244
  }
209
245
  notifyListeners(change) {
@@ -218,6 +254,7 @@ var DefaultI18nEngine = class {
218
254
  const normalizedLocale = this.normalizeLocaleTag(locale);
219
255
  if (this.i18n.locale !== normalizedLocale) {
220
256
  this.i18n.locale = normalizedLocale;
257
+ this.invalidateCaches();
221
258
  this.handleRTL(normalizedLocale);
222
259
  this.version += 1;
223
260
  this.notifyListeners({ type: "locale", version: this.version });
@@ -233,39 +270,36 @@ var DefaultI18nEngine = class {
233
270
  this.i18n.locale = prevLocale;
234
271
  }
235
272
  }
236
- getLocaleChain(locale) {
237
- const chain = [];
238
- const normalizedLocale = this.normalizeLocaleTag(locale);
239
- chain.push(normalizedLocale);
240
- const parts = normalizedLocale.split("-").filter(Boolean);
273
+ /** 将 locale 及其自动降级(如 en-US → en)追加到 chain 中 */
274
+ pushWithDegradation(chain, locale) {
275
+ const normalized = this.normalizeLocaleTag(locale);
276
+ chain.push(normalized);
277
+ const parts = normalized.split("-").filter(Boolean);
241
278
  for (let i = parts.length - 1; i >= 1; i -= 1) {
242
279
  chain.push(parts.slice(0, i).join("-"));
243
280
  }
281
+ }
282
+ getLocaleChain(locale) {
283
+ const cached = this.localeChainCache.get(locale);
284
+ if (cached) return cached;
285
+ const chain = [];
286
+ this.pushWithDegradation(chain, locale);
287
+ const normalizedLocale = this.normalizeLocaleTag(locale);
244
288
  const extra = this.fallbackLocales ? typeof this.fallbackLocales === "function" ? this.fallbackLocales(normalizedLocale) : this.fallbackLocales : [];
245
289
  for (const l of extra) {
246
- const normalizedFallback = this.normalizeLocaleTag(l);
247
- chain.push(normalizedFallback);
248
- const fallbackParts = normalizedFallback.split("-").filter(Boolean);
249
- for (let i = fallbackParts.length - 1; i >= 1; i -= 1) {
250
- chain.push(fallbackParts.slice(0, i).join("-"));
251
- }
290
+ this.pushWithDegradation(chain, l);
252
291
  }
253
292
  if (this.i18n.defaultLocale) {
254
- const normalizedDefault = this.normalizeLocaleTag(this.i18n.defaultLocale);
255
- chain.push(normalizedDefault);
256
- const defaultParts = normalizedDefault.split("-").filter(Boolean);
257
- for (let i = defaultParts.length - 1; i >= 1; i -= 1) {
258
- chain.push(defaultParts.slice(0, i).join("-"));
259
- }
293
+ this.pushWithDegradation(chain, this.i18n.defaultLocale);
260
294
  }
261
295
  const seen = /* @__PURE__ */ new Set();
262
- return chain.filter((l) => {
263
- if (!l) return false;
264
- const normalized = l;
265
- if (seen.has(normalized)) return false;
266
- seen.add(normalized);
296
+ const result = chain.filter((l) => {
297
+ if (!l || seen.has(l)) return false;
298
+ seen.add(l);
267
299
  return true;
268
300
  });
301
+ this.localeChainCache.set(locale, result);
302
+ return result;
269
303
  }
270
304
  hasTranslation(locale, key) {
271
305
  var _a;
@@ -300,7 +334,7 @@ var DefaultI18nEngine = class {
300
334
  }
301
335
  isRTLLocale(locale) {
302
336
  var _a;
303
- const normalized = locale.replace(/_/g, "-");
337
+ const normalized = this.normalizeLocaleTag(locale);
304
338
  const parts = normalized.split("-").filter(Boolean);
305
339
  const languageCode = (_a = parts[0]) == null ? void 0 : _a.toLowerCase();
306
340
  if (!languageCode) return false;
@@ -400,13 +434,13 @@ function useI18n() {
400
434
  }, [context]);
401
435
  }
402
436
  var escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
437
+ var TAG_REGEX = /<(\/?)([A-Za-z][\w-]*)(?:\s[^>]*?)?(\/?)>/g;
403
438
  var parseString = (input) => {
404
439
  var _a;
405
440
  const root = { type: "tag", name: "root", children: [] };
406
441
  const stack = [root];
407
- const tagRegex = /<(\/?)([A-Za-z][\w-]*)(?:\s[^>]*?)?(\/?)>/g;
408
442
  let lastIndex = 0;
409
- for (const match of input.matchAll(tagRegex)) {
443
+ for (const match of input.matchAll(TAG_REGEX)) {
410
444
  const fullMatch = match[0];
411
445
  const isClosing = match[1] === "/";
412
446
  const tagName = match[2];
@@ -436,51 +470,57 @@ var parseString = (input) => {
436
470
  }
437
471
  return root.children;
438
472
  };
473
+ var renderAST = (nodes, keyPrefix, components, placeholderToElement) => {
474
+ return nodes.map((node, index) => {
475
+ const key = `${keyPrefix}-${index}`;
476
+ if (node.type === "text") {
477
+ const placeholders = Object.keys(placeholderToElement);
478
+ if (placeholders.length === 0) return node.content;
479
+ const pattern = new RegExp(`(${placeholders.map(escapeRegExp).join("|")})`, "g");
480
+ const parts = node.content.split(pattern);
481
+ if (parts.length === 1) return node.content;
482
+ return /* @__PURE__ */ React.createElement(Fragment, { key }, parts.map((part, i) => {
483
+ if (placeholderToElement[part]) {
484
+ return cloneElement(placeholderToElement[part], { key: `${key}-${i}` });
485
+ }
486
+ return part;
487
+ }));
488
+ } else if (node.type === "tag") {
489
+ const Component = components == null ? void 0 : components[node.name];
490
+ const children = renderAST(node.children, key, components, placeholderToElement);
491
+ if (isValidElement(Component)) {
492
+ return cloneElement(Component, { key }, children.length > 0 ? children : void 0);
493
+ }
494
+ return /* @__PURE__ */ React.createElement(Fragment, { key }, children);
495
+ }
496
+ return null;
497
+ });
498
+ };
439
499
  var Trans = ({ i18nKey, values, components, ...props }) => {
440
500
  const { t: t2 } = useI18n();
441
- const stringValues = {};
442
- const placeholderToElement = {};
443
- if (values) {
444
- Object.keys(values).forEach((key) => {
445
- const value = values[key];
446
- if (isValidElement(value)) {
447
- const placeholder = `__ELEMENT_${key}__`;
448
- placeholderToElement[placeholder] = value;
449
- stringValues[key] = placeholder;
450
- } else {
451
- stringValues[key] = value == null ? "" : String(value);
452
- }
453
- });
454
- }
455
- const translatedText = t2(i18nKey, stringValues);
456
- const ast = parseString(translatedText);
457
- const renderAST = (nodes, keyPrefix) => {
458
- return nodes.map((node, index) => {
459
- const key = `${keyPrefix}-${index}`;
460
- if (node.type === "text") {
461
- const placeholders = Object.keys(placeholderToElement);
462
- if (placeholders.length === 0) return node.content;
463
- const pattern = new RegExp(`(${placeholders.map(escapeRegExp).join("|")})`, "g");
464
- const parts = node.content.split(pattern);
465
- if (parts.length === 1) return node.content;
466
- return /* @__PURE__ */ React.createElement(Fragment, { key }, parts.map((part, i) => {
467
- if (placeholderToElement[part]) {
468
- return cloneElement(placeholderToElement[part], { key: `${key}-${i}` });
469
- }
470
- return part;
471
- }));
472
- } else if (node.type === "tag") {
473
- const Component = components == null ? void 0 : components[node.name];
474
- const children2 = renderAST(node.children, key);
475
- if (isValidElement(Component)) {
476
- return cloneElement(Component, { key }, children2.length > 0 ? children2 : void 0);
501
+ const { stringValues, placeholderToElement } = useMemo(() => {
502
+ const sv = {};
503
+ const pte = {};
504
+ if (values) {
505
+ for (const key of Object.keys(values)) {
506
+ const value = values[key];
507
+ if (isValidElement(value)) {
508
+ const placeholder = `__ELEMENT_${key}__`;
509
+ pte[placeholder] = value;
510
+ sv[key] = placeholder;
511
+ } else {
512
+ sv[key] = value == null ? "" : String(value);
477
513
  }
478
- return /* @__PURE__ */ React.createElement(Fragment, { key }, children2);
479
514
  }
480
- return null;
481
- });
482
- };
483
- const children = renderAST(ast, "trans");
515
+ }
516
+ return { stringValues: sv, placeholderToElement: pte };
517
+ }, [values]);
518
+ const translatedText = t2(i18nKey, stringValues);
519
+ const ast = useMemo(() => parseString(translatedText), [translatedText]);
520
+ const children = useMemo(
521
+ () => renderAST(ast, "trans", components, placeholderToElement),
522
+ [ast, components, placeholderToElement]
523
+ );
484
524
  return /* @__PURE__ */ React.createElement(Text, { ...props }, children);
485
525
  };
486
526
 
@@ -490,8 +530,14 @@ var loadTranslations = (translations) => i18nService.loadTranslations(translatio
490
530
  var setLocale = (locale) => i18nService.setLocale(locale);
491
531
  var getLocale = () => i18nService.getLocale();
492
532
  var t = (scope, options) => i18nService.t(scope, options);
533
+ var formatNumber = (n, options) => i18nService.formatNumber(n, options);
534
+ var formatCurrency = (n, currency, options) => i18nService.formatCurrency(n, currency, options);
535
+ var formatDate = (date, options) => i18nService.formatDate(date, options);
536
+ var subscribe = (listener) => i18nService.subscribe(listener);
537
+ var isRTL = () => i18nService.isRTL();
538
+ var resetToSystem = () => i18nService.resetToSystem();
493
539
  var readyI18n = () => i18nService.ready();
494
540
  var isI18nReady = () => i18nService.isReady();
495
541
  var index_default = i18nService;
496
542
 
497
- export { I18nContext, I18nProvider, Trans, index_default as default, getLocale, initI18n, isI18nReady, loadTranslations, readyI18n, setLocale, t, useI18n, withI18n };
543
+ export { I18nContext, I18nProvider, Trans, index_default as default, formatCurrency, formatDate, formatNumber, getLocale, initI18n, isI18nReady, isRTL, loadTranslations, readyI18n, resetToSystem, setLocale, subscribe, t, useI18n, withI18n };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-i18njs",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "一个轻量级、类型安全、零心智负担的 React Native 国际化解决方案。",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,6 +8,17 @@
8
8
  },
9
9
  "license": "ISC",
10
10
  "author": "wangws",
11
+ "keywords": [
12
+ "react-native",
13
+ "i18n",
14
+ "internationalization",
15
+ "localization",
16
+ "l10n",
17
+ "translation",
18
+ "locale",
19
+ "react",
20
+ "typescript"
21
+ ],
11
22
  "sideEffects": false,
12
23
  "main": "dist/index.js",
13
24
  "module": "dist/index.mjs",