react-native-i18njs 0.0.1 → 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 +15 -0
- package/README.md +176 -158
- package/dist/index.d.mts +21 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.js +125 -73
- package/dist/index.mjs +120 -74
- package/package.json +17 -6
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
|
@@ -1,97 +1,76 @@
|
|
|
1
1
|
# react-native-i18njs
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/react-native-i18njs)
|
|
4
|
+
[](https://www.npmjs.com/package/react-native-i18njs)
|
|
5
|
+
[](https://reactnative.dev)
|
|
6
|
+
[](https://www.typescriptlang.org)
|
|
7
|
+
|
|
3
8
|
一个轻量级、类型安全、零心智负担的 React Native 国际化解决方案。
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
专为 React Native 设计,集成了最佳实践,解决了常见的国际化痛点:繁琐的配置、类型缺失、复杂的 API 以及系统语言跟随问题。
|
|
6
11
|
|
|
7
|
-
|
|
8
|
-
- 📦 **自动语言检测**:基于 `react-native-localize` 自动匹配设备语言。
|
|
9
|
-
- 🔄 **自动重渲染**:语言切换时,UI 自动更新。
|
|
10
|
-
- 🛡️ **TypeScript 支持**:完全使用 TypeScript 编写,提供完整的类型提示。
|
|
11
|
-
- 🧩 **基于 `i18n-js`**:轻量封装,默认使用 `i18n-js` 作为翻译引擎。
|
|
12
|
-
- 🔌 **Hooks 支持**:提供 `useI18n` Hook,方便在组件中使用。
|
|
13
|
-
- 📝 **富文本支持**:支持在翻译中嵌入组件。
|
|
14
|
-
- 🔢 **格式化支持**:基于 JS 内置 `Intl` 的数字/货币/日期格式化(可传 options 自定义;环境不支持会自动降级)。
|
|
12
|
+
## ✨ 特性
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
- � **零配置启动**:内置智能默认值,安装即用。
|
|
15
|
+
- 🛡️ **极致类型安全**:完全 TypeScript 编写,提供从 Key 到插值参数的完整类型推导。
|
|
16
|
+
- 📱 **自动跟随系统**:基于 `react-native-localize`,自动检测并响应设备语言变更。
|
|
17
|
+
- ⚡ **高性能**:基于 `i18n-js` 核心,轻量高效,无多余运行时开销。
|
|
18
|
+
- 🔌 **灵活 API**:同时支持 Hook (`useI18n`)、高阶组件 (`withI18n`) 和全局函数 (`t`)。
|
|
19
|
+
- 📝 **富文本支持**:`Trans` 组件轻松处理嵌套样式和组件插值。
|
|
20
|
+
- 🌍 **格式化内置**:开箱即用的数字、货币、日期格式化支持。
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
## 📦 安装
|
|
19
23
|
|
|
20
24
|
```bash
|
|
21
|
-
npm install react-native-i18njs
|
|
25
|
+
npm install react-native-i18njs
|
|
22
26
|
# 或者
|
|
23
|
-
yarn add react-native-i18njs
|
|
27
|
+
yarn add react-native-i18njs
|
|
24
28
|
```
|
|
25
29
|
|
|
26
|
-
>
|
|
27
|
-
|
|
28
|
-
## 上手方式(两种都支持)
|
|
30
|
+
> **注意**:本库已内置 `i18n-js` 和 `react-native-localize` 的稳定版本,无需手动安装 peer dependencies。
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
- **方式 B:使用 `I18nProvider`(可选)**:需要“切换语言后 UI 自动刷新”时再用。
|
|
32
|
+
## 🚀 快速开始
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
### 1. 定义翻译资源
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
建议在单独的文件中管理翻译资源,例如 `src/locales/index.ts`:
|
|
36
37
|
|
|
37
38
|
```ts
|
|
39
|
+
// src/locales/index.ts
|
|
38
40
|
export const translations = {
|
|
39
|
-
en: {
|
|
40
|
-
|
|
41
|
+
en: {
|
|
42
|
+
welcome: 'Welcome',
|
|
43
|
+
hello: 'Hello, %{name}!',
|
|
44
|
+
},
|
|
45
|
+
zh: {
|
|
46
|
+
welcome: '欢迎',
|
|
47
|
+
hello: '你好,%{name}!',
|
|
48
|
+
},
|
|
41
49
|
};
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
### 2) 初始化(只做一次)
|
|
45
|
-
|
|
46
|
-
```ts
|
|
47
|
-
import { initI18n } from 'react-native-i18njs';
|
|
48
|
-
import { translations } from './locales';
|
|
49
|
-
|
|
50
|
-
initI18n(translations, { defaultLocale: 'en', enableFallback: true });
|
|
51
|
-
```
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
```ts
|
|
56
|
-
import { t, setLocale, getLocale } from 'react-native-i18njs';
|
|
57
|
-
|
|
58
|
-
setLocale('zh');
|
|
59
|
-
getLocale(); // 'zh'
|
|
60
|
-
t('hello', { name: 'Trae' }); // '你好,Trae!'
|
|
61
|
-
t('missing.key', { defaultValue: '默认文案' });
|
|
51
|
+
// 导出类型以获得类型提示
|
|
52
|
+
export type Translations = typeof translations.en;
|
|
62
53
|
```
|
|
63
54
|
|
|
64
|
-
###
|
|
55
|
+
### 2. 初始化
|
|
65
56
|
|
|
66
|
-
|
|
67
|
-
- 表单校验/工具函数/Toast 文案
|
|
68
|
-
- Redux/Saga/Store 初始化阶段
|
|
69
|
-
- React 组件之外(比如 navigation 配置、原生桥接回调)
|
|
70
|
-
|
|
71
|
-
> 注意:不用 Provider 时,切换语言不会自动触发页面重渲染;如果你需要 UI 自动刷新,请看“模块 2”。
|
|
72
|
-
|
|
73
|
-
## 模块 2:React 自动重渲染(I18nProvider + useI18n)
|
|
57
|
+
在你的 App 入口文件(如 `App.tsx`)中初始化:
|
|
74
58
|
|
|
75
59
|
```tsx
|
|
76
60
|
import React from 'react';
|
|
77
|
-
import {
|
|
78
|
-
import {
|
|
61
|
+
import { initI18n, I18nProvider } from 'react-native-i18njs';
|
|
62
|
+
import { translations } from './src/locales';
|
|
63
|
+
import Home from './src/Home';
|
|
79
64
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
<Text>{t('hello', { name: 'User' })}</Text>
|
|
86
|
-
<Text>{locale}</Text>
|
|
87
|
-
<Button title="EN" onPress={() => setLocale('en')} />
|
|
88
|
-
<Button title="中文" onPress={() => setLocale('zh')} />
|
|
89
|
-
</>
|
|
90
|
-
);
|
|
91
|
-
}
|
|
65
|
+
// 初始化配置
|
|
66
|
+
initI18n(translations, {
|
|
67
|
+
defaultLocale: 'en',
|
|
68
|
+
enableFallback: true, // 找不到翻译时回退到默认语言
|
|
69
|
+
});
|
|
92
70
|
|
|
93
71
|
export default function App() {
|
|
94
72
|
return (
|
|
73
|
+
// 使用 Provider 以支持语言切换时的自动重渲染
|
|
95
74
|
<I18nProvider>
|
|
96
75
|
<Home />
|
|
97
76
|
</I18nProvider>
|
|
@@ -99,149 +78,188 @@ export default function App() {
|
|
|
99
78
|
}
|
|
100
79
|
```
|
|
101
80
|
|
|
102
|
-
###
|
|
81
|
+
### 3. 在组件中使用
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
// src/Home.tsx
|
|
85
|
+
import React from 'react';
|
|
86
|
+
import { Text, Button, View } from 'react-native';
|
|
87
|
+
import { useI18n } from 'react-native-i18njs';
|
|
88
|
+
import { Translations } from './locales';
|
|
89
|
+
|
|
90
|
+
export default function Home() {
|
|
91
|
+
// 传入泛型 Translations 以获得 key 的自动补全和类型检查
|
|
92
|
+
const { t, locale, setLocale } = useI18n<Translations>();
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<View>
|
|
96
|
+
<Text>{t('welcome')}</Text>
|
|
97
|
+
{/* 这里的 name 参数会有类型提示 */}
|
|
98
|
+
<Text>{t('hello', { name: 'Trae' })}</Text>
|
|
99
|
+
|
|
100
|
+
<Text>当前语言: {locale}</Text>
|
|
101
|
+
|
|
102
|
+
<Button title="Switch to English" onPress={() => setLocale('en')} />
|
|
103
|
+
<Button title="切换到中文" onPress={() => setLocale('zh')} />
|
|
104
|
+
</View>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## 📖 核心功能详解
|
|
110
|
+
|
|
111
|
+
### 1. 非组件环境使用(Global API)
|
|
103
112
|
|
|
104
|
-
|
|
105
|
-
- `fallback?: React.ReactNode`:`readyGate` 期间显示的占位
|
|
113
|
+
在 Redux、Axios 拦截器、工具函数等非组件环境中,你可以直接使用全局导出的 API。
|
|
106
114
|
|
|
107
|
-
|
|
115
|
+
#### 基础用法
|
|
108
116
|
|
|
109
117
|
```ts
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
118
|
+
import { t, getLocale, setLocale } from 'react-native-i18njs';
|
|
119
|
+
|
|
120
|
+
// 获取当前语言
|
|
121
|
+
const current = getLocale();
|
|
122
|
+
|
|
123
|
+
// 切换语言
|
|
124
|
+
setLocale('zh');
|
|
125
|
+
|
|
126
|
+
// 直接翻译
|
|
127
|
+
const message = t('errors.network_timeout');
|
|
114
128
|
```
|
|
115
129
|
|
|
116
|
-
|
|
130
|
+
#### 进阶:监听语言变化
|
|
131
|
+
|
|
132
|
+
如果你需要在组件外监听语言变更(例如同步更新全局状态),可以使用顶层 `subscribe` 函数:
|
|
117
133
|
|
|
118
134
|
```ts
|
|
119
|
-
import {
|
|
135
|
+
import { subscribe } from 'react-native-i18njs';
|
|
120
136
|
|
|
121
|
-
|
|
137
|
+
// 订阅语言变更
|
|
138
|
+
const unsubscribe = subscribe((locale) => {
|
|
139
|
+
console.log('Language changed to:', locale);
|
|
140
|
+
// 更新 API 默认 Header 或其他全局状态
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// 取消订阅
|
|
144
|
+
// unsubscribe();
|
|
122
145
|
```
|
|
123
146
|
|
|
124
|
-
|
|
147
|
+
#### 进阶:重置为跟随系统
|
|
125
148
|
|
|
126
|
-
|
|
127
|
-
import React from 'react';
|
|
128
|
-
import { Text } from 'react-native';
|
|
129
|
-
import { Trans } from 'react-native-i18njs';
|
|
149
|
+
用户手动调用 `setLocale` 后会锁定语言,不再自动跟随系统。如果需要恢复跟随系统语言:
|
|
130
150
|
|
|
131
|
-
|
|
132
|
-
|
|
151
|
+
```ts
|
|
152
|
+
import { resetToSystem } from 'react-native-i18njs';
|
|
133
153
|
|
|
134
|
-
|
|
135
|
-
|
|
154
|
+
// 撤销用户锁定,重新跟随系统语言
|
|
155
|
+
resetToSystem();
|
|
136
156
|
```
|
|
137
157
|
|
|
138
|
-
|
|
158
|
+
#### 进阶:RTL 检测
|
|
139
159
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
- `components?: Record<string, React.ReactNode>`:标签映射(如 `bold/link`)
|
|
160
|
+
```ts
|
|
161
|
+
import { isRTL } from 'react-native-i18njs';
|
|
143
162
|
|
|
144
|
-
|
|
163
|
+
if (isRTL()) {
|
|
164
|
+
// 当前为从右到左语言(如阿拉伯语、希伯来语)
|
|
165
|
+
}
|
|
166
|
+
```
|
|
145
167
|
|
|
146
|
-
|
|
147
|
-
- 插值里插入组件(用 `values` 传 ReactElement,例如把 “Terms” 做成可点击文本)
|
|
148
|
-
- 嵌套标签(例如 `<bold>...<italic>...</italic>...</bold>`)
|
|
149
|
-
- 自闭合标签(例如 `<br/>`),可映射成 `components={{ br: <Text>{'\n'}</Text> }}`
|
|
168
|
+
#### 实战示例:Axios 拦截器
|
|
150
169
|
|
|
151
|
-
|
|
170
|
+
```ts
|
|
171
|
+
import axios from 'axios';
|
|
172
|
+
import { getLocale } from 'react-native-i18njs';
|
|
173
|
+
|
|
174
|
+
axios.interceptors.request.use((config) => {
|
|
175
|
+
// 动态获取当前语言,确保每次请求都携带最新的语言标识
|
|
176
|
+
config.headers['Accept-Language'] = getLocale();
|
|
177
|
+
return config;
|
|
178
|
+
});
|
|
179
|
+
```
|
|
152
180
|
|
|
153
|
-
|
|
154
|
-
- `components` 目前接收的是“元素”(例如 `<Text />`),不是组件函数(例如 `Text` / `Bold`),因为内部是 `cloneElement`
|
|
155
|
-
- `Trans` 总是返回一个外层 `<Text>`;并且被插入的组件也必须是 `Text` 可渲染的内容(不要把 `<View>` 直接塞进文本里)
|
|
156
|
-
- 不提供 ICU MessageFormat(select/plural/gender)语法;更复杂的语言结构建议拆 key + 用 JS 逻辑组合,或引入 ICU 方案
|
|
157
|
-
- 翻译里如果出现不成对/不匹配的标签,会按纯文本保留(避免静默吞字),建议在翻译侧保持标签成对
|
|
181
|
+
### 2. 富文本翻译 (`Trans` 组件)
|
|
158
182
|
|
|
159
|
-
|
|
183
|
+
当翻译内容中包含样式或组件时,使用 `Trans` 组件:
|
|
160
184
|
|
|
161
185
|
```tsx
|
|
162
|
-
import
|
|
186
|
+
import { Trans } from 'react-native-i18njs';
|
|
163
187
|
import { Text } from 'react-native';
|
|
164
|
-
import { withI18n } from 'react-native-i18njs';
|
|
165
188
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
return <Text>{this.props.t('welcome')}</Text>;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
189
|
+
// 翻译资源:
|
|
190
|
+
// zh: { agreement: '我同意 <link>服务条款</link>' }
|
|
171
191
|
|
|
172
|
-
|
|
192
|
+
<Trans
|
|
193
|
+
i18nKey="agreement"
|
|
194
|
+
components={{
|
|
195
|
+
link: <Text style={{ color: 'blue' }} onPress={openTerms} />,
|
|
196
|
+
}}
|
|
197
|
+
/>
|
|
173
198
|
```
|
|
174
199
|
|
|
175
|
-
|
|
200
|
+
### 3. 动态加载翻译
|
|
201
|
+
|
|
202
|
+
适用于大型应用的分包加载场景:
|
|
176
203
|
|
|
177
204
|
```ts
|
|
178
|
-
import {
|
|
205
|
+
import { loadTranslations } from 'react-native-i18njs';
|
|
179
206
|
|
|
180
|
-
|
|
181
|
-
|
|
207
|
+
// 异步加载法语包
|
|
208
|
+
async function loadFrench() {
|
|
209
|
+
const fr = await import('./locales/fr');
|
|
210
|
+
loadTranslations({ fr: fr.default });
|
|
211
|
+
}
|
|
182
212
|
```
|
|
183
213
|
|
|
184
|
-
|
|
214
|
+
### 4. 格式化工具
|
|
185
215
|
|
|
186
|
-
`Intl`
|
|
187
|
-
|
|
188
|
-
这三个方法基于 `Intl`,默认使用“当前 locale”来格式化,并且都支持传入 `options` 自定义格式。
|
|
216
|
+
利用 `Intl` 标准进行格式化,在组件中通过 Hook 使用:
|
|
189
217
|
|
|
190
218
|
```ts
|
|
191
|
-
import { useI18n } from 'react-native-i18njs';
|
|
192
|
-
|
|
193
219
|
const { formatNumber, formatCurrency, formatDate } = useI18n();
|
|
194
220
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
formatDate(Date.now(), { dateStyle: 'medium', timeStyle: 'short' });
|
|
198
|
-
```
|
|
221
|
+
// 数字
|
|
222
|
+
formatNumber(1234.56); // "1,234.56"
|
|
199
223
|
|
|
200
|
-
|
|
224
|
+
// 货币
|
|
225
|
+
formatCurrency(99.99, 'USD'); // "$99.99"
|
|
201
226
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
- date:`toISOString()`
|
|
206
|
-
- 如果你希望所有平台表现完全一致,请在你的 App 侧引入 `Intl` polyfill。
|
|
207
|
-
- 如果你想用“非当前 locale”做格式化,请直接使用 `new Intl.NumberFormat('xx', options)` 自行处理。
|
|
227
|
+
// 日期
|
|
228
|
+
formatDate(new Date(), { dateStyle: 'full' }); // "Tuesday, October 10, 2023"
|
|
229
|
+
```
|
|
208
230
|
|
|
209
|
-
|
|
231
|
+
在非组件环境中,也可以直接使用顶层导出:
|
|
210
232
|
|
|
211
|
-
|
|
233
|
+
```ts
|
|
234
|
+
import { formatNumber, formatCurrency, formatDate } from 'react-native-i18njs';
|
|
212
235
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
- `followSystem?: boolean`:默认 `true`;用户手动 `setLocale` 后不再跟随系统
|
|
218
|
-
- `fallbackLocales?: string[] | ((locale: string) => string[])`:自定义回退链(按顺序)
|
|
219
|
-
- `missingBehavior?: 'key' | 'empty' | 'throw'`:默认 `'key'`
|
|
220
|
-
- `onMissingKey?: (key: string, locale: string) => void`:缺失 key 回调
|
|
221
|
-
- `onLocaleChange?: (locale: string) => void`:语言变更回调
|
|
236
|
+
formatNumber(1234.56);
|
|
237
|
+
formatCurrency(99.99, 'USD');
|
|
238
|
+
formatDate(new Date());
|
|
239
|
+
```
|
|
222
240
|
|
|
223
|
-
##
|
|
241
|
+
## ⚙️ 配置选项 (I18nOptions)
|
|
224
242
|
|
|
225
|
-
|
|
226
|
-
- `initI18n`, `t`, `setLocale`, `getLocale`, `loadTranslations`, `readyI18n`, `isI18nReady`
|
|
227
|
-
- **React 绑定(可选)**
|
|
228
|
-
- `I18nProvider`, `useI18n`, `I18nContext`, `withI18n`
|
|
229
|
-
- **组件**
|
|
230
|
-
- `Trans`
|
|
231
|
-
- **类型**
|
|
232
|
-
- `I18nOptions`, `Translations`, `Path`
|
|
233
|
-
- **默认导出**
|
|
234
|
-
- `i18nService`:服务单例(`formatNumber/formatCurrency/formatDate/subscribe/updateLocale` 等)
|
|
243
|
+
`initI18n` 接受的第二个参数对象:
|
|
235
244
|
|
|
236
|
-
|
|
245
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
246
|
+
|------|------|--------|------|
|
|
247
|
+
| `defaultLocale` | `string` | `'en'` | 默认语言 |
|
|
248
|
+
| `enableFallback` | `boolean` | `true` | 是否启用回退机制 |
|
|
249
|
+
| `followSystem` | `boolean` | `true` | 是否初始化时自动跟随系统语言 |
|
|
250
|
+
| `fallbackLocales` | `string[]` \| `func` | - | 自定义回退链 |
|
|
251
|
+
| `missingBehavior` | `'key'` \| `'empty'` \| `'throw'` | `'key'` | 缺失翻译时的行为 |
|
|
252
|
+
| `onMissingKey` | `function` | - | 缺失 key 的回调 |
|
|
253
|
+
| `onLocaleChange` | `function` | - | 语言变更回调 |
|
|
237
254
|
|
|
238
|
-
|
|
255
|
+
## 🧩 常见问题
|
|
239
256
|
|
|
240
|
-
|
|
257
|
+
### TypeScript 类型提示不工作?
|
|
258
|
+
确保你在使用 `useI18n<MyTranslations>()` 时传入了你的翻译类型定义。
|
|
241
259
|
|
|
242
|
-
|
|
260
|
+
### 安卓上语言检测不准确?
|
|
261
|
+
请确保你的 `android/app/src/main/res` 目录下有对应的语言资源文件夹(如 `values-zh`),React Native 有时依赖这些原生配置来正确识别系统语言。
|
|
243
262
|
|
|
244
|
-
##
|
|
263
|
+
## 📄 License
|
|
245
264
|
|
|
246
|
-
|
|
247
|
-
- **Web**: 需要配置 `webpack` 以支持 `react-native-localize`。请参考 [react-native-localize 文档](https://github.com/zoontek/react-native-localize)。
|
|
265
|
+
ISC
|
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
|
|
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
|
|
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
|
|
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
|
|
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 !==
|
|
232
|
-
reactNative.I18nManager.allowRTL(
|
|
233
|
-
reactNative.I18nManager.forceRTL(
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
chain.push(
|
|
267
|
-
const parts =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
290
|
-
if (!l) return false;
|
|
291
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
508
|
-
}
|
|
509
|
-
};
|
|
510
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 !==
|
|
205
|
-
I18nManager.allowRTL(
|
|
206
|
-
I18nManager.forceRTL(
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
chain.push(
|
|
240
|
-
const parts =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
263
|
-
if (!l) return false;
|
|
264
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
481
|
-
}
|
|
482
|
-
};
|
|
483
|
-
const
|
|
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.
|
|
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",
|
|
@@ -32,11 +43,13 @@
|
|
|
32
43
|
"build": "tsup",
|
|
33
44
|
"prepack": "npm run build"
|
|
34
45
|
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"i18n-js": "^4.5.1",
|
|
48
|
+
"react-native-localize": "^3.6.1"
|
|
49
|
+
},
|
|
35
50
|
"peerDependencies": {
|
|
36
|
-
"i18n-js": ">=4 <5",
|
|
37
51
|
"react": ">=16.8",
|
|
38
|
-
"react-native": ">=0.64"
|
|
39
|
-
"react-native-localize": ">=3 <4"
|
|
52
|
+
"react-native": ">=0.64"
|
|
40
53
|
},
|
|
41
54
|
"devDependencies": {
|
|
42
55
|
"@types/jest": "^30.0.0",
|
|
@@ -44,11 +57,9 @@
|
|
|
44
57
|
"@types/react-native": "^0.72.8",
|
|
45
58
|
"@types/react-test-renderer": "^19.1.0",
|
|
46
59
|
"esbuild": "^0.25.0",
|
|
47
|
-
"i18n-js": "^4.5.1",
|
|
48
60
|
"jest": "^30.2.0",
|
|
49
61
|
"react": "^19.2.3",
|
|
50
62
|
"react-native": "^0.83.1",
|
|
51
|
-
"react-native-localize": "^3.6.1",
|
|
52
63
|
"react-native-web": "^0.20.0",
|
|
53
64
|
"react-test-renderer": "^19.2.3",
|
|
54
65
|
"ts-jest": "^29.4.6",
|