From 70d81598b03dfb28c470a7a7c2df6fc1632084ac Mon Sep 17 00:00:00 2001 From: Koji Miyazawa Date: Tue, 27 Oct 2026 20:56:46 +0900 Subject: [PATCH] fix(i18n): localize sidebar copy and correct Japanese mistranslations (#28921) Route hardcoded sidebar strings (cache timer, delete-worktree dialog, imported-worktrees line, remote file browser, setup-script prompt, card agents/ports/status, add-repo busy labels, batch action menus) through i18n keys, and fix literal-sense Japanese translations in ja.json. Co-authored-by: Koji Miyazawa <24467062+kojikojikojikoji@users.noreply.github.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> --- .../src/components/sidebar/CacheTimer.tsx | 12 +- .../sidebar/DeleteWorktreeDialogFooter.tsx | 22 +++- .../sidebar/DeleteWorktreeDirtyChangeHint.tsx | 17 ++- .../ImportedWorktreesVisibilityLine.tsx | 72 ++++++++--- .../components/sidebar/PendingWorktreeRow.tsx | 5 +- .../components/sidebar/RemoteFileBrowser.tsx | 6 +- .../sidebar/RemoteFileBrowserEntryList.tsx | 11 +- .../sidebar/SetupScriptPromptCard.tsx | 17 ++- .../sidebar/WorkspaceKanbanStatusLane.tsx | 6 +- .../components/sidebar/WorktreeCardAgents.tsx | 10 +- .../components/sidebar/WorktreeCardPorts.tsx | 6 +- .../sidebar/WorktreeCardStatusSlot.tsx | 4 +- .../sidebar/add-repo-busy-labels.ts | 15 +++ .../imported-worktrees-card-actions.test.ts | 12 +- .../imported-worktrees-card-actions.ts | 23 +++- .../use-worktree-card-linked-details.ts | 26 +++- .../use-worktree-card-workspace-actions.ts | 4 +- .../use-worktree-context-menu-model.tsx | 14 +- .../sidebar/useAddRepoLocalFolderFlow.ts | 7 +- .../sidebar/useAddRepoServerPathFlow.ts | 11 +- .../sidebar/workspace-batch-action-labels.ts | 49 +++++++ .../listing/use-external-worktree-cards.ts | 6 +- .../rows/repo-header-project-actions.tsx | 10 +- .../src/i18n/en-runtime-required.json | 22 +++- src/renderer/src/i18n/locales/en.json | 87 +++++++++++-- src/renderer/src/i18n/locales/es.json | 5 +- src/renderer/src/i18n/locales/fr.json | 5 +- src/renderer/src/i18n/locales/ja.json | 121 +++++++++++++----- src/renderer/src/i18n/locales/ko.json | 5 +- src/renderer/src/i18n/locales/pt.json | 5 +- src/renderer/src/i18n/locales/zh.json | 5 +- 31 files changed, 492 insertions(+), 128 deletions(-) create mode 100644 src/renderer/src/components/sidebar/add-repo-busy-labels.ts create mode 100644 src/renderer/src/components/sidebar/workspace-batch-action-labels.ts diff --git a/src/renderer/src/components/sidebar/CacheTimer.tsx b/src/renderer/src/components/sidebar/CacheTimer.tsx index 6702e330421..69892f9da07 100644 --- a/src/renderer/src/components/sidebar/CacheTimer.tsx +++ b/src/renderer/src/components/sidebar/CacheTimer.tsx @@ -9,6 +9,7 @@ import { getPromptCacheCountdownForPane, type PromptCacheCountdownSelection } from './prompt-cache-timer-selection' +import { translate } from '@/i18n/i18n' /** Resolve the soonest-expiring Claude cache once for both row visibility and countdown rendering. */ export function usePromptCacheCountdownStartedAt(worktreeId: string, active = true): number | null { @@ -70,8 +71,15 @@ export default function CacheTimer({ const warning = !expired && remainingMs <= 60_000 const tooltipText = expired - ? 'The next message will re-send the full context as uncached tokens' - : `Prompt cache expires in ${label}` + ? translate( + 'auto.components.sidebar.CacheTimer.expiredTooltip', + 'The next message will re-send the full context as uncached tokens' + ) + : translate( + 'auto.components.sidebar.CacheTimer.expiresIn', + 'Prompt cache expires in {{time}}', + { time: label } + ) return ( diff --git a/src/renderer/src/components/sidebar/DeleteWorktreeDialogFooter.tsx b/src/renderer/src/components/sidebar/DeleteWorktreeDialogFooter.tsx index dce8257956d..19579b70ec6 100644 --- a/src/renderer/src/components/sidebar/DeleteWorktreeDialogFooter.tsx +++ b/src/renderer/src/components/sidebar/DeleteWorktreeDialogFooter.tsx @@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button' import { translate } from '@/i18n/i18n' import { useAppStore } from '@/store' import { WorktreeForceDeleteButton } from './WorktreeForceDeleteButton' +import { deleteWorkspacesCountLabel } from './workspace-batch-action-labels' export function DeleteWorktreeDialogFooter({ isMainWorktree, @@ -34,15 +35,24 @@ export function DeleteWorktreeDialogFooter({ }): JSX.Element { const label = isDeleting ? canForceDelete - ? 'Force Deleting...' - : 'Deleting...' + ? translate( + 'auto.components.sidebar.DeleteWorktreeDialogFooter.forceDeleting', + 'Force Deleting...' + ) + : translate('auto.components.sidebar.DeleteWorktreeDialogFooter.deleting', 'Deleting...') : isBatchDelete - ? `Delete ${worktreeCount} Workspaces` + ? deleteWorkspacesCountLabel(worktreeCount) : canDeleteAllLineage - ? `Delete ${lineageDeleteTargetCount} Workspaces` + ? deleteWorkspacesCountLabel(lineageDeleteTargetCount) : canForceDelete - ? 'Force Delete' - : 'Delete Workspace' + ? translate( + 'auto.components.sidebar.DeleteWorktreeDialogFooter.forceDelete', + 'Force Delete' + ) + : translate( + 'auto.components.sidebar.DeleteWorktreeDialogFooter.deleteWorkspace', + 'Delete Workspace' + ) return ( <> diff --git a/src/renderer/src/components/sidebar/DeleteWorktreeDirtyChangeHint.tsx b/src/renderer/src/components/sidebar/DeleteWorktreeDirtyChangeHint.tsx index 75deea5df63..6b5b590bbc0 100644 --- a/src/renderer/src/components/sidebar/DeleteWorktreeDirtyChangeHint.tsx +++ b/src/renderer/src/components/sidebar/DeleteWorktreeDirtyChangeHint.tsx @@ -42,8 +42,21 @@ export function DeleteWorktreeDirtyChangeHint({ const label = changeCount > 0 - ? `${changeCount} uncommitted or untracked ${changeCount === 1 ? 'change' : 'changes'}` - : 'Uncommitted or untracked changes' + ? changeCount === 1 + ? translate( + 'auto.components.sidebar.DeleteWorktreeDirtyChangeHint.changeCount_one', + '1 uncommitted or untracked change', + { count: changeCount } + ) + : translate( + 'auto.components.sidebar.DeleteWorktreeDirtyChangeHint.changeCount_other', + '{{count}} uncommitted or untracked changes', + { count: changeCount } + ) + : translate( + 'auto.components.sidebar.DeleteWorktreeDirtyChangeHint.changes', + 'Uncommitted or untracked changes' + ) const warningLabel = ( <> diff --git a/src/renderer/src/components/sidebar/ImportedWorktreesVisibilityLine.tsx b/src/renderer/src/components/sidebar/ImportedWorktreesVisibilityLine.tsx index f548e10f722..e1420a974cb 100644 --- a/src/renderer/src/components/sidebar/ImportedWorktreesVisibilityLine.tsx +++ b/src/renderer/src/components/sidebar/ImportedWorktreesVisibilityLine.tsx @@ -34,7 +34,6 @@ type ImportedWorktreesVisibilityLineProps = { } const PREVIEW_LIMIT = 3 -const KEEP_HIDDEN_LABEL = 'Keep hidden - recover from the project menu' const GROUP_LIMIT = 5 type ImportedWorktreePathGroup = { @@ -42,10 +41,6 @@ type ImportedWorktreePathGroup = { worktrees: ImportedWorktreeVisibilityPreview[] } -function pluralizeWorktree(count: number): string { - return count === 1 ? 'worktree' : 'worktrees' -} - function getWorktreeKey( worktree: ImportedWorktreeVisibilityPreview, index: number, @@ -92,15 +87,29 @@ export default function ImportedWorktreesVisibilityLine({ const [isExpanded, setIsExpanded] = useState(false) const [expandedGroupPathKeys, setExpandedGroupPathKeys] = useState>(new Set()) const hiddenCount = hiddenWorktrees.length - const worktreeNoun = pluralizeWorktree(hiddenCount) const worktreeGroups = groupWorktreesByParentPath(hiddenWorktrees) const visibleWorktreeGroups = worktreeGroups.slice(0, GROUP_LIMIT) const remainingGroupCount = Math.max(0, worktreeGroups.length - visibleWorktreeGroups.length) // Host labels distinguish otherwise identical project notice rows. const repoScopeLabel = hostContextLabel - ? `${repoDisplayName} on ${hostContextLabel}` + ? translate( + 'auto.components.sidebar.ImportedWorktreesVisibilityLine.repoOnHost', + '{{repo}} on {{host}}', + { repo: repoDisplayName, host: hostContextLabel } + ) : repoDisplayName - const keepHiddenAriaLabel = `Keep ${hiddenCount} discovered ${worktreeNoun} hidden for ${repoScopeLabel}; recover from the project menu` + const keepHiddenAriaLabel = + hiddenCount === 1 + ? translate( + 'auto.components.sidebar.ImportedWorktreesVisibilityLine.keepHiddenAriaLabel_one', + 'Keep 1 discovered worktree hidden for {{repo}}; recover from the project menu', + { count: hiddenCount, repo: repoScopeLabel } + ) + : translate( + 'auto.components.sidebar.ImportedWorktreesVisibilityLine.keepHiddenAriaLabel_other', + 'Keep {{count}} discovered worktrees hidden for {{repo}}; recover from the project menu', + { count: hiddenCount, repo: repoScopeLabel } + ) if (hiddenCount === 0) { return null @@ -108,8 +117,28 @@ export default function ImportedWorktreesVisibilityLine({ const lineText = placement === 'pinned-fallback' - ? `Hiding ${hiddenCount} discovered ${worktreeNoun} in ${repoScopeLabel}` - : `Hiding ${hiddenCount} discovered ${worktreeNoun}` + ? hiddenCount === 1 + ? translate( + 'auto.components.sidebar.ImportedWorktreesVisibilityLine.hidingInRepo_one', + 'Hiding 1 discovered worktree in {{repo}}', + { count: hiddenCount, repo: repoScopeLabel } + ) + : translate( + 'auto.components.sidebar.ImportedWorktreesVisibilityLine.hidingInRepo_other', + 'Hiding {{count}} discovered worktrees in {{repo}}', + { count: hiddenCount, repo: repoScopeLabel } + ) + : hiddenCount === 1 + ? translate( + 'auto.components.sidebar.ImportedWorktreesVisibilityLine.hiding_one', + 'Hiding 1 discovered worktree', + { count: hiddenCount } + ) + : translate( + 'auto.components.sidebar.ImportedWorktreesVisibilityLine.hiding_other', + 'Hiding {{count}} discovered worktrees', + { count: hiddenCount } + ) const toggleGroupExpanded = (path: string): void => { const key = normalizeRuntimePathForComparison(path) @@ -141,11 +170,19 @@ export default function ImportedWorktreesVisibilityLine({ size="icon-xs" disabled={pending} aria-expanded={isExpanded} - aria-label={translate( - 'auto.components.sidebar.ImportedWorktreesVisibilityLine.f54f2bec5d', - '{{value0}} hidden worktrees for {{value1}}', - { value0: isExpanded ? 'Collapse' : 'Expand', value1: repoScopeLabel } - )} + aria-label={ + isExpanded + ? translate( + 'auto.components.sidebar.ImportedWorktreesVisibilityLine.collapseAriaLabel', + 'Collapse hidden worktrees for {{repo}}', + { repo: repoScopeLabel } + ) + : translate( + 'auto.components.sidebar.ImportedWorktreesVisibilityLine.expandAriaLabel', + 'Expand hidden worktrees for {{repo}}', + { repo: repoScopeLabel } + ) + } onClick={() => setIsExpanded((value) => !value)} className="shrink-0 rounded-[4px] text-muted-foreground hover:bg-worktree-sidebar-accent hover:text-worktree-sidebar-accent-foreground" > @@ -185,7 +222,10 @@ export default function ImportedWorktreesVisibilityLine({ - {KEEP_HIDDEN_LABEL} + {translate( + 'auto.components.sidebar.ImportedWorktreesVisibilityLine.keepHiddenTooltip', + 'Keep hidden - recover from the project menu' + )} ) : null} diff --git a/src/renderer/src/components/sidebar/PendingWorktreeRow.tsx b/src/renderer/src/components/sidebar/PendingWorktreeRow.tsx index 896ae3d1e80..f3674aca6c1 100644 --- a/src/renderer/src/components/sidebar/PendingWorktreeRow.tsx +++ b/src/renderer/src/components/sidebar/PendingWorktreeRow.tsx @@ -10,7 +10,10 @@ import { translate } from '@/i18n/i18n' function statusLabel(entry: PendingWorktreeCreation): string { if (entry.status === 'error') { - return entry.error ?? 'Creation failed' + return ( + entry.error ?? + translate('auto.components.sidebar.PendingWorktreeRow.creationFailed', 'Creation failed') + ) } return getCreationProgressLabel(entry) } diff --git a/src/renderer/src/components/sidebar/RemoteFileBrowser.tsx b/src/renderer/src/components/sidebar/RemoteFileBrowser.tsx index 07be1d9a3c1..e880a581205 100644 --- a/src/renderer/src/components/sidebar/RemoteFileBrowser.tsx +++ b/src/renderer/src/components/sidebar/RemoteFileBrowser.tsx @@ -27,7 +27,6 @@ type RemoteFileBrowserProps = ( const FILE_HINT_MS = 2000 const NO_ENTRIES: readonly DirEntry[] = [] -const FILE_HINT_TEXT = "Files can't be opened as a project" export function RemoteFileBrowser({ targetId, @@ -345,7 +344,10 @@ export function RemoteFileBrowser({ title={fileHint ? undefined : resolvedPath} > {fileHint - ? FILE_HINT_TEXT + ? translate( + 'auto.components.sidebar.RemoteFileBrowser.fileHint', + "Files can't be opened as a project" + ) : translate( 'auto.components.sidebar.RemoteFileBrowser.971d85cc84', 'Opens as a project on this host · {{value0}}', diff --git a/src/renderer/src/components/sidebar/RemoteFileBrowserEntryList.tsx b/src/renderer/src/components/sidebar/RemoteFileBrowserEntryList.tsx index e5598091080..b44e47cccca 100644 --- a/src/renderer/src/components/sidebar/RemoteFileBrowserEntryList.tsx +++ b/src/renderer/src/components/sidebar/RemoteFileBrowserEntryList.tsx @@ -50,9 +50,6 @@ export function RemoteFileBrowserEntryList({ ?.scrollIntoView?.({ block: 'nearest' }) }, [activeIndex, displayEntries]) - const displayEmptyDirCopy = isPreviewActive - ? `${preview!.resolvedPath} is empty` - : 'Empty directory' const noMatchesFilter = isPreviewActive ? preview!.filter : filter const displayNoMatchesCopy = isRemoteFileBrowserPathResolveTextTooLarge(noMatchesFilter) ? translate( @@ -90,7 +87,13 @@ export function RemoteFileBrowserEntryList({ !preview!.error && !preview!.loading ? (
-

{displayEmptyDirCopy}

+

+ {translate( + 'auto.components.sidebar.RemoteFileBrowserEntryList.pathEmpty', + '{{path}} is empty', + { path: preview!.resolvedPath } + )} +

) : !isPreviewActive && entries.length === 0 ? (
diff --git a/src/renderer/src/components/sidebar/SetupScriptPromptCard.tsx b/src/renderer/src/components/sidebar/SetupScriptPromptCard.tsx index 65a0e1a112a..1285739c8ce 100644 --- a/src/renderer/src/components/sidebar/SetupScriptPromptCard.tsx +++ b/src/renderer/src/components/sidebar/SetupScriptPromptCard.tsx @@ -267,8 +267,21 @@ function SetupScriptPromptCard(): React.JSX.Element | null { onOpenSettings: () => openLocalCommandSettings(importedRepoId, importedHostId), description: skippedCount > 0 - ? `${skippedCount} unsupported field${skippedCount === 1 ? '' : 's'} skipped. Saved the setup command.` - : 'Saved the setup command.' + ? skippedCount === 1 + ? translate( + 'auto.components.sidebar.SetupScriptPromptCard.savedWithSkippedFields_one', + '1 unsupported field skipped. Saved the setup command.', + { count: skippedCount } + ) + : translate( + 'auto.components.sidebar.SetupScriptPromptCard.savedWithSkippedFields_other', + '{{count}} unsupported fields skipped. Saved the setup command.', + { count: skippedCount } + ) + : translate( + 'auto.components.sidebar.SetupScriptPromptCard.savedSetupCommand', + 'Saved the setup command.' + ) }) } } catch (error) { diff --git a/src/renderer/src/components/sidebar/WorkspaceKanbanStatusLane.tsx b/src/renderer/src/components/sidebar/WorkspaceKanbanStatusLane.tsx index d9cf6164944..6e169700221 100644 --- a/src/renderer/src/components/sidebar/WorkspaceKanbanStatusLane.tsx +++ b/src/renderer/src/components/sidebar/WorkspaceKanbanStatusLane.tsx @@ -90,7 +90,11 @@ function WorkspaceKanbanStatusLane({ undefined ) }, [fullWorktreeIds, hasQuery, items]) - const createTooltip = `New workspace in ${status.label}` + const createTooltip = translate( + 'auto.components.sidebar.WorkspaceKanbanStatusLane.newWorkspaceIn', + 'New workspace in {{status}}', + { status: status.label } + ) const createButton = (