mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
parent
fafbe81d51
commit
6efb992bae
@ -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 () => {
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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 [];
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>}
|
||||
|
||||
@ -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]",
|
||||
}}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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",
|
||||
}
|
||||
|
||||
@ -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 для управління секретами"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -185,7 +185,7 @@ function GitSettingsScreen() {
|
||||
!isGitHubTokenSet && !isGitLabTokenSet && !isBitbucketTokenSet
|
||||
}
|
||||
>
|
||||
Disconnect Tokens
|
||||
{t(I18nKey.GIT$DISCONNECT_TOKENS)}
|
||||
</BrandButton>
|
||||
<BrandButton
|
||||
testId="submit-button"
|
||||
|
||||
@ -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} />
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
81
frontend/src/test/localization-fix.test.ts
Normal file
81
frontend/src/test/localization-fix.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user