localize handoff editor button

This commit is contained in:
qiongyu1999
2026-05-21 17:25:09 +08:00
parent 6c51768d87
commit bdc57655d6
22 changed files with 190 additions and 7 deletions
+9 -7
View File
@@ -11,6 +11,7 @@ import type {
HostEditorsResponse,
} from '@open-design/contracts';
import { fetchHostEditors, openProjectInEditor } from '../providers/registry';
import { useT } from '../i18n';
import { Icon } from './Icon';
import { EditorIcon } from './EditorIcon';
@@ -42,6 +43,7 @@ function writePreferred(id: HostEditorId): void {
}
export function HandoffButton({ projectId, onRequestRevealInFinder }: Props) {
const t = useT();
const [editors, setEditors] = useState<HostEditor[]>([]);
const [platform, setPlatform] = useState<HostEditorsResponse['platform']>('unknown');
const [loaded, setLoaded] = useState(false);
@@ -135,7 +137,7 @@ export function HandoffButton({ projectId, onRequestRevealInFinder }: Props) {
<button
type="button"
className="handoff-trigger handoff-trigger--solo"
title={`No editors found on $PATH — opens in ${fallbackLabel}`}
title={t('handoff.fallbackTitle', { target: fallbackLabel })}
onClick={() => onRequestRevealInFinder?.()}
>
<EditorIcon editorId={fallbackId} size={20} />
@@ -159,7 +161,7 @@ export function HandoffButton({ projectId, onRequestRevealInFinder }: Props) {
type="button"
className="handoff-trigger"
data-testid="handoff-trigger"
title={primary ? `交付给 ${primary.label}` : '交付'}
title={primary ? t('handoff.toTarget', { target: primary.label }) : t('handoff.action')}
onClick={() => {
if (primary && busy !== primary.id) {
void launch(primary);
@@ -173,20 +175,20 @@ export function HandoffButton({ projectId, onRequestRevealInFinder }: Props) {
<>
<EditorIcon editorId={primary.id} size={20} />
<span className="handoff-trigger-label">
交付给 {primary.label}
{t('handoff.toTarget', { target: primary.label })}
</span>
</>
) : (
<>
<EditorIcon editorId="finder" size={20} />
<span className="handoff-trigger-label">交付</span>
<span className="handoff-trigger-label">{t('handoff.action')}</span>
</>
)}
</button>
<button
type="button"
className="handoff-caret"
aria-label="Choose hand-off target"
aria-label={t('handoff.chooseTargetAria')}
data-testid="handoff-caret"
onClick={() => setOpen((v) => !v)}
disabled={busy !== null}
@@ -216,7 +218,7 @@ export function HandoffButton({ projectId, onRequestRevealInFinder }: Props) {
{unavailable.length > 0 ? (
<>
<div className="handoff-menu-divider" />
<div className="handoff-menu-section">Not installed</div>
<div className="handoff-menu-section">{t('handoff.notInstalled')}</div>
{unavailable.map((editor) => (
<button
key={editor.id}
@@ -226,7 +228,7 @@ export function HandoffButton({ projectId, onRequestRevealInFinder }: Props) {
data-testid={`handoff-menu-item-${editor.id}`}
onClick={() => void launch(editor)}
disabled={busy === editor.id}
title={`${editor.label} — not detected on $PATH`}
title={t('handoff.notDetectedTitle', { target: editor.label })}
>
<EditorIcon editorId={editor.id} size={20} />
<span>{editor.label}</span>
+6
View File
@@ -15,6 +15,12 @@ export const ar: Dict = {
'workingDirPicker.showInFileManager': "Show in file manager",
'workingDirPicker.replace': "Clear and replace directory…",
'workingDirPicker.recent': "Recent directories",
'handoff.toTarget': 'Hand off to {target}',
'handoff.action': 'Hand off',
'handoff.fallbackTitle': 'No editors found on $PATH - opens in {target}',
'handoff.chooseTargetAria': 'Choose hand-off target',
'handoff.notInstalled': 'Not installed',
'handoff.notDetectedTitle': '{target} - not detected on $PATH',
'homeHero.promptExamples': "أمثلة",
'homeHero.footer.designSystem': "النمط",
'homeHero.footer.autoDesignSystem': "تلقائي",
+6
View File
@@ -15,6 +15,12 @@ export const de: Dict = {
'workingDirPicker.showInFileManager': "Show in file manager",
'workingDirPicker.replace': "Clear and replace directory…",
'workingDirPicker.recent': "Recent directories",
'handoff.toTarget': 'Hand off to {target}',
'handoff.action': 'Hand off',
'handoff.fallbackTitle': 'No editors found on $PATH - opens in {target}',
'handoff.chooseTargetAria': 'Choose hand-off target',
'handoff.notInstalled': 'Not installed',
'handoff.notDetectedTitle': '{target} - not detected on $PATH',
'homeHero.promptExamples': "Beispiele",
'homeHero.footer.designSystem': "Stil",
'homeHero.footer.autoDesignSystem': "Auto",
+6
View File
@@ -479,6 +479,12 @@ export const en: Dict = {
'workingDirPicker.showInFileManager': 'Show in file manager',
'workingDirPicker.replace': 'Clear and replace directory…',
'workingDirPicker.recent': 'Recent directories',
'handoff.toTarget': 'Hand off to {target}',
'handoff.action': 'Hand off',
'handoff.fallbackTitle': 'No editors found on $PATH - opens in {target}',
'handoff.chooseTargetAria': 'Choose hand-off target',
'handoff.notInstalled': 'Not installed',
'handoff.notDetectedTitle': '{target} - not detected on $PATH',
'designSystemPicker.select': 'Choose design system',
'designSystemPicker.loading': 'Loading design systems…',
'designSystemPicker.searchPlaceholder': 'Search design systems (title / category / summary)',
+6
View File
@@ -15,6 +15,12 @@ export const esES: Dict = {
'workingDirPicker.showInFileManager': "Show in file manager",
'workingDirPicker.replace': "Clear and replace directory…",
'workingDirPicker.recent': "Recent directories",
'handoff.toTarget': 'Hand off to {target}',
'handoff.action': 'Hand off',
'handoff.fallbackTitle': 'No editors found on $PATH - opens in {target}',
'handoff.chooseTargetAria': 'Choose hand-off target',
'handoff.notInstalled': 'Not installed',
'handoff.notDetectedTitle': '{target} - not detected on $PATH',
'homeHero.promptExamples': "Ejemplos",
'homeHero.footer.designSystem': "Estilo",
'homeHero.footer.autoDesignSystem': "Auto",
+6
View File
@@ -15,6 +15,12 @@ export const fa: Dict = {
'workingDirPicker.showInFileManager': "Show in file manager",
'workingDirPicker.replace': "Clear and replace directory…",
'workingDirPicker.recent': "Recent directories",
'handoff.toTarget': 'Hand off to {target}',
'handoff.action': 'Hand off',
'handoff.fallbackTitle': 'No editors found on $PATH - opens in {target}',
'handoff.chooseTargetAria': 'Choose hand-off target',
'handoff.notInstalled': 'Not installed',
'handoff.notDetectedTitle': '{target} - not detected on $PATH',
'homeHero.promptExamples': "نمونه‌ها",
'homeHero.footer.designSystem': "سبک",
'homeHero.footer.autoDesignSystem': "خودکار",
+6
View File
@@ -15,6 +15,12 @@ export const fr: Dict = {
'workingDirPicker.showInFileManager': "Show in file manager",
'workingDirPicker.replace': "Clear and replace directory…",
'workingDirPicker.recent': "Recent directories",
'handoff.toTarget': 'Hand off to {target}',
'handoff.action': 'Hand off',
'handoff.fallbackTitle': 'No editors found on $PATH - opens in {target}',
'handoff.chooseTargetAria': 'Choose hand-off target',
'handoff.notInstalled': 'Not installed',
'handoff.notDetectedTitle': '{target} - not detected on $PATH',
'homeHero.promptExamples': "Exemples",
'homeHero.footer.designSystem': "Style",
'homeHero.footer.autoDesignSystem': "Auto",
+6
View File
@@ -15,6 +15,12 @@ export const hu: Dict = {
'workingDirPicker.showInFileManager': "Show in file manager",
'workingDirPicker.replace': "Clear and replace directory…",
'workingDirPicker.recent': "Recent directories",
'handoff.toTarget': 'Hand off to {target}',
'handoff.action': 'Hand off',
'handoff.fallbackTitle': 'No editors found on $PATH - opens in {target}',
'handoff.chooseTargetAria': 'Choose hand-off target',
'handoff.notInstalled': 'Not installed',
'handoff.notDetectedTitle': '{target} - not detected on $PATH',
'homeHero.promptExamples': "Példák",
'homeHero.footer.designSystem': "Stílus",
'homeHero.footer.autoDesignSystem': "Auto",
+6
View File
@@ -15,6 +15,12 @@ export const id: Dict = {
'workingDirPicker.showInFileManager': "Show in file manager",
'workingDirPicker.replace': "Clear and replace directory…",
'workingDirPicker.recent': "Recent directories",
'handoff.toTarget': 'Hand off to {target}',
'handoff.action': 'Hand off',
'handoff.fallbackTitle': 'No editors found on $PATH - opens in {target}',
'handoff.chooseTargetAria': 'Choose hand-off target',
'handoff.notInstalled': 'Not installed',
'handoff.notDetectedTitle': '{target} - not detected on $PATH',
'homeHero.promptExamples': "Contoh",
'homeHero.footer.designSystem': "Gaya",
'homeHero.footer.autoDesignSystem': "Auto",
+6
View File
@@ -15,6 +15,12 @@ export const it: Dict = {
'workingDirPicker.showInFileManager': "Show in file manager",
'workingDirPicker.replace': "Clear and replace directory…",
'workingDirPicker.recent': "Recent directories",
'handoff.toTarget': 'Hand off to {target}',
'handoff.action': 'Hand off',
'handoff.fallbackTitle': 'No editors found on $PATH - opens in {target}',
'handoff.chooseTargetAria': 'Choose hand-off target',
'handoff.notInstalled': 'Not installed',
'handoff.notDetectedTitle': '{target} - not detected on $PATH',
'homeHero.promptExamples': "Esempi",
'homeHero.footer.designSystem': "Stile",
'homeHero.footer.autoDesignSystem': "Auto",
+6
View File
@@ -15,6 +15,12 @@ export const ja: Dict = {
'workingDirPicker.showInFileManager': "Show in file manager",
'workingDirPicker.replace': "Clear and replace directory…",
'workingDirPicker.recent': "Recent directories",
'handoff.toTarget': 'Hand off to {target}',
'handoff.action': 'Hand off',
'handoff.fallbackTitle': 'No editors found on $PATH - opens in {target}',
'handoff.chooseTargetAria': 'Choose hand-off target',
'handoff.notInstalled': 'Not installed',
'handoff.notDetectedTitle': '{target} - not detected on $PATH',
'homeHero.promptExamples': "例",
'homeHero.footer.designSystem': "スタイル",
'homeHero.footer.autoDesignSystem': "自動",
+6
View File
@@ -15,6 +15,12 @@ export const ko: Dict = {
'workingDirPicker.showInFileManager': "Show in file manager",
'workingDirPicker.replace': "Clear and replace directory…",
'workingDirPicker.recent': "Recent directories",
'handoff.toTarget': 'Hand off to {target}',
'handoff.action': 'Hand off',
'handoff.fallbackTitle': 'No editors found on $PATH - opens in {target}',
'handoff.chooseTargetAria': 'Choose hand-off target',
'handoff.notInstalled': 'Not installed',
'handoff.notDetectedTitle': '{target} - not detected on $PATH',
'homeHero.promptExamples': "예시",
'homeHero.footer.designSystem': "스타일",
'homeHero.footer.autoDesignSystem': "자동",
+6
View File
@@ -15,6 +15,12 @@ export const pl: Dict = {
'workingDirPicker.showInFileManager': "Show in file manager",
'workingDirPicker.replace': "Clear and replace directory…",
'workingDirPicker.recent': "Recent directories",
'handoff.toTarget': 'Hand off to {target}',
'handoff.action': 'Hand off',
'handoff.fallbackTitle': 'No editors found on $PATH - opens in {target}',
'handoff.chooseTargetAria': 'Choose hand-off target',
'handoff.notInstalled': 'Not installed',
'handoff.notDetectedTitle': '{target} - not detected on $PATH',
'homeHero.promptExamples': "Przykłady",
'homeHero.footer.designSystem': "Styl",
'homeHero.footer.autoDesignSystem': "Auto",
+6
View File
@@ -15,6 +15,12 @@ export const ptBR: Dict = {
'workingDirPicker.showInFileManager': "Show in file manager",
'workingDirPicker.replace': "Clear and replace directory…",
'workingDirPicker.recent': "Recent directories",
'handoff.toTarget': 'Hand off to {target}',
'handoff.action': 'Hand off',
'handoff.fallbackTitle': 'No editors found on $PATH - opens in {target}',
'handoff.chooseTargetAria': 'Choose hand-off target',
'handoff.notInstalled': 'Not installed',
'handoff.notDetectedTitle': '{target} - not detected on $PATH',
'homeHero.promptExamples': "Exemplos",
'homeHero.footer.designSystem': "Estilo",
'homeHero.footer.autoDesignSystem': "Auto",
+6
View File
@@ -15,6 +15,12 @@ export const ru: Dict = {
'workingDirPicker.showInFileManager': "Show in file manager",
'workingDirPicker.replace': "Clear and replace directory…",
'workingDirPicker.recent': "Recent directories",
'handoff.toTarget': 'Hand off to {target}',
'handoff.action': 'Hand off',
'handoff.fallbackTitle': 'No editors found on $PATH - opens in {target}',
'handoff.chooseTargetAria': 'Choose hand-off target',
'handoff.notInstalled': 'Not installed',
'handoff.notDetectedTitle': '{target} - not detected on $PATH',
'homeHero.promptExamples': "Примеры",
'homeHero.footer.designSystem': "Стиль",
'homeHero.footer.autoDesignSystem': "Авто",
+6
View File
@@ -15,6 +15,12 @@ export const th: Dict = {
'workingDirPicker.showInFileManager': "Show in file manager",
'workingDirPicker.replace': "Clear and replace directory…",
'workingDirPicker.recent': "Recent directories",
'handoff.toTarget': 'Hand off to {target}',
'handoff.action': 'Hand off',
'handoff.fallbackTitle': 'No editors found on $PATH - opens in {target}',
'handoff.chooseTargetAria': 'Choose hand-off target',
'handoff.notInstalled': 'Not installed',
'handoff.notDetectedTitle': '{target} - not detected on $PATH',
'homeHero.promptExamples': "ตัวอย่าง",
'homeHero.footer.designSystem': "สไตล์",
'homeHero.footer.autoDesignSystem': "อัตโนมัติ",
+6
View File
@@ -15,6 +15,12 @@ export const tr: Dict = {
'workingDirPicker.showInFileManager': "Show in file manager",
'workingDirPicker.replace': "Clear and replace directory…",
'workingDirPicker.recent': "Recent directories",
'handoff.toTarget': 'Hand off to {target}',
'handoff.action': 'Hand off',
'handoff.fallbackTitle': 'No editors found on $PATH - opens in {target}',
'handoff.chooseTargetAria': 'Choose hand-off target',
'handoff.notInstalled': 'Not installed',
'handoff.notDetectedTitle': '{target} - not detected on $PATH',
'homeHero.promptExamples': "Örnekler",
'homeHero.footer.designSystem': "Stil",
'homeHero.footer.autoDesignSystem': "Otomatik",
+6
View File
@@ -15,6 +15,12 @@ export const uk: Dict = {
'workingDirPicker.showInFileManager': "Show in file manager",
'workingDirPicker.replace': "Clear and replace directory…",
'workingDirPicker.recent': "Recent directories",
'handoff.toTarget': 'Hand off to {target}',
'handoff.action': 'Hand off',
'handoff.fallbackTitle': 'No editors found on $PATH - opens in {target}',
'handoff.chooseTargetAria': 'Choose hand-off target',
'handoff.notInstalled': 'Not installed',
'handoff.notDetectedTitle': '{target} - not detected on $PATH',
'homeHero.promptExamples': "Приклади",
'homeHero.footer.designSystem': "Стиль",
'homeHero.footer.autoDesignSystem': "Авто",
+6
View File
@@ -479,6 +479,12 @@ export const zhCN: Dict = {
'workingDirPicker.showInFileManager': '在文件管理器中显示',
'workingDirPicker.replace': '清空并替换目录…',
'workingDirPicker.recent': '最近使用的目录',
'handoff.toTarget': '交付给 {target}',
'handoff.action': '交付',
'handoff.fallbackTitle': '未找到编辑器 - 使用 {target} 打开',
'handoff.chooseTargetAria': '选择交付目标',
'handoff.notInstalled': '未安装',
'handoff.notDetectedTitle': '{target} - 未在 $PATH 中检测到',
'designSystemPicker.select': '选择设计系统',
'designSystemPicker.loading': '正在加载设计系统…',
'designSystemPicker.searchPlaceholder': '搜索设计系统(标题 / 分类 / 摘要)',
+6
View File
@@ -15,6 +15,12 @@ export const zhTW: Dict = {
'workingDirPicker.showInFileManager': "在檔案管理器中顯示",
'workingDirPicker.replace': "清空並替換目錄…",
'workingDirPicker.recent': "最近使用的目錄",
'handoff.toTarget': '交付給 {target}',
'handoff.action': '交付',
'handoff.fallbackTitle': '未找到編輯器 - 使用 {target} 開啟',
'handoff.chooseTargetAria': '選擇交付目標',
'handoff.notInstalled': '未安裝',
'handoff.notDetectedTitle': '{target} - 未在 $PATH 中偵測到',
'homeHero.promptExamples': "範例提示詞",
'homeHero.footer.designSystem': "風格",
'homeHero.footer.autoDesignSystem': "自動",
+6
View File
@@ -786,6 +786,12 @@ export interface Dict {
'workingDirPicker.showInFileManager': string;
'workingDirPicker.replace': string;
'workingDirPicker.recent': string;
'handoff.toTarget': string;
'handoff.action': string;
'handoff.fallbackTitle': string;
'handoff.chooseTargetAria': string;
'handoff.notInstalled': string;
'handoff.notDetectedTitle': string;
'designSystemPicker.select': string;
'designSystemPicker.loading': string;
'designSystemPicker.searchPlaceholder': string;
@@ -0,0 +1,61 @@
// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import type { HostEditor, HostEditorsResponse } from '@open-design/contracts';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { HandoffButton } from '../../src/components/HandoffButton';
import { I18nProvider, type Locale } from '../../src/i18n';
afterEach(() => {
cleanup();
vi.restoreAllMocks();
window.localStorage.clear();
});
function stubEditors(editors: HostEditor[], platform: HostEditorsResponse['platform'] = 'darwin') {
vi.stubGlobal('fetch', vi.fn<typeof fetch>(async (input) => {
if (String(input) === '/api/editors') {
return new Response(JSON.stringify({ editors, platform }), {
status: 200,
headers: { 'content-type': 'application/json' },
});
}
throw new Error(`unexpected fetch ${String(input)}`);
}));
}
function renderLocalized(locale: Locale) {
render(
<I18nProvider initial={locale}>
<HandoffButton projectId="project-1" />
</I18nProvider>,
);
}
describe('HandoffButton i18n', () => {
it('localizes the primary handoff label', async () => {
stubEditors([{ id: 'finder', label: 'Finder', available: true }]);
renderLocalized('en');
const trigger = await screen.findByTestId('handoff-trigger');
expect(trigger.getAttribute('title')).toBe('Hand off to Finder');
expect(trigger.textContent).toContain('Hand off to Finder');
});
it('localizes the unavailable editor section', async () => {
stubEditors([
{ id: 'finder', label: 'Finder', available: true },
{ id: 'cursor', label: 'Cursor', available: false },
]);
renderLocalized('zh-CN');
fireEvent.click(await screen.findByTestId('handoff-caret'));
expect(await screen.findByText('未安装')).toBeTruthy();
expect(screen.getByTestId('handoff-menu-item-cursor').getAttribute('title'))
.toBe('Cursor - 未在 $PATH 中检测到');
});
});