diff --git a/webview-ui/src/components/package-manager/PackageManagerView.tsx b/webview-ui/src/components/package-manager/PackageManagerView.tsx index 3001b00687..c865406313 100644 --- a/webview-ui/src/components/package-manager/PackageManagerView.tsx +++ b/webview-ui/src/components/package-manager/PackageManagerView.tsx @@ -6,12 +6,13 @@ import { PackageManagerSource } from "../../../../src/services/package-manager/t import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "cmdk" import { PackageManagerItemCard } from "./components/PackageManagerItemCard" import { useStateManager } from "./useStateManager" +import { useAppTranslation } from "@/i18n/TranslationContext" interface PackageManagerViewProps { onDone?: () => void } - const PackageManagerView: React.FC = ({ onDone }) => { + const { t } = useAppTranslation() const [state, manager] = useStateManager() const [tagSearch, setTagSearch] = useState("") @@ -31,7 +32,7 @@ const PackageManagerView: React.FC = ({ onDone }) => {
-

Package Manager

+

{t("package-manager:title")}

@@ -61,7 +62,7 @@ const PackageManagerView: React.FC = ({ onDone }) => {
manager.transition({ @@ -75,7 +76,7 @@ const PackageManagerView: React.FC = ({ onDone }) => {
- + )}
setIsTagInputActive(true)} @@ -164,7 +175,7 @@ const PackageManagerView: React.FC = ({ onDone }) => { {(isTagInputActive || tagSearch) && ( - No matching tags found + {t("package-manager:filters.tags.noResults")} {allTags @@ -222,8 +233,10 @@ const PackageManagerView: React.FC = ({ onDone }) => {
{state.filters.tags.length > 0 - ? `Showing items with any of the selected tags (${state.filters.tags.length} selected)` - : "Click tags to filter items"} + ? t("package-manager:filters.tags.selected", { + count: state.filters.tags.length, + }) + : t("package-manager:filters.tags.clickToFilter")}
)} @@ -243,7 +256,7 @@ const PackageManagerView: React.FC = ({ onDone }) => { ) { return (
-

Loading items...

+

{t("package-manager:items.refresh.refreshing")}

) } @@ -252,7 +265,7 @@ const PackageManagerView: React.FC = ({ onDone }) => { if (isEmpty) { return (
-

No package manager items found

+

{t("package-manager:items.empty.noItems")}

) } @@ -261,9 +274,7 @@ const PackageManagerView: React.FC = ({ onDone }) => { return (

- {state.filters.type || state.filters.search || state.filters.tags.length > 0 - ? `${items.length} items found (filtered)` - : `${items.length} ${items.length === 1 ? "item" : "items"} total`} + {t("package-manager:items.count", { count: items.length })}

{items.map((item) => ( @@ -313,48 +324,49 @@ const PackageManagerSourcesConfig: React.FC = onRefreshSource, onSourcesChange, }) => { + const { t } = useAppTranslation() const [newSourceUrl, setNewSourceUrl] = useState("") const [newSourceName, setNewSourceName] = useState("") const [error, setError] = useState("") const handleAddSource = () => { if (!newSourceUrl) { - setError("URL cannot be empty") + setError(t("package-manager:sources.errors.emptyUrl")) return } try { new URL(newSourceUrl) } catch (e) { - setError("Invalid URL format") + setError(t("package-manager:sources.errors.invalidUrl")) return } const nonVisibleCharRegex = /[^\S ]/ if (nonVisibleCharRegex.test(newSourceUrl)) { - setError("URL contains non-visible characters other than spaces") + setError(t("package-manager:sources.errors.nonVisibleChars")) return } if (!isValidGitRepositoryUrl(newSourceUrl)) { - setError("URL must be a valid Git repository URL (e.g., https://github.com/username/repo)") + setError(t("package-manager:sources.errors.invalidGitUrl")) return } const normalizedNewUrl = newSourceUrl.toLowerCase().replace(/\s+/g, "") if (sources.some((source) => source.url.toLowerCase().replace(/\s+/g, "") === normalizedNewUrl)) { - setError("This URL is already in the list (case and whitespace insensitive match)") + setError(t("package-manager:sources.errors.duplicateUrl")) return } if (newSourceName) { if (newSourceName.length > 20) { - setError("Name must be 20 characters or less") + setError(t("package-manager:sources.errors.nameTooLong")) return } if (nonVisibleCharRegex.test(newSourceName)) { - setError("Name contains non-visible characters other than spaces") + setError(t("package-manager:sources.errors.nonVisibleCharsName")) return } @@ -364,14 +376,14 @@ const PackageManagerSourcesConfig: React.FC = (source) => source.name && source.name.toLowerCase().replace(/\s+/g, "") === normalizedNewName, ) ) { - setError("This name is already in use (case and whitespace insensitive match)") + setError(t("package-manager:sources.errors.duplicateName")) return } } const MAX_SOURCES = 10 if (sources.length >= MAX_SOURCES) { - setError(`Maximum of ${MAX_SOURCES} sources allowed`) + setError(t("package-manager:sources.errors.maxSources", { max: MAX_SOURCES })) return } @@ -401,18 +413,15 @@ const PackageManagerSourcesConfig: React.FC = return (
-

Configure Package Manager Sources

-

- Add Git repositories that contain package manager items. These repositories will be fetched when - browsing the package manager. -

+

{t("package-manager:sources.title")}

+

{t("package-manager:sources.description")}

-
Add New Source
+
{t("package-manager:sources.add.title")}
{ setNewSourceUrl(e.target.value) @@ -421,12 +430,11 @@ const PackageManagerSourcesConfig: React.FC = className="p-2 bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded" />

- Supported formats: HTTPS (https://github.com/username/repo), SSH - (git@github.com:username/repo.git), or Git protocol (git://github.com/username/repo.git) + {t("package-manager:sources.add.urlFormats")}

{ setNewSourceName(e.target.value.slice(0, 20)) @@ -439,15 +447,17 @@ const PackageManagerSourcesConfig: React.FC = {error &&

{error}

}
- Current Sources{" "} - ({sources.length}/10 max) + {t("package-manager:sources.current.title")}{" "} + + {t("package-manager:sources.current.count", { current: sources.length, max: 10 })} +
{sources.length === 0 ? ( -

No sources configured. Add a source to get started.

+

{t("package-manager:sources.current.empty")}

) : (
{sources.map((source, index) => ( @@ -477,7 +487,7 @@ const PackageManagerSourcesConfig: React.FC = variant="ghost" size="icon" onClick={() => onRefreshSource(source.url)} - title="Refresh this source" + title={t("package-manager:sources.current.refresh")} className="text-vscode-foreground" disabled={refreshingUrls.includes(source.url)}> = variant="ghost" size="icon" onClick={() => handleRemoveSource(index)} + title={t("package-manager:sources.current.remove")} className="text-red-500"> diff --git a/webview-ui/src/components/package-manager/components/PackageManagerItemCard.tsx b/webview-ui/src/components/package-manager/components/PackageManagerItemCard.tsx index d07b940839..211a6c8955 100644 --- a/webview-ui/src/components/package-manager/components/PackageManagerItemCard.tsx +++ b/webview-ui/src/components/package-manager/components/PackageManagerItemCard.tsx @@ -36,15 +36,15 @@ export const PackageManagerItemCard: React.FC = ({ const getTypeLabel = (type: string) => { switch (type) { case "mode": - return t("package_manager:item_card.type_mode") + return t("package-manager:filters.type.mode") case "mcp server": - return t("package_manager:item_card.type_mcp_server") + return t("package-manager:filters.type.mcp server") case "prompt": - return t("package_manager:item_card.type_prompt") + return t("package-manager:filters.type.prompt") case "package": - return t("package_manager:item_card.type_package") + return t("package-manager:filters.type.package") default: - return t("package_manager:item_card.type_other") + return t("package-manager:filters.type.all") } } @@ -113,7 +113,7 @@ export const PackageManagerItemCard: React.FC = ({ url: item.authorUrl, }) }}> - {t("package_manager:item_card.by_author", { author: item.author })} + {t("package-manager:items.card.by", { author: item.author })} ) : ( )}

) : item.author ? (

- {t("package_manager:item_card.by_author", { author: item.author })} + {t("package-manager:items.card.by", { author: item.author })}

) : null}
@@ -168,8 +168,8 @@ export const PackageManagerItemCard: React.FC = ({ }} title={ filters.tags.includes(tag) - ? t("package_manager:item_card.remove_tag_filter", { tag }) - : t("package_manager:item_card.filter_by_tag", { tag }) + ? t("package-manager:filters.tags.clear", { count: tag }) + : t("package-manager:filters.tags.clickToFilter") }> {tag} @@ -202,23 +202,21 @@ export const PackageManagerItemCard: React.FC = ({ aria-label={ item.sourceUrl && isValidUrl(item.sourceUrl) ? "" - : item.sourceName || t("package_manager:item_card.source") + : item.sourceName || t("package-manager:items.card.viewSource") }> {(!item.sourceUrl || !isValidUrl(item.sourceUrl)) && - (item.sourceName || t("package_manager:item_card.source"))} + (item.sourceName || t("package-manager:items.card.viewSource"))}
{groupedItems && ( { const matchCount = item.items?.filter((subItem) => subItem.matchInfo?.matched).length ?? 0 - return matchCount > 0 - ? t("package_manager:item_card.match_count", { count: matchCount }) - : undefined + return matchCount > 0 ? t("package-manager:items.count", { count: matchCount }) : undefined })()} defaultExpanded={item.items?.some((subItem) => subItem.matchInfo?.matched) ?? false}>
diff --git a/webview-ui/src/components/package-manager/components/TypeGroup.tsx b/webview-ui/src/components/package-manager/components/TypeGroup.tsx index de1008a248..5d10ff477d 100644 --- a/webview-ui/src/components/package-manager/components/TypeGroup.tsx +++ b/webview-ui/src/components/package-manager/components/TypeGroup.tsx @@ -22,15 +22,15 @@ export const TypeGroup: React.FC = ({ type, items, className }) const getTypeLabel = (type: string) => { switch (type) { case "mode": - return t("package_manager:type_group.modes") + return t("package-manager:type-group.modes") case "mcp server": - return t("package_manager:type_group.mcp_servers") + return t("package-manager:type-group.mcp-servers") case "prompt": - return t("package_manager:type_group.prompts") + return t("package-manager:type-group.prompts") case "package": - return t("package_manager:type_group.packages") + return t("package-manager:type-group.packages") default: - return t("package_manager:type_group.generic_type", { + return t("package-manager:type-group.generic-type", { type: type.charAt(0).toUpperCase() + type.slice(1), }) } @@ -60,7 +60,7 @@ export const TypeGroup: React.FC = ({ type, items, className }) )} {item.matchInfo?.matched && ( - {t("package_manager:type_group.match")} + {t("package-manager:type-group.match")} )} diff --git a/webview-ui/src/components/package-manager/components/__tests__/PackageManagerItemCard.test.tsx b/webview-ui/src/components/package-manager/components/__tests__/PackageManagerItemCard.test.tsx index af842b334b..500513c1de 100644 --- a/webview-ui/src/components/package-manager/components/__tests__/PackageManagerItemCard.test.tsx +++ b/webview-ui/src/components/package-manager/components/__tests__/PackageManagerItemCard.test.tsx @@ -65,7 +65,7 @@ describe("PackageManagerItemCard", () => { expect(screen.getByText("Test Package")).toBeInTheDocument() expect(screen.getByText("A test package")).toBeInTheDocument() expect(screen.getByText("by Test Author")).toBeInTheDocument() - expect(screen.getByText("Package")).toBeInTheDocument() + expect(screen.getByText(/Package/i)).toBeInTheDocument() // Using case-insensitive regex since translations might vary in case }) it("should render tags", () => { diff --git a/webview-ui/src/i18n/setup.ts b/webview-ui/src/i18n/setup.ts index 678cdc1d49..984b32c4c8 100644 --- a/webview-ui/src/i18n/setup.ts +++ b/webview-ui/src/i18n/setup.ts @@ -37,6 +37,8 @@ i18next.use(initReactI18next).init({ interpolation: { escapeValue: false, // React already escapes by default }, + defaultNS: "package-manager", + ns: ["package-manager"], }) export function loadTranslations() { diff --git a/webview-ui/src/test/test-utils.tsx b/webview-ui/src/test/test-utils.tsx index 933356b49f..7b6af8ea02 100644 --- a/webview-ui/src/test/test-utils.tsx +++ b/webview-ui/src/test/test-utils.tsx @@ -19,25 +19,25 @@ i18next.use(initReactI18next).init({ }, resources: { en: { - package_manager: { + "package-manager": { // Type group translations - "type_group.mcp_servers": "MCP Servers", - "type_group.modes": "Modes", - "type_group.prompts": "Prompts", - "type_group.packages": "Packages", - "type_group.match": "Match", - "type_group.generic_type": "{{type}}s", + "type-group.mcp-servers": "MCP Servers", + "type-group.modes": "Modes", + "type-group.prompts": "Prompts", + "type-group.packages": "Packages", + "type-group.match": "Match", + "type-group.generic-type": "{{type}}s", // Item card translations - "item_card.by_author": "by {{author}}", - "item_card.type_package": "Package", - "item_card.type_mode": "Mode", - "item_card.type_mcp_server": "MCP Server", - "item_card.type_prompt": "Prompt", - "item_card.source": "Source", - "item_card.component_details": "Component Details", - "item_card.filter_by_tag": "Filter by tag", - "item_card.by": "by", + "item-card.by-author": "by {{author}}", + "item-card.type-package": "Package", + "item-card.type-mode": "Mode", + "item-card.type-mcp-server": "MCP Server", + "item-card.type-prompt": "Prompt", + "item-card.source": "Source", + "item-card.component-details": "Component Details", + "item-card.filter-by-tag": "Filter by tag", + "item-card.by": "by", }, }, },