@xiping/react-i18n 1.0.7
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 +21 -0
- package/README.md +206 -0
- package/dist/index.d.mts +150 -0
- package/dist/index.d.ts +150 -0
- package/dist/index.js +201 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +159 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023-present, xiping.wang
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# @xiping/react-i18n
|
|
2
|
+
|
|
3
|
+
一个基于Zustand的React国际化库,支持多语言、本地和远程资源加载。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- 基于Zustand的状态管理
|
|
8
|
+
- 支持多语言切换
|
|
9
|
+
- 支持本地和远程资源加载
|
|
10
|
+
- TypeScript类型友好
|
|
11
|
+
- 支持参数替换
|
|
12
|
+
- 支持本地存储语言偏好
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @xiping/react-i18n
|
|
18
|
+
# 或
|
|
19
|
+
yarn add @xiping/react-i18n
|
|
20
|
+
# 或
|
|
21
|
+
pnpm add @xiping/react-i18n
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 基本用法
|
|
25
|
+
|
|
26
|
+
### 1. 设置I18nProvider
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import { I18nProvider } from '@xiping/react-i18n';
|
|
30
|
+
|
|
31
|
+
// 本地资源
|
|
32
|
+
const localResources = {
|
|
33
|
+
'zh-CN': {
|
|
34
|
+
hello: '你好',
|
|
35
|
+
welcome: '欢迎, {name}!',
|
|
36
|
+
},
|
|
37
|
+
'en-US': {
|
|
38
|
+
hello: 'Hello',
|
|
39
|
+
welcome: 'Welcome, {name}!',
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// 创建本地资源加载器
|
|
44
|
+
const resourceLoader = (locale: string) => localResources[locale];
|
|
45
|
+
|
|
46
|
+
function App() {
|
|
47
|
+
return (
|
|
48
|
+
<I18nProvider
|
|
49
|
+
options={{
|
|
50
|
+
defaultLocale: 'zh-CN',
|
|
51
|
+
locales: ['zh-CN', 'en-US'],
|
|
52
|
+
resourceLoader,
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
<YourApp />
|
|
56
|
+
</I18nProvider>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 2. 在组件中使用
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { useI18n } from '@xiping/react-i18n';
|
|
65
|
+
|
|
66
|
+
function Greeting() {
|
|
67
|
+
const { t, locale, setLocale } = useI18n();
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div>
|
|
71
|
+
<h1>{t('hello')}</h1>
|
|
72
|
+
<p>{t('welcome', { name: 'John' })}</p>
|
|
73
|
+
|
|
74
|
+
<div>
|
|
75
|
+
<button
|
|
76
|
+
onClick={() => setLocale('zh-CN')}
|
|
77
|
+
disabled={locale === 'zh-CN'}
|
|
78
|
+
>
|
|
79
|
+
中文
|
|
80
|
+
</button>
|
|
81
|
+
<button
|
|
82
|
+
onClick={() => setLocale('en-US')}
|
|
83
|
+
disabled={locale === 'en-US'}
|
|
84
|
+
>
|
|
85
|
+
English
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## 高级用法
|
|
94
|
+
|
|
95
|
+
### 远程资源加载
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
import { I18nProvider, createRemoteResourceLoader } from '@xiping/react-i18n';
|
|
99
|
+
|
|
100
|
+
// 创建远程资源加载器
|
|
101
|
+
const resourceLoader = createRemoteResourceLoader('https://api.example.com/i18n');
|
|
102
|
+
|
|
103
|
+
function App() {
|
|
104
|
+
return (
|
|
105
|
+
<I18nProvider
|
|
106
|
+
options={{
|
|
107
|
+
defaultLocale: 'zh-CN',
|
|
108
|
+
locales: ['zh-CN', 'en-US'],
|
|
109
|
+
resourceLoader,
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
<YourApp />
|
|
113
|
+
</I18nProvider>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 混合资源加载(本地+远程)
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
import { I18nProvider, createHybridResourceLoader } from '@xiping/react-i18n';
|
|
122
|
+
|
|
123
|
+
// 本地资源
|
|
124
|
+
const localResources = {
|
|
125
|
+
'zh-CN': {
|
|
126
|
+
hello: '你好',
|
|
127
|
+
},
|
|
128
|
+
'en-US': {
|
|
129
|
+
hello: 'Hello',
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// 创建混合资源加载器
|
|
134
|
+
const resourceLoader = createHybridResourceLoader(
|
|
135
|
+
localResources,
|
|
136
|
+
'https://api.example.com/i18n'
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
function App() {
|
|
140
|
+
return (
|
|
141
|
+
<I18nProvider
|
|
142
|
+
options={{
|
|
143
|
+
defaultLocale: 'zh-CN',
|
|
144
|
+
locales: ['zh-CN', 'en-US'],
|
|
145
|
+
resourceLoader,
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
<YourApp />
|
|
149
|
+
</I18nProvider>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 自定义存储键名
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
<I18nProvider
|
|
158
|
+
options={{
|
|
159
|
+
defaultLocale: 'zh-CN',
|
|
160
|
+
locales: ['zh-CN', 'en-US'],
|
|
161
|
+
resourceLoader,
|
|
162
|
+
storageKey: 'my_app_locale', // 自定义存储键名
|
|
163
|
+
loadFromStorage: true, // 是否从本地存储加载语言设置
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
<YourApp />
|
|
167
|
+
</I18nProvider>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## API
|
|
171
|
+
|
|
172
|
+
### I18nProvider
|
|
173
|
+
|
|
174
|
+
i18n提供者组件,用于包装应用程序。
|
|
175
|
+
|
|
176
|
+
#### 属性
|
|
177
|
+
|
|
178
|
+
- `options`: i18n配置选项
|
|
179
|
+
- `defaultLocale`: 默认语言
|
|
180
|
+
- `locales`: 支持的语言列表
|
|
181
|
+
- `resourceLoader`: 语言资源加载器
|
|
182
|
+
- `storageKey`: 本地存储的语言键名
|
|
183
|
+
- `loadFromStorage`: 是否在初始化时从本地存储加载语言设置
|
|
184
|
+
|
|
185
|
+
### useI18n
|
|
186
|
+
|
|
187
|
+
使用i18n的钩子。
|
|
188
|
+
|
|
189
|
+
#### 返回值
|
|
190
|
+
|
|
191
|
+
- `locale`: 当前语言
|
|
192
|
+
- `locales`: 支持的语言列表
|
|
193
|
+
- `loading`: 是否正在加载资源
|
|
194
|
+
- `error`: 错误信息
|
|
195
|
+
- `setLocale`: 设置语言的函数
|
|
196
|
+
- `t`: 翻译函数
|
|
197
|
+
|
|
198
|
+
### 工具函数
|
|
199
|
+
|
|
200
|
+
- `createLocalResourceLoader`: 创建本地资源加载器
|
|
201
|
+
- `createRemoteResourceLoader`: 创建远程资源加载器
|
|
202
|
+
- `createHybridResourceLoader`: 创建混合资源加载器
|
|
203
|
+
|
|
204
|
+
## 许可证
|
|
205
|
+
|
|
206
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import * as zustand from 'zustand';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 翻译文本的类型
|
|
6
|
+
*/
|
|
7
|
+
type TranslationValue = string | number | boolean | null | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* 翻译参数的类型
|
|
10
|
+
*/
|
|
11
|
+
type TranslationParams = Record<string, TranslationValue>;
|
|
12
|
+
/**
|
|
13
|
+
* 翻译函数的类型
|
|
14
|
+
*/
|
|
15
|
+
type TranslateFunction = (key: string, params?: TranslationParams) => string;
|
|
16
|
+
/**
|
|
17
|
+
* 语言资源加载器的类型
|
|
18
|
+
*/
|
|
19
|
+
type ResourceLoader = (locale: string) => Promise<Record<string, any>> | Record<string, any>;
|
|
20
|
+
/**
|
|
21
|
+
* i18n配置选项
|
|
22
|
+
*/
|
|
23
|
+
interface I18nOptions {
|
|
24
|
+
/**
|
|
25
|
+
* 默认语言
|
|
26
|
+
*/
|
|
27
|
+
defaultLocale: string;
|
|
28
|
+
/**
|
|
29
|
+
* 支持的语言列表
|
|
30
|
+
*/
|
|
31
|
+
locales: string[];
|
|
32
|
+
/**
|
|
33
|
+
* 语言资源加载器
|
|
34
|
+
*/
|
|
35
|
+
resourceLoader?: ResourceLoader;
|
|
36
|
+
/**
|
|
37
|
+
* 本地存储的语言键名
|
|
38
|
+
*/
|
|
39
|
+
storageKey?: string;
|
|
40
|
+
/**
|
|
41
|
+
* 是否在初始化时从本地存储加载语言设置
|
|
42
|
+
*/
|
|
43
|
+
loadFromStorage?: boolean;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* i18n状态
|
|
47
|
+
*/
|
|
48
|
+
interface I18nState {
|
|
49
|
+
/**
|
|
50
|
+
* 当前语言
|
|
51
|
+
*/
|
|
52
|
+
locale: string;
|
|
53
|
+
/**
|
|
54
|
+
* 翻译资源
|
|
55
|
+
*/
|
|
56
|
+
resources: Record<string, Record<string, any>>;
|
|
57
|
+
/**
|
|
58
|
+
* 是否正在加载资源
|
|
59
|
+
*/
|
|
60
|
+
loading: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* 错误信息
|
|
63
|
+
*/
|
|
64
|
+
error: Error | null;
|
|
65
|
+
/**
|
|
66
|
+
* 设置语言
|
|
67
|
+
*/
|
|
68
|
+
setLocale: (locale: string) => Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* 翻译函数
|
|
71
|
+
*/
|
|
72
|
+
t: TranslateFunction;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* i18n提供者属性
|
|
76
|
+
*/
|
|
77
|
+
interface I18nProviderProps {
|
|
78
|
+
/**
|
|
79
|
+
* i18n配置选项
|
|
80
|
+
*/
|
|
81
|
+
options: I18nOptions;
|
|
82
|
+
/**
|
|
83
|
+
* 子组件
|
|
84
|
+
*/
|
|
85
|
+
children: ReactNode;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 使用i18n钩子的返回值类型
|
|
89
|
+
*/
|
|
90
|
+
interface UseI18nResult {
|
|
91
|
+
/**
|
|
92
|
+
* 当前语言
|
|
93
|
+
*/
|
|
94
|
+
locale: string;
|
|
95
|
+
/**
|
|
96
|
+
* 支持的语言列表
|
|
97
|
+
*/
|
|
98
|
+
locales: string[];
|
|
99
|
+
/**
|
|
100
|
+
* 是否正在加载资源
|
|
101
|
+
*/
|
|
102
|
+
loading: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* 错误信息
|
|
105
|
+
*/
|
|
106
|
+
error: Error | null;
|
|
107
|
+
/**
|
|
108
|
+
* 设置语言
|
|
109
|
+
*/
|
|
110
|
+
setLocale: (locale: string) => Promise<void>;
|
|
111
|
+
/**
|
|
112
|
+
* 翻译函数
|
|
113
|
+
*/
|
|
114
|
+
t: TranslateFunction;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* i18n提供者组件
|
|
119
|
+
*/
|
|
120
|
+
declare const I18nProvider: React.FC<I18nProviderProps>;
|
|
121
|
+
/**
|
|
122
|
+
* 使用i18n的钩子
|
|
123
|
+
*/
|
|
124
|
+
declare const useI18n: () => UseI18nResult;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 创建本地资源加载器
|
|
128
|
+
* @param resources 本地资源对象
|
|
129
|
+
*/
|
|
130
|
+
declare const createLocalResourceLoader: (resources: Record<string, Record<string, any>>) => ResourceLoader;
|
|
131
|
+
/**
|
|
132
|
+
* 创建远程资源加载器
|
|
133
|
+
* @param baseUrl 资源基础URL
|
|
134
|
+
* @param format 资源文件格式,默认为json
|
|
135
|
+
*/
|
|
136
|
+
declare const createRemoteResourceLoader: (baseUrl: string, format?: string) => ResourceLoader;
|
|
137
|
+
/**
|
|
138
|
+
* 创建混合资源加载器(先尝试本地,再尝试远程)
|
|
139
|
+
* @param localResources 本地资源对象
|
|
140
|
+
* @param remoteBaseUrl 远程资源基础URL
|
|
141
|
+
* @param format 远程资源文件格式,默认为json
|
|
142
|
+
*/
|
|
143
|
+
declare const createHybridResourceLoader: (localResources: Record<string, Record<string, any>>, remoteBaseUrl: string, format?: string) => ResourceLoader;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 创建i18n存储
|
|
147
|
+
*/
|
|
148
|
+
declare const createI18nStore: (options: I18nOptions) => zustand.UseBoundStore<zustand.StoreApi<I18nState>>;
|
|
149
|
+
|
|
150
|
+
export { type I18nOptions, I18nProvider, type I18nProviderProps, type I18nState, type ResourceLoader, type TranslateFunction, type TranslationParams, type TranslationValue, type UseI18nResult, createHybridResourceLoader, createI18nStore, createLocalResourceLoader, createRemoteResourceLoader, useI18n };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import * as zustand from 'zustand';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 翻译文本的类型
|
|
6
|
+
*/
|
|
7
|
+
type TranslationValue = string | number | boolean | null | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* 翻译参数的类型
|
|
10
|
+
*/
|
|
11
|
+
type TranslationParams = Record<string, TranslationValue>;
|
|
12
|
+
/**
|
|
13
|
+
* 翻译函数的类型
|
|
14
|
+
*/
|
|
15
|
+
type TranslateFunction = (key: string, params?: TranslationParams) => string;
|
|
16
|
+
/**
|
|
17
|
+
* 语言资源加载器的类型
|
|
18
|
+
*/
|
|
19
|
+
type ResourceLoader = (locale: string) => Promise<Record<string, any>> | Record<string, any>;
|
|
20
|
+
/**
|
|
21
|
+
* i18n配置选项
|
|
22
|
+
*/
|
|
23
|
+
interface I18nOptions {
|
|
24
|
+
/**
|
|
25
|
+
* 默认语言
|
|
26
|
+
*/
|
|
27
|
+
defaultLocale: string;
|
|
28
|
+
/**
|
|
29
|
+
* 支持的语言列表
|
|
30
|
+
*/
|
|
31
|
+
locales: string[];
|
|
32
|
+
/**
|
|
33
|
+
* 语言资源加载器
|
|
34
|
+
*/
|
|
35
|
+
resourceLoader?: ResourceLoader;
|
|
36
|
+
/**
|
|
37
|
+
* 本地存储的语言键名
|
|
38
|
+
*/
|
|
39
|
+
storageKey?: string;
|
|
40
|
+
/**
|
|
41
|
+
* 是否在初始化时从本地存储加载语言设置
|
|
42
|
+
*/
|
|
43
|
+
loadFromStorage?: boolean;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* i18n状态
|
|
47
|
+
*/
|
|
48
|
+
interface I18nState {
|
|
49
|
+
/**
|
|
50
|
+
* 当前语言
|
|
51
|
+
*/
|
|
52
|
+
locale: string;
|
|
53
|
+
/**
|
|
54
|
+
* 翻译资源
|
|
55
|
+
*/
|
|
56
|
+
resources: Record<string, Record<string, any>>;
|
|
57
|
+
/**
|
|
58
|
+
* 是否正在加载资源
|
|
59
|
+
*/
|
|
60
|
+
loading: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* 错误信息
|
|
63
|
+
*/
|
|
64
|
+
error: Error | null;
|
|
65
|
+
/**
|
|
66
|
+
* 设置语言
|
|
67
|
+
*/
|
|
68
|
+
setLocale: (locale: string) => Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* 翻译函数
|
|
71
|
+
*/
|
|
72
|
+
t: TranslateFunction;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* i18n提供者属性
|
|
76
|
+
*/
|
|
77
|
+
interface I18nProviderProps {
|
|
78
|
+
/**
|
|
79
|
+
* i18n配置选项
|
|
80
|
+
*/
|
|
81
|
+
options: I18nOptions;
|
|
82
|
+
/**
|
|
83
|
+
* 子组件
|
|
84
|
+
*/
|
|
85
|
+
children: ReactNode;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 使用i18n钩子的返回值类型
|
|
89
|
+
*/
|
|
90
|
+
interface UseI18nResult {
|
|
91
|
+
/**
|
|
92
|
+
* 当前语言
|
|
93
|
+
*/
|
|
94
|
+
locale: string;
|
|
95
|
+
/**
|
|
96
|
+
* 支持的语言列表
|
|
97
|
+
*/
|
|
98
|
+
locales: string[];
|
|
99
|
+
/**
|
|
100
|
+
* 是否正在加载资源
|
|
101
|
+
*/
|
|
102
|
+
loading: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* 错误信息
|
|
105
|
+
*/
|
|
106
|
+
error: Error | null;
|
|
107
|
+
/**
|
|
108
|
+
* 设置语言
|
|
109
|
+
*/
|
|
110
|
+
setLocale: (locale: string) => Promise<void>;
|
|
111
|
+
/**
|
|
112
|
+
* 翻译函数
|
|
113
|
+
*/
|
|
114
|
+
t: TranslateFunction;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* i18n提供者组件
|
|
119
|
+
*/
|
|
120
|
+
declare const I18nProvider: React.FC<I18nProviderProps>;
|
|
121
|
+
/**
|
|
122
|
+
* 使用i18n的钩子
|
|
123
|
+
*/
|
|
124
|
+
declare const useI18n: () => UseI18nResult;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 创建本地资源加载器
|
|
128
|
+
* @param resources 本地资源对象
|
|
129
|
+
*/
|
|
130
|
+
declare const createLocalResourceLoader: (resources: Record<string, Record<string, any>>) => ResourceLoader;
|
|
131
|
+
/**
|
|
132
|
+
* 创建远程资源加载器
|
|
133
|
+
* @param baseUrl 资源基础URL
|
|
134
|
+
* @param format 资源文件格式,默认为json
|
|
135
|
+
*/
|
|
136
|
+
declare const createRemoteResourceLoader: (baseUrl: string, format?: string) => ResourceLoader;
|
|
137
|
+
/**
|
|
138
|
+
* 创建混合资源加载器(先尝试本地,再尝试远程)
|
|
139
|
+
* @param localResources 本地资源对象
|
|
140
|
+
* @param remoteBaseUrl 远程资源基础URL
|
|
141
|
+
* @param format 远程资源文件格式,默认为json
|
|
142
|
+
*/
|
|
143
|
+
declare const createHybridResourceLoader: (localResources: Record<string, Record<string, any>>, remoteBaseUrl: string, format?: string) => ResourceLoader;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 创建i18n存储
|
|
147
|
+
*/
|
|
148
|
+
declare const createI18nStore: (options: I18nOptions) => zustand.UseBoundStore<zustand.StoreApi<I18nState>>;
|
|
149
|
+
|
|
150
|
+
export { type I18nOptions, I18nProvider, type I18nProviderProps, type I18nState, type ResourceLoader, type TranslateFunction, type TranslationParams, type TranslationValue, type UseI18nResult, createHybridResourceLoader, createI18nStore, createLocalResourceLoader, createRemoteResourceLoader, useI18n };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
I18nProvider: () => I18nProvider,
|
|
34
|
+
createHybridResourceLoader: () => createHybridResourceLoader,
|
|
35
|
+
createI18nStore: () => createI18nStore,
|
|
36
|
+
createLocalResourceLoader: () => createLocalResourceLoader,
|
|
37
|
+
createRemoteResourceLoader: () => createRemoteResourceLoader,
|
|
38
|
+
useI18n: () => useI18n
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(index_exports);
|
|
41
|
+
|
|
42
|
+
// src/context.tsx
|
|
43
|
+
var import_react = __toESM(require("react"));
|
|
44
|
+
|
|
45
|
+
// src/store.ts
|
|
46
|
+
var import_zustand = require("zustand");
|
|
47
|
+
var defaultResourceLoader = () => ({});
|
|
48
|
+
var createI18nStore = (options) => {
|
|
49
|
+
const {
|
|
50
|
+
defaultLocale,
|
|
51
|
+
locales,
|
|
52
|
+
resourceLoader = defaultResourceLoader,
|
|
53
|
+
storageKey = "i18n_locale",
|
|
54
|
+
loadFromStorage = true
|
|
55
|
+
} = options;
|
|
56
|
+
const getInitialLocale = () => {
|
|
57
|
+
if (loadFromStorage && typeof window !== "undefined") {
|
|
58
|
+
const storedLocale = localStorage.getItem(storageKey);
|
|
59
|
+
if (storedLocale && locales.includes(storedLocale)) {
|
|
60
|
+
return storedLocale;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return defaultLocale;
|
|
64
|
+
};
|
|
65
|
+
const createTranslateFunction = (resources) => {
|
|
66
|
+
return (key, params) => {
|
|
67
|
+
const keys = key.split(".");
|
|
68
|
+
let value = resources[getInitialLocale()];
|
|
69
|
+
for (const k of keys) {
|
|
70
|
+
if (value === void 0 || value === null) {
|
|
71
|
+
return key;
|
|
72
|
+
}
|
|
73
|
+
value = value[k];
|
|
74
|
+
}
|
|
75
|
+
if (value === void 0 || value === null) {
|
|
76
|
+
return key;
|
|
77
|
+
}
|
|
78
|
+
if (params && typeof value === "string") {
|
|
79
|
+
return value.replace(/\{(\w+)\}/g, (_, key2) => {
|
|
80
|
+
return params[key2] !== void 0 ? String(params[key2]) : `{${key2}}`;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return String(value);
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
return (0, import_zustand.create)((set, get) => ({
|
|
87
|
+
locale: getInitialLocale(),
|
|
88
|
+
resources: {},
|
|
89
|
+
loading: false,
|
|
90
|
+
error: null,
|
|
91
|
+
setLocale: async (locale) => {
|
|
92
|
+
if (!locales.includes(locale)) {
|
|
93
|
+
set({ error: new Error(`Locale ${locale} is not supported`) });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (get().resources[locale]) {
|
|
97
|
+
set({ locale });
|
|
98
|
+
if (typeof window !== "undefined" && storageKey) {
|
|
99
|
+
localStorage.setItem(storageKey, locale);
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
set({ loading: true, error: null });
|
|
104
|
+
try {
|
|
105
|
+
const resources = await resourceLoader(locale);
|
|
106
|
+
set((state) => ({
|
|
107
|
+
locale,
|
|
108
|
+
resources: { ...state.resources, [locale]: resources },
|
|
109
|
+
loading: false
|
|
110
|
+
}));
|
|
111
|
+
if (typeof window !== "undefined" && storageKey) {
|
|
112
|
+
localStorage.setItem(storageKey, locale);
|
|
113
|
+
}
|
|
114
|
+
} catch (error) {
|
|
115
|
+
set({
|
|
116
|
+
error: error instanceof Error ? error : new Error("Failed to load resources"),
|
|
117
|
+
loading: false
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
t: (key, params) => {
|
|
122
|
+
const { resources } = get();
|
|
123
|
+
const translate = createTranslateFunction(resources);
|
|
124
|
+
return translate(key, params);
|
|
125
|
+
}
|
|
126
|
+
}));
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// src/context.tsx
|
|
130
|
+
var I18nContext = (0, import_react.createContext)(null);
|
|
131
|
+
var I18nProvider = ({ options, children }) => {
|
|
132
|
+
const store = (0, import_react.useState)(() => createI18nStore(options))[0];
|
|
133
|
+
const locale = store((state) => state.locale);
|
|
134
|
+
const setLocale = store((state) => state.setLocale);
|
|
135
|
+
const t = store((state) => state.t);
|
|
136
|
+
const loading = store((state) => state.loading);
|
|
137
|
+
const error = store((state) => state.error);
|
|
138
|
+
(0, import_react.useEffect)(() => {
|
|
139
|
+
setLocale(locale);
|
|
140
|
+
}, []);
|
|
141
|
+
const value = {
|
|
142
|
+
locale,
|
|
143
|
+
locales: options.locales,
|
|
144
|
+
setLocale,
|
|
145
|
+
t,
|
|
146
|
+
loading,
|
|
147
|
+
error
|
|
148
|
+
};
|
|
149
|
+
return /* @__PURE__ */ import_react.default.createElement(I18nContext.Provider, { value }, children);
|
|
150
|
+
};
|
|
151
|
+
var useI18n = () => {
|
|
152
|
+
const context = (0, import_react.useContext)(I18nContext);
|
|
153
|
+
if (!context) {
|
|
154
|
+
throw new Error("useI18n must be used within an I18nProvider");
|
|
155
|
+
}
|
|
156
|
+
return context;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// src/utils.ts
|
|
160
|
+
var createLocalResourceLoader = (resources) => {
|
|
161
|
+
return (locale) => {
|
|
162
|
+
if (!resources[locale]) {
|
|
163
|
+
throw new Error(`Locale ${locale} not found in local resources`);
|
|
164
|
+
}
|
|
165
|
+
return resources[locale];
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
var createRemoteResourceLoader = (baseUrl, format = "json") => {
|
|
169
|
+
return async (locale) => {
|
|
170
|
+
try {
|
|
171
|
+
const response = await fetch(`${baseUrl}/${locale}.${format}`);
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
throw new Error(`Failed to load resources for locale ${locale}`);
|
|
174
|
+
}
|
|
175
|
+
return await response.json();
|
|
176
|
+
} catch (error) {
|
|
177
|
+
throw new Error(`Failed to load resources for locale ${locale}: ${error}`);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
var createHybridResourceLoader = (localResources, remoteBaseUrl, format = "json") => {
|
|
182
|
+
const localLoader = createLocalResourceLoader(localResources);
|
|
183
|
+
const remoteLoader = createRemoteResourceLoader(remoteBaseUrl, format);
|
|
184
|
+
return async (locale) => {
|
|
185
|
+
try {
|
|
186
|
+
return localLoader(locale);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
return remoteLoader(locale);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
193
|
+
0 && (module.exports = {
|
|
194
|
+
I18nProvider,
|
|
195
|
+
createHybridResourceLoader,
|
|
196
|
+
createI18nStore,
|
|
197
|
+
createLocalResourceLoader,
|
|
198
|
+
createRemoteResourceLoader,
|
|
199
|
+
useI18n
|
|
200
|
+
});
|
|
201
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/context.tsx","../src/store.ts","../src/utils.ts"],"sourcesContent":["export * from './types';\nexport * from './context';\nexport * from './utils';\nexport { createI18nStore } from './store'; ","import React, { createContext, useContext, useEffect, useState } from 'react';\nimport { I18nProviderProps, UseI18nResult } from './types';\nimport { createI18nStore } from './store';\n\n// 创建上下文\nconst I18nContext = createContext<UseI18nResult | null>(null);\n\n/**\n * i18n提供者组件\n */\nexport const I18nProvider: React.FC<I18nProviderProps> = ({ options, children }) => {\n const store = useState(() => createI18nStore(options))[0];\n \n // 使用 store 函数获取状态\n const locale = store((state) => state.locale);\n const setLocale = store((state) => state.setLocale);\n const t = store((state) => state.t);\n const loading = store((state) => state.loading);\n const error = store((state) => state.error);\n \n // 初始化时加载默认语言资源\n useEffect(() => {\n setLocale(locale);\n }, []);\n \n const value: UseI18nResult = {\n locale,\n locales: options.locales,\n setLocale,\n t,\n loading,\n error,\n };\n \n return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;\n};\n\n/**\n * 使用i18n的钩子\n */\nexport const useI18n = (): UseI18nResult => {\n const context = useContext(I18nContext);\n \n if (!context) {\n throw new Error('useI18n must be used within an I18nProvider');\n }\n \n return context;\n}; ","import { create } from 'zustand';\nimport { I18nOptions, I18nState, ResourceLoader, TranslateFunction } from './types';\n\n/**\n * 默认资源加载器\n */\nconst defaultResourceLoader: ResourceLoader = () => ({});\n\n/**\n * 创建i18n存储\n */\nexport const createI18nStore = (options: I18nOptions) => {\n const {\n defaultLocale,\n locales,\n resourceLoader = defaultResourceLoader,\n storageKey = 'i18n_locale',\n loadFromStorage = true,\n } = options;\n\n // 从本地存储获取初始语言\n const getInitialLocale = (): string => {\n if (loadFromStorage && typeof window !== 'undefined') {\n const storedLocale = localStorage.getItem(storageKey);\n if (storedLocale && locales.includes(storedLocale)) {\n return storedLocale;\n }\n }\n return defaultLocale;\n };\n\n // 创建翻译函数\n const createTranslateFunction = (resources: Record<string, Record<string, any>>): TranslateFunction => {\n return (key: string, params?: Record<string, any>) => {\n const keys = key.split('.');\n let value: any = resources[getInitialLocale()];\n \n // 遍历键路径获取翻译值\n for (const k of keys) {\n if (value === undefined || value === null) {\n return key;\n }\n value = value[k];\n }\n \n // 如果找不到翻译,返回键名\n if (value === undefined || value === null) {\n return key;\n }\n \n // 替换参数\n if (params && typeof value === 'string') {\n return value.replace(/\\{(\\w+)\\}/g, (_, key) => {\n return params[key] !== undefined ? String(params[key]) : `{${key}}`;\n });\n }\n \n return String(value);\n };\n };\n\n return create<I18nState>((set, get) => ({\n locale: getInitialLocale(),\n resources: {},\n loading: false,\n error: null,\n \n setLocale: async (locale: string) => {\n // 检查语言是否支持\n if (!locales.includes(locale)) {\n set({ error: new Error(`Locale ${locale} is not supported`) });\n return;\n }\n \n // 如果语言已经加载,直接切换\n if (get().resources[locale]) {\n set({ locale });\n if (typeof window !== 'undefined' && storageKey) {\n localStorage.setItem(storageKey, locale);\n }\n return;\n }\n \n // 加载语言资源\n set({ loading: true, error: null });\n try {\n const resources = await resourceLoader(locale);\n set((state) => ({\n locale,\n resources: { ...state.resources, [locale]: resources },\n loading: false,\n }));\n \n if (typeof window !== 'undefined' && storageKey) {\n localStorage.setItem(storageKey, locale);\n }\n } catch (error) {\n set({ \n error: error instanceof Error ? error : new Error('Failed to load resources'),\n loading: false \n });\n }\n },\n \n t: (key: string, params?: Record<string, any>) => {\n const { resources } = get();\n const translate = createTranslateFunction(resources);\n return translate(key, params);\n },\n }));\n}; ","import { ResourceLoader } from './types';\n\n/**\n * 创建本地资源加载器\n * @param resources 本地资源对象\n */\nexport const createLocalResourceLoader = (resources: Record<string, Record<string, any>>): ResourceLoader => {\n return (locale: string) => {\n if (!resources[locale]) {\n throw new Error(`Locale ${locale} not found in local resources`);\n }\n return resources[locale];\n };\n};\n\n/**\n * 创建远程资源加载器\n * @param baseUrl 资源基础URL\n * @param format 资源文件格式,默认为json\n */\nexport const createRemoteResourceLoader = (\n baseUrl: string,\n format: string = 'json'\n): ResourceLoader => {\n return async (locale: string) => {\n try {\n const response = await fetch(`${baseUrl}/${locale}.${format}`);\n \n if (!response.ok) {\n throw new Error(`Failed to load resources for locale ${locale}`);\n }\n \n return await response.json();\n } catch (error) {\n throw new Error(`Failed to load resources for locale ${locale}: ${error}`);\n }\n };\n};\n\n/**\n * 创建混合资源加载器(先尝试本地,再尝试远程)\n * @param localResources 本地资源对象\n * @param remoteBaseUrl 远程资源基础URL\n * @param format 远程资源文件格式,默认为json\n */\nexport const createHybridResourceLoader = (\n localResources: Record<string, Record<string, any>>,\n remoteBaseUrl: string,\n format: string = 'json'\n): ResourceLoader => {\n const localLoader = createLocalResourceLoader(localResources);\n const remoteLoader = createRemoteResourceLoader(remoteBaseUrl, format);\n \n return async (locale: string) => {\n try {\n // 先尝试本地资源\n return localLoader(locale);\n } catch (error) {\n // 本地资源不存在,尝试远程加载\n return remoteLoader(locale);\n }\n };\n}; "],"mappings":"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","names":["key","React"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// src/context.tsx
|
|
2
|
+
import React, { createContext, useContext, useEffect, useState } from "react";
|
|
3
|
+
|
|
4
|
+
// src/store.ts
|
|
5
|
+
import { create } from "zustand";
|
|
6
|
+
var defaultResourceLoader = () => ({});
|
|
7
|
+
var createI18nStore = (options) => {
|
|
8
|
+
const {
|
|
9
|
+
defaultLocale,
|
|
10
|
+
locales,
|
|
11
|
+
resourceLoader = defaultResourceLoader,
|
|
12
|
+
storageKey = "i18n_locale",
|
|
13
|
+
loadFromStorage = true
|
|
14
|
+
} = options;
|
|
15
|
+
const getInitialLocale = () => {
|
|
16
|
+
if (loadFromStorage && typeof window !== "undefined") {
|
|
17
|
+
const storedLocale = localStorage.getItem(storageKey);
|
|
18
|
+
if (storedLocale && locales.includes(storedLocale)) {
|
|
19
|
+
return storedLocale;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return defaultLocale;
|
|
23
|
+
};
|
|
24
|
+
const createTranslateFunction = (resources) => {
|
|
25
|
+
return (key, params) => {
|
|
26
|
+
const keys = key.split(".");
|
|
27
|
+
let value = resources[getInitialLocale()];
|
|
28
|
+
for (const k of keys) {
|
|
29
|
+
if (value === void 0 || value === null) {
|
|
30
|
+
return key;
|
|
31
|
+
}
|
|
32
|
+
value = value[k];
|
|
33
|
+
}
|
|
34
|
+
if (value === void 0 || value === null) {
|
|
35
|
+
return key;
|
|
36
|
+
}
|
|
37
|
+
if (params && typeof value === "string") {
|
|
38
|
+
return value.replace(/\{(\w+)\}/g, (_, key2) => {
|
|
39
|
+
return params[key2] !== void 0 ? String(params[key2]) : `{${key2}}`;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return String(value);
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
return create((set, get) => ({
|
|
46
|
+
locale: getInitialLocale(),
|
|
47
|
+
resources: {},
|
|
48
|
+
loading: false,
|
|
49
|
+
error: null,
|
|
50
|
+
setLocale: async (locale) => {
|
|
51
|
+
if (!locales.includes(locale)) {
|
|
52
|
+
set({ error: new Error(`Locale ${locale} is not supported`) });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (get().resources[locale]) {
|
|
56
|
+
set({ locale });
|
|
57
|
+
if (typeof window !== "undefined" && storageKey) {
|
|
58
|
+
localStorage.setItem(storageKey, locale);
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
set({ loading: true, error: null });
|
|
63
|
+
try {
|
|
64
|
+
const resources = await resourceLoader(locale);
|
|
65
|
+
set((state) => ({
|
|
66
|
+
locale,
|
|
67
|
+
resources: { ...state.resources, [locale]: resources },
|
|
68
|
+
loading: false
|
|
69
|
+
}));
|
|
70
|
+
if (typeof window !== "undefined" && storageKey) {
|
|
71
|
+
localStorage.setItem(storageKey, locale);
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
set({
|
|
75
|
+
error: error instanceof Error ? error : new Error("Failed to load resources"),
|
|
76
|
+
loading: false
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
t: (key, params) => {
|
|
81
|
+
const { resources } = get();
|
|
82
|
+
const translate = createTranslateFunction(resources);
|
|
83
|
+
return translate(key, params);
|
|
84
|
+
}
|
|
85
|
+
}));
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// src/context.tsx
|
|
89
|
+
var I18nContext = createContext(null);
|
|
90
|
+
var I18nProvider = ({ options, children }) => {
|
|
91
|
+
const store = useState(() => createI18nStore(options))[0];
|
|
92
|
+
const locale = store((state) => state.locale);
|
|
93
|
+
const setLocale = store((state) => state.setLocale);
|
|
94
|
+
const t = store((state) => state.t);
|
|
95
|
+
const loading = store((state) => state.loading);
|
|
96
|
+
const error = store((state) => state.error);
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
setLocale(locale);
|
|
99
|
+
}, []);
|
|
100
|
+
const value = {
|
|
101
|
+
locale,
|
|
102
|
+
locales: options.locales,
|
|
103
|
+
setLocale,
|
|
104
|
+
t,
|
|
105
|
+
loading,
|
|
106
|
+
error
|
|
107
|
+
};
|
|
108
|
+
return /* @__PURE__ */ React.createElement(I18nContext.Provider, { value }, children);
|
|
109
|
+
};
|
|
110
|
+
var useI18n = () => {
|
|
111
|
+
const context = useContext(I18nContext);
|
|
112
|
+
if (!context) {
|
|
113
|
+
throw new Error("useI18n must be used within an I18nProvider");
|
|
114
|
+
}
|
|
115
|
+
return context;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// src/utils.ts
|
|
119
|
+
var createLocalResourceLoader = (resources) => {
|
|
120
|
+
return (locale) => {
|
|
121
|
+
if (!resources[locale]) {
|
|
122
|
+
throw new Error(`Locale ${locale} not found in local resources`);
|
|
123
|
+
}
|
|
124
|
+
return resources[locale];
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
var createRemoteResourceLoader = (baseUrl, format = "json") => {
|
|
128
|
+
return async (locale) => {
|
|
129
|
+
try {
|
|
130
|
+
const response = await fetch(`${baseUrl}/${locale}.${format}`);
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
throw new Error(`Failed to load resources for locale ${locale}`);
|
|
133
|
+
}
|
|
134
|
+
return await response.json();
|
|
135
|
+
} catch (error) {
|
|
136
|
+
throw new Error(`Failed to load resources for locale ${locale}: ${error}`);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
var createHybridResourceLoader = (localResources, remoteBaseUrl, format = "json") => {
|
|
141
|
+
const localLoader = createLocalResourceLoader(localResources);
|
|
142
|
+
const remoteLoader = createRemoteResourceLoader(remoteBaseUrl, format);
|
|
143
|
+
return async (locale) => {
|
|
144
|
+
try {
|
|
145
|
+
return localLoader(locale);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return remoteLoader(locale);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
export {
|
|
152
|
+
I18nProvider,
|
|
153
|
+
createHybridResourceLoader,
|
|
154
|
+
createI18nStore,
|
|
155
|
+
createLocalResourceLoader,
|
|
156
|
+
createRemoteResourceLoader,
|
|
157
|
+
useI18n
|
|
158
|
+
};
|
|
159
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/context.tsx","../src/store.ts","../src/utils.ts"],"sourcesContent":["import React, { createContext, useContext, useEffect, useState } from 'react';\nimport { I18nProviderProps, UseI18nResult } from './types';\nimport { createI18nStore } from './store';\n\n// 创建上下文\nconst I18nContext = createContext<UseI18nResult | null>(null);\n\n/**\n * i18n提供者组件\n */\nexport const I18nProvider: React.FC<I18nProviderProps> = ({ options, children }) => {\n const store = useState(() => createI18nStore(options))[0];\n \n // 使用 store 函数获取状态\n const locale = store((state) => state.locale);\n const setLocale = store((state) => state.setLocale);\n const t = store((state) => state.t);\n const loading = store((state) => state.loading);\n const error = store((state) => state.error);\n \n // 初始化时加载默认语言资源\n useEffect(() => {\n setLocale(locale);\n }, []);\n \n const value: UseI18nResult = {\n locale,\n locales: options.locales,\n setLocale,\n t,\n loading,\n error,\n };\n \n return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;\n};\n\n/**\n * 使用i18n的钩子\n */\nexport const useI18n = (): UseI18nResult => {\n const context = useContext(I18nContext);\n \n if (!context) {\n throw new Error('useI18n must be used within an I18nProvider');\n }\n \n return context;\n}; ","import { create } from 'zustand';\nimport { I18nOptions, I18nState, ResourceLoader, TranslateFunction } from './types';\n\n/**\n * 默认资源加载器\n */\nconst defaultResourceLoader: ResourceLoader = () => ({});\n\n/**\n * 创建i18n存储\n */\nexport const createI18nStore = (options: I18nOptions) => {\n const {\n defaultLocale,\n locales,\n resourceLoader = defaultResourceLoader,\n storageKey = 'i18n_locale',\n loadFromStorage = true,\n } = options;\n\n // 从本地存储获取初始语言\n const getInitialLocale = (): string => {\n if (loadFromStorage && typeof window !== 'undefined') {\n const storedLocale = localStorage.getItem(storageKey);\n if (storedLocale && locales.includes(storedLocale)) {\n return storedLocale;\n }\n }\n return defaultLocale;\n };\n\n // 创建翻译函数\n const createTranslateFunction = (resources: Record<string, Record<string, any>>): TranslateFunction => {\n return (key: string, params?: Record<string, any>) => {\n const keys = key.split('.');\n let value: any = resources[getInitialLocale()];\n \n // 遍历键路径获取翻译值\n for (const k of keys) {\n if (value === undefined || value === null) {\n return key;\n }\n value = value[k];\n }\n \n // 如果找不到翻译,返回键名\n if (value === undefined || value === null) {\n return key;\n }\n \n // 替换参数\n if (params && typeof value === 'string') {\n return value.replace(/\\{(\\w+)\\}/g, (_, key) => {\n return params[key] !== undefined ? String(params[key]) : `{${key}}`;\n });\n }\n \n return String(value);\n };\n };\n\n return create<I18nState>((set, get) => ({\n locale: getInitialLocale(),\n resources: {},\n loading: false,\n error: null,\n \n setLocale: async (locale: string) => {\n // 检查语言是否支持\n if (!locales.includes(locale)) {\n set({ error: new Error(`Locale ${locale} is not supported`) });\n return;\n }\n \n // 如果语言已经加载,直接切换\n if (get().resources[locale]) {\n set({ locale });\n if (typeof window !== 'undefined' && storageKey) {\n localStorage.setItem(storageKey, locale);\n }\n return;\n }\n \n // 加载语言资源\n set({ loading: true, error: null });\n try {\n const resources = await resourceLoader(locale);\n set((state) => ({\n locale,\n resources: { ...state.resources, [locale]: resources },\n loading: false,\n }));\n \n if (typeof window !== 'undefined' && storageKey) {\n localStorage.setItem(storageKey, locale);\n }\n } catch (error) {\n set({ \n error: error instanceof Error ? error : new Error('Failed to load resources'),\n loading: false \n });\n }\n },\n \n t: (key: string, params?: Record<string, any>) => {\n const { resources } = get();\n const translate = createTranslateFunction(resources);\n return translate(key, params);\n },\n }));\n}; ","import { ResourceLoader } from './types';\n\n/**\n * 创建本地资源加载器\n * @param resources 本地资源对象\n */\nexport const createLocalResourceLoader = (resources: Record<string, Record<string, any>>): ResourceLoader => {\n return (locale: string) => {\n if (!resources[locale]) {\n throw new Error(`Locale ${locale} not found in local resources`);\n }\n return resources[locale];\n };\n};\n\n/**\n * 创建远程资源加载器\n * @param baseUrl 资源基础URL\n * @param format 资源文件格式,默认为json\n */\nexport const createRemoteResourceLoader = (\n baseUrl: string,\n format: string = 'json'\n): ResourceLoader => {\n return async (locale: string) => {\n try {\n const response = await fetch(`${baseUrl}/${locale}.${format}`);\n \n if (!response.ok) {\n throw new Error(`Failed to load resources for locale ${locale}`);\n }\n \n return await response.json();\n } catch (error) {\n throw new Error(`Failed to load resources for locale ${locale}: ${error}`);\n }\n };\n};\n\n/**\n * 创建混合资源加载器(先尝试本地,再尝试远程)\n * @param localResources 本地资源对象\n * @param remoteBaseUrl 远程资源基础URL\n * @param format 远程资源文件格式,默认为json\n */\nexport const createHybridResourceLoader = (\n localResources: Record<string, Record<string, any>>,\n remoteBaseUrl: string,\n format: string = 'json'\n): ResourceLoader => {\n const localLoader = createLocalResourceLoader(localResources);\n const remoteLoader = createRemoteResourceLoader(remoteBaseUrl, format);\n \n return async (locale: string) => {\n try {\n // 先尝试本地资源\n return localLoader(locale);\n } catch (error) {\n // 本地资源不存在,尝试远程加载\n return remoteLoader(locale);\n }\n };\n}; "],"mappings":"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","names":["key"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xiping/react-i18n",
|
|
3
|
+
"version": "1.0.7",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "A React i18n library with Zustand integration",
|
|
6
|
+
"author": "The-End-Hero <527409987@qq.com>",
|
|
7
|
+
"homepage": "https://github.com/The-End-Hero/xiping#readme",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"module": "dist/index.mjs",
|
|
11
|
+
"types": "dist/index.d.ts",
|
|
12
|
+
"directories": {
|
|
13
|
+
"lib": "lib",
|
|
14
|
+
"test": "__tests__"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/The-End-Hero/wang-ping.git"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "echo \"Error: run tests from root\" && exit 1",
|
|
25
|
+
"build": "tsup",
|
|
26
|
+
"dev": "tsup --watch",
|
|
27
|
+
"clean": "rimraf dist",
|
|
28
|
+
"typecheck": "tsc --noEmit"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/The-End-Hero/wang-ping/issues"
|
|
32
|
+
},
|
|
33
|
+
"gitHead": "a48894117df00b61c933f72e6e352928e67b7578",
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public",
|
|
36
|
+
"registry": "https://registry.npmjs.org/"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/react": "^18.3.20",
|
|
40
|
+
"react": "^18.3.1",
|
|
41
|
+
"react-dom": "^18.3.1",
|
|
42
|
+
"rimraf": "^5.0.10",
|
|
43
|
+
"tsup": "^8.4.0",
|
|
44
|
+
"typescript": "^5.7.3"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"react": ">=16.8.0",
|
|
48
|
+
"zustand": ">=4.0.0"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"zustand": "^5.0.3"
|
|
52
|
+
},
|
|
53
|
+
"keywords": [
|
|
54
|
+
"react",
|
|
55
|
+
"i18n",
|
|
56
|
+
"internationalization",
|
|
57
|
+
"localization",
|
|
58
|
+
"translation",
|
|
59
|
+
"zustand"
|
|
60
|
+
]
|
|
61
|
+
}
|