feat: align with upstream 1.5.9.9
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
import type { MaybeRefOrGetter } from 'vue'
|
||||
import { useLocale } from '~/utils/locale'
|
||||
|
||||
/**
|
||||
* Shared locale-access helpers.
|
||||
*
|
||||
* Historically ~30 components each re-implemented their own accessor
|
||||
* (`callLocale`, `getLocaleMessage`, `getNestedMessage`, `formatLocaleValue`,
|
||||
* `getThrownMessage`, `formatString`, ...). These are the single source of truth.
|
||||
*
|
||||
* Two families:
|
||||
* - Standalone helpers (`formatLocaleValue`, `getThrownMessage`) that operate on an
|
||||
* already-resolved value / error object and need no locale source.
|
||||
* - `useLocaleText(source)` which binds a locale section (raw object or a
|
||||
* `useSafeLocale` proxy) and exposes key/path lookups (`t`, `msg`, `nested`)
|
||||
* plus the `format` / `errMessage` conveniences.
|
||||
*
|
||||
* Substitution supports `{0}`, `{1}`, ... positional placeholders. `format`/`t`
|
||||
* additionally accept the `{count}` alias (→ arg 0) used by a couple of pluralized
|
||||
* strings.
|
||||
*/
|
||||
|
||||
/** Replace `{0}`, `{1}`, ... plus the `{count}` alias (→ arg 0) in a template string. */
|
||||
const substituteWithCount = (template: string, args: unknown[]): string =>
|
||||
template.replace(/{(\d+)}|{count}/g, (match, index) => {
|
||||
const argIndex = match === '{count}' ? 0 : Number(index)
|
||||
// 缺参时回退为空串,避免把原始占位符(如 {0})暴露给用户
|
||||
return args[argIndex] !== undefined ? String(args[argIndex]) : ''
|
||||
})
|
||||
|
||||
/**
|
||||
* Resolve an already-obtained locale value to a display string.
|
||||
* - function → call it with `args` (locale formatter)
|
||||
* - string → positional `{n}` substitution
|
||||
* - otherwise → the value itself, or `''`
|
||||
*
|
||||
* Matches the dominant `formatLocaleValue(value, ...args)` variant used across the app.
|
||||
*/
|
||||
export const formatLocaleValue = (value: unknown, ...args: unknown[]): string => {
|
||||
if (typeof value === 'function') return (value as (...a: unknown[]) => unknown)(...args) as string
|
||||
if (typeof value === 'string') return substituteWithCount(value, args)
|
||||
return (value as string) ?? ''
|
||||
}
|
||||
|
||||
/** Alias kept for call sites that used the `formatLocale` name (identical semantics). */
|
||||
export const formatLocale = formatLocaleValue
|
||||
|
||||
/**
|
||||
* ofetch 抛出的 FetchError.message 形如 `[POST] "/api/x": 401 ...`、`[GET] "...": <no response>`,
|
||||
* 属于面向开发者的技术串,绝不能作为用户可见文案;识别后跳过,交由上层本地化兜底处理。
|
||||
*/
|
||||
const isFetchEnvelopeMessage = (message: string): boolean =>
|
||||
/^\[[A-Za-z]+\]\s+".*":\s+/.test(message)
|
||||
|
||||
/**
|
||||
* Extract a human-readable message from a thrown error / failed `$fetch` response.
|
||||
* Superset of the previous `getThrownMessage` / `getErrorMessage` variants.
|
||||
* 会过滤掉 ofetch 的技术性包装串,避免其泄漏到用户界面。
|
||||
*/
|
||||
export const getThrownMessage = (err: any): string => {
|
||||
if (!err) return ''
|
||||
if (typeof err === 'string') return err
|
||||
const candidates = [
|
||||
err?.data?.message,
|
||||
err?.data?.statusMessage,
|
||||
err?.message,
|
||||
err?.statusMessage
|
||||
]
|
||||
for (const candidate of candidates) {
|
||||
if (typeof candidate === 'string' && candidate && !isFetchEnvelopeMessage(candidate)) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/** Alias kept for call sites that used the `getErrorMessage` name. */
|
||||
export const getErrorMessage = getThrownMessage
|
||||
|
||||
export const useLocaleText = (
|
||||
source: MaybeRefOrGetter<Record<string, any> | null | undefined>
|
||||
) => {
|
||||
/** Resolve a value at `path` ("a" or "a.b.c") within the bound locale source. */
|
||||
const lookup = (path: string): unknown => {
|
||||
const base = toValue(source)
|
||||
return String(path)
|
||||
.split('.')
|
||||
.reduce<any>((target, key) => (target == null ? undefined : target[key]), base)
|
||||
}
|
||||
|
||||
/**
|
||||
* key/path → localized string with `{n}`/`{count}` substitution and a fallback.
|
||||
* Replaces the various `callLocale(key, fallback, ...args)` implementations.
|
||||
*/
|
||||
const t = (path: string, fallback = '', ...args: unknown[]): string => {
|
||||
const value = lookup(path)
|
||||
if (typeof value === 'function') return (value as (...a: unknown[]) => unknown)(...args) as string || fallback
|
||||
if (typeof value === 'string') return substituteWithCount(value, args) || fallback
|
||||
return (value as string) || fallback
|
||||
}
|
||||
|
||||
/** flat key → localized string, no fallback. Replaces `getLocaleMessage(key, ...args)`. */
|
||||
const msg = (key: string, ...args: unknown[]): string => t(key, '', ...args)
|
||||
|
||||
/** two-level section+key → localized string. Replaces `getNestedMessage(section, key, ...args)`. */
|
||||
const nested = (section: string, key: string, ...args: unknown[]): string =>
|
||||
t(`${section}.${key}`, '', ...args)
|
||||
|
||||
/**
|
||||
* Resolved value → localized string with a fallback and `{count}` support.
|
||||
* Replaces `formatLocaleValue(value, fallback, ...args)` (the fallback-bearing variant).
|
||||
*/
|
||||
const format = (value: unknown, fallback = '', ...args: unknown[]): string => {
|
||||
if (typeof value === 'function') return (value as (...a: unknown[]) => unknown)(...args) as string || fallback
|
||||
if (typeof value === 'string') return substituteWithCount(value, args) || fallback
|
||||
return (value as string) || fallback
|
||||
}
|
||||
|
||||
return { t, msg, nested, format, errMessage: getThrownMessage, lookup }
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the stable machine-readable error code from a thrown error / failed `$fetch`.
|
||||
* Errors created via `server/utils/apiError.ts#createApiError` carry the code in both
|
||||
* `statusMessage` and `data.code`; Nuxt/`$fetch` nests the server payload under `err.data`,
|
||||
* so we probe both depths. Mirrors the ad-hoc `getCardCodeErrorCode` this replaces.
|
||||
*/
|
||||
export const extractErrorCode = (err: any): string => {
|
||||
if (!err) return ''
|
||||
return (
|
||||
err?.data?.data?.code ||
|
||||
err?.data?.code ||
|
||||
err?.data?.statusMessage ||
|
||||
err?.statusMessage ||
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
/** Positional substitution params optionally carried on the error payload (`data.params`). */
|
||||
const extractErrorParams = (err: any): unknown[] => {
|
||||
const params = err?.data?.data?.params ?? err?.data?.params
|
||||
return Array.isArray(params) ? params : []
|
||||
}
|
||||
|
||||
/**
|
||||
* Code-aware localization for server-thrown errors.
|
||||
*
|
||||
* Resolution order:
|
||||
* 1. `serverErrors[code]` dictionary entry in the current locale (with `{n}` substitution
|
||||
* from `err.data.params`), falling back to zh-CN via the dictionary merge;
|
||||
* 2. the server-provided `message` (简体中文默认文案, from `createApiError`), 已过滤 ofetch 技术包装串;
|
||||
* 3. the supplied `fallback` string;
|
||||
* 4. 本地化通用兜底文案(按当前语言)。
|
||||
*
|
||||
* This is the single client entry point that decouples server messages from a specific
|
||||
* language, superseding the hand-maintained mirror maps (card-code tables, `serverMessages`).
|
||||
*/
|
||||
export const useServerErrors = () => {
|
||||
const { serverErrors, currentLocale } = useLocale()
|
||||
|
||||
const localize = (err: any, fallback = ''): string => {
|
||||
const code = extractErrorCode(err)
|
||||
if (code) {
|
||||
const entry = (serverErrors.value as Record<string, unknown> | undefined)?.[code]
|
||||
if (entry !== undefined && entry !== null) {
|
||||
return formatLocaleValue(entry, ...extractErrorParams(err))
|
||||
}
|
||||
}
|
||||
const thrown = getThrownMessage(err)
|
||||
if (thrown) return thrown
|
||||
if (fallback) return fallback
|
||||
// catch-all:码未命中、无干净消息、调用方也未给兜底时,返回本地化通用文案,
|
||||
// 避免向用户展示空白或 ofetch 原始技术串。
|
||||
return currentLocale.value === 'en-US'
|
||||
? 'Request failed, please try again later.'
|
||||
: '请求失败,请稍后重试'
|
||||
}
|
||||
|
||||
return { localize, extractErrorCode }
|
||||
}
|
||||
Reference in New Issue
Block a user