feat(payment): i18n payment error codes and label localization
Pairs with the backend structured payment errors (reason + metadata). The
frontend now maps reason codes to localized messages with metadata as
interpolation variables, and automatically localizes raw config-field names
(e.g. "certSerial" → "证书序列号") using the existing UI-label i18n
namespace.
- frontend/src/utils/apiError.ts
- extractApiErrorCode now prefers the string `reason` over the numeric HTTP
`code`; reason is granular enough to drive i18n lookup, HTTP code is not.
- New extractApiErrorMetadata to pull interpolation params off the error.
- New extractI18nErrorMessage(err, t, namespace, fallback): looks up
`<namespace>.<REASON>` in i18n and substitutes metadata. Before
substitution, `metadata.key` and `metadata.keys` (slash-joined) are
re-translated through `admin.settings.payment.field_<key>` so users see
"缺少必填项:证书序列号" instead of "缺少必填项:certSerial".
- frontend/src/i18n/locales/{zh,en}.ts
- Add payment.errors entries for every structured reason code returned by
the backend (PAYMENT_DISABLED, INVALID_AMOUNT, TOO_MANY_PENDING,
DAILY_LIMIT_EXCEEDED, NO_AVAILABLE_INSTANCE, PAYMENT_PROVIDER_MISCONFIGURED,
WXPAY_CONFIG_MISSING_KEY / INVALID_KEY_LENGTH / INVALID_KEY, NOT_FOUND,
FORBIDDEN, CONFLICT, INVALID_ORDER_TYPE, INVALID_STATUS,
BALANCE_NOT_ENOUGH, REFUND_AMOUNT_EXCEEDED, REFUND_FAILED, and more),
with placeholders for template variables.
- 13 payment-related Vue files
- Migrate catch-block error reporting from extractApiErrorMessage to
extractI18nErrorMessage(err, t, 'payment.errors', fallback).
- Remove the ad-hoc paymentErrorMap computed in SettingsView.vue, which the
new helper supersedes (it reads i18n directly via t).
- frontend/src/components/payment/providerConfig.ts
- wxpay: publicKey and publicKeyId are now required (was optional), matching
the pubkey-only verifier direction; certSerial is already required.
This PR is drop-in safe: reason-preferring extractApiErrorCode is backward
compatible with callers that pass their own i18nMap, and error codes missing
from i18n fall back to the existing message-based path.
This commit is contained in:
@@ -23,14 +23,96 @@ interface ApiErrorLike {
|
||||
|
||||
/**
|
||||
* Extract the error code from an API error object.
|
||||
*
|
||||
* Prefers the string `reason` (e.g. "PAYMENT_PROVIDER_MISCONFIGURED") over the
|
||||
* numeric HTTP `code`, because reason is granular enough to drive i18n lookup
|
||||
* while HTTP code is not.
|
||||
*/
|
||||
export function extractApiErrorCode(err: unknown): string | undefined {
|
||||
if (!err || typeof err !== 'object') return undefined
|
||||
const e = err as ApiErrorLike
|
||||
const code = e.code ?? e.reason ?? e.response?.data?.code
|
||||
const code = e.reason ?? e.code ?? e.response?.data?.code
|
||||
return code != null ? String(code) : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract metadata (interpolation params) from an API error object.
|
||||
* Backend errors carry `metadata` with template variables that fill i18n placeholders.
|
||||
*/
|
||||
export function extractApiErrorMetadata(err: unknown): Record<string, unknown> | undefined {
|
||||
if (!err || typeof err !== 'object') return undefined
|
||||
const e = err as ApiErrorLike
|
||||
return e.metadata
|
||||
}
|
||||
|
||||
type TranslateFn = (key: string, params?: Record<string, unknown>) => string
|
||||
type TranslateWithExistsFn = TranslateFn & { te?: (key: string) => boolean }
|
||||
|
||||
/**
|
||||
* Translate a value via i18n if a matching key exists, otherwise return the original.
|
||||
* Example: "certSerial" → t('admin.settings.payment.field_certSerial') → "证书序列号".
|
||||
*/
|
||||
function tryTranslate(t: TranslateFn, key: string, fallback: string): string {
|
||||
const translated = t(key)
|
||||
if (translated === key) return fallback
|
||||
const te = (t as TranslateWithExistsFn).te
|
||||
if (te && !te(key)) return fallback
|
||||
return translated
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace raw config field names in metadata (e.g. "certSerial") with their
|
||||
* localized UI labels (e.g. "证书序列号"), using the provider-config field i18n namespace.
|
||||
* Handles both single `key` and `/`-joined `keys` patterns used by wxpay errors.
|
||||
*/
|
||||
function localizeMetadata(metadata: Record<string, unknown>, t: TranslateFn): Record<string, unknown> {
|
||||
const out: Record<string, unknown> = { ...metadata }
|
||||
if (typeof out.key === 'string') {
|
||||
out.key = tryTranslate(t, `admin.settings.payment.field_${out.key}`, out.key)
|
||||
}
|
||||
if (typeof out.keys === 'string') {
|
||||
out.keys = out.keys
|
||||
.split('/')
|
||||
.map(k => tryTranslate(t, `admin.settings.payment.field_${k}`, k))
|
||||
.join(' / ')
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a localized error message from an API error by looking up
|
||||
* `<namespace>.<REASON>` in i18n and substituting metadata as placeholders.
|
||||
*
|
||||
* Config-field names in metadata (`key` / `keys`) are automatically translated
|
||||
* to their UI labels before substitution, so error messages read like
|
||||
* "缺少必填项:证书序列号" instead of "缺少必填项:certSerial".
|
||||
*
|
||||
* @param err - The caught error
|
||||
* @param t - Vue i18n translate function
|
||||
* @param namespace- i18n key prefix, e.g. "payment.errors"
|
||||
* @param fallback - Fallback key or plain string if no localized mapping exists
|
||||
*/
|
||||
export function extractI18nErrorMessage(
|
||||
err: unknown,
|
||||
t: TranslateFn,
|
||||
namespace: string,
|
||||
fallback: string,
|
||||
): string {
|
||||
const code = extractApiErrorCode(err)
|
||||
if (code) {
|
||||
const key = `${namespace}.${code}`
|
||||
const rawMetadata = extractApiErrorMetadata(err) ?? {}
|
||||
const metadata = localizeMetadata(rawMetadata, t)
|
||||
const translated = t(key, metadata)
|
||||
// Vue i18n returns the key itself when missing; detect that and fall back.
|
||||
if (translated !== key) return translated
|
||||
// If the framework exposes `te`, use it to double-check.
|
||||
const te = (t as TranslateWithExistsFn).te
|
||||
if (te && te(key)) return translated
|
||||
}
|
||||
return extractApiErrorMessage(err, fallback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a displayable error message from an API error.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user