diff --git a/src/renderer/src/components/github/CommentReactions.tsx b/src/renderer/src/components/github/CommentReactions.tsx index fc9027e89ca..600ae8e973e 100644 --- a/src/renderer/src/components/github/CommentReactions.tsx +++ b/src/renderer/src/components/github/CommentReactions.tsx @@ -30,6 +30,22 @@ const REACTION_LABEL: Record = { eyes: 'eyes' } +// Why whole sentences per count: the plural ending used to travel as an +// interpolation value, which glued an English "s" onto translated nouns. +function reactionCountLabel(count: number, reaction: string): string { + return count === 1 + ? translate( + 'auto.components.GitHubItemDialog.reactionCountSingular', + '{{count}} {{reaction}} reaction', + { count, reaction } + ) + : translate( + 'auto.components.GitHubItemDialog.reactionCountPlural', + '{{count}} {{reaction}} reactions', + { count, reaction } + ) +} + export function CommentReactions({ reactions, className, @@ -97,15 +113,7 @@ export function CommentReactions({ reaction.viewerHasReacted && 'border-ring bg-accent text-accent-foreground' )} aria-pressed={Boolean(reaction.viewerHasReacted)} - aria-label={translate( - 'auto.components.GitHubItemDialog.a18f669c7a', - '{{value0}} {{value1}} reaction{{value2}}', - { - value0: reaction.count, - value1: REACTION_LABEL[reaction.content], - value2: reaction.count === 1 ? '' : 's' - } - )} + aria-label={reactionCountLabel(reaction.count, REACTION_LABEL[reaction.content])} onClick={() => void changeReaction( reaction.content, @@ -121,15 +129,7 @@ export function CommentReactions({ ) : ( {reaction.count} diff --git a/src/renderer/src/components/gitlab-item-dialog/gitlab-description-tab.tsx b/src/renderer/src/components/gitlab-item-dialog/gitlab-description-tab.tsx index 4258b323ca4..807032c8563 100644 --- a/src/renderer/src/components/gitlab-item-dialog/gitlab-description-tab.tsx +++ b/src/renderer/src/components/gitlab-item-dialog/gitlab-description-tab.tsx @@ -73,14 +73,17 @@ export function GitLabDescriptionTab({ item, state, detailsEditing, reviewAction
{approvalState.approvalsLeft === 0 ? translate('auto.components.GitLabItemDialog.22511537d2', 'Approved') - : translate( - 'auto.components.GitLabItemDialog.40c56b95e2', - '{{value0}} approval{{value1}} remaining', - { - value0: approvalState.approvalsLeft ?? 0, - value1: approvalState.approvalsLeft === 1 ? '' : 's' - } - )} + : approvalState.approvalsLeft === 1 + ? translate( + 'auto.components.GitLabItemDialog.approvalsRemainingSingular', + '{{count}} approval remaining', + { count: approvalState.approvalsLeft } + ) + : translate( + 'auto.components.GitLabItemDialog.approvalsRemainingPlural', + '{{count}} approvals remaining', + { count: approvalState.approvalsLeft ?? 0 } + )} {typeof approvalState.approvalsRequired === 'number' ? translate( 'auto.components.GitLabItemDialog.00f3bab87b', diff --git a/src/renderer/src/components/onboarding/IntegrationsStep.tsx b/src/renderer/src/components/onboarding/IntegrationsStep.tsx index c65e68099f8..05995f8aa29 100644 --- a/src/renderer/src/components/onboarding/IntegrationsStep.tsx +++ b/src/renderer/src/components/onboarding/IntegrationsStep.tsx @@ -169,11 +169,17 @@ export function LinearRow(props: { compact?: boolean } = {}): React.JSX.Element

{linearStatus.connected - ? translate( - 'auto.components.onboarding.IntegrationsStep.b08a6ac93c', - '{{value0}} workspace{{value1}} linked. Add another workspace or replace a restricted key any time.', - { value0: workspaceCount, value1: workspaceCount === 1 ? '' : 's' } - ) + ? workspaceCount === 1 + ? translate( + 'auto.components.onboarding.IntegrationsStep.linearWorkspacesLinkedSingular', + '{{count}} workspace linked. Add another workspace or replace a restricted key any time.', + { count: workspaceCount } + ) + : translate( + 'auto.components.onboarding.IntegrationsStep.linearWorkspacesLinkedPlural', + '{{count}} workspaces linked. Add another workspace or replace a restricted key any time.', + { count: workspaceCount } + ) : translate( 'auto.components.onboarding.IntegrationsStep.4983ae7433', 'Add Linear access with a Personal API key. Full-access keys can show every team the key owner can access.' diff --git a/src/renderer/src/components/right-sidebar/source-control/commit/use-discard-confirmation.ts b/src/renderer/src/components/right-sidebar/source-control/commit/use-discard-confirmation.ts index d3f164b2f34..56d83937fa8 100644 --- a/src/renderer/src/components/right-sidebar/source-control/commit/use-discard-confirmation.ts +++ b/src/renderer/src/components/right-sidebar/source-control/commit/use-discard-confirmation.ts @@ -126,11 +126,17 @@ export function useSourceControlDiscardConfirmation({ const sample = result.failed.slice(0, 3).join(', ') const more = result.failed.length > 3 ? `, +${result.failed.length - 3} more` : '' toast.error( - translate( - 'auto.components.right.sidebar.SourceControl.8eb3782a0c', - 'Failed to discard {{value0}} file{{value1}}', - { value0: result.failed.length, value1: result.failed.length === 1 ? '' : 's' } - ), + result.failed.length === 1 + ? translate( + 'auto.components.right.sidebar.SourceControl.discardFailedSingular', + 'Failed to discard {{count}} file', + { count: result.failed.length } + ) + : translate( + 'auto.components.right.sidebar.SourceControl.discardFailedPlural', + 'Failed to discard {{count}} files', + { count: result.failed.length } + ), { description: firstMsg ? translate( diff --git a/src/renderer/src/components/right-sidebar/source-control/listing/branch-entry-row.tsx b/src/renderer/src/components/right-sidebar/source-control/listing/branch-entry-row.tsx index 68fd0a91223..c7c1d433411 100644 --- a/src/renderer/src/components/right-sidebar/source-control/listing/branch-entry-row.tsx +++ b/src/renderer/src/components/right-sidebar/source-control/listing/branch-entry-row.tsx @@ -4,9 +4,9 @@ import { getFileTypeIcon } from '@/lib/file-type-icons' import { basename, dirname, joinPath } from '@/lib/path' import { WORKSPACE_FILE_PATH_MIME } from '@/lib/workspace-file-drag' import { writeWorkspaceFileDragSourceForWorkspace } from '@/lib/workspace-file-drag-source' -import { translate } from '@/i18n/i18n' import type { GitBranchChangeEntry } from '../../../../../../shared/git-diff-compare-types' import { DiffLineCounts } from './diff-line-counts' +import { reviewNoteCountTitle } from './review-note-count-title' import { SourceControlEntryContextMenu } from './entry-context-menu' import { toPermanentSourceControlRowOpenEvent, type SourceControlRowOpenEvent } from './split-open' import { SOURCE_CONTROL_TREE_FILE_PADDING_PX, SOURCE_CONTROL_TREE_INDENT_PX } from './row-layout' @@ -76,11 +76,7 @@ export function BranchEntryRow({ {commentCount > 0 && ( {commentCount} diff --git a/src/renderer/src/components/right-sidebar/source-control/listing/review-note-count-title.test.ts b/src/renderer/src/components/right-sidebar/source-control/listing/review-note-count-title.test.ts new file mode 100644 index 00000000000..da3f5235a9e --- /dev/null +++ b/src/renderer/src/components/right-sidebar/source-control/listing/review-note-count-title.test.ts @@ -0,0 +1,37 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { reviewNoteCountTitle } from './review-note-count-title' + +const mocks = vi.hoisted(() => ({ + translate: vi.fn((key: string, fallback: string) => `${key}:${fallback}`) +})) + +vi.mock('@/i18n/i18n', () => ({ + translate: mocks.translate +})) + +describe('reviewNoteCountTitle', () => { + beforeEach(() => { + mocks.translate.mockClear() + }) + + it('uses the singular sentence only for exactly one note', () => { + expect(reviewNoteCountTitle(1)).toBe( + 'auto.components.right.sidebar.SourceControl.noteCountSingular:{{count}} note' + ) + expect(reviewNoteCountTitle(4)).toBe( + 'auto.components.right.sidebar.SourceControl.noteCountPlural:{{count}} notes' + ) + expect(mocks.translate).toHaveBeenNthCalledWith( + 1, + 'auto.components.right.sidebar.SourceControl.noteCountSingular', + '{{count}} note', + { count: 1 } + ) + expect(mocks.translate).toHaveBeenNthCalledWith( + 2, + 'auto.components.right.sidebar.SourceControl.noteCountPlural', + '{{count}} notes', + { count: 4 } + ) + }) +}) diff --git a/src/renderer/src/components/right-sidebar/source-control/listing/review-note-count-title.ts b/src/renderer/src/components/right-sidebar/source-control/listing/review-note-count-title.ts new file mode 100644 index 00000000000..3442cb17a1c --- /dev/null +++ b/src/renderer/src/components/right-sidebar/source-control/listing/review-note-count-title.ts @@ -0,0 +1,13 @@ +import { translate } from '@/i18n/i18n' + +// Why whole sentences per count: the plural ending used to travel as an +// interpolation value, which glued an English "s" onto translated nouns. +export function reviewNoteCountTitle(count: number): string { + return count === 1 + ? translate('auto.components.right.sidebar.SourceControl.noteCountSingular', '{{count}} note', { + count + }) + : translate('auto.components.right.sidebar.SourceControl.noteCountPlural', '{{count}} notes', { + count + }) +} diff --git a/src/renderer/src/components/right-sidebar/source-control/listing/uncommitted-entry-row.tsx b/src/renderer/src/components/right-sidebar/source-control/listing/uncommitted-entry-row.tsx index d733b37e7cd..8e1c22e2256 100644 --- a/src/renderer/src/components/right-sidebar/source-control/listing/uncommitted-entry-row.tsx +++ b/src/renderer/src/components/right-sidebar/source-control/listing/uncommitted-entry-row.tsx @@ -11,6 +11,7 @@ import { ActionButton } from './action-button' import { ConflictBadge } from './conflict-badge' import { getLocalizedConflictKindLabel } from './conflict-label' import { DiffLineCounts } from './diff-line-counts' +import { reviewNoteCountTitle } from './review-note-count-title' import { SourceControlEntryContextMenu } from './entry-context-menu' import { canDiscardStatusEntry, canStageStatusEntry, canUnstageStatusEntry } from './entry-actions' import { isSubmoduleWorktreeOnlyChange } from '../commit/discard-all-sequence' @@ -185,11 +186,7 @@ export const UncommittedEntryRow = React.memo(function UncommittedEntryRow({ // Why: surface a per-row marker so files with review notes are visible without opening the Notes tab. {commentCount} diff --git a/src/renderer/src/components/right-sidebar/source-control/sync/compare-summary.tsx b/src/renderer/src/components/right-sidebar/source-control/sync/compare-summary.tsx index 93fd343a624..8e59e012d9f 100644 --- a/src/renderer/src/components/right-sidebar/source-control/sync/compare-summary.tsx +++ b/src/renderer/src/components/right-sidebar/source-control/sync/compare-summary.tsx @@ -110,13 +110,19 @@ export function CompareSummary({ const commitsAhead = summary.commitsAhead const showCommitsAhead = typeof commitsAhead === 'number' && commitsAhead > 0 - const commitsAheadTitle = showCommitsAhead - ? translate( - 'auto.components.right.sidebar.source.control.compare.summary.dd72a6fd37', - '{{value0}} commit{{value1}} ahead of {{value2}}', - { value0: commitsAhead, value1: commitsAhead === 1 ? '' : 's', value2: summary.baseRef } - ) - : undefined + const commitsAheadTitle = !showCommitsAhead + ? undefined + : commitsAhead === 1 + ? translate( + 'auto.components.right.sidebar.source.control.compare.summary.commitsAheadSingular', + '{{count}} commit ahead of {{ref}}', + { count: commitsAhead, ref: summary.baseRef } + ) + : translate( + 'auto.components.right.sidebar.source.control.compare.summary.commitsAheadPlural', + '{{count}} commits ahead of {{ref}}', + { count: commitsAhead, ref: summary.baseRef } + ) if (!showCommitsAhead) { return null diff --git a/src/renderer/src/components/sidebar/add-remote-host-ssh-actions.ts b/src/renderer/src/components/sidebar/add-remote-host-ssh-actions.ts index 76ac27ab2f9..072773f8e3b 100644 --- a/src/renderer/src/components/sidebar/add-remote-host-ssh-actions.ts +++ b/src/renderer/src/components/sidebar/add-remote-host-ssh-actions.ts @@ -183,11 +183,17 @@ export async function addAllSshConfigHostsToOrca({ return { kind: 'already-synced' } } toast.success( - translate( - 'auto.components.sidebar.AddRemoteHostDialog.sshImportSynced', - 'Added {{value0}} host{{value1}} to Orca.', - { value0: result.targets.length, value1: result.targets.length > 1 ? 's' : '' } - ) + result.targets.length > 1 + ? translate( + 'auto.components.sidebar.AddRemoteHostDialog.sshImportSyncedPlural', + 'Added {{count}} hosts to Orca.', + { count: result.targets.length } + ) + : translate( + 'auto.components.sidebar.AddRemoteHostDialog.sshImportSyncedSingular', + 'Added {{count}} host to Orca.', + { count: result.targets.length } + ) ) return { kind: 'added', count: result.targets.length } } catch (error) { diff --git a/src/renderer/src/components/sidebar/worktree-list/rows/use-project-group-dialogs.ts b/src/renderer/src/components/sidebar/worktree-list/rows/use-project-group-dialogs.ts index c9c3a87b2ac..ae0d8f68dcf 100644 --- a/src/renderer/src/components/sidebar/worktree-list/rows/use-project-group-dialogs.ts +++ b/src/renderer/src/components/sidebar/worktree-list/rows/use-project-group-dialogs.ts @@ -50,15 +50,18 @@ function reportProjectGroupDeleteFailures(result: { 'Some projects could not be removed from Orca' ), { - description: translate( - 'auto.components.sidebar.WorktreeList.f94466bc39', - '{{value0}} of {{value1}} contained project{{value2}} remained after deleting the group.', - { - value0: result.failedProjectRemovals.length, - value1: requestedCount, - value2: requestedCount === 1 ? '' : 's' - } - ) + description: + requestedCount === 1 + ? translate( + 'auto.components.sidebar.WorktreeList.groupProjectsRemainedSingular', + '{{remaining}} of {{count}} contained project remained after deleting the group.', + { remaining: result.failedProjectRemovals.length, count: requestedCount } + ) + : translate( + 'auto.components.sidebar.WorktreeList.groupProjectsRemainedPlural', + '{{remaining}} of {{count}} contained projects remained after deleting the group.', + { remaining: result.failedProjectRemovals.length, count: requestedCount } + ) } ) } diff --git a/src/renderer/src/components/task-page/linear/IssueAttributes.tsx b/src/renderer/src/components/task-page/linear/IssueAttributes.tsx index 21ec2584b0b..11182ecba6d 100644 --- a/src/renderer/src/components/task-page/linear/IssueAttributes.tsx +++ b/src/renderer/src/components/task-page/linear/IssueAttributes.tsx @@ -1,6 +1,7 @@ import type { TaskPageComposerActionsModel } from '../../use-task-page-composer-actions' import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover' import { translate } from '@/i18n/i18n' +import { linearLabelCountLabel } from './linear-attribute-count-labels' import { ChevronDown, LoaderCircle, Check, UserRound, FolderKanban, Tag } from 'lucide-react' import { LinearPriorityIcon } from '@/components/linear-priority-icon' export function TaskPageLinearIssueAttributes({ @@ -315,10 +316,7 @@ export function TaskPageLinearIssueAttributes({ {newLinearIssueLabelIds.length === 0 ? translate('auto.components.TaskPage.d0ca4aa1d0', 'Labels') - : translate('auto.components.TaskPage.eff9800d4b', '{{value0}} label{{value1}}', { - value0: newLinearIssueLabelIds.length, - value1: newLinearIssueLabelIds.length > 1 ? 's' : '' - })} + : linearLabelCountLabel(newLinearIssueLabelIds.length)} diff --git a/src/renderer/src/components/task-page/linear/ProjectFields.tsx b/src/renderer/src/components/task-page/linear/ProjectFields.tsx index 20683773123..6f2ac4e10db 100644 --- a/src/renderer/src/components/task-page/linear/ProjectFields.tsx +++ b/src/renderer/src/components/task-page/linear/ProjectFields.tsx @@ -1,6 +1,7 @@ import { ImeInput } from '@/lib/ime-text-field' import type { TaskPageComposerActionsModel } from '../../use-task-page-composer-actions' import { translate } from '@/i18n/i18n' +import { linearLabelCountLabel, linearMemberCountLabel } from './linear-attribute-count-labels' import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover' import { LinearPriorityIcon } from '@/components/linear-priority-icon' import { getLinearPriorityLabel } from '@/components/task-page-localized-options' @@ -188,14 +189,7 @@ export function TaskPageLinearProjectFields({ {newLinearProjectMemberIds.length === 0 ? translate('auto.components.TaskPage.d6cda23ef1', 'Members') - : translate( - 'auto.components.TaskPage.7719d8daa9', - '{{value0}} member{{value1}}', - { - value0: newLinearProjectMemberIds.length, - value1: newLinearProjectMemberIds.length > 1 ? 's' : '' - } - )} + : linearMemberCountLabel(newLinearProjectMemberIds.length)} @@ -260,10 +254,7 @@ export function TaskPageLinearProjectFields({ {newLinearProjectLabelIds.length === 0 ? translate('auto.components.TaskPage.d0ca4aa1d0', 'Labels') - : translate('auto.components.TaskPage.eff9800d4b', '{{value0}} label{{value1}}', { - value0: newLinearProjectLabelIds.length, - value1: newLinearProjectLabelIds.length > 1 ? 's' : '' - })} + : linearLabelCountLabel(newLinearProjectLabelIds.length)} diff --git a/src/renderer/src/components/task-page/linear/linear-attribute-count-labels.test.ts b/src/renderer/src/components/task-page/linear/linear-attribute-count-labels.test.ts new file mode 100644 index 00000000000..460c720d2af --- /dev/null +++ b/src/renderer/src/components/task-page/linear/linear-attribute-count-labels.test.ts @@ -0,0 +1,58 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { linearLabelCountLabel, linearMemberCountLabel } from './linear-attribute-count-labels' + +const mocks = vi.hoisted(() => ({ + translate: vi.fn((key: string, fallback: string) => `${key}:${fallback}`) +})) + +vi.mock('@/i18n/i18n', () => ({ + translate: mocks.translate +})) + +describe('Linear attribute count labels', () => { + beforeEach(() => { + mocks.translate.mockClear() + }) + + it('uses the singular sentence for one label and the plural one above that', () => { + expect(linearLabelCountLabel(1)).toBe( + 'auto.components.TaskPage.labelCountSingular:{{count}} label' + ) + expect(linearLabelCountLabel(3)).toBe( + 'auto.components.TaskPage.labelCountPlural:{{count}} labels' + ) + expect(mocks.translate).toHaveBeenNthCalledWith( + 1, + 'auto.components.TaskPage.labelCountSingular', + '{{count}} label', + { count: 1 } + ) + expect(mocks.translate).toHaveBeenNthCalledWith( + 2, + 'auto.components.TaskPage.labelCountPlural', + '{{count}} labels', + { count: 3 } + ) + }) + + it('uses the singular sentence for one member and the plural one above that', () => { + expect(linearMemberCountLabel(1)).toBe( + 'auto.components.TaskPage.memberCountSingular:{{count}} member' + ) + expect(linearMemberCountLabel(2)).toBe( + 'auto.components.TaskPage.memberCountPlural:{{count}} members' + ) + expect(mocks.translate).toHaveBeenNthCalledWith( + 1, + 'auto.components.TaskPage.memberCountSingular', + '{{count}} member', + { count: 1 } + ) + expect(mocks.translate).toHaveBeenNthCalledWith( + 2, + 'auto.components.TaskPage.memberCountPlural', + '{{count}} members', + { count: 2 } + ) + }) +}) diff --git a/src/renderer/src/components/task-page/linear/linear-attribute-count-labels.ts b/src/renderer/src/components/task-page/linear/linear-attribute-count-labels.ts new file mode 100644 index 00000000000..a63e65a0f51 --- /dev/null +++ b/src/renderer/src/components/task-page/linear/linear-attribute-count-labels.ts @@ -0,0 +1,16 @@ +import { translate } from '@/i18n/i18n' + +// Why whole sentences per count: the plural ending used to travel as an +// interpolation value, which glued an English "s" onto translated nouns. +// Both callers show these only for a count of one or more. +export function linearLabelCountLabel(count: number): string { + return count > 1 + ? translate('auto.components.TaskPage.labelCountPlural', '{{count}} labels', { count }) + : translate('auto.components.TaskPage.labelCountSingular', '{{count}} label', { count }) +} + +export function linearMemberCountLabel(count: number): string { + return count > 1 + ? translate('auto.components.TaskPage.memberCountPlural', '{{count}} members', { count }) + : translate('auto.components.TaskPage.memberCountSingular', '{{count}} member', { count }) +} diff --git a/src/renderer/src/components/terminal-pane/terminal-drop-upload-report.test.ts b/src/renderer/src/components/terminal-pane/terminal-drop-upload-report.test.ts index 8448a117b83..63666fd00a1 100644 --- a/src/renderer/src/components/terminal-pane/terminal-drop-upload-report.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-drop-upload-report.test.ts @@ -32,12 +32,32 @@ describe('reportTerminalDropUploadSkipsAndFailures', () => { reportTerminalDropUploadSkipsAndFailures([{ reason: 'symlink' }, { reason: 'too_large' }], []) const mixedSkipKey = mocks.translate.mock.calls[0]?.[0] - expect(symlinkOnlyKey).toBe('auto.components.terminal.pane.terminal.drop.handler.53f015fd85') + expect(symlinkOnlyKey).toBe( + 'auto.components.terminal.pane.terminal.drop.handler.skippedSymlinkSingular' + ) expect(mixedSkipKey).toBe('auto.components.terminal.pane.terminal.drop.handler.b4cf68e889') expect(symlinkOnlyKey).not.toBe(mixedSkipKey) expect(toast.message).toHaveBeenCalledTimes(2) }) + it('picks a whole sentence per symlink count instead of passing a plural ending', () => { + reportTerminalDropUploadSkipsAndFailures([{ reason: 'symlink' }], []) + reportTerminalDropUploadSkipsAndFailures([{ reason: 'symlink' }, { reason: 'symlink' }], []) + + expect(mocks.translate.mock.calls).toEqual([ + [ + 'auto.components.terminal.pane.terminal.drop.handler.skippedSymlinkSingular', + 'Skipped {{count}} symlink.', + { count: 1 } + ], + [ + 'auto.components.terminal.pane.terminal.drop.handler.skippedSymlinkPlural', + 'Skipped {{count}} symlinks.', + { count: 2 } + ] + ]) + }) + it('reports upload failures without leaking individual paths', () => { reportTerminalDropUploadSkipsAndFailures([], [{ reason: '/secret/project/file.txt' }]) diff --git a/src/renderer/src/components/terminal-pane/terminal-drop-upload-report.ts b/src/renderer/src/components/terminal-pane/terminal-drop-upload-report.ts index b74e677aadf..07d46572250 100644 --- a/src/renderer/src/components/terminal-pane/terminal-drop-upload-report.ts +++ b/src/renderer/src/components/terminal-pane/terminal-drop-upload-report.ts @@ -13,11 +13,17 @@ export function reportTerminalDropUploadSkipsAndFailures( const commonReason = sharedReason(skipped) toast.message( allSymlinks - ? translate( - 'auto.components.terminal.pane.terminal.drop.handler.53f015fd85', - 'Skipped {{value0}} symlink{{value1}}.', - { value0: skipped.length, value1: skipped.length === 1 ? '' : 's' } - ) + ? skipped.length === 1 + ? translate( + 'auto.components.terminal.pane.terminal.drop.handler.skippedSymlinkSingular', + 'Skipped {{count}} symlink.', + { count: skipped.length } + ) + : translate( + 'auto.components.terminal.pane.terminal.drop.handler.skippedSymlinkPlural', + 'Skipped {{count}} symlinks.', + { count: skipped.length } + ) : translate( 'auto.components.terminal.pane.terminal.drop.handler.b4cf68e889', 'Skipped {{value0}} {{value1}}.', diff --git a/src/renderer/src/components/terminal-pane/terminal-native-file-drop.ts b/src/renderer/src/components/terminal-pane/terminal-native-file-drop.ts index ec512423fd9..bba3a823a36 100644 --- a/src/renderer/src/components/terminal-pane/terminal-native-file-drop.ts +++ b/src/renderer/src/components/terminal-pane/terminal-native-file-drop.ts @@ -128,11 +128,17 @@ async function uploadRuntimeDropPaths( const targetShell = getTerminalTargetShellForWorktreePath(args.worktreePath) const destinationDir = joinRuntimeTerminalDropDir(args.worktreePath) const pending = toast.loading( - translate( - 'auto.components.terminal.pane.terminal.drop.handler.29c031b49a', - 'Uploading {{value0}} file{{value1}} to runtime…', - { value0: args.dataPaths.length, value1: args.dataPaths.length === 1 ? '' : 's' } - ) + args.dataPaths.length === 1 + ? translate( + 'auto.components.terminal.pane.terminal.drop.handler.uploadingFileSingular', + 'Uploading {{count}} file to runtime…', + { count: args.dataPaths.length } + ) + : translate( + 'auto.components.terminal.pane.terminal.drop.handler.uploadingFilePlural', + 'Uploading {{count}} files to runtime…', + { count: args.dataPaths.length } + ) ) try { const { results } = await importExternalPathsToRuntime( @@ -200,11 +206,17 @@ async function uploadRemoteDropPaths( args: NativeDropFlowArgs & { connectionId: string; targetShell: 'posix' | 'windows' } ): Promise { const pending = toast.loading( - translate( - 'auto.components.terminal.pane.terminal.drop.handler.29c031b49a', - 'Uploading {{value0}} file{{value1}} to remote…', - { value0: args.dataPaths.length, value1: args.dataPaths.length === 1 ? '' : 's' } - ) + args.dataPaths.length === 1 + ? translate( + 'auto.components.terminal.pane.terminal.drop.handler.uploadingFileSingular', + 'Uploading {{count}} file to remote…', + { count: args.dataPaths.length } + ) + : translate( + 'auto.components.terminal.pane.terminal.drop.handler.uploadingFilePlural', + 'Uploading {{count}} files to remote…', + { count: args.dataPaths.length } + ) ) try { const { resolvedPaths, skipped, failed } = await window.api.fs.resolveDroppedPathsForAgent({ diff --git a/src/renderer/src/i18n/count-sentence-locales.test.ts b/src/renderer/src/i18n/count-sentence-locales.test.ts new file mode 100644 index 00000000000..a7604878bfc --- /dev/null +++ b/src/renderer/src/i18n/count-sentence-locales.test.ts @@ -0,0 +1,61 @@ +import { beforeEach, describe, expect, it } from 'vitest' + +import { i18n, setRendererPluginLanguagePacks } from './i18n' + +// Why: these sentences used to be a noun plus an English plural ending passed as an +// interpolation value, so Japanese, Korean and Chinese rendered a Latin "s" after the +// noun. Each is now a pair of whole sentences chosen by count; this guards what every +// bundled locale actually renders for one and for several. +const COUNT_SENTENCES: readonly (readonly [string, Record])[] = [ + ['auto.components.GitHubItemDialog.reactionCount', { reaction: 'heart' }], + ['auto.components.GitLabItemDialog.approvalsRemaining', {}], + ['auto.components.TaskPage.labelCount', {}], + ['auto.components.TaskPage.memberCount', {}], + ['auto.components.terminal.pane.terminal.drop.handler.skippedSymlink', {}], + ['auto.components.terminal.pane.terminal.drop.handler.uploadingFile', {}], + ['auto.components.sidebar.WorktreeList.groupProjectsRemained', { remaining: 1 }], + ['auto.components.sidebar.AddRemoteHostDialog.sshImportSynced', {}], + ['auto.components.right.sidebar.SourceControl.discardFailed', {}], + ['auto.components.right.sidebar.SourceControl.noteCount', {}], + ['auto.components.right.sidebar.source.control.compare.summary.commitsAhead', { ref: 'main' }], + ['auto.components.onboarding.IntegrationsStep.linearWorkspacesLinked', {}] +] + +const LATIN_S_AFTER_CJK = /[\u3040-\u30ff\u4e00-\u9fff\uac00-\ud7af]\s?s(?![a-z])/i + +function renderCountSentence( + baseKey: string, + count: number, + values: Record +): string { + const key = `${baseKey}${count === 1 ? 'Singular' : 'Plural'}` + return i18n.t(key, { ...values, count, defaultValue: '' }) +} + +describe('count sentences in the bundled locales', () => { + beforeEach(async () => { + setRendererPluginLanguagePacks([]) + await i18n.changeLanguage('en') + }) + + it.each(['es', 'fr', 'ja', 'ko', 'zh'])( + '%s renders whole sentences for one and several', + async (locale) => { + await i18n.changeLanguage(locale) + for (const [baseKey, values] of COUNT_SENTENCES) { + for (const count of [1, 3]) { + const text = renderCountSentence(baseKey, count, values) + expect(text, `${baseKey} (${count})`).toContain(String(count)) + expect(text, `${baseKey} (${count})`).not.toContain('{{') + } + } + } + ) + + it.each(['ja', 'ko', 'zh'])('%s gets no Latin plural ending on its nouns', async (locale) => { + await i18n.changeLanguage(locale) + for (const [baseKey, values] of COUNT_SENTENCES) { + expect(renderCountSentence(baseKey, 3, values), baseKey).not.toMatch(LATIN_S_AFTER_CJK) + } + }) +}) diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index 55263d5ee7f..48fd78035d9 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -37,6 +37,7 @@ "68796dafa0": "pr", "88fd82474d": "page", "924c2fe05e": "destructive", + "a18f669c7a": "{{value0}} {{value1}} reaction{{value2}}", "b0b7344684": "Request up to 15 reviewers", "b1157c78ff": "sheet", "ce8a85d209": "default", @@ -47,6 +48,7 @@ }, "GitLabItemDialog": { "11384f99aa": "number", + "40c56b95e2": "{{value0}} approval{{value1}} remaining", "4186685c78": "reopen", "881e522e04": "merge", "cae2712a23": "close" @@ -155,6 +157,7 @@ "6459faa8b3": "error", "68df347677": "you@example.com", "6edf402e11": "overview", + "7719d8daa9": "{{value0}} member{{value1}}", "7799ad9ab6": "draft", "887efe9140": "Connect", "937b29fa35": "items", @@ -176,6 +179,7 @@ "d0e3c8f933": "No matching GitHub work", "d6d08c1650": "Select a project to see GitLab work items.", "duplicateIssueNumberPlaceholder": "Issue number", + "eff9800d4b": "{{value0}} label{{value1}}", "f294c500ef": "No GitLab work matches this filter.", "fe28c9821f": "view" }, @@ -667,7 +671,8 @@ "IntegrationsStep": { "93f0c49ad1": "not-authenticated", "a3bcf13694": "connected", - "a74c6d6b18": "not-installed" + "a74c6d6b18": "not-installed", + "b08a6ac93c": "{{value0}} workspace{{value1}} linked. Add another workspace or replace a restricted key any time." }, "NotificationStep": { "2c47f5465f": "Turn on Allow notifications for Orca in System Settings. This step updates automatically once enabled.", @@ -839,9 +844,11 @@ "48a003c1b1": "Staged Changes", "522f44dce5": "Untracked Files", "527e130b6f": "Stop generating", + "657e0c90ad": "{{value0}} note{{value1}}", "77afaa8152": "All", "783a808870": "Close", "7a09d7f9d2": "base", + "8eb3782a0c": "Failed to discard {{value0}} file{{value1}}", "901140f47d": "Review the prompt before starting an agent.", "9bb062a886": "uncommitted", "9febd8ab5f": "create_pr", @@ -1034,6 +1041,11 @@ } } }, + "compare": { + "summary": { + "dd72a6fd37": "{{value0}} commit{{value1}} ahead of {{value2}}" + } + }, "discard": { "confirmation": { "1426c2efff": "This will revert all changes to this file. This cannot be undone.", @@ -1872,6 +1884,7 @@ "serverName": "Server name", "sshConfigPickerBulkHint": "Bulk sync stays in Settings → SSH", "sshConfigPickerDescription": "Pick a host to fill the form. Nothing is saved until you click Save.", + "sshImportSynced": "Added {{value0}} host{{value1}} to Orca.", "sshPersistenceDefault": "Remote terminals on this host stay alive until you end them or reset the relay." }, "AddRepoHostSelector": { @@ -2010,7 +2023,8 @@ "5fc9d1891b": "Deleting…", "84a2238242": "Show child workspaces", "ebadb7eadb": "{{value0}} {{value1}} child {{value2}}", - "ebc5c7dcef": "Hide child workspaces" + "ebc5c7dcef": "Hide child workspaces", + "f94466bc39": "{{value0}} of {{value1}} contained project{{value2}} remained after deleting the group." }, "WorktreeMetaDialog": { "029ea5ec57": "Open GitHub issue", @@ -2558,9 +2572,9 @@ "3eed73394f": "Your keyboard is paused" }, "TerminalErrorToast": { + "hostTerminalCap": "The host is already running as many terminals as Orca allows. Close a terminal you no longer need, then try again.", "ptyAllocationLimit": "Your system cannot allocate any more pty devices. Close some terminals you are not using, in Orca or another program, then try again.", - "terminalProcessLimit": "Your system cannot start another terminal process. Close unused terminals or quit unused processes, then try again.", - "hostTerminalCap": "The host is already running as many terminals as Orca allows. Close a terminal you no longer need, then try again." + "terminalProcessLimit": "Your system cannot start another terminal process. Close unused terminals or quit unused processes, then try again." }, "TerminalSessionStateSaveFailureDialog": { "678c780a2c": "Disk space is unavailable", @@ -2573,7 +2587,10 @@ "terminal": { "drop": { "handler": { - "29c031b49a": "Uploading {{value0}} file{{value1}} to runtime…" + "29c031b49a": "Uploading {{value0}} file{{value1}} to runtime…", + "53f015fd85": "Skipped {{value0}} symlink{{value1}}.", + "uploadingFilePlural": "Uploading {{count}} files to runtime…", + "uploadingFileSingular": "Uploading {{count}} file to runtime…" } } } diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 4d51de70365..60291226247 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -1587,7 +1587,9 @@ "8812225174": "Reopen", "09d67a0f9b": "Failed to close pull request", "88809e79db": "Failed to reopen pull request", - "00f55cc17b": "Repository access is unavailable for this pull request." + "00f55cc17b": "Repository access is unavailable for this pull request.", + "reactionCountSingular": "{{count}} {{reaction}} reaction", + "reactionCountPlural": "{{count}} {{reaction}} reactions" }, "GitLabItemDialog": { "65e784c1f1": "Reopen", @@ -1653,7 +1655,9 @@ "32f8bef818": "No log output.", "4168eb2c51": "Resolve", "commentTooLarge": "Comment is too large to submit safely.", - "gitlabActionFailed": "GitLab action failed." + "gitlabActionFailed": "GitLab action failed.", + "approvalsRemainingSingular": "{{count}} approval remaining", + "approvalsRemainingPlural": "{{count}} approvals remaining" }, "JiraIssueWorkspace": { "b0b92666c9": "Comment", @@ -2503,7 +2507,11 @@ "linearModeHasWorktreeTooltip": "Linear tickets linked to an Orca workspace", "linearEmptyHasWorktree": "No Linear tickets are linked to an Orca workspace yet. Start work from a Linear issue to see it here.", "linearOpenAttachedWorkspace": "Open workspace attached to {{value0}}", - "linearWorktreesColumn": "Workspaces" + "linearWorktreesColumn": "Workspaces", + "labelCountPlural": "{{count}} labels", + "labelCountSingular": "{{count}} label", + "memberCountPlural": "{{count}} members", + "memberCountSingular": "{{count}} member" }, "Terminal": { "73768427cf": "Close", @@ -3506,7 +3514,11 @@ "internalHostMismatch": "Drop files from the same host as this terminal.", "b4cf68e889": "Skipped {{value0}} {{value1}}.", "writeTimeout": "File drop cancelled: terminal did not accept the path before the safety timeout.", - "writeRejected": "File drop cancelled: terminal could not accept the path." + "writeRejected": "File drop cancelled: terminal could not accept the path.", + "skippedSymlinkSingular": "Skipped {{count}} symlink.", + "skippedSymlinkPlural": "Skipped {{count}} symlinks.", + "uploadingFileSingular": "Uploading {{count}} file to runtime…", + "uploadingFilePlural": "Uploading {{count}} files to runtime…" } } }, @@ -6025,7 +6037,9 @@ "groupRenameFailedDesc": "Orca could not confirm the new name with the group's host. Recheck the group after reconnecting.", "failedNestWorkspace": "Failed to nest workspace", "sidebarRowMissing": "Target no longer exists", - "failedUnnestWorkspace": "Failed to unnest workspace" + "failedUnnestWorkspace": "Failed to unnest workspace", + "groupProjectsRemainedSingular": "{{remaining}} of {{count}} contained project remained after deleting the group.", + "groupProjectsRemainedPlural": "{{remaining}} of {{count}} contained projects remained after deleting the group." }, "WorktreeMetaDialog": { "3db0a2a593": "Cancel", @@ -6515,7 +6529,9 @@ "sshConfigPickerAddAllEmpty": "Add all to Orca", "identityFileFromConfigHint": "Left empty on purpose: Orca uses every key ~/.ssh/config resolves for {{value0}}. Type a path to use just that key.", "sshConfigPickerRetry": "Try again", - "sshConfigPickerResolving": "Reading…" + "sshConfigPickerResolving": "Reading…", + "sshImportSyncedPlural": "Added {{count}} hosts to Orca.", + "sshImportSyncedSingular": "Added {{count}} host to Orca." }, "ForgetSshWorkspaceDialog": { "reconnectFailed": "Reconnection failed", @@ -13234,7 +13250,11 @@ "entryDiscardFailed": "Failed to discard “{{value0}}”", "entryDeleteFailed": "Failed to delete “{{value0}}”", "entryFailedInWorkspace": "{{value0}} in {{value1}}", - "diffCommentNotesCopyFailed": "Failed to copy notes" + "diffCommentNotesCopyFailed": "Failed to copy notes", + "discardFailedSingular": "Failed to discard {{count}} file", + "discardFailedPlural": "Failed to discard {{count}} files", + "noteCountSingular": "{{count}} note", + "noteCountPlural": "{{count}} notes" }, "SourceControlAgentActionDialog": { "8e856842d1": "Could not start the selected agent.", @@ -13768,7 +13788,9 @@ }, "compare": { "summary": { - "dd72a6fd37": "{{value0}} commit{{value1}} ahead of {{value2}}" + "dd72a6fd37": "{{value0}} commit{{value1}} ahead of {{value2}}", + "commitsAheadSingular": "{{count}} commit ahead of {{ref}}", + "commitsAheadPlural": "{{count}} commits ahead of {{ref}}" } }, "conflict": { @@ -14324,7 +14346,9 @@ "a3bcf13694": "connected", "d6e5dba05a": "Sign in", "0b4a7d23ab": "Signing in", - "a74c6d6b18": "not-installed" + "a74c6d6b18": "not-installed", + "linearWorkspacesLinkedSingular": "{{count}} workspace linked. Add another workspace or replace a restricted key any time.", + "linearWorkspacesLinkedPlural": "{{count}} workspaces linked. Add another workspace or replace a restricted key any time." }, "NotificationStep": { "3bede04483": "Send test notification", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 4b92605e734..19388622658 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -1399,7 +1399,9 @@ "8812225174": "Reabrir", "09d67a0f9b": "No se pudo cerrar la PR", "88809e79db": "No se pudo reabrir la PR", - "00f55cc17b": "El acceso al repositorio no está disponible para esta PR." + "00f55cc17b": "El acceso al repositorio no está disponible para esta PR.", + "reactionCountSingular": "{{count}} {{reaction}} reacción", + "reactionCountPlural": "{{count}} {{reaction}} reacciones" }, "GitLabItemDialog": { "65e784c1f1": "Reabrir", @@ -1465,7 +1467,9 @@ "32f8bef818": "Sin output de log.", "4168eb2c51": "Resolver", "commentTooLarge": "El comentario es demasiado grande para enviarlo de forma segura.", - "gitlabActionFailed": "La acción de GitLab falló." + "gitlabActionFailed": "La acción de GitLab falló.", + "approvalsRemainingSingular": "{{count}} aprobación restante", + "approvalsRemainingPlural": "{{count}} aprobaciones restantes" }, "JiraIssueWorkspace": { "b0b92666c9": "Comentar", @@ -2272,7 +2276,11 @@ "jiraLinkSourceUnavailable": "Couldn’t link this Jira issue. Reconnect Jira or pick the matching site, then try again.", "loadPageUnreachable": "La página {{value0}} está más allá de lo que la búsqueda de GitHub puede devolver.", "loadPageFailed": "No se pudo cargar la página {{value0}} desde GitHub.", - "loadPageNoMoreResults": "No hay más resultados en la página {{value0}}." + "loadPageNoMoreResults": "No hay más resultados en la página {{value0}}.", + "labelCountPlural": "{{count}} etiquetas", + "labelCountSingular": "{{count}} etiqueta", + "memberCountPlural": "{{count}} miembros", + "memberCountSingular": "{{count}} miembro" }, "Terminal": { "73768427cf": "Cerrar", @@ -3256,7 +3264,11 @@ "internalHostMismatch": "Arrastra archivos del mismo host que este terminal.", "b4cf68e889": "Se omitió {{value0}} {{value1}}.", "writeTimeout": "Se canceló el drop de archivos: el terminal no aceptó la ruta antes del tiempo límite de seguridad.", - "writeRejected": "Se canceló el drop de archivos: el terminal no pudo aceptar la ruta." + "writeRejected": "Se canceló el drop de archivos: el terminal no pudo aceptar la ruta.", + "skippedSymlinkSingular": "Se omitió {{count}} symlink.", + "skippedSymlinkPlural": "Se omitieron {{count}} symlinks.", + "uploadingFileSingular": "Subiendo {{count}} archivo al host…", + "uploadingFilePlural": "Subiendo {{count}} archivos al host…" } } }, @@ -5737,7 +5749,9 @@ "sidebarRowMissing": "El destino ya no existe", "failedUnnestWorkspace": "Error al expandir el espacio de trabajo", "groupRenameFailed": "No se pudo cambiar el nombre del grupo", - "groupRenameFailedDesc": "Orca no pudo confirmar el nuevo nombre con el anfitrión del grupo. Vuelva a verificar el grupo después de volver a conectarse." + "groupRenameFailedDesc": "Orca no pudo confirmar el nuevo nombre con el anfitrión del grupo. Vuelva a verificar el grupo después de volver a conectarse.", + "groupProjectsRemainedSingular": "{{remaining}} de {{count}} proyecto contenido permaneció tras eliminar el grupo.", + "groupProjectsRemainedPlural": "Proyectos contenidos que permanecieron tras eliminar el grupo: {{remaining}} de {{count}}." }, "WorktreeMetaDialog": { "3db0a2a593": "Cancelar", @@ -6276,7 +6290,9 @@ "sshConfigPickerAddAllEmpty": "Agregar todo a Orca", "identityFileFromConfigHint": "Dejado vacío a propósito: Orca usa todas las claves ~/.ssh/config resuelve para {{value0}}. Escriba una ruta para usar solo esa clave.", "sshConfigPickerRetry": "Probar de nuevo", - "sshConfigPickerResolving": "Lectura…" + "sshConfigPickerResolving": "Lectura…", + "sshImportSyncedPlural": "Se sincronizaron {{count}} hosts.", + "sshImportSyncedSingular": "Se sincronizó {{count}} host." }, "ForgetSshWorkspaceDialog": { "reconnectFailed": "Error de reconexión", @@ -12938,7 +12954,11 @@ "entryDiscardFailed": "No se pudo descartar \"{{value0}}\"", "entryDeleteFailed": "No se pudo eliminar \"{{value0}}\"", "entryFailedInWorkspace": "{{value0}} en {{value1}}", - "diffCommentNotesCopyFailed": "No se pudieron copiar notas" + "diffCommentNotesCopyFailed": "No se pudieron copiar notas", + "discardFailedSingular": "No se pudo descartar {{count}} archivo", + "discardFailedPlural": "No se pudieron descartar {{count}} archivos", + "noteCountSingular": "{{count}} nota", + "noteCountPlural": "{{count}} notas" }, "SourceControlAgentActionDialog": { "8e856842d1": "No se pudo iniciar el agente seleccionado.", @@ -13471,7 +13491,9 @@ }, "compare": { "summary": { - "dd72a6fd37": "{{value0}} confirma{{value1}} antes de {{value2}}" + "dd72a6fd37": "{{value0}} confirma{{value1}} antes de {{value2}}", + "commitsAheadSingular": "{{count}} commit por delante de {{ref}}", + "commitsAheadPlural": "{{count}} commits por delante de {{ref}}" } }, "conflict": { @@ -14027,7 +14049,9 @@ "a3bcf13694": "conectado", "d6e5dba05a": "Iniciar sesión", "0b4a7d23ab": "Iniciando sesión", - "a74c6d6b18": "no instalado" + "a74c6d6b18": "no instalado", + "linearWorkspacesLinkedSingular": "{{count}} workspace vinculado. Agrega otro workspace o reemplaza una clave restringida en cualquier momento.", + "linearWorkspacesLinkedPlural": "{{count}} workspaces vinculados. Agrega otro workspace o reemplaza una clave restringida en cualquier momento." }, "NotificationStep": { "3bede04483": "Enviar notificación de prueba", diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index a9eae06bb86..a50d08a43fb 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -1504,7 +1504,9 @@ "8812225174": "Rouvrir", "09d67a0f9b": "Échec de la fermeture de la pull request", "88809e79db": "Échec de la réouverture de la pull request", - "00f55cc17b": "L'accès au dépôt est indisponible pour cette pull request." + "00f55cc17b": "L'accès au dépôt est indisponible pour cette pull request.", + "reactionCountSingular": "{{count}} {{reaction}} réaction", + "reactionCountPlural": "{{count}} {{reaction}} réactions" }, "GitLabItemDialog": { "65e784c1f1": "Rouvrir", @@ -1570,7 +1572,9 @@ "32f8bef818": "Aucune sortie de log.", "4168eb2c51": "Résoudre", "commentTooLarge": "Le commentaire est trop volumineux pour être envoyé sans risque.", - "gitlabActionFailed": "L'action GitLab a échoué." + "gitlabActionFailed": "L'action GitLab a échoué.", + "approvalsRemainingSingular": "Il reste {{count}} approbation", + "approvalsRemainingPlural": "Il reste {{count}} approbations" }, "JiraIssueWorkspace": { "b0b92666c9": "Commentaire", @@ -2377,7 +2381,11 @@ "linearModeHasWorktreeTooltip": "Tickets Linear liés à un espace de travail Orca", "linearEmptyHasWorktree": "Aucun ticket Linear n'est encore lié à un espace de travail Orca. Démarrez le travail depuis une issue Linear pour le voir ici.", "linearOpenAttachedWorkspace": "Ouvrir l'espace de travail attaché à {{value0}}", - "linearWorktreesColumn": "Espaces de travail" + "linearWorktreesColumn": "Espaces de travail", + "labelCountPlural": "{{count}} labels", + "labelCountSingular": "{{count}} label", + "memberCountPlural": "{{count}} membres", + "memberCountSingular": "{{count}} membre" }, "Terminal": { "73768427cf": "Fermer", @@ -3380,7 +3388,11 @@ "internalHostMismatch": "Déposez des fichiers provenant du même hôte que ce terminal.", "b4cf68e889": "Ignoré : {{value0}} {{value1}}.", "writeTimeout": "Glisser-déposer de fichiers annulé : le terminal n'a pas accepté le chemin avant le délai de sécurité.", - "writeRejected": "Glisser-déposer de fichiers annulé : le terminal n'a pas pu accepter le chemin." + "writeRejected": "Glisser-déposer de fichiers annulé : le terminal n'a pas pu accepter le chemin.", + "skippedSymlinkSingular": "Ignoré : {{count}} symlink.", + "skippedSymlinkPlural": "Ignorés : {{count}} symlinks.", + "uploadingFileSingular": "Envoi de {{count}} fichier vers le runtime…", + "uploadingFilePlural": "Envoi de {{count}} fichiers vers le runtime…" } } }, @@ -5866,7 +5878,9 @@ "groupRenameFailedDesc": "Orca n'a pas pu confirmer le nouveau nom auprès de l'hôte du groupe. Revérifiez le groupe après reconnexion.", "failedNestWorkspace": "Échec de l'imbrication de l'espace de travail", "sidebarRowMissing": "La cible n'existe plus", - "failedUnnestWorkspace": "Échec de la désimbrication de l'espace de travail" + "failedUnnestWorkspace": "Échec de la désimbrication de l'espace de travail", + "groupProjectsRemainedSingular": "{{remaining}} projet sur {{count}} est resté après la suppression du groupe.", + "groupProjectsRemainedPlural": "Projets restés après la suppression du groupe : {{remaining}} sur {{count}}." }, "WorktreeMetaDialog": { "3db0a2a593": "Annuler", @@ -6356,7 +6370,9 @@ "sshConfigPickerAddAllEmpty": "Tout ajouter à Orca", "identityFileFromConfigHint": "Volontairement laissé vide : Orca utilise toutes les clés résolues par ~/.ssh/config pour {{value0}}. Saisissez un chemin pour n'utiliser que cette clé.", "sshConfigPickerRetry": "Réessayer", - "sshConfigPickerResolving": "Lecture…" + "sshConfigPickerResolving": "Lecture…", + "sshImportSyncedPlural": "Ajout de {{count}} hôtes à Orca.", + "sshImportSyncedSingular": "Ajout de {{count}} hôte à Orca." }, "ForgetSshWorkspaceDialog": { "reconnectFailed": "Échec de la reconnexion", @@ -13044,7 +13060,11 @@ "entryDiscardFailed": "Échec de la suppression de « {{value0}} »", "entryDeleteFailed": "Échec de la suppression de « {{value0}} »", "entryFailedInWorkspace": "{{value0}} dans {{value1}}", - "diffCommentNotesCopyFailed": "Échec de la copie des notes" + "diffCommentNotesCopyFailed": "Échec de la copie des notes", + "discardFailedSingular": "Échec de l'abandon de {{count}} fichier", + "discardFailedPlural": "Échec de l'abandon de {{count}} fichiers", + "noteCountSingular": "{{count}} note", + "noteCountPlural": "{{count}} notes" }, "SourceControlAgentActionDialog": { "8e856842d1": "Impossible de démarrer l'agent sélectionné.", @@ -13577,7 +13597,9 @@ }, "compare": { "summary": { - "dd72a6fd37": "{{value0}} commit{{value1}} d'avance sur {{value2}}" + "dd72a6fd37": "{{value0}} commit{{value1}} d'avance sur {{value2}}", + "commitsAheadSingular": "{{count}} commit d'avance sur {{ref}}", + "commitsAheadPlural": "{{count}} commits d'avance sur {{ref}}" } }, "conflict": { @@ -14133,7 +14155,9 @@ "a3bcf13694": "connecté", "d6e5dba05a": "Se connecter", "0b4a7d23ab": "Connexion en cours", - "a74c6d6b18": "not-installed" + "a74c6d6b18": "not-installed", + "linearWorkspacesLinkedSingular": "{{count}} espace de travail associé. Ajoutez un autre espace de travail ou remplacez une clé restreinte à tout moment.", + "linearWorkspacesLinkedPlural": "{{count}} espaces de travail associés. Ajoutez un autre espace de travail ou remplacez une clé restreinte à tout moment." }, "NotificationStep": { "3bede04483": "Envoyer une notification de test", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index ae613af9533..c02a0103312 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -1399,7 +1399,9 @@ "8812225174": "再度開く", "09d67a0f9b": "PR をクローズできませんでした", "88809e79db": "PR の再開に失敗しました", - "00f55cc17b": "この PR ではリポジトリにアクセスできません。" + "00f55cc17b": "この PR ではリポジトリにアクセスできません。", + "reactionCountSingular": "{{count}} {{reaction}} 反応", + "reactionCountPlural": "{{count}} {{reaction}} 反応" }, "GitLabItemDialog": { "65e784c1f1": "再度開く", @@ -1465,7 +1467,9 @@ "32f8bef818": "ログは出力されません。", "4168eb2c51": "解決", "commentTooLarge": "コメントが大きすぎるため安全に送信できません。", - "gitlabActionFailed": "GitLab 操作が失敗しました。" + "gitlabActionFailed": "GitLab 操作が失敗しました。", + "approvalsRemainingSingular": "{{count}} 承認 残り", + "approvalsRemainingPlural": "{{count}} 承認 残り" }, "JiraIssueWorkspace": { "b0b92666c9": "コメント", @@ -2272,7 +2276,11 @@ "jiraLinkSourceUnavailable": "この Jira Issue をリンクできませんでした。Jira に再接続するか該当するサイトを選択してから、もう一度お試しください。", "loadPageUnreachable": "ページ {{value0}} は GitHub 検索が返せる範囲を超えています。", "loadPageFailed": "GitHub からページ {{value0}} を読み込めませんでした。", - "loadPageNoMoreResults": "ページ {{value0}} にはこれ以上結果がありません。" + "loadPageNoMoreResults": "ページ {{value0}} にはこれ以上結果がありません。", + "labelCountPlural": "{{count}} ラベル", + "labelCountSingular": "{{count}} ラベル", + "memberCountPlural": "{{count}} メンバー", + "memberCountSingular": "{{count}} メンバー" }, "Terminal": { "73768427cf": "閉じる", @@ -3256,7 +3264,11 @@ "internalHostMismatch": "このターミナルと同じホストのファイルをドロップしてください。", "b4cf68e889": "{{value0}} {{value1}} をスキップしました。", "writeTimeout": "ファイルドロップをキャンセルしました: 安全タイムアウト前にターミナルがパスを受け付けませんでした。", - "writeRejected": "ファイルドロップをキャンセルしました: ターミナルがパスを受け付けられませんでした。" + "writeRejected": "ファイルドロップをキャンセルしました: ターミナルがパスを受け付けられませんでした。", + "skippedSymlinkSingular": "{{count}} シンボリックリンク をスキップしました。", + "skippedSymlinkPlural": "{{count}} シンボリックリンク をスキップしました。", + "uploadingFileSingular": "{{count}} ファイル をランタイムにアップロードしています…", + "uploadingFilePlural": "{{count}} ファイル をランタイムにアップロードしています…" } } }, @@ -5718,7 +5730,9 @@ "sidebarRowMissing": "対象はもう存在しません", "failedUnnestWorkspace": "ワークスペースの展開に失敗しました", "groupRenameFailed": "グループ名の変更に失敗しました", - "groupRenameFailedDesc": "Orca はグループのホストに新規名前を確認できませんでした。再接続後、グループを再確認してください。" + "groupRenameFailedDesc": "Orca はグループのホストに新規名前を確認できませんでした。再接続後、グループを再確認してください。", + "groupProjectsRemainedSingular": "グループ削除後も、含まれていたプロジェクト {{count}} 件中 {{remaining}} 件が残りました。", + "groupProjectsRemainedPlural": "グループ削除後も、含まれていたプロジェクト {{count}} 件中 {{remaining}} 件が残りました。" }, "WorktreeMetaDialog": { "3db0a2a593": "キャンセル", @@ -6276,7 +6290,9 @@ "sshConfigPickerAddAllEmpty": "すべてを Orca に追加", "identityFileFromConfigHint": "意図的に空のままにします: Orca は ~/.ssh/config が {{value0}} に対して解決するすべてのキーを使用します。そのキーのみを使用するパスを入力します。", "sshConfigPickerRetry": "再試行", - "sshConfigPickerResolving": "読む…" + "sshConfigPickerResolving": "読む…", + "sshImportSyncedPlural": "{{count}} ホスト を同期しました。", + "sshImportSyncedSingular": "{{count}} ホスト を同期しました。" }, "ForgetSshWorkspaceDialog": { "reconnectFailed": "再接続に失敗しました", @@ -12938,7 +12954,11 @@ "entryDiscardFailed": "「{{value0}}」を破棄できませんでした", "entryDeleteFailed": "「{{value0}}」を削除できませんでした", "entryFailedInWorkspace": "{{value0}} の {{value1}}", - "diffCommentNotesCopyFailed": "メモのコピーに失敗しました" + "diffCommentNotesCopyFailed": "メモのコピーに失敗しました", + "discardFailedSingular": "{{count}} ファイル を破棄できませんでした", + "discardFailedPlural": "{{count}} ファイル を破棄できませんでした", + "noteCountSingular": "{{count}} 注", + "noteCountPlural": "{{count}} 注" }, "SourceControlAgentActionDialog": { "8e856842d1": "選択した Agent を開始できませんでした。", @@ -13471,7 +13491,9 @@ }, "compare": { "summary": { - "dd72a6fd37": "{{value0}} {{value2}} より先に{{value1}} をコミット" + "dd72a6fd37": "{{value0}} {{value2}} より先に{{value1}} をコミット", + "commitsAheadSingular": "{{ref}}より{{count}}コミット進んでいます", + "commitsAheadPlural": "{{ref}}より{{count}}コミット進んでいます" } }, "conflict": { @@ -14027,7 +14049,9 @@ "a3bcf13694": "接続されています", "d6e5dba05a": "サインイン", "0b4a7d23ab": "サインインする", - "a74c6d6b18": "未インストール" + "a74c6d6b18": "未インストール", + "linearWorkspacesLinkedSingular": "{{count}} ワークスペース がリンクされました。いつでも別のワークスペースを追加したり、制限付きキーを置き換えたりできます。", + "linearWorkspacesLinkedPlural": "{{count}} ワークスペース がリンクされました。いつでも別のワークスペースを追加したり、制限付きキーを置き換えたりできます。" }, "NotificationStep": { "3bede04483": "テスト通知を送信する", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 673eb9c6c06..6695ae57f26 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -1399,7 +1399,9 @@ "8812225174": "다시 열기", "09d67a0f9b": "PR을 닫지 못했습니다.", "88809e79db": "PR을 다시 열지 못했습니다.", - "00f55cc17b": "이 PR에는 저장소 액세스를 사용할 수 없습니다." + "00f55cc17b": "이 PR에는 저장소 액세스를 사용할 수 없습니다.", + "reactionCountSingular": "{{count}} {{reaction}} 반응", + "reactionCountPlural": "{{count}} {{reaction}} 반응" }, "GitLabItemDialog": { "65e784c1f1": "다시 열기", @@ -1465,7 +1467,9 @@ "32f8bef818": "로그 출력이 없습니다.", "4168eb2c51": "해결", "commentTooLarge": "댓글이 너무 길어 안전하게 제출할 수 없습니다.", - "gitlabActionFailed": "GitLab 작업이 실패했습니다." + "gitlabActionFailed": "GitLab 작업이 실패했습니다.", + "approvalsRemainingSingular": "{{count}} 승인 남음", + "approvalsRemainingPlural": "{{count}} 승인 남음" }, "JiraIssueWorkspace": { "b0b92666c9": "댓글", @@ -2272,7 +2276,11 @@ "jiraLinkSourceUnavailable": "Couldn’t link this Jira issue. Reconnect Jira or pick the matching site, then try again.", "loadPageUnreachable": "{{value0}} 페이지는 GitHub 검색이 반환할 수 있는 범위를 벗어났습니다.", "loadPageFailed": "GitHub에서 {{value0}} 페이지를 불러오지 못했습니다.", - "loadPageNoMoreResults": "{{value0}} 페이지에 더 이상 결과가 없습니다." + "loadPageNoMoreResults": "{{value0}} 페이지에 더 이상 결과가 없습니다.", + "labelCountPlural": "{{count}} 라벨", + "labelCountSingular": "{{count}} 라벨", + "memberCountPlural": "{{count}} 멤버", + "memberCountSingular": "{{count}} 멤버" }, "Terminal": { "73768427cf": "닫기", @@ -3256,7 +3264,11 @@ "internalHostMismatch": "이 터미널과 같은 호스트의 파일을 드롭하세요.", "b4cf68e889": "{{value0}} {{value1}}을(를) 건너뛰었습니다.", "writeTimeout": "파일 드롭이 취소되었습니다. 안전 시간 제한 전에 터미널이 경로를 받지 않았습니다.", - "writeRejected": "파일 드롭이 취소되었습니다. 터미널이 경로를 받을 수 없습니다." + "writeRejected": "파일 드롭이 취소되었습니다. 터미널이 경로를 받을 수 없습니다.", + "skippedSymlinkSingular": "{{count}} 심볼릭 링크를 건너뛰었습니다.", + "skippedSymlinkPlural": "{{count}} 심볼릭 링크를 건너뛰었습니다.", + "uploadingFileSingular": "{{count}} 파일을 런타임에 업로드하는 중…", + "uploadingFilePlural": "{{count}} 파일을 런타임에 업로드하는 중…" } } }, @@ -5718,7 +5730,9 @@ "sidebarRowMissing": "대상이 더 이상 존재하지 않습니다", "failedUnnestWorkspace": "워크트리 펼치기 실패", "groupRenameFailed": "그룹 이름을 바꾸지 못했습니다.", - "groupRenameFailedDesc": "Orca는 그룹 호스트에게 새 이름을 확인할 수 없습니다. 다시 연결한 후 그룹을 다시 확인하세요." + "groupRenameFailedDesc": "Orca는 그룹 호스트에게 새 이름을 확인할 수 없습니다. 다시 연결한 후 그룹을 다시 확인하세요.", + "groupProjectsRemainedSingular": "그룹 삭제 후에도 {{remaining}}/{{count}} 프로젝트가 남아 있습니다.", + "groupProjectsRemainedPlural": "그룹 삭제 후에도 {{remaining}}/{{count}} 프로젝트가 남아 있습니다." }, "WorktreeMetaDialog": { "3db0a2a593": "취소", @@ -6276,7 +6290,9 @@ "sshConfigPickerAddAllEmpty": "Orca에 모두 추가", "identityFileFromConfigHint": "의도적으로 비워 두기: Orca는 ~/.ssh/config가 {{value0}}을 확인하는 모든 키를 사용합니다. 해당 키만 사용하려면 경로를 입력하세요.", "sshConfigPickerRetry": "다시 시도하세요", - "sshConfigPickerResolving": "독서…" + "sshConfigPickerResolving": "독서…", + "sshImportSyncedPlural": "{{count}} 호스트을(를) Orca에 추가했습니다.", + "sshImportSyncedSingular": "{{count}} 호스트을(를) Orca에 추가했습니다." }, "ForgetSshWorkspaceDialog": { "reconnectFailed": "재연결 실패", @@ -12938,7 +12954,11 @@ "entryDiscardFailed": "'{{value0}}'을(를) 삭제하지 못했습니다.", "entryDeleteFailed": "'{{value0}}'을(를) 삭제하지 못했습니다.", "entryFailedInWorkspace": "{{value1}}의 {{value0}}", - "diffCommentNotesCopyFailed": "메모를 복사하지 못했습니다." + "diffCommentNotesCopyFailed": "메모를 복사하지 못했습니다.", + "discardFailedSingular": "{{count}} 파일을(를) 삭제하지 못했습니다.", + "discardFailedPlural": "{{count}} 파일을(를) 삭제하지 못했습니다.", + "noteCountSingular": "{{count}} 메모", + "noteCountPlural": "{{count}} 메모" }, "SourceControlAgentActionDialog": { "8e856842d1": "선택한 agent를 시작할 수 없습니다.", @@ -13471,7 +13491,9 @@ }, "compare": { "summary": { - "dd72a6fd37": "{{value0}}은 {{value2}}보다 먼저 {{value1}}을 커밋합니다." + "dd72a6fd37": "{{value0}}은 {{value2}}보다 먼저 {{value1}}을 커밋합니다.", + "commitsAheadSingular": "{{ref}}보다 {{count}} commit 앞서 있음", + "commitsAheadPlural": "{{ref}}보다 {{count}} commits 앞서 있음" } }, "conflict": { @@ -14027,7 +14049,9 @@ "a3bcf13694": "연결됨", "d6e5dba05a": "로그인", "0b4a7d23ab": "로그인 중", - "a74c6d6b18": "설치되지 않은" + "a74c6d6b18": "설치되지 않은", + "linearWorkspacesLinkedSingular": "{{count}} 워크스페이스가 연결되었습니다. 언제든지 다른 워크스페이스를 추가하거나 제한된 키를 교체하세요.", + "linearWorkspacesLinkedPlural": "{{count}} 워크스페이스가 연결되었습니다. 언제든지 다른 워크스페이스를 추가하거나 제한된 키를 교체하세요." }, "NotificationStep": { "3bede04483": "테스트 알림 보내기", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 5920c46f849..d8ea1212440 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -1399,7 +1399,9 @@ "8812225174": "重新打开", "09d67a0f9b": "无法关闭拉取请求", "88809e79db": "无法重新提交拉取请求", - "00f55cc17b": "此拉取请求无法访问存储库。" + "00f55cc17b": "此拉取请求无法访问存储库。", + "reactionCountSingular": "{{count}} {{reaction}} 反应", + "reactionCountPlural": "{{count}} {{reaction}} 反应" }, "GitLabItemDialog": { "65e784c1f1": "重新打开", @@ -1465,7 +1467,9 @@ "32f8bef818": "无日志输出。", "4168eb2c51": "解决", "commentTooLarge": "评论过长,无法安全提交。", - "gitlabActionFailed": "GitLab 操作失败。" + "gitlabActionFailed": "GitLab 操作失败。", + "approvalsRemainingSingular": "{{count}} 批准 剩余", + "approvalsRemainingPlural": "{{count}} 批准 剩余" }, "JiraIssueWorkspace": { "b0b92666c9": "评论", @@ -2272,7 +2276,11 @@ "jiraLinkSourceUnavailable": "无法关联此 Jira 议题。请重新连接 Jira 或选择匹配的站点,然后重试。", "loadPageUnreachable": "第 {{value0}} 页超出了 GitHub 搜索可返回的范围。", "loadPageFailed": "无法从 GitHub 加载第 {{value0}} 页。", - "loadPageNoMoreResults": "第 {{value0}} 页没有更多结果。" + "loadPageNoMoreResults": "第 {{value0}} 页没有更多结果。", + "labelCountPlural": "{{count}} 标签", + "labelCountSingular": "{{count}} 标签", + "memberCountPlural": "{{count}} 成员", + "memberCountSingular": "{{count}} 成员" }, "Terminal": { "73768427cf": "关闭", @@ -3256,7 +3264,11 @@ "internalHostMismatch": "请拖放与此终端位于同一主机的文件。", "b4cf68e889": "已跳过 {{value0}} {{value1}}。", "writeTimeout": "文件拖放已取消:终端在安全超时前未接受路径。", - "writeRejected": "文件拖放已取消:终端无法接受路径。" + "writeRejected": "文件拖放已取消:终端无法接受路径。", + "skippedSymlinkSingular": "跳过 {{count}} 符号链接。", + "skippedSymlinkPlural": "跳过 {{count}} 符号链接。", + "uploadingFileSingular": "正在上传 {{count}} 文件 到运行时...", + "uploadingFilePlural": "正在上传 {{count}} 文件 到运行时..." } } }, @@ -5718,7 +5730,9 @@ "sidebarRowMissing": "目标不再存在", "failedUnnestWorkspace": "展开工作区失败", "groupRenameFailed": "重命名组失败", - "groupRenameFailedDesc": "Orca 无法与该群组的主持人确认新名称。重新连接后重新检查该组。" + "groupRenameFailedDesc": "Orca 无法与该群组的主持人确认新名称。重新连接后重新检查该组。", + "groupProjectsRemainedSingular": "删除分组后,{{count}} 个包含项目中仍剩 {{remaining}} 个。", + "groupProjectsRemainedPlural": "删除分组后,{{count}} 个包含项目中仍剩 {{remaining}} 个。" }, "WorktreeMetaDialog": { "3db0a2a593": "取消", @@ -6276,7 +6290,9 @@ "sshConfigPickerAddAllEmpty": "全部添加到 Orca", "identityFileFromConfigHint": "故意留空:Orca 使用 ~/.ssh/config 解析为 {{value0}} 的每个密钥。键入一个路径以仅使用该键。", "sshConfigPickerRetry": "重试", - "sshConfigPickerResolving": "阅读…" + "sshConfigPickerResolving": "阅读…", + "sshImportSyncedPlural": "已同步 {{count}} 个主机。", + "sshImportSyncedSingular": "已同步 {{count}} 个主机。" }, "ForgetSshWorkspaceDialog": { "reconnectFailed": "重新连接失败", @@ -12952,7 +12968,11 @@ "entryDiscardFailed": "无法丢弃“{{value0}}”", "entryDeleteFailed": "删除“{{value0}}”失败", "entryFailedInWorkspace": "{{value0}} 在 {{value1}} 中", - "diffCommentNotesCopyFailed": "复制笔记失败" + "diffCommentNotesCopyFailed": "复制笔记失败", + "discardFailedSingular": "无法丢弃 {{count}} 文件", + "discardFailedPlural": "无法丢弃 {{count}} 文件", + "noteCountSingular": "{{count}} 注", + "noteCountPlural": "{{count}} 注" }, "SourceControlAgentActionDialog": { "8e856842d1": "无法启动选定的智能体。", @@ -13485,7 +13505,9 @@ }, "compare": { "summary": { - "dd72a6fd37": "{{value0}} 在 {{value2}} 之前提交 {{value1}}" + "dd72a6fd37": "{{value0}} 在 {{value2}} 之前提交 {{value1}}", + "commitsAheadSingular": "领先 {{ref}} {{count}} 个提交", + "commitsAheadPlural": "领先 {{ref}} {{count}} 个提交" } }, "conflict": { @@ -14041,7 +14063,9 @@ "a3bcf13694": "已连接", "d6e5dba05a": "登录", "0b4a7d23ab": "登录", - "a74c6d6b18": "未安装" + "a74c6d6b18": "未安装", + "linearWorkspacesLinkedSingular": "{{count}} 工作区 已链接。随时添加另一个工作区或更换受限密钥。", + "linearWorkspacesLinkedPlural": "{{count}} 工作区 已链接。随时添加另一个工作区或更换受限密钥。" }, "NotificationStep": { "3bede04483": "发送测试通知",