mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
fix(desktop): localize output style descriptions (#966)
Localize built-in output style names and descriptions in desktop settings while preserving custom project/user/plugin style text. Also localize the provider model ID placeholder. Tested: - cd desktop && bun run test -- src/pages/SettingsOutputStyle.test.tsx - cd desktop && bun run test -- src/__tests__/generalSettings.test.tsx -t "localizes the main model placeholder" - bun run check:desktop Scope-risk: narrow Confidence: high
This commit is contained in:
parent
ce2c908161
commit
3e685eb6c2
@ -1651,6 +1651,34 @@ describe('Settings > Providers tab', () => {
|
||||
expect(within(dialog).getByText('Requests will be translated via the local proxy')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('localizes the main model placeholder in the provider form', () => {
|
||||
useSettingsStore.setState({ locale: 'zh' })
|
||||
providerStoreState.presets = [
|
||||
{
|
||||
id: 'custom',
|
||||
name: 'Custom',
|
||||
baseUrl: 'https://api.example.com/anthropic',
|
||||
apiFormat: 'anthropic',
|
||||
defaultModels: {
|
||||
main: '',
|
||||
haiku: '',
|
||||
sonnet: '',
|
||||
opus: '',
|
||||
},
|
||||
needsApiKey: true,
|
||||
websiteUrl: '',
|
||||
},
|
||||
]
|
||||
|
||||
render(<Settings />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /添加服务商/i }))
|
||||
|
||||
const dialog = screen.getByRole('dialog')
|
||||
expect(within(dialog).getByPlaceholderText('模型 ID')).toBeInTheDocument()
|
||||
expect(within(dialog).queryByPlaceholderText('Model ID')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('normalizes blank model mappings to the main model when saving a provider', async () => {
|
||||
providerStoreState.createProvider = vi.fn().mockResolvedValue({
|
||||
id: 'provider-new',
|
||||
|
||||
@ -416,6 +416,7 @@ export const en = {
|
||||
'settings.providers.sonnetModel': 'Sonnet Model',
|
||||
'settings.providers.opusModel': 'Opus Model',
|
||||
'settings.providers.model1mSupportShort': '1M',
|
||||
'settings.providers.modelIdPlaceholder': 'Model ID',
|
||||
'settings.providers.sameAsMain': 'Same as main',
|
||||
'settings.providers.contextSettingsTitle': 'Context and auto-compact',
|
||||
'settings.providers.contextSettingsDesc': 'Preset limits are applied automatically. Edit only when the provider changes limits or you use a custom model ID.',
|
||||
@ -969,6 +970,12 @@ export const en = {
|
||||
'settings.general.responseLangDefault': 'Default (English)',
|
||||
'settings.general.outputStyleTitle': 'Output Style',
|
||||
'settings.general.outputStyleDescription': 'Choose how Claude communicates in new or restarted sessions.',
|
||||
'settings.general.outputStyleBuiltin.default.label': 'Default',
|
||||
'settings.general.outputStyleBuiltin.default.description': 'Claude completes coding tasks efficiently and provides concise responses',
|
||||
'settings.general.outputStyleBuiltin.explanatory.label': 'Explanatory',
|
||||
'settings.general.outputStyleBuiltin.explanatory.description': 'Claude explains its implementation choices and codebase patterns',
|
||||
'settings.general.outputStyleBuiltin.learning.label': 'Learning',
|
||||
'settings.general.outputStyleBuiltin.learning.description': 'Claude pauses and asks you to write small pieces of code for hands-on practice',
|
||||
'settings.general.outputStyleSelectLabel': 'Select output style',
|
||||
'settings.general.outputStyleLoading': 'Loading output styles...',
|
||||
'settings.general.outputStyleSaved': 'Output style saved.',
|
||||
|
||||
@ -418,6 +418,7 @@ export const jp: Record<TranslationKey, string> = {
|
||||
'settings.providers.sonnetModel': 'Sonnet モデル',
|
||||
'settings.providers.opusModel': 'Opus モデル',
|
||||
'settings.providers.model1mSupportShort': '1M',
|
||||
'settings.providers.modelIdPlaceholder': 'モデル ID',
|
||||
'settings.providers.sameAsMain': 'メインと同じ',
|
||||
'settings.providers.contextSettingsTitle': 'コンテキストと自動コンパクト',
|
||||
'settings.providers.contextSettingsDesc': 'プリセットの上限は自動的に適用されます。プロバイダーが上限を変更した場合やカスタムモデル ID を使用する場合のみ編集してください。',
|
||||
@ -971,6 +972,12 @@ export const jp: Record<TranslationKey, string> = {
|
||||
'settings.general.responseLangDefault': 'デフォルト(英語)',
|
||||
'settings.general.outputStyleTitle': '出力スタイル',
|
||||
'settings.general.outputStyleDescription': '新しいセッションまたは再起動後のセッションで Claude がどのように応答するかを選択します。',
|
||||
'settings.general.outputStyleBuiltin.default.label': 'デフォルト',
|
||||
'settings.general.outputStyleBuiltin.default.description': 'Claude はコーディングタスクを効率よく完了し、簡潔に応答します',
|
||||
'settings.general.outputStyleBuiltin.explanatory.label': '解説型',
|
||||
'settings.general.outputStyleBuiltin.explanatory.description': 'Claude は実装上の判断とコードベースのパターンを説明します',
|
||||
'settings.general.outputStyleBuiltin.learning.label': '学習型',
|
||||
'settings.general.outputStyleBuiltin.learning.description': 'Claude は一時停止し、実践練習として短いコードの記述を依頼します',
|
||||
'settings.general.outputStyleSelectLabel': '出力スタイルを選択',
|
||||
'settings.general.outputStyleLoading': '出力スタイルを読み込み中...',
|
||||
'settings.general.outputStyleSaved': '出力スタイルを保存しました。',
|
||||
|
||||
@ -418,6 +418,7 @@ export const kr: Record<TranslationKey, string> = {
|
||||
'settings.providers.sonnetModel': 'Sonnet 모델',
|
||||
'settings.providers.opusModel': 'Opus 모델',
|
||||
'settings.providers.model1mSupportShort': '1M',
|
||||
'settings.providers.modelIdPlaceholder': '모델 ID',
|
||||
'settings.providers.sameAsMain': '메인과 동일',
|
||||
'settings.providers.contextSettingsTitle': '컨텍스트 및 자동 압축',
|
||||
'settings.providers.contextSettingsDesc': '사전 설정 한도가 자동으로 적용됩니다. 공급자가 한도를 변경하거나 사용자 지정 모델 ID를 사용할 때만 편집하세요.',
|
||||
@ -971,6 +972,12 @@ export const kr: Record<TranslationKey, string> = {
|
||||
'settings.general.responseLangDefault': '기본값(영어)',
|
||||
'settings.general.outputStyleTitle': '출력 스타일',
|
||||
'settings.general.outputStyleDescription': '새 세션 또는 다시 시작한 세션에서 Claude가 응답하는 방식을 선택합니다.',
|
||||
'settings.general.outputStyleBuiltin.default.label': '기본값',
|
||||
'settings.general.outputStyleBuiltin.default.description': 'Claude가 코딩 작업을 효율적으로 완료하고 간결하게 응답합니다',
|
||||
'settings.general.outputStyleBuiltin.explanatory.label': '설명형',
|
||||
'settings.general.outputStyleBuiltin.explanatory.description': 'Claude가 구현 선택과 코드베이스 패턴을 설명합니다',
|
||||
'settings.general.outputStyleBuiltin.learning.label': '학습형',
|
||||
'settings.general.outputStyleBuiltin.learning.description': 'Claude가 잠시 멈추고 실습을 위해 짧은 코드를 작성하도록 요청합니다',
|
||||
'settings.general.outputStyleSelectLabel': '출력 스타일 선택',
|
||||
'settings.general.outputStyleLoading': '출력 스타일 불러오는 중...',
|
||||
'settings.general.outputStyleSaved': '출력 스타일이 저장되었습니다.',
|
||||
|
||||
@ -418,6 +418,7 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'settings.providers.sonnetModel': 'Sonnet 模型',
|
||||
'settings.providers.opusModel': 'Opus 模型',
|
||||
'settings.providers.model1mSupportShort': '1M',
|
||||
'settings.providers.modelIdPlaceholder': '模型 ID',
|
||||
'settings.providers.sameAsMain': '與主模型相同',
|
||||
'settings.providers.contextSettingsTitle': '上下文與自動壓縮',
|
||||
'settings.providers.contextSettingsDesc': '預設會自動應用模型視窗。只有服務商調整限制,或使用自定義模型 ID 時才需要修改。',
|
||||
@ -971,6 +972,12 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'settings.general.responseLangDefault': '預設(英語)',
|
||||
'settings.general.outputStyleTitle': '輸出風格',
|
||||
'settings.general.outputStyleDescription': '選擇 Claude 在新會話或重啟後的會話中如何表達。',
|
||||
'settings.general.outputStyleBuiltin.default.label': '預設',
|
||||
'settings.general.outputStyleBuiltin.default.description': 'Claude 會高效完成編碼任務,並提供簡潔回覆',
|
||||
'settings.general.outputStyleBuiltin.explanatory.label': '解釋型',
|
||||
'settings.general.outputStyleBuiltin.explanatory.description': 'Claude 會解釋實作選擇和程式碼庫模式',
|
||||
'settings.general.outputStyleBuiltin.learning.label': '學習型',
|
||||
'settings.general.outputStyleBuiltin.learning.description': 'Claude 會暫停並請你編寫小段程式碼進行實戰練習',
|
||||
'settings.general.outputStyleSelectLabel': '選擇輸出風格',
|
||||
'settings.general.outputStyleLoading': '正在載入輸出風格...',
|
||||
'settings.general.outputStyleSaved': '輸出風格已儲存。',
|
||||
|
||||
@ -418,6 +418,7 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'settings.providers.sonnetModel': 'Sonnet 模型',
|
||||
'settings.providers.opusModel': 'Opus 模型',
|
||||
'settings.providers.model1mSupportShort': '1M',
|
||||
'settings.providers.modelIdPlaceholder': '模型 ID',
|
||||
'settings.providers.sameAsMain': '与主模型相同',
|
||||
'settings.providers.contextSettingsTitle': '上下文与自动压缩',
|
||||
'settings.providers.contextSettingsDesc': '预设会自动应用模型窗口。只有服务商调整限制,或使用自定义模型 ID 时才需要修改。',
|
||||
@ -971,6 +972,12 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'settings.general.responseLangDefault': '默认(英语)',
|
||||
'settings.general.outputStyleTitle': '输出风格',
|
||||
'settings.general.outputStyleDescription': '选择 Claude 在新会话或重启后的会话中如何表达。',
|
||||
'settings.general.outputStyleBuiltin.default.label': '默认',
|
||||
'settings.general.outputStyleBuiltin.default.description': 'Claude 会高效完成编码任务,并提供简洁回复',
|
||||
'settings.general.outputStyleBuiltin.explanatory.label': '解释型',
|
||||
'settings.general.outputStyleBuiltin.explanatory.description': 'Claude 会解释实现选择和代码库模式',
|
||||
'settings.general.outputStyleBuiltin.learning.label': '学习型',
|
||||
'settings.general.outputStyleBuiltin.learning.description': 'Claude 会暂停并请你编写小段代码进行实战练习',
|
||||
'settings.general.outputStyleSelectLabel': '选择输出风格',
|
||||
'settings.general.outputStyleLoading': '正在加载输出风格...',
|
||||
'settings.general.outputStyleSaved': '输出风格已保存。',
|
||||
|
||||
@ -83,6 +83,20 @@ const NETWORK_TIMEOUT_MIN_SECONDS = 30
|
||||
const NETWORK_TIMEOUT_MAX_SECONDS = 1800
|
||||
const NETWORK_TIMEOUT_STEP_SECONDS = 30
|
||||
const SETTINGS_CHECKBOX_INPUT_CLASS = 'settings-checkbox-input peer'
|
||||
const BUILT_IN_OUTPUT_STYLE_TRANSLATION_KEYS = {
|
||||
default: {
|
||||
label: 'settings.general.outputStyleBuiltin.default.label',
|
||||
description: 'settings.general.outputStyleBuiltin.default.description',
|
||||
},
|
||||
Explanatory: {
|
||||
label: 'settings.general.outputStyleBuiltin.explanatory.label',
|
||||
description: 'settings.general.outputStyleBuiltin.explanatory.description',
|
||||
},
|
||||
Learning: {
|
||||
label: 'settings.general.outputStyleBuiltin.learning.label',
|
||||
description: 'settings.general.outputStyleBuiltin.learning.description',
|
||||
},
|
||||
} satisfies Record<string, { label: TranslationKey; description: TranslationKey }>
|
||||
|
||||
function buildH5LaunchUrl(baseUrl: string | null, token: string | null): string | null {
|
||||
if (!baseUrl) return null
|
||||
@ -1763,7 +1777,7 @@ function ProviderFormModal({ open, onClose, mode, provider, presets }: ProviderF
|
||||
required={slot === 'main'}
|
||||
value={models[slot]}
|
||||
onChange={(e) => handleModelChange(slot, e.target.value)}
|
||||
placeholder={slot === 'main' ? 'Model ID' : t('settings.providers.sameAsMain')}
|
||||
placeholder={slot === 'main' ? t('settings.providers.modelIdPlaceholder') : t('settings.providers.sameAsMain')}
|
||||
/>
|
||||
<label className="mt-1 inline-flex h-6 w-fit cursor-pointer items-center gap-1.5 rounded-[var(--radius-sm)] px-1 text-xs text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)]">
|
||||
<input
|
||||
@ -2156,11 +2170,17 @@ export function GeneralSettings() {
|
||||
RESPONSE_LANGUAGES.find(({ value }) => value === responseLanguage)?.label ?? RESPONSE_LANGUAGES[0]!.label
|
||||
const outputStyleItems = outputStyles.map((style) => ({
|
||||
value: style.value,
|
||||
label: style.label,
|
||||
description: `${style.description} · ${getOutputStyleSourceLabel(style.source, t)}`,
|
||||
label: getOutputStyleLabel(style, t),
|
||||
description: `${getOutputStyleDescription(style, t)} · ${getOutputStyleSourceLabel(style.source, t)}`,
|
||||
}))
|
||||
const selectedOutputStyle =
|
||||
outputStyles.find((style) => style.value === outputStyle) ?? outputStyles[0]
|
||||
const selectedOutputStyleLabel = selectedOutputStyle
|
||||
? getOutputStyleLabel(selectedOutputStyle, t)
|
||||
: outputStyle
|
||||
const selectedOutputStyleDescription = selectedOutputStyle
|
||||
? getOutputStyleDescription(selectedOutputStyle, t)
|
||||
: ''
|
||||
const outputStyleScopeLabel = outputStyleScope === 'localSettings'
|
||||
? t('settings.general.outputStyleScopeLocal')
|
||||
: t('settings.general.outputStyleScopeUser')
|
||||
@ -2633,11 +2653,11 @@ export function GeneralSettings() {
|
||||
<span className="block truncate font-medium">
|
||||
{outputStylesLoading
|
||||
? t('settings.general.outputStyleLoading')
|
||||
: selectedOutputStyle?.label ?? outputStyle}
|
||||
: selectedOutputStyleLabel}
|
||||
</span>
|
||||
{selectedOutputStyle?.description && (
|
||||
{selectedOutputStyleDescription && (
|
||||
<span className="mt-0.5 block truncate text-xs text-[var(--color-text-tertiary)]">
|
||||
{selectedOutputStyle.description}
|
||||
{selectedOutputStyleDescription}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
@ -3280,6 +3300,40 @@ export function GeneralSettings() {
|
||||
)
|
||||
}
|
||||
|
||||
function getBuiltInOutputStyleTranslationKeys(style: {
|
||||
value: string
|
||||
source: OutputStyleSource
|
||||
}) {
|
||||
if (style.source !== 'built-in') return null
|
||||
return BUILT_IN_OUTPUT_STYLE_TRANSLATION_KEYS[
|
||||
style.value as keyof typeof BUILT_IN_OUTPUT_STYLE_TRANSLATION_KEYS
|
||||
] ?? null
|
||||
}
|
||||
|
||||
function getOutputStyleLabel(
|
||||
style: {
|
||||
value: string
|
||||
label: string
|
||||
source: OutputStyleSource
|
||||
},
|
||||
t: (key: TranslationKey) => string,
|
||||
) {
|
||||
const keys = getBuiltInOutputStyleTranslationKeys(style)
|
||||
return keys ? t(keys.label) : style.label
|
||||
}
|
||||
|
||||
function getOutputStyleDescription(
|
||||
style: {
|
||||
value: string
|
||||
description: string
|
||||
source: OutputStyleSource
|
||||
},
|
||||
t: (key: TranslationKey) => string,
|
||||
) {
|
||||
const keys = getBuiltInOutputStyleTranslationKeys(style)
|
||||
return keys ? t(keys.description) : style.description
|
||||
}
|
||||
|
||||
function getOutputStyleSourceLabel(
|
||||
source: OutputStyleSource,
|
||||
t: (key: TranslationKey) => string,
|
||||
|
||||
@ -125,4 +125,43 @@ describe('GeneralSettings output style', () => {
|
||||
})
|
||||
expect(useSettingsStore.getState().outputStyle).toBe('Learning')
|
||||
})
|
||||
|
||||
it('localizes built-in output style descriptions in Chinese', async () => {
|
||||
useSettingsStore.setState({ locale: 'zh' })
|
||||
settingsApiMock.getOutputStyles.mockResolvedValueOnce({
|
||||
outputStyle: 'default',
|
||||
scope: 'localSettings',
|
||||
workDir: '/repo',
|
||||
styles: [
|
||||
{
|
||||
value: 'default',
|
||||
label: 'Default',
|
||||
description: 'Claude completes coding tasks efficiently and provides concise responses',
|
||||
source: 'built-in',
|
||||
},
|
||||
{
|
||||
value: 'Learning',
|
||||
label: 'Learning',
|
||||
description: 'Claude pauses and asks you to write small pieces of code for hands-on practice',
|
||||
source: 'built-in',
|
||||
},
|
||||
{
|
||||
value: 'Project Style',
|
||||
label: 'Project Style',
|
||||
description: 'Project custom voice',
|
||||
source: 'projectSettings',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
render(<GeneralSettings />)
|
||||
|
||||
expect(await screen.findByText('Claude 会高效完成编码任务,并提供简洁回复')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '选择输出风格' }))
|
||||
|
||||
expect(screen.getByText('Claude 会暂停并请你编写小段代码进行实战练习 · 内置')).toBeInTheDocument()
|
||||
expect(screen.getByText('Project custom voice · 项目风格')).toBeInTheDocument()
|
||||
expect(screen.queryByText(/Claude completes coding tasks efficiently/)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user