@liujitcn/kratos-taro-app-core 0.0.29 → 0.0.32

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 (44) hide show
  1. package/README.md +6 -4
  2. package/dist/runner.mjs +14 -7
  3. package/package.json +2 -1
  4. package/src/api/base/{config.ts → v1/config.ts} +2 -2
  5. package/src/api/base/{file.ts → v1/file.ts} +4 -4
  6. package/src/api/base/{language.ts → v1/language.ts} +2 -2
  7. package/src/api/base/{login.ts → v1/login.ts} +3 -3
  8. package/src/api/base/v1/mfa.ts +127 -0
  9. package/src/api/base/{oauth.ts → v1/oauth.ts} +3 -3
  10. package/src/api/system/{auth.ts → app/v1/auth.ts} +8 -15
  11. package/src/api/system/{base_area.ts → app/v1/base_area.ts} +2 -2
  12. package/src/api/system/app/v1/base_dict.ts +17 -0
  13. package/src/api/{base/menu.ts → system/app/v1/base_menu.ts} +2 -2
  14. package/src/bootstrap.ts +27 -3
  15. package/src/components/KratosTabBar.scss +64 -0
  16. package/src/components/KratosTabBar.tsx +8 -1
  17. package/src/components/MfaRecoveryCodesDialog.scss +94 -0
  18. package/src/components/MfaRecoveryCodesDialog.tsx +61 -0
  19. package/src/components/MfaSetupPanel.scss +70 -0
  20. package/src/components/MfaSetupPanel.tsx +51 -0
  21. package/src/components/PasswordVerifyDialog.scss +146 -0
  22. package/src/components/PasswordVerifyDialog.tsx +146 -0
  23. package/src/index.ts +2 -0
  24. package/src/locales/README.md +2 -1
  25. package/src/locales/en-US.json +25 -4
  26. package/src/locales/index.ts +10 -1
  27. package/src/locales/ja-JP.json +25 -4
  28. package/src/locales/zh-CN.json +25 -4
  29. package/src/locales/zh-TW.json +25 -4
  30. package/src/navigation.ts +103 -63
  31. package/src/rpc/base/v1/login.ts +37 -4
  32. package/src/rpc/base/v1/mfa.ts +180 -0
  33. package/src/rpc/base/v1/oauth.ts +25 -0
  34. package/src/rpc/system/app/v1/auth.ts +7 -0
  35. package/src/rpc/system/common/v1/common.ts +35 -0
  36. package/src/runner.ts +23 -7
  37. package/src/stores/setting.ts +1 -1
  38. package/src/stores/user.ts +20 -8
  39. package/src/utils/http.ts +3 -7
  40. package/src/utils/passwordCrypto.ts +1 -1
  41. package/src/utils/webauthn.ts +94 -0
  42. package/src/views/pages/login/login.scss +29 -0
  43. package/src/views/pages/login/login.tsx +336 -53
  44. package/src/api/system/base_dict.ts +0 -24
@@ -0,0 +1,61 @@
1
+ import { Button, Text, View } from '@tarojs/components'
2
+ import Taro from '@tarojs/taro'
3
+ import { useI18n } from '../locales'
4
+ import './MfaRecoveryCodesDialog.scss'
5
+
6
+ /** MFA 恢复码弹窗属性。 */
7
+ export interface MfaRecoveryCodesDialogProps {
8
+ /** 弹窗显示状态。 */
9
+ open: boolean
10
+ /** 本次生成的一次性恢复码。 */
11
+ codes: string[]
12
+ /** 用户确认已保存恢复码。 */
13
+ onConfirm: () => void | Promise<void>
14
+ }
15
+
16
+ /** 统一展示、复制并确认保存 MFA 恢复码。 */
17
+ export default function MfaRecoveryCodesDialog({
18
+ open,
19
+ codes,
20
+ onConfirm,
21
+ }: MfaRecoveryCodesDialogProps) {
22
+ const { t } = useI18n()
23
+ if (!open) return null
24
+
25
+ const codesText = codes.join('\n')
26
+
27
+ /** 复制当前显示的恢复码。 */
28
+ const copyRecoveryCodes = async () => {
29
+ if (!codesText) return
30
+ await Taro.setClipboardData({ data: codesText })
31
+ await Taro.showToast({ icon: 'none', title: t('core.login.mfa_recovery_codes_copied') })
32
+ }
33
+
34
+ return (
35
+ <View className='mfa-recovery-dialog'>
36
+ <View className='mfa-recovery-dialog__panel' onClick={(event) => event.stopPropagation()}>
37
+ <View className='mfa-recovery-dialog__header'>
38
+ <Text className='mfa-recovery-dialog__title'>
39
+ {t('core.login.mfa_recovery_codes_title')}
40
+ </Text>
41
+ </View>
42
+ <View className='mfa-recovery-dialog__body'>
43
+ <View className='mfa-recovery-dialog__codes'>
44
+ <Text>{codesText}</Text>
45
+ </View>
46
+ <Button className='mfa-recovery-dialog__copy' onClick={() => void copyRecoveryCodes()}>
47
+ {t('core.login.mfa_copy_recovery_codes')}
48
+ </Button>
49
+ <Text className='mfa-recovery-dialog__warning'>
50
+ {t('core.login.mfa_recovery_codes_warning')}
51
+ </Text>
52
+ </View>
53
+ <View className='mfa-recovery-dialog__footer'>
54
+ <Button className='mfa-recovery-dialog__confirm' onClick={() => void onConfirm()}>
55
+ {t('core.login.mfa_recovery_codes_confirm')}
56
+ </Button>
57
+ </View>
58
+ </View>
59
+ </View>
60
+ )
61
+ }
@@ -0,0 +1,70 @@
1
+ .mfa-setup-panel__qr {
2
+ display: block;
3
+ width: min(360px, 100%);
4
+ aspect-ratio: 1;
5
+ margin: 0 auto 24px;
6
+ }
7
+
8
+ .mfa-setup-panel__uri {
9
+ display: flex;
10
+ gap: 16px;
11
+ align-items: flex-start;
12
+ margin-bottom: 20px;
13
+ }
14
+
15
+ .mfa-setup-panel__uri-text {
16
+ flex: 1;
17
+ min-width: 0;
18
+ color: #666;
19
+ font-size: 24px;
20
+ line-height: 1.5;
21
+ overflow-wrap: anywhere;
22
+ }
23
+
24
+ .mfa-setup-panel__copy {
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: center;
28
+ width: 58px;
29
+ min-width: 58px;
30
+ height: 58px;
31
+ margin: 0;
32
+ padding: 0;
33
+ color: #27ba9b;
34
+ background: #f0fbf8;
35
+ border: 0;
36
+ border-radius: 8px;
37
+ }
38
+
39
+ .mfa-setup-panel__copy::after {
40
+ border: 0;
41
+ }
42
+
43
+ .mfa-setup-panel__copy-icon {
44
+ position: relative;
45
+ display: block;
46
+ width: 18px;
47
+ height: 18px;
48
+ color: currentColor;
49
+ }
50
+
51
+ .mfa-setup-panel__copy-back,
52
+ .mfa-setup-panel__copy-front {
53
+ position: absolute;
54
+ width: 12px;
55
+ height: 14px;
56
+ border: 1.5px solid currentColor;
57
+ border-radius: 3px;
58
+ box-sizing: border-box;
59
+ }
60
+
61
+ .mfa-setup-panel__copy-back {
62
+ top: 0;
63
+ left: 0;
64
+ }
65
+
66
+ .mfa-setup-panel__copy-front {
67
+ right: 0;
68
+ bottom: 0;
69
+ background: #f0fbf8;
70
+ }
@@ -0,0 +1,51 @@
1
+ import { Button, Image, Text, View } from '@tarojs/components'
2
+ import Taro from '@tarojs/taro'
3
+ import generateQrCode from 'qrcode-generator'
4
+ import { useMemo } from 'react'
5
+ import { useI18n } from '../locales'
6
+ import './MfaSetupPanel.scss'
7
+
8
+ /** MFA TOTP 绑定面板属性。 */
9
+ export interface MfaSetupPanelProps {
10
+ /** TOTP 绑定地址。 */
11
+ uri: string
12
+ }
13
+
14
+ /** 统一展示 TOTP 二维码、绑定地址和复制操作。 */
15
+ export default function MfaSetupPanel({ uri }: MfaSetupPanelProps) {
16
+ const { t } = useI18n()
17
+ const qrDataUrl = useMemo(() => {
18
+ if (!uri) return ''
19
+ const code = generateQrCode(0, 'M')
20
+ code.addData(uri)
21
+ code.make()
22
+ return code.createDataURL(4, 8)
23
+ }, [uri])
24
+
25
+ if (!uri) return null
26
+
27
+ /** 复制 TOTP 绑定地址。 */
28
+ const copySetupUri = async () => {
29
+ await Taro.setClipboardData({ data: uri })
30
+ await Taro.showToast({ icon: 'none', title: t('core.login.mfa_copy_success') })
31
+ }
32
+
33
+ return (
34
+ <View className='mfa-setup-panel'>
35
+ {qrDataUrl ? <Image className='mfa-setup-panel__qr' src={qrDataUrl} mode='aspectFit' /> : null}
36
+ <View className='mfa-setup-panel__uri'>
37
+ <Text className='mfa-setup-panel__uri-text'>{uri}</Text>
38
+ <Button
39
+ className='mfa-setup-panel__copy'
40
+ aria-label={t('core.login.mfa_copy_uri')}
41
+ onClick={() => void copySetupUri()}
42
+ >
43
+ <View className='mfa-setup-panel__copy-icon' aria-hidden='true'>
44
+ <View className='mfa-setup-panel__copy-back' />
45
+ <View className='mfa-setup-panel__copy-front' />
46
+ </View>
47
+ </Button>
48
+ </View>
49
+ </View>
50
+ )
51
+ }
@@ -0,0 +1,146 @@
1
+ .password-verify-dialog {
2
+ position: fixed;
3
+ z-index: 1000;
4
+ top: 0;
5
+ right: 0;
6
+ bottom: 0;
7
+ left: 0;
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ padding: 32px;
12
+ background-color: rgba(0, 0, 0, 0.45);
13
+ }
14
+
15
+ .password-verify-dialog__panel {
16
+ box-sizing: border-box;
17
+ width: 100%;
18
+ max-width: 640px;
19
+ overflow: hidden;
20
+ border-radius: 16px;
21
+ background-color: #fff;
22
+ box-shadow: 0 20px 64px rgba(0, 0, 0, 0.2);
23
+ }
24
+
25
+ .password-verify-dialog__header {
26
+ display: flex;
27
+ align-items: center;
28
+ justify-content: space-between;
29
+ padding: 28px 32px;
30
+ border-bottom: 1px solid #eee;
31
+ }
32
+
33
+ .password-verify-dialog__title {
34
+ color: #222;
35
+ font-size: 34px;
36
+ font-weight: 600;
37
+ }
38
+
39
+ .password-verify-dialog__close {
40
+ padding: 0 8px;
41
+ color: #999;
42
+ font-size: 42px;
43
+ line-height: 1;
44
+ }
45
+
46
+ .password-verify-dialog__body {
47
+ padding: 32px;
48
+ }
49
+
50
+ .password-verify-dialog__description,
51
+ .password-verify-dialog__label,
52
+ .password-verify-dialog__error {
53
+ display: block;
54
+ }
55
+
56
+ .password-verify-dialog__description {
57
+ margin-bottom: 24px;
58
+ color: #666;
59
+ font-size: 26px;
60
+ line-height: 1.6;
61
+ }
62
+
63
+ .password-verify-dialog__label {
64
+ margin-bottom: 12px;
65
+ color: #333;
66
+ font-size: 28px;
67
+ }
68
+
69
+ .password-verify-dialog__input {
70
+ display: block;
71
+ box-sizing: border-box;
72
+ width: 100%;
73
+ height: 84px;
74
+ margin: 0;
75
+ padding: 0 24px;
76
+ color: #222 !important;
77
+ font-size: 30px;
78
+ line-height: 84px;
79
+ text-align: left;
80
+ background: #fff;
81
+ border: 1px solid #ddd;
82
+ border-radius: 8px;
83
+ outline: none;
84
+ }
85
+
86
+ .password-verify-dialog__error {
87
+ margin-top: 10px;
88
+ color: #e64340;
89
+ font-size: 24px;
90
+ }
91
+
92
+ .password-verify-dialog__footer {
93
+ display: flex;
94
+ justify-content: flex-end;
95
+ gap: 20px;
96
+ padding: 0 32px 32px;
97
+ }
98
+
99
+ .password-verify-dialog__button {
100
+ display: flex;
101
+ flex: 1 1 0;
102
+ align-items: center;
103
+ justify-content: center;
104
+ box-sizing: border-box;
105
+ width: 0;
106
+ min-width: 160px;
107
+ height: 72px;
108
+ margin: 0;
109
+ padding: 0 24px;
110
+ font-size: 28px;
111
+ line-height: 72px;
112
+ text-align: center;
113
+ border: 0;
114
+ border-radius: 8px;
115
+ }
116
+
117
+ .password-verify-dialog__button::after {
118
+ border: 0;
119
+ }
120
+
121
+ .password-verify-dialog__button--cancel {
122
+ color: #555 !important;
123
+ -webkit-text-fill-color: #555;
124
+ background-color: #f3f3f3;
125
+ }
126
+
127
+ .password-verify-dialog__button--confirm {
128
+ color: #fff;
129
+ background-color: #27ba9b;
130
+ }
131
+
132
+ .password-verify-dialog__button[disabled] {
133
+ opacity: 1;
134
+ }
135
+
136
+ .password-verify-dialog__button--cancel[disabled] {
137
+ color: #555 !important;
138
+ -webkit-text-fill-color: #555;
139
+ background-color: #f3f3f3;
140
+ }
141
+
142
+ .password-verify-dialog__button--confirm[disabled] {
143
+ color: #fff !important;
144
+ -webkit-text-fill-color: #fff;
145
+ background-color: #27ba9b;
146
+ }
@@ -0,0 +1,146 @@
1
+ import { Button, Input, Text, View } from '@tarojs/components'
2
+ import { useEffect, useState } from 'react'
3
+ import { useI18n } from '../locales'
4
+ import { PASSWORD_CRYPTO_SCENE, encryptPassword } from '../utils/passwordCrypto'
5
+ import type { PasswordCryptoScene } from '../utils/passwordCrypto'
6
+ import type { PasswordCrypto } from '../rpc/common/v1/types'
7
+ import './PasswordVerifyDialog.scss'
8
+
9
+ /** Taro 通用密码验证弹窗属性。 */
10
+ export interface PasswordVerifyDialogProps {
11
+ /** 弹窗显示状态。 */
12
+ open: boolean
13
+ /** 弹窗标题。 */
14
+ title?: string
15
+ /** 密码输入前的可选补充说明。 */
16
+ description?: string
17
+ /** 密码字段标题。 */
18
+ passwordLabel?: string
19
+ /** 密码输入提示。 */
20
+ passwordPlaceholder?: string
21
+ /** 确认按钮文案。 */
22
+ confirmText?: string
23
+ /** 取消按钮文案。 */
24
+ cancelText?: string
25
+ /** 外部业务提交状态。 */
26
+ confirmLoading?: boolean
27
+ /** 密码加密场景。 */
28
+ scene?: PasswordCryptoScene
29
+ /** 是否允许点击遮罩关闭弹窗。 */
30
+ closeOnClickModal?: boolean
31
+ /** 确认并返回加密密码。 */
32
+ onConfirm: (password: PasswordCrypto) => void | Promise<void>
33
+ /** 取消验证。 */
34
+ onCancel: () => void
35
+ }
36
+
37
+ /** 通用密码验证弹窗,负责收集并加密当前密码。 */
38
+ export default function PasswordVerifyDialog({
39
+ open,
40
+ title,
41
+ description,
42
+ passwordLabel,
43
+ passwordPlaceholder,
44
+ confirmText,
45
+ cancelText,
46
+ confirmLoading = false,
47
+ scene = PASSWORD_CRYPTO_SCENE.PASSWORD_CRYPTO_SCENE_MFA,
48
+ closeOnClickModal = false,
49
+ onConfirm,
50
+ onCancel,
51
+ }: PasswordVerifyDialogProps) {
52
+ const { t } = useI18n()
53
+ const [password, setPassword] = useState('')
54
+ const [errorMessage, setErrorMessage] = useState('')
55
+ const [encrypting, setEncrypting] = useState(false)
56
+ const submitting = confirmLoading || encrypting
57
+
58
+ useEffect(() => {
59
+ if (!open) {
60
+ setPassword('')
61
+ setErrorMessage('')
62
+ }
63
+ }, [open])
64
+
65
+ if (!open) return null
66
+
67
+ const handleConfirm = async () => {
68
+ if (submitting) return
69
+ if (!password.trim()) {
70
+ setErrorMessage(t('core.crypto.password_required'))
71
+ return
72
+ }
73
+ setErrorMessage('')
74
+ setEncrypting(true)
75
+ try {
76
+ const encryptedPassword = await encryptPassword(password, scene)
77
+ await onConfirm(encryptedPassword)
78
+ } catch (error) {
79
+ setErrorMessage(error instanceof Error ? error.message : t('common.message.request_error'))
80
+ } finally {
81
+ setEncrypting(false)
82
+ }
83
+ }
84
+
85
+ const handleCancel = () => {
86
+ if (submitting) return
87
+ onCancel()
88
+ }
89
+
90
+ const handleMaskClick = () => {
91
+ if (closeOnClickModal) handleCancel()
92
+ }
93
+
94
+ return (
95
+ <View className='password-verify-dialog' onClick={handleMaskClick}>
96
+ <View className='password-verify-dialog__panel' onClick={(event) => event.stopPropagation()}>
97
+ <View className='password-verify-dialog__header'>
98
+ <Text className='password-verify-dialog__title'>
99
+ {title || t('core.password.verify_title')}
100
+ </Text>
101
+ <Text className='password-verify-dialog__close' onClick={handleCancel}>
102
+ ×
103
+ </Text>
104
+ </View>
105
+ <View className='password-verify-dialog__body'>
106
+ {description ? (
107
+ <Text className='password-verify-dialog__description'>{description}</Text>
108
+ ) : null}
109
+ <Text className='password-verify-dialog__label'>
110
+ {passwordLabel || t('core.password.verify_label')}
111
+ </Text>
112
+ <Input
113
+ className='password-verify-dialog__input'
114
+ password
115
+ value={password}
116
+ placeholder={passwordPlaceholder || t('core.crypto.password_required')}
117
+ onInput={(event) => {
118
+ setPassword(event.detail.value)
119
+ setErrorMessage('')
120
+ }}
121
+ onConfirm={() => void handleConfirm()}
122
+ />
123
+ {errorMessage ? (
124
+ <Text className='password-verify-dialog__error'>{errorMessage}</Text>
125
+ ) : null}
126
+ </View>
127
+ <View className='password-verify-dialog__footer'>
128
+ <Button
129
+ className='password-verify-dialog__button password-verify-dialog__button--cancel'
130
+ disabled={submitting}
131
+ onClick={handleCancel}
132
+ >
133
+ {cancelText || t('common.action.cancel')}
134
+ </Button>
135
+ <Button
136
+ className='password-verify-dialog__button password-verify-dialog__button--confirm'
137
+ loading={submitting}
138
+ onClick={() => void handleConfirm()}
139
+ >
140
+ {confirmText || t('common.action.confirm')}
141
+ </Button>
142
+ </View>
143
+ </View>
144
+ </View>
145
+ )
146
+ }
package/src/index.ts CHANGED
@@ -38,3 +38,5 @@ export * from './utils/file'
38
38
  export * from './utils/index'
39
39
  export * from './utils/navigation'
40
40
  export * from './utils/passwordCrypto'
41
+ export { default as PasswordVerifyDialog } from './components/PasswordVerifyDialog'
42
+ export type { PasswordVerifyDialogProps } from './components/PasswordVerifyDialog'
@@ -1,11 +1,12 @@
1
1
  # Taro Core 语言包
2
2
 
3
- 本目录提供 Taro 应用底座和公共页面使用的稳定文案,包括登录、启动状态、导航、认证、文件处理和网络错误提示。语言包由 `../index.ts` 的 `coreModule` 导入并注册,应用启动时由 `bootstrapKratosTaroApp` 调用 `registerLocaleMessages` 合并;页面、hooks、请求工具通过 `useI18n()` 或 `t()` 使用。
3
+ 本目录提供 Taro 应用底座和公共页面使用的稳定文案,包括登录、启动状态、导航、认证、MFA/WebAuthn、文件处理和网络错误提示。语言包由 `../index.ts` 的 `coreModule` 导入并注册,应用启动时由 `bootstrapKratosTaroApp` 调用 `registerLocaleMessages` 合并;页面、hooks、请求工具通过 `useI18n()` 或 `t()` 使用。
4
4
 
5
5
  | 文件 | 用途 | 使用位置 |
6
6
  | --- | --- | --- |
7
7
  | `zh-CN.json` | 简体中文固定 UI 默认语言包,也是所有语言键集合和占位符校验基准。 | Taro 固定文案默认语言及缺失翻译回退。 |
8
8
  | `zh-TW.json` | 繁体中文 Core 语言包。 | Taro 切换为繁体中文时使用。 |
9
9
  | `en-US.json` | 英文 Core 语言包。 | Taro 切换为英文时使用。 |
10
+ | `ja-JP.json` | 日文 Core 语言包。 | Taro 切换为日文时使用。 |
10
11
 
11
12
  语言包是标准 JSON,不能在文件内直接写注释;公共键使用 `common.` 或 `core.` 命名空间。新增语言后执行仓库根目录的 `make i18n-sync`。语言切换选项固定显示后端 `base_language.native_name`,接口不可用时使用代码内置的原生名称;`common.language.*` 仅用于普通文案和语言迁移初始数据。新增语言的完整文件清单见[国际化语言扩展指南](../../../../../../docs/国际化语言扩展指南.md)。
@@ -27,6 +27,8 @@
27
27
  "core.crypto.password_required": "Password is required",
28
28
  "core.crypto.password_too_long": "Password exceeds the supported length",
29
29
  "core.crypto.public_key_invalid": "Invalid public key",
30
+ "core.password.verify_title": "Verify password",
31
+ "core.password.verify_label": "Current password",
30
32
  "core.file.download_failed": "Download failed",
31
33
  "core.file.temporary_path_required": "Use a temporary file path when uploading from a Mini Program",
32
34
  "core.file.upload_failed": "Upload failed",
@@ -60,6 +62,23 @@
60
62
  "core.login.default_sub_title": "Welcome",
61
63
  "core.login.login_failed": "Login failed. Try again.",
62
64
  "core.login.login_success": "Logged in",
65
+ "core.login.mfa_title": "Multi-factor authentication",
66
+ "core.login.mfa_code": "Enter the authenticator code",
67
+ "core.login.mfa_recovery_code": "Recovery code (optional)",
68
+ "core.login.mfa_setup_title": "Bind multi-factor authentication",
69
+ "core.login.mfa_copy_uri": "Copy binding address",
70
+ "core.login.mfa_copy_success": "Binding address copied",
71
+ "core.login.mfa_recovery_codes_copied": "Recovery codes copied",
72
+ "core.login.mfa_recovery_codes_title": "Save MFA recovery codes",
73
+ "core.login.mfa_recovery_codes_warning": "Each code works once and will not be shown again after this dialog closes.",
74
+ "core.login.mfa_copy_recovery_codes": "Copy recovery codes",
75
+ "core.login.mfa_recovery_codes_confirm": "I saved them",
76
+ "core.login.mfa_setup_success": "Multi-factor authentication enabled. Please sign in again.",
77
+ "core.login.mfa_webauthn_action": "Use passkey",
78
+ "core.login.mfa_webauthn_setup_unsupported": "WebAuthn is not supported on this platform. Use TOTP or a recovery code.",
79
+ "core.login.mfa_webauthn_setup_invalid": "WebAuthn registration did not return a valid credential.",
80
+ "core.login.mfa_webauthn_login_unsupported": "WebAuthn is not supported on this platform. Use a recovery code.",
81
+ "core.login.mfa_webauthn_login_invalid": "WebAuthn authentication did not return a valid credential.",
63
82
  "core.login.password": "Enter password",
64
83
  "core.login.privacy": "Privacy Policy",
65
84
  "core.login.protocol_missing": "Terms of Service and Privacy Policy are not configured",
@@ -73,16 +92,14 @@
73
92
  "core.login.wechat_bind_tip": "This WeChat account is not linked. Log in to an existing account to link it.",
74
93
  "core.login.wechat_failed": "WeChat login failed. Try again later.",
75
94
  "core.navigation.home": "Home",
95
+ "core.navigation.message": "Messages",
76
96
  "core.navigation.error.access": "Invalid menu access mode: {access}",
77
97
  "core.navigation.error.child_tab": "Only second-level app menus can be tabs: {name}",
78
98
  "core.navigation.error.cycle": "Menu parent cycle detected: {name}",
79
99
  "core.navigation.error.identity": "Menu id, name, or app/ path is invalid",
80
- "core.navigation.error.item_object": "Menu item is not an object",
81
- "core.navigation.error.list_missing": "Menu response does not contain a flat list",
82
100
  "core.navigation.error.name_prefix": "Menu name must start with App: {name}",
83
101
  "core.navigation.error.parent_missing": "Menu parent id is missing: {name}",
84
102
  "core.navigation.error.parent_not_found": "Menu parent was not found: {name}",
85
- "core.navigation.error.response_object": "Menu response is not an object",
86
103
  "core.navigation.error.root_tab": "Second-level app menu must be a tab: {name}",
87
104
  "core.navigation.error.tab_count": "tabBar must contain 0 or 2-5 items",
88
105
  "core.navigation.error.unique": "Menu id, name, and path must be unique",
@@ -101,5 +118,9 @@
101
118
  "core.status.loading": "Loading",
102
119
  "core.webview.embed_blocked": "The destination may block embedding on H5",
103
120
  "core.webview.invalid_url": "No valid link was provided",
104
- "core.webview.open_failed": "Unable to open link"
121
+ "core.webview.open_failed": "Unable to open link",
122
+ "common.value.top_level": "Top level",
123
+ "common.resource.default": "{resource}",
124
+ "common.action.delete_resource": "Delete {resource}",
125
+ "common.action.set_field_status": "Set {field}"
105
126
  }
@@ -119,7 +119,7 @@ export function registerLocaleMessages(modules: KratosTaroModule[]): void {
119
119
  }
120
120
  const target = localeMessages.get(locale) as LocaleMessages
121
121
  Object.keys(messages).forEach((key) => {
122
- if (!key.startsWith('common.') && !key.startsWith('core.') && !key.startsWith('system.')) {
122
+ if (!isAllowedLocaleKey(module.name, key)) {
123
123
  throw new Error(`${module.name} 的语言键命名空间无效: ${key}`)
124
124
  }
125
125
  if (Object.prototype.hasOwnProperty.call(target, key)) {
@@ -132,6 +132,15 @@ export function registerLocaleMessages(modules: KratosTaroModule[]): void {
132
132
  })
133
133
  }
134
134
 
135
+ /** 校验公共键和当前业务模块专属的语言键命名空间。 */
136
+ function isAllowedLocaleKey(moduleName: string, key: string): boolean {
137
+ const packageName = moduleName.split('/').at(-1) ?? moduleName
138
+ const moduleNamespace = packageName.split('-').at(-1) ?? packageName
139
+ return ['common', 'core', 'system', moduleNamespace].some((namespace) =>
140
+ key.startsWith(`${namespace}.`),
141
+ )
142
+ }
143
+
135
144
  /** 获取当前语言区域。 */
136
145
  export function getCurrentLocale(): SupportedLocale {
137
146
  return useLocaleStore.getState().locale
@@ -27,6 +27,8 @@
27
27
  "core.crypto.password_required": "パスワードを空にすることはできません",
28
28
  "core.crypto.password_too_long": "パスワードの長さが制限を超えています",
29
29
  "core.crypto.public_key_invalid": "公開鍵の形式が無効です",
30
+ "core.password.verify_title": "パスワードの確認",
31
+ "core.password.verify_label": "現在のパスワード",
30
32
  "core.file.download_failed": "ダウンロードに失敗しました",
31
33
  "core.file.temporary_path_required": "ミニ プログラムをアップロードするには、一時ファイル パスを使用してください。",
32
34
  "core.file.upload_failed": "アップロードに失敗しました",
@@ -60,6 +62,23 @@
60
62
  "core.login.default_sub_title": "このアプリケーションへようこそ",
61
63
  "core.login.login_failed": "ログインに失敗しました。もう一度お試しください。",
62
64
  "core.login.login_success": "ログインに成功しました",
65
+ "core.login.mfa_title": "多要素認証",
66
+ "core.login.mfa_code": "認証コードを入力",
67
+ "core.login.mfa_recovery_code": "リカバリーコード(任意)",
68
+ "core.login.mfa_setup_title": "多要素認証を設定",
69
+ "core.login.mfa_copy_uri": "登録アドレスをコピー",
70
+ "core.login.mfa_copy_success": "登録アドレスをコピーしました",
71
+ "core.login.mfa_recovery_codes_copied": "リカバリーコードをコピーしました",
72
+ "core.login.mfa_recovery_codes_title": "MFA リカバリーコードを保存",
73
+ "core.login.mfa_recovery_codes_warning": "各コードは一度だけ使用でき、ダイアログを閉じると再表示されません。",
74
+ "core.login.mfa_copy_recovery_codes": "リカバリーコードをコピー",
75
+ "core.login.mfa_recovery_codes_confirm": "保存しました",
76
+ "core.login.mfa_setup_success": "多要素認証を設定しました。再度ログインしてください。",
77
+ "core.login.mfa_webauthn_action": "パスキーを使用",
78
+ "core.login.mfa_webauthn_setup_unsupported": "このプラットフォームは WebAuthn に対応していません。TOTP またはリカバリーコードを使用してください。",
79
+ "core.login.mfa_webauthn_setup_invalid": "WebAuthn 登録で有効なクレデンシャルが返されませんでした。",
80
+ "core.login.mfa_webauthn_login_unsupported": "このプラットフォームは WebAuthn に対応していません。リカバリーコードを使用してください。",
81
+ "core.login.mfa_webauthn_login_invalid": "WebAuthn 認証で有効なクレデンシャルが返されませんでした。",
63
82
  "core.login.password": "パスワードを入力してください",
64
83
  "core.login.privacy": "「プライバシー規約」",
65
84
  "core.login.protocol_missing": "利用規約とプライバシー規約が設定されていません",
@@ -73,16 +92,14 @@
73
92
  "core.login.wechat_bind_tip": "この WeChat アカウントはまだバインドされていません。バインドを完了するには、既存のアカウントにログインしてください。",
74
93
  "core.login.wechat_failed": "WeChat ログインに失敗しました。後でもう一度試してください。",
75
94
  "core.navigation.home": "ホーム",
95
+ "core.navigation.message": "メッセージ",
76
96
  "core.navigation.error.access": "無効なメニュー アクセス モード: {access}",
77
97
  "core.navigation.error.child_tab": "モバイル セカンダリ メニューのみがタブとして使用できます: {name}",
78
98
  "core.navigation.error.cycle": "モバイル メニューにはループの親があります: {name}",
79
99
  "core.navigation.error.identity": "メニュー ID、名前、またはアプリ/パスが無効です",
80
- "core.navigation.error.item_object": "メニュー項目はオブジェクトではありません",
81
- "core.navigation.error.list_missing": "メニュー応答にフラット リストがありません",
82
100
  "core.navigation.error.name_prefix": "メニュー名にはアプリの接頭辞を使用する必要があります: {name}",
83
101
  "core.navigation.error.parent_missing": "メニューに親番号がありません: {name}",
84
102
  "core.navigation.error.parent_not_found": "モバイル メニューの親が存在しません: {name}",
85
- "core.navigation.error.response_object": "メニュー応答はオブジェクトではありません",
86
103
  "core.navigation.error.root_tab": "モバイル セカンダリ メニューはタブとして使用する必要があります: {name}",
87
104
  "core.navigation.error.tab_count": "tabBar は 0 または 2 ~ 5 個の項目のみを設定できます",
88
105
  "core.navigation.error.unique": "メニュー ID、名前、パスは一意である必要があります",
@@ -101,5 +118,9 @@
101
118
  "core.status.loading": "をロードしています",
102
119
  "core.webview.embed_blocked": "現在の H5 ページは、ターゲット サイトによって埋め込みが制限されている可能性があります",
103
120
  "core.webview.invalid_url": "有効なリンク アドレスがありません",
104
- "core.webview.open_failed": "リンクを開けません"
121
+ "core.webview.open_failed": "リンクを開けません",
122
+ "common.value.top_level": "トップレベル",
123
+ "common.resource.default": "{resource}",
124
+ "common.action.delete_resource": "{resource}を削除",
125
+ "common.action.set_field_status": "{field}を設定"
105
126
  }