mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
62 Commits
fix-git-ch
...
i18n-impro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03d1d6c9a9 | ||
|
|
3f9e652a02 | ||
|
|
1da76b371b | ||
|
|
b3c883a1a9 | ||
|
|
95f1e5ead1 | ||
|
|
6844a1a7b7 | ||
|
|
5a5e6ec659 | ||
|
|
9df8d94435 | ||
|
|
cb8d70403e | ||
|
|
1550d9f0db | ||
|
|
96782b34ee | ||
|
|
43240e1da3 | ||
|
|
1a05c2a90b | ||
|
|
89581ff15b | ||
|
|
db6ce0c4d0 | ||
|
|
e5346c76c5 | ||
|
|
8697a022c6 | ||
|
|
6b33720456 | ||
|
|
b7fc110807 | ||
|
|
40b09a2d8e | ||
|
|
77b62290da | ||
|
|
24a19219d4 | ||
|
|
f13a1aa07e | ||
|
|
bd685b9fc2 | ||
|
|
91d6529561 | ||
|
|
9e4c9e0bf9 | ||
|
|
7d7ae4640c | ||
|
|
579c58fc92 | ||
|
|
06a8e5caae | ||
|
|
b7701b858f | ||
|
|
a90cc37f5d | ||
|
|
d3d4acd541 | ||
|
|
73c50f384f | ||
|
|
42b3e6992d | ||
|
|
ba04181697 | ||
|
|
1a87c85077 | ||
|
|
e1ab79c468 | ||
|
|
cd59652e46 | ||
|
|
f348cf11d9 | ||
|
|
9645ff8977 | ||
|
|
998e16a691 | ||
|
|
4b80de48cb | ||
|
|
0fd1582c82 | ||
|
|
0a49f21158 | ||
|
|
ae66a4eae3 | ||
|
|
e5538da015 | ||
|
|
8f24a99efd | ||
|
|
1b55a7e27f | ||
|
|
72bbf936f8 | ||
|
|
d281d5e774 | ||
|
|
959a181ad8 | ||
|
|
986de97a50 | ||
|
|
3241ff4ae0 | ||
|
|
ace4f58c4b | ||
|
|
2a70fdcf27 | ||
|
|
f7f7b6a68c | ||
|
|
2039f159c0 | ||
|
|
49b12d90bc | ||
|
|
e05d7a9673 | ||
|
|
222835355a | ||
|
|
e01078800a | ||
|
|
853a62bab8 |
92
0001-Consolidate-duplicate-translation-keys.patch
Normal file
92
0001-Consolidate-duplicate-translation-keys.patch
Normal 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
1
OpenHands
Submodule
Submodule OpenHands added at efd0267919
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ describe("GitHubRepositorySelector", () => {
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByPlaceholderText("Select a GitHub project"),
|
||||
screen.getByPlaceholderText("LANDING$SELECT_REPO"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
190
frontend/__tests__/components/landing-translations.test.tsx
Normal file
190
frontend/__tests__/components/landing-translations.test.tsx
Normal 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(', ')}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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: {
|
||||
|
||||
@@ -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} />);
|
||||
|
||||
@@ -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(
|
||||
|
||||
76
frontend/__tests__/i18n/duplicate-keys.test.ts
Normal file
76
frontend/__tests__/i18n/duplicate-keys.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
61
frontend/__tests__/i18n/translations.test.tsx
Normal file
61
frontend/__tests__/i18n/translations.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
47
frontend/__tests__/utils/check-hardcoded-strings.test.tsx
Normal file
47
frontend/__tests__/utils/check-hardcoded-strings.test.tsx
Normal 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");
|
||||
});
|
||||
});
|
||||
29
frontend/__tests__/utils/i18n-test-utils.tsx
Normal file
29
frontend/__tests__/utils/i18n-test-utils.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
373
frontend/package-lock.json
generated
373
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
70
frontend/scripts/fix-duplicate-translations.py
Executable file
70
frontend/scripts/fix-duplicate-translations.py
Executable 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)
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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" }}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]"
|
||||
/>
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
66
frontend/src/components/shared/task-form.test.tsx
Normal file
66
frontend/src/components/shared/task-form.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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}
|
||||
|
||||
@@ -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
0
frontend/src/i18n/translation.json.new
Normal file
0
frontend/src/i18n/translation.json.new
Normal 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
|
||||
{t(I18nKey.LANDING$OR)}
|
||||
<a
|
||||
className="underline"
|
||||
href={`/conversations/${latestConversation}`}
|
||||
>
|
||||
jump back to your most recent conversation
|
||||
{t(I18nKey.LANDING$RECENT_CONVERSATION)}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
|
||||
@@ -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.`;
|
||||
|
||||
@@ -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.`;
|
||||
|
||||
@@ -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
28
package-lock.json
generated
Normal 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
5
package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"husky": "^9.1.7"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user