Add i18n foundation and translations (#1027)

* feat: add i18n foundation

* feat: localize sessions sidebar

* Localize multirun/scheduled tasks and fix dialog dropdown interactions

* localize git sidebar surface and add zh-CN keys

* feat(ui): localize context panel, diff/plan views, and context sidebar content

* fix(config): resolve user config home via fs/home before embedded home

* localize header/chat UI and complete model/worktree panel strings

* localize worktree + github issue/pr dialog flows

* localize settings sections and split settings i18n dictionaries

* localize additional settings sections and sidebars

* localize more settings pages and dialogs

* fix settings select trigger localization

* localize tunnel settings ui surface

* localize additional settings sections

* localize keyboard shortcuts labels in settings

* localize terminal and utility dialogs surfaces

* feat(i18n): localize remaining UI strings

* Add Ukrainian locale

* Add Spanish locale

* Add Brazilian Portuguese locale

* Polish locale translations
This commit is contained in:
Bohdan Triapitsyn
2026-04-26 14:03:39 +03:00
committed by GitHub
parent 87db2ea210
commit 7d7285655d
198 changed files with 24173 additions and 4365 deletions
@@ -34,6 +34,7 @@ import { GitHubSettings } from '@/components/sections/openchamber/GitHubSettings
import { GitIdentityEditorDialog } from './GitIdentityEditorDialog';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { cn } from '@/lib/utils';
import { useI18n } from '@/lib/i18n';
const ICON_MAP: Record<string, React.ComponentType<{ className?: string; style?: React.CSSProperties }>> = {
branch: RiGitBranchLine,
@@ -53,6 +54,7 @@ const COLOR_MAP: Record<string, string> = {
};
export const GitPage: React.FC = () => {
const { t } = useI18n();
const {
profiles,
globalIdentity,
@@ -91,10 +93,10 @@ export const GitPage: React.FC = () => {
const next = defaultGitIdentityId === profileId ? null : profileId;
const ok = await setDefaultGitIdentityId(next);
if (!ok) {
toast.error('Failed to update default identity');
toast.error(t('settings.gitIdentities.page.toast.updateDefaultFailed'));
return;
}
toast.success(next ? 'Default identity updated' : 'Default identity unset');
toast.success(next ? t('settings.gitIdentities.page.toast.defaultUpdated') : t('settings.gitIdentities.page.toast.defaultUnset'));
};
const handleConfirmDelete = async () => {
@@ -102,10 +104,10 @@ export const GitPage: React.FC = () => {
setIsDeletePending(true);
const success = await deleteProfile(deleteDialogProfile.id);
if (success) {
toast.success(`Profile "${deleteDialogProfile.name}" deleted`);
toast.success(t('settings.gitIdentities.page.toast.profileDeleted', { name: deleteDialogProfile.name }));
setDeleteDialogProfile(null);
} else {
toast.error('Failed to delete profile');
toast.error(t('settings.gitIdentities.page.toast.deleteProfileFailed'));
}
setIsDeletePending(false);
};
@@ -120,10 +122,10 @@ export const GitPage: React.FC = () => {
<div className="border-t border-border/40 pt-6">
<div className="mb-3 px-1 flex items-start justify-between gap-4">
<div className="flex items-center gap-2">
<h3 className="typography-ui-header font-semibold text-foreground">Identities</h3>
<h3 className="typography-ui-header font-semibold text-foreground">{t('settings.gitIdentities.page.section.title')}</h3>
</div>
<Button size="sm" variant="outline" onClick={() => openEditor('new')}>
<RiAddLine className="w-3.5 h-3.5 mr-1" /> New
<RiAddLine className="w-3.5 h-3.5 mr-1" /> {t('settings.common.badge.new')}
</Button>
</div>
@@ -157,8 +159,8 @@ export const GitPage: React.FC = () => {
{!globalIdentity && profiles.length === 0 && unimportedCredentials.length === 0 && (
<div className="py-8 px-4 text-center text-muted-foreground">
<RiShieldKeyholeLine className="mx-auto mb-2 h-8 w-8 opacity-40" />
<p className="typography-ui-label">No identities configured</p>
<p className="typography-meta mt-1 opacity-75">Create one to manage Git author settings per project</p>
<p className="typography-ui-label">{t('settings.gitIdentities.page.empty.title')}</p>
<p className="typography-meta mt-1 opacity-75">{t('settings.gitIdentities.page.empty.description')}</p>
</div>
)}
@@ -167,7 +169,7 @@ export const GitPage: React.FC = () => {
<>
<div className="px-4 py-2 border-t border-[var(--surface-subtle)]">
<span className="typography-micro text-muted-foreground">
Found in ~/.git-credentials
{t('settings.gitIdentities.page.discoveredCredentials.title')}
</span>
</div>
{unimportedCredentials.map((cred, i) => (
@@ -202,17 +204,17 @@ export const GitPage: React.FC = () => {
>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Delete Profile</DialogTitle>
<DialogTitle>{t('settings.gitIdentities.page.deleteDialog.title')}</DialogTitle>
<DialogDescription>
Are you sure you want to delete "{deleteDialogProfile?.name}"?
{t('settings.gitIdentities.page.deleteDialog.description', { name: deleteDialogProfile?.name ?? '' })}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="ghost" onClick={() => setDeleteDialogProfile(null)} disabled={isDeletePending}>
Cancel
{t('settings.common.actions.cancel')}
</Button>
<Button size="sm" variant="destructive" onClick={() => void handleConfirmDelete()} disabled={isDeletePending}>
Delete
{t('settings.common.actions.delete')}
</Button>
</DialogFooter>
</DialogContent>
@@ -242,6 +244,7 @@ const IdentityRow: React.FC<IdentityRowProps> = ({
isReadOnly,
hasBorder,
}) => {
const { t } = useI18n();
const IconComponent = ICON_MAP[profile.icon || 'branch'] || RiGitBranchLine;
const iconColor = COLOR_MAP[profile.color || ''];
const authType = profile.authType || 'ssh';
@@ -267,12 +270,12 @@ const IdentityRow: React.FC<IdentityRowProps> = ({
</span>
{isDefault && (
<span className="typography-micro text-primary bg-primary/12 px-1 rounded flex-shrink-0 leading-none pb-px border border-primary/25">
default
{t('settings.gitIdentities.page.badge.default')}
</span>
)}
{isReadOnly && (
<span className="typography-micro text-muted-foreground bg-muted px-1 rounded flex-shrink-0 leading-none pb-px border border-border/50">
system
{t('settings.agents.sidebar.badge.system')}
</span>
)}
</div>
@@ -295,7 +298,7 @@ const IdentityRow: React.FC<IdentityRowProps> = ({
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-fit min-w-28">
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onToggleDefault(); }}>
{isDefault ? 'Unset default' : 'Set as default'}
{isDefault ? t('settings.gitIdentities.page.actions.unsetDefault') : t('settings.gitIdentities.page.actions.setAsDefault')}
</DropdownMenuItem>
{!isReadOnly && onDelete && (
<DropdownMenuItem
@@ -303,7 +306,7 @@ const IdentityRow: React.FC<IdentityRowProps> = ({
className="text-destructive focus:text-destructive"
>
<RiDeleteBinLine className="h-4 w-4 mr-px" />
Delete
{t('settings.common.actions.delete')}
</DropdownMenuItem>
)}
</DropdownMenuContent>
@@ -321,6 +324,7 @@ interface DiscoveredRowProps {
}
const DiscoveredRow: React.FC<DiscoveredRowProps> = ({ credential, onImport, hasBorder }) => {
const { t } = useI18n();
const parts = credential.host.split('/');
const displayName = parts.length >= 3 ? parts[parts.length - 1] : credential.host;
const isRepoSpecific = credential.host.includes('/');
@@ -340,7 +344,7 @@ const DiscoveredRow: React.FC<DiscoveredRowProps> = ({ credential, onImport, has
</div>
<Button size="sm" variant="ghost" onClick={onImport} className="gap-1 shrink-0">
<RiDownloadLine className="h-3 w-3" />
Import
{t('settings.gitIdentities.page.actions.import')}
</Button>
</div>
);