Compare commits

...

62 Commits

Author SHA1 Message Date
openhands
03d1d6c9a9 fix: Add i18n support for 'What do you want to build?' placeholder text
Fixes #4280
2025-01-07 08:30:10 +00:00
Graham Neubig
3f9e652a02 Merge branch 'main' into add-japanese-translations 2025-01-07 17:10:16 +09:00
openhands
1da76b371b Add missing translations for various languages 2025-01-07 08:02:29 +00:00
openhands
b3c883a1a9 Add missing translations for various languages 2025-01-07 07:51:15 +00:00
openhands
95f1e5ead1 Add remaining Japanese translations 2025-01-07 07:22:44 +00:00
openhands
6844a1a7b7 Add missing Turkish translations 2025-01-07 07:20:57 +00:00
openhands
5a5e6ec659 Add missing German translations 2025-01-07 07:19:47 +00:00
openhands
9df8d94435 Add missing Korean translations 2025-01-07 07:19:20 +00:00
openhands
cb8d70403e Add missing Chinese (Traditional) translations 2025-01-07 07:18:19 +00:00
openhands
1550d9f0db Add missing Chinese (Simplified) translations 2025-01-07 07:17:20 +00:00
openhands
96782b34ee Add missing Japanese translations 2025-01-07 07:16:23 +00:00
openhands
43240e1da3 Modify translation test to check all keys 2025-01-07 07:14:50 +00:00
openhands
1a05c2a90b Add Japanese translations for Account Settings 2025-01-07 07:08:34 +00:00
openhands
89581ff15b Add tests for Japanese translations 2025-01-07 06:10:20 +00:00
openhands
db6ce0c4d0 Add Japanese translation for Logout 2025-01-07 06:05:23 +00:00
openhands
e5346c76c5 fix: consolidate duplicate translation keys and add test 2025-01-07 05:55:56 +00:00
openhands
8697a022c6 fix: consolidate Japanese translations for 'What do you want to build?' prompt 2025-01-07 05:46:35 +00:00
openhands
6b33720456 fix: Restore complete translations that were accidentally removed 2025-01-07 05:38:37 +00:00
openhands
b7fc110807 feat: Add German translations for landing page and account settings 2025-01-07 05:11:48 +00:00
openhands
40b09a2d8e fix: Fix code formatting and remove unused imports 2025-01-07 04:58:44 +00:00
openhands
77b62290da Fix translations in account settings form 2025-01-07 04:21:13 +00:00
openhands
24a19219d4 Fix translations in settings form 2025-01-07 04:09:19 +00:00
openhands
f13a1aa07e Fix i18n initialization to properly transform translations 2025-01-07 04:02:11 +00:00
openhands
bd685b9fc2 Fix i18n initialization to properly load translations 2025-01-07 03:56:19 +00:00
openhands
91d6529561 Fix Japanese translations for LLM provider settings 2025-01-07 03:49:44 +00:00
openhands
9e4c9e0bf9 Add Japanese translations for LLM provider settings 2025-01-07 03:39:20 +00:00
openhands
7d7ae4640c fix: Handle localStorage access during SSR 2025-01-07 02:57:00 +00:00
openhands
579c58fc92 fix: Initialize i18n with language from settings 2025-01-07 02:50:47 +00:00
openhands
06a8e5caae fix: Add Japanese translation for Terminal label 2025-01-07 02:43:51 +00:00
openhands
b7701b858f fix: Update VS Code button to use correct translation key 2025-01-07 01:52:09 +00:00
openhands
a90cc37f5d feat(i18n): Simplify Japanese translations for workspace, browser, and Jupyter labels 2025-01-07 01:48:44 +00:00
openhands
d3d4acd541 test: Update Japanese translation tests to match new translations 2025-01-07 01:32:34 +00:00
openhands
73c50f384f feat(i18n): Simplify Japanese translations for workspace, browser, and Jupyter labels 2025-01-07 01:28:07 +00:00
openhands
42b3e6992d feat(i18n): Update Japanese translation for welcome message 2025-01-07 01:23:45 +00:00
openhands
ba04181697 feat(i18n): Add Japanese translations for welcome message and VS Code button 2025-01-07 01:11:18 +00:00
openhands
1a87c85077 Add Japanese translations for project buttons 2025-01-07 00:18:04 +00:00
openhands
e1ab79c468 Add missing Japanese translations and fix tests 2025-01-07 00:14:39 +00:00
Graham Neubig
cd59652e46 Merge branch 'main' into add-japanese-translations 2025-01-07 08:51:59 +09:00
openhands
f348cf11d9 Remove more unused translation keys:
- Remove WORKSPACE$LABEL (unused)
- Remove CONNECT_TO_GITHUB_BY_TOKEN_MODAL$TERMS_OF_SERVICE (unused)
2025-01-06 23:47:06 +00:00
openhands
9645ff8977 Consolidate duplicate translation keys:
- Remove CHAT$WHAT_DO_YOU_WANT_TO_BUILD and LANDING$BUILD_PROMPT in favor of SUGGESTIONS$WHAT_TO_BUILD
- Remove unused keys:
  - STATUS$CONNECTED_TO_SERVER
  - BROWSER$SCREENSHOT and BROWSER$SCREENSHOT_ALT
  - SUGGESTIONS$AUTO_MERGE and SUGGESTIONS$AUTO_MERGE_PRS
  - USER$AVATAR_PLACEHOLDER (duplicate)
2025-01-06 23:41:02 +00:00
openhands
998e16a691 Consolidate duplicate translation keys: LETS_START_BUILDING -> LANDING$TITLE 2025-01-06 23:26:52 +00:00
openhands
4b80de48cb fix: Add missing translations and improve i18n testing
- Add missing translation keys for browser, user avatar, and suggestions
- Add translations for all supported languages
- Create test to catch hardcoded English strings
- Fix components to use translation system properly
2025-01-06 17:09:57 +00:00
openhands
0fd1582c82 Improve i18n support:
- Add missing translations for all supported languages
- Add i18n test utilities and translation tests
- Fix duplicate key detection in tests
- Update UI components to use translations
2025-01-06 16:10:35 +00:00
openhands
0a49f21158 Consolidate Japanese translation tests into landing-translations.test.tsx 2025-01-06 15:18:21 +00:00
openhands
ae66a4eae3 Add tests for Japanese translations 2025-01-06 15:17:15 +00:00
openhands
e5538da015 Add more Japanese translations for UI elements 2025-01-06 15:08:04 +00:00
openhands
8f24a99efd Add more Japanese translations for UI elements 2025-01-06 12:11:23 +00:00
openhands
1b55a7e27f Add tests for Japanese translations and fix missing translations 2025-01-06 12:05:02 +00:00
openhands
72bbf936f8 Add missing Japanese translations for UI elements 2025-01-06 11:51:38 +00:00
openhands
d281d5e774 Fix suggestion localization and add test 2025-01-06 11:31:23 +00:00
openhands
959a181ad8 Fix remaining linting issues 2025-01-06 11:09:37 +00:00
openhands
986de97a50 Add translations for all suggestions 2025-01-06 11:04:28 +00:00
openhands
3241ff4ae0 Fix suggestion localization by using proper translation keys 2025-01-06 11:01:18 +00:00
Graham Neubig
ace4f58c4b Update frontend/src/i18n/translation.json 2025-01-06 19:56:34 +09:00
openhands
2a70fdcf27 Fix frontend linting issues 2025-01-06 10:55:26 +00:00
Graham Neubig
f7f7b6a68c Apply suggestions from code review 2025-01-06 19:46:07 +09:00
openhands
2039f159c0 Add translations for main page content 2025-01-06 10:42:32 +00:00
openhands
49b12d90bc Add translations for settings modal and sidebar tooltips 2025-01-06 10:37:23 +00:00
openhands
e05d7a9673 Fix frontend tests and lint issues 2025-01-06 10:21:08 +00:00
openhands
222835355a Add Japanese translations and update UI components 2025-01-06 10:03:40 +00:00
openhands
e01078800a Add Japanese language option to language selector 2025-01-06 08:50:11 +00:00
openhands
853a62bab8 Add Japanese translations to frontend 2025-01-06 07:34:35 +00:00
58 changed files with 5869 additions and 2248 deletions

View File

@@ -0,0 +1,92 @@
From 9645ff89770c5c491edd69bc6d944808eb2c90eb Mon Sep 17 00:00:00 2001
From: openhands <openhands@all-hands.dev>
Date: Mon, 6 Jan 2025 23:41:02 +0000
Subject: [PATCH] Consolidate duplicate translation keys:
- Remove CHAT$WHAT_DO_YOU_WANT_TO_BUILD and LANDING$BUILD_PROMPT in favor of SUGGESTIONS$WHAT_TO_BUILD
- Remove unused keys:
- STATUS$CONNECTED_TO_SERVER
- BROWSER$SCREENSHOT and BROWSER$SCREENSHOT_ALT
- SUGGESTIONS$AUTO_MERGE and SUGGESTIONS$AUTO_MERGE_PRS
- USER$AVATAR_PLACEHOLDER (duplicate)
---
frontend/src/components/shared/task-form.tsx | 2 +-
frontend/src/i18n/translation.json | 36 ++------------------
2 files changed, 4 insertions(+), 34 deletions(-)
diff --git a/frontend/src/components/shared/task-form.tsx b/frontend/src/components/shared/task-form.tsx
index 3045244d..741c1f9e 100644
--- a/frontend/src/components/shared/task-form.tsx
+++ b/frontend/src/components/shared/task-form.tsx
@@ -57,7 +57,7 @@ export function TaskForm({ ref }: TaskFormProps) {
return t(I18nKey.LANDING$CHANGE_PROMPT, { repo: selectedRepository });
}
- return t(I18nKey.LANDING$BUILD_PROMPT);
+ return t(I18nKey.SUGGESTIONS$WHAT_TO_BUILD);
}, [selectedRepository]);
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json
index 1254db07..db1f6bd3 100644
--- a/frontend/src/i18n/translation.json
+++ b/frontend/src/i18n/translation.json
@@ -663,10 +663,7 @@
"tr": "Varsayılanlara Sıfırla",
"ja": "デフォルトにリセット"
},
- "CHAT$WHAT_DO_YOU_WANT_TO_BUILD": {
- "en": "What do you want to build?",
- "ja": "何を開発したいですか?"
- },
+
"STATUS$CONNECTED": {
"en": "Connected",
"ja": "接続済み",
@@ -829,21 +826,7 @@
"no": "Kunne ikke hente modeller og agenter",
"ja": "モデルとエージェントの取得に失敗しました"
},
- "SESSION$SERVER_CONNECTED_MESSAGE": {
- "en": "Connected to server",
- "zh-CN": "已连接到服务器",
- "de": "Verbindung zum Server hergestellt",
- "zh-TW": "已連接到伺服器",
- "es": "Conectado al servidor",
- "fr": "Connecté au serveur",
- "it": "Connesso al server",
- "pt": "Conectado ao servidor",
- "ko-KR": "서버에 연결됨",
- "ar": "تم الاتصال بالخادم",
- "tr": "Sunucuya bağlandı",
- "no": "Koblet til server",
- "ja": "サーバーに接続しました"
- },
+
"SESSION$SESSION_HANDLING_ERROR_MESSAGE": {
"en": "Error handling message",
"zh-CN": "处理消息时发生错误",
@@ -1740,20 +1723,7 @@
"ar": "كتابة سكربت باش يعرض أهم خبر على هاكر نيوز",
"no": "Skriv et bash-script som viser topphistorien på Hacker News"
},
- "LANDING$BUILD_PROMPT": {
- "en": "What do you want to build?",
- "ja": "何を開発したいですか?",
- "zh-CN": "你想构建什么?",
- "zh-TW": "你想構建什麼?",
- "ko-KR": "무엇을 만들고 싶으신가요?",
- "fr": "Que voulez-vous construire ?",
- "es": "¿Qué quieres construir?",
- "de": "Was möchtest du bauen?",
- "it": "Cosa vuoi costruire?",
- "pt": "O que você quer construir?",
- "ar": "ماذا تريد أن تبني؟",
- "no": "Hva vil du bygge?"
- },
+
"LANDING$CHANGE_PROMPT": {
"en": "What would you like to change in {{repo}}?",
"ja": "{{repo}}で何を変更したいですか?",
--
2.39.5

1
OpenHands Submodule

Submodule OpenHands added at efd0267919

View File

@@ -61,7 +61,7 @@ describe("ConversationPanel", () => {
renderConversationPanel();
const emptyState = await screen.findByText("No conversations found");
const emptyState = await screen.findByText("CONVERSATION$NO_CONVERSATIONS");
expect(emptyState).toBeInTheDocument();
});

View File

@@ -20,7 +20,7 @@ describe("GitHubRepositorySelector", () => {
);
expect(
screen.getByPlaceholderText("Select a GitHub project"),
screen.getByPlaceholderText("LANDING$SELECT_REPO"),
).toBeInTheDocument();
});

View File

@@ -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

View File

@@ -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 }) => (
<div>
{children}
<div>{content}</div>
</div>
),
}));
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<string, Record<string, string>>)[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<string, any>) {
const seen = new Set<string>();
const duplicates = new Set<string>();
// 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<string, Record<string, string>>)[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 (
<div>
<UserAvatar onClick={() => {}} />
<div data-testid="main-content">
<h1>{t("LANDING$TITLE")}</h1>
<button>{t("OPEN_IN_VSCODE")}</button>
<button>{t("INCREASE_TEST_COVERAGE")}</button>
<button>{t("AUTO_MERGE_PRS")}</button>
<button>{t("FIX_README")}</button>
<button>{t("CLEAN_DEPENDENCIES")}</button>
</div>
<div data-testid="tabs">
<span>{t("WORKSPACE$TERMINAL_TAB_LABEL")}</span>
<span>{t("WORKSPACE$BROWSER_TAB_LABEL")}</span>
<span>{t("WORKSPACE$JUPYTER_TAB_LABEL")}</span>
<span>{t("WORKSPACE$CODE_EDITOR_TAB_LABEL")}</span>
</div>
<div data-testid="workspace-label">{t("WORKSPACE$TITLE")}</div>
<button data-testid="new-project">{t("PROJECT$NEW_PROJECT")}</button>
<div data-testid="status">
<span>{t("TERMINAL$WAITING_FOR_CLIENT")}</span>
<span>{t("STATUS$CONNECTED")}</span>
<span>{t("STATUS$CONNECTED_TO_SERVER")}</span>
</div>
<div data-testid="time">
<span>{`5 ${t("TIME$MINUTES_AGO")}`}</span>
<span>{`2 ${t("TIME$HOURS_AGO")}`}</span>
<span>{`3 ${t("TIME$DAYS_AGO")}`}</span>
</div>
</div>
);
};
render(<TestComponent />);
// Check main content translations
expect(screen.getByText("開発を始めましょう!")).toBeInTheDocument();
expect(screen.getByText("VS Codeで開く")).toBeInTheDocument();
expect(screen.getByText("テストカバレッジを向上")).toBeInTheDocument();
expect(screen.getByText("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",
"OPEN_IN_VSCODE",
"INCREASE_TEST_COVERAGE",
"AUTO_MERGE_PRS",
"FIX_README",
"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<string, string[]>();
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(', ')}`);
}
});
});

View File

@@ -1,8 +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";
// Mock react-i18next
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: { [key: string]: string } = {
LLM_PROVIDER: "LLM Provider",
LLM_MODEL: "LLM Model",
SELECT_PROVIDER_PLACEHOLDER: "Select a provider",
SELECT_MODEL_PLACEHOLDER: "Select a model",
};
return translations[key] || key;
},
}),
}));
describe("ModelSelector", () => {
const models = {
openai: {

View File

@@ -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<string, string> = {
"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(<SuggestionItem suggestion={translatedSuggestion} onClick={onClick} />);
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(<SuggestionItem suggestion={suggestionItem} onClick={onClick} />);

View File

@@ -14,7 +14,7 @@ describe("UserAvatar", () => {
render(<UserAvatar onClick={onClickMock} />);
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(<UserAvatar onClick={onClickMock} />);
expect(screen.queryByTestId("loading-spinner")).not.toBeInTheDocument();
expect(
screen.getByLabelText("user avatar placeholder"),
screen.getByLabelText("USER$AVATAR_PLACEHOLDER"),
).toBeInTheDocument();
rerender(<UserAvatar onClick={onClickMock} isLoading />);
expect(screen.getByTestId("loading-spinner")).toBeInTheDocument();
expect(
screen.queryByLabelText("user avatar placeholder"),
screen.queryByLabelText("USER$AVATAR_PLACEHOLDER"),
).not.toBeInTheDocument();
rerender(

View File

@@ -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<string, number>();
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<string, string>();
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);
});
});

View File

@@ -0,0 +1,61 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { I18nextProvider } from 'react-i18next';
import i18n, { AvailableLanguages } from '../../src/i18n';
import { AccountSettingsContextMenu } from '../../src/components/features/context-menu/account-settings-context-menu';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import translations from '../../src/i18n/translation.json';
const queryClient = new QueryClient();
const renderWithI18n = (component: React.ReactNode, language: string = 'en') => {
i18n.changeLanguage(language);
return render(
<QueryClientProvider client={queryClient}>
<I18nextProvider i18n={i18n}>
{component}
</I18nextProvider>
</QueryClientProvider>
);
};
describe('Translations', () => {
describe('Translation Coverage', () => {
it('should have translations for all supported languages', () => {
// Get all language codes from AvailableLanguages
const languageCodes = AvailableLanguages.map(lang => lang.value);
// Keep track of missing translations
const missingTranslations: { key: string; languages: string[] }[] = [];
// Check each translation key
Object.entries(translations).forEach(([key, value]: [string, any]) => {
if (typeof value === 'object') {
const missingLangs = languageCodes.filter(lang => {
// Handle special case for language codes with hyphens
const langKey = lang.includes('-') ? `${lang}` : lang;
return value[langKey] === undefined;
});
if (missingLangs.length > 0) {
missingTranslations.push({
key,
languages: missingLangs
});
}
}
});
// If there are missing translations, create a helpful error message
if (missingTranslations.length > 0) {
const errorMessage = `Found missing translations:\n${missingTranslations
.map(({ key, languages }) => ` - "${key}" is missing translations for: ${languages.join(', ')}`)
.join('\n')}`;
throw new Error(errorMessage);
}
// Expect no missing translations
expect(missingTranslations).toHaveLength(0);
});
});
});

View File

@@ -0,0 +1,47 @@
import { render } 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(
<InteractiveChatBox
onSubmit={() => {}}
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", () => {
const { container } = render(
<ChatInput
onSubmit={() => {}}
/>
);
// The placeholder should be a translation key, not English text
const textarea = container.querySelector("textarea");
expect(textarea?.placeholder).toBe("SUGGESTIONS$WHAT_TO_BUILD");
});
});

View File

@@ -0,0 +1,29 @@
import { ReactNode } from "react";
import { I18nextProvider } from "react-i18next";
const mockI18n = {
language: "ja",
t: (key: string) => {
const translations: Record<string, string> = {
"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 (
<I18nextProvider i18n={mockI18n as any}>{children}</I18nextProvider>
);
}

View File

@@ -56,6 +56,7 @@
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.5",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
@@ -76,7 +77,7 @@
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.3",
"eslint-plugin-react-hooks": "^4.6.2",
"husky": "^9.1.6",
"husky": "^9.1.7",
"jsdom": "^25.0.1",
"lint-staged": "^15.3.0",
"msw": "^2.6.6",
@@ -1516,6 +1517,19 @@
"node": ">=8"
}
},
"node_modules/@jest/expect-utils": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz",
"integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==",
"dev": true,
"license": "MIT",
"dependencies": {
"jest-get-type": "^29.6.3"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@jest/schemas": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
@@ -1529,6 +1543,24 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@jest/types": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz",
"integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "^29.6.3",
"@types/istanbul-lib-coverage": "^2.0.0",
"@types/istanbul-reports": "^3.0.0",
"@types/node": "*",
"@types/yargs": "^17.0.8",
"chalk": "^4.0.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
@@ -5586,6 +5618,79 @@
"@types/unist": "*"
}
},
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
"integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/istanbul-lib-report": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz",
"integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/istanbul-lib-coverage": "*"
}
},
"node_modules/@types/istanbul-reports": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz",
"integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/istanbul-lib-report": "*"
}
},
"node_modules/@types/jest": {
"version": "29.5.14",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz",
"integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"expect": "^29.0.0",
"pretty-format": "^29.0.0"
}
},
"node_modules/@types/jest/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@types/jest/node_modules/pretty-format": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
"integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "^29.6.3",
"ansi-styles": "^5.0.0",
"react-is": "^18.0.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@types/jest/node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@@ -5671,6 +5776,13 @@
"@types/react": "*"
}
},
"node_modules/@types/stack-utils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/statuses": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz",
@@ -5707,6 +5819,23 @@
"@types/node": "*"
}
},
"node_modules/@types/yargs": {
"version": "17.0.33",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
"integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/yargs-parser": "*"
}
},
"node_modules/@types/yargs-parser": {
"version": "21.0.3",
"resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
"integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
@@ -7137,6 +7266,22 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/ci-info": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/sibiraj-s"
}
],
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/cli-cursor": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
@@ -9084,6 +9229,23 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/expect": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz",
"integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/expect-utils": "^29.7.0",
"jest-get-type": "^29.6.3",
"jest-matcher-utils": "^29.7.0",
"jest-message-util": "^29.7.0",
"jest-util": "^29.7.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/express": {
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
@@ -10969,6 +11131,192 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/jest-diff": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz",
"integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "^4.0.0",
"diff-sequences": "^29.6.3",
"jest-get-type": "^29.6.3",
"pretty-format": "^29.7.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/jest-diff/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/jest-diff/node_modules/pretty-format": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
"integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "^29.6.3",
"ansi-styles": "^5.0.0",
"react-is": "^18.0.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/jest-diff/node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"dev": true,
"license": "MIT"
},
"node_modules/jest-get-type": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",
"integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/jest-matcher-utils": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz",
"integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "^4.0.0",
"jest-diff": "^29.7.0",
"jest-get-type": "^29.6.3",
"pretty-format": "^29.7.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/jest-matcher-utils/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/jest-matcher-utils/node_modules/pretty-format": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
"integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "^29.6.3",
"ansi-styles": "^5.0.0",
"react-is": "^18.0.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/jest-matcher-utils/node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"dev": true,
"license": "MIT"
},
"node_modules/jest-message-util": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz",
"integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.12.13",
"@jest/types": "^29.6.3",
"@types/stack-utils": "^2.0.0",
"chalk": "^4.0.0",
"graceful-fs": "^4.2.9",
"micromatch": "^4.0.4",
"pretty-format": "^29.7.0",
"slash": "^3.0.0",
"stack-utils": "^2.0.3"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/jest-message-util/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/jest-message-util/node_modules/pretty-format": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
"integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "^29.6.3",
"ansi-styles": "^5.0.0",
"react-is": "^18.0.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/jest-message-util/node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"dev": true,
"license": "MIT"
},
"node_modules/jest-util": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz",
"integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "^29.6.3",
"@types/node": "*",
"chalk": "^4.0.0",
"ci-info": "^3.2.0",
"graceful-fs": "^4.2.9",
"picomatch": "^2.2.3"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/jiti": {
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
@@ -15477,6 +15825,29 @@
"dev": true,
"license": "CC0-1.0"
},
"node_modules/stack-utils": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
"integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"escape-string-regexp": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/stack-utils/node_modules/escape-string-regexp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",

View File

@@ -83,6 +83,7 @@
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.5",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
@@ -103,7 +104,7 @@
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.3",
"eslint-plugin-react-hooks": "^4.6.2",
"husky": "^9.1.6",
"husky": "^9.1.7",
"jsdom": "^25.0.1",
"lint-staged": "^15.3.0",
"msw": "^2.6.6",

View File

@@ -0,0 +1,70 @@
#!/usr/bin/env python3
import json
import re
from collections import defaultdict
from typing import Dict, List, Set
def load_json_file(file_path: str) -> str:
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
def save_json_file(file_path: str, content: dict) -> None:
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(content, f, ensure_ascii=False, indent=2)
f.write('\n') # Add newline at end of file
def find_duplicate_keys(content: str) -> Dict[str, List[int]]:
"""Find all duplicate keys and their line numbers in the file."""
key_pattern = r'"([^"]+)": {'
matches = re.finditer(key_pattern, content)
key_positions = defaultdict(list)
for match in matches:
key = match.group(1)
# Get line number by counting newlines before this position
line_number = content[:match.start()].count('\n') + 1
key_positions[key].append(line_number)
return {k: v for k, v in key_positions.items() if len(v) > 1}
def merge_translations(translations: List[dict]) -> dict:
"""Merge multiple translation objects, keeping all unique translations."""
result = {}
for trans in translations:
for lang, text in trans.items():
if lang not in result:
result[lang] = text
elif result[lang].lower() != text.lower():
# If we have conflicting translations, prefer the one with proper capitalization
if text[0].isupper():
result[lang] = text
return result
def fix_duplicates(file_path: str) -> None:
content = load_json_file(file_path)
data = json.loads(content)
# Find all duplicate keys
duplicates = find_duplicate_keys(content)
# Process each duplicate
for key, positions in duplicates.items():
print(f"Processing duplicate key: {key}")
# Collect all translations for this key
translations = []
for pos in positions:
if key in data:
translations.append(data[key])
del data[key] # Remove the duplicate
# Merge translations and add back to data
if translations:
data[key] = merge_translations(translations)
# Save the fixed file
save_json_file(file_path, data)
if __name__ == "__main__":
file_path = "src/i18n/translation.json"
fix_duplicates(file_path)

View File

@@ -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";
@@ -39,6 +41,7 @@ export function ChatInput({
className,
buttonClassName,
}: ChatInputProps) {
const { t } = useTranslation();
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
const [isDraggingOver, setIsDraggingOver] = React.useState(false);
@@ -117,7 +120,7 @@ export function ChatInput({
<TextareaAutosize
ref={textareaRef}
name={name}
placeholder={placeholder}
placeholder={placeholder || t(I18nKey.SUGGESTIONS$WHAT_TO_BUILD)}
onKeyDown={handleKeyPress}
onChange={handleChange}
onFocus={onFocus}

View File

@@ -1,8 +1,10 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { ChatInput } from "./chat-input";
import { cn } from "#/utils/utils";
import { ImageCarousel } from "../images/image-carousel";
import { UploadImageInput } from "../images/upload-image-input";
import { I18nKey } from "#/i18n/declaration";
interface InteractiveChatBoxProps {
isDisabled?: boolean;
@@ -21,6 +23,7 @@ export function InteractiveChatBox({
value,
onChange,
}: InteractiveChatBoxProps) {
const { t } = useTranslation();
const [images, setImages] = React.useState<File[]>([]);
const handleUpload = (files: File[]) => {
@@ -68,7 +71,7 @@ export function InteractiveChatBox({
<ChatInput
disabled={isDisabled}
button={mode}
placeholder="What do you want to build?"
placeholder={t(I18nKey.SUGGESTIONS$WHAT_TO_BUILD)}
onChange={onChange}
onSubmit={handleSubmit}
onStop={onStop}

View File

@@ -1,4 +1,6 @@
import { useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import PauseIcon from "#/assets/pause";
import PlayIcon from "#/assets/play";
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
@@ -9,6 +11,7 @@ import { IGNORE_TASK_STATE_MAP } from "#/ignore-task-state-map.constant";
import { ActionButton } from "#/components/shared/buttons/action-button";
export function AgentControlBar() {
const { t } = useTranslation();
const { send } = useWsClient();
const { curAgentState } = useSelector((state: RootState) => 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

View File

@@ -1,5 +1,7 @@
import React from "react";
import { useLocation, useNavigate, 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 navigate = useNavigate();
const location = useLocation();
@@ -89,7 +92,9 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
)}
{conversations?.length === 0 && (
<div className="flex flex-col items-center justify-center h-full">
<p className="text-neutral-400">No conversations found</p>
<p className="text-neutral-400">
{t(I18nKey.CONVERSATION$NO_CONVERSATIONS)}
</p>
</div>
)}
{conversations?.map((project) => (

View File

@@ -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 (
<button
data-testid="new-conversation-button"
@@ -10,7 +14,7 @@ export function NewConversationButton({ onClick }: NewConversationButtonProps) {
onClick={onClick}
className="font-bold bg-[#4465DB] px-2 py-1 rounded"
>
+ New Project
+ {t(I18nKey.PROJECT$NEW)}
</button>
);
}

View File

@@ -1,4 +1,5 @@
import React from "react";
import { useTranslation } from "react-i18next";
import {
Autocomplete,
AutocompleteItem,
@@ -6,6 +7,7 @@ import {
} from "@nextui-org/react";
import { useDispatch } from "react-redux";
import posthog from "posthog-js";
import { I18nKey } from "#/i18n/declaration";
import { setSelectedRepository } from "#/state/initial-query-slice";
import { useConfig } from "#/hooks/query/use-config";
import { sanitizeQuery } from "#/utils/sanitize-query";
@@ -23,6 +25,7 @@ export function GitHubRepositorySelector({
userRepositories,
publicRepositories,
}: GitHubRepositorySelectorProps) {
const { t } = useTranslation();
const { data: config } = useConfig();
const [selectedKey, setSelectedKey] = React.useState<string | null>(null);
@@ -49,14 +52,14 @@ export function GitHubRepositorySelector({
dispatch(setSelectedRepository(null));
};
const emptyContent = "No results found.";
const emptyContent = t(I18nKey.GITHUB$NO_RESULTS);
return (
<Autocomplete
data-testid="github-repo-selector"
name="repo"
aria-label="GitHub Repository"
placeholder="Select a GitHub project"
placeholder={t(I18nKey.LANDING$SELECT_REPO)}
isVirtualized={false}
selectedKey={selectedKey}
inputProps={{
@@ -86,12 +89,12 @@ export function GitHubRepositorySelector({
rel="noreferrer noopener"
onClick={(e) => e.stopPropagation()}
>
Add more repositories...
{t(I18nKey.GITHUB$ADD_MORE_REPOS)}
</a>
</AutocompleteItem> // eslint-disable-next-line @typescript-eslint/no-explicit-any
) as any)}
{userRepositories.length > 0 && (
<AutocompleteSection showDivider title="Your Repos">
<AutocompleteSection showDivider title={t(I18nKey.GITHUB$YOUR_REPOS)}>
{userRepositories.map((repo) => (
<AutocompleteItem
data-testid="github-repo-item"
@@ -106,7 +109,7 @@ export function GitHubRepositorySelector({
</AutocompleteSection>
)}
{publicRepositories.length > 0 && (
<AutocompleteSection showDivider title="Public Repos">
<AutocompleteSection showDivider title={t(I18nKey.GITHUB$PUBLIC_REPOS)}>
{publicRepositories.map((repo) => (
<AutocompleteItem
data-testid="github-repo-item"

View File

@@ -1,4 +1,6 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { SuggestionBox } from "#/components/features/suggestions/suggestion-box";
import GitHubLogo from "#/assets/branding/github-logo.svg?react";
import { GitHubRepositorySelector } from "./github-repo-selector";
@@ -23,6 +25,7 @@ export function GitHubRepositoriesSuggestionBox({
gitHubAuthUrl,
user,
}: GitHubRepositoriesSuggestionBoxProps) {
const { t } = useTranslation();
const [connectToGitHubModalOpen, setConnectToGitHubModalOpen] =
React.useState(false);
const [searchQuery, setSearchQuery] = React.useState<string>("");
@@ -53,7 +56,7 @@ export function GitHubRepositoriesSuggestionBox({
return (
<>
<SuggestionBox
title="Open a Repo"
title={t(I18nKey.LANDING$OPEN_REPO)}
content={
isLoggedIn ? (
<GitHubRepositorySelector
@@ -64,7 +67,7 @@ export function GitHubRepositoriesSuggestionBox({
/>
) : (
<ModalButton
text="Connect to GitHub"
text={t(I18nKey.GITHUB$CONNECT)}
icon={<GitHubLogo width={20} height={20} />}
className="bg-[#791B80] w-full"
onClick={handleConnectToGitHub}

View File

@@ -1,10 +1,13 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import Clip from "#/icons/clip.svg?react";
export function AttachImageLabel() {
const { t } = useTranslation();
return (
<div className="flex self-start items-center text-[#A3A3A3] text-xs leading-[18px] -tracking-[0.08px] cursor-pointer">
<Clip width={16} height={16} />
Attach images
{t(I18nKey.LANDING$ATTACH_IMAGES)}
</div>
);
}

View File

@@ -1,6 +1,8 @@
import Markdown from "react-markdown";
import SyntaxHighlighter from "react-syntax-highlighter";
import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { JupyterLine } from "#/utils/parse-cell-content";
interface JupyterCellOutputProps {
@@ -8,9 +10,12 @@ interface JupyterCellOutputProps {
}
export function JupyterCellOutput({ lines }: JupyterCellOutputProps) {
const { t } = useTranslation();
return (
<div className="rounded-lg bg-gray-800 dark:bg-gray-900 p-2 text-xs">
<div className="mb-1 text-gray-400">STDOUT/STDERR</div>
<div className="mb-1 text-gray-400">
{t(I18nKey.JUPYTER$OUTPUT_LABEL)}
</div>
<pre
className="scrollbar-custom scrollbar-thumb-gray-500 hover:scrollbar-thumb-gray-400 dark:scrollbar-thumb-white/10 dark:hover:scrollbar-thumb-white/20 overflow-auto px-5 max-h-[60vh] bg-gray-800"
style={{ padding: 0, marginBottom: 0, fontSize: "0.75rem" }}

View File

@@ -1,4 +1,6 @@
import { Tooltip } from "@nextui-org/react";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { LoadingSpinner } from "#/components/shared/loading-spinner";
import DefaultUserAvatar from "#/icons/default-user.svg?react";
import { cn } from "#/utils/utils";
@@ -11,6 +13,7 @@ interface UserAvatarProps {
}
export function UserAvatar({ onClick, avatarUrl, isLoading }: UserAvatarProps) {
const { t } = useTranslation();
const buttonContent = (
<button
data-testid="user-avatar"
@@ -24,7 +27,7 @@ export function UserAvatar({ onClick, avatarUrl, isLoading }: UserAvatarProps) {
{!isLoading && avatarUrl && <Avatar src={avatarUrl} />}
{!isLoading && !avatarUrl && (
<DefaultUserAvatar
aria-label="user avatar placeholder"
aria-label={t(I18nKey.USER$AVATAR_PLACEHOLDER)}
width={20}
height={20}
/>
@@ -34,7 +37,7 @@ export function UserAvatar({ onClick, avatarUrl, isLoading }: UserAvatarProps) {
);
return (
<Tooltip content="Account settings" closeDelay={100}>
<Tooltip content={t(I18nKey.USER$ACCOUNT_SETTINGS)} closeDelay={100}>
{buttonContent}
</Tooltip>
);

View File

@@ -1,3 +1,5 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { SuggestionBox } from "./suggestion-box";
interface ImportProjectSuggestionBoxProps {
@@ -7,13 +9,14 @@ interface ImportProjectSuggestionBoxProps {
export function ImportProjectSuggestionBox({
onChange,
}: ImportProjectSuggestionBoxProps) {
const { t } = useTranslation();
return (
<SuggestionBox
title="+ Import Project"
title={t(I18nKey.LANDING$IMPORT_PROJECT)}
content={
<label htmlFor="import-project" className="w-full flex justify-center">
<span className="border-2 border-dashed border-neutral-600 rounded px-2 py-1 cursor-pointer">
Upload a .zip
{t(I18nKey.LANDING$UPLOAD_ZIP)}
</span>
<input
hidden

View File

@@ -1,8 +1,10 @@
import { useTranslation } from "react-i18next";
import { RefreshButton } from "#/components/shared/buttons/refresh-button";
import Lightbulb from "#/icons/lightbulb.svg?react";
import { I18nKey } from "#/i18n/declaration";
interface SuggestionBubbleProps {
suggestion: string;
suggestion: { key: string; value: string };
onClick: () => void;
onRefresh: () => void;
}
@@ -12,6 +14,7 @@ export function SuggestionBubble({
onClick,
onRefresh,
}: SuggestionBubbleProps) {
const { t } = useTranslation();
const handleRefresh = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
onRefresh();
@@ -24,7 +27,7 @@ export function SuggestionBubble({
>
<div className="flex items-center gap-2">
<Lightbulb width={18} height={18} />
<span className="text-sm">{suggestion}</span>
<span className="text-sm">{t(suggestion.key as I18nKey)}</span>
</div>
<RefreshButton onClick={handleRefresh} />
</div>

View File

@@ -1,4 +1,7 @@
export type Suggestion = { label: string; value: string };
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
export type Suggestion = { label: I18nKey | string; value: string };
interface SuggestionItemProps {
suggestion: Suggestion;
@@ -6,6 +9,7 @@ interface SuggestionItemProps {
}
export function SuggestionItem({ suggestion, onClick }: SuggestionItemProps) {
const { t } = useTranslation();
return (
<li className="list-none border border-neutral-600 rounded-xl hover:bg-neutral-700 flex-1">
<button
@@ -14,7 +18,7 @@ export function SuggestionItem({ suggestion, onClick }: SuggestionItemProps) {
onClick={() => onClick(suggestion.value)}
className="text-[16px] leading-6 -tracking-[0.01em] text-center w-full p-3 font-semibold"
>
{suggestion.label}
{t(suggestion.label)}
</button>
</li>
);

View File

@@ -1,9 +1,12 @@
import { useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { cn } from "#/utils/utils";
import { AgentState } from "#/types/agent-state";
import { RootState } from "#/store";
import { I18nKey } from "#/i18n/declaration";
export function TerminalStatusLabel() {
const { t } = useTranslation();
const { curAgentState } = useSelector((state: RootState) => state.agent);
return (
@@ -17,7 +20,7 @@ export function TerminalStatusLabel() {
: "bg-green-500",
)}
/>
Terminal
{t(I18nKey.WORKSPACE$TERMINAL_TAB_LABEL)}
</div>
);
}

View File

@@ -1,20 +1,24 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
interface TOSCheckboxProps {
onChange: () => void;
}
export function TOSCheckbox({ onChange }: TOSCheckboxProps) {
const { t } = useTranslation();
return (
<label className="flex items-center gap-2">
<input type="checkbox" onChange={onChange} />
<span>
I accept the{" "}
{t(I18nKey.TOS$ACCEPT)}{" "}
<a
href="https://www.all-hands.dev/tos"
target="_blank"
rel="noopener noreferrer"
className="underline underline-offset-2 text-blue-500 hover:text-blue-700"
>
terms of service
{t(I18nKey.TOS$TERMS)}
</a>
</span>
</label>

View File

@@ -1,7 +1,11 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
export function BetaBadge() {
const { t } = useTranslation();
return (
<span className="text-[11px] leading-5 text-root-primary bg-neutral-400 px-1 rounded-xl">
Beta
{t(I18nKey.BADGE$BETA)}
</span>
);
}

View File

@@ -22,7 +22,11 @@ export function ActionTooltip({ type, onClick }: ActionTooltipProps) {
<button
data-testid={`action-${type}-button`}
type="button"
aria-label={type === "confirm" ? "Confirm action" : "Reject action"}
aria-label={
type === "confirm"
? t(I18nKey.ACTION$CONFIRM)
: t(I18nKey.ACTION$REJECT)
}
className="bg-neutral-700 rounded-full p-1 hover:bg-neutral-800"
onClick={onClick}
>

View File

@@ -1,11 +1,14 @@
import { useTranslation } from "react-i18next";
import DocsIcon from "#/icons/docs.svg?react";
import { I18nKey } from "#/i18n/declaration";
import { TooltipButton } from "./tooltip-button";
export function DocsButton() {
const { t } = useTranslation();
return (
<TooltipButton
tooltip="Documentation"
ariaLabel="Documentation"
tooltip={t(I18nKey.SIDEBAR$DOCS)}
ariaLabel={t(I18nKey.SIDEBAR$DOCS)}
href="https://docs.all-hands.dev"
>
<DocsIcon width={28} height={28} />

View File

@@ -1,3 +1,5 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import NewProjectIcon from "#/icons/new-project.svg?react";
import { TooltipButton } from "./tooltip-button";
@@ -6,10 +8,12 @@ interface ExitProjectButtonProps {
}
export function ExitProjectButton({ onClick }: ExitProjectButtonProps) {
const { t } = useTranslation();
const startNewProject = t(I18nKey.PROJECT$START_NEW);
return (
<TooltipButton
tooltip="Start new project"
ariaLabel="Start new project"
tooltip={startNewProject}
ariaLabel={startNewProject}
onClick={onClick}
testId="new-project-button"
>

View File

@@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next";
import { cn } from "#/utils/utils";
import VSCodeIcon from "#/assets/vscode-alt.svg?react";
@@ -10,6 +11,9 @@ export function OpenVSCodeButton({
isDisabled,
onClick,
}: OpenVSCodeButtonProps) {
const { t } = useTranslation();
const buttonText = t("OPEN_IN_VSCODE");
return (
<button
type="button"
@@ -21,10 +25,10 @@ export function OpenVSCodeButton({
? "bg-neutral-600 cursor-not-allowed"
: "bg-[#4465DB] hover:bg-[#3451C7]",
)}
aria-label="Open in VS Code"
aria-label={buttonText}
>
<VSCodeIcon width={20} height={20} />
Open in VS Code
{buttonText}
</button>
);
}

View File

@@ -1,4 +1,6 @@
import { useTranslation } from "react-i18next";
import CogTooth from "#/assets/cog-tooth";
import { I18nKey } from "#/i18n/declaration";
import { TooltipButton } from "./tooltip-button";
interface SettingsButtonProps {
@@ -6,11 +8,12 @@ interface SettingsButtonProps {
}
export function SettingsButton({ onClick }: SettingsButtonProps) {
const { t } = useTranslation();
return (
<TooltipButton
testId="settings-button"
tooltip="Settings"
ariaLabel="Settings"
tooltip={t(I18nKey.SIDEBAR$SETTINGS)}
ariaLabel={t(I18nKey.SIDEBAR$SETTINGS)}
onClick={onClick}
>
<CogTooth />

View File

@@ -1,13 +1,17 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
interface StopButtonProps {
isDisabled?: boolean;
onClick?: () => void;
}
export function StopButton({ isDisabled, onClick }: StopButtonProps) {
const { t } = useTranslation();
return (
<button
data-testid="stop-button"
aria-label="Stop"
aria-label={t(I18nKey.BUTTON$STOP)}
disabled={isDisabled}
onClick={onClick}
type="button"

View File

@@ -1,4 +1,6 @@
import { useTranslation } from "react-i18next";
import ArrowSendIcon from "#/icons/arrow-send.svg?react";
import { I18nKey } from "#/i18n/declaration";
interface SubmitButtonProps {
isDisabled?: boolean;
@@ -6,9 +8,10 @@ interface SubmitButtonProps {
}
export function SubmitButton({ isDisabled, onClick }: SubmitButtonProps) {
const { t } = useTranslation();
return (
<button
aria-label="Send"
aria-label={t(I18nKey.BUTTON$SEND)}
disabled={isDisabled}
onClick={onClick}
type="submit"

View File

@@ -1,3 +1,6 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
export interface DownloadProgressState {
filesTotal: number;
filesDownloaded: number;
@@ -16,6 +19,7 @@ export function DownloadProgress({
progress,
onCancel,
}: DownloadProgressProps) {
const { t } = useTranslation();
const formatBytes = (bytes: number) => {
const units = ["B", "KB", "MB", "GB"];
let size = bytes;
@@ -33,12 +37,12 @@ export function DownloadProgress({
<div className="mb-4">
<h3 className="text-lg font-semibold mb-2 text-white">
{progress.isDiscoveringFiles
? "Preparing Download..."
: "Downloading Files"}
? t(I18nKey.DOWNLOAD$PREPARING)
: t(I18nKey.DOWNLOAD$DOWNLOADING)}
</h3>
<p className="text-sm text-gray-400 truncate">
{progress.isDiscoveringFiles
? `Found ${progress.filesTotal} files...`
? t(I18nKey.DOWNLOAD$FOUND_FILES, { count: progress.filesTotal })
: progress.currentFile}
</p>
</div>
@@ -64,8 +68,11 @@ export function DownloadProgress({
<div className="flex justify-between text-sm text-gray-400">
<span>
{progress.isDiscoveringFiles
? `Scanning workspace...`
: `${progress.filesDownloaded} of ${progress.filesTotal} files`}
? t(I18nKey.DOWNLOAD$SCANNING)
: t(I18nKey.DOWNLOAD$FILES_PROGRESS, {
downloaded: progress.filesDownloaded,
total: progress.filesTotal,
})}
</span>
{!progress.isDiscoveringFiles && (
<span>{formatBytes(progress.bytesDownloadedPerSecond)}/s</span>
@@ -78,7 +85,7 @@ export function DownloadProgress({
onClick={onCancel}
className="px-4 py-2 text-sm text-gray-400 hover:text-white transition-colors"
>
Cancel
{t(I18nKey.DOWNLOAD$CANCEL)}
</button>
</div>
</div>

View File

@@ -1,24 +1,26 @@
import { useTranslation } from "react-i18next";
import BuildIt from "#/icons/build-it.svg?react";
import { I18nKey } from "#/i18n/declaration";
export function HeroHeading() {
const { t } = useTranslation();
return (
<div className="w-[304px] text-center flex flex-col gap-4 items-center py-4">
<BuildIt width={88} height={104} />
<h1 className="text-[38px] leading-[32px] -tracking-[0.02em]">
Let&apos;s Start Building!
{t(I18nKey.LANDING$TITLE)}
</h1>
<p className="mx-4 text-sm flex flex-col gap-2">
OpenHands makes it easy to build and maintain software using a simple
prompt.{" "}
{t(I18nKey.LANDING$SUBTITLE)}{" "}
<span className="">
Not sure how to start?{" "}
{t(I18nKey.LANDING$START_HELP)}{" "}
<a
rel="noopener noreferrer"
target="_blank"
href="https://docs.all-hands.dev/modules/usage/getting-started"
className="text-hyperlink underline underline-offset-[3px]"
>
Read this
{t(I18nKey.LANDING$START_HELP_LINK)}
</a>
</span>
</p>

View File

@@ -1,7 +1,6 @@
import { Input, Tooltip } from "@nextui-org/react";
import { useTranslation } from "react-i18next";
import { FaCheckCircle, FaExclamationCircle } from "react-icons/fa";
import { I18nKey } from "#/i18n/declaration";
interface APIKeyInputProps {
isDisabled: boolean;
@@ -22,14 +21,14 @@ export function APIKeyInput({ isDisabled, isSet }: APIKeyInputProps) {
{!isSet && (
<FaExclamationCircle className="text-[#FF3860] inline-block" />
)}
{t(I18nKey.SETTINGS_FORM$API_KEY_LABEL)}
{t("API_KEY")}
</label>
</Tooltip>
<Input
isDisabled={isDisabled}
id="api-key"
name="api-key"
aria-label="API Key"
aria-label={t("API_KEY")}
type="password"
defaultValue=""
classNames={{
@@ -37,14 +36,14 @@ export function APIKeyInput({ isDisabled, isSet }: APIKeyInputProps) {
}}
/>
<p className="text-sm text-[#A3A3A3]">
{t(I18nKey.SETTINGS_FORM$DONT_KNOW_API_KEY_LABEL)}{" "}
{t("DONT_KNOW_API_KEY")}{" "}
<a
href="https://docs.all-hands.dev/modules/usage/llms"
rel="noreferrer noopener"
target="_blank"
className="underline underline-offset-2"
>
{t(I18nKey.SETTINGS_FORM$CLICK_HERE_FOR_INSTRUCTIONS_LABEL)}
{t("CLICK_FOR_INSTRUCTIONS")}
</a>
</p>
</fieldset>

View File

@@ -64,7 +64,7 @@ export function AccountSettingsForm({
<ModalBody>
<form className="flex flex-col w-full gap-6" onSubmit={handleSubmit}>
<div className="w-full flex flex-col gap-2">
<BaseModalTitle title="Account Settings" />
<BaseModalTitle title={t(I18nKey.ACCOUNT_SETTINGS$TITLE)} />
{config?.APP_MODE === "saas" && config?.APP_SLUG && (
<a
@@ -73,12 +73,12 @@ export function AccountSettingsForm({
rel="noreferrer noopener"
className="underline"
>
Configure Github Repositories
{t("CONFIGURE_GITHUB_REPOS")}
</a>
)}
<FormFieldset
id="language"
label="Language"
label={t("LANGUAGE_LABEL")}
defaultSelectedKey={selectedLanguage}
isClearable={false}
items={AvailableLanguages.map(({ label, value: key }) => ({
@@ -91,32 +91,30 @@ export function AccountSettingsForm({
<>
<CustomInput
name="ghToken"
label="GitHub Token"
label={t("GITHUB_TOKEN_OPTIONAL")}
type="password"
defaultValue={gitHubToken ?? ""}
/>
<BaseModalDescription>
{t(I18nKey.CONNECT_TO_GITHUB_MODAL$GET_YOUR_TOKEN)}{" "}
{t("GET_YOUR_TOKEN")}{" "}
<a
href="https://github.com/settings/tokens/new?description=openhands-app&scopes=repo,user,workflow"
target="_blank"
rel="noreferrer noopener"
className="text-[#791B80] underline"
>
{t(I18nKey.CONNECT_TO_GITHUB_MODAL$HERE)}
{t("HERE")}
</a>
</BaseModalDescription>
</>
)}
{gitHubError && (
<p className="text-danger text-xs">
{t(I18nKey.ACCOUNT_SETTINGS_MODAL$GITHUB_TOKEN_INVALID)}
</p>
<p className="text-danger text-xs">{t("GITHUB_TOKEN_INVALID")}</p>
)}
{gitHubToken && !gitHubError && (
<ModalButton
variant="text-like"
text={t(I18nKey.ACCOUNT_SETTINGS_MODAL$DISCONNECT)}
text={t("DISCONNECT_BUTTON")}
onClick={() => {
logout();
onClose();
@@ -132,18 +130,18 @@ export function AccountSettingsForm({
type="checkbox"
defaultChecked={analyticsConsent === "true"}
/>
Enable analytics
{t("ENABLE_ANALYTICS")}
</label>
<div className="flex flex-col gap-2 w-full">
<ModalButton
type="submit"
intent="account"
text={t(I18nKey.ACCOUNT_SETTINGS_MODAL$SAVE)}
text={t("SAVE_BUTTON")}
className="bg-[#4465DB]"
/>
<ModalButton
text={t(I18nKey.ACCOUNT_SETTINGS_MODAL$CLOSE)}
text={t("CLOSE_BUTTON")}
onClick={onClose}
className="bg-[#737373]"
/>

View File

@@ -4,6 +4,7 @@ import {
AutocompleteSection,
} from "@nextui-org/react";
import React from "react";
import { useTranslation } from "react-i18next";
import { mapProvider } from "#/utils/map-provider";
import { VERIFIED_MODELS, VERIFIED_PROVIDERS } from "#/utils/verified-models";
import { extractModelAndProvider } from "#/utils/extract-model-and-provider";
@@ -60,12 +61,14 @@ export function ModelSelector({
setLitellmId(null);
};
const { t } = useTranslation();
return (
<div data-testid="model-selector" className="flex flex-col gap-2">
<div className="flex flex-row gap-3">
<fieldset className="flex flex-col gap-2">
<label htmlFor="agent" className="font-[500] text-[#A3A3A3] text-xs">
LLM Provider
{t("LLM_PROVIDER")}
</label>
<Autocomplete
data-testid="llm-provider"
@@ -73,8 +76,8 @@ export function ModelSelector({
isVirtualized={false}
name="llm-provider"
isDisabled={isDisabled}
aria-label="LLM Provider"
placeholder="Select a provider"
aria-label={t("LLM_PROVIDER")}
placeholder={t("SELECT_PROVIDER_PLACEHOLDER")}
isClearable={false}
onSelectionChange={(e) => {
if (e?.toString()) handleChangeProvider(e.toString());
@@ -115,15 +118,15 @@ export function ModelSelector({
<fieldset className="flex flex-col gap-2">
<label htmlFor="agent" className="font-[500] text-[#A3A3A3] text-xs">
LLM Model
{t("LLM_MODEL")}
</label>
<Autocomplete
data-testid="llm-model"
isRequired
isVirtualized={false}
name="llm-model"
aria-label="LLM Model"
placeholder="Select a model"
aria-label={t("LLM_MODEL")}
placeholder={t("SELECT_MODEL_PLACEHOLDER")}
isClearable={false}
onSelectionChange={(e) => {
if (e?.toString()) handleChangeModel(e.toString());

View File

@@ -6,7 +6,6 @@ import { organizeModelsAndProviders } from "#/utils/organize-models-and-provider
import { getDefaultSettings, Settings } from "#/services/settings";
import { extractModelAndProvider } from "#/utils/extract-model-and-provider";
import { DangerModal } from "../confirmation-modals/danger-modal";
import { I18nKey } from "#/i18n/declaration";
import { extractSettings, saveSettingsView } from "#/utils/settings-utils";
import { useEndSession } from "#/hooks/use-end-session";
import { ModalButton } from "../../buttons/modal-button";
@@ -206,18 +205,18 @@ export function SettingsForm({
<ModalButton
disabled={disabled}
type="submit"
text={t(I18nKey.SETTINGS_FORM$SAVE_LABEL)}
text={t("SAVE_BUTTON")}
className="bg-[#4465DB] w-full"
/>
<ModalButton
text={t(I18nKey.SETTINGS_FORM$CLOSE_LABEL)}
text={t("CLOSE_BUTTON")}
className="bg-[#737373] w-full"
onClick={onClose}
/>
</div>
<ModalButton
disabled={disabled}
text={t(I18nKey.SETTINGS_FORM$RESET_TO_DEFAULTS_LABEL)}
text={t("RESET_TO_DEFAULTS")}
variant="text-like"
className="text-danger self-start"
onClick={() => {
@@ -231,17 +230,15 @@ export function SettingsForm({
<ModalBackdrop>
<DangerModal
testId="reset-defaults-modal"
title={t(I18nKey.SETTINGS_FORM$ARE_YOU_SURE_LABEL)}
description={t(
I18nKey.SETTINGS_FORM$ALL_INFORMATION_WILL_BE_DELETED_MESSAGE,
)}
title={t("CONFIRM_RESET_TITLE")}
description={t("CONFIRM_RESET_MESSAGE")}
buttons={{
danger: {
text: t(I18nKey.SETTINGS_FORM$RESET_TO_DEFAULTS_LABEL),
text: t("RESET_TO_DEFAULTS"),
onClick: handleConfirmResetSettings,
},
cancel: {
text: t(I18nKey.SETTINGS_FORM$CANCEL_LABEL),
text: t("CANCEL_BUTTON"),
onClick: () => setConfirmResetDefaultsModalOpen(false),
},
}}
@@ -251,17 +248,15 @@ export function SettingsForm({
{confirmEndSessionModalOpen && (
<ModalBackdrop>
<DangerModal
title={t(I18nKey.SETTINGS_FORM$END_SESSION_LABEL)}
description={t(
I18nKey.SETTINGS_FORM$CHANGING_WORKSPACE_WARNING_MESSAGE,
)}
title={t("END_SESSION_TITLE")}
description={t("END_SESSION_MESSAGE")}
buttons={{
danger: {
text: t(I18nKey.SETTINGS_FORM$END_SESSION_LABEL),
text: t("END_SESSION_BUTTON"),
onClick: handleConfirmEndSession,
},
cancel: {
text: t(I18nKey.SETTINGS_FORM$CANCEL_LABEL),
text: t("CANCEL_BUTTON"),
onClick: () => setConfirmEndSessionModalOpen(false),
},
}}

View File

@@ -1,5 +1,7 @@
import { useTranslation } from "react-i18next";
import { useAIConfigOptions } from "#/hooks/query/use-ai-config-options";
import { Settings } from "#/services/settings";
import { I18nKey } from "#/i18n/declaration";
import { LoadingSpinner } from "../../loading-spinner";
import { ModalBackdrop } from "../modal-backdrop";
import { SettingsForm } from "./settings-form";
@@ -11,6 +13,7 @@ interface SettingsModalProps {
export function SettingsModal({ onClose, settings }: SettingsModalProps) {
const aiConfigOptions = useAIConfigOptions();
const { t } = useTranslation();
return (
<ModalBackdrop onClose={onClose}>
@@ -22,14 +25,12 @@ export function SettingsModal({ onClose, settings }: SettingsModalProps) {
<p className="text-danger text-xs">{aiConfigOptions.error.message}</p>
)}
<span className="text-xl leading-6 font-semibold -tracking-[0.01em">
AI Provider Configuration
{t(I18nKey.SETTINGS$TITLE)}
</span>
<p className="text-xs text-[#A3A3A3]">
To continue, connect an OpenAI, Anthropic, or other LLM account
</p>
<p className="text-xs text-danger">
Changing settings during an active session will end the session
{t(I18nKey.SETTINGS$DESCRIPTION)}
</p>
<p className="text-xs text-danger">{t(I18nKey.SETTINGS$WARNING)}</p>
{aiConfigOptions.isLoading && (
<div className="flex justify-center">
<LoadingSpinner size="small" />

View File

@@ -0,0 +1,66 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { TaskForm } from "./task-form";
import { Provider } from "react-redux";
import { configureStore } from "@reduxjs/toolkit";
import initialQueryReducer from "#/state/initial-query-slice";
import { vi, describe, it, expect, beforeEach } from "vitest";
vi.mock("react-router", () => ({
useNavigation: () => ({ state: "idle" }),
useNavigate: () => vi.fn(),
}));
vi.mock("#/context/auth-context", () => ({
useAuth: () => ({
gitHubToken: null,
}),
}));
vi.mock("@tanstack/react-query", () => ({
useMutation: () => ({
mutate: vi.fn(),
isPending: false,
}),
useQueryClient: () => ({
invalidateQueries: vi.fn(),
}),
}));
describe("TaskForm", () => {
beforeEach(() => {
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => {
if (key === "SUGGESTIONS$WHAT_TO_BUILD") {
return "What do you want to build?";
}
return key;
},
}),
}));
});
it("should use i18n key for placeholder text", () => {
const store = configureStore({
reducer: {
initialQuery: initialQueryReducer,
},
});
const ref = React.createRef<HTMLFormElement>();
render(
<Provider store={store}>
<TaskForm ref={ref} />
</Provider>
);
// The placeholder text should be translated
const input = screen.getByPlaceholderText("What do you want to build?");
expect(input).toBeInTheDocument();
});
});

View File

@@ -1,6 +1,8 @@
import React from "react";
import { useNavigation } from "react-router";
import { useDispatch, useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { RootState } from "#/store";
import { addFile, removeFile } from "#/state/initial-query-slice";
import { SuggestionBubble } from "#/components/features/suggestions/suggestion-bubble";
@@ -20,6 +22,7 @@ interface TaskFormProps {
}
export function TaskForm({ ref }: TaskFormProps) {
const { t } = useTranslation();
const dispatch = useDispatch();
const navigation = useNavigation();
@@ -28,9 +31,10 @@ export function TaskForm({ ref }: TaskFormProps) {
);
const [text, setText] = React.useState("");
const [suggestion, setSuggestion] = React.useState(
getRandomKey(SUGGESTIONS["non-repo"]),
);
const [suggestion, setSuggestion] = React.useState(() => {
const key = getRandomKey(SUGGESTIONS["non-repo"]);
return { key, value: SUGGESTIONS["non-repo"][key] };
});
const [inputIsFocused, setInputIsFocused] = React.useState(false);
const { mutate: createConversation, isPending } = useCreateConversation();
@@ -38,24 +42,22 @@ export function TaskForm({ ref }: TaskFormProps) {
const suggestions = SUGGESTIONS["non-repo"];
// remove current suggestion to avoid refreshing to the same suggestion
const suggestionCopy = { ...suggestions };
delete suggestionCopy[suggestion];
delete suggestionCopy[suggestion.key];
const key = getRandomKey(suggestionCopy);
setSuggestion(key);
setSuggestion({ key, value: suggestions[key] });
};
const onClickSuggestion = () => {
const suggestions = SUGGESTIONS["non-repo"];
const value = suggestions[suggestion];
setText(value);
setText(suggestion.value);
};
const placeholder = React.useMemo(() => {
if (selectedRepository) {
return `What would you like to change in ${selectedRepository}?`;
return t(I18nKey.LANDING$CHANGE_PROMPT, { repo: selectedRepository });
}
return "What do you want to build?";
return t(I18nKey.SUGGESTIONS$WHAT_TO_BUILD);
}, [selectedRepository]);
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
@@ -105,7 +107,7 @@ export function TaskForm({ ref }: TaskFormProps) {
dispatch(addFile(base64));
});
}}
placeholder={placeholder}
placeholder={t(I18nKey.SUGGESTIONS$WHAT_TO_BUILD)}
value={text}
maxRows={15}
showButton={!!text}

View File

@@ -2,12 +2,30 @@ import i18n from "i18next";
import Backend from "i18next-http-backend";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";
import translations from "./translation.json";
type TranslationValue = {
en: string;
ja?: string;
"zh-CN"?: string;
"zh-TW"?: string;
"ko-KR"?: string;
no?: string;
ar?: string;
de?: string;
fr?: string;
it?: string;
pt?: string;
es?: string;
tr?: string;
};
export const AvailableLanguages = [
{ label: "English", value: "en" },
{ label: "简体中文", value: "zh-CN" },
{ label: "繁體中文", value: "zh-TW" },
{ label: "한국어", value: "ko-KR" },
{ label: "日本語", value: "ja" },
{ label: "Norsk", value: "no" },
{ label: "Arabic", value: "ar" },
{ label: "Deutsch", value: "de" },
@@ -25,6 +43,119 @@ i18n
.init({
fallbackLng: "en",
debug: import.meta.env.NODE_ENV === "development",
lng:
typeof window !== "undefined"
? localStorage.getItem("LANGUAGE") || "en"
: "en",
resources: {
en: {
translation: Object.fromEntries(
Object.entries(translations).map(([key, value]) => [
key,
(value as TranslationValue).en,
]),
),
},
ja: {
translation: Object.fromEntries(
Object.entries(translations).map(([key, value]) => [
key,
(value as TranslationValue).ja || (value as TranslationValue).en,
]),
),
},
"zh-CN": {
translation: Object.fromEntries(
Object.entries(translations).map(([key, value]) => [
key,
(value as TranslationValue)["zh-CN"] ||
(value as TranslationValue).en,
]),
),
},
"zh-TW": {
translation: Object.fromEntries(
Object.entries(translations).map(([key, value]) => [
key,
(value as TranslationValue)["zh-TW"] ||
(value as TranslationValue).en,
]),
),
},
"ko-KR": {
translation: Object.fromEntries(
Object.entries(translations).map(([key, value]) => [
key,
(value as TranslationValue)["ko-KR"] ||
(value as TranslationValue).en,
]),
),
},
no: {
translation: Object.fromEntries(
Object.entries(translations).map(([key, value]) => [
key,
(value as TranslationValue).no || (value as TranslationValue).en,
]),
),
},
ar: {
translation: Object.fromEntries(
Object.entries(translations).map(([key, value]) => [
key,
(value as TranslationValue).ar || (value as TranslationValue).en,
]),
),
},
de: {
translation: Object.fromEntries(
Object.entries(translations).map(([key, value]) => [
key,
(value as TranslationValue).de || (value as TranslationValue).en,
]),
),
},
fr: {
translation: Object.fromEntries(
Object.entries(translations).map(([key, value]) => [
key,
(value as TranslationValue).fr || (value as TranslationValue).en,
]),
),
},
it: {
translation: Object.fromEntries(
Object.entries(translations).map(([key, value]) => [
key,
(value as TranslationValue).it || (value as TranslationValue).en,
]),
),
},
pt: {
translation: Object.fromEntries(
Object.entries(translations).map(([key, value]) => [
key,
(value as TranslationValue).pt || (value as TranslationValue).en,
]),
),
},
es: {
translation: Object.fromEntries(
Object.entries(translations).map(([key, value]) => [
key,
(value as TranslationValue).es || (value as TranslationValue).en,
]),
),
},
tr: {
translation: Object.fromEntries(
Object.entries(translations).map(([key, value]) => [
key,
(value as TranslationValue).tr || (value as TranslationValue).en,
]),
),
},
},
});
export default i18n;

File diff suppressed because it is too large Load Diff

View File

View File

@@ -1,6 +1,8 @@
import React from "react";
import { useDispatch } from "react-redux";
import { useTranslation } from "react-i18next";
import posthog from "posthog-js";
import { I18nKey } from "#/i18n/declaration";
import { setImportedProjectZip } from "#/state/initial-query-slice";
import { convertZipToBase64 } from "#/utils/convert-zip-to-base64";
import { useGitHubUser } from "#/hooks/query/use-github-user";
@@ -13,6 +15,7 @@ import { HeroHeading } from "#/components/shared/hero-heading";
import { TaskForm } from "#/components/shared/task-form";
function Home() {
const { t } = useTranslation();
const { gitHubToken } = useAuth();
const dispatch = useDispatch();
const formRef = React.useRef<HTMLFormElement>(null);
@@ -62,12 +65,12 @@ function Home() {
{latestConversation && (
<div className="flex gap-4 w-full text-center mt-8">
<p className="text-center w-full">
Or&nbsp;
{t(I18nKey.LANDING$OR)}&nbsp;
<a
className="underline"
href={`/conversations/${latestConversation}`}
>
jump back to your most recent conversation
{t(I18nKey.LANDING$RECENT_CONVERSATION)}
</a>
</p>
</div>

View File

@@ -3,6 +3,8 @@ import React from "react";
import { Outlet } from "react-router";
import { useDispatch, useSelector } from "react-redux";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import {
ConversationProvider,
useConversation,
@@ -37,6 +39,7 @@ import { useSettings } from "#/hooks/query/use-settings";
import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
function AppContent() {
const { t } = useTranslation();
const { gitHubToken } = useAuth();
const { data: settings } = useSettings();
@@ -137,12 +140,20 @@ function AppContent() {
<Container
className="h-full"
labels={[
{ label: "Workspace", to: "", icon: <CodeIcon /> },
{ label: "Jupyter", to: "jupyter", icon: <ListIcon /> },
{
label: t(I18nKey.WORKSPACE$TITLE),
to: "",
icon: <CodeIcon />,
},
{
label: t(I18nKey.WORKSPACE$JUPYTER_TAB_LABEL),
to: "jupyter",
icon: <ListIcon />,
},
{
label: (
<div className="flex items-center gap-1">
Browser
{t(I18nKey.WORKSPACE$BROWSER_TAB_LABEL)}
{updateCount > 0 && <CountBadge count={updateCount} />}
</div>
),

View File

@@ -1,4 +1,6 @@
const KEY_1 = "Build an app to view pull requests";
import { I18nKey } from "#/i18n/declaration";
const KEY_1 = I18nKey.LANDING$BUILD_APP_BUTTON;
const VALUE_1 = `I want to create a React app to view all of the open pull
requests that exist on all of my team's github repos. Here
are some details:
@@ -15,7 +17,7 @@ are some details:
When things are working, initialize a github repo, create
a .gitignore file, and commit the changes.`;
const KEY_2 = "Build a todo list application";
const KEY_2 = I18nKey.SUGGESTIONS$TODO_APP;
const VALUE_2 = `I want to create a VueJS app that allows me to:
* See all the items on my todo list
* add a new item to the list
@@ -28,7 +30,7 @@ This should be a client-only app with no backend. The list should persist in loc
Please add tests for all of the above and make sure they pass`;
const KEY_3 = "Write a bash script that shows the top story on Hacker News";
const KEY_3 = I18nKey.SUGGESTIONS$HACKER_NEWS;
const VALUE_3 = `Please write a bash script which displays the top story on Hacker News. It should show the title, the link, and the number of points.
The script should only use tools that are widely available on unix systems, like curl and grep.`;

View File

@@ -1,4 +1,4 @@
const KEY_1 = "Increase my test coverage";
const KEY_1 = "INCREASE_TEST_COVERAGE";
const VALUE_1 = `I want to increase the test coverage of the repository in the current directory.
Please investigate the repo to figure out what language is being used, and where tests are located, if there are any.
@@ -9,10 +9,10 @@ If there are existing tests, find a function or method which lacks adequate unit
Make sure the tests pass before you finish.`;
const KEY_2 = "Auto-merge Dependabot PRs";
const KEY_2 = "AUTO_MERGE_PRS";
const VALUE_2 = `Please add a GitHub action to this repository which automatically merges pull requests from Dependabot so long as the tests are passing.`;
const KEY_3 = "Fix up my README";
const KEY_3 = "FIX_README";
const VALUE_3 = `Please look at the README and make the following improvements, if they make sense:
* correct any typos that you find
* add missing language annotations on codeblocks
@@ -22,7 +22,7 @@ const VALUE_3 = `Please look at the README and make the following improvements,
If there are no obvious ways to improve the README, make at least one small change to make the wording clearer or friendlier`;
const KEY_4 = "Clean up my dependencies";
const KEY_4 = "CLEAN_DEPENDENCIES";
const VALUE_4 = `Examine the dependencies of the current codebase. Make sure you can run the code and any tests.
Then run any commands necessary to update all dependencies to the latest versions, and make sure the code continues to run correctly and the tests pass. If changes need to be made to the codebase, go ahead and make those changes. You can look up documentation for new versions using the browser if you need to.
@@ -31,10 +31,10 @@ If a particular dependency update is causing trouble (e.g. breaking changes that
Additionally, if you're able to prune any dependencies that are obviously unused, please do so. You may use third party tools to check for unused dependencies.`;
const KEY_5 = "Add best practices docs for contributors";
const KEY_5 = "ADD_DOCS";
const VALUE_5 = `Investigate the documentation in the root of the current repo. Please add a CODE_OF_CONDUCT.md and CONTRIBUTORS.md with good defaults if they are not present. Use information in the README to inform the CONTRIBUTORS doc. If there is no LICENSE currently in the repo, please add the Apache 2.0 license. Add links to all these documents into the README`;
const KEY_6 = "Add/improve a Dockerfile";
const KEY_6 = "ADD_DOCKERFILE";
const VALUE_6 = `Investigate the current repo to understand the installation instructions. Then create a Dockerfile that runs the application, using best practices like arguments and multi-stage builds wherever appropriate.
If there is an existing Dockerfile, and there are ways to improve it according to best practices, do so.`;

View File

@@ -267,10 +267,7 @@ class DockerRuntime(ActionExecutionClient):
environment=environment,
volumes=volumes,
device_requests=(
[docker.types.DeviceRequest(
capabilities=[['gpu']],
count=-1
)]
[docker.types.DeviceRequest(capabilities=[['gpu']], count=-1)]
if self.config.sandbox.enable_gpu
else None
),

28
package-lock.json generated Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "OpenHands",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"devDependencies": {
"husky": "^9.1.7"
}
},
"node_modules/husky": {
"version": "9.1.7",
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
"integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
"dev": true,
"license": "MIT",
"bin": {
"husky": "bin.js"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/typicode"
}
}
}
}

5
package.json Normal file
View File

@@ -0,0 +1,5 @@
{
"devDependencies": {
"husky": "^9.1.7"
}
}