diff --git a/src/renderer/src/components/settings/BrowserLinkRoutingModifierSetting.tsx b/src/renderer/src/components/settings/BrowserLinkRoutingModifierSetting.tsx index aeb8f4151d8d..b0c55e87204d 100644 --- a/src/renderer/src/components/settings/BrowserLinkRoutingModifierSetting.tsx +++ b/src/renderer/src/components/settings/BrowserLinkRoutingModifierSetting.tsx @@ -4,7 +4,7 @@ import { SettingsSwitchRow } from './SettingsFormControls' import { getLinkRoutingModifierDescription, getLinkRoutingModifierTitle -} from './browser-link-routing-modifier-copy' +} from './browser-link-routing-copy' type BrowserLinkRoutingModifierSettingProps = { settings: Pick diff --git a/src/renderer/src/components/settings/BrowserPane.tsx b/src/renderer/src/components/settings/BrowserPane.tsx index d67aaf7ef70e..5c8088cc9bbf 100644 --- a/src/renderer/src/components/settings/BrowserPane.tsx +++ b/src/renderer/src/components/settings/BrowserPane.tsx @@ -2,7 +2,8 @@ import { useCallback, useEffect, useMemo, useRef, useState, type MutableRefObjec import type { GlobalSettings } from '../../../../shared/types' import { useAppStore } from '../../store' import { matchesSettingsSearch } from './settings-search' -import { getBrowserPaneSearchEntries, getBrowserLinkRoutingDescription } from './browser-search' +import { getBrowserPaneSearchEntries } from './browser-search' +import { getBrowserLinkRoutingDescription } from './browser-link-routing-copy' import { getBrowserUsePaneSearchEntries } from './browser-use-search' import { getBrowserPaneCombinedSearchEntries } from './browser-pane-search' import { BrowserHomePageSetting } from './BrowserHomePageSetting' diff --git a/src/renderer/src/components/settings/browser-link-routing-modifier-copy.ts b/src/renderer/src/components/settings/browser-link-routing-copy.ts similarity index 58% rename from src/renderer/src/components/settings/browser-link-routing-modifier-copy.ts rename to src/renderer/src/components/settings/browser-link-routing-copy.ts index 1151d3e70e13..5a4d5bac2d65 100644 --- a/src/renderer/src/components/settings/browser-link-routing-modifier-copy.ts +++ b/src/renderer/src/components/settings/browser-link-routing-copy.ts @@ -1,5 +1,30 @@ import { translate } from '@/i18n/i18n' +export function getBrowserLinkRoutingShortcutLabel(platform: { isMac: boolean }): string { + return platform.isMac ? '⇧⌘-click' : 'Shift+Ctrl+click' +} + +// Why: "always" stops being true once inverting is on, so only then does the nested +// row take over the chord sentence — with it off this reads exactly as it always has. +// Each variant is a complete catalog entry: stitched suffixes break in locales whose +// sentence joining differs from English. +export function getBrowserLinkRoutingDescription( + platform: { isMac: boolean }, + modifierInverts = false +): string { + if (modifierInverts) { + return translate( + 'auto.components.settings.BrowserLinkRoutingSetting.descriptionBase', + "Open http(s) links in Orca's built-in browser — from the terminal, markdown, and the editor." + ) + } + return translate( + 'auto.components.settings.BrowserLinkRoutingSetting.description', + "Open http(s) links in Orca's built-in browser — from the terminal, markdown, and the editor. {{shortcut}} always uses your system browser.", + { shortcut: getBrowserLinkRoutingShortcutLabel(platform) } + ) +} + /** * Title and description both name the destination the modifier reaches, which is * the opposite of wherever Link Routing points. Kept out of the component so the diff --git a/src/renderer/src/components/settings/browser-link-routing-localization.test.ts b/src/renderer/src/components/settings/browser-link-routing-localization.test.ts new file mode 100644 index 000000000000..613f8e0de1cf --- /dev/null +++ b/src/renderer/src/components/settings/browser-link-routing-localization.test.ts @@ -0,0 +1,55 @@ +import { afterEach, describe, expect, it } from 'vitest' + +import { i18n } from '@/i18n/i18n' +import { getBrowserLinkRoutingDescription } from './browser-link-routing-copy' + +// The Link Routing description was a bare template literal, so it stayed English in every +// locale while its own title and the rest of the pane translated. It carries the platform +// shortcut label, so each locale must interpolate it rather than embed a hardcoded modifier. +const NON_ENGLISH_LOCALES = ['es', 'ja', 'ko', 'zh'] as const + +const SHORTCUT_BY_PLATFORM = { + mac: '⇧⌘-click', + other: 'Shift+Ctrl+click' +} as const + +describe('Link Routing description localization', () => { + afterEach(async () => { + await i18n.changeLanguage('en') + }) + + it('translates the description in every non-English locale on both platforms', async () => { + const english = { + mac: getBrowserLinkRoutingDescription({ isMac: true }), + other: getBrowserLinkRoutingDescription({ isMac: false }) + } + + for (const locale of NON_ENGLISH_LOCALES) { + await i18n.changeLanguage(locale) + + for (const [platform, shortcut] of Object.entries(SHORTCUT_BY_PLATFORM)) { + const description = getBrowserLinkRoutingDescription({ isMac: platform === 'mac' }) + + expect(description, `${locale}/${platform} fell back to English`).not.toBe( + english[platform as keyof typeof english] + ) + // The label is a key symbol, not prose: it stays literal in every locale. + expect(description, `${locale}/${platform} lost the shortcut label`).toContain(shortcut) + expect(description, `${locale}/${platform} leaked a placeholder`).not.toMatch(/\{\{.+?\}\}/) + } + } + }) + + it('keeps each platform label out of the other platform copy', async () => { + for (const locale of ['en', ...NON_ENGLISH_LOCALES]) { + await i18n.changeLanguage(locale) + + expect(getBrowserLinkRoutingDescription({ isMac: true })).not.toContain( + SHORTCUT_BY_PLATFORM.other + ) + expect(getBrowserLinkRoutingDescription({ isMac: false })).not.toContain( + SHORTCUT_BY_PLATFORM.mac + ) + } + }) +}) diff --git a/src/renderer/src/components/settings/browser-search.test.ts b/src/renderer/src/components/settings/browser-search.test.ts index 2ac1b67239e5..0df9173b28d3 100644 --- a/src/renderer/src/components/settings/browser-search.test.ts +++ b/src/renderer/src/components/settings/browser-search.test.ts @@ -1,12 +1,25 @@ -import { describe, expect, it } from 'vitest' -import { getBrowserLinkRoutingDescription, getBrowserPaneSearchEntries } from './browser-search' +import { beforeEach, describe, expect, it } from 'vitest' + +import ko from '@/i18n/locales/ko.json' +import { i18n } from '@/i18n/i18n' +import { getBrowserPaneSearchEntries } from './browser-search' import { + getBrowserLinkRoutingDescription, + getBrowserLinkRoutingShortcutLabel, getLinkRoutingModifierDescription, getLinkRoutingModifierTitle -} from './browser-link-routing-modifier-copy' +} from './browser-link-routing-copy' describe('browser settings search copy', () => { - it('uses macOS shortcut keywords for Link Routing search metadata', () => { + it('uses macOS shortcut symbols for Link Routing copy and search metadata', () => { + expect(getBrowserLinkRoutingShortcutLabel({ isMac: true })).toBe('⇧⌘-click') + + const description = getBrowserLinkRoutingDescription({ isMac: true }) + expect(description).toContain('⇧⌘-click') + expect(description).not.toContain('Cmd/Ctrl') + // The copy is translated: a leaked `{{...}}` means the interpolation name drifted from the catalog. + expect(description).not.toMatch(/\{\{.+?\}\}/) + const linkRoutingEntry = getBrowserPaneSearchEntries({ isMac: true }).find( (entry) => entry.title === 'Link Routing' ) @@ -20,7 +33,14 @@ describe('browser settings search copy', () => { expect(defaultZoomEntry?.keywords).toContain('zoom') }) - it('uses Ctrl shortcut keywords for Link Routing search metadata off macOS', () => { + it('uses Ctrl shortcut text for Link Routing copy and search metadata off macOS', () => { + expect(getBrowserLinkRoutingShortcutLabel({ isMac: false })).toBe('Shift+Ctrl+click') + + const description = getBrowserLinkRoutingDescription({ isMac: false }) + expect(description).toContain('Shift+Ctrl+click') + expect(description).not.toContain('Cmd/Ctrl') + expect(description).not.toMatch(/\{\{.+?\}\}/) + const linkRoutingEntry = getBrowserPaneSearchEntries({ isMac: false }).find( (entry) => entry.title === 'Link Routing' ) @@ -102,3 +122,61 @@ describe('browser link routing modifier copy', () => { expect(entry?.keywords).toContain(getLinkRoutingModifierTitle(true)) }) }) + +// The bug this file guards: the Link Routing description was a bare template +// literal, so it stayed English in every locale. Asserting only "no {{...}} leaked" +// cannot catch that — the English literal has no placeholder either. +describe('Link Routing description localization', () => { + const KEY = 'auto.components.settings.BrowserLinkRoutingSetting.description' + const BASE_KEY = 'auto.components.settings.BrowserLinkRoutingSetting.descriptionBase' + + beforeEach(async () => { + await i18n.changeLanguage('en') + }) + + it('renders the Korean copy with the shortcut interpolated', async () => { + const koCopy = ( + ko.auto.components.settings.BrowserLinkRoutingSetting as unknown as Record + )['description'] + expect(koCopy).toBeTruthy() + expect(koCopy).toContain('{{shortcut}}') + + i18n.addResourceBundle('ko', 'translation', ko, true, true) + await i18n.changeLanguage('ko') + + const description = getBrowserLinkRoutingDescription({ isMac: true }) + expect(description).toBe(koCopy.replace('{{shortcut}}', '⇧⌘-click')) + expect(description).not.toMatch(/\{\{.+?\}\}/) + // Fails when the copy is a hardcoded English literal. + expect(description).not.toContain("Orca's built-in browser") + + // The entry title is localized too, so match on the description instead. + const entry = getBrowserPaneSearchEntries({ isMac: true }).find( + (item) => item.description === description + ) + expect(entry).toBeDefined() + + await i18n.changeLanguage('en') + expect(getBrowserLinkRoutingDescription({ isMac: true })).toContain("Orca's built-in browser") + }) + + it('renders the Korean copy for the invert-on variant', async () => { + const koBase = ( + ko.auto.components.settings.BrowserLinkRoutingSetting as unknown as Record + )['descriptionBase'] + expect(koBase).toBeTruthy() + + i18n.addResourceBundle('ko', 'translation', ko, true, true) + await i18n.changeLanguage('ko') + + const description = getBrowserLinkRoutingDescription({ isMac: true }, true) + expect(description).toBe(koBase) + // Fails when the invert-on branch regresses to a hardcoded English literal. + expect(description).not.toContain("Orca's built-in browser") + }) + + it('uses the catalog key rather than an inline literal', () => { + expect(i18n.exists(KEY)).toBe(true) + expect(i18n.exists(BASE_KEY)).toBe(true) + }) +}) diff --git a/src/renderer/src/components/settings/browser-search.ts b/src/renderer/src/components/settings/browser-search.ts index a9b204441429..67f5506de42c 100644 --- a/src/renderer/src/components/settings/browser-search.ts +++ b/src/renderer/src/components/settings/browser-search.ts @@ -2,9 +2,10 @@ import type { SettingsSearchEntry } from './settings-search' import { translate } from '@/i18n/i18n' import { translateSearchKeyword } from './settings-search-keywords' import { + getBrowserLinkRoutingDescription, getLinkRoutingModifierDescription, getLinkRoutingModifierTitle -} from './browser-link-routing-modifier-copy' +} from './browser-link-routing-copy' type BrowserShortcutPlatform = { isMac: boolean @@ -16,19 +17,6 @@ function getDefaultBrowserShortcutPlatform(): BrowserShortcutPlatform { } } -// Why: "always" stops being true once inverting is on, so only then does the nested -// row take over the chord sentence — with it off this reads exactly as it always has. -export function getBrowserLinkRoutingDescription( - platform: BrowserShortcutPlatform = getDefaultBrowserShortcutPlatform(), - modifierInverts = false -): string { - const base = - "Open http(s) links in Orca's built-in browser — from the terminal, markdown, and the editor." - return modifierInverts - ? base - : `${base} ${platform.isMac ? '⇧⌘-click' : 'Shift+Ctrl+click'} always uses your system browser.` -} - export function getBrowserPaneSearchEntries( platform: BrowserShortcutPlatform = getDefaultBrowserShortcutPlatform() ): SettingsSearchEntry[] { diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 5b74621aad73..dab15537c25c 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -9685,6 +9685,10 @@ "unpackedHelp": "Development builds must be updated from their source checkout.", "legacyHelp": "Update this server manually once to enable remote updates." }, + "BrowserLinkRoutingSetting": { + "description": "Open http(s) links in Orca's built-in browser — from the terminal, markdown, and the editor. {{shortcut}} always uses your system browser.", + "descriptionBase": "Open http(s) links in Orca's built-in browser — from the terminal, markdown, and the editor." + }, "BrowserLinkRoutingModifierSetting": { "titleSystem": "Hold Shift to open in your web browser", "titleOrca": "Hold Shift to open in Orca", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index e29d1ebcee4e..5b886a20857d 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -9651,6 +9651,10 @@ "unpackedHelp": "Development builds must be updated from their source checkout.", "legacyHelp": "Update this server manually once to enable remote updates." }, + "BrowserLinkRoutingSetting": { + "description": "Abre los enlaces http(s) del terminal, Markdown y el editor en el navegador integrado de Orca. {{shortcut}} siempre usa el navegador del sistema.", + "descriptionBase": "Abre los enlaces http(s) del terminal, Markdown y el editor en el navegador integrado de Orca." + }, "BrowserLinkRoutingModifierSetting": { "titleSystem": "Hold Shift to open in your web browser", "titleOrca": "Hold Shift to open in Orca", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 8def52442443..25c60820949a 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -9651,6 +9651,10 @@ "unpackedHelp": "開発ビルドはソースチェックアウトから更新する必要があります。", "legacyHelp": "リモート更新を有効にするには、このサーバーを手動で一度更新してください。" }, + "BrowserLinkRoutingSetting": { + "description": "ターミナル、Markdown、エディターの http(s) リンクを Orca の組み込みブラウザで開きます。{{shortcut}} は常にシステム ブラウザを使用します。", + "descriptionBase": "ターミナル、Markdown、エディターの http(s) リンクを Orca の組み込みブラウザで開きます。" + }, "BrowserLinkRoutingModifierSetting": { "titleSystem": "Hold Shift to open in your web browser", "titleOrca": "Hold Shift to open in Orca", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index e1cc97e3a0dd..95eef890f570 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -9651,6 +9651,10 @@ "unpackedHelp": "개발 빌드는 소스 체크아웃에서 업데이트해야 합니다.", "legacyHelp": "원격 업데이트를 활성화하려면 이 서버를 수동으로 한 번 업데이트하세요." }, + "BrowserLinkRoutingSetting": { + "description": "터미널, Markdown, 편집기의 http(s) 링크를 Orca 내장 브라우저에서 엽니다. {{shortcut}}을(를) 누르면 항상 시스템 브라우저를 사용합니다.", + "descriptionBase": "터미널, Markdown, 편집기의 http(s) 링크를 Orca 내장 브라우저에서 엽니다." + }, "BrowserLinkRoutingModifierSetting": { "titleSystem": "Hold Shift to open in your web browser", "titleOrca": "Hold Shift to open in Orca", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 40b1c6b3cc46..f0f620efb364 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -9651,6 +9651,10 @@ "unpackedHelp": "开发版本必须从其源代码检出进行更新。", "legacyHelp": "手动更新此服务器一次以启用远程更新。" }, + "BrowserLinkRoutingSetting": { + "description": "在 Orca 内置浏览器中打开来自终端、Markdown 和编辑器的 http(s) 链接。{{shortcut}} 始终使用系统浏览器。", + "descriptionBase": "在 Orca 内置浏览器中打开来自终端、Markdown 和编辑器的 http(s) 链接。" + }, "BrowserLinkRoutingModifierSetting": { "titleSystem": "Hold Shift to open in your web browser", "titleOrca": "Hold Shift to open in Orca",