diff --git a/web/app/components/plugins/marketplace/list/collection-list.tsx b/web/app/components/plugins/marketplace/list/collection-list.tsx index 0af7eb2d7b..5c2a3480bf 100644 --- a/web/app/components/plugins/marketplace/list/collection-list.tsx +++ b/web/app/components/plugins/marketplace/list/collection-list.tsx @@ -46,7 +46,7 @@ export function ViewMoreButton({ searchParams, searchTab }: ViewMoreButtonProps) return (
onMoreClick(searchParams, searchTab)} > {t('marketplace.viewMore', { ns: 'plugin' })} @@ -72,14 +72,23 @@ export function CollectionHeader({ && !!collection.search_params && itemsLength > GRID_DISPLAY_LIMIT + // The API only ships translations for a subset of locales (typically en_US + // and zh_Hans). For any other locale (e.g. ja_JP, pt_BR) the keyed lookup + // returns undefined and the title/description render as empty divs. Fall + // back to the en_US translation, then to whatever value is available, so + // the header always shows something meaningful. + const lang = getLanguage(locale) + const label = collection.label[lang] || collection.label.en_US || Object.values(collection.label)[0] || '' + const description = collection.description[lang] || collection.description.en_US || Object.values(collection.description)[0] || '' + return (
-
- {collection.label[getLanguage(locale)]} +
+ {label}
-
- {collection.description[getLanguage(locale)]} +
+ {description}
{showViewMore && viewMore} diff --git a/web/app/components/plugins/marketplace/search-page/constants.ts b/web/app/components/plugins/marketplace/search-page/constants.ts index 10da9c572b..238fb084ea 100644 --- a/web/app/components/plugins/marketplace/search-page/constants.ts +++ b/web/app/components/plugins/marketplace/search-page/constants.ts @@ -6,10 +6,7 @@ export type LanguageOption = { export const LANGUAGE_OPTIONS: LanguageOption[] = [ { value: 'en', label: 'English', nativeLabel: 'English' }, - { value: 'zh-Hans', label: 'Simplified Chinese', nativeLabel: '简体中文' }, - { value: 'zh-Hant', label: 'Traditional Chinese', nativeLabel: '繁體中文' }, + { value: 'zh-Hans', label: 'Simplified Chinese', nativeLabel: '中文' }, { value: 'ja', label: 'Japanese', nativeLabel: '日本語' }, - { value: 'es', label: 'Spanish', nativeLabel: 'Español' }, - { value: 'fr', label: 'French', nativeLabel: 'Français' }, - { value: 'ko', label: 'Korean', nativeLabel: '한국어' }, + { value: 'other', label: 'Other', nativeLabel: 'Other' }, ] diff --git a/web/app/components/plugins/marketplace/state.ts b/web/app/components/plugins/marketplace/state.ts index a214e56375..5f3fd95d90 100644 --- a/web/app/components/plugins/marketplace/state.ts +++ b/web/app/components/plugins/marketplace/state.ts @@ -6,6 +6,7 @@ import { CATEGORY_ALL } from './constants' import { useMarketplaceContainerScroll } from './hooks' import { useMarketplaceCollectionsAndPlugins, useMarketplacePlugins, useMarketplaceTemplateCollectionsAndTemplates, useMarketplaceTemplates } from './query' import { CREATION_TYPE } from './search-params' +import { useTemplateCollectionsMapFilteredBySystemLanguage, useTemplatesFilteredBySystemLanguage } from './use-sync-system-language' import { getCollectionsParams, getPluginFilterType, mapTemplateDetailToTemplate } from './utils' const getCategory = (category: string) => { @@ -112,10 +113,31 @@ export function useTemplatesMarketplaceData(enabled = true) { // Scroll pagination useMarketplaceContainerScroll(handlePageChange) + // Raw templates as returned by the API (after pagination/flattening). + const rawTemplates = useMemo( + () => templatesQuery.data?.pages.flatMap(page => page.templates).map(mapTemplateDetailToTemplate), + [templatesQuery.data], + ) + + // If the "Filter by language" dropdown is empty, post-filter the returned + // templates by the user's system language. If the dropdown has any picks, + // pass the list through untouched (the server has already filtered). + // + // The same rule applies to the curated-collections layout, which is what + // the page renders in default mode (no search, no category, no manual + // filter) — without this, the system-language filter never visibly takes + // effect on first entry. + const hasManualFilter = filterTemplateLanguages.length > 0 + const templates = useTemplatesFilteredBySystemLanguage(rawTemplates, hasManualFilter) + const templateCollectionTemplatesMap = useTemplateCollectionsMapFilteredBySystemLanguage( + templateCollectionsQuery.data?.templateCollectionTemplatesMap, + hasManualFilter, + ) + return { templateCollections: templateCollectionsQuery.data?.templateCollections, - templateCollectionTemplatesMap: templateCollectionsQuery.data?.templateCollectionTemplatesMap, - templates: templatesQuery.data?.pages.flatMap(page => page.templates).map(mapTemplateDetailToTemplate), + templateCollectionTemplatesMap, + templates, templatesTotal: templatesQuery.data?.pages[0]?.total, page: templatesQuery.data?.pages.length || 1, isLoading: templateCollectionsQuery.isLoading || templatesQuery.isLoading, diff --git a/web/app/components/plugins/marketplace/use-sync-system-language.ts b/web/app/components/plugins/marketplace/use-sync-system-language.ts new file mode 100644 index 0000000000..7c16ef2d99 --- /dev/null +++ b/web/app/components/plugins/marketplace/use-sync-system-language.ts @@ -0,0 +1,106 @@ +import type { Template } from './types' +import { useMemo } from 'react' +import { useLocale } from '@/context/i18n' + +export type TemplateLanguageBucket = 'en' | 'zh' | 'ja' | 'other' + +/** + * Map the user's system locale to one of the four template-language buckets + * exposed by LANGUAGE_OPTIONS: + * + * - en-* (e.g. en-US) → 'en' + * - zh-Hans, zh-Hant, zh-* → 'zh' + * - ja-* (e.g. ja-JP) → 'ja' + * - anything else → 'other' + */ +export function localeToLanguageBucket(locale: string): TemplateLanguageBucket { + const lower = locale.toLowerCase() + if (lower.startsWith('en')) + return 'en' + if (lower.startsWith('zh')) + return 'zh' + if (lower.startsWith('ja')) + return 'ja' + return 'other' +} + +/** + * Test whether a template's `preferred_languages` belong to a given bucket. + * + * - 'en' bucket: at least one entry starts with 'en' + * - 'zh' bucket: at least one entry starts with 'zh' + * - 'ja' bucket: at least one entry starts with 'ja' + * - 'other' bucket: NONE of the entries start with en / zh / ja (this also + * matches templates that have an empty preferred_languages array) + */ +export function templateMatchesBucket( + template: Pick, + bucket: TemplateLanguageBucket, +): boolean { + const langs = (template.preferred_languages || []).map(l => l.toLowerCase()) + + if (bucket === 'other') { + return !langs.some(l => + l.startsWith('en') || l.startsWith('zh') || l.startsWith('ja'), + ) + } + + return langs.some(l => l.startsWith(bucket)) +} + +/** + * Filter the templates list by the user's system language when they have + * NOT manually selected a language in the "Filter by language" dropdown. + * + * - When `hasManualFilter` is true, the server has already filtered the + * list, so the input is returned untouched. + * - Otherwise, the list is filtered client-side by the bucket derived + * from the current system locale. Changing the system locale causes + * this hook to re-compute and the visible list refreshes — but the + * templates `search/advanced` API is NOT refired (its queryKey doesn't + * depend on the locale). + */ +export function useTemplatesFilteredBySystemLanguage< + T extends Pick, +>( + templates: T[] | undefined, + hasManualFilter: boolean, +): T[] | undefined { + const locale = useLocale() + return useMemo(() => { + if (!templates) + return undefined + if (hasManualFilter) + return templates + const bucket = localeToLanguageBucket(locale) + return templates.filter(t => templateMatchesBucket(t, bucket)) + }, [templates, hasManualFilter, locale]) +} + +/** + * Same as `useTemplatesFilteredBySystemLanguage`, but for the collection → + * templates map used by the curated-collections layout. Each collection's + * template list is filtered independently. + * + * Empty collections (where every template was filtered out) are kept in + * the map; the consumer can decide whether to hide them. + */ +export function useTemplateCollectionsMapFilteredBySystemLanguage< + T extends Pick, +>( + map: Record | undefined, + hasManualFilter: boolean, +): Record | undefined { + const locale = useLocale() + return useMemo(() => { + if (!map) + return undefined + if (hasManualFilter) + return map + const bucket = localeToLanguageBucket(locale) + const filtered: Record = {} + for (const key of Object.keys(map)) + filtered[key] = map[key].filter(t => templateMatchesBucket(t, bucket)) + return filtered + }, [map, hasManualFilter, locale]) +}