@zfqh/uview-x 0.0.5 → 0.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +51 -8
  3. package/components/uv-badge/uv-badge.vue +4 -4
  4. package/components/uv-button/uv-button.vue +10 -10
  5. package/components/uv-checkbox/uv-checkbox.vue +11 -9
  6. package/components/uv-checkbox-group/uv-checkbox-group.vue +2 -2
  7. package/components/uv-code/uv-code.vue +2 -2
  8. package/components/uv-dialog/uv-dialog.vue +11 -10
  9. package/components/uv-form/uv-form.vue +244 -246
  10. package/components/uv-form-item/uv-form-item.vue +10 -9
  11. package/components/uv-icon/uv-icon.vue +13 -13
  12. package/components/uv-input/uv-input.vue +6 -5
  13. package/components/uv-line/uv-line.vue +7 -6
  14. package/components/uv-line-progress/uv-line-progress.vue +5 -5
  15. package/components/uv-loading-icon/uv-loading-icon.vue +9 -8
  16. package/components/uv-overlay/uv-overlay.vue +3 -2
  17. package/components/uv-popup/uv-popup.vue +7 -5
  18. package/components/uv-safe-bottom/uv-safe-bottom.vue +4 -3
  19. package/components/uv-status-bar/uv-status-bar.vue +4 -3
  20. package/components/uv-tabs/uv-tabs.vue +15 -12
  21. package/components/uv-transition/uv-transition.vue +2 -2
  22. package/hooks/index.ts +5 -5
  23. package/hooks/useDebounce.ts +33 -35
  24. package/hooks/useI18n.ts +4 -3
  25. package/hooks/useThrottle.ts +36 -38
  26. package/index.ts +50 -29
  27. package/locales/en.ts +56 -79
  28. package/locales/es.ts +47 -69
  29. package/locales/fr.ts +47 -69
  30. package/locales/index.ts +15 -159
  31. package/locales/ru.ts +47 -69
  32. package/locales/zh-Hans.ts +57 -79
  33. package/locales/zh-Hant.ts +47 -69
  34. package/package.json +2 -2
  35. package/types/index.d.ts +68 -66
  36. package/utils/color.ts +139 -0
  37. package/utils/compress.ts +28 -28
  38. package/utils/config.ts +45 -10
  39. package/utils/debounce.ts +4 -4
  40. package/utils/function.ts +2 -87
  41. package/utils/locale.ts +444 -304
  42. package/utils/merge.ts +74 -0
  43. package/utils/platform.ts +28 -28
  44. package/utils/route.ts +123 -129
  45. package/utils/scrollbar.ts +19 -19
  46. package/utils/throttle.ts +4 -4
  47. package/utils/colorGradient.ts +0 -139
  48. package/utils/index.ts +0 -58
package/utils/locale.ts CHANGED
@@ -1,304 +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 { getBuiltinMessage, mergeDefaultMessages } from '../locales';
6
-
7
- /**
8
- * 国际化能力(默认中文 + 可选语言包 + 4 个公共 API)。
9
- *
10
- * 默认语言只有简体中文(`zh-Hans`),`createI18n` 只注入并深合并内置中文;
11
- * 英文等其他语言为独立语言包(`locales/en.ts`),由使用方显式引入后合并进 `messages`。
12
- *
13
- * 公共 API(由 `utils/index.ts` 导出到包入口):
14
- * `createI18n`、`getLocale`、`getLocaleMessage`、`setLocale`。
15
- *
16
- * 其余导出(`localeState`、`getI18nGlobal`、`getI18nMessages`、`appTranslate`、`unwrapI18nValue`
17
- * 与 `I18nMessageValue` / `I18nMessages` / `I18nGlobalLike`)是包内实现的 internal helper,
18
- * 供 components / hooks / locales 深引用,不进入 barrel 与 `types/index.d.ts`。
19
- */
20
-
21
- /** i18n 消息树:值为字符串,或继续嵌套的对象 */
22
- export type I18nMessageValue = string | { [key: string]: I18nMessageValue };
23
-
24
- /** 语言包:语言 → 消息树 */
25
- export type I18nMessages = Record<string, I18nMessageValue>;
26
-
27
- /** 兼容 vue-i18n legacy / composition 两种 global 形态的运行时视图 */
28
- export interface I18nGlobalLike {
29
- t: (key: string, values?: Record<string, string | number>) => string;
30
- locale: string | { value: string };
31
- messages: I18nMessages | { value: I18nMessages };
32
- }
33
-
34
- interface RefLike<T> {
35
- value: T;
36
- }
37
-
38
- /**
39
- * `uni` 全局对象的最小运行时视图(含 i18n 实例挂载点)。
40
- *
41
- * 对外 `$i18n` 声明在 `types/index.d.ts`;这里再留一份最小结构,保证其他包深引用
42
- * `@zfqh/uview-x/utils/locale`(不会加载本包 types 入口)时仍能编译。
43
- * 纯 Node / 单测环境可能没有 `uni` 全局,所有访问都要容错。
44
- */
45
- interface UniWithI18n {
46
- $i18n?: I18n;
47
- getStorageSync?: (key: string) => unknown;
48
- setStorageSync?: (key: string, value: unknown) => void;
49
- getLocale?: () => string;
50
- setLocale?: (locale: string) => void;
51
- getAppBaseInfo?: () => { appLanguage?: string; language?: string };
52
- }
53
-
54
- /** 读取 uni 全局对象;不存在时返回 undefined */
55
- function getUni(): UniWithI18n | undefined {
56
- return typeof uni === 'undefined' ? undefined : (uni as unknown as UniWithI18n);
57
- }
58
-
59
- /** 读取挂到 `uni.$i18n` 上的 i18n 实例 */
60
- function getUniI18n(): I18n | undefined {
61
- const uniApi = getUni();
62
- return uniApi ? uniApi.$i18n : undefined;
63
- }
64
-
65
- /** 语言持久化 key:App 端切换语言可能触发重启,必须先落盘 */
66
- const STORAGE_KEY = 'UNI_LOCALE';
67
-
68
- /** 回退语言(内部使用,不对外导出) */
69
- const fallbackLocale = 'zh-Hans';
70
-
71
- /** 读取上次选择的语言;storage 不可用或无有效值时返回空字符串 */
72
- function readStorageLocale(): string {
73
- const uniApi = getUni();
74
- if (uniApi && typeof uniApi.getStorageSync === 'function') {
75
- try {
76
- const stored: unknown = uniApi.getStorageSync(STORAGE_KEY);
77
- if (typeof stored === 'string' && stored) {
78
- return stored;
79
- }
80
- } catch (e) {
81
- // storage 不可用时忽略,继续走系统语言
82
- }
83
- }
84
- // #ifdef WEB || H5
85
- // H5 nvue 页面上下文可能拿到非共享的 uni 存储视图,浏览器 localStorage 仍为同一来源
86
- if (typeof localStorage !== 'undefined') {
87
- try {
88
- const stored = localStorage.getItem(STORAGE_KEY);
89
- if (stored) {
90
- return stored;
91
- }
92
- } catch (e) {
93
- // localStorage 不可用时忽略
94
- }
95
- }
96
- // #endif
97
- return '';
98
- }
99
-
100
- /**
101
- * 获取当前语言。
102
- *
103
- * 顺序:
104
- * 1. 本地存储 `UNI_LOCALE`(用户上次通过 `setLocale` 选择的语言);
105
- * 2. `uni.getLocale()`(H5 / 小程序 / 经典 App);
106
- * 3. `uni.getAppBaseInfo().appLanguage`(uni-app x App);
107
- * 4. `uni.getAppBaseInfo().language`(经典端已废弃字段,兜底);
108
- * 5. `zh-Hans`。
109
- */
110
- export function getLocale(): string {
111
- const stored = readStorageLocale();
112
- if (stored) {
113
- return stored;
114
- }
115
- const uniApi = getUni();
116
- if (uniApi && typeof uniApi.getLocale === 'function') {
117
- const systemLocale = uniApi.getLocale();
118
- if (systemLocale) {
119
- return systemLocale;
120
- }
121
- }
122
- if (uniApi && typeof uniApi.getAppBaseInfo === 'function') {
123
- const info = uniApi.getAppBaseInfo();
124
- const systemLocale = info.appLanguage || info.language;
125
- if (systemLocale) {
126
- return systemLocale;
127
- }
128
- }
129
- return fallbackLocale;
130
- }
131
-
132
- /** 初始语言(内部使用,不对外导出) */
133
- const defaultLocale: string = getLocale();
134
-
135
- /**
136
- * 当前语言的内部响应式状态(internal,不进 barrel)。
137
- *
138
- * - 无 i18n 实例时是 `appTranslate` / `useI18n` 的唯一语言来源;
139
- * - 创建实例时用 `opts.locale` 初始化,`setLocale` 时同步更新,保证语言切换后组件重新渲染。
140
- */
141
- export const localeState: Ref<string> = ref<string>(defaultLocale);
142
-
143
- /** 兼容 vue-i18n legacy(字符串)与 composition(Ref)两种模式,取出实际值 */
144
- export function unwrapI18nValue<T>(value: T | { value: T }): T {
145
- if (value !== null && typeof value === 'object' && 'value' in value) {
146
- return (value as RefLike<T>).value;
147
- }
148
- return value as T;
149
- }
150
-
151
- function isI18nGlobalLike(value: unknown): value is I18nGlobalLike {
152
- if (!value || typeof value !== 'object') {
153
- return false;
154
- }
155
- return typeof (value as I18nGlobalLike).t === 'function';
156
- }
157
-
158
- /** 当前 `uni.$i18n.global` 的运行时视图;实例不存在时返回 undefined */
159
- export function getI18nGlobal(): I18nGlobalLike | undefined {
160
- const global = getUniI18n()?.global;
161
- return isI18nGlobalLike(global) ? global : undefined;
162
- }
163
-
164
- /** APP 下不支持动态变量,需要重写占位符替换 */
165
- function replacePlaceholders(str: string, values?: Record<string, string | number>): string {
166
- if (!values || typeof str !== 'string') {
167
- return str;
168
- }
169
- let out = str;
170
- for (const k of Object.keys(values)) {
171
- out = out.split(`{${k}}`).join(String(values[k]));
172
- }
173
- return out;
174
- }
175
-
176
- /** 按 `a.b.c` 路径从语言包中取消息 */
177
- function getMessage(messages: I18nMessages, locale: string, key: string): string | undefined {
178
- const segments = String(key).split('.');
179
- let current: I18nMessageValue | undefined = messages[locale];
180
- for (const segment of segments) {
181
- if (current == null || typeof current === 'string') {
182
- return undefined;
183
- }
184
- current = current[segment];
185
- }
186
- return typeof current === 'string' ? current : undefined;
187
- }
188
-
189
- /**
190
- * 翻译(App / 小程序 / 无 i18n 实例时的统一实现)。
191
- *
192
- * locale 优先级:storage `UNI_LOCALE` → `uni.$i18n.global.locale` → 内部 `localeState` → `zh-Hans`;
193
- * 消息优先级:全局语言包(当前语言 → 中文回退)→ 内置中文默认语言包 → key。
194
- * 其他语言(如英文)由使用方在 `createI18n` 中显式合并,未合并时回退中文。
195
- * storage 优先用于 nvue 等页面上下文模块副本不同、实例未共享的场景(setLocale 已先落盘)。
196
- */
197
- export function appTranslate(key: string, values?: Record<string, string | number>): string {
198
- const global = getI18nGlobal();
199
- const messages = unwrapI18nValue(global?.messages) || {};
200
- const globalLocale = String(unwrapI18nValue(global?.locale) || '');
201
- const locale = readStorageLocale() || globalLocale || localeState.value || fallbackLocale;
202
- const message = getMessage(messages, locale, key) || getMessage(messages, fallbackLocale, key);
203
- if (message) {
204
- return replacePlaceholders(message, values);
205
- }
206
- const builtin = getBuiltinMessage(locale, key, values) || getBuiltinMessage(fallbackLocale, key, values);
207
- return builtin || key;
208
- }
209
-
210
- /**
211
- * 创建 i18n 实例并挂到 `uni.$i18n`。
212
- *
213
- * - 只注入内置默认中文:`mergeDefaultMessages(opts.messages)` 仅深合并 `zh-Hans`,
214
- * 用户同名 key 优先;其他 locale(如显式引入的 `en`)原样透传,不做内置补全;
215
- * - 未显式传 `fallbackLocale` 时默认 `zh-Hans`,未合并的语言回退中文;
216
- * - App 端不依赖 vue-i18n 注入,直接把 `global.t` 替换为包内 `appTranslate`;
217
- * - 用 `opts.locale` 初始化内部 `localeState`,无实例时也能保持语言一致。
218
- */
219
- export function createI18n(opts: I18nOptions): I18n {
220
- const mergedMessages: I18nMessages = mergeDefaultMessages(opts.messages as I18nMessages | undefined);
221
- const mergedOptions: I18nOptions = {
222
- ...opts,
223
- // 默认回退中文;显式传入的值(含 false)优先
224
- fallbackLocale: opts.fallbackLocale ?? fallbackLocale,
225
- // #ifdef WEB || H5
226
- // vue-i18n 9.1.9 的 globalInjection 默认 false;H5 开启后 app.use(i18n) 会注入
227
- // `app.config.globalProperties.$i18n`,useI18n 才能检测到实例并走 vue-i18n 分支
228
- //(可显式传 globalInjection: false 关闭;App / 小程序不需要,保持 vue-i18n 默认)
229
- globalInjection: opts.globalInjection !== false,
230
- // #endif
231
- messages: mergedMessages,
232
- } as unknown as I18nOptions;
233
- const i18n: I18n = _createI18n(mergedOptions);
234
- // #ifdef APP
235
- i18n.global.t = appTranslate as unknown as typeof i18n.global.t;
236
- // #endif
237
- const uniApi = getUni();
238
- if (uniApi) {
239
- uniApi.$i18n = i18n;
240
- }
241
- if (typeof opts.locale === 'string' && opts.locale) {
242
- localeState.value = opts.locale;
243
- }
244
- return i18n;
245
- }
246
-
247
- /** 当前语言包(兼容 composition 模式的 Ref;internal,不进 barrel) */
248
- export function getI18nMessages(): I18nMessages {
249
- const messages = unwrapI18nValue(getI18nGlobal()?.messages);
250
- return messages || {};
251
- }
252
-
253
- /**
254
- * 设置当前语言。
255
- *
256
- * 顺序(关键,防止 App 端重启后丢失语言):
257
- * 1. 更新内部 `localeState`;
258
- * 2. 更新项目自己的 `uni.$i18n.global.locale`(兼容 WritableComputedRef 写 `.value` / legacy 字符串);
259
- * 3. 写入 storage 的 `UNI_LOCALE`;
260
- * 4. 最后才调用 `uni.setLocale`(App 端可能引起重启,此时前两步已落盘)。
261
- *
262
- * 注意:`useI18n()` 返回的 `locale` 的 setter 只更新 `global.locale` / `localeState`,不会调用本函数;
263
- * 需要持久化 / 通知系统时请显式调用 `setLocale`。
264
- */
265
- export function setLocale(locale: string): void {
266
- localeState.value = locale;
267
- const global = getI18nGlobal();
268
- if (global) {
269
- try {
270
- const target: unknown = global.locale;
271
- if (target && typeof target === 'object' && 'value' in target) {
272
- (target as RefLike<string>).value = locale;
273
- } else {
274
- global.locale = locale;
275
- }
276
- } catch (e) {
277
- // 实例不可写时忽略,继续持久化
278
- }
279
- }
280
- const uniApi = getUni();
281
- if (uniApi && typeof uniApi.setStorageSync === 'function') {
282
- try {
283
- uniApi.setStorageSync(STORAGE_KEY, locale);
284
- } catch (e) {
285
- // storage 不可用时忽略
286
- }
287
- }
288
- if (uniApi && typeof uniApi.setLocale === 'function') {
289
- try {
290
- uniApi.setLocale(locale);
291
- } catch (e) {
292
- // App 端 setLocale 可能触发重启 / 平台不支持,忽略
293
- }
294
- }
295
- }
296
-
297
- /**
298
- * 按当前语言取词(公共 API)。
299
- * 统一走 `appTranslate`(优先读 `uni.$i18n` 语言包,其次默认中文语言包),
300
- * 避免小程序端 vue-i18n runtime-only 构建缺少消息编译器导致翻译空白 / 占位符不插值。
301
- */
302
- export function getLocaleMessage(key: string, values?: Record<string, string | number>): string {
303
- return appTranslate(key, values);
304
- }
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
+ }