diff --git a/README.md b/README.md index b4f9d1f..bce796a 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ bound to a connection account, so platforms and multiple accounts work side by s - Saved chat history with deck, account and citation restoration across devices and backend restarts +The system interface is in English; user-entered text and external posts retain their original language. + `/` opens the daily home with a greeting, natural-language action proposals, flexible tasks/routines, messages, notes, and reading. `/deck` opens Twitter Lite. The shared sidebar also opens diff --git a/docs/execution-support.md b/docs/execution-support.md index 11a3af7..dbb085f 100644 --- a/docs/execution-support.md +++ b/docs/execution-support.md @@ -1,7 +1,7 @@ # Execution support prototype -The shared sidebar opens `/support` (連絡) and `/journal` (記録), alongside -リーダー and Twitter Lite. These are UI-only prototypes with fictional contacts +The shared sidebar opens `/support` (Messages) and `/journal` (Journal), alongside +Reader and Twitter Lite. These are UI-only prototypes with fictional contacts and in-memory state. Reloading resets the prototype state. Tasks, communication status, outcomes, and journal entries are shared between routes for the current browser session. No messages, calendar events, medical records, or Rensheng pages are written. @@ -37,21 +37,21 @@ the new communication and journal prototypes do not consume those accounts. ## Daily home `/` presents a greeting, a large input, and a flexible plan grouped as -「まずこれ」「いつものこと」「余裕があれば」. There are no fixed time slots. +Start here, Routines, and If there’s room. There are no fixed time slots. Tasks can be reordered, completed, deferred, restored, or followed as routine steps. Communications can be reviewed directly in a home dialog; changes also appear in `/support`. Journal entries and corrections appear on the home, and reader samples open for reading. Twitter Lite remains a separate work surface. -The input currently uses a deterministic Japanese mock interpreter, not an LLM. +The input currently uses a deterministic English/Japanese mock interpreter, not an LLM. It proposes typed actions for task creation/status/reordering, routine steps, contact reply/review or deferral, notes/weight/medication records, and navigation. Confirming a proposal dispatches the corresponding operation. Unknown or ambiguous input does not change state. Message sending still goes through the existing simulated review UI; this prototype performs no external send. -Examples: 「買い物を追加」「朝の支度をあとで」「青木さんに返信」 -「メモ:次は資料の2ページ目から」「体重は65.2kg」「リーダーを開く」. +Examples: `Add grocery shopping`, `Defer Morning routine`, `Reply to Aoki`, +`Note: Resume from page 2`, `Weight: 65.2 kg`, and `Open Reader`. The interaction reference is [Tiimo visual planning](https://www.tiimoapp.com/product/visual-planning), reviewed on 2026-09-28: combined routines and tasks, flexible reordering, small diff --git a/docs/vitals-preview.md b/docs/vitals-preview.md index 24b0aa2..6515db4 100644 --- a/docs/vitals-preview.md +++ b/docs/vitals-preview.md @@ -2,7 +2,7 @@ `/vitals` recreates the Body and Environment screenshots attached to [Alan Shiflett’s post](https://x.com/AlanShiflett/status/2103524300158366183). -The shared sidebar opens it as バイタル. Body categories and history, environment +The shared sidebar opens it as Vitals. Body categories and history, environment metric details, chart period/series selection, and simulated climate controls are interactive. Other screenshot navigation entries open a preview explanation. diff --git a/e2e/integrations/connections.test.ts b/e2e/integrations/connections.test.ts index 876fffa..b37234a 100644 --- a/e2e/integrations/connections.test.ts +++ b/e2e/integrations/connections.test.ts @@ -5,21 +5,21 @@ test('opens account management after hydration without inheriting real OAuth con }) => { await page.goto('/deck') await expect( - page.getByRole('button', { name: '名前を変更', includeHidden: true }), + page.getByRole('button', { name: 'Rename', includeHidden: true }), ).toBeEnabled() - const manage = page.getByRole('button', { name: '接続アカウントを管理' }) + const manage = page.getByRole('button', { name: 'Manage connected accounts' }) if (!(await manage.isVisible())) { - await page.getByRole('button', { name: 'サイドバーを切り替え' }).click() + await page.getByRole('button', { name: 'Toggle sidebar' }).click() } await expect(manage).toBeEnabled() await manage.click() - const dialog = page.getByRole('dialog', { name: '接続アカウント' }) + const dialog = page.getByRole('dialog', { name: 'Connected accounts' }) await expect(dialog).toBeVisible() await expect( - dialog.getByText('利用するMastodonサーバーがまだ設定されていません。'), + dialog.getByText('No Mastodon servers have been configured yet.'), ).toBeVisible() await expect( - dialog.getByRole('button', { name: 'Mastodonで認可する' }), + dialog.getByRole('button', { name: 'Authorize with Mastodon' }), ).toHaveCount(0) await page.keyboard.press('Escape') await expect(dialog).toHaveCount(0) @@ -32,19 +32,21 @@ test('opens account management from the inbox footer and restores focus on close }) => { await page.goto('/inbox') await page.waitForLoadState('networkidle') - const manage = page.getByRole('button', { name: '接続アカウントを管理' }) + const manage = page.getByRole('button', { name: 'Manage connected accounts' }) await expect(manage).toBeVisible() await manage.click() - const dialog = page.getByRole('dialog', { name: '接続アカウント' }) + const dialog = page.getByRole('dialog', { name: 'Connected accounts' }) await expect(dialog).toBeVisible() await expect( - dialog.getByText('利用するMastodonサーバーがまだ設定されていません。'), + dialog.getByText('No Mastodon servers have been configured yet.'), ).toBeVisible() expect((await a11y().analyze()).violations).toEqual([]) await page.keyboard.press('Escape') await expect(dialog).toHaveCount(0) await expect(manage).toBeFocused() - await expect(page.getByRole('textbox', { name: '記事を検索' })).toBeVisible() + await expect( + page.getByRole('textbox', { name: 'Search articles' }), + ).toBeVisible() }) test('rejects an unbound OAuth callback without contacting an instance', async ({ @@ -53,16 +55,16 @@ test('rejects an unbound OAuth callback without contacting an instance', async ( await page.goto('/oauth/mastodon/callback?state=invalid&code=not-a-real-code') await expect(page).toHaveURL(/\/deck\?mastodon=failed$/) await expect(page.getByRole('alert')).toContainText( - 'Mastodonの認可を完了できませんでした。', + 'Could not complete Mastodon authorization.', ) await expect( - page.getByRole('button', { name: '名前を変更', includeHidden: true }), + page.getByRole('button', { name: 'Rename', includeHidden: true }), ).toBeEnabled() - const manage = page.getByRole('button', { name: '接続アカウントを管理' }) + const manage = page.getByRole('button', { name: 'Manage connected accounts' }) if (!(await manage.isVisible())) { - await page.getByRole('button', { name: 'サイドバーを切り替え' }).click() + await page.getByRole('button', { name: 'Toggle sidebar' }).click() } await expect( - page.getByRole('button', { name: '接続アカウントを管理' }), + page.getByRole('button', { name: 'Manage connected accounts' }), ).toBeEnabled() }) diff --git a/e2e/integrations/deck.test.ts b/e2e/integrations/deck.test.ts index 8060cc4..d025b33 100644 --- a/e2e/integrations/deck.test.ts +++ b/e2e/integrations/deck.test.ts @@ -10,26 +10,26 @@ async function addColumn( kind = 'search', ) { await showDeckManagement(page) - await page.getByRole('button', { name: 'カラムを追加', exact: true }).click() - const editor = page.getByRole('form', { name: 'カラムを追加', exact: true }) - await editor.getByLabel('カラム名', { exact: true }).fill(title) + await page.getByRole('button', { name: 'Add column', exact: true }).click() + const editor = page.getByRole('form', { name: 'Add column', exact: true }) + await editor.getByLabel('Column name', { exact: true }).fill(title) await editor - .getByRole('combobox', { name: '接続プロファイル', exact: true }) + .getByRole('combobox', { name: 'Connection profile', exact: true }) .selectOption({ label: profile }) await editor - .getByRole('combobox', { name: 'カラムの種類', exact: true }) + .getByRole('combobox', { name: 'Column type', exact: true }) .selectOption(kind) await editor .getByLabel( kind === 'search' - ? 'Twitterの検索条件' + ? 'Twitter search query' : kind === 'user' - ? 'ユーザー' - : 'リスト', + ? 'User' + : 'List', { exact: true }, ) .fill(query) - await editor.getByRole('button', { name: '追加して検索' }).click() + await editor.getByRole('button', { name: 'Add and search' }).click() await closeMobileSidebar(page) await expect( page @@ -42,13 +42,13 @@ async function addColumn( test.beforeEach(async ({ page }) => { await page.goto('/deck') await expect( - page.getByRole('button', { name: 'デッキとして保存', exact: true }), + page.getByRole('button', { name: 'Save as deck', exact: true }), ).toBeEnabled() - await page - .getByRole('button', { name: 'デッキとして保存', exact: true }) - .click() + await page.getByRole('button', { name: 'Save as deck', exact: true }).click() await showDeckManagement(page) - await expect(page.getByText('サーバーに保存・端末間で共有')).toBeVisible() + await expect( + page.getByText('Saved to server · Shared across devices'), + ).toBeVisible() await closeMobileSidebar(page) }) @@ -65,19 +65,19 @@ test('binds identical searches to independent profiles through paging, editing a await expect( second.getByText('alternate · Latest · all page 1', { exact: true }), ).toBeVisible() - await first.getByRole('button', { name: '続きを読み込む' }).click() + await first.getByRole('button', { name: 'Load more' }).click() await expect(first.locator('article')).toHaveCount(2) await expect(second.locator('article')).toHaveCount(1) - await second.getByRole('button', { name: '続きを読み込む' }).click() + await second.getByRole('button', { name: 'Load more' }).click() await expect( second.getByText('alternate · Latest · all page 2'), ).toBeVisible() - await second.getByRole('button', { name: '別の接続の操作' }).click() - await page.getByRole('menuitem', { name: '編集', exact: true }).click() + await second.getByRole('button', { name: 'Actions for 別の接続' }).click() + await page.getByRole('menuitem', { name: 'Edit', exact: true }).click() await page - .getByRole('combobox', { name: '接続プロファイル', exact: true }) + .getByRole('combobox', { name: 'Connection profile', exact: true }) .selectOption({ label: 'e2e' }) - await page.getByRole('button', { name: '変更を保存' }).click() + await page.getByRole('button', { name: 'Save changes' }).click() await expect( second.getByText('Latest · all page 1', { exact: true }), ).toBeVisible() @@ -94,19 +94,19 @@ test('binds identical searches to independent profiles through paging, editing a test('creates, switches, renames and deletes persisted deck profiles', async ({ page, }) => { - await page.getByRole('button', { name: '名前を変更' }).click() - await page.getByLabel('調査テーマ・デッキ名').fill('最初の調査') - await page.getByRole('button', { name: '名前を保存' }).click() + await page.getByRole('button', { name: 'Rename' }).click() + await page.getByLabel('Research topic / deck name').fill('最初の調査') + await page.getByRole('button', { name: 'Save name' }).click() await addColumn(page, '最初の観点', 'first-deck') await showDeckManagement(page) - await page.getByRole('button', { name: 'デッキを作成', exact: true }).click() - await page.getByLabel('新しいデッキ名').fill('別の調査') - await page.getByRole('button', { name: '作成', exact: true }).click() + await page.getByRole('button', { name: 'Create deck', exact: true }).click() + await page.getByLabel('New deck name').fill('別の調査') + await page.getByRole('button', { name: 'Create', exact: true }).click() await expect(page.locator('.deck-column')).toHaveCount(0) await addColumn(page, '別の観点', 'second-deck', 'alternate') await showDeckManagement(page) await page - .getByLabel('デッキプロファイル', { exact: true }) + .getByLabel('Deck profile', { exact: true }) .selectOption({ label: '最初の調査' }) await closeMobileSidebar(page) await expect(page.locator('.deck-column h2')).toHaveText(['最初の観点']) @@ -114,18 +114,18 @@ test('creates, switches, renames and deletes persisted deck profiles', async ({ await expect(page.getByRole('heading', { level: 1 })).toHaveText('最初の調査') await showDeckManagement(page) await page - .getByLabel('デッキプロファイル', { exact: true }) + .getByLabel('Deck profile', { exact: true }) .selectOption({ label: '別の調査' }) await closeMobileSidebar(page) await expect(page.locator('.deck-column h2')).toHaveText(['別の観点']) await showDeckManagement(page) - await page.getByRole('button', { name: 'デッキを削除', exact: true }).click() - await page.getByRole('button', { name: '削除する', exact: true }).click() + await page.getByRole('button', { name: 'Delete deck', exact: true }).click() + await page.getByRole('button', { name: 'Delete', exact: true }).click() await page.reload() await expect(page.getByRole('heading', { level: 1 })).toHaveText('最初の調査') await showDeckManagement(page) await expect( - page.getByLabel('デッキプロファイル', { exact: true }).locator('option'), + page.getByLabel('Deck profile', { exact: true }).locator('option'), ).toHaveCount(1) }) @@ -138,17 +138,17 @@ test('reads user and list timelines within columns and reorders, deletes and res const list = page.getByRole('region', { name: 'リストの投稿', exact: true }) await expect(user.getByText('alternate profile page 1')).toBeVisible() await expect(list.getByText('list page 1')).toBeVisible() - await list.getByRole('button', { name: '続きを読み込む' }).click() + await list.getByRole('button', { name: 'Load more' }).click() await expect(list.getByText('list page 2')).toBeVisible() - await list.getByRole('button', { name: 'リストの投稿の操作' }).click() - await page.getByRole('menuitem', { name: 'リストの投稿を左へ' }).click() + await list.getByRole('button', { name: 'Actions for リストの投稿' }).click() + await page.getByRole('menuitem', { name: 'Move リストの投稿 left' }).click() await expect(page.locator('.deck-column h2')).toHaveText([ 'リストの投稿', 'アカウント', ]) - await list.getByRole('button', { name: 'リストの投稿の操作' }).click() - await page.getByRole('menuitem', { name: 'リストの投稿を削除' }).click() - await page.getByRole('button', { name: '元に戻す' }).click() + await list.getByRole('button', { name: 'Actions for リストの投稿' }).click() + await page.getByRole('menuitem', { name: 'Delete リストの投稿' }).click() + await page.getByRole('button', { name: 'Undo' }).click() await expect(page.locator('.deck-column h2')).toHaveText([ 'リストの投稿', 'アカウント', @@ -165,12 +165,12 @@ test('keeps other columns usable when pagination fails and retries that column', await addColumn(page, '別の検索', 'independent-deck') const failed = page.getByRole('region', { name: '失敗する検索', exact: true }) const other = page.getByRole('region', { name: '別の検索', exact: true }) - await failed.getByRole('button', { name: '続きを読み込む' }).click() + await failed.getByRole('button', { name: 'Load more' }).click() await expect(failed.getByRole('alert')).toBeVisible() await expect(failed.locator('article')).toHaveCount(1) - await other.getByRole('button', { name: '続きを読み込む' }).click() + await other.getByRole('button', { name: 'Load more' }).click() await expect(other.locator('article')).toHaveCount(2) - await failed.getByRole('button', { name: '再試行' }).click() + await failed.getByRole('button', { name: 'Retry' }).click() await expect(failed.locator('article')).toHaveCount(2) }) @@ -201,9 +201,7 @@ test('keeps the deck and editor accessible within narrow viewports', async ({ width: testInfo.project.name === 'desktop' ? 1440 : 393, height: 900, }) - await expect( - page.getByRole('navigation', { name: 'カラム一覧' }), - ).toHaveCount(0) + await expect(page.getByRole('navigation', { name: 'Columns' })).toHaveCount(0) await expect( page.getByRole('heading', { name: '公式アカウント', exact: true }), ).toBeVisible() @@ -213,7 +211,7 @@ test('keeps the deck and editor accessible within narrow viewports', async ({ fullPage: true, }) await showDeckManagement(page) - await page.getByRole('button', { name: 'カラムを追加', exact: true }).click() + await page.getByRole('button', { name: 'Add column', exact: true }).click() expect((await a11y().analyze()).violations).toEqual([]) await page.screenshot({ path: testInfo.outputPath('deck.png'), @@ -225,20 +223,22 @@ test('closes the column editor with Escape and restores focus without applying e page, }) => { await showDeckManagement(page) - const opener = page.getByRole('button', { name: 'カラムを追加', exact: true }) + const opener = page.getByRole('button', { name: 'Add column', exact: true }) await opener.click() await expect( - page.getByRole('dialog', { name: 'カラムを追加', exact: true }), + page.getByRole('dialog', { name: 'Add column', exact: true }), ).toBeVisible() - await page.getByLabel('カラム名', { exact: true }).fill('キャンセルする観点') + await page + .getByLabel('Column name', { exact: true }) + .fill('キャンセルする観点') await page.keyboard.press('Escape') await expect( - page.getByRole('dialog', { name: 'カラムを追加', exact: true }), + page.getByRole('dialog', { name: 'Add column', exact: true }), ).toHaveCount(0) await expect(opener).toBeFocused() await expect(page.locator('.deck-column')).toHaveCount(0) await opener.click() - await expect(page.getByLabel('カラム名', { exact: true })).toHaveValue('') + await expect(page.getByLabel('Column name', { exact: true })).toHaveValue('') }) test('does not overwrite unreadable storage on reload', async ({ page }) => { @@ -246,7 +246,7 @@ test('does not overwrite unreadable storage on reload', async ({ page }) => { localStorage.setItem('twitter-lite-research-deck', '{broken'), ) await page.reload() - await expect(page.getByRole('alert')).toContainText('読み込めませんでした') + await expect(page.getByRole('alert')).toContainText('Unable to load') expect( await page.evaluate(() => localStorage.getItem('twitter-lite-research-deck'), @@ -258,19 +258,19 @@ test('selects a list from the column profile catalog and refreshes it on profile page, }) => { await showDeckManagement(page) - await page.getByRole('button', { name: 'カラムを追加', exact: true }).click() - await page.getByLabel('カラム名', { exact: true }).fill('購読リスト') + await page.getByRole('button', { name: 'Add column', exact: true }).click() + await page.getByLabel('Column name', { exact: true }).fill('購読リスト') await page - .getByRole('combobox', { name: '接続プロファイル', exact: true }) + .getByRole('combobox', { name: 'Connection profile', exact: true }) .selectOption({ label: 'e2e' }) await page - .getByRole('combobox', { name: 'カラムの種類', exact: true }) + .getByRole('combobox', { name: 'Column type', exact: true }) .selectOption('list') await expect( page.getByRole('button', { name: 'My E2E List', exact: true }), ).toBeVisible() await page - .getByRole('combobox', { name: '接続プロファイル', exact: true }) + .getByRole('combobox', { name: 'Connection profile', exact: true }) .selectOption({ label: 'alternate' }) await expect( page.getByRole('button', { name: 'My E2E List', exact: true }), @@ -278,10 +278,10 @@ test('selects a list from the column profile catalog and refreshes it on profile await page .getByRole('button', { name: 'Alternate List', exact: true }) .click() - await expect(page.getByLabel('リスト', { exact: true })).toHaveValue( + await expect(page.getByLabel('List', { exact: true })).toHaveValue( '1234567890', ) - await page.getByRole('button', { name: '追加して検索' }).click() + await page.getByRole('button', { name: 'Add and search' }).click() await closeMobileSidebar(page) await expect( page @@ -302,27 +302,27 @@ test('keeps an open draft through remote edits and rejects its stale save', asyn try { const device = await other.newPage() await device.goto('/deck') - await page.getByRole('button', { name: '名前を変更' }).click() - await page.getByLabel('調査テーマ・デッキ名').fill('編集中の名前') - await device.getByRole('button', { name: '名前を変更' }).click() - await device.getByLabel('調査テーマ・デッキ名').fill('別の端末の変更') - await device.getByRole('button', { name: '名前を保存' }).click() + await page.getByRole('button', { name: 'Rename' }).click() + await page.getByLabel('Research topic / deck name').fill('編集中の名前') + await device.getByRole('button', { name: 'Rename' }).click() + await device.getByLabel('Research topic / deck name').fill('別の端末の変更') + await device.getByRole('button', { name: 'Save name' }).click() await expect(device.getByRole('heading', { level: 1 })).toHaveText( '別の端末の変更', ) await page.evaluate(() => window.dispatchEvent(new Event('focus'))) - await expect(page.getByLabel('調査テーマ・デッキ名')).toHaveValue( + await expect(page.getByLabel('Research topic / deck name')).toHaveValue( '編集中の名前', ) - await page.getByRole('button', { name: '名前を保存' }).click() + await page.getByRole('button', { name: 'Save name' }).click() await expect(page.getByRole('alert')).toContainText( - '別の端末で変更されました', + 'This deck changed on another device', ) - await expect(page.getByLabel('調査テーマ・デッキ名')).toHaveValue( + await expect(page.getByLabel('Research topic / deck name')).toHaveValue( '編集中の名前', ) await page.keyboard.press('Escape') - await page.getByRole('button', { name: '最新のデッキを再読み込み' }).click() + await page.getByRole('button', { name: 'Reload latest deck' }).click() await expect(page.getByRole('heading', { level: 1 })).toHaveText( '別の端末の変更', ) @@ -368,10 +368,10 @@ test('imports old browser decks explicitly and avoids duplicate imports after re ) await page.reload() await showDeckManagement(page) - const selector = page.getByLabel('デッキプロファイル', { exact: true }) + const selector = page.getByLabel('Deck profile', { exact: true }) await expect(selector.locator('option')).toHaveCount(1) await closeMobileSidebar(page) - await page.getByRole('button', { name: '旧デッキを取り込む' }).click() + await page.getByRole('button', { name: 'Import legacy decks' }).click() await showDeckManagement(page) await expect(selector.locator('option')).toHaveCount(2) await selector.selectOption({ label: '以前の調査' }) @@ -388,9 +388,9 @@ test('imports old browser decks explicitly and avoids duplicate imports after re legacy, ) await page.reload() - await page.getByRole('button', { name: '旧デッキを取り込む' }).click() + await page.getByRole('button', { name: 'Import legacy decks' }).click() await expect( - page.getByRole('button', { name: '旧デッキを取り込む' }), + page.getByRole('button', { name: 'Import legacy decks' }), ).toHaveCount(0) await showDeckManagement(page) await expect(selector.locator('option')).toHaveCount(2) @@ -400,19 +400,19 @@ test('preserves a new deck dialog and its draft across the mobile breakpoint', a page, }) => { await showDeckManagement(page) - await page.getByRole('button', { name: 'デッキを作成', exact: true }).click() - const dialog = page.getByRole('dialog', { name: 'デッキを作成', exact: true }) - await dialog.getByLabel('新しいデッキ名').fill('画面を回転しても残る調査') + await page.getByRole('button', { name: 'Create deck', exact: true }).click() + const dialog = page.getByRole('dialog', { name: 'Create deck', exact: true }) + await dialog.getByLabel('New deck name').fill('画面を回転しても残る調査') for (const width of [1024, 375, 1024]) { await page.setViewportSize({ width, height: 900 }) await expect(dialog).toHaveCount(1) await expect(dialog).toBeVisible() - await expect(dialog.getByLabel('新しいデッキ名')).toHaveValue( + await expect(dialog.getByLabel('New deck name')).toHaveValue( '画面を回転しても残る調査', ) await expect(page.locator('#new-deck-title')).toHaveCount(1) } - await dialog.getByRole('button', { name: '作成', exact: true }).click() + await dialog.getByRole('button', { name: 'Create', exact: true }).click() await expect(dialog).toHaveCount(0) await expect(page.getByRole('heading', { level: 1 })).toHaveText( '画面を回転しても残る調査', diff --git a/e2e/integrations/home.test.ts b/e2e/integrations/home.test.ts index e7c185d..610abaf 100644 --- a/e2e/integrations/home.test.ts +++ b/e2e/integrations/home.test.ts @@ -6,21 +6,17 @@ test('previews a natural-language memo and shares the executed record with journ await page.goto('/') await page.waitForLoadState('networkidle') const memo = '明日のレビューは導線から確認する' - await page - .getByRole('textbox', { name: 'ホームへの入力' }) - .fill(`メモ: ${memo}`) - await page.getByRole('button', { name: '候補を表示' }).click() - const execute = page.getByRole('button', { name: '実行する', exact: true }) + await page.getByRole('textbox', { name: 'Home input' }).fill(`Note: ${memo}`) + await page.getByRole('button', { name: 'Preview action' }).click() + const execute = page.getByRole('button', { name: 'Apply', exact: true }) await expect(execute).toBeVisible() await execute.click() - await page.getByRole('link', { name: '記録', exact: true }).first().click() - await expect(page.getByRole('article', { name: 'メモの記録' })).toContainText( + await page.getByRole('link', { name: 'Journal', exact: true }).first().click() + await expect(page.getByRole('article', { name: 'Note entry' })).toContainText( memo, ) - await page.getByRole('link', { name: 'ホーム', exact: true }).first().click() - await expect( - page.getByRole('textbox', { name: 'ホームへの入力' }), - ).toBeVisible() + await page.getByRole('link', { name: 'Home', exact: true }).first().click() + await expect(page.getByRole('textbox', { name: 'Home input' })).toBeVisible() }) test('keeps the home usable on narrow screens', async ({ @@ -32,7 +28,7 @@ test('keeps the home usable on narrow screens', async ({ for (const width of [320, 375, 768]) { await page.setViewportSize({ width, height: 850 }) await expect( - page.getByRole('textbox', { name: 'ホームへの入力' }), + page.getByRole('textbox', { name: 'Home input' }), ).toBeVisible() expect( await page.evaluate(() => document.documentElement.scrollWidth), @@ -47,28 +43,30 @@ test('adds tasks only after confirmation and can defer and restore them', async }) => { await page.goto('/') await page.waitForLoadState('networkidle') - const input = page.getByRole('textbox', { name: 'ホームへの入力' }) + const input = page.getByRole('textbox', { name: 'Home input' }) const complete = page.getByRole('button', { - name: '買い物を完了', + name: 'Complete grocery shopping', exact: true, }) - await input.fill('買い物を追加') - await page.getByRole('button', { name: '候補を表示' }).click() + await input.fill('Add grocery shopping') + await page.getByRole('button', { name: 'Preview action' }).click() await expect(complete).toHaveCount(0) - await page.getByRole('button', { name: '実行する', exact: true }).click() + await page.getByRole('button', { name: 'Apply', exact: true }).click() await expect(complete).toBeVisible() - await input.fill('買い物をあとで') - await page.getByRole('button', { name: '候補を表示' }).click() + await input.fill('Defer grocery shopping') + await page.getByRole('button', { name: 'Preview action' }).click() await expect(complete).toBeVisible() - await page.getByRole('button', { name: '実行する', exact: true }).click() + await page.getByRole('button', { name: 'Apply', exact: true }).click() await expect(complete).toHaveCount(0) await page.locator('.home-rest summary').click() - await page.getByRole('button', { name: '買い物を戻す', exact: true }).click() + await page + .getByRole('button', { name: 'Restore grocery shopping', exact: true }) + .click() await complete.click() await expect(complete).toHaveCount(0) await page.locator('.home-rest summary').click() await expect( - page.getByRole('button', { name: '買い物を戻す', exact: true }), + page.getByRole('button', { name: 'Restore grocery shopping', exact: true }), ).toBeVisible() }) @@ -78,30 +76,37 @@ test('resolves communication within home and carries the status to the shared qu }) => { await page.goto('/') await page.waitForLoadState('networkidle') - await page.getByRole('button', { name: '青木 遥との会話を開く' }).click() - const dialog = page.getByRole('dialog', { name: '青木 遥との連絡' }) + await page + .getByRole('button', { name: 'Open conversation with Haruka Aoki' }) + .click() + const dialog = page.getByRole('dialog', { + name: 'Conversation with Haruka Aoki', + }) await expect( - dialog.getByRole('textbox', { name: '返信', exact: true }), + dialog.getByRole('textbox', { name: 'Reply', exact: true }), ).toBeVisible() expect((await a11y().analyze()).violations).toEqual([]) - await dialog.getByRole('button', { name: '今は置いておく' }).click() + await dialog.getByRole('button', { name: 'Leave for later' }).click() await expect(dialog).toHaveCount(0) await expect( - page.getByRole('button', { name: '青木 遥との会話を開く' }), + page.getByRole('button', { name: 'Open conversation with Haruka Aoki' }), ).toHaveCount(0) - await page.getByRole('link', { name: '連絡', exact: true }).first().click() + await page + .getByRole('link', { name: 'Messages', exact: true }) + .first() + .click() const rows = page.locator('.support-row') await expect( - rows.filter({ hasText: '来週のレビュー、日程を決める' }), + rows.filter({ hasText: 'Schedule next week’s design review' }), ).toHaveCount(0) - await page.getByRole('button', { name: 'あとで', exact: true }).click() - await rows.filter({ hasText: '来週のレビュー、日程を決める' }).click() - await page.getByRole('button', { name: '今に戻す' }).click() - const back = page.getByRole('button', { name: '連絡一覧に戻る' }) + await page.getByRole('button', { name: 'Later', exact: true }).click() + await rows.filter({ hasText: 'Schedule next week’s design review' }).click() + await page.getByRole('button', { name: 'Move to Now' }).click() + const back = page.getByRole('button', { name: 'Back to messages' }) if (await back.isVisible()) await back.click() - await page.getByRole('link', { name: 'ホーム', exact: true }).first().click() + await page.getByRole('link', { name: 'Home', exact: true }).first().click() await expect( - page.getByRole('button', { name: '青木 遥との会話を開く' }), + page.getByRole('button', { name: 'Open conversation with Haruka Aoki' }), ).toBeVisible() }) @@ -110,25 +115,25 @@ test('updates the prose brief as priorities change and opens the referenced conv }) => { await page.goto('/') await page.waitForLoadState('networkidle') - const brief = page.getByRole('region', { name: '今日のブリーフ' }) - await expect(brief).toContainText('レビュー資料を確認する') - await expect(brief).toContainText('4件') + const brief = page.getByRole('region', { name: 'Daily brief' }) + await expect(brief).toContainText('Review the documents') + await expect(brief).toContainText('4 conversations') await page - .getByRole('button', { name: 'レビュー資料を確認するを完了', exact: true }) + .getByRole('button', { name: 'Complete Review the documents', exact: true }) .click() - await expect(brief).not.toContainText('レビュー資料を確認する') - await brief.getByRole('button', { name: '青木 遥さんへの返信' }).click() + await expect(brief).not.toContainText('Review the documents') + await brief.getByRole('button', { name: 'Reply to Haruka Aoki' }).click() const dialog = page.getByRole('dialog', { - name: '青木 遥との連絡', + name: 'Conversation with Haruka Aoki', exact: true, }) - await dialog.getByRole('button', { name: '今は置いておく' }).click() - await expect(brief).toContainText('3件') - await expect(brief).not.toContainText('青木 遥') + await dialog.getByRole('button', { name: 'Leave for later' }).click() + await expect(brief).toContainText('3 conversations') + await expect(brief).not.toContainText('Haruka Aoki') await page - .getByRole('textbox', { name: 'ホームへの入力' }) + .getByRole('textbox', { name: 'Home input' }) .fill('再開地点:資料の2ページ目から') - await page.getByRole('button', { name: '候補を表示' }).click() - await page.getByRole('button', { name: '実行する', exact: true }).click() + await page.getByRole('button', { name: 'Preview action' }).click() + await page.getByRole('button', { name: 'Apply', exact: true }).click() await expect(brief).toContainText('資料の2ページ目から') }) diff --git a/e2e/integrations/inbox.test.ts b/e2e/integrations/inbox.test.ts index 4670732..9e8fc7d 100644 --- a/e2e/integrations/inbox.test.ts +++ b/e2e/integrations/inbox.test.ts @@ -7,7 +7,7 @@ test('selects articles and keeps the reader focused on their content', async ({ await page.waitForLoadState('networkidle') const rows = page.locator('.inbox-article-row') await expect(rows).toHaveCount(9) - const articleTitle = 'エージェントに渡す文脈は、短くてもよい' + const articleTitle = 'Context for an agent can be brief' await rows.filter({ hasText: articleTitle }).click() const reader = page.getByRole('article', { name: articleTitle, exact: true }) await expect(reader).toBeVisible() @@ -16,26 +16,26 @@ test('selects articles and keeps the reader focused on their content', async ({ ).toBeVisible() await expect( reader.getByText( - 'まず目的を一文で書き、次に変えてはいけない条件を添える。', + 'Start with a one-sentence goal, then add the constraints that must hold.', { exact: false }, ), ).toBeVisible() await expect(page.getByText('FOR YOU', { exact: true })).toHaveCount(0) - await expect(page.getByText('モック', { exact: true })).toHaveCount(0) + await expect(page.getByText('Mock', { exact: true })).toHaveCount(0) + await expect(page.getByText('Why this article', { exact: true })).toHaveCount( + 0, + ) await expect( - page.getByText('この記事が届いた理由', { exact: true }), + page.getByRole('button', { name: /For you|Read later|Unread only/ }), ).toHaveCount(0) - await expect( - page.getByRole('button', { name: /あなたへ|あとで読む|未読のみ/ }), - ).toHaveCount(0) - const back = page.getByRole('button', { name: '記事一覧に戻る' }) + const back = page.getByRole('button', { name: 'Back to articles' }) if (await back.isVisible()) await back.click() await rows - .filter({ hasText: '3カラムの余白が、情報を読むリズムをつくる' }) + .filter({ hasText: 'Three columns create a rhythm for reading' }) .click() await expect( page.getByRole('article', { - name: '3カラムの余白が、情報を読むリズムをつくる', + name: 'Three columns create a rhythm for reading', exact: true, }), ).toBeVisible() @@ -49,21 +49,21 @@ test('recovers from empty search and shares navigation with Twitter Lite', async await page.waitForLoadState('networkidle') const rows = page.locator('.inbox-article-row') await page - .getByRole('textbox', { name: '記事を検索' }) - .fill('存在しない検索語xyz') + .getByRole('textbox', { name: 'Search articles' }) + .fill('nonexistent-query-xyz') await expect(rows).toHaveCount(0) await expect( - page.getByText('記事がありません', { exact: true }), + page.getByText('No articles found', { exact: true }), ).toBeVisible() - await page.getByRole('button', { name: '検索をクリア' }).click() + await page.getByRole('button', { name: 'Clear search' }).click() await expect(rows).toHaveCount(9) expect((await a11y().analyze()).violations).toEqual([]) await expect( - page.getByRole('button', { name: '興味を編集', exact: true }), + page.getByRole('button', { name: 'Edit interests', exact: true }), ).toHaveCount(0) await page.getByRole('link', { name: 'Twitter Lite', exact: true }).click() await expect(page).toHaveURL(/\/deck$/) - await page.getByRole('link', { name: 'リーダー', exact: true }).click() + await page.getByRole('link', { name: 'Reader', exact: true }).click() await expect(page).toHaveURL(/\/inbox$/) await expect(rows).toHaveCount(9) }) @@ -77,14 +77,14 @@ test('keeps article navigation readable without horizontal overflow', async ({ for (const width of [320, 375, 414, 768]) { await page.setViewportSize({ width, height: 850 }) await expect( - page.getByRole('textbox', { name: '記事を検索' }), + page.getByRole('textbox', { name: 'Search articles' }), ).toBeVisible() expect( await page.evaluate(() => document.documentElement.scrollWidth), ).toBe(width) await page.locator('.inbox-article-row').first().click() const reader = page.getByRole('article', { - name: 'ブラウザを見ながら考える。AIエージェントとの新しい開発の進め方', + name: 'Thinking in the browser: a new workflow with AI agents', exact: true, }) await expect(reader).toBeVisible() @@ -95,7 +95,7 @@ test('keeps article navigation readable without horizontal overflow', async ({ path: testInfo.outputPath(`inbox-reader-${width}.png`), }) if (width === 320) expect((await a11y().analyze()).violations).toEqual([]) - const back = page.getByRole('button', { name: '記事一覧に戻る' }) + const back = page.getByRole('button', { name: 'Back to articles' }) if (await back.isVisible()) await back.click() } }) diff --git a/e2e/integrations/research.test.ts b/e2e/integrations/research.test.ts index b4d6f3f..6114bad 100644 --- a/e2e/integrations/research.test.ts +++ b/e2e/integrations/research.test.ts @@ -43,9 +43,9 @@ test('restores the conversation and growing temporary deck from reconnect snapsh }) => { await page.goto('/deck') await showDeckManagement(page) - await page.getByRole('button', { name: 'カラムを追加', exact: true }).click() + await page.getByRole('button', { name: 'Add column', exact: true }).click() const accounts = page.getByRole('combobox', { - name: '接続プロファイル', + name: 'Connection profile', exact: true, }) await accounts.selectOption({ label: 'e2e' }) @@ -98,7 +98,7 @@ test('restores the conversation and growing temporary deck from reconnect snapsh ) await page.reload() await showResearch(page) - const conversation = page.getByRole('log', { name: '調査の会話' }) + const conversation = page.getByRole('log', { name: 'Research conversation' }) await expect(conversation).toContainText('既存の観点を使って調査して') await expect(conversation).toContainText('list_decks: 完了') await expect(conversation).toContainText('既存の検索条件を再利用しています。') @@ -135,11 +135,11 @@ test('restores the conversation and growing temporary deck from reconnect snapsh ]) await closeMobileSidebar(page) await expect( - page.getByRole('button', { name: 'デッキとして保存', exact: true }), + page.getByRole('button', { name: 'Save as deck', exact: true }), ).toBeEnabled() await showResearch(page) const accountPicker = page.getByRole('button', { - name: '接続アカウント · 1件', + name: 'Connected accounts · 1', }) await accountPicker.click() await expect(accountPicker).toHaveAttribute('aria-expanded', 'true') @@ -148,13 +148,11 @@ test('restores the conversation and growing temporary deck from reconnect snapsh ).toBeChecked() await accountPicker.click() await expect(accountPicker).toHaveAttribute('aria-expanded', 'false') - await page.getByLabel('メッセージ', { exact: true }).fill('この観点を詳しく') + await page.getByLabel('Message', { exact: true }).fill('この観点を詳しく') await expect( - page.getByRole('button', { name: '送信', exact: true }), + page.getByRole('button', { name: 'Send', exact: true }), ).toBeEnabled() - await page - .getByRole('button', { name: 'サイドバーを閉じる', exact: true }) - .click() + await page.getByRole('button', { name: 'Close sidebar', exact: true }).click() run.deckVersion = 3 if (run.deck) run.deck.title = '閉じている間に更新された調査' await expect(page.getByRole('heading', { level: 1 })).toHaveText( @@ -162,7 +160,7 @@ test('restores the conversation and growing temporary deck from reconnect snapsh { timeout: 15000 }, ) await showResearch(page) - await expect(page.getByLabel('メッセージ', { exact: true })).toHaveValue( + await expect(page.getByLabel('Message', { exact: true })).toHaveValue( 'この観点を詳しく', ) await page.setViewportSize({ width: 600, height: 900 }) @@ -170,20 +168,20 @@ test('restores the conversation and growing temporary deck from reconnect snapsh page.locator('[data-sidebar="sidebar"][data-mobile="true"]'), ).toHaveCount(1) await showResearch(page) - await expect(page.getByLabel('メッセージ', { exact: true })).toHaveValue( + await expect(page.getByLabel('Message', { exact: true })).toHaveValue( 'この観点を詳しく', ) - await page.getByRole('button', { name: '接続アカウント · 1件' }).click() + await page.getByRole('button', { name: 'Connected accounts · 1' }).click() await page.getByRole('checkbox', { name: 'e2e · Twitter' }).uncheck() await page.setViewportSize({ width: 1100, height: 900 }) await expect( page.locator('[data-sidebar="sidebar"][data-mobile="true"]'), ).toHaveCount(0) await showResearch(page) - await expect(page.getByLabel('メッセージ', { exact: true })).toHaveValue( + await expect(page.getByLabel('Message', { exact: true })).toHaveValue( 'この観点を詳しく', ) - await page.getByRole('button', { name: '接続アカウント · 0件' }).click() + await page.getByRole('button', { name: 'Connected accounts · 0' }).click() await expect( page.getByRole('checkbox', { name: 'e2e · Twitter' }), ).not.toBeChecked() @@ -192,10 +190,10 @@ test('restores the conversation and growing temporary deck from reconnect snapsh page.locator('[data-sidebar="sidebar"][data-mobile="true"]'), ).toHaveCount(1) await showResearch(page) - await expect(page.getByLabel('メッセージ', { exact: true })).toHaveValue( + await expect(page.getByLabel('Message', { exact: true })).toHaveValue( 'この観点を詳しく', ) - await page.getByRole('button', { name: '接続アカウント · 0件' }).click() + await page.getByRole('button', { name: 'Connected accounts · 0' }).click() await expect( page.getByRole('checkbox', { name: 'e2e · Twitter' }), ).not.toBeChecked() @@ -217,9 +215,9 @@ test('renders Markdown and reveals an unloaded citation in an offscreen deck col await page.emulateMedia({ reducedMotion: 'reduce' }) await page.goto('/deck') await showDeckManagement(page) - await page.getByRole('button', { name: 'カラムを追加', exact: true }).click() + await page.getByRole('button', { name: 'Add column', exact: true }).click() const accounts = page.getByRole('combobox', { - name: '接続プロファイル', + name: 'Connection profile', exact: true, }) await accounts.selectOption({ label: 'e2e' }) @@ -288,7 +286,7 @@ test('renders Markdown and reveals an unloaded citation in an offscreen deck col ) await page.reload() await showResearch(page) - const chat = page.getByRole('log', { name: '調査の会話' }) + const chat = page.getByRole('log', { name: 'Research conversation' }) await expect(chat.getByRole('heading', { name: '調査結果' })).toBeVisible() await expect(chat.getByRole('heading', { name: '調査結果' })).toHaveCSS( 'font-weight', @@ -309,7 +307,7 @@ test('renders Markdown and reveals an unloaded citation in an offscreen deck col await expect(card).toBeVisible() await expect(card).toHaveClass(/citation-highlight/) await expect(card).toBeFocused() - await expect(card).toContainText('調査時に取得した投稿') + await expect(card).toContainText('Post saved during research') await expect(card).toContainText('AIが調査時に取得した引用本文') expect( await page.locator('.deck-board').evaluate((element) => element.scrollLeft), @@ -327,23 +325,25 @@ test('explains unconfigured Codex and prevents starting research without affecti await expect(heading).toBeVisible() const originalTitle = await heading.textContent() await showResearch(page) - const panel = page.getByRole('complementary', { name: '調査チャット' }) + const panel = page.getByRole('complementary', { name: 'Research chat' }) await expect(panel).toBeVisible() await expect( - panel.getByText('自宅のCodexへの接続が設定されていません。'), + panel.getByText( + 'The connection to Codex on your home server is not configured.', + ), ).toBeVisible() await expect( - panel.getByRole('button', { name: '送信', exact: true }), + panel.getByRole('button', { name: 'Send', exact: true }), ).toBeDisabled() - await expect(panel.getByLabel('メッセージ', { exact: true })).toBeDisabled() + await expect(panel.getByLabel('Message', { exact: true })).toBeDisabled() const accessibility = await a11y().analyze() expect(accessibility.violations).toEqual([]) await closeMobileSidebar(page) await expect(heading).toHaveText(originalTitle ?? '') await showDeckManagement(page) - await page.getByRole('button', { name: 'デッキを作成', exact: true }).click() - await page.getByLabel('新しいデッキ名').fill('手動の調査') - await page.getByRole('button', { name: '作成', exact: true }).click() + await page.getByRole('button', { name: 'Create deck', exact: true }).click() + await page.getByLabel('New deck name').fill('手動の調査') + await page.getByRole('button', { name: 'Create', exact: true }).click() await closeMobileSidebar(page) await expect(heading).toHaveText('手動の調査') await showResearch(page) diff --git a/e2e/integrations/support.test.ts b/e2e/integrations/support.test.ts index cc65dd2..d49cfed 100644 --- a/e2e/integrations/support.test.ts +++ b/e2e/integrations/support.test.ts @@ -1,7 +1,7 @@ import { expect, test } from '../fixtures' -const reviewSubject = '来週のレビュー、日程を決める' -const coffeeSubject = '久しぶりに、お茶でも' +const reviewSubject = 'Schedule next week’s design review' +const coffeeSubject = 'Catch up over coffee' test('reviews an edited reply and keeps the simulated result in history', async ({ page, @@ -12,28 +12,31 @@ test('reviews an edited reply and keeps the simulated result in history', async const rows = page.locator('.support-row') await expect(rows).toHaveCount(3) await rows.filter({ hasText: reviewSubject }).click() - await page.getByRole('button', { name: '丁寧に', exact: true }).click() - const reply = page.getByRole('textbox', { name: '返信', exact: true }) - await expect(reply).toHaveValue(/資料をご共有/) - const edited = '資料を確認しました。初回の導線について当日お話しします。' + await page.getByRole('button', { name: 'Polite', exact: true }).click() + const reply = page.getByRole('textbox', { name: 'Reply', exact: true }) + await expect(reply).toHaveValue(/Thank you for sharing/) + const edited = + 'I reviewed the materials. Let’s discuss the first-time experience.' await reply.fill(edited) - const slot = '10月5日(月)14:00–14:30' - await page.getByRole('combobox', { name: '日程候補' }).selectOption(slot) - await page.getByRole('button', { name: '返信に入れる' }).click() - await expect(reply).toHaveValue(`${edited}\n\n${slot}はいかがでしょうか。`) - await page.getByRole('button', { name: '送信内容を確認' }).click() - const dialog = page.getByRole('dialog', { name: '送信内容の確認' }) + const slot = 'Mon, Oct 5, 14:00–14:30' + await page + .getByRole('combobox', { name: 'Suggested times' }) + .selectOption(slot) + await page.getByRole('button', { name: 'Add to reply' }).click() + await expect(reply).toHaveValue(`${edited}\n\nWould ${slot} work for you?`) + await page.getByRole('button', { name: 'Review reply' }).click() + const dialog = page.getByRole('dialog', { name: 'Review reply' }) await expect(dialog).toContainText(edited) await expect(dialog).toContainText(slot) - await expect(dialog).toContainText('外部には送信されません') + await expect(dialog).toContainText('No message will be sent') expect((await a11y().analyze()).violations).toEqual([]) - await dialog.getByRole('button', { name: '戻る', exact: true }).click() + await dialog.getByRole('button', { name: 'Back', exact: true }).click() await expect(reply).toHaveValue(new RegExp(edited)) - await page.getByRole('button', { name: '送信内容を確認' }).click() - await dialog.getByRole('button', { name: '送信を試す' }).click() + await page.getByRole('button', { name: 'Review reply' }).click() + await dialog.getByRole('button', { name: 'Try sending' }).click() await expect(dialog).toHaveCount(0) await expect(rows.filter({ hasText: reviewSubject })).toHaveCount(0) - await page.getByRole('button', { name: '履歴', exact: true }).click() + await page.getByRole('button', { name: 'History', exact: true }).click() await rows.filter({ hasText: reviewSubject }).click() await expect(page.locator('.support-outcome')).toContainText(edited) await expect(page.locator('.support-outcome')).toContainText(slot) @@ -47,21 +50,21 @@ test('defers a conversation and restores it to the current queue', async ({ const rows = page.locator('.support-row') await rows.filter({ hasText: coffeeSubject }).click() await page - .getByRole('textbox', { name: '返信', exact: true }) + .getByRole('textbox', { name: 'Reply', exact: true }) .fill('土曜の予定を確認してから返事するね。') - await page.getByRole('button', { name: '今は置いておく' }).click() + await page.getByRole('button', { name: 'Leave for later' }).click() await expect(rows.filter({ hasText: coffeeSubject })).toHaveCount(0) - await page.getByRole('button', { name: 'あとで', exact: true }).click() + await page.getByRole('button', { name: 'Later', exact: true }).click() await rows.filter({ hasText: coffeeSubject }).click() await expect( - page.getByRole('textbox', { name: '返信', exact: true }), + page.getByRole('textbox', { name: 'Reply', exact: true }), ).toHaveValue('土曜の予定を確認してから返事するね。') - await page.getByRole('button', { name: '今に戻す' }).click() - const back = page.getByRole('button', { name: '連絡一覧に戻る' }) + await page.getByRole('button', { name: 'Move to Now' }).click() + const back = page.getByRole('button', { name: 'Back to messages' }) if (await back.isVisible()) await back.click() await expect(rows.filter({ hasText: coffeeSubject })).toBeVisible() await expect( - page.getByRole('button', { name: '今', exact: true }), + page.getByRole('button', { name: 'Now', exact: true }), ).toHaveAttribute('aria-pressed', 'true') }) @@ -72,44 +75,52 @@ test('keeps corrections as new records and summarizes only selected current fact await page.goto('/journal') await page.waitForLoadState('networkidle') await expect( - page.getByRole('button', { name: '記録する', exact: true }), + page.getByRole('button', { name: 'Save entry', exact: true }), ).toBeDisabled() await page - .getByRole('textbox', { name: '記録内容' }) + .getByRole('textbox', { name: 'Entry text' }) .fill('次はレビュー資料の導線を確認する') - await page.getByRole('button', { name: '記録する', exact: true }).click() - await expect(page.getByRole('article', { name: 'メモの記録' })).toContainText( - '発生日時: 不明', + await page.getByRole('button', { name: 'Save entry', exact: true }).click() + await expect(page.getByRole('article', { name: 'Note entry' })).toContainText( + 'Occurred: Unknown', ) - await page.getByRole('button', { name: '体重', exact: true }).click() - await page.getByRole('spinbutton', { name: '体重', exact: true }).fill('65.2') - await page.getByRole('textbox', { name: '記録元' }).fill('自宅の体重計') - await page.getByRole('button', { name: '記録する', exact: true }).click() + await page.getByRole('button', { name: 'Weight', exact: true }).click() await page - .getByRole('article', { name: '体重の記録' }) - .getByRole('button', { name: '訂正する' }) + .getByRole('spinbutton', { name: 'Weight', exact: true }) + .fill('65.2') + await page.getByRole('textbox', { name: 'Source' }).fill('自宅の体重計') + await page.getByRole('button', { name: 'Save entry', exact: true }).click() + await page + .getByRole('article', { name: 'Weight entry' }) + .getByRole('button', { name: 'Correct' }) .click() - const correction = page.getByRole('dialog', { name: '記録を訂正' }) + const correction = page.getByRole('dialog', { name: 'Correct entry' }) await correction - .getByRole('spinbutton', { name: '体重', exact: true }) + .getByRole('spinbutton', { name: 'Weight', exact: true }) .fill('65.8') - await correction.getByRole('button', { name: '訂正を追加' }).click() - const weights = page.getByRole('article', { name: '体重の記録' }) + await correction.getByRole('button', { name: 'Add correction' }).click() + const weights = page.getByRole('article', { name: 'Weight entry' }) await expect(weights).toHaveCount(2) - await expect(weights.filter({ hasText: '65.2 kg' })).toContainText('訂正前') - await expect(weights.filter({ hasText: '65.8 kg' })).toContainText('訂正') + await expect(weights.filter({ hasText: '65.2 kg' })).toContainText('Original') + await expect(weights.filter({ hasText: '65.8 kg' })).toContainText( + 'Correction', + ) await page - .getByRole('button', { name: '記録をまとめる', exact: true }) + .getByRole('button', { name: 'Summarize entries', exact: true }) .click() - const review = page.getByRole('dialog', { name: '記録をまとめる' }) - const summary = review.getByRole('textbox', { name: '記録のまとめ' }) + const review = page.getByRole('dialog', { name: 'Summarize entries' }) + const summary = review.getByRole('textbox', { name: 'Entry summary' }) await expect(summary).toHaveValue(/65.8 kg/) await expect(summary).not.toHaveValue(/65.2 kg/) - await expect(summary).toHaveValue(/記録元: 自宅の体重計/) - await expect(summary).toHaveValue(/発生日時が不明の記録: 2件/) + await expect(summary).toHaveValue(/Source: 自宅の体重計/) + await expect(summary).toHaveValue( + /Entries with an unknown event time: 2 entries/, + ) await review.getByRole('checkbox', { name: /次はレビュー資料/ }).uncheck() await expect(summary).not.toHaveValue(/次はレビュー資料/) - await expect(summary).toHaveValue(/記録のない期間の服薬・体重は不明/) + await expect(summary).toHaveValue( + /Medication and weight are unknown for periods without entries/, + ) expect((await a11y().analyze()).violations).toEqual([]) }) @@ -126,18 +137,18 @@ test('fits communication and journal interactions in narrow screens', async ({ ).toBe(width) await page.locator('.support-row').first().click() await expect( - page.getByRole('textbox', { name: '返信', exact: true }), + page.getByRole('textbox', { name: 'Reply', exact: true }), ).toBeVisible() expect( await page.evaluate(() => document.documentElement.scrollWidth), ).toBe(width) if (width === 320) expect((await a11y().analyze()).violations).toEqual([]) - const back = page.getByRole('button', { name: '連絡一覧に戻る' }) + const back = page.getByRole('button', { name: 'Back to messages' }) if (await back.isVisible()) await back.click() - await page.getByRole('link', { name: '記録', exact: true }).click() - await page.getByRole('button', { name: '体重', exact: true }).click() + await page.getByRole('link', { name: 'Journal', exact: true }).click() + await page.getByRole('button', { name: 'Weight', exact: true }).click() await expect( - page.getByRole('spinbutton', { name: '体重', exact: true }), + page.getByRole('spinbutton', { name: 'Weight', exact: true }), ).toBeVisible() expect( await page.evaluate(() => document.documentElement.scrollWidth), diff --git a/e2e/integrations/vitals.test.ts b/e2e/integrations/vitals.test.ts index af5a622..fffaa31 100644 --- a/e2e/integrations/vitals.test.ts +++ b/e2e/integrations/vitals.test.ts @@ -17,7 +17,7 @@ test('filters body systems and opens lab history', async ({ page, a11y }) => { const dialog = page.getByRole('dialog', { name: 'Cardiovascular' }) await expect(dialog).toContainText('68') await dialog - .getByRole('button', { name: '閉じる', exact: true }) + .getByRole('button', { name: 'Close', exact: true }) .last() .click() await page.getByRole('button', { name: 'Lab history', exact: true }).click() @@ -38,7 +38,7 @@ test('explores environment charts and simulated climate controls', async ({ }) => { await page.goto('/vitals') await page.waitForLoadState('networkidle') - const menu = page.getByRole('button', { name: 'Healthメニューを開く' }) + const menu = page.getByRole('button', { name: 'Open Health menu' }) if (await menu.isVisible()) await menu.click() await page .getByRole('complementary', { name: 'Health navigation' }) @@ -95,7 +95,7 @@ test('keeps both dashboards within the viewport', async ({ await page.evaluate(() => document.documentElement.scrollWidth), ).toBe(width) await page.screenshot({ path: testInfo.outputPath(`body-${width}.png`) }) - const menu = page.getByRole('button', { name: 'Healthメニューを開く' }) + const menu = page.getByRole('button', { name: 'Open Health menu' }) if (await menu.isVisible()) await menu.click() const navigation = page.getByRole('complementary', { name: 'Health navigation', diff --git a/e2e/integrations/webmcp.test.ts b/e2e/integrations/webmcp.test.ts index 5ea17e7..b1be344 100644 --- a/e2e/integrations/webmcp.test.ts +++ b/e2e/integrations/webmcp.test.ts @@ -117,9 +117,9 @@ test('creates temporary research, edits it, persists explicitly and reopens it o ).toBeVisible() await page .getByRole('region', { name: '日本語', exact: true }) - .getByRole('button', { name: '日本語の操作' }) + .getByRole('button', { name: 'Actions for 日本語' }) .click() - await page.getByRole('menuitem', { name: '編集', exact: true }).click() + await page.getByRole('menuitem', { name: 'Edit', exact: true }).click() const replaced = await executeTool(page, 'set_deck', { deckId: deck.id, title: '更新した調査', @@ -234,7 +234,9 @@ test('keeps temporary views independent and loses only unsaved views on reload', ).toBe(false) expect(savedCount()).toEqual({ count: 0 }) await page.reload() - await expect(page.getByRole('heading', { level: 1 })).toHaveText('新しい調査') + await expect(page.getByRole('heading', { level: 1 })).toHaveText( + 'New research', + ) }) test('rejects invalid input without changing the active view', async ({ diff --git a/e2e/integrations/workspace.test.ts b/e2e/integrations/workspace.test.ts index 168f898..11bc7b8 100644 --- a/e2e/integrations/workspace.test.ts +++ b/e2e/integrations/workspace.test.ts @@ -9,10 +9,10 @@ test('keeps the sidebar, composer and deck reachable at compact widths', async ( for (const width of [320, 375, 414, 768]) { await page.setViewportSize({ width, height: 850 }) await showResearch(page) - const chat = page.getByRole('complementary', { name: '調査チャット' }) + const chat = page.getByRole('complementary', { name: 'Research chat' }) await expect(chat).toBeVisible() const composer = chat.getByRole('textbox', { - name: 'メッセージ', + name: 'Message', exact: true, }) await expect(composer).toBeVisible() @@ -30,12 +30,12 @@ test('keeps the sidebar, composer and deck reachable at compact widths', async ( if (width === 320) expect((await a11y().analyze()).violations).toEqual([]) await closeMobileSidebar(page) } - await page.getByRole('button', { name: 'サイドバーを切り替え' }).click() + await page.getByRole('button', { name: 'Toggle sidebar' }).click() await expect( - page.getByRole('complementary', { name: '調査チャット' }), + page.getByRole('complementary', { name: 'Research chat' }), ).toBeHidden() - await page.getByRole('button', { name: '調査チャット', exact: true }).click() + await page.getByRole('button', { name: 'Research chat', exact: true }).click() await expect( - page.getByRole('complementary', { name: '調査チャット' }), + page.getByRole('complementary', { name: 'Research chat' }), ).toBeVisible() }) diff --git a/e2e/workspace.ts b/e2e/workspace.ts index 5cbbf67..1a4b516 100644 --- a/e2e/workspace.ts +++ b/e2e/workspace.ts @@ -2,28 +2,28 @@ import { expect, type Page } from '@playwright/test' export async function showDeckManagement(page: Page) { await expect( - page.getByRole('button', { name: '名前を変更', includeHidden: true }), + page.getByRole('button', { name: 'Rename', includeHidden: true }), ).toBeEnabled() const navigation = page.getByRole('button', { - name: 'デッキ管理', + name: 'Deck manager', exact: true, }) if (!(await navigation.isVisible())) { - await page.getByRole('button', { name: 'サイドバーを切り替え' }).click() + await page.getByRole('button', { name: 'Toggle sidebar' }).click() } await navigation.click() } export async function showResearch(page: Page) { await expect( - page.getByRole('button', { name: '名前を変更', includeHidden: true }), + page.getByRole('button', { name: 'Rename', includeHidden: true }), ).toBeEnabled() const navigation = page.getByRole('button', { - name: '調査チャット', + name: 'Research chat', exact: true, }) if (!(await navigation.isVisible())) { - await page.getByRole('button', { name: 'サイドバーを切り替え' }).click() + await page.getByRole('button', { name: 'Toggle sidebar' }).click() } await navigation.click() } @@ -32,7 +32,7 @@ export async function closeMobileSidebar(page: Page) { const sheet = page.locator('[data-mobile="true"][data-sidebar="sidebar"]') if (await sheet.isVisible()) { await page - .getByRole('button', { name: 'サイドバーを閉じる', exact: true }) + .getByRole('button', { name: 'Close sidebar', exact: true }) .click() await expect(sheet).toBeHidden() } diff --git a/src/components/app-shell.tsx b/src/components/app-shell.tsx index d15170a..2a9b5ab 100644 --- a/src/components/app-shell.tsx +++ b/src/components/app-shell.tsx @@ -46,8 +46,8 @@ function Workspace({ const [panel, setPanel] = useState<'chat' | 'decks'>('chat') const { setOpen, setOpenMobile, isMobile } = useSidebar() const navigation = [ - { id: 'chat', title: '調査チャット', icon: MessageSquare }, - { id: 'decks', title: 'デッキ管理', icon: Columns3 }, + { id: 'chat', title: 'Research chat', icon: MessageSquare }, + { id: 'decks', title: 'Deck manager', icon: Columns3 }, ] as const return ( @@ -61,8 +61,8 @@ function Workspace({ footer={ isMobile ? setOpenMobile(false) : setOpen(false) } @@ -104,29 +104,29 @@ function Workspace({ {researchChat(() => setOpenMobile(false))}
-

デッキ

+

Decks

{sidebar}
- + Twitter Lite - 調査ワークスペース + Research workspace
{children}
diff --git a/src/components/dialog.tsx b/src/components/dialog.tsx index 8d21064..791892b 100644 --- a/src/components/dialog.tsx +++ b/src/components/dialog.tsx @@ -41,7 +41,7 @@ export function Dialog({ variant="ghost" size="icon-sm" className="absolute right-2 top-2" - aria-label="閉じる" + aria-label="Close" /> } > diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx index f6f5155..ba07896 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/ui/sidebar.tsx @@ -189,8 +189,10 @@ function Sidebar({ side={side} > - ワークスペース - 調査チャットとデッキ管理 + Workspace + + Research chat and deck management +
{children}
diff --git a/src/components/workspace-navigation.tsx b/src/components/workspace-navigation.tsx index b391159..a75f0ea 100644 --- a/src/components/workspace-navigation.tsx +++ b/src/components/workspace-navigation.tsx @@ -38,27 +38,27 @@ export function WorkspaceNavigation({ const [managingAccounts, setManagingAccounts] = useState(false) const pathname = useLocation({ select: (location) => location.pathname }) const navigation = [ - { title: 'ホーム', to: '/', icon: House, active: pathname === '/' }, + { title: 'Home', to: '/', icon: House, active: pathname === '/' }, { - title: 'バイタル', + title: 'Vitals', to: '/vitals', icon: Activity, active: pathname === '/vitals', }, { - title: '連絡', + title: 'Messages', to: '/support', icon: MessagesSquare, active: pathname === '/support', }, { - title: '記録', + title: 'Journal', to: '/journal', icon: NotebookPen, active: pathname === '/journal', }, { - title: 'リーダー', + title: 'Reader', to: '/inbox', icon: Inbox, active: pathname === '/inbox', @@ -80,7 +80,7 @@ export function WorkspaceNavigation({
@@ -123,8 +123,8 @@ export function WorkspaceNavigation({ setManagingAccounts(true)} > diff --git a/src/features/connections/connection-manager-dialog.tsx b/src/features/connections/connection-manager-dialog.tsx index c330423..c4c1531 100644 --- a/src/features/connections/connection-manager-dialog.tsx +++ b/src/features/connections/connection-manager-dialog.tsx @@ -20,14 +20,18 @@ export function ConnectionManagerDialog({ }) return ( - + {connections.isPending ? ( -

接続アカウントを読み込んでいます…

+

Loading connected accounts…

) : connections.isError ? (
-

接続アカウントを取得できませんでした。

+

Could not load connected accounts.

) : ( diff --git a/src/features/connections/connection-manager.test.tsx b/src/features/connections/connection-manager.test.tsx index c692881..da97bf1 100644 --- a/src/features/connections/connection-manager.test.tsx +++ b/src/features/connections/connection-manager.test.tsx @@ -60,11 +60,11 @@ it('shows per-account status and warns about temporary decks without blocking ac vi.fn(), true, ) - expect(screen.getByText('接続済み')).toBeVisible() - expect(screen.getByText('再接続が必要')).toBeVisible() - expect(screen.getByText(/一時デッキは消えます/)).toBeVisible() + expect(screen.getByText('Connected')).toBeVisible() + expect(screen.getByText('Reconnect required')).toBeVisible() + expect(screen.getByText(/Temporary decks will be lost/)).toBeVisible() expect( - await screen.findByRole('button', { name: 'Mastodonで認可する' }), + await screen.findByRole('button', { name: 'Authorize with Mastodon' }), ).toBeEnabled() }) @@ -73,29 +73,31 @@ it('uses the selected instance and reports a safe error when authorization fails new Error('private server credential detail'), ) show() - fireEvent.change(await screen.findByLabelText('Mastodonサーバー'), { + fireEvent.change(await screen.findByLabelText('Mastodon server'), { target: { value: 'https://second.invalid' }, }) - fireEvent.click(screen.getByRole('button', { name: 'Mastodonで認可する' })) + fireEvent.click( + screen.getByRole('button', { name: 'Authorize with Mastodon' }), + ) await waitFor(() => expect(actions.authorize).toHaveBeenCalledWith({ data: { origin: 'https://second.invalid' }, }), ) expect(await screen.findByRole('alert')).toHaveTextContent( - '認可を開始できませんでした', + 'Could not start Mastodon authorization', ) expect(screen.queryByText(/private server/)).toBeNull() expect( - screen.getByRole('button', { name: 'Mastodonで認可する' }), + screen.getByRole('button', { name: 'Authorize with Mastodon' }), ).toBeEnabled() }) it('binds reconnect to the selected account rather than whichever instance is selected for addition', async () => { actions.authorize.mockRejectedValue(new Error('offline')) show() - await screen.findByLabelText('Mastodonサーバー') - fireEvent.click(screen.getByRole('button', { name: '@firstを再接続' })) + await screen.findByLabelText('Mastodon server') + fireEvent.click(screen.getByRole('button', { name: 'Reconnect @first' })) await waitFor(() => expect(actions.authorize).toHaveBeenCalledWith({ data: { origin: account.origin, connectionId: account.id }, @@ -108,7 +110,7 @@ it('refreshes the account list after successful disconnect', async () => { const onChanged = vi.fn().mockResolvedValue(undefined) actions.disconnect.mockResolvedValue({ disconnected: true }) show([account], onChanged) - fireEvent.click(screen.getByRole('button', { name: '@firstの接続を解除' })) + fireEvent.click(screen.getByRole('button', { name: 'Disconnect @first' })) await waitFor(() => expect(onChanged).toHaveBeenCalledOnce()) expect(actions.disconnect).toHaveBeenCalledWith({ data: { connectionId: account.id }, @@ -119,21 +121,19 @@ it('keeps account controls available after a failed disconnect', async () => { actions.disconnect.mockRejectedValue(new Error('private token')) const onChanged = vi.fn() show([account], onChanged) - fireEvent.click(screen.getByRole('button', { name: '@firstの接続を解除' })) + fireEvent.click(screen.getByRole('button', { name: 'Disconnect @first' })) expect(await screen.findByRole('alert')).toHaveTextContent( - '接続を解除できませんでした', + 'Could not disconnect the account', ) expect(onChanged).not.toHaveBeenCalled() expect( - screen.getByRole('button', { name: '@firstの接続を解除' }), + screen.getByRole('button', { name: 'Disconnect @first' }), ).toBeEnabled() }) it('does not offer Mastodon reconnect or revoke for Twitter relay accounts', async () => { show([{ ...account, platform: 'twitter', id: 'twitter-first' }]) - await screen.findByLabelText('Mastodonサーバー') - expect(screen.queryByRole('button', { name: '@firstを再接続' })).toBeNull() - expect( - screen.queryByRole('button', { name: '@firstの接続を解除' }), - ).toBeNull() + await screen.findByLabelText('Mastodon server') + expect(screen.queryByRole('button', { name: 'Reconnect @first' })).toBeNull() + expect(screen.queryByRole('button', { name: 'Disconnect @first' })).toBeNull() }) diff --git a/src/features/connections/connection-manager.tsx b/src/features/connections/connection-manager.tsx index c141f09..ec485dd 100644 --- a/src/features/connections/connection-manager.tsx +++ b/src/features/connections/connection-manager.tsx @@ -14,9 +14,9 @@ import type { Connection } from './model' import './connection-manager.css' const statusNames = { - connected: '接続済み', - disconnected: '未接続', - expired: '再接続が必要', + connected: 'Connected', + disconnected: 'Disconnected', + expired: 'Reconnect required', } export function ConnectionManager({ @@ -52,7 +52,7 @@ export function ConnectionManager({ window.location.assign(result.authorizationUrl) } catch { setError( - 'Mastodonの認可を開始できませんでした。接続先を確認して再試行してください。', + 'Could not start Mastodon authorization. Check the server and try again.', ) } finally { setBusy(false) @@ -68,24 +68,27 @@ export function ConnectionManager({ await onChanged() } catch { setError( - '接続は解除しましたが、一覧を更新できませんでした。画面を再読み込みしてください。', + 'Account disconnected, but the list could not be refreshed. Reload the page.', ) } } catch { - setError('接続を解除できませんでした。時間をおいて再試行してください。') + setError('Could not disconnect the account. Try again later.') } finally { setBusy(false) } } return ( -
+

- 追加したアカウントは、どの端末のカラムからも選べます。 + Connected accounts are available in columns on all your devices.

{error &&

{error}

} {connections.length === 0 ? ( -

接続アカウントはまだありません。

+

No connected accounts yet.

) : (
    {connections.map((connection) => ( @@ -110,22 +113,22 @@ export function ConnectionManager({ variant="outline" type="button" disabled={busy} - aria-label={`${connection.displayName}を再接続`} + aria-label={`Reconnect ${connection.displayName}`} onClick={() => void connect(connection.origin, connection.id) } > - 再接続 + Reconnect {connection.status !== 'disconnected' && ( )} @@ -141,25 +144,25 @@ export function ConnectionManager({ if (origin) void connect(origin) }} > -

    Mastodonアカウントを追加

    +

    Add a Mastodon account

    {instances.isPending ? ( -

    接続先を確認しています…

    +

    Checking servers…

    ) : instances.isError ? (

    - 接続先を取得できませんでした。 + Could not load servers.

    ) : instances.data.origins.length === 0 ? ( -

    利用するMastodonサーバーがまだ設定されていません。

    +

    No Mastodon servers have been configured yet.

    ) : ( <> - + )} {hasTemporaryDecks && (

    - 認可画面へ移動すると、一時デッキは消えます。残したいデッキは先に保存してください。 + Temporary decks will be lost when you leave to authorize. Save any + decks you want to keep first.

    )} - {busy &&

    処理しています…

    } + {busy &&

    Working…

    }
) diff --git a/src/features/connections/repository.server.ts b/src/features/connections/repository.server.ts index 74f38dc..0b59360 100644 --- a/src/features/connections/repository.server.ts +++ b/src/features/connections/repository.server.ts @@ -9,9 +9,7 @@ import type { Connection } from './model' function relayOrigin(): string { const configured = process.env.TWITTER_RELAY_BASE_URL if (!configured) - throw new ProfileUnavailableError( - 'TWITTER_RELAY_BASE_URL を設定してください。', - ) + throw new ProfileUnavailableError('Set TWITTER_RELAY_BASE_URL.') return new URL(configured).origin } @@ -23,7 +21,7 @@ async function syncTwitterConnections( const profiles = new Set( await fetchProfiles(origin).catch(() => { throw new ProfileUnavailableError( - '接続プロファイルを確認できませんでした。Relay への接続を確認して再試行してください。', + 'Could not verify connection profiles. Check the relay connection and try again.', ) }), ) @@ -118,7 +116,7 @@ export async function requireTwitterConnection( !connection.relayProfile ) { throw new ProfileUnavailableError( - 'この接続は利用できません。カラムの接続アカウントを確認してください。', + 'This connection is unavailable. Check the connected account for this column.', ) } return connection.relayProfile diff --git a/src/features/connections/repository.test.ts b/src/features/connections/repository.test.ts index 46343d1..3052720 100644 --- a/src/features/connections/repository.test.ts +++ b/src/features/connections/repository.test.ts @@ -40,7 +40,7 @@ it('preserves unavailable bindings and reconnects the original ID when a profile ) await expect( requireTwitterConnection(id, database, async () => []), - ).rejects.toThrow('この接続は利用できません') + ).rejects.toThrow('This connection is unavailable') expect( (await listConnections(database, async () => ['first'])).connections, ).toEqual(original) @@ -69,7 +69,7 @@ it('does not reuse an old relay binding for the same profile at another origin', requireTwitterConnection(original[0]?.id ?? '', database, async () => [ 'first', ]), - ).rejects.toThrow('この接続は利用できません') + ).rejects.toThrow('This connection is unavailable') }) it('does not convert a relay outage into persisted account removal', async () => { @@ -80,7 +80,7 @@ it('does not convert a relay outage into persisted account removal', async () => const failed = await listConnections(database, unavailable) expect(failed.connections).toEqual(original.connections) expect(failed.relayError).toContain( - 'Relay への接続を確認して再試行してください。', + 'Check the relay connection and try again.', ) expect(database.select().from(connections).get()?.status).toBe('connected') expect(await listConnections(database, async () => ['first'])).toEqual( @@ -140,7 +140,7 @@ it('returns public metadata for all platforms without joining or exposing creden requireTwitterConnection('mastodon-account', database, async () => [ 'first', ]), - ).rejects.toThrow('この接続は利用できません') + ).rejects.toThrow('This connection is unavailable') expect( database .select() @@ -153,5 +153,5 @@ it('returns public metadata for all platforms without joining or exposing creden it('rejects an unknown connection without treating its ID as a relay profile', async () => { await expect( requireTwitterConnection('first', database, async () => ['first']), - ).rejects.toThrow('この接続は利用できません') + ).rejects.toThrow('This connection is unavailable') }) diff --git a/src/features/decks/column-editor.test.tsx b/src/features/decks/column-editor.test.tsx index 8b38f20..cb1b5d6 100644 --- a/src/features/decks/column-editor.test.tsx +++ b/src/features/decks/column-editor.test.tsx @@ -75,7 +75,7 @@ function show() { /> , ) - fireEvent.change(screen.getByLabelText('カラム名'), { + fireEvent.change(screen.getByLabelText('Column name'), { target: { value: '調査' }, }) return onSave @@ -83,16 +83,16 @@ function show() { it('creates a Mastodon search without Twitter ordering or following fields', async () => { const save = show() - fireEvent.change(screen.getByLabelText('接続プロファイル'), { + fireEvent.change(screen.getByLabelText('Connection profile'), { target: { value: 'mastodon-first' }, }) - expect(screen.queryByLabelText('表示順')).toBeNull() - expect(screen.queryByLabelText('フォロー中のみ')).toBeNull() - expect(screen.getByText(/結果が0件でも/)).toBeVisible() - fireEvent.change(screen.getByLabelText('Mastodonの検索条件'), { + expect(screen.queryByLabelText('Sort by')).toBeNull() + expect(screen.queryByLabelText('Following only')).toBeNull() + expect(screen.getByText(/Zero results/)).toBeVisible() + fireEvent.change(screen.getByLabelText('Mastodon search query'), { target: { value: 'WebMCP' }, }) - fireEvent.submit(screen.getByRole('form', { name: 'カラムを追加' })) + fireEvent.submit(screen.getByRole('form', { name: 'Add column' })) await waitFor(() => expect(save).toHaveBeenCalledWith( expect.objectContaining({ @@ -105,16 +105,16 @@ it('creates a Mastodon search without Twitter ordering or following fields', asy it('creates hashtags and resets unsupported source kind when changing to Twitter', async () => { const save = show() - fireEvent.change(screen.getByLabelText('接続プロファイル'), { + fireEvent.change(screen.getByLabelText('Connection profile'), { target: { value: 'mastodon-first' }, }) - fireEvent.change(screen.getByLabelText('カラムの種類'), { + fireEvent.change(screen.getByLabelText('Column type'), { target: { value: 'hashtag' }, }) - fireEvent.change(screen.getByLabelText('ハッシュタグ'), { + fireEvent.change(screen.getByLabelText('Hashtag'), { target: { value: 'WebMCP' }, }) - fireEvent.submit(screen.getByRole('form', { name: 'カラムを追加' })) + fireEvent.submit(screen.getByRole('form', { name: 'Add column' })) await waitFor(() => expect(save).toHaveBeenCalledWith( expect.objectContaining({ @@ -122,32 +122,32 @@ it('creates hashtags and resets unsupported source kind when changing to Twitter }), ), ) - fireEvent.change(screen.getByLabelText('接続プロファイル'), { + fireEvent.change(screen.getByLabelText('Connection profile'), { target: { value: 'twitter' }, }) - expect(screen.getByLabelText('カラムの種類')).toHaveValue('search') - expect(screen.getByLabelText('Twitterの検索条件')).toHaveValue('') - expect(screen.queryByRole('option', { name: 'ハッシュタグ' })).toBeNull() + expect(screen.getByLabelText('Column type')).toHaveValue('search') + expect(screen.getByLabelText('Twitter search query')).toHaveValue('') + expect(screen.queryByRole('option', { name: 'Hashtag' })).toBeNull() }) it('loads lists for each account and clears the selected list when the binding changes', async () => { show() - fireEvent.change(screen.getByLabelText('接続プロファイル'), { + fireEvent.change(screen.getByLabelText('Connection profile'), { target: { value: 'mastodon-first' }, }) - fireEvent.change(screen.getByLabelText('カラムの種類'), { + fireEvent.change(screen.getByLabelText('Column type'), { target: { value: 'list' }, }) fireEvent.click(await screen.findByRole('button', { name: 'mastodon-first' })) - expect(screen.getByLabelText('リスト')).toHaveValue('10') - fireEvent.change(screen.getByLabelText('接続プロファイル'), { + expect(screen.getByLabelText('List')).toHaveValue('10') + fireEvent.change(screen.getByLabelText('Connection profile'), { target: { value: 'mastodon-second' }, }) - expect(screen.getByLabelText('リスト')).toHaveValue('') + expect(screen.getByLabelText('List')).toHaveValue('') fireEvent.click( await screen.findByRole('button', { name: 'mastodon-second' }), ) - expect(screen.getByLabelText('リスト')).toHaveValue('20') + expect(screen.getByLabelText('List')).toHaveValue('20') expect(lists).toHaveBeenCalledWith({ data: { connectionId: 'mastodon-second' }, }) @@ -156,22 +156,22 @@ it('loads lists for each account and clears the selected list when the binding c it('preserves title and Twitter search options after a rejected save', async () => { const save = show() save.mockResolvedValue(false) - fireEvent.change(screen.getByLabelText('Twitterの検索条件'), { + fireEvent.change(screen.getByLabelText('Twitter search query'), { target: { value: 'from:owner WebMCP' }, }) - fireEvent.change(screen.getByLabelText('表示順'), { + fireEvent.change(screen.getByLabelText('Sort by'), { target: { value: 'Top' }, }) - fireEvent.click(screen.getByRole('checkbox', { name: 'フォロー中のみ' })) - fireEvent.submit(screen.getByRole('form', { name: 'カラムを追加' })) + fireEvent.click(screen.getByRole('checkbox', { name: 'Following only' })) + fireEvent.submit(screen.getByRole('form', { name: 'Add column' })) await waitFor(() => expect(save).toHaveBeenCalledOnce()) - expect(screen.getByLabelText('カラム名')).toHaveValue('調査') - expect(screen.getByLabelText('Twitterの検索条件')).toHaveValue( + expect(screen.getByLabelText('Column name')).toHaveValue('調査') + expect(screen.getByLabelText('Twitter search query')).toHaveValue( 'from:owner WebMCP', ) - expect(screen.getByLabelText('表示順')).toHaveValue('Top') - expect(screen.getByRole('checkbox', { name: 'フォロー中のみ' })).toBeChecked() - fireEvent.submit(screen.getByRole('form', { name: 'カラムを追加' })) + expect(screen.getByLabelText('Sort by')).toHaveValue('Top') + expect(screen.getByRole('checkbox', { name: 'Following only' })).toBeChecked() + fireEvent.submit(screen.getByRole('form', { name: 'Add column' })) await waitFor(() => expect(save).toHaveBeenCalledTimes(2)) expect(save.mock.calls[1]?.[0].source).toEqual(save.mock.calls[0]?.[0].source) }) diff --git a/src/features/decks/column-editor.tsx b/src/features/decks/column-editor.tsx index 11ac31b..87d7d94 100644 --- a/src/features/decks/column-editor.tsx +++ b/src/features/decks/column-editor.tsx @@ -40,7 +40,7 @@ export function ColumnEditor({ return (
{ event.preventDefault() setError(undefined) @@ -55,7 +55,7 @@ export function ColumnEditor({ if (!parsed.success || !validConnection) { setError( parsed.success - ? '接続プロファイルを選択してください。' + ? 'Select a connection profile.' : parsed.error.issues[0]?.message, ) return @@ -65,18 +65,18 @@ export function ColumnEditor({ >
- {connectionLabel ?? '接続が見つかりません'} + {connectionLabel ?? 'Connection not found'} @@ -126,16 +126,14 @@ export function ResearchColumn({

{sourceDescription.query}

- {query.isFetching - ? '投稿を取得しています…' - : `${posts.length}件を読み込み済み`} + {query.isFetching ? 'Loading posts…' : `${posts.length} posts loaded`}

{visiblePosts.map((post) => ( - 再試行 + Retry ) : null} ) : null} {!query.isPending && !query.isError && visiblePosts.length === 0 ? ( -

条件に一致する投稿はありません。

+

No posts match your filters.

) : null} {query.hasNextPage && !query.isFetchNextPageError ? (
@@ -179,7 +177,7 @@ export function ResearchColumn({ disabled={query.isFetching} onClick={() => void query.fetchNextPage({ cancelRefetch: false })} > - 続きを読み込む + Load more
) : null} @@ -187,7 +185,7 @@ export function ResearchColumn({ !query.isError && posts.length > 0 && !query.hasNextPage ? ( -

これ以上の投稿はありません。

+

No more posts.

) : null}
diff --git a/src/features/decks/deck-page.tsx b/src/features/decks/deck-page.tsx index d357988..e780150 100644 --- a/src/features/decks/deck-page.tsx +++ b/src/features/decks/deck-page.tsx @@ -126,7 +126,7 @@ export function DeckPage() { void refresh(true) }} > - 最新のデッキを再読み込み + Reload latest deck

) : null @@ -187,8 +187,8 @@ export function DeckPage() { />

{deck.persisted - ? 'サーバーに保存・端末間で共有' - : '一時ビュー · このタブのみ'} + ? 'Saved to server · Shared across devices' + : 'Temporary view · This tab only'}

} @@ -210,7 +210,7 @@ export function DeckPage() {

{deck.title}

- {deck.columns.length} / {MAX_COLUMNS} カラム + {deck.columns.length} / {MAX_COLUMNS} columns
@@ -220,11 +220,11 @@ export function DeckPage() { type="button" disabled={saving} onClick={() => { - createTemporary(`${deck.title}のコピー`, deck.columns) + createTemporary(`${deck.title} copy`, deck.columns) clearEditors() }} > - 一時コピーを作成 + Create temporary copy ) : ( )}
{saving ? (

- 保存しています… + Saving…

) : null} {legacyAvailable ? (

- このブラウザに旧デッキがあります。 + Legacy decks are available in this browser.

) : null} {!ready ? (

- デッキを読み込んでいます… + Loading decks…

) : null} {profiles.isError ? (

- 接続プロファイルを取得できませんでした。 + Unable to load connection profiles.

) : null} {oauthResult === 'failed' ? (

- Mastodonの認可を完了できませんでした。接続アカウントからやり直してください。 + Could not complete Mastodon authorization. Try again from + Connected accounts.

) : null} {oauthResult === 'connected' ? (

- Mastodonアカウントを接続しました。カラムの接続先に選べます。 + Mastodon account connected. You can now use it in a column.

) : null} {profiles.data?.relayError ? ( @@ -300,13 +301,13 @@ export function DeckPage() {

) : null} {profiles.isPending ? ( -

接続プロファイルを取得しています…

+

Loading connection profiles…

) : null} {!renaming && editing === null && !switcherEditing ? storageErrorNotice : null} {renaming ? ( - setRenaming(false)}> + setRenaming(false)}>
{ @@ -319,7 +320,7 @@ export function DeckPage() { }} >
{storageErrorNotice} @@ -337,7 +338,7 @@ export function DeckPage() { ) : null} {editing !== null ? ( setEditing(null)} > - 「{removed.column.title}」を削除しました。 + Deleted “{removed.column.title}”. ) : null} @@ -380,16 +381,17 @@ export function DeckPage() { -

調べたいことから、デッキを作りましょう

+

Start a deck with a question

- チャットでAIに調べたいことを伝えると、調査に合ったカラムを作成します。 - 検索・ユーザー投稿・リストのカラムを手動で追加することもできます。 + Tell the assistant what you want to research to create + relevant columns. You can also add search, user posts, and + list columns yourself. @@ -397,7 +399,7 @@ export function DeckPage() { {ready && deck.columns.length > 0 ? (
diff --git a/src/features/decks/deck-switcher.tsx b/src/features/decks/deck-switcher.tsx index 1d569ea..babe5fd 100644 --- a/src/features/decks/deck-switcher.tsx +++ b/src/features/decks/deck-switcher.tsx @@ -35,7 +35,7 @@ export function DeckSwitcher({ return (
- + ( {deck.title} - {deck.persisted ? '' : '(一時)'} + {deck.persisted ? '' : ' (temporary)'} ))}
{dialog === 'create' ? ( - onDialogChange(null)}> + onDialogChange(null)}>
{ @@ -94,7 +94,7 @@ export function DeckSwitcher({ }} > - +
{errorNotice}
) : null} {dialog === 'delete' ? ( - onDialogChange(null)}> + onDialogChange(null)}>
-

このデッキとカラムの設定を削除します。

+

Delete this deck and its column settings.

{errorNotice} diff --git a/src/features/decks/import.server.ts b/src/features/decks/import.server.ts index 44594e9..7dab701 100644 --- a/src/features/decks/import.server.ts +++ b/src/features/decks/import.server.ts @@ -29,7 +29,7 @@ export function importLegacyWorkspace( if (marker.payloadHash !== payloadHash) throw new DeckPersistenceError( 'conflict', - '旧デッキはすでに取り込み済みです。別の内容で上書きできません。', + 'Legacy decks have already been imported and cannot be overwritten.', ) return listDecks(database) } @@ -56,7 +56,7 @@ export function importLegacyWorkspace( if (!connection) throw new DeckPersistenceError( 'connection-unavailable', - `旧プロファイル「${profileName}」に対応する接続がありません。接続一覧を更新してください。`, + `No connection matches the legacy profile “${profileName}”. Refresh your connections.`, ) return { ...column, connectionId: connection.id } }), diff --git a/src/features/decks/import.test.ts b/src/features/decks/import.test.ts index 2aebfbf..4989a7c 100644 --- a/src/features/decks/import.test.ts +++ b/src/features/decks/import.test.ts @@ -77,7 +77,7 @@ it('blocks a different browser snapshot after the one-time import', () => { database, 'https://relay.invalid', ), - ).toThrow('旧デッキはすでに取り込み済みです') + ).toThrow('Legacy decks have already been imported') expect(listDecks(database)).toEqual(result) }) it('rolls back all imported decks and marker when any profile is unavailable', () => { @@ -92,7 +92,7 @@ it('rolls back all imported decks and marker when any profile is unavailable', ( database, 'https://relay.invalid', ), - ).toThrow('対応する接続がありません') + ).toThrow('No connection matches') expect(listDecks(database)).toEqual([]) expect(database.select().from(legacyImports).all()).toEqual([]) }) diff --git a/src/features/decks/legacy-import.ts b/src/features/decks/legacy-import.ts index fbc9f17..acca1d9 100644 --- a/src/features/decks/legacy-import.ts +++ b/src/features/decks/legacy-import.ts @@ -16,7 +16,7 @@ const legacyDeck = z (deck) => new Set(deck.columns.map((column) => column.id)).size === deck.columns.length, - 'カラム ID が重複しています。', + 'Column IDs must be unique.', ) export const legacyWorkspaceSchema = z .object({ @@ -28,5 +28,5 @@ export const legacyWorkspaceSchema = z .refine( (value) => new Set(value.decks.map((deck) => deck.id)).size === value.decks.length, - 'デッキ ID が重複しています。', + 'Deck IDs must be unique.', ) diff --git a/src/features/decks/mastodon-source-editor.tsx b/src/features/decks/mastodon-source-editor.tsx index 089e8b6..1c371a0 100644 --- a/src/features/decks/mastodon-source-editor.tsx +++ b/src/features/decks/mastodon-source-editor.tsx @@ -37,7 +37,7 @@ export function MastodonSourceEditor({ return ( <>

- 全文検索の対象はサーバーの設定によって異なります。結果が0件でも、検索に対応しているとは限りません。 + Full-text search depends on server settings. Zero results do not + necessarily mean search is supported.

) @@ -123,14 +124,14 @@ function MastodonUserEditor({ return (