From 6efb992baef241a9d2c97411a580683ec9f26b38 Mon Sep 17 00:00:00 2001 From: Graham Neubig Date: Wed, 25 Jun 2025 23:09:48 -0400 Subject: [PATCH] Fix incomplete localization issue #9282 (#9283) Co-authored-by: openhands --- .../features/home/repo-connector.test.tsx | 2 +- .../features/home/task-suggestions.test.tsx | 2 +- .../routes/secrets-settings.test.tsx | 4 +- .../scripts/check-unlocalized-strings.cjs | 11 +- .../features/home/repo-provider-links.tsx | 5 +- .../repository-selection/branch-dropdown.tsx | 6 +- .../repository-dropdown.tsx | 6 +- .../features/home/tasks/task-suggestions.tsx | 9 +- .../features/payment/payment-form.tsx | 2 +- .../bitbucket-token-help-anchor.tsx | 8 +- .../git-settings/github-token-help-anchor.tsx | 8 +- .../git-settings/gitlab-token-help-anchor.tsx | 8 +- .../settings/secrets-settings/secret-form.tsx | 4 +- .../shared/inputs/custom-model-input.tsx | 2 +- .../modals/security/invariant/invariant.tsx | 14 +- frontend/src/i18n/declaration.ts | 22 ++ frontend/src/i18n/translation.json | 352 ++++++++++++++++++ frontend/src/routes/app-settings.tsx | 2 +- frontend/src/routes/git-settings.tsx | 2 +- frontend/src/routes/llm-settings.tsx | 4 +- frontend/src/routes/secrets-settings.tsx | 3 +- frontend/src/test/localization-fix.test.ts | 81 ++++ 22 files changed, 517 insertions(+), 40 deletions(-) create mode 100644 frontend/src/test/localization-fix.test.ts diff --git a/frontend/__tests__/components/features/home/repo-connector.test.tsx b/frontend/__tests__/components/features/home/repo-connector.test.tsx index afbf218c79..3f7c2248e4 100644 --- a/frontend/__tests__/components/features/home/repo-connector.test.tsx +++ b/frontend/__tests__/components/features/home/repo-connector.test.tsx @@ -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 () => { diff --git a/frontend/__tests__/components/features/home/task-suggestions.test.tsx b/frontend/__tests__/components/features/home/task-suggestions.test.tsx index 4a3fa3a1f3..9a87da5ece 100644 --- a/frontend/__tests__/components/features/home/task-suggestions.test.tsx +++ b/frontend/__tests__/components/features/home/task-suggestions.test.tsx @@ -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 () => { diff --git a/frontend/__tests__/routes/secrets-settings.test.tsx b/frontend/__tests__/routes/secrets-settings.test.tsx index 824add9675..e2b922afad 100644 --- a/frontend/__tests__/routes/secrets-settings.test.tsx +++ b/frontend/__tests__/routes/secrets-settings.test.tsx @@ -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"); diff --git a/frontend/scripts/check-unlocalized-strings.cjs b/frontend/scripts/check-unlocalized-strings.cjs index 9115d5913d..1f0a0d0d08 100755 --- a/frontend/scripts/check-unlocalized-strings.cjs +++ b/frontend/scripts/check-unlocalized-strings.cjs @@ -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 []; } diff --git a/frontend/src/components/features/home/repo-provider-links.tsx b/frontend/src/components/features/home/repo-provider-links.tsx index e7e23b6a7a..5aa0cf3d49 100644 --- a/frontend/src/components/features/home/repo-provider-links.tsx +++ b/frontend/src/components/features/home/repo-provider-links.tsx @@ -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 (
- Add GitHub repos + {t(I18nKey.HOME$ADD_GITHUB_REPOS)}
); diff --git a/frontend/src/components/features/home/repository-selection/branch-dropdown.tsx b/frontend/src/components/features/home/repository-selection/branch-dropdown.tsx index 8f9221304b..587bd03f50 100644 --- a/frontend/src/components/features/home/repository-selection/branch-dropdown.tsx +++ b/frontend/src/components/features/home/repository-selection/branch-dropdown.tsx @@ -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 ( 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")} > -

Suggested Tasks

+

{t(I18nKey.TASKS$SUGGESTED_TASKS)}

{isLoading && } - {!hasSuggestedTasks && !isLoading &&

No tasks available

} + {!hasSuggestedTasks && !isLoading && ( +

{t(I18nKey.TASKS$NO_TASKS_AVAILABLE)}

+ )} {suggestedTasks?.map((taskGroup, index) => ( , , , 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 &&

{error}

} diff --git a/frontend/src/components/shared/inputs/custom-model-input.tsx b/frontend/src/components/shared/inputs/custom-model-input.tsx index b578325be9..e727bfea71 100644 --- a/frontend/src/components/shared/inputs/custom-model-input.tsx +++ b/frontend/src/components/shared/inputs/custom-model-input.tsx @@ -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]", }} diff --git a/frontend/src/components/shared/modals/security/invariant/invariant.tsx b/frontend/src/components/shared/modals/security/invariant/invariant.tsx index 9ac72184a7..debabfb431 100644 --- a/frontend/src/components/shared/modals/security/invariant/invariant.tsx +++ b/frontend/src/components/shared/modals/security/invariant/invariant.tsx @@ -198,46 +198,46 @@ function SecurityInvariant() { {t(I18nKey.INVARIANT$ASK_CONFIRMATION_RISK_SEVERITY_LABEL)}