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:
committed by
GitHub
parent
87db2ea210
commit
7d7285655d
@@ -28,6 +28,8 @@ import { useDeviceInfo } from '@/lib/device';
|
||||
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
|
||||
import { getContextFileOpenFailureMessage, validateContextFileOpen } from '@/lib/contextFileOpenGuard';
|
||||
import { sessionEvents } from '@/lib/sessionEvents';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import type { I18nKey } from '@/lib/i18n/store';
|
||||
|
||||
// Minimum width for side-by-side diff view (px)
|
||||
const SIDE_BY_SIDE_MIN_WIDTH = 1100;
|
||||
@@ -52,9 +54,10 @@ type FileEntry = GitStatus['files'][number] & {
|
||||
type DiffData = { original: string; modified: string; isBinary?: boolean };
|
||||
|
||||
const BinaryDiffPlaceholder = React.memo(() => {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<div className="rounded-lg border border-border/60 bg-background px-3 py-2">
|
||||
<div className="typography-meta text-muted-foreground">Content of this file cannot be viewed.</div>
|
||||
<div className="typography-meta text-muted-foreground">{t('diffView.binary.unavailable')}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -64,34 +67,34 @@ type DiffTabViewMode = 'single' | 'stacked';
|
||||
type ChangeDescriptor = {
|
||||
code: string;
|
||||
color: string;
|
||||
description: string;
|
||||
descriptionKey: I18nKey;
|
||||
};
|
||||
|
||||
const CHANGE_DESCRIPTORS: Record<string, ChangeDescriptor> = {
|
||||
'?': { code: '?', color: 'var(--status-info)', description: 'Untracked file' },
|
||||
A: { code: 'A', color: 'var(--status-success)', description: 'New file' },
|
||||
D: { code: 'D', color: 'var(--status-error)', description: 'Deleted file' },
|
||||
R: { code: 'R', color: 'var(--status-info)', description: 'Renamed file' },
|
||||
C: { code: 'C', color: 'var(--status-info)', description: 'Copied file' },
|
||||
M: { code: 'M', color: 'var(--status-warning)', description: 'Modified file' },
|
||||
'?': { code: '?', color: 'var(--status-info)', descriptionKey: 'diffView.change.untracked' },
|
||||
A: { code: 'A', color: 'var(--status-success)', descriptionKey: 'diffView.change.new' },
|
||||
D: { code: 'D', color: 'var(--status-error)', descriptionKey: 'diffView.change.deleted' },
|
||||
R: { code: 'R', color: 'var(--status-info)', descriptionKey: 'diffView.change.renamed' },
|
||||
C: { code: 'C', color: 'var(--status-info)', descriptionKey: 'diffView.change.copied' },
|
||||
M: { code: 'M', color: 'var(--status-warning)', descriptionKey: 'diffView.change.modified' },
|
||||
};
|
||||
|
||||
const DEFAULT_CHANGE_DESCRIPTOR = CHANGE_DESCRIPTORS.M;
|
||||
|
||||
const DIFF_VIEW_MODE_OPTIONS: Array<{
|
||||
value: DiffTabViewMode;
|
||||
label: string;
|
||||
description: string;
|
||||
labelKey: I18nKey;
|
||||
descriptionKey: I18nKey;
|
||||
}> = [
|
||||
{
|
||||
value: 'single',
|
||||
label: 'Single file',
|
||||
description: 'Show one file at a time',
|
||||
labelKey: 'diffView.mode.single.label',
|
||||
descriptionKey: 'diffView.mode.single.description',
|
||||
},
|
||||
{
|
||||
value: 'stacked',
|
||||
label: 'All files',
|
||||
description: 'Stack all modified files together',
|
||||
labelKey: 'diffView.mode.stacked.label',
|
||||
descriptionKey: 'diffView.mode.stacked.description',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -205,6 +208,7 @@ const FileSelector = React.memo<FileSelectorProps>(({
|
||||
mode,
|
||||
onModeChange,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const getLabel = React.useCallback((path: string) => {
|
||||
if (!isMobile) return path;
|
||||
const lastSlash = path.lastIndexOf('/');
|
||||
@@ -226,7 +230,7 @@ const FileSelector = React.memo<FileSelectorProps>(({
|
||||
{formatDiffTotals(selectedFileEntry.insertions, selectedFileEntry.deletions)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted-foreground">Select file</span>
|
||||
<span className="text-muted-foreground">{t('diffView.selector.selectFile')}</span>
|
||||
)}
|
||||
<RiArrowDownSLine className="size-4 opacity-50" />
|
||||
</button>
|
||||
@@ -235,7 +239,7 @@ const FileSelector = React.memo<FileSelectorProps>(({
|
||||
{showModeSelector && mode && onModeChange ? (
|
||||
<>
|
||||
<DropdownMenuLabel className="typography-meta text-muted-foreground">
|
||||
View mode
|
||||
{t('diffView.selector.viewMode')}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuRadioGroup
|
||||
value={mode}
|
||||
@@ -248,7 +252,7 @@ const FileSelector = React.memo<FileSelectorProps>(({
|
||||
className="items-center"
|
||||
>
|
||||
<span className="typography-meta text-foreground">
|
||||
{option.label}
|
||||
{t(option.labelKey)}
|
||||
</span>
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
@@ -282,6 +286,7 @@ interface DiffViewModeSelectorProps {
|
||||
}
|
||||
|
||||
const DiffViewModeSelector = React.memo<DiffViewModeSelectorProps>(({ mode, onModeChange }) => {
|
||||
const { t } = useI18n();
|
||||
const currentOption =
|
||||
DIFF_VIEW_MODE_OPTIONS.find((option) => option.value === mode) ?? DIFF_VIEW_MODE_OPTIONS[0];
|
||||
|
||||
@@ -290,7 +295,7 @@ const DiffViewModeSelector = React.memo<DiffViewModeSelectorProps>(({ mode, onMo
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="flex h-7 items-center gap-2 rounded-lg border border-input bg-transparent px-2 typography-ui-label text-foreground outline-none hover:bg-interactive-hover hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring">
|
||||
<span className="min-w-0 truncate typography-meta">
|
||||
{currentOption.label}
|
||||
{t(currentOption.labelKey)}
|
||||
</span>
|
||||
<RiArrowDownSLine className="size-4 opacity-50" />
|
||||
</button>
|
||||
@@ -303,7 +308,7 @@ const DiffViewModeSelector = React.memo<DiffViewModeSelectorProps>(({ mode, onMo
|
||||
{DIFF_VIEW_MODE_OPTIONS.map((option) => (
|
||||
<DropdownMenuRadioItem key={option.value} value={option.value}>
|
||||
<span className="typography-meta text-foreground">
|
||||
{option.label}
|
||||
{t(option.labelKey)}
|
||||
</span>
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
@@ -324,6 +329,7 @@ const FileList = React.memo<FileListProps>(({
|
||||
selectedFile,
|
||||
onSelectFile,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
if (changedFiles.length === 0) return null;
|
||||
|
||||
return (
|
||||
@@ -349,8 +355,8 @@ const FileList = React.memo<FileListProps>(({
|
||||
<span
|
||||
className="typography-micro font-semibold w-4 text-center uppercase"
|
||||
style={{ color: descriptor.color }}
|
||||
title={descriptor.description}
|
||||
aria-label={descriptor.description}
|
||||
title={t(descriptor.descriptionKey)}
|
||||
aria-label={t(descriptor.descriptionKey)}
|
||||
>
|
||||
{descriptor.code}
|
||||
</span>
|
||||
@@ -385,6 +391,7 @@ const ImageDiffViewer = React.memo<ImageDiffViewerProps>(({
|
||||
isVisible,
|
||||
renderSideBySide,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const hasOriginal = diff.original.length > 0;
|
||||
const hasModified = diff.modified.length > 0;
|
||||
|
||||
@@ -406,10 +413,10 @@ const ImageDiffViewer = React.memo<ImageDiffViewerProps>(({
|
||||
<div className={containerClass}>
|
||||
{hasOriginal && (
|
||||
<div className={imageContainerClass}>
|
||||
<span className="typography-meta text-muted-foreground font-medium">Original</span>
|
||||
<span className="typography-meta text-muted-foreground font-medium">{t('diffView.image.original')}</span>
|
||||
<img
|
||||
src={diff.original}
|
||||
alt={`Original: ${filePath}`}
|
||||
alt={t('diffView.image.originalAlt', { path: filePath })}
|
||||
className={renderSideBySide ? "max-w-full max-h-[calc(100%-2rem)] object-contain" : "max-w-full object-contain"}
|
||||
style={{ imageRendering: 'auto' }}
|
||||
/>
|
||||
@@ -418,11 +425,11 @@ const ImageDiffViewer = React.memo<ImageDiffViewerProps>(({
|
||||
{hasModified && (
|
||||
<div className={imageContainerClass}>
|
||||
<span className="typography-meta text-muted-foreground font-medium">
|
||||
{hasOriginal ? 'Modified' : 'New'}
|
||||
{hasOriginal ? t('diffView.image.modified') : t('diffView.image.new')}
|
||||
</span>
|
||||
<img
|
||||
src={diff.modified}
|
||||
alt={`Modified: ${filePath}`}
|
||||
alt={t('diffView.image.modifiedAlt', { path: filePath })}
|
||||
className={renderSideBySide ? "max-w-full max-h-[calc(100%-2rem)] object-contain" : "max-w-full object-contain"}
|
||||
style={{ imageRendering: 'auto' }}
|
||||
/>
|
||||
@@ -444,6 +451,7 @@ const InlineImageDiffViewer = React.memo<InlineImageDiffViewerProps>(({
|
||||
diff,
|
||||
renderSideBySide,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const hasOriginal = diff.original.length > 0;
|
||||
const hasModified = diff.modified.length > 0;
|
||||
|
||||
@@ -460,10 +468,10 @@ const InlineImageDiffViewer = React.memo<InlineImageDiffViewerProps>(({
|
||||
<div className={containerClass}>
|
||||
{hasOriginal && (
|
||||
<div className={imageContainerClass}>
|
||||
<span className="typography-meta text-muted-foreground font-medium">Original</span>
|
||||
<span className="typography-meta text-muted-foreground font-medium">{t('diffView.image.original')}</span>
|
||||
<img
|
||||
src={diff.original}
|
||||
alt={`Original: ${filePath}`}
|
||||
alt={t('diffView.image.originalAlt', { path: filePath })}
|
||||
className={renderSideBySide ? "max-w-full max-h-[70vh] object-contain" : "max-w-full object-contain"}
|
||||
style={{ imageRendering: 'auto' }}
|
||||
/>
|
||||
@@ -472,11 +480,11 @@ const InlineImageDiffViewer = React.memo<InlineImageDiffViewerProps>(({
|
||||
{hasModified && (
|
||||
<div className={imageContainerClass}>
|
||||
<span className="typography-meta text-muted-foreground font-medium">
|
||||
{hasOriginal ? 'Modified' : 'New'}
|
||||
{hasOriginal ? t('diffView.image.modified') : t('diffView.image.new')}
|
||||
</span>
|
||||
<img
|
||||
src={diff.modified}
|
||||
alt={`Modified: ${filePath}`}
|
||||
alt={t('diffView.image.modifiedAlt', { path: filePath })}
|
||||
className={renderSideBySide ? "max-w-full max-h-[70vh] object-contain" : "max-w-full object-contain"}
|
||||
style={{ imageRendering: 'auto' }}
|
||||
/>
|
||||
@@ -632,6 +640,7 @@ const MultiFileDiffEntry = React.memo<MultiFileDiffEntryProps>(({
|
||||
isOpeningInEditor = false,
|
||||
onOpenInEditor,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const { git } = useRuntimeAPIs();
|
||||
const cachedDiff = useGitStore(
|
||||
React.useCallback((state) => {
|
||||
@@ -787,8 +796,8 @@ const MultiFileDiffEntry = React.memo<MultiFileDiffEntryProps>(({
|
||||
<span
|
||||
className="typography-micro font-semibold leading-none w-4 text-center uppercase"
|
||||
style={{ color: descriptor.color }}
|
||||
title={descriptor.description}
|
||||
aria-label={descriptor.description}
|
||||
title={t(descriptor.descriptionKey)}
|
||||
aria-label={t(descriptor.descriptionKey)}
|
||||
>
|
||||
{descriptor.code}
|
||||
</span>
|
||||
@@ -839,7 +848,7 @@ const MultiFileDiffEntry = React.memo<MultiFileDiffEntryProps>(({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 w-5 p-0 opacity-70 hover:opacity-100"
|
||||
title="Open this file in editor at change"
|
||||
title={t('diffView.actions.openFileInEditorAtChange')}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onOpenInEditor(file.path, diffData);
|
||||
@@ -936,6 +945,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
||||
pinSelectedFileHeaderToTopOnNavigate = false,
|
||||
showOpenInEditorAction = false,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const { git, files } = useRuntimeAPIs();
|
||||
const effectiveDirectory = useEffectiveDirectory();
|
||||
const { screenWidth, isMobile } = useDeviceInfo();
|
||||
@@ -1559,7 +1569,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
||||
{showFileSidebar && (
|
||||
<section className="hidden lg:flex w-72 flex-col rounded-xl border border-border/60 bg-background/70 overflow-hidden">
|
||||
<div className="flex items-center justify-between px-3 py-1.5 border-b border-border/40">
|
||||
<span className="typography-ui-header font-semibold text-foreground">Files</span>
|
||||
<span className="typography-ui-header font-semibold text-foreground">{t('diffView.section.files')}</span>
|
||||
<span className="typography-meta text-muted-foreground">{changedFiles.length}</span>
|
||||
</div>
|
||||
<FileList
|
||||
@@ -1612,7 +1622,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
||||
if (!effectiveDirectory) {
|
||||
return (
|
||||
<div className="flex flex-1 items-center justify-center text-sm text-muted-foreground">
|
||||
Select a session directory to view diffs
|
||||
{t('diffView.state.selectSessionDirectory')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1621,7 +1631,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
||||
return (
|
||||
<div className="flex flex-1 items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<RiLoader4Line size={16} className="animate-spin" />
|
||||
Loading repository status…
|
||||
{t('diffView.state.loadingRepositoryStatus')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1629,7 +1639,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
||||
if (isGitRepo === false) {
|
||||
return (
|
||||
<div className="flex flex-1 items-center justify-center text-sm text-muted-foreground">
|
||||
Not a git repository. Use the Git tab to initialize or change directories.
|
||||
{t('diffView.state.notGitRepository')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1637,7 +1647,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
||||
if (changedFiles.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-1 items-center justify-center text-sm text-muted-foreground">
|
||||
Working tree clean — no changes to display
|
||||
{t('diffView.state.cleanWorkingTree')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1654,7 +1664,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
||||
{diffLoadError ? (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="typography-ui-label font-semibold text-foreground">
|
||||
Failed to load diff
|
||||
{t('diffView.state.failedToLoadDiff')}
|
||||
</div>
|
||||
<div className="typography-meta text-muted-foreground max-w-[32rem] text-center">
|
||||
{diffLoadError}
|
||||
@@ -1667,13 +1677,13 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
||||
setDiffRetryNonce((n) => n + 1);
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
{t('diffView.actions.retry')}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<RiLoader4Line size={16} className="animate-spin" />
|
||||
Loading diff…
|
||||
{t('diffView.state.loadingDiff')}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -1690,8 +1700,10 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
||||
<RiGitCommitLine size={16} />
|
||||
<span className="typography-ui-label font-semibold text-foreground">
|
||||
{isLoadingStatus && !status
|
||||
? 'Loading changes…'
|
||||
: `${changedFiles.length} ${changedFiles.length === 1 ? 'file' : 'files'} changed`}
|
||||
? t('diffView.state.loadingChanges')
|
||||
: (changedFiles.length === 1
|
||||
? t('diffView.summary.changedFilesSingle', { count: changedFiles.length })
|
||||
: t('diffView.summary.changedFilesPlural', { count: changedFiles.length }))}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -1720,7 +1732,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
||||
'h-5 w-5 p-0 transition-opacity',
|
||||
diffWrapLines ? 'text-foreground opacity-100' : 'text-muted-foreground opacity-60 hover:opacity-100'
|
||||
)}
|
||||
title={diffWrapLines ? 'Disable line wrap' : 'Enable line wrap'}
|
||||
title={diffWrapLines ? t('diffView.actions.disableLineWrap') : t('diffView.actions.enableLineWrap')}
|
||||
>
|
||||
<RiTextWrap className="size-4" />
|
||||
</Button>
|
||||
@@ -1734,7 +1746,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
|
||||
void openSelectedFileInEditorAtChange();
|
||||
}}
|
||||
disabled={isOpeningSelectedInEditor}
|
||||
title="Open this file at first changed line"
|
||||
title={t('diffView.actions.openFileAtFirstChangedLine')}
|
||||
>
|
||||
{isOpeningSelectedInEditor ? (
|
||||
<RiLoader4Line className="size-3.5 animate-spin" />
|
||||
|
||||
Reference in New Issue
Block a user