@liujitcn/kratos-taro-app-core 0.0.20 → 0.0.21

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.
@@ -0,0 +1,113 @@
1
+ {
2
+ "common.action.back": "返回",
3
+ "common.action.cancel": "取消",
4
+ "common.action.confirm": "確定",
5
+ "common.action.delete": "刪除",
6
+ "common.action.login": "登入",
7
+ "common.action.logout": "退出登入",
8
+ "common.action.new": "新建",
9
+ "common.action.openInNewWindow": "新視窗開啟",
10
+ "common.action.retry": "重試",
11
+ "common.action.save": "儲存",
12
+ "common.action.search": "搜尋",
13
+ "common.field.language": "語言",
14
+ "common.language.en-US": "English",
15
+ "common.language.ja-JP": "日本語",
16
+ "common.language.zh-CN": "簡體中文",
17
+ "common.language.zh-TW": "繁體中文",
18
+ "common.language.ko-KR": "韓語",
19
+ "common.language.fr-FR": "法語",
20
+ "common.language.es-ES": "西班牙語",
21
+ "common.message.loading": "正在載入",
22
+ "common.message.networkError": "網路錯誤,換個網路試試",
23
+ "common.message.requestError": "請求錯誤",
24
+ "common.message.unknown": "暫時無法處理,請稍後重試",
25
+ "common.title.notice": "提示",
26
+ "core.auth.loginAgain": "重新登入",
27
+ "core.auth.sessionExpired": "當前頁面已失效,請重新登入",
28
+ "core.config.missing": "移動端配置缺失:{names}",
29
+ "core.crypto.base64DecodeUnsupported": "當前環境不支援 Base64 解碼",
30
+ "core.crypto.base64EncodeUnsupported": "當前環境不支援 Base64 編碼",
31
+ "core.crypto.encryptFailed": "密碼加密失敗",
32
+ "core.crypto.passwordRequired": "密碼不能為空",
33
+ "core.crypto.passwordTooLong": "密碼長度超出限制",
34
+ "core.crypto.publicKeyInvalid": "公鑰格式無效",
35
+ "core.crypto.randomFailed": "隨機數生成失敗",
36
+ "core.crypto.unsupported": "當前環境不支援密碼加密",
37
+ "core.file.downloadFailed": "下載失敗",
38
+ "core.file.temporaryPathRequired": "小程式端請使用檔案臨時路徑上傳",
39
+ "core.file.uploadFailed": "上傳失敗",
40
+ "core.home.accountCapability": "賬戶能力",
41
+ "core.home.accountCapabilityDescription": "個人資料、應用設定與智慧助手入口已保留",
42
+ "core.home.crossPlatform": "跨端頁面",
43
+ "core.home.crossPlatformDescription": "同一套頁面程式碼適配 H5 和微信小程式",
44
+ "core.home.demoScope": "演示範圍",
45
+ "core.home.framework": "應用框架",
46
+ "core.home.language": "開發語言",
47
+ "core.home.mainTitle": "應用框架示例",
48
+ "core.home.stateManagement": "狀態管理",
49
+ "core.home.staticHome": "靜態首頁",
50
+ "core.home.staticHomeDescription": "當前首頁用於展示框架資訊,不依賴業務介面",
51
+ "core.home.styleSolution": "樣式方案",
52
+ "core.home.subTitle": "保留通用導航與個人中心體驗",
53
+ "core.home.techStack": "當前技術棧",
54
+ "core.login.agreePrefix": "我已閱讀並同意",
55
+ "core.login.agreeSeparator": "和",
56
+ "core.login.behaviorClick": "請按順序點選文字",
57
+ "core.login.behaviorLoading": "載入中...",
58
+ "core.login.behaviorPuzzle": "請拖動滑塊完成拼圖",
59
+ "core.login.behaviorRotate": "拖動滑塊,將內圈圖片轉正",
60
+ "core.login.bindWechat": "登入並繫結微信",
61
+ "core.login.captcha": "請輸入驗證碼",
62
+ "core.login.captchaLoadFailed": "驗證碼載入失敗",
63
+ "core.login.captchaTypeMissing": "登入驗證碼型別未配置",
64
+ "core.login.captchaTypeName": "登入驗證碼型別",
65
+ "core.login.captchaTypeResponseMissing": "驗證碼介面未返回型別",
66
+ "core.login.captchaWriteFailed": "驗證碼圖片寫入失敗",
67
+ "core.login.defaultSubTitle": "歡迎使用本應用",
68
+ "core.login.loginFailed": "登入失敗,請重試",
69
+ "core.login.loginSuccess": "登入成功",
70
+ "core.login.password": "請輸入密碼",
71
+ "core.login.privacy": "《隱私協議》",
72
+ "core.login.protocolMissing": "服務條款和隱私協議未配置",
73
+ "core.login.protocolPrompt": "請先閱讀並勾選協議內容,點選確定後將自動勾選並繼續登入",
74
+ "core.login.service": "《服務條款》",
75
+ "core.login.tenant": "請輸入租戶編號",
76
+ "core.login.userName": "請輸入使用者名稱或手機號",
77
+ "core.login.userNameMobile": "請輸入使用者名稱/手機號碼",
78
+ "core.login.userNamePassword": "請輸入使用者名稱和密碼",
79
+ "core.login.wechat": "微信一鍵登入",
80
+ "core.login.wechatBindTip": "該微信尚未繫結,請登入已有賬號完成繫結",
81
+ "core.login.wechatFailed": "微信登入失敗,請稍後重試",
82
+ "core.navigation.home": "首頁",
83
+ "core.navigation.error.access": "選單訪問模式無效:{access}",
84
+ "core.navigation.error.childTab": "只有移動端二級選單可以作為 tab:{name}",
85
+ "core.navigation.error.cycle": "移動端選單存在迴圈父級:{name}",
86
+ "core.navigation.error.identity": "選單 id、name 或 app/ 路徑無效",
87
+ "core.navigation.error.itemObject": "選單項不是物件",
88
+ "core.navigation.error.listMissing": "選單響應缺少扁平列表",
89
+ "core.navigation.error.namePrefix": "選單名稱必須使用 App 字首:{name}",
90
+ "core.navigation.error.parentInvalid": "移動端選單父級編號無效:{name}",
91
+ "core.navigation.error.parentMissing": "選單缺少父級編號:{name}",
92
+ "core.navigation.error.parentNotFound": "移動端選單父級不存在:{name}",
93
+ "core.navigation.error.responseObject": "選單響應不是物件",
94
+ "core.navigation.error.rootTab": "移動端二級選單必須作為 tab:{name}",
95
+ "core.navigation.error.tabCount": "tabBar 只能配置 0 或 2-5 項",
96
+ "core.navigation.error.unique": "選單 id、name、path 必須唯一",
97
+ "core.navigation.error.viewKey": "未註冊 viewKey:{viewKey}",
98
+ "core.navigation.my": "我的",
99
+ "core.navigation.protocol": "協議詳情",
100
+ "core.protocol.loadFailed": "移動端配置載入失敗",
101
+ "core.protocol.notConfigured": "{title}未配置,暫時無法檢視",
102
+ "core.protocol.privacy": "隱私協議",
103
+ "core.protocol.service": "服務條款",
104
+ "core.status.CONFIG_ERROR": "導航配置無效",
105
+ "core.status.FORBIDDEN": "暫無訪問許可權",
106
+ "core.status.NOT_FOUND": "頁面不存在",
107
+ "core.status.OFFLINE": "網路不可用",
108
+ "core.status.PAGE_UNAVAILABLE": "頁面暫不可用",
109
+ "core.status.loading": "正在載入",
110
+ "core.webview.embedBlocked": "當前 H5 頁面可能被目標站點限制嵌入",
111
+ "core.webview.invalidUrl": "缺少有效連結地址",
112
+ "core.webview.openFailed": "連結無法開啟"
113
+ }
package/src/module.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { LocaleMessages, SupportedLocale } from './locales'
2
+
1
3
  /** bootstrap 可见状态键。 */
2
4
  export type BootstrapViewKey =
3
5
  | 'BOOTSTRAP_LOADING'
@@ -19,6 +21,8 @@ export interface KratosTaroModule {
19
21
  pages: Record<string, KratosTaroPageConfig>
20
22
  views: Record<string, string>
21
23
  icons?: Record<string, string>
24
+ /** 模块按语言区域贡献的扁平语言包。 */
25
+ messages?: Partial<Record<SupportedLocale, LocaleMessages>>
22
26
  }
23
27
 
24
28
  /** 声明 Taro 模块。 */
package/src/navigation.ts CHANGED
@@ -6,6 +6,7 @@ import { matchLogicalPath, parseLogicalQuery } from './navigation-pattern.mjs'
6
6
  import { buildMenuTree } from './navigation-tree.mjs'
7
7
  import { hasValidToken } from './utils/auth'
8
8
  import { navigateToLogin } from './utils/navigation'
9
+ import { getCurrentLocale, t } from './locales'
9
10
 
10
11
  /** 移动菜单访问模式。 */
11
12
  export type AppMenuAccess = 'PUBLIC' | 'GUEST_ONLY' | 'AUTHENTICATED'
@@ -62,7 +63,7 @@ export const defaultAppMenus: AppMenu[] = [
62
63
  name: 'AppHome',
63
64
  path: 'app/home',
64
65
  viewKey: 'HOME',
65
- title: '首页',
66
+ title: '',
66
67
  access: 'PUBLIC',
67
68
  inTabBar: true,
68
69
  icon: 'HOME_DEFAULT',
@@ -74,7 +75,7 @@ export const defaultAppMenus: AppMenu[] = [
74
75
  name: 'AppMy',
75
76
  path: 'app/my',
76
77
  viewKey: 'PROFILE_HOME',
77
- title: '我的',
78
+ title: '',
78
79
  access: 'PUBLIC',
79
80
  inTabBar: true,
80
81
  icon: 'USER_DEFAULT',
@@ -86,7 +87,7 @@ export const defaultAppMenus: AppMenu[] = [
86
87
  name: 'AppLogin',
87
88
  path: 'app/login',
88
89
  viewKey: 'LOGIN',
89
- title: '登录',
90
+ title: '',
90
91
  access: 'GUEST_ONLY',
91
92
  inTabBar: false,
92
93
  },
@@ -96,7 +97,7 @@ export const defaultAppMenus: AppMenu[] = [
96
97
  name: 'AppProtocol',
97
98
  path: 'app/protocol/:type',
98
99
  viewKey: 'PROTOCOL',
99
- title: '协议详情',
100
+ title: '',
100
101
  access: 'PUBLIC',
101
102
  inTabBar: false,
102
103
  },
@@ -106,7 +107,7 @@ export const defaultAppMenus: AppMenu[] = [
106
107
  name: 'AppProfile',
107
108
  path: 'app/profile',
108
109
  viewKey: 'PROFILE',
109
- title: '个人信息',
110
+ title: '',
110
111
  access: 'AUTHENTICATED',
111
112
  inTabBar: false,
112
113
  },
@@ -116,7 +117,7 @@ export const defaultAppMenus: AppMenu[] = [
116
117
  name: 'AppSettings',
117
118
  path: 'app/settings',
118
119
  viewKey: 'SETTINGS',
119
- title: '设置',
120
+ title: '',
120
121
  access: 'AUTHENTICATED',
121
122
  inTabBar: false,
122
123
  },
@@ -126,7 +127,7 @@ export const defaultAppMenus: AppMenu[] = [
126
127
  name: 'AppAi',
127
128
  path: 'app/ai',
128
129
  viewKey: 'AI',
129
- title: 'AI 助手',
130
+ title: '',
130
131
  access: 'AUTHENTICATED',
131
132
  inTabBar: false,
132
133
  },
@@ -142,6 +143,23 @@ export const defaultAppMenus: AppMenu[] = [
142
143
  },
143
144
  ]
144
145
 
146
+ const defaultMenuTitleKeys: Record<string, string> = {
147
+ AppHome: 'core.navigation.home',
148
+ AppMy: 'core.navigation.my',
149
+ AppLogin: 'common.action.login',
150
+ AppProtocol: 'core.navigation.protocol',
151
+ AppProfile: 'system.profile.title',
152
+ AppSettings: 'system.settings.title',
153
+ AppAi: 'system.ai.chatTitle',
154
+ }
155
+
156
+ function localizedDefaultAppMenus(): AppMenu[] {
157
+ return defaultAppMenus.map((menu) => ({
158
+ ...menu,
159
+ title: defaultMenuTitleKeys[menu.name] ? t(defaultMenuTitleKeys[menu.name]) : menu.title,
160
+ }))
161
+ }
162
+
145
163
  function createNavigationState(menus: AppMenu[], ready: boolean): AppNavigationState {
146
164
  const menuTree = buildMenuTree(menus, APP_MENU_ROOT_ID) as AppMenuNode[]
147
165
  return { menus, ready, menuTree, tabBar: menuTree.filter((menu) => menu.inTabBar) }
@@ -165,7 +183,7 @@ export function setAppNavigationAdapter(nextAdapter: AppNavigationAdapter): void
165
183
  export async function initializeAppNavigation(): Promise<void> {
166
184
  const cacheKey = resolveCacheKey()
167
185
  const cached = readCachedMenus(cacheKey)
168
- useAppNavigation.setState(createNavigationState(cached ?? defaultAppMenus, false))
186
+ useAppNavigation.setState(createNavigationState(cached ?? localizedDefaultAppMenus(), false))
169
187
  try {
170
188
  const nextMenus = normalizeMenuResponse(await adapter.list())
171
189
  validateMenus(nextMenus)
@@ -242,7 +260,8 @@ export function launchAppStatus(
242
260
  }
243
261
 
244
262
  function resolveCacheKey(): string {
245
- return hasValidToken() ? AUTHENTICATED_CACHE_KEY : ANONYMOUS_CACHE_KEY
263
+ const identity = hasValidToken() ? AUTHENTICATED_CACHE_KEY : ANONYMOUS_CACHE_KEY
264
+ return `${identity}:${getCurrentLocale()}`
246
265
  }
247
266
 
248
267
  function readCachedMenus(cacheKey: string): AppMenu[] | undefined {
@@ -259,15 +278,15 @@ function readCachedMenus(cacheKey: string): AppMenu[] | undefined {
259
278
 
260
279
  function normalizeMenuResponse(response: unknown): AppMenu[] {
261
280
  if (Array.isArray(response)) return response.map(normalizeMenu)
262
- if (!response || typeof response !== 'object') throw new Error('菜单响应不是对象')
281
+ if (!response || typeof response !== 'object') throw new Error(t('core.navigation.error.responseObject'))
263
282
  const record = response as Record<string, unknown>
264
283
  const list = record.items ?? record.list ?? record.data
265
- if (!Array.isArray(list)) throw new Error('菜单响应缺少扁平列表')
284
+ if (!Array.isArray(list)) throw new Error(t('core.navigation.error.listMissing'))
266
285
  return list.map(normalizeMenu)
267
286
  }
268
287
 
269
288
  function normalizeMenu(value: unknown): AppMenu {
270
- if (!value || typeof value !== 'object') throw new Error('菜单项不是对象')
289
+ if (!value || typeof value !== 'object') throw new Error(t('core.navigation.error.itemObject'))
271
290
  const item = value as Record<string, unknown>
272
291
  const meta = (item.meta ?? {}) as Record<string, unknown>
273
292
  const app = (meta.app ?? item.app ?? {}) as Record<string, unknown>
@@ -299,15 +318,19 @@ function validateMenus(nextMenus: AppMenu[]): void {
299
318
  const paths = new Set<string>()
300
319
  for (const menu of nextMenus) {
301
320
  if (!Number.isSafeInteger(menu.id) || !menu.name || !menu.path.startsWith('app/')) {
302
- throw new Error('菜单 id、name 或 app/ 路径无效')
321
+ throw new Error(t('core.navigation.error.identity'))
322
+ }
323
+ if (!menu.name.startsWith('App')) {
324
+ throw new Error(t('core.navigation.error.namePrefix', { name: menu.name }))
303
325
  }
304
- if (!menu.name.startsWith('App')) throw new Error(`菜单名称必须使用 App 前缀:${menu.name}`)
305
326
  if (!['PUBLIC', 'GUEST_ONLY', 'AUTHENTICATED'].includes(menu.access)) {
306
- throw new Error(`菜单访问模式无效:${menu.access}`)
327
+ throw new Error(t('core.navigation.error.access', { access: menu.access }))
328
+ }
329
+ if (!resolveStaticView(menu.viewKey)) {
330
+ throw new Error(t('core.navigation.error.viewKey', { viewKey: menu.viewKey }))
307
331
  }
308
- if (!resolveStaticView(menu.viewKey)) throw new Error(`未注册 viewKey:${menu.viewKey}`)
309
332
  if (ids.has(menu.id) || names.has(menu.name) || paths.has(menu.path)) {
310
- throw new Error('菜单 id、name、path 必须唯一')
333
+ throw new Error(t('core.navigation.error.unique'))
311
334
  }
312
335
  ids.add(menu.id)
313
336
  names.add(menu.name)
@@ -315,22 +338,24 @@ function validateMenus(nextMenus: AppMenu[]): void {
315
338
  }
316
339
  const menuMap = new Map(nextMenus.map((menu) => [menu.id, menu]))
317
340
  for (const menu of nextMenus) {
318
- if (menu.parentId === undefined) throw new Error(`菜单缺少父级编号:${menu.name}`)
341
+ if (menu.parentId === undefined) {
342
+ throw new Error(t('core.navigation.error.parentMissing', { name: menu.name }))
343
+ }
319
344
  if (menu.parentId === APP_MENU_ROOT_ID) {
320
- if (!menu.inTabBar) throw new Error(`移动端二级菜单必须作为 tab:${menu.name}`)
345
+ if (!menu.inTabBar) throw new Error(t('core.navigation.error.rootTab', { name: menu.name }))
321
346
  continue
322
347
  }
323
- if (menu.inTabBar) throw new Error(`只有移动端二级菜单可以作为 tab:${menu.name}`)
348
+ if (menu.inTabBar) throw new Error(t('core.navigation.error.childTab', { name: menu.name }))
324
349
  const visited = new Set<number>([menu.id])
325
350
  let parentId = menu.parentId
326
351
  while (parentId !== APP_MENU_ROOT_ID) {
327
- if (visited.has(parentId)) throw new Error(`移动端菜单存在循环父级:${menu.name}`)
352
+ if (visited.has(parentId)) throw new Error(t('core.navigation.error.cycle', { name: menu.name }))
328
353
  visited.add(parentId)
329
354
  const parent = menuMap.get(parentId)
330
- if (!parent?.parentId) throw new Error(`移动端菜单父级不存在:${menu.name}`)
355
+ if (!parent?.parentId) throw new Error(t('core.navigation.error.parentNotFound', { name: menu.name }))
331
356
  parentId = parent.parentId
332
357
  }
333
358
  }
334
359
  const tabCount = nextMenus.filter((menu) => menu.parentId === APP_MENU_ROOT_ID).length
335
- if (tabCount === 1 || tabCount > 5) throw new Error('tabBar 只能配置 0 或 2-5 项')
360
+ if (tabCount === 1 || tabCount > 5) throw new Error(t('core.navigation.error.tabCount'))
336
361
  }
package/src/pages.ts CHANGED
@@ -6,11 +6,11 @@ export const corePages: Record<string, KratosTaroPageConfig> = {
6
6
  style: {
7
7
  navigationStyle: 'custom',
8
8
  navigationBarTextStyle: 'white',
9
- navigationBarTitleText: '首页',
9
+ navigationBarTitleText: '',
10
10
  },
11
11
  },
12
- 'pages/login/login': { style: { navigationBarTitleText: '登录' } },
13
- 'pages/login/protocal': { style: { navigationBarTitleText: '协议详情' } },
12
+ 'pages/login/login': { style: { navigationBarTitleText: '' } },
13
+ 'pages/login/protocal': { style: { navigationBarTitleText: '' } },
14
14
  'pages/webview/webview': { style: { navigationBarTitleText: '' } },
15
15
  'pages/status/index': {
16
16
  style: {
@@ -15,6 +15,8 @@ export interface GetConfigRequest {
15
15
 
16
16
  /** 系统配置项 */
17
17
  export interface ConfigItem {
18
+ /** 配置ID */
19
+ id: number;
18
20
  /** 配置key */
19
21
  key: string;
20
22
  /** 配置value */
@@ -0,0 +1,37 @@
1
+ // Code generated by protoc-gen-ts_proto. DO NOT EDIT.
2
+ // versions:
3
+ // protoc-gen-ts_proto v2.11.8
4
+ // protoc unknown
5
+ // source: base/v1/language.proto
6
+
7
+ /* eslint-disable */
8
+
9
+ /** 查询语言请求参数。 */
10
+ export interface GetLanguageRequest {
11
+ }
12
+
13
+ /** 支持的语言。 */
14
+ export interface LanguageItem {
15
+ /** 标准语言代码 */
16
+ language_code: string;
17
+ /** 语言名称 */
18
+ language_name: string;
19
+ /** 本地语言名称 */
20
+ native_name: string;
21
+ /** 排序,值越小越靠前 */
22
+ sort: number;
23
+ }
24
+
25
+ /** 当前支持的语言查询结果。 */
26
+ export interface GetLanguageResponse {
27
+ /** 当前支持的语言列表 */
28
+ languages: LanguageItem[];
29
+ /** 主语言代码 */
30
+ primary_language_code: string;
31
+ }
32
+
33
+ /** 语言公共服务。 */
34
+ export interface LanguageService {
35
+ /** 查询当前支持的语言和主语言。 */
36
+ GetLanguage(request: GetLanguageRequest): Promise<GetLanguageResponse>;
37
+ }
@@ -1,11 +1,12 @@
1
1
  import { create } from 'zustand'
2
2
  import { defConfigService } from '../api/base/config'
3
+ import { t } from '../locales'
3
4
  import { BaseConfigSite } from '../rpc/base/v1/enum'
4
5
 
5
6
  const REQUIRED_APP_CONFIGS = [
6
- { key: 'serviceProtocol', name: '服务条款' },
7
- { key: 'privacyProtocol', name: '隐私协议' },
8
- { key: 'captchaType', name: '登录验证码类型' },
7
+ { key: 'serviceProtocol', nameKey: 'core.login.service' },
8
+ { key: 'privacyProtocol', nameKey: 'core.login.privacy' },
9
+ { key: 'captchaType', nameKey: 'core.login.captchaTypeName' },
9
10
  ] as const
10
11
 
11
12
  /** 应用配置状态及操作。 */
@@ -30,7 +31,11 @@ export const useSettingStore = create<SettingStoreState>((set, get) => ({
30
31
  const nextData = new Map(response.configs.map((item) => [item.key, item.value]))
31
32
  const missing = REQUIRED_APP_CONFIGS.filter(({ key }) => !nextData.get(key))
32
33
  if (missing.length) {
33
- throw new Error(`移动端配置缺失:${missing.map(({ name }) => name).join('、')}`)
34
+ throw new Error(
35
+ t('core.config.missing', {
36
+ names: missing.map(({ nameKey }) => t(nameKey)).join(', '),
37
+ }),
38
+ )
34
39
  }
35
40
  set({ data: nextData })
36
41
  })()
package/src/utils/file.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import Taro from '@tarojs/taro'
2
2
  import type { FileInfo, MultiUploadFileResponse } from '../rpc/base/v1/file'
3
+ import { getLocaleRequestHeaders, t } from '../locales'
3
4
  import { formatSrc } from './index'
4
5
  import { getRequestAccessToken, requestBaseURL } from './http'
5
6
 
@@ -11,9 +12,13 @@ export async function uploadFile(fileType: string, filePath: string): Promise<Fi
11
12
  name: 'file',
12
13
  filePath,
13
14
  formData: { fileType },
14
- header: token ? { Authorization: token, 'source-client': 'miniapp' } : {},
15
+ header: {
16
+ ...getLocaleRequestHeaders(),
17
+ 'source-client': 'miniapp',
18
+ ...(token ? { Authorization: token } : {}),
19
+ },
15
20
  })
16
- if (response.statusCode !== 200) throw new Error('上传失败')
21
+ if (response.statusCode !== 200) throw new Error(t('core.file.uploadFailed'))
17
22
  return JSON.parse(response.data) as FileInfo
18
23
  }
19
24
 
package/src/utils/http.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  shouldRefreshToken,
11
11
  } from './auth'
12
12
  import { saveCurrentRoute } from './navigation'
13
+ import { getLocaleRequestHeaders, t } from '../locales'
13
14
 
14
15
  const apiBasePath = process.env.KRATOS_TARO_API_BASE || '/api'
15
16
  const apiTargetUrl = process.env.KRATOS_TARO_API_URL || ''
@@ -22,12 +23,14 @@ const SESSION_URL = '/v1/base/session'
22
23
  const REFRESH_TOKEN_URL = '/v1/base/token'
23
24
  const CAPTCHA_URL = '/v1/base/captcha'
24
25
  const CONFIG_URL = '/v1/base/config'
26
+ const LANGUAGE_URL = '/v1/base/language'
25
27
  const PASSWORD_PUBLIC_KEY_URL = '/v1/base/password-public-key'
26
28
  const NO_AUTH_URL_SET = new Set([
27
29
  SESSION_URL,
28
30
  REFRESH_TOKEN_URL,
29
31
  CAPTCHA_URL,
30
32
  CONFIG_URL,
33
+ LANGUAGE_URL,
31
34
  PASSWORD_PUBLIC_KEY_URL,
32
35
  '/auth',
33
36
  '/login/captcha',
@@ -37,6 +40,7 @@ const AUTH_EXPIRED_EXCLUDED_URL_SET = new Set([
37
40
  SESSION_URL,
38
41
  CAPTCHA_URL,
39
42
  CONFIG_URL,
43
+ LANGUAGE_URL,
40
44
  PASSWORD_PUBLIC_KEY_URL,
41
45
  '/auth',
42
46
  '/login/captcha',
@@ -65,7 +69,9 @@ function isAuthErrorResponse(data: unknown): boolean {
65
69
  const response = data as ErrorData
66
70
  const code = response.code === undefined ? '' : String(response.code)
67
71
  const reason = response.reason === undefined ? '' : String(response.reason)
68
- return authErrorCodeSet.has(code) || authErrorCodeSet.has(reason) || authErrorReasonSet.has(reason)
72
+ return (
73
+ authErrorCodeSet.has(code) || authErrorCodeSet.has(reason) || authErrorReasonSet.has(reason)
74
+ )
69
75
  }
70
76
 
71
77
  function resolveAuthMode(options: HttpRequestOptions, url: string): AuthMode {
@@ -75,7 +81,9 @@ function resolveAuthMode(options: HttpRequestOptions, url: string): AuthMode {
75
81
  }
76
82
 
77
83
  function resolveRequestUrl(url: string): string {
78
- return /^https?:\/\//.test(url) ? url : `${requestBaseURL}${url.startsWith('/') ? url : `/${url}`}`
84
+ return /^https?:\/\//.test(url)
85
+ ? url
86
+ : `${requestBaseURL}${url.startsWith('/') ? url : `/${url}`}`
79
87
  }
80
88
 
81
89
  /** 发送经过认证、刷新令牌和统一错误处理的 Taro 请求。 */
@@ -110,11 +118,16 @@ async function sendRequest<T>(
110
118
  header: {
111
119
  ...options.header,
112
120
  'source-client': 'miniapp',
121
+ ...getLocaleRequestHeaders(),
113
122
  ...(accessToken ? { Authorization: accessToken } : {}),
114
123
  },
115
124
  })
116
125
  const responseData = response.data as ErrorData | null | undefined
117
- if (response.statusCode >= 200 && response.statusCode < 300 && !isAuthErrorResponse(responseData)) {
126
+ if (
127
+ response.statusCode >= 200 &&
128
+ response.statusCode < 300 &&
129
+ !isAuthErrorResponse(responseData)
130
+ ) {
118
131
  return response.data as T
119
132
  }
120
133
  if (
@@ -129,7 +142,10 @@ async function sendRequest<T>(
129
142
  handleAuthExpiredByMode(authMode, requestUrl, responseData)
130
143
  throw response
131
144
  }
132
- await Taro.showToast({ icon: 'none', title: responseData?.message || '请求错误' })
145
+ await Taro.showToast({
146
+ icon: 'none',
147
+ title: responseData?.message || t('common.message.requestError'),
148
+ })
133
149
  throw response
134
150
  } catch (error) {
135
151
  if (
@@ -139,7 +155,7 @@ async function sendRequest<T>(
139
155
  ) {
140
156
  throw error
141
157
  }
142
- await Taro.showToast({ icon: 'none', title: '网络错误,换个网络试试' })
158
+ await Taro.showToast({ icon: 'none', title: t('common.message.networkError') })
143
159
  throw error
144
160
  }
145
161
  }
@@ -195,7 +211,7 @@ async function refreshAccessToken(): Promise<void> {
195
211
  url: resolveRequestUrl(REFRESH_TOKEN_URL),
196
212
  method: 'POST',
197
213
  data: { refresh_token: refreshToken },
198
- header: { 'source-client': 'miniapp' },
214
+ header: { 'source-client': 'miniapp', ...getLocaleRequestHeaders() },
199
215
  })
200
216
  const data = response.data as ErrorData & {
201
217
  token_type?: string
@@ -224,10 +240,10 @@ async function promptRelogin(): Promise<void> {
224
240
  isPromptingRelogin = true
225
241
  try {
226
242
  const modal = await Taro.showModal({
227
- title: '提示',
228
- content: '当前页面已失效,请重新登录',
243
+ title: t('common.title.notice'),
244
+ content: t('core.auth.sessionExpired'),
229
245
  showCancel: false,
230
- confirmText: '重新登录',
246
+ confirmText: t('core.auth.loginAgain'),
231
247
  })
232
248
  if (!modal.confirm) return
233
249
  await new Promise((resolve) => setTimeout(resolve, 80))
@@ -251,7 +267,10 @@ function handleAuthExpiredByMode(
251
267
  }
252
268
  silentClearAuthData()
253
269
  if (authMode !== 'optional') {
254
- void Taro.showToast({ icon: 'none', title: responseData?.message || '请求错误' })
270
+ void Taro.showToast({
271
+ icon: 'none',
272
+ title: responseData?.message || t('common.message.requestError'),
273
+ })
255
274
  }
256
275
  }
257
276
 
@@ -1,6 +1,7 @@
1
1
  import Taro from '@tarojs/taro'
2
2
  import * as miniCrypto from 'asmcrypto.js'
3
3
  import { defLoginService } from '../api/base/login'
4
+ import { t } from '../locales'
4
5
  import { PasswordCryptoScene } from '../rpc/base/v1/enum'
5
6
  import type { PasswordCrypto } from '../rpc/common/v1/types'
6
7
 
@@ -17,7 +18,7 @@ function pemToArrayBuffer(pem: string) {
17
18
  .replace(/\s/g, '')
18
19
  const decode = globalThis.atob
19
20
  if (!decode) {
20
- throw new Error('当前环境不支持 Base64 解码')
21
+ throw new Error(t('core.crypto.base64DecodeUnsupported'))
21
22
  }
22
23
  const binary = decode(base64)
23
24
  const buffer = new Uint8Array(binary.length)
@@ -36,7 +37,7 @@ function arrayBufferToBase64(buffer: ArrayBuffer) {
36
37
  }
37
38
  const encode = globalThis.btoa
38
39
  if (!encode) {
39
- throw new Error('当前环境不支持 Base64 编码')
40
+ throw new Error(t('core.crypto.base64EncodeUnsupported'))
40
41
  }
41
42
  return encode(binary)
42
43
  }
@@ -59,7 +60,7 @@ type DerNode = {
59
60
  /** 读取 DER 编码节点,用于解析 RSA SubjectPublicKeyInfo。 */
60
61
  function readDerNode(data: Uint8Array, offset: number): DerNode {
61
62
  if (offset + 2 > data.length) {
62
- throw new Error('公钥格式无效')
63
+ throw new Error(t('core.crypto.publicKeyInvalid'))
63
64
  }
64
65
  const tag = data[offset]
65
66
  const firstLengthByte = data[offset + 1]
@@ -68,7 +69,7 @@ function readDerNode(data: Uint8Array, offset: number): DerNode {
68
69
  if (firstLengthByte & 0x80) {
69
70
  const lengthByteCount = firstLengthByte & 0x7f
70
71
  if (lengthByteCount === 0 || valueOffset + lengthByteCount > data.length) {
71
- throw new Error('公钥格式无效')
72
+ throw new Error(t('core.crypto.publicKeyInvalid'))
72
73
  }
73
74
  length = 0
74
75
  for (let index = 0; index < lengthByteCount; index += 1) {
@@ -78,7 +79,7 @@ function readDerNode(data: Uint8Array, offset: number): DerNode {
78
79
  }
79
80
  const end = valueOffset + length
80
81
  if (end > data.length) {
81
- throw new Error('公钥格式无效')
82
+ throw new Error(t('core.crypto.publicKeyInvalid'))
82
83
  }
83
84
  return {
84
85
  tag,
@@ -107,13 +108,13 @@ function parseMiniRsaPublicKey(publicKey: string): [Uint8Array, Uint8Array] {
107
108
  const algorithm = readDerNode(subjectPublicKeyInfo.value, 0)
108
109
  const bitString = readDerNode(subjectPublicKeyInfo.value, algorithm.next)
109
110
  if (bitString.tag !== 0x03 || bitString.value[0] !== 0) {
110
- throw new Error('公钥格式无效')
111
+ throw new Error(t('core.crypto.publicKeyInvalid'))
111
112
  }
112
113
  const rsaPublicKey = readDerNode(bitString.value, 1)
113
114
  const modulus = readDerNode(rsaPublicKey.value, 0)
114
115
  const exponent = readDerNode(rsaPublicKey.value, modulus.next)
115
116
  if (modulus.tag !== 0x02 || exponent.tag !== 0x02) {
116
- throw new Error('公钥格式无效')
117
+ throw new Error(t('core.crypto.publicKeyInvalid'))
117
118
  }
118
119
  return [trimDerInteger(modulus.value), trimDerInteger(exponent.value)]
119
120
  }
@@ -131,7 +132,7 @@ function generateMgf1Mask(crypto: MiniCrypto, seed: Uint8Array, length: number)
131
132
  counter[3] = index
132
133
  let block = hash.reset().process(seed).process(counter).finish().result
133
134
  if (!block) {
134
- throw new Error('密码加密失败')
135
+ throw new Error(t('core.crypto.encryptFailed'))
135
136
  }
136
137
  const target = mask.subarray(index * hash.HASH_SIZE)
137
138
  if (block.length > target.length) {
@@ -171,14 +172,14 @@ async function encryptPortablePassword(
171
172
  const dataBlockLength = keySize - hash.HASH_SIZE - 1
172
173
  const paddingLength = dataBlockLength - aesKey.length - hash.HASH_SIZE - 1
173
174
  if (paddingLength < 0) {
174
- throw new Error('密码长度超出限制')
175
+ throw new Error(t('core.crypto.passwordTooLong'))
175
176
  }
176
177
  const encodedMessage = new Uint8Array(keySize)
177
178
  const maskedSeed = encodedMessage.subarray(1, hash.HASH_SIZE + 1)
178
179
  const dataBlock = encodedMessage.subarray(hash.HASH_SIZE + 1)
179
180
  const labelHash = hash.reset().process(new Uint8Array()).finish().result
180
181
  if (!labelHash) {
181
- throw new Error('密码加密失败')
182
+ throw new Error(t('core.crypto.encryptFailed'))
182
183
  }
183
184
  dataBlock.set(labelHash, 0)
184
185
  dataBlock.set(aesKey, hash.HASH_SIZE + paddingLength + 1)
@@ -196,7 +197,7 @@ async function encryptPortablePassword(
196
197
  new crypto.BigNumber(encodedMessage),
197
198
  ).result
198
199
  if (!encryptedKey) {
199
- throw new Error('密码加密失败')
200
+ throw new Error(t('core.crypto.encryptFailed'))
200
201
  }
201
202
  const ciphertext = crypto.AES_GCM.encrypt(plaintext, aesKey, iv)
202
203
  return {
@@ -216,7 +217,7 @@ export async function encryptPassword(
216
217
  ): Promise<PasswordCrypto> {
217
218
  const plainPassword = password.trim()
218
219
  if (!plainPassword) {
219
- throw new Error('密码不能为空')
220
+ throw new Error(t('core.crypto.passwordRequired'))
220
221
  }
221
222
 
222
223
  const publicKeyResponse = await defLoginService.PasswordPublicKey({ scene })