Fix incomplete localization issue #9282 (#9283)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Graham Neubig 2025-06-25 23:09:48 -04:00 committed by GitHub
parent fafbe81d51
commit 6efb992bae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 517 additions and 40 deletions

View File

@ -128,7 +128,7 @@ describe("RepoConnector", () => {
renderRepoConnector();
await screen.findByText("Add GitHub repos");
await screen.findByText("HOME$ADD_GITHUB_REPOS");
});
it("should not render the 'add git(hub|lab) repos' links if oss mode", async () => {

View File

@ -53,7 +53,7 @@ describe("TaskSuggestions", () => {
it("should render an empty message if there are no tasks", async () => {
getSuggestedTasksSpy.mockResolvedValue([]);
renderTaskSuggestions();
await screen.findByText(/No tasks available/i);
await screen.findByText("TASKS$NO_TASKS_AVAILABLE");
});
it("should render the task groups with the correct titles", async () => {

View File

@ -473,7 +473,7 @@ describe("Secret actions", () => {
// make POST request
expect(createSecretSpy).not.toHaveBeenCalled();
expect(screen.queryByText(/secret already exists/i)).toBeInTheDocument();
expect(screen.queryByText("SECRETS$SECRET_ALREADY_EXISTS")).toBeInTheDocument();
await userEvent.clear(nameInput);
await userEvent.type(nameInput, "My_Custom_Secret");
@ -557,7 +557,7 @@ describe("Secret actions", () => {
// make POST request
expect(createSecretSpy).not.toHaveBeenCalled();
expect(screen.queryByText(/secret already exists/i)).toBeInTheDocument();
expect(screen.queryByText("SECRETS$SECRET_ALREADY_EXISTS")).toBeInTheDocument();
expect(nameInput).toHaveValue(MOCK_GET_SECRETS_RESPONSE[0].name);
expect(valueInput).toHaveValue("my-custom-secret-value");

View File

@ -56,8 +56,6 @@ const NON_TEXT_ATTRIBUTES = [
"type",
"href",
"src",
"alt",
"placeholder",
"rel",
"target",
"style",
@ -65,7 +63,6 @@ const NON_TEXT_ATTRIBUTES = [
"onChange",
"onSubmit",
"data-testid",
"aria-label",
"aria-labelledby",
"aria-describedby",
"aria-hidden",
@ -139,6 +136,7 @@ function isLikelyCode(str) {
}
function isCommonDevelopmentString(str) {
// Technical patterns that are definitely not UI strings
const technicalPatterns = [
// URLs and paths
@ -191,7 +189,7 @@ function isCommonDevelopmentString(str) {
// CSS units and values
const cssUnitsPattern =
/(px|rem|em|vh|vw|vmin|vmax|ch|ex|fr|deg|rad|turn|grad|ms|s)$/;
/\b\d+(px|rem|em|vh|vw|vmin|vmax|ch|ex|fr|deg|rad|turn|grad|ms|s)$|^(px|rem|em|vh|vw|vmin|vmax|ch|ex|fr|deg|rad|turn|grad|ms|s)$/;
const cssValuesPattern =
/(rgb|rgba|hsl|hsla|#[0-9a-fA-F]+|solid|absolute|relative|sticky|fixed|static|block|inline|flex|grid|none|auto|hidden|visible)/;
@ -394,6 +392,7 @@ function isCommonDevelopmentString(str) {
}
function isLikelyUserFacingText(str) {
// Basic validation - skip very short strings or strings without letters
if (!str || str.length <= 2 || !/[a-zA-Z]/.test(str)) {
return false;
@ -540,8 +539,8 @@ function isInTranslationContext(path) {
}
function scanFileForUnlocalizedStrings(filePath) {
// Skip all suggestion files as they contain special strings
if (filePath.includes("suggestions")) {
// Skip suggestion content files as they contain special strings that are already properly localized
if (filePath.includes("utils/suggestions/") || filePath.includes("mocks/task-suggestions-handlers.ts")) {
return [];
}

View File

@ -1,6 +1,9 @@
import { useTranslation } from "react-i18next";
import { useConfig } from "#/hooks/query/use-config";
import { I18nKey } from "#/i18n/declaration";
export function RepoProviderLinks() {
const { t } = useTranslation();
const { data: config } = useConfig();
const githubHref = config
@ -10,7 +13,7 @@ export function RepoProviderLinks() {
return (
<div className="flex flex-col text-sm underline underline-offset-2 text-content-2 gap-4 w-fit">
<a href={githubHref} target="_blank" rel="noopener noreferrer">
Add GitHub repos
{t(I18nKey.HOME$ADD_GITHUB_REPOS)}
</a>
</div>
);

View File

@ -1,5 +1,7 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { SettingsDropdownInput } from "../../settings/settings-dropdown-input";
import { I18nKey } from "#/i18n/declaration";
export interface BranchDropdownProps {
items: { key: React.Key; label: string }[];
@ -16,11 +18,13 @@ export function BranchDropdown({
isDisabled,
selectedKey,
}: BranchDropdownProps) {
const { t } = useTranslation();
return (
<SettingsDropdownInput
testId="branch-dropdown"
name="branch-dropdown"
placeholder="Select a branch"
placeholder={t(I18nKey.REPOSITORY$SELECT_BRANCH)}
items={items}
wrapperClassName="max-w-[500px]"
onSelectionChange={onSelectionChange}

View File

@ -1,5 +1,7 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { SettingsDropdownInput } from "../../settings/settings-dropdown-input";
import { I18nKey } from "#/i18n/declaration";
export interface RepositoryDropdownProps {
items: { key: React.Key; label: string }[];
@ -14,11 +16,13 @@ export function RepositoryDropdown({
onInputChange,
defaultFilter,
}: RepositoryDropdownProps) {
const { t } = useTranslation();
return (
<SettingsDropdownInput
testId="repo-dropdown"
name="repo-dropdown"
placeholder="Select a repo"
placeholder={t(I18nKey.REPOSITORY$SELECT_REPO)}
items={items}
wrapperClassName="max-w-[500px]"
onSelectionChange={onSelectionChange}

View File

@ -1,13 +1,16 @@
import { useTranslation } from "react-i18next";
import { TaskGroup } from "./task-group";
import { useSuggestedTasks } from "#/hooks/query/use-suggested-tasks";
import { TaskSuggestionsSkeleton } from "./task-suggestions-skeleton";
import { cn } from "#/utils/utils";
import { I18nKey } from "#/i18n/declaration";
interface TaskSuggestionsProps {
filterFor?: string | null;
}
export function TaskSuggestions({ filterFor }: TaskSuggestionsProps) {
const { t } = useTranslation();
const { data: tasks, isLoading } = useSuggestedTasks();
const suggestedTasks = filterFor
? tasks?.filter((task) => task.title === filterFor)
@ -20,11 +23,13 @@ export function TaskSuggestions({ filterFor }: TaskSuggestionsProps) {
data-testid="task-suggestions"
className={cn("flex flex-col w-full", !hasSuggestedTasks && "gap-6")}
>
<h2 className="heading">Suggested Tasks</h2>
<h2 className="heading">{t(I18nKey.TASKS$SUGGESTED_TASKS)}</h2>
<div className="flex flex-col gap-6">
{isLoading && <TaskSuggestionsSkeleton />}
{!hasSuggestedTasks && !isLoading && <p>No tasks available</p>}
{!hasSuggestedTasks && !isLoading && (
<p>{t(I18nKey.TASKS$NO_TASKS_AVAILABLE)}</p>
)}
{suggestedTasks?.map((taskGroup, index) => (
<TaskGroup
key={index}

View File

@ -64,7 +64,7 @@ export function PaymentForm() {
onChange={handleTopUpInputChange}
type="number"
label={t(I18nKey.PAYMENT$ADD_FUNDS)}
placeholder="Specify an amount in USD to add - min $10"
placeholder={t(I18nKey.PAYMENT$SPECIFY_AMOUNT_USD)}
className="w-[680px]"
min={10}
max={25000}

View File

@ -1,7 +1,9 @@
import { Trans } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
export function BitbucketTokenHelpAnchor() {
const { t } = useTranslation();
return (
<p data-testid="bitbucket-token-help-anchor" className="text-xs">
<Trans
@ -9,7 +11,7 @@ export function BitbucketTokenHelpAnchor() {
components={[
<a
key="bitbucket-token-help-anchor-link"
aria-label="Bitbucket token help link"
aria-label={t(I18nKey.GIT$BITBUCKET_TOKEN_HELP_LINK)}
href="https://bitbucket.org/account/settings/app-passwords/new?scopes=repository:write,pullrequest:write,issue:write"
target="_blank"
className="underline underline-offset-2"
@ -17,7 +19,7 @@ export function BitbucketTokenHelpAnchor() {
/>,
<a
key="bitbucket-token-help-anchor-link-2"
aria-label="Bitbucket token see more link"
aria-label={t(I18nKey.GIT$BITBUCKET_TOKEN_SEE_MORE_LINK)}
href="https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/"
target="_blank"
className="underline underline-offset-2"

View File

@ -1,7 +1,9 @@
import { Trans } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
export function GitHubTokenHelpAnchor() {
const { t } = useTranslation();
return (
<p data-testid="github-token-help-anchor" className="text-xs">
<Trans
@ -9,7 +11,7 @@ export function GitHubTokenHelpAnchor() {
components={[
<a
key="github-token-help-anchor-link"
aria-label="GitHub token help link"
aria-label={t(I18nKey.GIT$GITHUB_TOKEN_HELP_LINK)}
href="https://github.com/settings/tokens/new?description=openhands-app&scopes=repo,user,workflow"
target="_blank"
className="underline underline-offset-2"
@ -17,7 +19,7 @@ export function GitHubTokenHelpAnchor() {
/>,
<a
key="github-token-help-anchor-link-2"
aria-label="GitHub token see more link"
aria-label={t(I18nKey.GIT$GITHUB_TOKEN_SEE_MORE_LINK)}
href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token"
target="_blank"
className="underline underline-offset-2"

View File

@ -1,7 +1,9 @@
import { Trans } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
export function GitLabTokenHelpAnchor() {
const { t } = useTranslation();
return (
<p data-testid="gitlab-token-help-anchor" className="text-xs">
<Trans
@ -9,7 +11,7 @@ export function GitLabTokenHelpAnchor() {
components={[
<a
key="gitlab-token-help-anchor-link"
aria-label="Gitlab token help link"
aria-label={t(I18nKey.GIT$GITLAB_TOKEN_HELP_LINK)}
href="https://gitlab.com/-/user_settings/personal_access_tokens?name=openhands-app&scopes=api,read_user,read_repository,write_repository"
target="_blank"
className="underline underline-offset-2"
@ -17,7 +19,7 @@ export function GitLabTokenHelpAnchor() {
/>,
<a
key="gitlab-token-help-anchor-link-2"
aria-label="GitLab token see more link"
aria-label={t(I18nKey.GIT$GITLAB_TOKEN_SEE_MORE_LINK)}
href="https://docs.gitlab.com/user/profile/personal_access_tokens/"
target="_blank"
className="underline underline-offset-2"

View File

@ -111,7 +111,7 @@ export function SecretForm({
(secret) => secret.name === name && secret.name !== selectedSecret,
);
if (isNameAlreadyUsed) {
setError("Secret already exists");
setError(t("SECRETS$SECRET_ALREADY_EXISTS"));
return;
}
@ -144,7 +144,7 @@ export function SecretForm({
className="w-full max-w-[350px]"
required
defaultValue={mode === "edit" && selectedSecret ? selectedSecret : ""}
placeholder="e.g. OpenAI_API_Key"
placeholder={t("SECRETS$API_KEY_EXAMPLE")}
pattern="^\S*$"
/>
{error && <p className="text-red-500 text-sm">{error}</p>}

View File

@ -28,7 +28,7 @@ export function CustomModelInput({
id="custom-model"
name="custom-model"
defaultValue={defaultValue}
aria-label="Custom Model"
aria-label={t(I18nKey.MODEL$CUSTOM_MODEL)}
classNames={{
inputWrapper: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]",
}}

View File

@ -198,46 +198,46 @@ function SecurityInvariant() {
{t(I18nKey.INVARIANT$ASK_CONFIRMATION_RISK_SEVERITY_LABEL)}
</p>
<Select
placeholder="Select risk severity"
placeholder={t(I18nKey.SECURITY$SELECT_RISK_SEVERITY)}
value={selectedRisk}
onChange={(e) =>
setSelectedRisk(Number(e.target.value) as ActionSecurityRisk)
}
className={getRiskColor(selectedRisk)}
selectedKeys={new Set([selectedRisk.toString()])}
aria-label="Select risk severity"
aria-label={t(I18nKey.SECURITY$SELECT_RISK_SEVERITY)}
>
<SelectItem
key={ActionSecurityRisk.UNKNOWN}
aria-label="Unknown Risk"
aria-label={t(I18nKey.SECURITY$UNKNOWN_RISK)}
className={getRiskColor(ActionSecurityRisk.UNKNOWN)}
>
{getRiskText(ActionSecurityRisk.UNKNOWN)}
</SelectItem>
<SelectItem
key={ActionSecurityRisk.LOW}
aria-label="Low Risk"
aria-label={t(I18nKey.SECURITY$LOW_RISK)}
className={getRiskColor(ActionSecurityRisk.LOW)}
>
{getRiskText(ActionSecurityRisk.LOW)}
</SelectItem>
<SelectItem
key={ActionSecurityRisk.MEDIUM}
aria-label="Medium Risk"
aria-label={t(I18nKey.SECURITY$MEDIUM_RISK)}
className={getRiskColor(ActionSecurityRisk.MEDIUM)}
>
{getRiskText(ActionSecurityRisk.MEDIUM)}
</SelectItem>
<SelectItem
key={ActionSecurityRisk.HIGH}
aria-label="High Risk"
aria-label={t(I18nKey.SECURITY$HIGH_RISK)}
className={getRiskColor(ActionSecurityRisk.HIGH)}
>
{getRiskText(ActionSecurityRisk.HIGH)}
</SelectItem>
<SelectItem
key={ActionSecurityRisk.HIGH + 1}
aria-label="Don't ask for confirmation"
aria-label={t(I18nKey.SECURITY$DONT_ASK_CONFIRMATION)}
>
{t(I18nKey.INVARIANT$DONT_ASK_FOR_CONFIRMATION_LABEL)}
</SelectItem>

View File

@ -606,4 +606,26 @@ export enum I18nKey {
FEEDBACK$REASON_OTHER = "FEEDBACK$REASON_OTHER",
FEEDBACK$THANK_YOU_FOR_FEEDBACK = "FEEDBACK$THANK_YOU_FOR_FEEDBACK",
FEEDBACK$FAILED_TO_SUBMIT = "FEEDBACK$FAILED_TO_SUBMIT",
HOME$ADD_GITHUB_REPOS = "HOME$ADD_GITHUB_REPOS",
REPOSITORY$SELECT_BRANCH = "REPOSITORY$SELECT_BRANCH",
REPOSITORY$SELECT_REPO = "REPOSITORY$SELECT_REPO",
TASKS$SUGGESTED_TASKS = "TASKS$SUGGESTED_TASKS",
TASKS$NO_TASKS_AVAILABLE = "TASKS$NO_TASKS_AVAILABLE",
PAYMENT$SPECIFY_AMOUNT_USD = "PAYMENT$SPECIFY_AMOUNT_USD",
GIT$BITBUCKET_TOKEN_HELP_LINK = "GIT$BITBUCKET_TOKEN_HELP_LINK",
GIT$BITBUCKET_TOKEN_SEE_MORE_LINK = "GIT$BITBUCKET_TOKEN_SEE_MORE_LINK",
GIT$GITHUB_TOKEN_HELP_LINK = "GIT$GITHUB_TOKEN_HELP_LINK",
GIT$GITHUB_TOKEN_SEE_MORE_LINK = "GIT$GITHUB_TOKEN_SEE_MORE_LINK",
GIT$GITLAB_TOKEN_HELP_LINK = "GIT$GITLAB_TOKEN_HELP_LINK",
GIT$GITLAB_TOKEN_SEE_MORE_LINK = "GIT$GITLAB_TOKEN_SEE_MORE_LINK",
SECRETS$SECRET_ALREADY_EXISTS = "SECRETS$SECRET_ALREADY_EXISTS",
SECRETS$API_KEY_EXAMPLE = "SECRETS$API_KEY_EXAMPLE",
MODEL$CUSTOM_MODEL = "MODEL$CUSTOM_MODEL",
SECURITY$SELECT_RISK_SEVERITY = "SECURITY$SELECT_RISK_SEVERITY",
SECURITY$DONT_ASK_CONFIRMATION = "SECURITY$DONT_ASK_CONFIRMATION",
SETTINGS$MAXIMUM_BUDGET_USD = "SETTINGS$MAXIMUM_BUDGET_USD",
GIT$DISCONNECT_TOKENS = "GIT$DISCONNECT_TOKENS",
API$TAVILY_KEY_EXAMPLE = "API$TAVILY_KEY_EXAMPLE",
API$TVLY_KEY_EXAMPLE = "API$TVLY_KEY_EXAMPLE",
SECRETS$CONNECT_GIT_PROVIDER = "SECRETS$CONNECT_GIT_PROVIDER",
}

View File

@ -9694,5 +9694,357 @@
"tr": "Geri bildirim gönderilemedi",
"de": "Feedback konnte nicht gesendet werden",
"uk": "Не вдалося надіслати відгук"
},
"HOME$ADD_GITHUB_REPOS": {
"en": "Add GitHub repos",
"ja": "GitHubリポジトリを追加",
"zh-CN": "添加GitHub仓库",
"zh-TW": "新增GitHub儲存庫",
"ko-KR": "GitHub 저장소 추가",
"no": "Legg til GitHub-repositorier",
"it": "Aggiungi repository GitHub",
"pt": "Adicionar repositórios GitHub",
"es": "Agregar repositorios de GitHub",
"ar": "إضافة مستودعات GitHub",
"fr": "Ajouter des dépôts GitHub",
"tr": "GitHub depoları ekle",
"de": "GitHub-Repositories hinzufügen",
"uk": "Додати репозиторії GitHub"
},
"REPOSITORY$SELECT_BRANCH": {
"en": "Select a branch",
"ja": "ブランチを選択",
"zh-CN": "选择分支",
"zh-TW": "選擇分支",
"ko-KR": "브랜치 선택",
"no": "Velg en gren",
"it": "Seleziona un ramo",
"pt": "Selecionar um branch",
"es": "Seleccionar una rama",
"ar": "اختر فرع",
"fr": "Sélectionner une branche",
"tr": "Bir dal seç",
"de": "Einen Branch auswählen",
"uk": "Вибрати гілку"
},
"REPOSITORY$SELECT_REPO": {
"en": "Select a repo",
"ja": "リポジトリを選択",
"zh-CN": "选择仓库",
"zh-TW": "選擇儲存庫",
"ko-KR": "저장소 선택",
"no": "Velg et repositorium",
"it": "Seleziona un repository",
"pt": "Selecionar um repositório",
"es": "Seleccionar un repositorio",
"ar": "اختر مستودع",
"fr": "Sélectionner un dépôt",
"tr": "Bir depo seç",
"de": "Ein Repository auswählen",
"uk": "Вибрати репозиторій"
},
"TASKS$SUGGESTED_TASKS": {
"en": "Suggested Tasks",
"ja": "推奨タスク",
"zh-CN": "建议任务",
"zh-TW": "建議任務",
"ko-KR": "추천 작업",
"no": "Foreslåtte oppgaver",
"it": "Attività suggerite",
"pt": "Tarefas sugeridas",
"es": "Tareas sugeridas",
"ar": "المهام المقترحة",
"fr": "Tâches suggérées",
"tr": "Önerilen görevler",
"de": "Vorgeschlagene Aufgaben",
"uk": "Запропоновані завдання"
},
"TASKS$NO_TASKS_AVAILABLE": {
"en": "No tasks available",
"ja": "利用可能なタスクがありません",
"zh-CN": "没有可用任务",
"zh-TW": "沒有可用任務",
"ko-KR": "사용 가능한 작업이 없습니다",
"no": "Ingen oppgaver tilgjengelig",
"it": "Nessuna attività disponibile",
"pt": "Nenhuma tarefa disponível",
"es": "No hay tareas disponibles",
"ar": "لا توجد مهام متاحة",
"fr": "Aucune tâche disponible",
"tr": "Mevcut görev yok",
"de": "Keine Aufgaben verfügbar",
"uk": "Немає доступних завдань"
},
"PAYMENT$SPECIFY_AMOUNT_USD": {
"en": "Specify an amount in USD to add - min $10",
"ja": "追加するUSD金額を指定してください - 最小$10",
"zh-CN": "指定要添加的美元金额 - 最少$10",
"zh-TW": "指定要新增的美元金額 - 最少$10",
"ko-KR": "추가할 USD 금액을 지정하세요 - 최소 $10",
"no": "Spesifiser et beløp i USD å legge til - min $10",
"it": "Specifica un importo in USD da aggiungere - min $10",
"pt": "Especifique um valor em USD para adicionar - mín $10",
"es": "Especifique una cantidad en USD para agregar - mín $10",
"ar": "حدد مبلغًا بالدولار الأمريكي لإضافته - الحد الأدنى 10 دولارات",
"fr": "Spécifiez un montant en USD à ajouter - min 10 $",
"tr": "Eklenecek USD tutarını belirtin - min $10",
"de": "Geben Sie einen USD-Betrag zum Hinzufügen an - min $10",
"uk": "Вкажіть суму в доларах США для додавання - мін $10"
},
"GIT$BITBUCKET_TOKEN_HELP_LINK": {
"en": "Bitbucket token help link",
"ja": "Bitbucketトークンヘルプリンク",
"zh-CN": "Bitbucket令牌帮助链接",
"zh-TW": "Bitbucket令牌幫助連結",
"ko-KR": "Bitbucket 토큰 도움말 링크",
"no": "Bitbucket token hjelpelenke",
"it": "Link di aiuto per il token Bitbucket",
"pt": "Link de ajuda do token Bitbucket",
"es": "Enlace de ayuda del token de Bitbucket",
"ar": "رابط مساعدة رمز Bitbucket",
"fr": "Lien d'aide pour le jeton Bitbucket",
"tr": "Bitbucket token yardım bağlantısı",
"de": "Bitbucket-Token-Hilfe-Link",
"uk": "Посилання на довідку токена Bitbucket"
},
"GIT$BITBUCKET_TOKEN_SEE_MORE_LINK": {
"en": "Bitbucket token see more link",
"ja": "Bitbucketトークン詳細リンク",
"zh-CN": "Bitbucket令牌查看更多链接",
"zh-TW": "Bitbucket令牌查看更多連結",
"ko-KR": "Bitbucket 토큰 더 보기 링크",
"no": "Bitbucket token se mer lenke",
"it": "Link per vedere di più sul token Bitbucket",
"pt": "Link para ver mais sobre o token Bitbucket",
"es": "Enlace para ver más del token de Bitbucket",
"ar": "رابط لرؤية المزيد حول رمز Bitbucket",
"fr": "Lien pour en voir plus sur le jeton Bitbucket",
"tr": "Bitbucket token daha fazla görme bağlantısı",
"de": "Bitbucket-Token mehr sehen Link",
"uk": "Посилання для перегляду більше про токен Bitbucket"
},
"GIT$GITHUB_TOKEN_HELP_LINK": {
"en": "GitHub token help link",
"ja": "GitHubトークンヘルプリンク",
"zh-CN": "GitHub令牌帮助链接",
"zh-TW": "GitHub令牌幫助連結",
"ko-KR": "GitHub 토큰 도움말 링크",
"no": "GitHub token hjelpelenke",
"it": "Link di aiuto per il token GitHub",
"pt": "Link de ajuda do token GitHub",
"es": "Enlace de ayuda del token de GitHub",
"ar": "رابط مساعدة رمز GitHub",
"fr": "Lien d'aide pour le jeton GitHub",
"tr": "GitHub token yardım bağlantısı",
"de": "GitHub-Token-Hilfe-Link",
"uk": "Посилання на довідку токена GitHub"
},
"GIT$GITHUB_TOKEN_SEE_MORE_LINK": {
"en": "GitHub token see more link",
"ja": "GitHubトークン詳細リンク",
"zh-CN": "GitHub令牌查看更多链接",
"zh-TW": "GitHub令牌查看更多連結",
"ko-KR": "GitHub 토큰 더 보기 링크",
"no": "GitHub token se mer lenke",
"it": "Link per vedere di più sul token GitHub",
"pt": "Link para ver mais sobre o token GitHub",
"es": "Enlace para ver más del token de GitHub",
"ar": "رابط لرؤية المزيد حول رمز GitHub",
"fr": "Lien pour en voir plus sur le jeton GitHub",
"tr": "GitHub token daha fazla görme bağlantısı",
"de": "GitHub-Token mehr sehen Link",
"uk": "Посилання для перегляду більше про токен GitHub"
},
"GIT$GITLAB_TOKEN_HELP_LINK": {
"en": "Gitlab token help link",
"ja": "GitLabトークンヘルプリンク",
"zh-CN": "GitLab令牌帮助链接",
"zh-TW": "GitLab令牌幫助連結",
"ko-KR": "GitLab 토큰 도움말 링크",
"no": "GitLab token hjelpelenke",
"it": "Link di aiuto per il token GitLab",
"pt": "Link de ajuda do token GitLab",
"es": "Enlace de ayuda del token de GitLab",
"ar": "رابط مساعدة رمز GitLab",
"fr": "Lien d'aide pour le jeton GitLab",
"tr": "GitLab token yardım bağlantısı",
"de": "GitLab-Token-Hilfe-Link",
"uk": "Посилання на довідку токена GitLab"
},
"GIT$GITLAB_TOKEN_SEE_MORE_LINK": {
"en": "GitLab token see more link",
"ja": "GitLabトークン詳細リンク",
"zh-CN": "GitLab令牌查看更多链接",
"zh-TW": "GitLab令牌查看更多連結",
"ko-KR": "GitLab 토큰 더 보기 링크",
"no": "GitLab token se mer lenke",
"it": "Link per vedere di più sul token GitLab",
"pt": "Link para ver mais sobre o token GitLab",
"es": "Enlace para ver más del token de GitLab",
"ar": "رابط لرؤية المزيد حول رمز GitLab",
"fr": "Lien pour en voir plus sur le jeton GitLab",
"tr": "GitLab token daha fazla görme bağlantısı",
"de": "GitLab-Token mehr sehen Link",
"uk": "Посилання для перегляду більше про токен GitLab"
},
"SECRETS$SECRET_ALREADY_EXISTS": {
"en": "Secret already exists",
"ja": "シークレットは既に存在します",
"zh-CN": "密钥已存在",
"zh-TW": "密鑰已存在",
"ko-KR": "시크릿이 이미 존재합니다",
"no": "Hemmelighet eksisterer allerede",
"it": "Il segreto esiste già",
"pt": "Segredo já existe",
"es": "El secreto ya existe",
"ar": "السر موجود بالفعل",
"fr": "Le secret existe déjà",
"tr": "Gizli anahtar zaten mevcut",
"de": "Geheimnis existiert bereits",
"uk": "Секрет вже існує"
},
"SECRETS$API_KEY_EXAMPLE": {
"en": "e.g. OpenAI_API_Key",
"ja": "例: OpenAI_API_Key",
"zh-CN": "例如 OpenAI_API_Key",
"zh-TW": "例如 OpenAI_API_Key",
"ko-KR": "예: OpenAI_API_Key",
"no": "f.eks. OpenAI_API_Key",
"it": "es. OpenAI_API_Key",
"pt": "ex. OpenAI_API_Key",
"es": "ej. OpenAI_API_Key",
"ar": "مثل OpenAI_API_Key",
"fr": "ex. OpenAI_API_Key",
"tr": "örn. OpenAI_API_Key",
"de": "z.B. OpenAI_API_Key",
"uk": "наприклад OpenAI_API_Key"
},
"MODEL$CUSTOM_MODEL": {
"en": "Custom Model",
"ja": "カスタムモデル",
"zh-CN": "自定义模型",
"zh-TW": "自訂模型",
"ko-KR": "사용자 정의 모델",
"no": "Tilpasset modell",
"it": "Modello personalizzato",
"pt": "Modelo personalizado",
"es": "Modelo personalizado",
"ar": "نموذج مخصص",
"fr": "Modèle personnalisé",
"tr": "Özel model",
"de": "Benutzerdefiniertes Modell",
"uk": "Користувацька модель"
},
"SECURITY$SELECT_RISK_SEVERITY": {
"en": "Select risk severity",
"ja": "リスクの重要度を選択",
"zh-CN": "选择风险严重程度",
"zh-TW": "選擇風險嚴重程度",
"ko-KR": "위험 심각도 선택",
"no": "Velg risikoalvorlighet",
"it": "Seleziona gravità del rischio",
"pt": "Selecionar gravidade do risco",
"es": "Seleccionar gravedad del riesgo",
"ar": "اختر شدة المخاطر",
"fr": "Sélectionner la gravité du risque",
"tr": "Risk ciddiyetini seç",
"de": "Risikoschweregrad auswählen",
"uk": "Вибрати ступінь ризику"
},
"SECURITY$DONT_ASK_CONFIRMATION": {
"en": "Don't ask for confirmation",
"ja": "確認を求めない",
"zh-CN": "不要求确认",
"zh-TW": "不要求確認",
"ko-KR": "확인을 요청하지 않음",
"no": "Ikke spør om bekreftelse",
"it": "Non chiedere conferma",
"pt": "Não pedir confirmação",
"es": "No pedir confirmación",
"ar": "لا تطلب التأكيد",
"fr": "Ne pas demander de confirmation",
"tr": "Onay isteme",
"de": "Nicht nach Bestätigung fragen",
"uk": "Не запитувати підтвердження"
},
"SETTINGS$MAXIMUM_BUDGET_USD": {
"en": "Maximum budget per conversation in USD",
"ja": "会話あたりの最大予算USD",
"zh-CN": "每次对话的最大预算(美元)",
"zh-TW": "每次對話的最大預算(美元)",
"ko-KR": "대화당 최대 예산(USD)",
"no": "Maksimalt budsjett per samtale i USD",
"it": "Budget massimo per conversazione in USD",
"pt": "Orçamento máximo por conversa em USD",
"es": "Presupuesto máximo por conversación en USD",
"ar": "الحد الأقصى للميزانية لكل محادثة بالدولار الأمريكي",
"fr": "Budget maximum par conversation en USD",
"tr": "Konuşma başına maksimum bütçe (USD)",
"de": "Maximales Budget pro Gespräch in USD",
"uk": "Максимальний бюджет на розмову в доларах США"
},
"GIT$DISCONNECT_TOKENS": {
"en": "Disconnect Tokens",
"ja": "トークンを切断",
"zh-CN": "断开令牌连接",
"zh-TW": "中斷令牌連接",
"ko-KR": "토큰 연결 해제",
"no": "Koble fra tokens",
"it": "Disconnetti token",
"pt": "Desconectar tokens",
"es": "Desconectar tokens",
"ar": "قطع اتصال الرموز",
"fr": "Déconnecter les jetons",
"tr": "Token bağlantısını kes",
"de": "Token trennen",
"uk": "Відключити токени"
},
"API$TAVILY_KEY_EXAMPLE": {
"en": "sk-tavily-...",
"ja": "sk-tavily-...",
"zh-CN": "sk-tavily-...",
"zh-TW": "sk-tavily-...",
"ko-KR": "sk-tavily-...",
"no": "sk-tavily-...",
"it": "sk-tavily-...",
"pt": "sk-tavily-...",
"es": "sk-tavily-...",
"ar": "sk-tavily-...",
"fr": "sk-tavily-...",
"tr": "sk-tavily-...",
"de": "sk-tavily-...",
"uk": "sk-tavily-..."
},
"API$TVLY_KEY_EXAMPLE": {
"en": "tvly-...",
"ja": "tvly-...",
"zh-CN": "tvly-...",
"zh-TW": "tvly-...",
"ko-KR": "tvly-...",
"no": "tvly-...",
"it": "tvly-...",
"pt": "tvly-...",
"es": "tvly-...",
"ar": "tvly-...",
"fr": "tvly-...",
"tr": "tvly-...",
"de": "tvly-...",
"uk": "tvly-..."
},
"SECRETS$CONNECT_GIT_PROVIDER": {
"en": "Connect a Git provider to manage secrets",
"ja": "シークレットを管理するためにGitプロバイダーに接続",
"zh-CN": "连接Git提供商以管理密钥",
"zh-TW": "連接Git提供商以管理密鑰",
"ko-KR": "시크릿 관리를 위해 Git 제공자에 연결",
"no": "Koble til en Git-leverandør for å administrere hemmeligheter",
"it": "Connetti un provider Git per gestire i segreti",
"pt": "Conectar um provedor Git para gerenciar segredos",
"es": "Conectar un proveedor Git para gestionar secretos",
"ar": "اتصل بمزود Git لإدارة الأسرار",
"fr": "Connecter un fournisseur Git pour gérer les secrets",
"tr": "Gizli anahtarları yönetmek için bir Git sağlayıcısına bağlan",
"de": "Git-Anbieter verbinden, um Geheimnisse zu verwalten",
"uk": "Підключити провайдера Git для управління секретами"
}
}

View File

@ -189,7 +189,7 @@ function AppSettingsScreen() {
label={t(I18nKey.SETTINGS$MAX_BUDGET_PER_CONVERSATION)}
defaultValue={settings.MAX_BUDGET_PER_TASK?.toString() || ""}
onChange={checkIfMaxBudgetPerTaskHasChanged}
placeholder="Maximum budget per conversation in USD"
placeholder={t(I18nKey.SETTINGS$MAXIMUM_BUDGET_USD)}
min={1}
step={1}
className="w-[680px]" // Match the width of the language field

View File

@ -185,7 +185,7 @@ function GitSettingsScreen() {
!isGitHubTokenSet && !isGitLabTokenSet && !isBitbucketTokenSet
}
>
Disconnect Tokens
{t(I18nKey.GIT$DISCONNECT_TOKENS)}
</BrandButton>
<BrandButton
testId="submit-button"

View File

@ -318,7 +318,7 @@ function LlmSettingsScreen() {
className="w-full max-w-[680px]"
defaultValue={settings.SEARCH_API_KEY || ""}
onChange={handleSearchApiKeyIsDirty}
placeholder="sk-tavily-..."
placeholder={t(I18nKey.API$TAVILY_KEY_EXAMPLE)}
startContent={
settings.SEARCH_API_KEY_SET && (
<KeyStatusIcon isSet={settings.SEARCH_API_KEY_SET} />
@ -393,7 +393,7 @@ function LlmSettingsScreen() {
className="w-full max-w-[680px]"
defaultValue={settings.SEARCH_API_KEY || ""}
onChange={handleSearchApiKeyIsDirty}
placeholder="tvly-..."
placeholder={t(I18nKey.API$TVLY_KEY_EXAMPLE)}
startContent={
settings.SEARCH_API_KEY_SET && (
<KeyStatusIcon isSet={settings.SEARCH_API_KEY_SET} />

View File

@ -13,6 +13,7 @@ import { BrandButton } from "#/components/features/settings/brand-button";
import { ConfirmationModal } from "#/components/shared/modals/confirmation-modal";
import { GetSecretsResponse } from "#/api/secrets-service.types";
import { useUserProviders } from "#/hooks/use-user-providers";
import { I18nKey } from "#/i18n/declaration";
import { useConfig } from "#/hooks/query/use-config";
function SecretsSettingsScreen() {
@ -90,7 +91,7 @@ function SecretsSettingsScreen() {
type="button"
>
<BrandButton type="button" variant="secondary">
Connect a Git provider to manage secrets
{t(I18nKey.SECRETS$CONNECT_GIT_PROVIDER)}
</BrandButton>
</Link>
)}

View File

@ -0,0 +1,81 @@
import { describe, it, expect } from "vitest";
import { execSync } from "child_process";
import path from "path";
import fs from "fs";
describe("Localization Fix Tests", () => {
it("should not find any unlocalized strings in the frontend code", () => {
const scriptPath = path.join(
__dirname,
"../../scripts/check-unlocalized-strings.cjs",
);
// Run the localization check script
const result = execSync(`node ${scriptPath}`, {
cwd: path.join(__dirname, "../.."),
encoding: "utf8",
});
// The script should output success message and exit with code 0
expect(result).toContain(
"✅ No unlocalized strings found in frontend code.",
);
});
it("should properly detect user-facing attributes like placeholder, alt, and aria-label", () => {
// This test verifies that our fix to include placeholder, alt, and aria-label
// attributes in the localization check is working correctly by testing the regex patterns
const scriptPath = path.join(
__dirname,
"../../scripts/check-unlocalized-strings.cjs",
);
const scriptContent = fs.readFileSync(scriptPath, "utf8");
// Verify that these attributes are now being checked for localization
// by ensuring they're not excluded from text extraction
const nonTextAttributesMatch = scriptContent.match(
/const NON_TEXT_ATTRIBUTES = \[(.*?)\]/s,
);
expect(nonTextAttributesMatch).toBeTruthy();
const nonTextAttributes = nonTextAttributesMatch![1];
expect(nonTextAttributes).not.toContain('"placeholder"');
expect(nonTextAttributes).not.toContain('"alt"');
expect(nonTextAttributes).not.toContain('"aria-label"');
// Verify that the script contains the correct attributes that should be excluded
expect(nonTextAttributes).toContain('"className"');
expect(nonTextAttributes).toContain('"testId"');
expect(nonTextAttributes).toContain('"href"');
});
it("should not incorrectly flag CSS units as unlocalized strings", () => {
// This test verifies that our fix to the CSS units regex pattern
// prevents false positives like "Suggested Tasks" being flagged
const testStrings = [
"Suggested Tasks",
"No tasks available",
"Select a branch",
"Select a repo",
"Custom Models",
"API Keys",
"Git Settings",
];
// These strings should not be flagged as CSS units
const cssUnitsPattern =
/\b\d+(px|rem|em|vh|vw|vmin|vmax|ch|ex|fr|deg|rad|turn|grad|ms|s)$|^(px|rem|em|vh|vw|vmin|vmax|ch|ex|fr|deg|rad|turn|grad|ms|s)$/;
testStrings.forEach((str) => {
expect(cssUnitsPattern.test(str)).toBe(false);
});
// But actual CSS units should still be detected
const actualCssUnits = ["10px", "2rem", "100vh", "px", "rem", "s"];
actualCssUnits.forEach((unit) => {
expect(cssUnitsPattern.test(unit)).toBe(true);
});
});
});