Implements a zero-dependency i18n framework with TypeScript type safety. Thanks to @gy212 for the original i18n infrastructure design (PR #89) and comprehensive translation files (PR #90-96) that inspired this implementation. ## New files (5) - src/i18n/en.ts: English translations (source of truth, ~270 keys), exports TranslationKey type for compile-time key completeness - src/i18n/zh.ts: Chinese translations, typed as Record<TranslationKey, string> - src/i18n/index.ts: translate() function with {param} interpolation, Locale type, SUPPORTED_LOCALES array - src/components/layout/I18nProvider.tsx: React Context provider, loads/persists locale preference via /api/settings/app - src/hooks/useTranslation.ts: useTranslation() hook returning { locale, setLocale, t } ## Infrastructure wiring - src/app/layout.tsx: I18nProvider wrapped inside ThemeProvider - src/app/api/settings/app/route.ts: added "locale" to ALLOWED_KEYS for persistence ## Language picker - src/components/settings/GeneralSection.tsx: added language selection dropdown (Select component) in Settings > General, using SUPPORTED_LOCALES and setLocale() ## String replacements (33 component files) All hardcoded user-facing strings replaced with t() calls: High priority (chat): - ChatListPanel.tsx: session list, search, empty state, relative time - NavRail.tsx: nav labels, theme toggle, auto-approve indicator - MessageInput.tsx: command descriptions, mode labels, attach tooltip - MessageList.tsx: empty state, load earlier, loading indicator - StreamingMessage.tsx: thinking, permission actions - chat/[id]/page.tsx: session title fallback Settings: - GeneralSection.tsx: update card, auto-approve section, warning dialog - SettingsLayout.tsx: title, description, sidebar labels - CliSettingsSection.tsx: form/json tabs, buttons, dynamic field labels (skipDangerousModePermissionPrompt etc.), Enabled/Disabled states - ProviderForm.tsx: dialog title, form labels, buttons - ProviderManager.tsx: status badges, delete dialog, section headers Extensions: - extensions/page.tsx: title, tab labels - SkillsManager.tsx: empty state, loading, search - SkillEditor.tsx: toolbar buttons, placeholder - SkillListItem.tsx: delete confirmation - CreateSkillDialog.tsx: form labels, scope, templates, validation Plugins: - McpManager.tsx: header, tabs, loading state - McpServerList.tsx: empty state, badges, field labels - McpServerEditor.tsx: dialog title, form labels, buttons - ConfigEditor.tsx: save/format buttons Layout: - RightPanel.tsx: panel header, open/close tooltips - FileTree.tsx: search placeholder, empty states - FilePreview.tsx: back button, line count, copy path - DocPreview.tsx: iframe title - ConnectionStatus.tsx: install prompts, version display - InstallWizard.tsx: phase descriptions, button labels - ImportSessionDialog.tsx: search, relative time, import buttons - FolderPicker.tsx: dialog title, loading, buttons - TaskList.tsx: filter tabs, input placeholder, empty states
66 lines
1.7 KiB
TypeScript
66 lines
1.7 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Label } from '@/components/ui/label';
|
|
import { useTranslation } from '@/hooks/useTranslation';
|
|
|
|
interface ConfigEditorProps {
|
|
value: string;
|
|
onSave: (value: string) => void;
|
|
label?: string;
|
|
}
|
|
|
|
export function ConfigEditor({ value, onSave, label }: ConfigEditorProps) {
|
|
const { t } = useTranslation();
|
|
const [text, setText] = useState(value);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => { setText(value); }, [value]);
|
|
|
|
function handleSave() {
|
|
try {
|
|
JSON.parse(text);
|
|
setError(null);
|
|
onSave(text);
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Invalid JSON');
|
|
}
|
|
}
|
|
|
|
function handleFormat() {
|
|
try {
|
|
const parsed = JSON.parse(text);
|
|
setText(JSON.stringify(parsed, null, 2));
|
|
setError(null);
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Invalid JSON');
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
{label && <Label>{label}</Label>}
|
|
<Textarea
|
|
value={text}
|
|
onChange={(e) => {
|
|
setText(e.target.value);
|
|
setError(null);
|
|
}}
|
|
className="font-mono text-sm min-h-[200px]"
|
|
placeholder="{}"
|
|
/>
|
|
{error && <p className="text-xs text-destructive">{error}</p>}
|
|
<div className="flex gap-2">
|
|
<Button size="sm" onClick={handleSave}>
|
|
{t('common.save')}
|
|
</Button>
|
|
<Button size="sm" variant="outline" onClick={handleFormat}>
|
|
Format
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|