@zfqh/uview-x 0.0.6 → 0.0.8
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/CHANGELOG.md +16 -0
- package/README.md +52 -9
- package/components/uv-badge/uv-badge.vue +4 -4
- package/components/uv-button/uv-button.vue +11 -11
- package/components/uv-checkbox/uv-checkbox.vue +11 -9
- package/components/uv-checkbox-group/uv-checkbox-group.vue +2 -2
- package/components/uv-code/uv-code.vue +2 -2
- package/components/uv-dialog/uv-dialog.vue +10 -11
- package/components/uv-form/uv-form.vue +244 -246
- package/components/uv-form-item/uv-form-item.vue +10 -9
- package/components/uv-icon/uv-icon.vue +13 -13
- package/components/uv-input/uv-input.vue +6 -5
- package/components/uv-line/uv-line.vue +7 -6
- package/components/uv-line-progress/uv-line-progress.vue +5 -5
- package/components/uv-loading-icon/uv-loading-icon.vue +9 -8
- package/components/uv-overlay/uv-overlay.vue +3 -2
- package/components/uv-popup/uv-popup.vue +6 -5
- package/components/uv-safe-bottom/uv-safe-bottom.vue +4 -3
- package/components/uv-status-bar/uv-status-bar.vue +4 -3
- package/components/uv-tabs/uv-tabs.vue +15 -12
- package/components/uv-transition/uv-transition.vue +2 -2
- package/hooks/index.ts +5 -5
- package/hooks/useDebounce.ts +33 -35
- package/hooks/useI18n.ts +4 -3
- package/hooks/useThrottle.ts +36 -38
- package/index.ts +50 -29
- package/locales/en.ts +56 -0
- package/locales/es.ts +57 -0
- package/locales/fr.ts +57 -0
- package/locales/index.ts +15 -159
- package/locales/ru.ts +57 -0
- package/locales/zh-Hans.ts +57 -0
- package/locales/zh-Hant.ts +57 -0
- package/package.json +2 -2
- package/types/index.d.ts +68 -59
- package/utils/color.ts +139 -0
- package/utils/compress.ts +28 -28
- package/utils/config.ts +46 -11
- package/utils/debounce.ts +4 -4
- package/utils/function.ts +11 -89
- package/utils/locale.ts +444 -270
- package/utils/merge.ts +74 -0
- package/utils/platform.ts +28 -28
- package/utils/route.ts +123 -129
- package/utils/scrollbar.ts +19 -19
- package/utils/throttle.ts +4 -4
- package/locales/en-US.ts +0 -79
- package/locales/es-ES.ts +0 -79
- package/locales/fr-FR.ts +0 -79
- package/locales/ru-RU.ts +0 -79
- package/locales/zh-CN.ts +0 -79
- package/locales/zh-TW.ts +0 -79
- package/utils/colorGradient.ts +0 -139
- package/utils/index.ts +0 -58
- /package/components/uv-form/{valid.ts → validator.ts} +0 -0
package/utils/locale.ts
CHANGED
|
@@ -1,270 +1,444 @@
|
|
|
1
|
-
import { ref } from 'vue';
|
|
2
|
-
import type { Ref } from 'vue';
|
|
3
|
-
import { createI18n as _createI18n } from 'vue-i18n';
|
|
4
|
-
import type { I18n, I18nOptions } from 'vue-i18n';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
*
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
1
|
+
import { ref } from 'vue';
|
|
2
|
+
import type { Ref } from 'vue';
|
|
3
|
+
import { createI18n as _createI18n } from 'vue-i18n';
|
|
4
|
+
import type { I18n, I18nOptions as VueI18nOptions } from 'vue-i18n';
|
|
5
|
+
import { registerI18nConfigHandler } from './config';
|
|
6
|
+
import en from '../locales/en';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 国际化能力(默认英文 + 可选语言包 + 4 个公共 API)。
|
|
10
|
+
*
|
|
11
|
+
* 默认组件语言为 `en`,内置 `en` 语言包;其他语言包由使用方显式引入后通过
|
|
12
|
+
* `setConfig({ i18n })` / `app.use(UviewX, { i18n })` 合并。
|
|
13
|
+
*
|
|
14
|
+
* 公共 API(由根入口 index.ts 导出):
|
|
15
|
+
* `createI18n`、`getLocale`、`getLocaleMessage`、`setLocale`。
|
|
16
|
+
*
|
|
17
|
+
* 其余导出(`configureI18n`、`localeState`、`getI18nGlobal`、`getI18nMessages`、`appTranslate`、
|
|
18
|
+
* `unwrapI18nValue` 与相关类型)是包内实现的 internal helper,供 components / hooks / locales 深引用,
|
|
19
|
+
* 不进入根入口与 `types/index.d.ts`。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** 单语言扁平语言包 */
|
|
23
|
+
export type LocaleMessages = Record<string, string>;
|
|
24
|
+
|
|
25
|
+
/** 语言包:locale -> 扁平 key 映射 */
|
|
26
|
+
export type UViewI18nMessages = Record<string, LocaleMessages>;
|
|
27
|
+
|
|
28
|
+
/** 内部组件语言包:默认只包含 en,可由 setConfig / 插件 options 合并 */
|
|
29
|
+
export const componentMessages: Record<string, LocaleMessages> = {
|
|
30
|
+
en: { ...en },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function mergeLocaleMessages(base: LocaleMessages, patch: LocaleMessages): LocaleMessages {
|
|
34
|
+
return { ...base, ...patch };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 把 locale -> 扁平 messages 合并进内部组件语言包 */
|
|
38
|
+
export function mergeComponentMessages(messages?: UViewI18nMessages): void {
|
|
39
|
+
if (!messages) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
for (const locale of Object.keys(messages)) {
|
|
43
|
+
const incoming = messages[locale];
|
|
44
|
+
if (!incoming || typeof incoming !== 'object') {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
componentMessages[locale] = mergeLocaleMessages(componentMessages[locale] || {}, incoming);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 生成 createI18n 使用的合并 messages(返回新对象,不修改入参)。
|
|
53
|
+
*
|
|
54
|
+
* - 以内置 `en` + 已配置的组件语言包为基础;
|
|
55
|
+
* - 用户传入的同 locale 扁平 key 覆盖已有文案。
|
|
56
|
+
*/
|
|
57
|
+
export function mergeDefaultMessages(userMessages?: UViewI18nMessages): UViewI18nMessages {
|
|
58
|
+
const merged: UViewI18nMessages = {};
|
|
59
|
+
for (const locale of Object.keys(componentMessages)) {
|
|
60
|
+
merged[locale] = { ...componentMessages[locale] };
|
|
61
|
+
}
|
|
62
|
+
if (userMessages) {
|
|
63
|
+
for (const locale of Object.keys(userMessages)) {
|
|
64
|
+
const incoming = userMessages[locale];
|
|
65
|
+
if (!incoming || typeof incoming !== 'object') {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
merged[locale] = mergeLocaleMessages(merged[locale] || {}, incoming);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return merged;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 组件初始化 i18n 配置(字段与 createI18n 对齐) */
|
|
75
|
+
export interface UViewI18nOptions {
|
|
76
|
+
/** 组件语言,默认 'en' */
|
|
77
|
+
locale?: string;
|
|
78
|
+
/** 组件回退语言,默认 'en' */
|
|
79
|
+
fallbackLocale?: string;
|
|
80
|
+
/** locale -> 扁平组件文案 */
|
|
81
|
+
messages?: UViewI18nMessages;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** createI18n 选项:messages 使用扁平 locale 包,其余与 vue-i18n 一致 */
|
|
85
|
+
export interface UViewCreateI18nOptions extends Omit<VueI18nOptions, 'messages'> {
|
|
86
|
+
messages?: UViewI18nMessages;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** vue-i18n 实例内的 messages(内部已转换为嵌套结构) */
|
|
90
|
+
export type I18nGlobalMessages = Record<string, Record<string, unknown>>;
|
|
91
|
+
|
|
92
|
+
/** 兼容 vue-i18n legacy / composition 两种 global 形态的运行时视图 */
|
|
93
|
+
export interface I18nGlobalLike {
|
|
94
|
+
t: (key: string, values?: Record<string, string | number>) => string;
|
|
95
|
+
locale: string | { value: string };
|
|
96
|
+
messages: I18nGlobalMessages | { value: I18nGlobalMessages };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface RefLike<T> {
|
|
100
|
+
value: T;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 语言持久化 key:App 端切换语言可能触发重启,必须先落盘 */
|
|
104
|
+
const STORAGE_KEY = 'UNI_LOCALE';
|
|
105
|
+
|
|
106
|
+
/** 默认/回退语言(内部使用) */
|
|
107
|
+
const fallbackLocale = 'en';
|
|
108
|
+
|
|
109
|
+
/** 当前组件默认语言与回退语言(由 setConfig / 插件 options 更新) */
|
|
110
|
+
let componentLocale = 'en';
|
|
111
|
+
let componentFallbackLocale = 'en';
|
|
112
|
+
|
|
113
|
+
/** 读取上次选择的语言;storage 不可用或无有效值时返回空字符串 */
|
|
114
|
+
function readStorageLocale(): string {
|
|
115
|
+
if (typeof uni !== 'undefined' && typeof uni.getStorageSync === 'function') {
|
|
116
|
+
try {
|
|
117
|
+
const stored: unknown = uni.getStorageSync(STORAGE_KEY);
|
|
118
|
+
if (typeof stored === 'string' && stored) {
|
|
119
|
+
return stored;
|
|
120
|
+
}
|
|
121
|
+
} catch (e) {
|
|
122
|
+
// storage 不可用时忽略,继续走系统语言
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// #ifdef WEB || H5
|
|
126
|
+
// H5 nvue 页面上下文可能拿到非共享的 uni 存储视图,浏览器 localStorage 仍为同一来源
|
|
127
|
+
if (typeof localStorage !== 'undefined') {
|
|
128
|
+
try {
|
|
129
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
130
|
+
if (stored) {
|
|
131
|
+
return stored;
|
|
132
|
+
}
|
|
133
|
+
} catch (e) {
|
|
134
|
+
// localStorage 不可用时忽略
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// #endif
|
|
138
|
+
return '';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 获取当前系统语言。
|
|
143
|
+
*
|
|
144
|
+
* 顺序:
|
|
145
|
+
* 1. 本地存储 `UNI_LOCALE`(用户上次通过 `setLocale` 选择的语言);
|
|
146
|
+
* 2. `uni.getLocale()`;
|
|
147
|
+
* 3. `uni.getAppBaseInfo().appLanguage`;
|
|
148
|
+
* 4. `en`。
|
|
149
|
+
*/
|
|
150
|
+
export function getLocale(): string {
|
|
151
|
+
const stored = readStorageLocale();
|
|
152
|
+
if (stored) {
|
|
153
|
+
return stored;
|
|
154
|
+
}
|
|
155
|
+
if (typeof uni !== 'undefined' && typeof uni.getLocale === 'function') {
|
|
156
|
+
const systemLocale = uni.getLocale();
|
|
157
|
+
if (systemLocale) {
|
|
158
|
+
return systemLocale;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (typeof uni !== 'undefined' && typeof uni.getAppBaseInfo === 'function') {
|
|
162
|
+
const info = uni.getAppBaseInfo() as { appLanguage?: string };
|
|
163
|
+
const systemLocale = info.appLanguage;
|
|
164
|
+
if (systemLocale) {
|
|
165
|
+
return systemLocale;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return fallbackLocale;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** 当前组件语言状态(internal,不进 barrel) */
|
|
172
|
+
export const localeState: Ref<string> = ref<string>(readStorageLocale() || componentLocale);
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 应用组件 i18n 初始化配置(internal)。
|
|
176
|
+
*
|
|
177
|
+
* - 更新组件默认/回退语言;
|
|
178
|
+
* - 合并扁平 locale 包;
|
|
179
|
+
* - storage 中已有用户选择时优先于默认 locale。
|
|
180
|
+
*/
|
|
181
|
+
export function configureI18n(options: UViewI18nOptions = {}): void {
|
|
182
|
+
if (options.locale) {
|
|
183
|
+
componentLocale = options.locale;
|
|
184
|
+
}
|
|
185
|
+
if (options.fallbackLocale) {
|
|
186
|
+
componentFallbackLocale = options.fallbackLocale;
|
|
187
|
+
}
|
|
188
|
+
if (options.messages) {
|
|
189
|
+
mergeComponentMessages(options.messages);
|
|
190
|
+
}
|
|
191
|
+
localeState.value = readStorageLocale() || componentLocale;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// setConfig 位于 utils/config.ts,通过注册处理器避免 config ↔ locale 值循环
|
|
195
|
+
registerI18nConfigHandler(configureI18n);
|
|
196
|
+
|
|
197
|
+
/** 兼容 vue-i18n legacy(字符串)与 composition(Ref)两种模式,取出实际值 */
|
|
198
|
+
export function unwrapI18nValue<T>(value: T | { value: T }): T {
|
|
199
|
+
if (value !== null && typeof value === 'object' && 'value' in value) {
|
|
200
|
+
return (value as RefLike<T>).value;
|
|
201
|
+
}
|
|
202
|
+
return value as T;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function isI18nGlobalLike(value: unknown): value is I18nGlobalLike {
|
|
206
|
+
if (!value || typeof value !== 'object') {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
return typeof (value as I18nGlobalLike).t === 'function';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** 当前 `uni.$i18n.global` 的运行时视图;实例不存在时返回 undefined */
|
|
213
|
+
export function getI18nGlobal(): I18nGlobalLike | undefined {
|
|
214
|
+
if (typeof uni === 'undefined') {
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
const global = uni.$i18n?.global;
|
|
218
|
+
return isI18nGlobalLike(global) ? global : undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** APP 下不支持动态变量,需要重写占位符替换 */
|
|
222
|
+
function replacePlaceholders(str: string, values?: Record<string, string | number>): string {
|
|
223
|
+
if (!values || typeof str !== 'string') {
|
|
224
|
+
return str;
|
|
225
|
+
}
|
|
226
|
+
let out = str;
|
|
227
|
+
for (const k of Object.keys(values)) {
|
|
228
|
+
out = out.split(`{${k}}`).join(String(values[k]));
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 读取组件内置文案(含已合并的初始化 messages);按扁平 key 查找并替换 `{name}` 占位符。
|
|
235
|
+
*
|
|
236
|
+
* 找不到时返回空字符串,由调用方继续回退到 key。
|
|
237
|
+
*/
|
|
238
|
+
export function getBuiltinMessage(locale: string, key: string, values?: Record<string, string | number>): string {
|
|
239
|
+
const messages = componentMessages[locale];
|
|
240
|
+
if (!messages) {
|
|
241
|
+
return '';
|
|
242
|
+
}
|
|
243
|
+
return replacePlaceholders(messages[key] || '', values);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 用 `t` 构建 async-validator 的校验消息对象(结构与 `components/uv-form/valid.ts` 的 `newMessages()` 对齐)。
|
|
248
|
+
*
|
|
249
|
+
* 显式 `rules[].message` 仍由校验器优先使用;这里只提供默认文案。
|
|
250
|
+
*/
|
|
251
|
+
export function buildFormValidationMessages(t: (key: string) => string): Record<string, unknown> {
|
|
252
|
+
const typeMessage = (type: string): string => t(`uv.form.types.${type}`);
|
|
253
|
+
const lengthMessages = (type: string): Record<string, string> => ({
|
|
254
|
+
len: t(`uv.form.${type}.len`),
|
|
255
|
+
min: t(`uv.form.${type}.min`),
|
|
256
|
+
max: t(`uv.form.${type}.max`),
|
|
257
|
+
range: t(`uv.form.${type}.range`),
|
|
258
|
+
});
|
|
259
|
+
return {
|
|
260
|
+
default: t('uv.form.default'),
|
|
261
|
+
required: t('uv.form.required'),
|
|
262
|
+
enum: t('uv.form.enum'),
|
|
263
|
+
whitespace: t('uv.form.whitespace'),
|
|
264
|
+
date: {
|
|
265
|
+
format: t('uv.form.date.format'),
|
|
266
|
+
parse: t('uv.form.date.parse'),
|
|
267
|
+
invalid: t('uv.form.date.invalid'),
|
|
268
|
+
},
|
|
269
|
+
types: {
|
|
270
|
+
string: typeMessage('string'),
|
|
271
|
+
method: typeMessage('method'),
|
|
272
|
+
array: typeMessage('array'),
|
|
273
|
+
object: typeMessage('object'),
|
|
274
|
+
number: typeMessage('number'),
|
|
275
|
+
date: typeMessage('date'),
|
|
276
|
+
boolean: typeMessage('boolean'),
|
|
277
|
+
integer: typeMessage('integer'),
|
|
278
|
+
float: typeMessage('float'),
|
|
279
|
+
regexp: typeMessage('regexp'),
|
|
280
|
+
email: typeMessage('email'),
|
|
281
|
+
url: typeMessage('url'),
|
|
282
|
+
hex: typeMessage('hex'),
|
|
283
|
+
},
|
|
284
|
+
string: lengthMessages('string'),
|
|
285
|
+
number: lengthMessages('number'),
|
|
286
|
+
array: lengthMessages('array'),
|
|
287
|
+
pattern: {
|
|
288
|
+
mismatch: t('uv.form.pattern.mismatch'),
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** 按 `a.b.c` 路径从 vue-i18n 实例 messages 中取消息 */
|
|
294
|
+
function getNestedMessage(messages: I18nGlobalMessages, locale: string, key: string): string | undefined {
|
|
295
|
+
const segments = String(key).split('.');
|
|
296
|
+
let current: unknown = messages[locale];
|
|
297
|
+
for (const segment of segments) {
|
|
298
|
+
if (current == null || typeof current === 'string') {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
current = (current as Record<string, unknown>)[segment];
|
|
302
|
+
}
|
|
303
|
+
return typeof current === 'string' ? current : undefined;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* 翻译(App / 小程序 / 无 i18n 实例时的统一实现)。
|
|
308
|
+
*
|
|
309
|
+
* locale 优先级:storage `UNI_LOCALE` → `uni.$i18n.global.locale` → 内部 `localeState` → `componentLocale`;
|
|
310
|
+
* 消息优先级:实例 messages(若存在)→ 内置/初始化扁平语言包 → 配置 fallback → key。
|
|
311
|
+
*/
|
|
312
|
+
export function appTranslate(key: string, values?: Record<string, string | number>): string {
|
|
313
|
+
const global = getI18nGlobal();
|
|
314
|
+
const messages = unwrapI18nValue(global?.messages) || {};
|
|
315
|
+
const globalLocale = String(unwrapI18nValue(global?.locale) || '');
|
|
316
|
+
const locale = readStorageLocale() || globalLocale || localeState.value || componentLocale;
|
|
317
|
+
const fallback = componentFallbackLocale;
|
|
318
|
+
const message = getNestedMessage(messages, locale, key) || getNestedMessage(messages, fallback, key);
|
|
319
|
+
if (message) {
|
|
320
|
+
return replacePlaceholders(message, values);
|
|
321
|
+
}
|
|
322
|
+
const builtin = getBuiltinMessage(locale, key, values) || getBuiltinMessage(fallback, key, values);
|
|
323
|
+
return builtin || key;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** 把扁平 locale 包转换为 vue-i18n 需要的嵌套结构 */
|
|
327
|
+
function toNestedMessages(messages: UViewI18nMessages): I18nGlobalMessages {
|
|
328
|
+
const nested: I18nGlobalMessages = {};
|
|
329
|
+
for (const locale of Object.keys(messages)) {
|
|
330
|
+
const target: Record<string, unknown> = {};
|
|
331
|
+
for (const key of Object.keys(messages[locale])) {
|
|
332
|
+
const segments = key.split('.');
|
|
333
|
+
let current = target;
|
|
334
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
335
|
+
const segment = segments[i];
|
|
336
|
+
const next = current[segment];
|
|
337
|
+
if (typeof next !== 'object' || next === null) {
|
|
338
|
+
current[segment] = {};
|
|
339
|
+
}
|
|
340
|
+
current = current[segment] as Record<string, unknown>;
|
|
341
|
+
}
|
|
342
|
+
current[segments[segments.length - 1]] = messages[locale][key];
|
|
343
|
+
}
|
|
344
|
+
nested[locale] = target;
|
|
345
|
+
}
|
|
346
|
+
return nested;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* 创建 i18n 实例并挂到 `uni.$i18n`。
|
|
351
|
+
*
|
|
352
|
+
* - locale:显式 `opts.locale` 优先,否则使用当前组件 locale(默认 `en`);
|
|
353
|
+
* - fallback:显式 `opts.fallbackLocale` 优先,否则使用当前组件 fallback;
|
|
354
|
+
* - messages:内置 `en` + 已配置组件语言包 + `opts.messages`,按扁平 key 覆盖后转嵌套传给 vue-i18n;
|
|
355
|
+
* - App 端不依赖 vue-i18n 注入,直接把 `global.t` 替换为包内 `appTranslate`。
|
|
356
|
+
*/
|
|
357
|
+
export function createI18n(opts: UViewCreateI18nOptions = {}): I18n {
|
|
358
|
+
const locale = typeof opts.locale === 'string' && opts.locale ? opts.locale : componentLocale;
|
|
359
|
+
const fallback = opts.fallbackLocale ?? componentFallbackLocale;
|
|
360
|
+
const flatMessages = mergeDefaultMessages(opts.messages);
|
|
361
|
+
const mergedOptions: UViewI18nOptions = {
|
|
362
|
+
...opts,
|
|
363
|
+
locale,
|
|
364
|
+
fallbackLocale: fallback,
|
|
365
|
+
// #ifdef WEB || H5
|
|
366
|
+
// vue-i18n 9.1.9 的 globalInjection 默认 false;H5 开启后 app.use(i18n) 会注入
|
|
367
|
+
// `app.config.globalProperties.$i18n`,useI18n 才能检测到实例并走 vue-i18n 分支
|
|
368
|
+
//(可显式传 globalInjection: false 关闭;App / 小程序不需要,保持 vue-i18n 默认)
|
|
369
|
+
globalInjection: opts.globalInjection !== false,
|
|
370
|
+
// #endif
|
|
371
|
+
messages: toNestedMessages(flatMessages),
|
|
372
|
+
} as unknown as UViewI18nOptions;
|
|
373
|
+
const i18n: I18n = _createI18n(mergedOptions);
|
|
374
|
+
// #ifdef APP
|
|
375
|
+
i18n.global.t = appTranslate as unknown as typeof i18n.global.t;
|
|
376
|
+
// #endif
|
|
377
|
+
if (typeof uni !== 'undefined') {
|
|
378
|
+
uni.$i18n = i18n;
|
|
379
|
+
}
|
|
380
|
+
componentLocale = locale;
|
|
381
|
+
if (typeof fallback === 'string') {
|
|
382
|
+
componentFallbackLocale = fallback;
|
|
383
|
+
}
|
|
384
|
+
localeState.value = locale;
|
|
385
|
+
return i18n;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** 当前实例语言包(内部已转换为嵌套结构;internal,不进 barrel) */
|
|
389
|
+
export function getI18nMessages(): I18nGlobalMessages {
|
|
390
|
+
const messages = unwrapI18nValue(getI18nGlobal()?.messages);
|
|
391
|
+
return messages || {};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* 设置当前语言。
|
|
396
|
+
*
|
|
397
|
+
* 顺序(关键,防止 App 端重启后丢失语言):
|
|
398
|
+
* 1. 更新内部 `localeState`;
|
|
399
|
+
* 2. 更新项目自己的 `uni.$i18n.global.locale`(兼容 WritableComputedRef 写 `.value` / legacy 字符串);
|
|
400
|
+
* 3. 写入 storage 的 `UNI_LOCALE`;
|
|
401
|
+
* 4. 最后才调用 `uni.setLocale`(App 端可能引起重启,此时前两步已落盘)。
|
|
402
|
+
*
|
|
403
|
+
* 注意:`useI18n()` 返回的 `locale` 的 setter 只更新 `global.locale` / `localeState`,不会调用本函数;
|
|
404
|
+
* 需要持久化 / 通知系统时请显式调用 `setLocale`。
|
|
405
|
+
*/
|
|
406
|
+
export function setLocale(locale: string): void {
|
|
407
|
+
localeState.value = locale;
|
|
408
|
+
const global = getI18nGlobal();
|
|
409
|
+
if (global) {
|
|
410
|
+
try {
|
|
411
|
+
const target: unknown = global.locale;
|
|
412
|
+
if (target && typeof target === 'object' && 'value' in target) {
|
|
413
|
+
(target as RefLike<string>).value = locale;
|
|
414
|
+
} else {
|
|
415
|
+
global.locale = locale;
|
|
416
|
+
}
|
|
417
|
+
} catch (e) {
|
|
418
|
+
// 实例不可写时忽略,继续持久化
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (typeof uni !== 'undefined' && typeof uni.setStorageSync === 'function') {
|
|
422
|
+
try {
|
|
423
|
+
uni.setStorageSync(STORAGE_KEY, locale);
|
|
424
|
+
} catch (e) {
|
|
425
|
+
// storage 不可用时忽略
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (typeof uni !== 'undefined' && typeof uni.setLocale === 'function') {
|
|
429
|
+
try {
|
|
430
|
+
uni.setLocale(locale);
|
|
431
|
+
} catch (e) {
|
|
432
|
+
// App 端 setLocale 可能触发重启 / 平台不支持,忽略
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* 按当前语言取词(公共 API)。
|
|
439
|
+
* 统一走 `appTranslate`(优先读 `uni.$i18n` 语言包,其次默认/初始化语言包),
|
|
440
|
+
* 避免小程序端 vue-i18n runtime-only 构建缺少消息编译器导致翻译空白 / 占位符不插值。
|
|
441
|
+
*/
|
|
442
|
+
export function getLocaleMessage(key: string, values?: Record<string, string | number>): string {
|
|
443
|
+
return appTranslate(key, values);
|
|
444
|
+
}
|