chore: updated template displays to match the system language (#35629)

This commit is contained in:
Hanqing Zhao 2026-04-28 13:54:57 +08:00 committed by GitHub
parent 0b6bef394a
commit 6eb8433282
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 146 additions and 12 deletions

View File

@ -46,7 +46,7 @@ export function ViewMoreButton({ searchParams, searchTab }: ViewMoreButtonProps)
return (
<div
className="system-xs-medium flex cursor-pointer items-center text-text-accent"
className="flex cursor-pointer items-center text-text-accent system-xs-medium"
onClick={() => onMoreClick(searchParams, searchTab)}
>
{t('marketplace.viewMore', { ns: 'plugin' })}
@ -72,14 +72,23 @@ export function CollectionHeader<TCollection extends BaseCollection>({
&& !!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 (
<div className="mb-2 flex items-end justify-between">
<div>
<div className="title-xl-semi-bold text-text-primary">
{collection.label[getLanguage(locale)]}
<div className="text-text-primary title-xl-semi-bold">
{label}
</div>
<div className="system-xs-regular text-text-tertiary">
{collection.description[getLanguage(locale)]}
<div className="text-text-tertiary system-xs-regular">
{description}
</div>
</div>
{showViewMore && viewMore}

View File

@ -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' },
]

View File

@ -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,

View File

@ -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<Template, 'preferred_languages'>,
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<Template, 'preferred_languages'>,
>(
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<Template, 'preferred_languages'>,
>(
map: Record<string, T[]> | undefined,
hasManualFilter: boolean,
): Record<string, T[]> | undefined {
const locale = useLocale()
return useMemo(() => {
if (!map)
return undefined
if (hasManualFilter)
return map
const bucket = localeToLanguageBucket(locale)
const filtered: Record<string, T[]> = {}
for (const key of Object.keys(map))
filtered[key] = map[key].filter(t => templateMatchesBucket(t, bucket))
return filtered
}, [map, hasManualFilter, locale])
}