From c0f615bca77f06de54f173d6d1995c618084464a Mon Sep 17 00:00:00 2001 From: Smartsheet-JB-Brown Date: Thu, 17 Apr 2025 21:40:26 -0700 Subject: [PATCH] more memory pressure work --- src/services/package-manager/GitFetcher.ts | 52 ++++- .../package-manager/MetadataScanner.ts | 21 +- .../PackageManagerSourceValidation.ts | 196 ++++++++++++------ .../PackageManagerViewStateManager.ts | 102 ++++----- .../components/PackageManagerItemCard.tsx | 15 +- .../__tests__/PackageManagerItemCard.test.tsx | 18 +- .../package-manager/useStateManager.ts | 11 +- .../src/i18n/locales/en/package-manager.json | 3 +- .../src/i18n/locales/ja/package-manager.json | 3 +- .../src/i18n/locales/ko/package-manager.json | 3 +- .../src/i18n/locales/pl/package-manager.json | 4 +- .../src/i18n/locales/tr/package-manager.json | 3 +- .../src/i18n/locales/vi/package-manager.json | 3 +- .../i18n/locales/zh-CN/package-manager.json | 3 +- .../i18n/locales/zh-TW/package-manager.json | 3 +- webview-ui/src/test/test-utils.tsx | 3 +- 16 files changed, 286 insertions(+), 157 deletions(-) diff --git a/src/services/package-manager/GitFetcher.ts b/src/services/package-manager/GitFetcher.ts index b1c1a8cce2..eee1bc0504 100644 --- a/src/services/package-manager/GitFetcher.ts +++ b/src/services/package-manager/GitFetcher.ts @@ -16,6 +16,7 @@ export class GitFetcher { private metadataScanner: MetadataScanner private git?: SimpleGit private localizationOptions: LocalizationOptions + private activeGitInstances: Set = new Set() constructor(context: vscode.ExtensionContext, localizationOptions?: LocalizationOptions) { this.cacheDir = path.join(context.globalStorageUri.fsPath, "package-manager-cache") @@ -26,14 +27,55 @@ export class GitFetcher { this.metadataScanner = new MetadataScanner(undefined, this.localizationOptions) } + /** + * Clean up resources + */ + dispose(): void { + // Clean up all git instances + this.activeGitInstances.forEach((git) => { + try { + // Force cleanup of git instance + ;(git as any)._executor = null + } catch { + // Ignore cleanup errors + } + }) + this.activeGitInstances.clear() + + // Clean up metadata scanner + if (this.metadataScanner) { + this.metadataScanner = null as any + } + } + /** * Initialize git instance for a repository * @param repoDir Repository directory */ private initGit(repoDir: string): void { + // Clean up old git instance if it exists + if (this.git) { + this.activeGitInstances.delete(this.git) + try { + // Force cleanup of git instance + ;(this.git as any)._executor = null + } catch { + // Ignore cleanup errors + } + } + + // Create new git instance this.git = simpleGit(repoDir) + this.activeGitInstances.add(this.git) + // Update MetadataScanner with new git instance + const oldScanner = this.metadataScanner this.metadataScanner = new MetadataScanner(this.git, this.localizationOptions) + + // Clean up old scanner + if (oldScanner) { + oldScanner.dispose?.() + } } /** @@ -68,9 +110,8 @@ export class GitFetcher { const metadata = await this.parseRepositoryMetadata(repoDir) // Parse package manager items - // Get current branch - const git = simpleGit(repoDir) - const branch = await git.revparse(["--abbrev-ref", "HEAD"]) + // Get current branch using existing git instance + const branch = (await this.git?.revparse(["--abbrev-ref", "HEAD"])) || "main" const items = await this.parsePackageManagerItems(repoDir, repoUrl, sourceName || metadata.name) @@ -196,9 +237,8 @@ export class GitFetcher { } } - // Get current branch - const git = simpleGit(repoDir) - const branch = await git.revparse(["--abbrev-ref", "HEAD"]) + // Get current branch using existing git instance + const branch = (await this.git?.revparse(["--abbrev-ref", "HEAD"])) || "main" } catch (error) { throw new Error( `Failed to clone/pull repository: ${error instanceof Error ? error.message : String(error)}`, diff --git a/src/services/package-manager/MetadataScanner.ts b/src/services/package-manager/MetadataScanner.ts index dc9afa7dbd..f55f012915 100644 --- a/src/services/package-manager/MetadataScanner.ts +++ b/src/services/package-manager/MetadataScanner.ts @@ -18,12 +18,13 @@ import { getUserLocale } from "./utils" * Handles component discovery and metadata loading */ export class MetadataScanner { - private readonly git?: SimpleGit + private git?: SimpleGit private localizationOptions: LocalizationOptions private originalRootDir: string | null = null private static readonly MAX_DEPTH = 5 // Maximum directory depth private static readonly BATCH_SIZE = 50 // Number of items to process at once private static readonly CONCURRENT_SCANS = 3 // Number of concurrent directory scans + private isDisposed = false constructor(git?: SimpleGit, localizationOptions?: LocalizationOptions) { this.git = git @@ -33,6 +34,24 @@ export class MetadataScanner { } } + /** + * Clean up resources + */ + dispose(): void { + if (this.isDisposed) { + return + } + + // Clean up git instance reference + this.git = undefined + + // Clear any other references + this.originalRootDir = null + this.localizationOptions = null as any + + this.isDisposed = true + } + /** * Generator function to yield items in batches */ diff --git a/src/services/package-manager/PackageManagerSourceValidation.ts b/src/services/package-manager/PackageManagerSourceValidation.ts index bec78f7f3e..21aca403f8 100644 --- a/src/services/package-manager/PackageManagerSourceValidation.ts +++ b/src/services/package-manager/PackageManagerSourceValidation.ts @@ -130,81 +130,146 @@ export function validateSourceName(name?: string): ValidationError[] { * @param newSource The new source to check against the list (optional) * @returns An array of validation errors, empty if valid */ +// Cache for normalized strings to avoid repeated operations +const normalizeCache = new Map() + +function normalizeString(str: string): string { + const cached = normalizeCache.get(str) + if (cached) return cached + + const normalized = str.toLowerCase().replace(/\s+/g, "") + normalizeCache.set(str, normalized) + return normalized +} + export function validateSourceDuplicates( sources: PackageManagerSource[], newSource?: PackageManagerSource, ): ValidationError[] { const errors: ValidationError[] = [] - const normalizedUrls: { url: string; index: number }[] = [] - const normalizedNames: { name: string; index: number }[] = [] + const urlMap = new Map() + const nameMap = new Map() // Process existing sources - sources.forEach((source, index) => { - // Normalize URL (case and whitespace insensitive) - const normalizedUrl = source.url.toLowerCase().replace(/\s+/g, "") - normalizedUrls.push({ url: normalizedUrl, index }) + // Process existing sources + const seen = new Set() - // Normalize name if it exists (case and whitespace insensitive) - if (source.name) { - const normalizedName = source.name.toLowerCase().replace(/\s+/g, "") - normalizedNames.push({ name: normalizedName, index }) + // Check for duplicates within existing sources + for (let i = 0; i < sources.length; i++) { + const source = sources[i] + const normalizedUrl = normalizeString(source.url) + const normalizedName = source.name ? normalizeString(source.name) : null + + // Check for URL duplicates + for (let j = i + 1; j < sources.length; j++) { + const otherSource = sources[j] + const otherUrl = normalizeString(otherSource.url) + + if (normalizedUrl === otherUrl) { + const key = `url:${i}:${j}` + if (!seen.has(key)) { + errors.push({ + field: "url", + message: `Source #${i + 1} has a duplicate URL with Source #${j + 1}`, + }) + errors.push({ + field: "url", + message: `Source #${j + 1} has a duplicate URL with Source #${i + 1}`, + }) + seen.add(key) + seen.add(`url:${j}:${i}`) + } + } + + // Check for name duplicates if both have names + if (normalizedName && otherSource.name) { + const otherName = normalizeString(otherSource.name) + if (normalizedName === otherName) { + const key = `name:${i}:${j}` + if (!seen.has(key)) { + errors.push({ + field: "name", + message: `Source #${i + 1} has a duplicate name with Source #${j + 1}`, + }) + errors.push({ + field: "name", + message: `Source #${j + 1} has a duplicate name with Source #${i + 1}`, + }) + seen.add(key) + seen.add(`name:${j}:${i}`) + } + } + } } - }) - - // Check for duplicates within the existing sources - normalizedUrls.forEach((item, index) => { - const duplicates = normalizedUrls.filter((other, otherIndex) => other.url === item.url && otherIndex !== index) - - if (duplicates.length > 0) { - errors.push({ - field: "url", - message: `Source #${item.index + 1} has a duplicate URL with Source #${duplicates[0].index + 1} (case and whitespace insensitive match)`, - }) - } - }) - - normalizedNames.forEach((item, index) => { - const duplicates = normalizedNames.filter( - (other, otherIndex) => other.name === item.name && otherIndex !== index, - ) - - if (duplicates.length > 0) { - errors.push({ - field: "name", - message: `Source #${item.index + 1} has a duplicate name with Source #${duplicates[0].index + 1} (case and whitespace insensitive match)`, - }) - } - }) + } // Check new source against existing sources if provided if (newSource) { - // Validate URL if (newSource.url) { - const normalizedNewUrl = newSource.url.toLowerCase().replace(/\s+/g, "") - const duplicateUrl = normalizedUrls.find((item) => item.url === normalizedNewUrl) - - if (duplicateUrl) { + const normalizedNewUrl = normalizeString(newSource.url) + const existingUrlIndex = urlMap.get(normalizedNewUrl) + if (existingUrlIndex !== undefined) { errors.push({ field: "url", - message: `URL is a duplicate of Source #${duplicateUrl.index + 1} (case and whitespace insensitive match)`, + message: `URL is a duplicate of Source #${existingUrlIndex + 1}`, }) } } - // Validate name if (newSource.name) { - const normalizedNewName = newSource.name.toLowerCase().replace(/\s+/g, "") - const duplicateName = normalizedNames.find((item) => item.name === normalizedNewName) - - if (duplicateName) { + const normalizedNewName = normalizeString(newSource.name) + const existingNameIndex = nameMap.get(normalizedNewName) + if (existingNameIndex !== undefined) { errors.push({ field: "name", - message: `Name is a duplicate of Source #${duplicateName.index + 1} (case and whitespace insensitive match)`, + message: `Name is a duplicate of Source #${existingNameIndex + 1}`, }) } } } + // Check new source against existing sources if provided + if (newSource) { + const normalizedNewUrl = normalizeString(newSource.url) + const normalizedNewName = newSource.name ? normalizeString(newSource.name) : null + + // Add new source to maps temporarily + const newIndex = sources.length + urlMap.set(normalizedNewUrl, newIndex) + if (normalizedNewName) { + nameMap.set(normalizedNewName, newIndex) + } + + // Check for duplicates with existing sources + for (let i = 0; i < sources.length; i++) { + const source = sources[i] + const sourceUrl = normalizeString(source.url) + + if (sourceUrl === normalizedNewUrl) { + errors.push({ + field: "url", + message: `URL is a duplicate of Source #${i + 1}`, + }) + } + + if (source.name && normalizedNewName) { + const sourceName = normalizeString(source.name) + if (sourceName === normalizedNewName) { + errors.push({ + field: "name", + message: `Name is a duplicate of Source #${i + 1}`, + }) + } + } + } + + // Remove temporary entries + urlMap.delete(normalizedNewUrl) + if (normalizedNewName) { + nameMap.delete(normalizedNewName) + } + } + return errors } @@ -232,24 +297,37 @@ export function validateSource( * @returns An array of validation errors, empty if valid */ export function validateSources(sources: PackageManagerSource[]): ValidationError[] { - const errors: ValidationError[] = [] + // Pre-allocate maximum possible size for errors array + const errors: ValidationError[] = new Array(sources.length * 2 + (sources.length * (sources.length - 1)) / 2) + let errorIndex = 0 // Validate each source individually - sources.forEach((source, index) => { - const sourceErrors = [...validateSourceUrl(source.url), ...validateSourceName(source.name)] + for (let i = 0; i < sources.length; i++) { + const source = sources[i] + const urlErrors = validateSourceUrl(source.url) + const nameErrors = validateSourceName(source.name) // Add index to error messages - sourceErrors.forEach((error) => { - errors.push({ + for (const error of urlErrors) { + errors[errorIndex++] = { field: error.field, - message: `Source #${index + 1}: ${error.message}`, - }) - }) - }) + message: `Source #${i + 1}: ${error.message}`, + } + } + for (const error of nameErrors) { + errors[errorIndex++] = { + field: error.field, + message: `Source #${i + 1}: ${error.message}`, + } + } + } // Check for duplicates across all sources const duplicateErrors = validateSourceDuplicates(sources) - errors.push(...duplicateErrors) + for (const error of duplicateErrors) { + errors[errorIndex++] = error + } - return errors + // Trim array to actual size + return errors.slice(0, errorIndex) } diff --git a/webview-ui/src/components/package-manager/PackageManagerViewStateManager.ts b/webview-ui/src/components/package-manager/PackageManagerViewStateManager.ts index 7e251ebb02..c585b3a6fd 100644 --- a/webview-ui/src/components/package-manager/PackageManagerViewStateManager.ts +++ b/webview-ui/src/components/package-manager/PackageManagerViewStateManager.ts @@ -86,15 +86,21 @@ export class PackageManagerViewStateManager { } public getState(): ViewState { + // Only create new arrays if they exist and have items + const displayItems = this.state.displayItems?.length ? [...this.state.displayItems] : this.state.displayItems + const refreshingUrls = this.state.refreshingUrls.length ? [...this.state.refreshingUrls] : [] + const tags = this.state.filters.tags.length ? [...this.state.filters.tags] : [] + + // Create minimal new state object return { ...this.state, - allItems: [...this.state.allItems], - displayItems: this.state.displayItems ? [...this.state.displayItems] : undefined, - refreshingUrls: [...this.state.refreshingUrls], - sources: [...this.state.sources], + allItems: this.state.allItems.length ? [...this.state.allItems] : [], + displayItems, + refreshingUrls, + sources: this.state.sources.length ? [...this.state.sources] : [DEFAULT_PACKAGE_MANAGER_SOURCE], filters: { ...this.state.filters, - tags: [...this.state.filters.tags], + tags, }, } } @@ -113,17 +119,11 @@ export class PackageManagerViewStateManager { return } - // Create a new state object to ensure React sees the change - const newState = { - ...this.state, - isFetching: true, - } - // Clear any existing timeout before starting new fetch this.clearFetchTimeout() - // Update state and notify before starting fetch - this.state = newState + // Update state directly + this.state.isFetching = true this.notifyStateChange() // Set timeout for fetch operation @@ -146,21 +146,20 @@ export class PackageManagerViewStateManager { this.clearFetchTimeout() // Create a new state object with sorted items - const sortedItems = this.sortItems([...items]) - const newState = { - ...this.state, - isFetching: false, - displayItems: sortedItems, // Use items directly from backend + // Sort items in place to avoid creating unnecessary copies + const sortedItems = this.sortItems(items) + + // Minimize state updates + if (this.isFilterActive()) { + this.state.displayItems = sortedItems + this.state.isFetching = false + } else { + this.state.allItems = sortedItems + this.state.displayItems = sortedItems + this.state.isFetching = false } - // Only update allItems if this isn't a filter response - if (!this.isFilterActive()) { - newState.allItems = sortedItems - } - - // Update state and notify - this.state = newState - + // Notify state change this.notifyStateChange() break } @@ -168,12 +167,8 @@ export class PackageManagerViewStateManager { case "FETCH_ERROR": { this.clearFetchTimeout() - // Create a new state object to ensure React sees the change - this.state = { - ...this.state, - isFetching: false, - } - + // Update state directly + this.state.isFetching = false this.notifyStateChange() break } @@ -181,23 +176,18 @@ export class PackageManagerViewStateManager { case "SET_ACTIVE_TAB": { const { tab } = transition.payload as TransitionPayloads["SET_ACTIVE_TAB"] - // Create a new state object - const newState = { - ...this.state, - activeTab: tab, - } + // Update state directly + this.state.activeTab = tab // Add default source when switching to sources tab if no sources exist - if (tab === "sources" && newState.sources.length === 0) { - newState.sources = [DEFAULT_PACKAGE_MANAGER_SOURCE] + if (tab === "sources" && this.state.sources.length === 0) { + this.state.sources = [DEFAULT_PACKAGE_MANAGER_SOURCE] vscode.postMessage({ type: "packageManagerSources", sources: [DEFAULT_PACKAGE_MANAGER_SOURCE], } as WebviewMessage) } - // Update state and notify - this.state = newState this.notifyStateChange() // Handle browse tab switch @@ -254,13 +244,12 @@ export class PackageManagerViewStateManager { } // Apply sorting to both allItems and displayItems // Sort items immutably - const sortedAllItems = this.sortItems(this.state.allItems) - const sortedDisplayItems = this.state.displayItems ? this.sortItems(this.state.displayItems) : undefined - - this.state = { - ...this.state, - allItems: sortedAllItems, - displayItems: sortedDisplayItems, + // Sort arrays in place + if (this.state.allItems.length) { + this.sortItems(this.state.allItems) + } + if (this.state.displayItems?.length) { + this.sortItems(this.state.displayItems) } this.notifyStateChange() break @@ -377,19 +366,16 @@ export class PackageManagerViewStateManager { private sortItems(items: PackageManagerItem[]): PackageManagerItem[] { const { by, order } = this.state.sortConfig - return [...items].sort((a, b) => { - let aValue = a[by] || "" - let bValue = b[by] || "" - // Handle dates for lastUpdated - if (by === "lastUpdated") { - aValue = aValue || "1970-01-01T00:00:00Z" - bValue = bValue || "1970-01-01T00:00:00Z" - } + // Sort array in place + items.sort((a, b) => { + const aValue = by === "lastUpdated" ? a[by] || "1970-01-01T00:00:00Z" : a[by] || "" + const bValue = by === "lastUpdated" ? b[by] || "1970-01-01T00:00:00Z" : b[by] || "" - const comparison = aValue.localeCompare(bValue) - return order === "asc" ? comparison : -comparison + return order === "asc" ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue) }) + + return items } public async handleMessage(message: any): Promise { diff --git a/webview-ui/src/components/package-manager/components/PackageManagerItemCard.tsx b/webview-ui/src/components/package-manager/components/PackageManagerItemCard.tsx index 87c86092b9..f3ee454a6e 100644 --- a/webview-ui/src/components/package-manager/components/PackageManagerItemCard.tsx +++ b/webview-ui/src/components/package-manager/components/PackageManagerItemCard.tsx @@ -208,21 +208,22 @@ export const PackageManagerItemCard: React.FC = ({ - {groupedItems && ( +
{ const matchCount = item.items?.filter((subItem) => subItem.matchInfo?.matched).length ?? 0 - return matchCount > 0 ? t("package-manager:items.count", { count: matchCount }) : undefined + return matchCount > 0 ? t("package-manager:items.components", { count: matchCount }) : undefined })()} defaultExpanded={item.items?.some((subItem) => subItem.matchInfo?.matched) ?? false}>
- {Object.entries(groupedItems).map(([type, group]) => ( - - ))} + {groupedItems && + Object.entries(groupedItems).map(([type, group]) => ( + + ))}
- )} +
) } 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 24dd6fd690..f7429c7e26 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 @@ -168,11 +168,19 @@ describe("PackageManagerItemCard", () => { }) describe("Details section", () => { - it("should render expandable details section when item has subcomponents", () => { + it("should render expandable details section with correct count when item has no components", () => { + const itemWithNoItems = { ...mockItem, items: [] } + renderWithProviders() + + // The component uses t("package-manager:items.components", { count: 0 }) + expect(screen.getByText("0 components")).toBeInTheDocument() + }) + + it("should render expandable details section with correct count when item has components", () => { renderWithProviders() - // The component uses t("package-manager:items.card.externalComponents", { count: 0 }) - expect(screen.getByText("Contains 0 external component")).toBeInTheDocument() + // The component uses t("package-manager:items.components", { count: 2 }) + expect(screen.getByText("2 components")).toBeInTheDocument() }) it("should not render details section when item has no subcomponents", () => { @@ -184,7 +192,7 @@ describe("PackageManagerItemCard", () => { it("should show grouped items when expanded", () => { renderWithProviders() - fireEvent.click(screen.getByText("Contains 0 external component")) + fireEvent.click(screen.getByText("2 components")) // These use the type-group translations expect(screen.getByText((content, element) => element?.textContent === "MCP Servers")).toBeInTheDocument() @@ -200,7 +208,7 @@ describe("PackageManagerItemCard", () => { it("should maintain proper order of items within groups", () => { renderWithProviders() - fireEvent.click(screen.getByText("Contains 0 external component")) + fireEvent.click(screen.getByText("2 components")) const items = screen.getAllByRole("listitem") expect(items[0]).toHaveTextContent("Test Server") diff --git a/webview-ui/src/components/package-manager/useStateManager.ts b/webview-ui/src/components/package-manager/useStateManager.ts index eaaf00143e..e6efc6981b 100644 --- a/webview-ui/src/components/package-manager/useStateManager.ts +++ b/webview-ui/src/components/package-manager/useStateManager.ts @@ -3,12 +3,17 @@ import { PackageManagerViewStateManager, ViewState } from "./PackageManagerViewS export function useStateManager() { const [manager] = useState(() => new PackageManagerViewStateManager()) - const [state, setState] = useState(() => manager.getState()) useEffect(() => { const handleStateChange = (newState: ViewState) => { - setState(newState) + setState((prevState) => { + // Only update if something actually changed + if (JSON.stringify(prevState) === JSON.stringify(newState)) { + return prevState + } + return newState + }) } const handleMessage = (event: MessageEvent) => { @@ -23,7 +28,7 @@ export function useStateManager() { unsubscribe() manager.cleanup() } - }, [manager, state]) + }, [manager]) // Remove state from dependencies return [state, manager] as const } diff --git a/webview-ui/src/i18n/locales/en/package-manager.json b/webview-ui/src/i18n/locales/en/package-manager.json index 6c64859c43..bc74c9fd60 100644 --- a/webview-ui/src/i18n/locales/en/package-manager.json +++ b/webview-ui/src/i18n/locales/en/package-manager.json @@ -39,6 +39,7 @@ "noSources": "Try adding a source in the Sources tab" }, "count": "{{count}} items found", + "components": "{{count}} components", "refresh": { "button": "Refresh", "refreshing": "Refreshing..." @@ -46,8 +47,6 @@ "card": { "by": "by {{author}}", "from": "from {{source}}", - "externalComponents": "Contains {{count}} external component", - "externalComponents_plural": "Contains {{count}} external components", "viewSource": "View", "viewOnSource": "View on {{source}}" } diff --git a/webview-ui/src/i18n/locales/ja/package-manager.json b/webview-ui/src/i18n/locales/ja/package-manager.json index 3c41dfcfd5..e837b1a488 100644 --- a/webview-ui/src/i18n/locales/ja/package-manager.json +++ b/webview-ui/src/i18n/locales/ja/package-manager.json @@ -39,6 +39,7 @@ "noSources": "ソースタブでソースを追加してみてください" }, "count": "{{count}}個のアイテムが見つかりました", + "components": "{{count}}個のコンポーネント", "refresh": { "button": "更新", "refreshing": "更新中..." @@ -46,8 +47,6 @@ "card": { "by": "作者: {{author}}", "from": "ソース: {{source}}", - "externalComponents": "{{count}}個の外部コンポーネントを含む", - "externalComponents_plural": "{{count}}個の外部コンポーネントを含む", "viewSource": "表示", "viewOnSource": "{{source}}で表示" } diff --git a/webview-ui/src/i18n/locales/ko/package-manager.json b/webview-ui/src/i18n/locales/ko/package-manager.json index 4f73b9a829..3a4e222034 100644 --- a/webview-ui/src/i18n/locales/ko/package-manager.json +++ b/webview-ui/src/i18n/locales/ko/package-manager.json @@ -39,6 +39,7 @@ "noSources": "소스 탭에서 소스를 추가해 보세요" }, "count": "{{count}}개의 항목을 찾았습니다", + "components": "{{count}}개의 컴포넌트", "refresh": { "button": "새로 고침", "refreshing": "새로 고치는 중..." @@ -46,8 +47,6 @@ "card": { "by": "작성자: {{author}}", "from": "출처: {{source}}", - "externalComponents": "외부 컴포넌트 {{count}}개 포함", - "externalComponents_plural": "외부 컴포넌트 {{count}}개 포함", "viewSource": "보기", "viewOnSource": "{{source}}에서 보기" } diff --git a/webview-ui/src/i18n/locales/pl/package-manager.json b/webview-ui/src/i18n/locales/pl/package-manager.json index 2e9779b364..c87599b93a 100644 --- a/webview-ui/src/i18n/locales/pl/package-manager.json +++ b/webview-ui/src/i18n/locales/pl/package-manager.json @@ -39,6 +39,8 @@ "noSources": "Spróbuj dodać źródło w zakładce Źródła" }, "count": "Znaleziono {{count}} elementów", + "components": "{{count}} komponent", + "components_plural": "{{count}} komponenty", "refresh": { "button": "Odśwież", "refreshing": "Odświeżanie..." @@ -46,8 +48,6 @@ "card": { "by": "autor: {{author}}", "from": "z: {{source}}", - "externalComponents": "Zawiera {{count}} komponent zewnętrzny", - "externalComponents_plural": "Zawiera {{count}} komponenty zewnętrzne", "viewSource": "Zobacz", "viewOnSource": "Zobacz na {{source}}" } diff --git a/webview-ui/src/i18n/locales/tr/package-manager.json b/webview-ui/src/i18n/locales/tr/package-manager.json index 9491f8bbe5..c179bf732c 100644 --- a/webview-ui/src/i18n/locales/tr/package-manager.json +++ b/webview-ui/src/i18n/locales/tr/package-manager.json @@ -39,6 +39,7 @@ "noSources": "Kaynaklar sekmesinde bir kaynak eklemeyi deneyin" }, "count": "{{count}} öğe bulundu", + "components": "{{count}} bileşen", "refresh": { "button": "Yenile", "refreshing": "Yenileniyor..." @@ -46,8 +47,6 @@ "card": { "by": "yazar: {{author}}", "from": "kaynak: {{source}}", - "externalComponents": "{{count}} harici bileşen içeriyor", - "externalComponents_plural": "{{count}} harici bileşen içeriyor", "viewSource": "Görüntüle", "viewOnSource": "{{source}} üzerinde görüntüle" } diff --git a/webview-ui/src/i18n/locales/vi/package-manager.json b/webview-ui/src/i18n/locales/vi/package-manager.json index 4bba19c593..185cbd29d7 100644 --- a/webview-ui/src/i18n/locales/vi/package-manager.json +++ b/webview-ui/src/i18n/locales/vi/package-manager.json @@ -39,6 +39,7 @@ "noSources": "Thử thêm một nguồn trong tab Nguồn" }, "count": "Tìm thấy {{count}} mục", + "components": "{{count}} thành phần", "refresh": { "button": "Làm mới", "refreshing": "Đang làm mới..." @@ -46,8 +47,6 @@ "card": { "by": "bởi {{author}}", "from": "từ {{source}}", - "externalComponents": "Chứa {{count}} thành phần bên ngoài", - "externalComponents_plural": "Chứa {{count}} thành phần bên ngoài", "viewSource": "Xem", "viewOnSource": "Xem trên {{source}}" } diff --git a/webview-ui/src/i18n/locales/zh-CN/package-manager.json b/webview-ui/src/i18n/locales/zh-CN/package-manager.json index e910ecd315..78025cf1f2 100644 --- a/webview-ui/src/i18n/locales/zh-CN/package-manager.json +++ b/webview-ui/src/i18n/locales/zh-CN/package-manager.json @@ -39,6 +39,7 @@ "noSources": "尝试在源标签页中添加源" }, "count": "找到{{count}}个项目", + "components": "{{count}}个组件", "refresh": { "button": "刷新", "refreshing": "正在刷新..." @@ -46,8 +47,6 @@ "card": { "by": "作者:{{author}}", "from": "来源:{{source}}", - "externalComponents": "包含{{count}}个外部组件", - "externalComponents_plural": "包含{{count}}个外部组件", "viewSource": "查看", "viewOnSource": "在{{source}}上查看" } diff --git a/webview-ui/src/i18n/locales/zh-TW/package-manager.json b/webview-ui/src/i18n/locales/zh-TW/package-manager.json index 7192b62405..59130948dd 100644 --- a/webview-ui/src/i18n/locales/zh-TW/package-manager.json +++ b/webview-ui/src/i18n/locales/zh-TW/package-manager.json @@ -39,6 +39,7 @@ "noSources": "嘗試在來源分頁中新增來源" }, "count": "找到{{count}}個項目", + "components": "{{count}}個元件", "refresh": { "button": "重新整理", "refreshing": "正在重新整理..." @@ -46,8 +47,6 @@ "card": { "by": "作者:{{author}}", "from": "來源:{{source}}", - "externalComponents": "包含{{count}}個外部元件", - "externalComponents_plural": "包含{{count}}個外部元件", "viewSource": "檢視", "viewOnSource": "在{{source}}上檢視" } diff --git a/webview-ui/src/test/test-utils.tsx b/webview-ui/src/test/test-utils.tsx index 99cd1405ad..09d10edf70 100644 --- a/webview-ui/src/test/test-utils.tsx +++ b/webview-ui/src/test/test-utils.tsx @@ -60,6 +60,7 @@ i18next.use(initReactI18next).init({ noSources: "Try adding a source in the Sources tab", }, count: "{{count}} items found", + components: "{{count}} components", refresh: { button: "Refresh", refreshing: "Refreshing...", @@ -69,8 +70,6 @@ i18next.use(initReactI18next).init({ from: "from {{source}}", viewSource: "View", viewOnSource: "View on {{source}}", - externalComponents: "Contains {{count}} external component", - externalComponents_plural: "Contains {{count}} external components", }, }, "type-group": {