- {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])
+}