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
@@ -22,6 +22,7 @@ import { useUIStore } from '@/stores/useUIStore';
import { useTerminalStore } from '@/stores/useTerminalStore';
import { useDesktopSshStore } from '@/stores/useDesktopSshStore';
import { openExternalUrl } from '@/lib/url';
import { useI18n } from '@/lib/i18n';
import {
getProjectActionsState,
type OpenChamberProjectAction,
@@ -154,10 +155,10 @@ const extractBestUrl = (value: string): string | null => {
return normalized[0] ?? null;
};
const formatActionButtonLabel = (value: string): string => {
const formatActionButtonLabel = (value: string, fallbackLabel: string): string => {
const trimmed = value.trim();
if (!trimmed) {
return 'Action';
return fallbackLabel;
}
const words = trimmed.split(/\s+/).filter(Boolean);
@@ -181,6 +182,7 @@ export const ProjectActionsButton = ({
compact = false,
allowMobile = false,
}: ProjectActionsButtonProps) => {
const { t } = useI18n();
const { terminal, runtime } = useRuntimeAPIs();
const { isMobile } = useDeviceInfo();
const isDesktopShellApp = React.useMemo(() => isDesktopShell(), []);
@@ -357,7 +359,7 @@ export const ProjectActionsButton = ({
if (maybeUrl) {
watch.openedUrl = true;
void openExternal(maybeUrl);
toast.success('Opened URL from action output');
toast.success(t('projectActions.toast.openedUrlFromOutput'));
}
urlWatchByRunKeyRef.current[runKey] = watch;
}
@@ -383,7 +385,7 @@ export const ProjectActionsButton = ({
const getOrCreateActionTab = React.useCallback(async (action: OpenChamberProjectAction) => {
if (!normalizedDirectory) {
throw new Error('No active directory');
throw new Error(t('projectActions.error.noActiveDirectory'));
}
const key = toProjectActionRunKey(normalizedDirectory, action.id);
@@ -430,7 +432,7 @@ export const ProjectActionsButton = ({
}
if (!normalizedDirectory) {
toast.error('No active directory for action');
toast.error(t('projectActions.error.noActiveDirectoryForAction'));
return;
}
@@ -458,7 +460,7 @@ export const ProjectActionsButton = ({
}
if (!activeSessionId) {
throw new Error('Failed to create terminal session');
throw new Error(t('projectActions.error.failedToCreateTerminalSession'));
}
if (createdSession) {
@@ -488,14 +490,14 @@ export const ProjectActionsButton = ({
if (desktopForwardUrl) {
void openExternal(desktopForwardUrl);
toast.success('Opened forwarded URL');
toast.success(t('projectActions.toast.openedForwardedUrl'));
} else if (manualOpenUrl) {
void openExternal(manualOpenUrl);
toast.success('Opened action URL');
toast.success(t('projectActions.toast.openedActionUrl'));
} else if (hasCustomOpenUrl) {
toast.error('Invalid custom URL format');
toast.error(t('projectActions.error.invalidCustomUrlFormat'));
} else if (hasDesktopForwardSelection) {
toast.error('Selected desktop SSH forward is unavailable');
toast.error(t('projectActions.error.selectedDesktopSshForwardUnavailable'));
}
urlWatchByRunKeyRef.current[key] = {
@@ -513,7 +515,7 @@ export const ProjectActionsButton = ({
return next;
});
delete urlWatchByRunKeyRef.current[runKey];
toast.error(error instanceof Error ? error.message : 'Failed to run action');
toast.error(error instanceof Error ? error.message : t('projectActions.error.failedToRunAction'));
}
}, [
desktopSshInstances,
@@ -645,7 +647,7 @@ export const ProjectActionsButton = ({
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
className
)}
aria-label="Add action"
aria-label={t('projectActions.actions.addActionAria')}
onClick={openProjectActionsSettings}
>
<RiAddLine className="h-5 w-5" />
@@ -666,7 +668,7 @@ export const ProjectActionsButton = ({
onClick={openProjectActionsSettings}
>
<RiAddLine className="h-4 w-4 text-muted-foreground" />
<span className="header-open-label whitespace-nowrap">Add action</span>
<span className="header-open-label whitespace-nowrap">{t('projectActions.actions.addAction')}</span>
</button>
);
}
@@ -678,7 +680,10 @@ export const ProjectActionsButton = ({
const selectedIconKey = (resolvedSelected.icon || 'play') as keyof typeof PROJECT_ACTION_ICON_MAP;
const SelectedIcon = PROJECT_ACTION_ICON_MAP[selectedIconKey] || RiPlayLine;
const selectedButtonLabel = formatActionButtonLabel(resolvedSelected.name);
const selectedButtonLabel = formatActionButtonLabel(
resolvedSelected.name,
t('projectActions.label.fallbackAction'),
);
const selectedRunKey = toProjectActionRunKey(normalizedDirectory, resolvedSelected.id);
const selectedRunning = runningByKey[selectedRunKey];
const isStoppingSelected = selectedRunning?.status === 'stopping';
@@ -697,7 +702,9 @@ export const ProjectActionsButton = ({
'disabled:cursor-not-allowed',
className
)}
aria-label={selectedRunning ? `Stop ${resolvedSelected.name}` : `Run ${resolvedSelected.name}`}
aria-label={selectedRunning
? t('projectActions.actions.stopNamedAria', { name: resolvedSelected.name })
: t('projectActions.actions.runNamedAria', { name: resolvedSelected.name })}
>
{isStoppingSelected
? <RiLoader4Line className="h-5 w-5 animate-spin text-[var(--status-warning)]" />
@@ -709,7 +716,7 @@ export const ProjectActionsButton = ({
<DropdownMenuContent align="end" className="w-52 max-h-[70vh] overflow-y-auto">
<DropdownMenuItem className="flex items-center gap-2" onClick={openProjectActionsSettings}>
<RiAddLine className="h-4 w-4" />
<span className="typography-ui-label text-foreground">Add new action</span>
<span className="typography-ui-label text-foreground">{t('projectActions.actions.addNewAction')}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
{actions.map((entry) => {
@@ -762,7 +769,9 @@ export const ProjectActionsButton = ({
compact ? 'w-9 justify-center px-0' : 'gap-2 px-3',
'transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:cursor-not-allowed'
)}
aria-label={selectedRunning ? `Stop ${resolvedSelected.name}` : `Run ${resolvedSelected.name}`}
aria-label={selectedRunning
? t('projectActions.actions.stopNamedAria', { name: resolvedSelected.name })
: t('projectActions.actions.runNamedAria', { name: resolvedSelected.name })}
>
<span className="inline-flex h-4 w-4 shrink-0 items-center justify-center">
{isStoppingSelected
@@ -783,7 +792,7 @@ export const ProjectActionsButton = ({
'border-l border-[var(--interactive-border)] text-muted-foreground',
'hover:bg-interactive-hover hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary'
)}
aria-label="Choose project action"
aria-label={t('projectActions.actions.chooseActionAria')}
>
<RiArrowDownSLine className="h-4 w-4" />
</button>
@@ -791,7 +800,7 @@ export const ProjectActionsButton = ({
<DropdownMenuContent align="center" className="w-52 max-h-[70vh] overflow-y-auto" style={{ translate: '-30px 0' }}>
<DropdownMenuItem className="flex items-center gap-2" onClick={openProjectActionsSettings}>
<RiAddLine className="h-4 w-4" />
<span className="typography-ui-label text-foreground">Add new action</span>
<span className="typography-ui-label text-foreground">{t('projectActions.actions.addNewAction')}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
{actions.map((entry) => {