diff --git a/frontend/__tests__/components/browser.test.tsx b/frontend/__tests__/components/browser.test.tsx
index 6b4bfba73d..c51519bf0f 100644
--- a/frontend/__tests__/components/browser.test.tsx
+++ b/frontend/__tests__/components/browser.test.tsx
@@ -45,7 +45,7 @@ describe("Browser", () => {
});
// i18n empty message key
- expect(screen.getByText("BROWSER$EMPTY_MESSAGE")).toBeInTheDocument();
+ expect(screen.getByText("BROWSER$NO_PAGE_LOADED")).toBeInTheDocument();
});
it("renders the url and a screenshot", () => {
diff --git a/frontend/__tests__/components/chat/chat-input.test.tsx b/frontend/__tests__/components/chat/chat-input.test.tsx
index 3cf83a1e7b..f3248371e3 100644
--- a/frontend/__tests__/components/chat/chat-input.test.tsx
+++ b/frontend/__tests__/components/chat/chat-input.test.tsx
@@ -84,12 +84,10 @@ describe("ChatInput", () => {
expect(onSubmitMock).not.toHaveBeenCalled();
});
- it("should render a placeholder", () => {
- render(
- ,
- );
+ it("should render a placeholder with translation key", () => {
+ render();
- const textarea = screen.getByPlaceholderText("Enter your message");
+ const textarea = screen.getByPlaceholderText("SUGGESTIONS$WHAT_TO_BUILD");
expect(textarea).toBeInTheDocument();
});
diff --git a/frontend/__tests__/components/features/conversation-panel/conversation-panel.test.tsx b/frontend/__tests__/components/features/conversation-panel/conversation-panel.test.tsx
index 4ec5212bb5..beb89a9562 100644
--- a/frontend/__tests__/components/features/conversation-panel/conversation-panel.test.tsx
+++ b/frontend/__tests__/components/features/conversation-panel/conversation-panel.test.tsx
@@ -71,7 +71,7 @@ describe("ConversationPanel", () => {
renderConversationPanel();
- const emptyState = await screen.findByText("No conversations found");
+ const emptyState = await screen.findByText("CONVERSATION$NO_CONVERSATIONS");
expect(emptyState).toBeInTheDocument();
});
diff --git a/frontend/__tests__/components/features/github/github-repo-selector.test.tsx b/frontend/__tests__/components/features/github/github-repo-selector.test.tsx
index 5ef98b251a..783bc82020 100644
--- a/frontend/__tests__/components/features/github/github-repo-selector.test.tsx
+++ b/frontend/__tests__/components/features/github/github-repo-selector.test.tsx
@@ -19,7 +19,7 @@ describe("GitHubRepositorySelector", () => {
);
expect(
- screen.getByPlaceholderText("Select a GitHub project"),
+ screen.getByPlaceholderText("LANDING$SELECT_REPO"),
).toBeInTheDocument();
});
diff --git a/frontend/__tests__/components/features/sidebar/sidebar.test.tsx b/frontend/__tests__/components/features/sidebar/sidebar.test.tsx
index 3b7a2a275e..43a0ddc660 100644
--- a/frontend/__tests__/components/features/sidebar/sidebar.test.tsx
+++ b/frontend/__tests__/components/features/sidebar/sidebar.test.tsx
@@ -128,7 +128,7 @@ describe("Sidebar", () => {
await user.click(norskOption);
const tokenInput =
- within(accountSettingsModal).getByLabelText(/github token/i);
+ within(accountSettingsModal).getByLabelText(/GITHUB\$TOKEN_OPTIONAL/i);
await user.type(tokenInput, "new-token");
const saveButton =
@@ -151,7 +151,11 @@ describe("Sidebar", () => {
const settingsModal = screen.getByTestId("ai-config-modal");
- const apiKeyInput = within(settingsModal).getByLabelText(/api key/i);
+ // Click the advanced options switch to show the API key input
+ const advancedOptionsSwitch = within(settingsModal).getByTestId("advanced-option-switch");
+ await user.click(advancedOptionsSwitch);
+
+ const apiKeyInput = within(settingsModal).getByLabelText(/API\$KEY/i);
await user.type(apiKeyInput, "SET");
const saveButton = within(settingsModal).getByTestId(
@@ -162,7 +166,7 @@ describe("Sidebar", () => {
expect(saveSettingsSpy).toHaveBeenCalledWith({
...MOCK_USER_PREFERENCES.settings,
llm_api_key: undefined,
- llm_base_url: undefined,
+ llm_base_url: "",
security_analyzer: undefined,
});
});
diff --git a/frontend/__tests__/components/feedback-form.test.tsx b/frontend/__tests__/components/feedback-form.test.tsx
index c9234e7374..e3cad75d45 100644
--- a/frontend/__tests__/components/feedback-form.test.tsx
+++ b/frontend/__tests__/components/feedback-form.test.tsx
@@ -14,6 +14,7 @@ import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "test-utils";
import { FeedbackForm } from "#/components/features/feedback/feedback-form";
+import { I18nKey } from "#/i18n/declaration";
describe("FeedbackForm", () => {
const user = userEvent.setup();
@@ -28,20 +29,20 @@ describe("FeedbackForm", () => {
,
);
- screen.getByLabelText("Email");
- screen.getByLabelText("Private");
- screen.getByLabelText("Public");
+ screen.getByLabelText(I18nKey.FEEDBACK$EMAIL_LABEL);
+ screen.getByLabelText(I18nKey.FEEDBACK$PRIVATE_LABEL);
+ screen.getByLabelText(I18nKey.FEEDBACK$PUBLIC_LABEL);
- screen.getByRole("button", { name: "Submit" });
- screen.getByRole("button", { name: "Cancel" });
+ screen.getByRole("button", { name: I18nKey.FEEDBACK$CONTRIBUTE_LABEL });
+ screen.getByRole("button", { name: I18nKey.FEEDBACK$CANCEL_LABEL });
});
it("should switch between private and public permissions", async () => {
renderWithProviders(
,
);
- const privateRadio = screen.getByLabelText("Private");
- const publicRadio = screen.getByLabelText("Public");
+ const privateRadio = screen.getByLabelText(I18nKey.FEEDBACK$PRIVATE_LABEL);
+ const publicRadio = screen.getByLabelText(I18nKey.FEEDBACK$PUBLIC_LABEL);
expect(privateRadio).toBeChecked(); // private is the default value
expect(publicRadio).not.toBeChecked();
@@ -59,7 +60,7 @@ describe("FeedbackForm", () => {
renderWithProviders(
,
);
- await user.click(screen.getByRole("button", { name: "Cancel" }));
+ await user.click(screen.getByRole("button", { name: I18nKey.FEEDBACK$CANCEL_LABEL }));
expect(onCloseMock).toHaveBeenCalled();
});
diff --git a/frontend/__tests__/components/interactive-chat-box.test.tsx b/frontend/__tests__/components/interactive-chat-box.test.tsx
index fe6ba32976..6ec74ddc88 100644
--- a/frontend/__tests__/components/interactive-chat-box.test.tsx
+++ b/frontend/__tests__/components/interactive-chat-box.test.tsx
@@ -157,7 +157,7 @@ describe("InteractiveChatBox", () => {
expect(onChange).not.toHaveBeenCalledWith("");
// Submit the message with image
- const submitButton = screen.getByRole("button", { name: "Send" });
+ const submitButton = screen.getByRole("button", { name: "BUTTON$SEND" });
await user.click(submitButton);
// Verify onSubmit was called with the message and image
diff --git a/frontend/__tests__/components/landing-translations.test.tsx b/frontend/__tests__/components/landing-translations.test.tsx
new file mode 100644
index 0000000000..9cfb9a07f1
--- /dev/null
+++ b/frontend/__tests__/components/landing-translations.test.tsx
@@ -0,0 +1,190 @@
+import { render, screen } from "@testing-library/react";
+import { test, expect, describe, vi } from "vitest";
+import { useTranslation } from "react-i18next";
+import translations from "../../src/i18n/translation.json";
+import { UserAvatar } from "../../src/components/features/sidebar/user-avatar";
+
+vi.mock("@nextui-org/react", () => ({
+ Tooltip: ({ content, children }: { content: string; children: React.ReactNode }) => (
+
+ {children}
+
{content}
+
+ ),
+}));
+
+const supportedLanguages = ['en', 'ja', 'zh-CN', 'zh-TW', 'ko-KR', 'de', 'no', 'it', 'pt', 'es', 'ar', 'fr', 'tr'];
+
+// Helper function to check if a translation exists for all supported languages
+function checkTranslationExists(key: string) {
+ const missingTranslations: string[] = [];
+
+ const translationEntry = (translations as Record>)[key];
+ if (!translationEntry) {
+ throw new Error(`Translation key "${key}" does not exist in translation.json`);
+ }
+
+ for (const lang of supportedLanguages) {
+ if (!translationEntry[lang]) {
+ missingTranslations.push(lang);
+ }
+ }
+
+ return missingTranslations;
+}
+
+// Helper function to find duplicate translation keys
+function findDuplicateKeys(obj: Record) {
+ const seen = new Set();
+ const duplicates = new Set();
+
+ // Only check top-level keys as these are our translation keys
+ for (const key in obj) {
+ if (seen.has(key)) {
+ duplicates.add(key);
+ } else {
+ seen.add(key);
+ }
+ }
+
+ return Array.from(duplicates);
+}
+
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
+ t: (key: string) => {
+ const translationEntry = (translations as Record>)[key];
+ return translationEntry?.ja || key;
+ },
+ }),
+}));
+
+describe("Landing page translations", () => {
+ test("should render Japanese translations correctly", () => {
+ // Mock a simple component that uses the translations
+ const TestComponent = () => {
+ const { t } = useTranslation();
+ return (
+
+
{}} />
+
+
{t("LANDING$TITLE")}
+
+
+
+
+
+
+
+ {t("WORKSPACE$TERMINAL_TAB_LABEL")}
+ {t("WORKSPACE$BROWSER_TAB_LABEL")}
+ {t("WORKSPACE$JUPYTER_TAB_LABEL")}
+ {t("WORKSPACE$CODE_EDITOR_TAB_LABEL")}
+
+ {t("WORKSPACE$TITLE")}
+
+
+ {t("TERMINAL$WAITING_FOR_CLIENT")}
+ {t("STATUS$CONNECTED")}
+ {t("STATUS$CONNECTED_TO_SERVER")}
+
+
+ {`5 ${t("TIME$MINUTES_AGO")}`}
+ {`2 ${t("TIME$HOURS_AGO")}`}
+ {`3 ${t("TIME$DAYS_AGO")}`}
+
+
+ );
+ };
+
+ render();
+
+ // Check main content translations
+ expect(screen.getByText("開発を始めましょう!")).toBeInTheDocument();
+ expect(screen.getByText("VS Codeで開く")).toBeInTheDocument();
+ expect(screen.getByText("テストカバレッジを向上させる")).toBeInTheDocument();
+ expect(screen.getByText("Dependabot PRを自動マージ")).toBeInTheDocument();
+ expect(screen.getByText("READMEを改善")).toBeInTheDocument();
+ expect(screen.getByText("依存関係を整理")).toBeInTheDocument();
+
+ // Check user avatar tooltip
+ const userAvatar = screen.getByTestId("user-avatar");
+ userAvatar.focus();
+ expect(screen.getByText("アカウント設定")).toBeInTheDocument();
+
+ // Check tab labels
+ const tabs = screen.getByTestId("tabs");
+ expect(tabs).toHaveTextContent("ターミナル");
+ expect(tabs).toHaveTextContent("ブラウザ");
+ expect(tabs).toHaveTextContent("Jupyter");
+ expect(tabs).toHaveTextContent("コードエディタ");
+
+ // Check workspace label and new project button
+ expect(screen.getByTestId("workspace-label")).toHaveTextContent("ワークスペース");
+ expect(screen.getByTestId("new-project")).toHaveTextContent("新規プロジェクト");
+
+ // Check status messages
+ const status = screen.getByTestId("status");
+ expect(status).toHaveTextContent("クライアントの準備を待機中");
+ expect(status).toHaveTextContent("接続済み");
+ expect(status).toHaveTextContent("サーバーに接続済み");
+
+ // Check account settings menu
+ expect(screen.getByText("アカウント設定")).toBeInTheDocument();
+
+ // Check time-related translations
+ const time = screen.getByTestId("time");
+ expect(time).toHaveTextContent("5 分前");
+ expect(time).toHaveTextContent("2 時間前");
+ expect(time).toHaveTextContent("3 日前");
+ });
+
+ test("all translation keys should have translations for all supported languages", () => {
+ // Test all translation keys used in the component
+ const translationKeys = [
+ "LANDING$TITLE",
+ "VSCODE$OPEN",
+ "SUGGESTIONS$INCREASE_TEST_COVERAGE",
+ "SUGGESTIONS$AUTO_MERGE_PRS",
+ "SUGGESTIONS$FIX_README",
+ "SUGGESTIONS$CLEAN_DEPENDENCIES",
+ "WORKSPACE$TERMINAL_TAB_LABEL",
+ "WORKSPACE$BROWSER_TAB_LABEL",
+ "WORKSPACE$JUPYTER_TAB_LABEL",
+ "WORKSPACE$CODE_EDITOR_TAB_LABEL",
+ "WORKSPACE$TITLE",
+ "PROJECT$NEW_PROJECT",
+ "TERMINAL$WAITING_FOR_CLIENT",
+ "STATUS$CONNECTED",
+ "STATUS$CONNECTED_TO_SERVER",
+ "TIME$MINUTES_AGO",
+ "TIME$HOURS_AGO",
+ "TIME$DAYS_AGO"
+ ];
+
+ // Check all keys and collect missing translations
+ const missingTranslationsMap = new Map();
+ translationKeys.forEach(key => {
+ const missing = checkTranslationExists(key);
+ if (missing.length > 0) {
+ missingTranslationsMap.set(key, missing);
+ }
+ });
+
+ // If any translations are missing, throw an error with all missing translations
+ if (missingTranslationsMap.size > 0) {
+ const errorMessage = Array.from(missingTranslationsMap.entries())
+ .map(([key, langs]) => `\n- "${key}" is missing translations for: ${langs.join(', ')}`)
+ .join('');
+ throw new Error(`Missing translations:${errorMessage}`);
+ }
+ });
+
+ test("translation file should not have duplicate keys", () => {
+ const duplicates = findDuplicateKeys(translations);
+
+ if (duplicates.length > 0) {
+ throw new Error(`Found duplicate translation keys: ${duplicates.join(', ')}`);
+ }
+ });
+});
diff --git a/frontend/__tests__/components/modals/settings/model-selector.test.tsx b/frontend/__tests__/components/modals/settings/model-selector.test.tsx
index d2f9510509..757f5dcd45 100644
--- a/frontend/__tests__/components/modals/settings/model-selector.test.tsx
+++ b/frontend/__tests__/components/modals/settings/model-selector.test.tsx
@@ -1,7 +1,23 @@
-import { describe, it, expect } from "vitest";
+import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ModelSelector } from "#/components/shared/modals/settings/model-selector";
+import { I18nKey } from "#/i18n/declaration";
+
+// Mock react-i18next
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
+ t: (key: string) => {
+ const translations: { [key: string]: string } = {
+ LLM$PROVIDER: "LLM Provider",
+ LLM$MODEL: "LLM Model",
+ LLM$SELECT_PROVIDER_PLACEHOLDER: "Select a provider",
+ LLM$SELECT_MODEL_PLACEHOLDER: "Select a model",
+ };
+ return translations[key] || key;
+ },
+ }),
+}));
describe("ModelSelector", () => {
const models = {
diff --git a/frontend/__tests__/components/suggestion-item.test.tsx b/frontend/__tests__/components/suggestion-item.test.tsx
index 23d2aaa41e..dcdd532e7f 100644
--- a/frontend/__tests__/components/suggestion-item.test.tsx
+++ b/frontend/__tests__/components/suggestion-item.test.tsx
@@ -2,6 +2,20 @@ import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { SuggestionItem } from "#/components/features/suggestions/suggestion-item";
+import { I18nKey } from "#/i18n/declaration";
+
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
+ t: (key: string) => {
+ const translations: Record = {
+ "SUGGESTIONS$TODO_APP": "ToDoリストアプリを開発する",
+ "LANDING$BUILD_APP_BUTTON": "プルリクエストを表示するアプリを開発する",
+ "SUGGESTIONS$HACKER_NEWS": "Hacker Newsのトップ記事を表示するbashスクリプトを作成する",
+ };
+ return translations[key] || key;
+ },
+ }),
+}));
describe("SuggestionItem", () => {
const suggestionItem = { label: "suggestion1", value: "a long text value" };
@@ -18,6 +32,19 @@ describe("SuggestionItem", () => {
expect(screen.getByText(/suggestion1/i)).toBeInTheDocument();
});
+ it("should render a translated suggestion when using I18nKey", async () => {
+ const translatedSuggestion = {
+ label: I18nKey.SUGGESTIONS$TODO_APP,
+ value: "todo app value",
+ };
+
+ const { container } = render();
+ console.log('Rendered HTML:', container.innerHTML);
+
+
+ expect(screen.getByText("ToDoリストアプリを開発する")).toBeInTheDocument();
+ });
+
it("should call onClick when clicking a suggestion", async () => {
const user = userEvent.setup();
render();
diff --git a/frontend/__tests__/components/user-avatar.test.tsx b/frontend/__tests__/components/user-avatar.test.tsx
index 076eb75b49..59bc90cecc 100644
--- a/frontend/__tests__/components/user-avatar.test.tsx
+++ b/frontend/__tests__/components/user-avatar.test.tsx
@@ -14,7 +14,7 @@ describe("UserAvatar", () => {
render();
expect(screen.getByTestId("user-avatar")).toBeInTheDocument();
expect(
- screen.getByLabelText("user avatar placeholder"),
+ screen.getByLabelText("USER$AVATAR_PLACEHOLDER"),
).toBeInTheDocument();
});
@@ -38,7 +38,7 @@ describe("UserAvatar", () => {
expect(screen.getByAltText("user avatar")).toBeInTheDocument();
expect(
- screen.queryByLabelText("user avatar placeholder"),
+ screen.queryByLabelText("USER$AVATAR_PLACEHOLDER"),
).not.toBeInTheDocument();
});
@@ -46,13 +46,13 @@ describe("UserAvatar", () => {
const { rerender } = render();
expect(screen.queryByTestId("loading-spinner")).not.toBeInTheDocument();
expect(
- screen.getByLabelText("user avatar placeholder"),
+ screen.getByLabelText("USER$AVATAR_PLACEHOLDER"),
).toBeInTheDocument();
rerender();
expect(screen.getByTestId("loading-spinner")).toBeInTheDocument();
expect(
- screen.queryByLabelText("user avatar placeholder"),
+ screen.queryByLabelText("USER$AVATAR_PLACEHOLDER"),
).not.toBeInTheDocument();
rerender(
diff --git a/frontend/__tests__/i18n/duplicate-keys.test.ts b/frontend/__tests__/i18n/duplicate-keys.test.ts
new file mode 100644
index 0000000000..35ab9b89c9
--- /dev/null
+++ b/frontend/__tests__/i18n/duplicate-keys.test.ts
@@ -0,0 +1,76 @@
+import { describe, expect, it } from 'vitest';
+import fs from 'fs';
+import path from 'path';
+
+describe('translation.json', () => {
+ it('should not have duplicate translation keys', () => {
+ // Read the translation.json file
+ const translationPath = path.join(__dirname, '../../src/i18n/translation.json');
+ const translationContent = fs.readFileSync(translationPath, 'utf-8');
+
+ // First, let's check for exact string matches of key definitions
+ const keyRegex = /"([^"]+)": {/g;
+ const matches = translationContent.matchAll(keyRegex);
+ const keyOccurrences = new Map();
+ const duplicateKeys: string[] = [];
+
+ for (const match of matches) {
+ const key = match[1];
+ const count = (keyOccurrences.get(key) || 0) + 1;
+ keyOccurrences.set(key, count);
+ if (count > 1) {
+ duplicateKeys.push(key);
+ }
+ }
+
+ // Remove duplicates from duplicateKeys array
+ const uniqueDuplicates = [...new Set(duplicateKeys)];
+
+ // If there are duplicates, create a helpful error message
+ if (uniqueDuplicates.length > 0) {
+ const errorMessage = `Found duplicate translation keys:\n${uniqueDuplicates
+ .map((key) => ` - "${key}" appears ${keyOccurrences.get(key)} times`)
+ .join('\n')}`;
+ throw new Error(errorMessage);
+ }
+
+ // Expect no duplicates (this will pass if we reach here)
+ expect(uniqueDuplicates).toHaveLength(0);
+ });
+
+ it('should have consistent translations for each key', () => {
+ // Read the translation.json file
+ const translationPath = path.join(__dirname, '../../src/i18n/translation.json');
+ const translationContent = fs.readFileSync(translationPath, 'utf-8');
+ const translations = JSON.parse(translationContent);
+
+ // Create a map to store English translations for each key
+ const englishTranslations = new Map();
+ const inconsistentKeys: string[] = [];
+
+ // Check each key's English translation
+ Object.entries(translations).forEach(([key, value]: [string, any]) => {
+ if (typeof value === 'object' && value.en !== undefined) {
+ const currentEn = value.en.toLowerCase();
+ const existingEn = englishTranslations.get(key)?.toLowerCase();
+
+ if (existingEn !== undefined && existingEn !== currentEn) {
+ inconsistentKeys.push(key);
+ } else {
+ englishTranslations.set(key, value.en);
+ }
+ }
+ });
+
+ // If there are inconsistencies, create a helpful error message
+ if (inconsistentKeys.length > 0) {
+ const errorMessage = `Found inconsistent translations for keys:\n${inconsistentKeys
+ .map((key) => ` - "${key}" has multiple different English translations`)
+ .join('\n')}`;
+ throw new Error(errorMessage);
+ }
+
+ // Expect no inconsistencies
+ expect(inconsistentKeys).toHaveLength(0);
+ });
+});
diff --git a/frontend/__tests__/i18n/translations.test.tsx b/frontend/__tests__/i18n/translations.test.tsx
new file mode 100644
index 0000000000..3833b4d306
--- /dev/null
+++ b/frontend/__tests__/i18n/translations.test.tsx
@@ -0,0 +1,20 @@
+import { screen } from '@testing-library/react';
+import { describe, expect, it } from 'vitest';
+import i18n from '../../src/i18n';
+import { AccountSettingsContextMenu } from '../../src/components/features/context-menu/account-settings-context-menu';
+import { renderWithProviders } from '../../test-utils';
+
+describe('Translations', () => {
+ it('should render translated text', () => {
+ i18n.changeLanguage('en');
+ renderWithProviders(
+ {}}
+ onLogout={() => {}}
+ onClose={() => {}}
+ isLoggedIn={true}
+ />
+ );
+ expect(screen.getByTestId('account-settings-context-menu')).toBeInTheDocument();
+ });
+});
diff --git a/frontend/__tests__/utils/check-hardcoded-strings.test.tsx b/frontend/__tests__/utils/check-hardcoded-strings.test.tsx
new file mode 100644
index 0000000000..74c288ad39
--- /dev/null
+++ b/frontend/__tests__/utils/check-hardcoded-strings.test.tsx
@@ -0,0 +1,40 @@
+import { render, screen } from "@testing-library/react";
+import { test, expect, describe, vi } from "vitest";
+import { InteractiveChatBox } from "#/components/features/chat/interactive-chat-box";
+import { ChatInput } from "#/components/features/chat/chat-input";
+
+// Mock react-i18next
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
+ t: (key: string) => key,
+ }),
+}));
+
+describe("Check for hardcoded English strings", () => {
+ test("InteractiveChatBox should not have hardcoded English strings", () => {
+ const { container } = render(
+ {}}
+ onStop={() => {}}
+ />
+ );
+
+ // Get all text content
+ const text = container.textContent;
+
+ // List of English strings that should be translated
+ const hardcodedStrings = [
+ "What do you want to build?",
+ ];
+
+ // Check each string
+ hardcodedStrings.forEach(str => {
+ expect(text).not.toContain(str);
+ });
+ });
+
+ test("ChatInput should use translation key for placeholder", () => {
+ render( {}} />);
+ screen.getByPlaceholderText("SUGGESTIONS$WHAT_TO_BUILD");
+ });
+});
diff --git a/frontend/__tests__/utils/i18n-test-utils.tsx b/frontend/__tests__/utils/i18n-test-utils.tsx
new file mode 100644
index 0000000000..7153540a19
--- /dev/null
+++ b/frontend/__tests__/utils/i18n-test-utils.tsx
@@ -0,0 +1,29 @@
+import { ReactNode } from "react";
+import { I18nextProvider } from "react-i18next";
+
+const mockI18n = {
+ language: "ja",
+ t: (key: string) => {
+ const translations: Record = {
+ "SUGGESTIONS$TODO_APP": "ToDoリストアプリを開発する",
+ "LANDING$BUILD_APP_BUTTON": "プルリクエストを表示するアプリを開発する",
+ "SUGGESTIONS$HACKER_NEWS": "Hacker Newsのトップ記事を表示するbashスクリプトを作成する",
+ "LANDING$TITLE": "一緒に開発を始めましょう!",
+ "OPEN_IN_VSCODE": "VS Codeで開く",
+ "INCREASE_TEST_COVERAGE": "テストカバレッジを向上",
+ "AUTO_MERGE_PRS": "PRを自動マージ",
+ "FIX_README": "READMEを修正",
+ "CLEAN_DEPENDENCIES": "依存関係を整理"
+ };
+ return translations[key] || key;
+ },
+ exists: () => true,
+ changeLanguage: () => new Promise(() => {}),
+ use: () => mockI18n,
+};
+
+export function I18nTestProvider({ children }: { children: ReactNode }) {
+ return (
+ {children}
+ );
+}
diff --git a/frontend/src/components/features/browser/empty-browser-message.tsx b/frontend/src/components/features/browser/empty-browser-message.tsx
index bf034bf2c1..a4adc95292 100644
--- a/frontend/src/components/features/browser/empty-browser-message.tsx
+++ b/frontend/src/components/features/browser/empty-browser-message.tsx
@@ -8,7 +8,7 @@ export function EmptyBrowserMessage() {
return (
- {t(I18nKey.BROWSER$EMPTY_MESSAGE)}
+ {t(I18nKey.BROWSER$NO_PAGE_LOADED)}
);
}
diff --git a/frontend/src/components/features/chat/chat-input.tsx b/frontend/src/components/features/chat/chat-input.tsx
index 02e346ca27..fbf3aff7aa 100644
--- a/frontend/src/components/features/chat/chat-input.tsx
+++ b/frontend/src/components/features/chat/chat-input.tsx
@@ -1,5 +1,7 @@
import React from "react";
import TextareaAutosize from "react-textarea-autosize";
+import { useTranslation } from "react-i18next";
+import { I18nKey } from "#/i18n/declaration";
import { cn } from "#/utils/utils";
import { SubmitButton } from "#/components/shared/buttons/submit-button";
import { StopButton } from "#/components/shared/buttons/stop-button";
@@ -8,7 +10,6 @@ interface ChatInputProps {
name?: string;
button?: "submit" | "stop";
disabled?: boolean;
- placeholder?: string;
showButton?: boolean;
value?: string;
maxRows?: number;
@@ -26,7 +27,6 @@ export function ChatInput({
name,
button = "submit",
disabled,
- placeholder,
showButton = true,
value,
maxRows = 4,
@@ -39,6 +39,7 @@ export function ChatInput({
className,
buttonClassName,
}: ChatInputProps) {
+ const { t } = useTranslation();
const textareaRef = React.useRef(null);
const [isDraggingOver, setIsDraggingOver] = React.useState(false);
@@ -117,7 +118,7 @@ export function ChatInput({
- Let's start building!
+ {t(I18nKey.LANDING$TITLE)}
state.agent);
@@ -27,8 +30,8 @@ export function AgentControlBar() {
}
content={
curAgentState === AgentState.PAUSED
- ? "Resume the agent task"
- : "Pause the current task"
+ ? t(I18nKey.AGENT$RESUME_TASK)
+ : t(I18nKey.AGENT$PAUSE_TASK)
}
action={
curAgentState === AgentState.PAUSED
diff --git a/frontend/src/components/features/conversation-panel/conversation-panel.tsx b/frontend/src/components/features/conversation-panel/conversation-panel.tsx
index 8594143c3e..a96c649a54 100644
--- a/frontend/src/components/features/conversation-panel/conversation-panel.tsx
+++ b/frontend/src/components/features/conversation-panel/conversation-panel.tsx
@@ -1,5 +1,7 @@
import React from "react";
import { NavLink, useParams } from "react-router";
+import { useTranslation } from "react-i18next";
+import { I18nKey } from "#/i18n/declaration";
import { ConversationCard } from "./conversation-card";
import { useUserConversations } from "#/hooks/query/use-user-conversations";
import { useDeleteConversation } from "#/hooks/mutation/use-delete-conversation";
@@ -15,6 +17,7 @@ interface ConversationPanelProps {
}
export function ConversationPanel({ onClose }: ConversationPanelProps) {
+ const { t } = useTranslation();
const { conversationId: cid } = useParams();
const endSession = useEndSession();
const ref = useClickOutsideElement(onClose);
@@ -78,7 +81,9 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
)}
{conversations?.length === 0 && (
-
No conversations found
+
+ {t(I18nKey.CONVERSATION$NO_CONVERSATIONS)}
+
)}
{conversations?.map((project) => (
diff --git a/frontend/src/components/features/conversation-panel/new-conversation-button.tsx b/frontend/src/components/features/conversation-panel/new-conversation-button.tsx
index b7563952cf..6b391cca90 100644
--- a/frontend/src/components/features/conversation-panel/new-conversation-button.tsx
+++ b/frontend/src/components/features/conversation-panel/new-conversation-button.tsx
@@ -1,8 +1,12 @@
+import { useTranslation } from "react-i18next";
+import { I18nKey } from "#/i18n/declaration";
+
interface NewConversationButtonProps {
onClick: () => void;
}
export function NewConversationButton({ onClick }: NewConversationButtonProps) {
+ const { t } = useTranslation();
return (
);
}
diff --git a/frontend/src/components/features/feedback/feedback-form.tsx b/frontend/src/components/features/feedback/feedback-form.tsx
index 31705a1014..9c60990249 100644
--- a/frontend/src/components/features/feedback/feedback-form.tsx
+++ b/frontend/src/components/features/feedback/feedback-form.tsx
@@ -1,5 +1,7 @@
import React from "react";
import hotToast from "react-hot-toast";
+import { useTranslation } from "react-i18next";
+import { I18nKey } from "#/i18n/declaration";
import { Feedback } from "#/api/open-hands.types";
import { useSubmitFeedback } from "#/hooks/mutation/use-submit-feedback";
import { ModalButton } from "#/components/shared/buttons/modal-button";
@@ -13,8 +15,9 @@ interface FeedbackFormProps {
}
export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
+ const { t } = useTranslation();
const copiedToClipboardToast = () => {
- hotToast("Password copied to clipboard", {
+ hotToast(t(I18nKey.FEEDBACK$PASSWORD_COPIED_MESSAGE), {
icon: "📋",
position: "bottom-right",
});
@@ -41,10 +44,13 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
target="_blank"
rel="noreferrer"
>
- Go to shared feedback
+ {t(I18nKey.FEEDBACK$GO_TO_FEEDBACK)}
onPressToast(password)} className="cursor-pointer">
- Password: {password} (copy)
+ {t(I18nKey.FEEDBACK$PASSWORD)}: {password}{" "}
+
+ ({t(I18nKey.FEEDBACK$COPY_LABEL)})
+
,
{ duration: 10000 },
@@ -86,12 +92,14 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
return (