feat(client): adopt the locale seat in theme, sidebar, question, and model
Each package ships its zh/en dictionaries as satisfies-typed pairs (zh is the key-set source of truth; en is checked complete against it), merges its namespace into LocaleNamespaceMap, and declares locale: NS at register — components read the framework-injected typed t seat instead of a hand-carried inject member. Overlapping verbatim words (retry, submit, submitting) drop out of package dictionaries in favor of the shared common vocabulary; the question composer stores validation feedback as dictionary keys so shown feedback follows a locale switch.
This commit is contained in:
@@ -65,7 +65,10 @@ function QuestionFlow({ pending, t }: { pending: PendingQuestion } & Pick<Questi
|
||||
selected: [], custom: '', customOpen: (question.options?.length ?? 0) === 0, skipped: false,
|
||||
})))
|
||||
const [busy, setBusy] = useState<'answer' | 'cancel' | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
// Validation feedback is stored as a dictionary KEY and translated at
|
||||
// render, so already-shown feedback follows a locale switch; runtime
|
||||
// failure messages (finished strings from the wire) pass through verbatim.
|
||||
const [error, setError] = useState<{ key: 'error.incomplete' | 'error.unanswered' } | { text: string } | null>(null)
|
||||
// index stays in bounds (every setIndex site clamps) and drafts mirrors questions 1:1.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const question = questions[index]!
|
||||
@@ -78,7 +81,7 @@ function QuestionFlow({ pending, t }: { pending: PendingQuestion } & Pick<Questi
|
||||
setError(null)
|
||||
void pending.cancel().catch((cause: unknown) => {
|
||||
setBusy(null)
|
||||
setError(cause instanceof Error ? cause.message : String(cause))
|
||||
setError({ text: cause instanceof Error ? cause.message : String(cause) })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -114,7 +117,7 @@ function QuestionFlow({ pending, t }: { pending: PendingQuestion } & Pick<Questi
|
||||
const missing = values.findIndex(item => !completed(item))
|
||||
if (missing >= 0) {
|
||||
setIndex(missing)
|
||||
setError(t('error.incomplete'))
|
||||
setError({ key: 'error.incomplete' })
|
||||
return
|
||||
}
|
||||
const answer: QuestionAnswer = {
|
||||
@@ -133,13 +136,13 @@ function QuestionFlow({ pending, t }: { pending: PendingQuestion } & Pick<Questi
|
||||
setError(null)
|
||||
void pending.answer(answer).catch((cause: unknown) => {
|
||||
setBusy(null)
|
||||
setError(cause instanceof Error ? cause.message : String(cause))
|
||||
setError({ text: cause instanceof Error ? cause.message : String(cause) })
|
||||
})
|
||||
}
|
||||
|
||||
const continueFlow = (): void => {
|
||||
if (!answered(draft)) {
|
||||
setError(t('error.unanswered'))
|
||||
setError({ key: 'error.unanswered' })
|
||||
return
|
||||
}
|
||||
if (index < questions.length - 1) {
|
||||
@@ -292,7 +295,7 @@ function QuestionFlow({ pending, t }: { pending: PendingQuestion } & Pick<Questi
|
||||
</div>
|
||||
|
||||
<footer className={css.footer}>
|
||||
<div className={css.feedback} role="status">{error}</div>
|
||||
<div className={css.feedback} role="status">{error === null ? null : 'key' in error ? t(error.key) : error.text}</div>
|
||||
<div className={css.footerActions}>
|
||||
<Button variant="ghost" size="sm" disabled={busy !== null} onClick={skipQuestion}>
|
||||
{t('action.skip')}
|
||||
@@ -302,8 +305,8 @@ function QuestionFlow({ pending, t }: { pending: PendingQuestion } & Pick<Questi
|
||||
disabled={busy !== null || !answered(draft)} onClick={continueFlow}
|
||||
>
|
||||
{busy === 'answer'
|
||||
? t('action.submitting')
|
||||
: index === questions.length - 1 ? t('action.submit') : t('action.next')}
|
||||
? t('submitting')
|
||||
: index === questions.length - 1 ? t('submit') : t('action.next')}
|
||||
</Button>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -12,8 +12,6 @@ export const zh = {
|
||||
'option.custom': '其他,请填写自定义答案',
|
||||
'custom.placeholder': '输入你的答案',
|
||||
'action.skip': '跳过本题',
|
||||
'action.submitting': '正在提交…',
|
||||
'action.submit': '提交',
|
||||
'action.next': '下一题',
|
||||
} satisfies Record<string, string>
|
||||
|
||||
@@ -21,7 +19,7 @@ export const zh = {
|
||||
export type QuestionKey = keyof typeof zh
|
||||
|
||||
/** English dictionary, checked complete against the zh key set. */
|
||||
export const en: Record<QuestionKey, string> = {
|
||||
export const en = {
|
||||
'error.incomplete': 'Please complete this question first.',
|
||||
'error.unanswered': 'Please select an option or enter a custom answer.',
|
||||
'title.multi': 'Multi-select',
|
||||
@@ -32,7 +30,5 @@ export const en: Record<QuestionKey, string> = {
|
||||
'option.custom': 'Other — enter a custom answer',
|
||||
'custom.placeholder': 'Type your answer',
|
||||
'action.skip': 'Skip this question',
|
||||
'action.submitting': 'Submitting…',
|
||||
'action.submit': 'Submit',
|
||||
'action.next': 'Next',
|
||||
}
|
||||
} satisfies Record<QuestionKey, string>
|
||||
|
||||
Reference in New Issue
Block a user