diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml
index 148eb5604f6..36dd7e05fad 100644
--- a/.github/workflows/main-ci.yml
+++ b/.github/workflows/main-ci.yml
@@ -108,7 +108,6 @@ jobs:
- 'docker/docker-compose.middleware.yaml'
- 'docker/envs/middleware.env.example'
- '.github/workflows/web-e2e.yml'
- - '.github/workflows/marketplace-performance-e2e.yml'
- '.github/workflows/main-ci.yml'
- '.github/actions/setup-web/**'
vdb:
@@ -394,66 +393,6 @@ jobs:
echo "Web full-stack E2E was not required, but the skip job finished with result: $SKIP_RESULT" >&2
exit 1
- marketplace-performance-run:
- name: Run Marketplace Performance E2E
- needs:
- - pre_job
- - check-changes
- if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.e2e-changed == 'true'
- uses: ./.github/workflows/marketplace-performance-e2e.yml
- secrets: inherit
-
- marketplace-performance-skip:
- name: Skip Marketplace Performance E2E
- needs:
- - pre_job
- - check-changes
- if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.e2e-changed != 'true'
- runs-on: depot-ubuntu-24.04
- steps:
- - name: Report skipped marketplace performance E2E
- run: echo "No E2E-related changes detected; skipping marketplace performance E2E."
-
- marketplace-performance:
- name: Marketplace Performance E2E
- if: ${{ always() }}
- needs:
- - pre_job
- - check-changes
- - marketplace-performance-run
- - marketplace-performance-skip
- runs-on: depot-ubuntu-24.04
- steps:
- - name: Finalize Marketplace Performance E2E status
- env:
- SHOULD_SKIP_WORKFLOW: ${{ needs.pre_job.outputs.should_skip }}
- TESTS_CHANGED: ${{ needs.check-changes.outputs.e2e-changed }}
- RUN_RESULT: ${{ needs.marketplace-performance-run.result }}
- SKIP_RESULT: ${{ needs.marketplace-performance-skip.result }}
- run: |
- if [[ "$SHOULD_SKIP_WORKFLOW" == 'true' ]]; then
- echo "Marketplace performance E2E was skipped because this workflow run duplicated a successful or newer run."
- exit 0
- fi
-
- if [[ "$TESTS_CHANGED" == 'true' ]]; then
- if [[ "$RUN_RESULT" == 'success' ]]; then
- echo "Marketplace performance E2E ran successfully."
- exit 0
- fi
-
- echo "Marketplace performance E2E was required but finished with result: $RUN_RESULT" >&2
- exit 1
- fi
-
- if [[ "$SKIP_RESULT" == 'success' ]]; then
- echo "Marketplace performance E2E was skipped because no E2E-related files changed."
- exit 0
- fi
-
- echo "Marketplace performance E2E was not required, but the skip job finished with result: $SKIP_RESULT" >&2
- exit 1
-
style-check:
name: Style Check
needs: pre_job
diff --git a/.github/workflows/marketplace-performance-e2e.yml b/.github/workflows/marketplace-performance-e2e.yml
index 6dceb665b21..94de24a534a 100644
--- a/.github/workflows/marketplace-performance-e2e.yml
+++ b/.github/workflows/marketplace-performance-e2e.yml
@@ -1,7 +1,9 @@
name: Marketplace Performance E2E
+# Opt-in diagnostic: single-sample timing budgets are too noisy to gate every
+# PR, so this lane is only run on demand instead of from the main CI pipeline.
on:
- workflow_call:
+ workflow_dispatch:
permissions:
contents: read
diff --git a/web/app/(commonLayout)/templates/[[...category]]/__tests__/page.spec.tsx b/web/app/(commonLayout)/templates/[[...category]]/__tests__/page.spec.tsx
index b528f3b5e56..c73e16675be 100644
--- a/web/app/(commonLayout)/templates/[[...category]]/__tests__/page.spec.tsx
+++ b/web/app/(commonLayout)/templates/[[...category]]/__tests__/page.spec.tsx
@@ -4,8 +4,24 @@ import { redirect } from '@/next/navigation'
import TemplatesPage from '../page'
vi.mock('@/app/components/plugins/marketplace/templates', () => ({
- EmbeddedTemplatesMarketplace: ({ category, query }: { category: string; query: string }) => (
-
{`Templates catalog: ${category}:${query}`}
+ EmbeddedTemplatesMarketplace: ({
+ category,
+ page,
+ query,
+ sortBy,
+ sortOrder,
+ view,
+ }: {
+ category: string
+ page: number
+ query: string
+ sortBy?: string
+ sortOrder?: string
+ view?: string
+ }) => (
+
+ {`Templates catalog: ${category}:${query}`}
+
),
}))
@@ -46,6 +62,48 @@ describe('embedded templates route', () => {
expect(screen.getByText('Templates catalog: marketing:')).toBeInTheDocument()
})
+ it('validates page, view and sort params at the route boundary', async () => {
+ const page = await TemplatesPage({
+ params: Promise.resolve({}),
+ searchParams: Promise.resolve({
+ page: '3',
+ q: 'agent',
+ sort_by: 'created_at',
+ sort_order: 'ASC',
+ view: 'search',
+ }),
+ })
+
+ render(page)
+
+ const catalog = screen.getByTestId('catalog')
+ expect(catalog).toHaveAttribute('data-page', '3')
+ expect(catalog).toHaveAttribute('data-sort-by', 'created_at')
+ expect(catalog).toHaveAttribute('data-sort-order', 'ASC')
+ expect(catalog).toHaveAttribute('data-view', 'search')
+ })
+
+ it('falls back to defaults for unsupported page, view and sort params', async () => {
+ const page = await TemplatesPage({
+ params: Promise.resolve({}),
+ searchParams: Promise.resolve({
+ page: '-2',
+ q: 'agent',
+ sort_by: 'garbage',
+ sort_order: 'sideways',
+ view: 'iframe',
+ }),
+ })
+
+ render(page)
+
+ const catalog = screen.getByTestId('catalog')
+ expect(catalog).toHaveAttribute('data-page', '1')
+ expect(catalog).not.toHaveAttribute('data-sort-by')
+ expect(catalog).not.toHaveAttribute('data-sort-order')
+ expect(catalog).not.toHaveAttribute('data-view')
+ })
+
it('opens template recommendations in the existing Dify import flow', async () => {
await expect(
TemplatesPage({
diff --git a/web/app/(commonLayout)/templates/[[...category]]/page.tsx b/web/app/(commonLayout)/templates/[[...category]]/page.tsx
index 3c02c067b95..08209f2a6a3 100644
--- a/web/app/(commonLayout)/templates/[[...category]]/page.tsx
+++ b/web/app/(commonLayout)/templates/[[...category]]/page.tsx
@@ -6,6 +6,7 @@ import { redirect } from '@/next/navigation'
type TemplatesPageProps = {
params: Promise<{ category?: string[] }>
searchParams: Promise<{
+ page?: string
q?: string
sort_by?: string
sort_order?: string
@@ -14,6 +15,26 @@ type TemplatesPageProps = {
}>
}
+// These values arrive from a public URL, so validate them against the
+// supported enums here at the route boundary. Unknown values fall back to the
+// defaults instead of reaching the Marketplace API, where e.g.
+// `sort_order=garbage` fails and would surface as a false "no templates" state.
+const TEMPLATE_SORT_FIELDS = new Set(['usage_count', 'created_at'])
+const TEMPLATE_SORT_ORDERS = new Set(['ASC', 'DESC'])
+
+const parseView = (value?: string) => (value === 'search' ? 'search' : undefined)
+
+const parseSortBy = (value?: string) =>
+ value && TEMPLATE_SORT_FIELDS.has(value) ? value : undefined
+
+const parseSortOrder = (value?: string) =>
+ value && TEMPLATE_SORT_ORDERS.has(value) ? value : undefined
+
+const parsePage = (value?: string) => {
+ const parsed = Number(value)
+ return Number.isInteger(parsed) && parsed >= 1 ? parsed : 1
+}
+
export default async function TemplatesPage({ params, searchParams }: TemplatesPageProps) {
const [resolvedParams, resolvedSearchParams, locale] = await Promise.all([
params,
@@ -36,10 +57,11 @@ export default async function TemplatesPage({ params, searchParams }: TemplatesP
)
diff --git a/web/app/components/plugins/marketplace/atoms.ts b/web/app/components/plugins/marketplace/atoms.ts
index df9c4ec9312..76a72cd2756 100644
--- a/web/app/components/plugins/marketplace/atoms.ts
+++ b/web/app/components/plugins/marketplace/atoms.ts
@@ -2,7 +2,7 @@ import type { PluginsSort, SearchParamsFromCollection } from '@dify/contracts/ma
import type { ActivePluginType } from './constants'
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
import { useQueryState } from 'nuqs'
-import { useCallback } from 'react'
+import { useCallback, useEffect } from 'react'
import { DEFAULT_SORT, PLUGIN_CATEGORY_WITH_COLLECTIONS } from './constants'
import { marketplaceSearchParamsParsers } from './search-params'
@@ -43,6 +43,21 @@ export function useMarketplaceSearchMode(activePluginTypeOverride?: ActivePlugin
return isSearchMode
}
+/**
+ * The forced search mode lives in the app-wide Jotai store, so a "View More"
+ * click would otherwise leak into the next visit of the plugin catalog after
+ * navigating away (e.g. to /templates) and back, rendering empty-query search
+ * results instead of the prefetched collections. Reset it when the catalog
+ * route mounts; URL-owned state (q, tags, category) is not affected.
+ */
+export function useResetMarketplaceSearchModeOnMount() {
+ const setSearchMode = useSetAtom(searchModeAtom)
+
+ useEffect(() => {
+ setSearchMode(null)
+ }, [setSearchMode])
+}
+
export function useMarketplaceMoreClick() {
const [, setQ] = useSearchPluginText()
const setSort = useSetAtom(marketplaceSortAtom)
diff --git a/web/app/components/plugins/marketplace/embedded.tsx b/web/app/components/plugins/marketplace/embedded.tsx
index 8ec53c559d4..d1797844c7c 100644
--- a/web/app/components/plugins/marketplace/embedded.tsx
+++ b/web/app/components/plugins/marketplace/embedded.tsx
@@ -5,6 +5,7 @@ import type { MarketplaceViewProps } from './view'
import { queryOptions, useQuery } from '@tanstack/react-query'
import { useLocale } from '@/context/i18n'
import { marketplaceQuery } from '@/service/client'
+import { useResetMarketplaceSearchModeOnMount } from './atoms'
import { fetchPluginBanners } from './home/banners'
import { MarketplaceView } from './view'
@@ -27,6 +28,7 @@ export function EmbeddedMarketplace({
variant = 'default',
...props
}: EmbeddedMarketplaceProps) {
+ useResetMarketplaceSearchModeOnMount()
const locale = useLocale()
const input = {
query: {
diff --git a/web/app/components/plugins/marketplace/home/__tests__/home-header.spec.tsx b/web/app/components/plugins/marketplace/home/__tests__/home-header.spec.tsx
index 3528a33769a..f47b027d39c 100644
--- a/web/app/components/plugins/marketplace/home/__tests__/home-header.spec.tsx
+++ b/web/app/components/plugins/marketplace/home/__tests__/home-header.spec.tsx
@@ -44,7 +44,7 @@ describe('HomeHeader', () => {
it('shows Creator Center before Guide', () => {
render()
- const creatorCenterLink = screen.getByRole('link', { name: 'Creator Center' })
+ const creatorCenterLink = screen.getByRole('link', { name: 'marketplace.home.creatorCenter' })
const guideLink = screen.getByRole('link', { name: 'marketplace.home.guide' })
expect(creatorCenterLink).toHaveAttribute('href', 'https://creators.dify.ai/')
@@ -63,7 +63,7 @@ describe('HomeHeader', () => {
render()
- expect(screen.getByRole('link', { name: 'Creator Center' })).toHaveAttribute(
+ expect(screen.getByRole('link', { name: 'marketplace.home.creatorCenter' })).toHaveAttribute(
'href',
'https://creators-staging.dify.dev/',
)
@@ -74,7 +74,7 @@ describe('HomeHeader', () => {
render()
- expect(screen.getByRole('link', { name: 'Creator Center' })).toHaveAttribute(
+ expect(screen.getByRole('link', { name: 'marketplace.home.creatorCenter' })).toHaveAttribute(
'href',
'https://creators.dify.ai/',
)
diff --git a/web/app/components/plugins/marketplace/home/__tests__/home-trending.spec.tsx b/web/app/components/plugins/marketplace/home/__tests__/home-trending.spec.tsx
index 2e247168d98..567f7bba550 100644
--- a/web/app/components/plugins/marketplace/home/__tests__/home-trending.spec.tsx
+++ b/web/app/components/plugins/marketplace/home/__tests__/home-trending.spec.tsx
@@ -121,7 +121,7 @@ describe('HomeTrending', () => {
expect(screen.getByRole('heading', { name: 'Dify v1.9 new launch' })).toBeInTheDocument()
const blogSlide = screen.getByRole('group', { name: 'Dify Updates' })
const blogLink = within(blogSlide).getByRole('link', {
- name: 'Read more about Dify v1.9 new launch',
+ name: 'plugin.marketplace.home.trendingReadMoreAbout',
})
expect(blogLink).toHaveAttribute('href', 'https://dify.ai/blog')
expect(within(blogSlide).getAllByRole('link')).toHaveLength(1)
@@ -365,6 +365,51 @@ describe('HomeTrending', () => {
})
})
+ it('resumes autoplay when Play is activated without moving keyboard focus', async () => {
+ const pause = vi.fn()
+ const play = vi.fn()
+ const progressAnimation = {
+ cancel: vi.fn(),
+ onfinish: null,
+ pause,
+ play,
+ } as unknown as Animation
+ const originalAnimate = Element.prototype.animate
+ Object.defineProperty(Element.prototype, 'animate', {
+ configurable: true,
+ value: vi.fn(() => progressAnimation),
+ })
+ const user = userEvent.setup()
+
+ render()
+
+ const toggleButton = screen.getByRole('button', {
+ name: 'plugin.marketplace.home.trendingPause',
+ })
+
+ // Focusing the toggle adds the implicit focus pause reason, then Enter
+ // adds the explicit user pause.
+ toggleButton.focus()
+ await user.keyboard('{Enter}')
+ expect(pause).toHaveBeenCalled()
+
+ // Play must resume the rotation even though the button is still focused
+ // (and would normally keep the focus pause reason active).
+ const playsBeforePlay = play.mock.calls.length
+ await user.keyboard('{Enter}')
+
+ expect(play.mock.calls.length).toBeGreaterThan(playsBeforePlay)
+ expect(document.activeElement).toBe(toggleButton)
+ expect(
+ screen.getByRole('button', { name: 'plugin.marketplace.home.trendingPause' }),
+ ).toBeInTheDocument()
+
+ Object.defineProperty(Element.prototype, 'animate', {
+ configurable: true,
+ value: originalAnimate,
+ })
+ })
+
it('renders no carousel when the API returns no banners', () => {
render()
diff --git a/web/app/components/plugins/marketplace/home/__tests__/marketplace-search-autocomplete.spec.tsx b/web/app/components/plugins/marketplace/home/__tests__/marketplace-search-autocomplete.spec.tsx
index 14a00e21c4f..55b8dd0857d 100644
--- a/web/app/components/plugins/marketplace/home/__tests__/marketplace-search-autocomplete.spec.tsx
+++ b/web/app/components/plugins/marketplace/home/__tests__/marketplace-search-autocomplete.spec.tsx
@@ -3,13 +3,16 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useState } from 'react'
-import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
import {
MarketplaceSearchAutocomplete,
MarketplaceSearchForm,
} from '../marketplace-search-autocomplete'
-const { mockPluginSearch, mockTemplateSearch } = vi.hoisted(() => ({
+const { debounceState, mockPluginSearch, mockTemplateSearch } = vi.hoisted(() => ({
+ // Most tests bypass the debounce for simplicity; the debounce-window test
+ // flips this on to exercise the real 300ms lag.
+ debounceState: { useRealDebounce: false },
mockPluginSearch: vi.fn(),
mockTemplateSearch: vi.fn(),
}))
@@ -19,7 +22,8 @@ vi.mock('ahooks', async (importOriginal) => {
return {
...original,
- useDebounce: (value: T) => value,
+ useDebounce: (value: T, options?: { wait?: number }) =>
+ debounceState.useRealDebounce ? original.useDebounce(value, options) : value,
}
})
@@ -60,6 +64,7 @@ function Wrapper({ children }: { children: ReactNode }) {
describe('MarketplaceSearchAutocomplete', () => {
beforeEach(() => {
vi.clearAllMocks()
+ debounceState.useRealDebounce = false
queryClient = new QueryClient({
defaultOptions: {
queries: {
@@ -219,4 +224,51 @@ describe('MarketplaceSearchAutocomplete', () => {
expect(screen.queryByText('Google Search')).not.toBeInTheDocument()
expect(screen.getByRole('status')).toHaveTextContent('Searching...')
})
+
+ it('clears suggestions while the edited value is still debouncing', async () => {
+ debounceState.useRealDebounce = true
+ mockPluginSearch.mockResolvedValue({
+ data: {
+ plugins: [
+ {
+ type: 'plugin',
+ org: 'langgenius',
+ name: 'google-search',
+ label: { en_US: 'Google Search' },
+ brief: { en_US: 'Search the web from your workflow.' },
+ category: 'tool',
+ },
+ ],
+ total: 1,
+ },
+ })
+ const user = userEvent.setup()
+
+ const ControlledSearch = () => {
+ const [value, setValue] = useState('')
+
+ return (
+
+ )
+ }
+
+ render(, { wrapper: Wrapper })
+
+ // Suggestions only appear once the real 300ms debounce has elapsed.
+ await user.type(screen.getByRole('combobox'), 'google')
+ expect(await screen.findByText('Google Search')).toBeInTheDocument()
+
+ // For the first 300ms after editing, the debounced term still points at
+ // the old query; the previous suggestions must already be gone.
+ await user.type(screen.getByRole('combobox'), ' drive')
+
+ expect(screen.queryByText('Google Search')).not.toBeInTheDocument()
+ expect(screen.getByRole('status')).toHaveTextContent('Searching...')
+ })
})
diff --git a/web/app/components/plugins/marketplace/home/home-header.tsx b/web/app/components/plugins/marketplace/home/home-header.tsx
index eb8ca932f9e..c2c9a7c59d6 100644
--- a/web/app/components/plugins/marketplace/home/home-header.tsx
+++ b/web/app/components/plugins/marketplace/home/home-header.tsx
@@ -1,6 +1,7 @@
import type { HomeCatalogTab, HomeCatalogTabLabels } from './home-catalog-tabs'
import { buttonVariants } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
+import { useTranslation } from '#i18n'
import { MARKETPLACE_URL_PREFIX } from '@/config'
import Link from '@/next/link'
import MarketplaceLogoDark from '@/public/marketplace/dify-marketplace-logo-dark.svg'
@@ -39,20 +40,25 @@ const getCreatorCenterUrl = (marketplaceUrlPrefix: string) => {
}
const CreatorCenter = () => {
+ const { t } = useTranslation('plugin')
const creatorCenterUrl = getCreatorCenterUrl(MARKETPLACE_URL_PREFIX)
+ const label = t(($) => $['marketplace.home.creatorCenter'])
return (
- Creator Center
+ {label}
)
}
diff --git a/web/app/components/plugins/marketplace/home/home-trending.tsx b/web/app/components/plugins/marketplace/home/home-trending.tsx
index 08281451fa9..7768d749466 100644
--- a/web/app/components/plugins/marketplace/home/home-trending.tsx
+++ b/web/app/components/plugins/marketplace/home/home-trending.tsx
@@ -231,6 +231,7 @@ function TrendingRecommendationSlide({
}
function BlogBannerSlide({ banner }: { banner: BannerBlog }) {
+ const { t } = useTranslation('plugin')
const opensInNewTab = /^https?:\/\//.test(banner.content.link)
return (
@@ -238,7 +239,9 @@ function BlogBannerSlide({ banner }: { banner: BannerBlog }) {
href={banner.content.link}
target={opensInNewTab ? '_blank' : undefined}
rel={opensInNewTab ? 'noopener noreferrer' : undefined}
- aria-label={`Read more about ${banner.content.blog_title}`}
+ aria-label={t(($) => $['marketplace.home.trendingReadMoreAbout'], {
+ title: banner.content.blog_title,
+ })}
className="flex h-[200px] w-full overflow-hidden rounded-2xl bg-background-body outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid"
>
@@ -265,7 +268,7 @@ function BlogBannerSlide({ banner }: { banner: BannerBlog }) {
aria-hidden
className="flex shrink-0 items-center gap-1 text-[13px] leading-[normal] font-medium text-text-accent underline decoration-[10%] underline-offset-2"
>
- Read more
+ {t(($) => $['marketplace.home.trendingReadMore'])}
@@ -480,6 +483,12 @@ function TrendingNavigation({
setIsReducedMotionPaused(false)
setPauseReason('user', false)
setPauseReason('reduced-motion', false)
+ // Activating Play keeps the pointer and/or keyboard focus on the button
+ // itself, so the implicit hover/focus reasons would silently keep the
+ // rotation paused. An explicit Play overrides them; they re-engage on
+ // the next mouseenter/focusin.
+ setPauseReason('focus', false)
+ setPauseReason('hover', false)
return
}
diff --git a/web/app/components/plugins/marketplace/home/marketplace-search-autocomplete.tsx b/web/app/components/plugins/marketplace/home/marketplace-search-autocomplete.tsx
index 106ec46559d..0c29a73f070 100644
--- a/web/app/components/plugins/marketplace/home/marketplace-search-autocomplete.tsx
+++ b/web/app/components/plugins/marketplace/home/marketplace-search-autocomplete.tsx
@@ -121,16 +121,22 @@ export function MarketplaceSearchAutocomplete({
enabled: hasQuery && searchesTemplates,
staleTime: 60_000,
})
- const pluginSuggestions = searchesPlugins
- ? (pluginQuery.data?.data.bundles ?? pluginQuery.data?.data.plugins ?? []).map((plugin) =>
- toPluginSuggestion(plugin, locale),
- )
- : []
- const templateSuggestions = searchesTemplates
- ? (templateQuery.data?.data?.templates ?? []).map(toTemplateSuggestion)
- : []
+ // While the edited value is still debouncing, the queries above still hold
+ // the previous term's data; gate the suggestions until both agree so stale
+ // options are never visible or keyboard-selectable.
+ const isDebouncing = value.trim() !== debouncedSearch
+ const pluginSuggestions =
+ !isDebouncing && searchesPlugins
+ ? (pluginQuery.data?.data.bundles ?? pluginQuery.data?.data.plugins ?? []).map((plugin) =>
+ toPluginSuggestion(plugin, locale),
+ )
+ : []
+ const templateSuggestions =
+ !isDebouncing && searchesTemplates
+ ? (templateQuery.data?.data?.templates ?? []).map(toTemplateSuggestion)
+ : []
const suggestions = [...templateSuggestions, ...pluginSuggestions]
- const isSearching = pluginQuery.isFetching || templateQuery.isFetching
+ const isSearching = isDebouncing || pluginQuery.isFetching || templateQuery.isFetching
const emptyText =
scope === 'templates'
? translate('newApp.noTemplateFound', { ns: 'app' })
diff --git a/web/app/components/plugins/marketplace/list/__tests__/carousel.spec.tsx b/web/app/components/plugins/marketplace/list/__tests__/carousel.spec.tsx
index 486ef300543..f1372fe28a5 100644
--- a/web/app/components/plugins/marketplace/list/__tests__/carousel.spec.tsx
+++ b/web/app/components/plugins/marketplace/list/__tests__/carousel.spec.tsx
@@ -232,7 +232,7 @@ describe('Marketplace Carousel', () => {
expect(autoplay.play).toHaveBeenCalledTimes(2)
reducedMotion = true
- reducedMotionListener?.()
+ act(() => reducedMotionListener?.())
expect(autoplay.stop).toHaveBeenCalled()
Object.defineProperty(document, 'visibilityState', {
@@ -248,7 +248,7 @@ describe('Marketplace Carousel', () => {
expect(autoplay.play).toHaveBeenCalledTimes(2)
reducedMotion = false
- reducedMotionListener?.()
+ act(() => reducedMotionListener?.())
expect(autoplay.play).toHaveBeenCalledTimes(3)
triggerIntersection(intersectionObservers[0]!, 0)
@@ -268,6 +268,38 @@ describe('Marketplace Carousel', () => {
expect(intersectionObservers).toHaveLength(0)
})
+ it('honors reduced motion for the eagerly playing first-collection carousel', () => {
+ let reducedMotion = true
+ let reducedMotionListener: (() => void) | undefined
+ vi.stubGlobal('matchMedia', () => ({
+ get matches() {
+ return reducedMotion
+ },
+ media: '(prefers-reduced-motion: reduce)',
+ onchange: null,
+ addEventListener: (_event: string, listener: () => void) => {
+ reducedMotionListener = listener
+ },
+ removeEventListener: vi.fn(),
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ }))
+
+ // The production first collection renders without pauseWhenOffscreen, so
+ // the reduced-motion guard must work outside the viewport-managed path.
+ render()
+ const autoplay = mocks.autoplayInstances[0]!
+
+ expect(autoplay.stop).toHaveBeenCalled()
+ expect(autoplay.play).not.toHaveBeenCalled()
+
+ reducedMotion = false
+ act(() => reducedMotionListener?.())
+
+ expect(autoplay.play).toHaveBeenCalled()
+ })
+
it('keeps off-screen pages out of the tab order and accessibility tree', () => {
render()
diff --git a/web/app/components/plugins/marketplace/list/carousel.tsx b/web/app/components/plugins/marketplace/list/carousel.tsx
index 7e05a886745..c0dd5a518d0 100644
--- a/web/app/components/plugins/marketplace/list/carousel.tsx
+++ b/web/app/components/plugins/marketplace/list/carousel.tsx
@@ -168,6 +168,13 @@ const Carousel = ({
}: CarouselProps) => {
const carouselRootRef = useRef(null)
const [isUserPaused, setIsUserPaused] = useState(false)
+ // Tracked independently of pauseWhenOffscreen so every autoplay path honors
+ // prefers-reduced-motion, including the eagerly-playing first collection.
+ const [isReducedMotion, setIsReducedMotion] = useState(
+ () =>
+ typeof window !== 'undefined' &&
+ (window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ?? false),
+ )
const autoplay = useMemo(() => {
if (!autoPlay) return undefined
@@ -264,12 +271,28 @@ const Carousel = ({
return () => carouselRoot.removeEventListener('focusin', handleFocusIn)
}, [autoplay])
+ // The viewport-managed effect below tracks reduced motion itself; this
+ // effect covers the eager autoplay path (pauseWhenOffscreen=false), which
+ // previously ignored the preference entirely.
+ useEffect(() => {
+ if (!autoPlay || pauseWhenOffscreen) return
+
+ const reducedMotionQuery = window.matchMedia?.('(prefers-reduced-motion: reduce)')
+ if (!reducedMotionQuery) return
+
+ const syncReducedMotion = () => setIsReducedMotion(reducedMotionQuery.matches)
+
+ syncReducedMotion()
+ reducedMotionQuery.addEventListener('change', syncReducedMotion)
+ return () => reducedMotionQuery.removeEventListener('change', syncReducedMotion)
+ }, [autoPlay, pauseWhenOffscreen])
+
useEffect(() => {
if (!autoplay || !api || pauseWhenOffscreen) return
- if (isUserPaused) autoplay.stop()
+ if (isUserPaused || isReducedMotion) autoplay.stop()
else autoplay.play()
- }, [api, autoplay, isUserPaused, pauseWhenOffscreen])
+ }, [api, autoplay, isReducedMotion, isUserPaused, pauseWhenOffscreen])
useEffect(() => {
if (!pauseWhenOffscreen || !autoplay || !api) return
diff --git a/web/app/components/plugins/marketplace/templates/__tests__/template-language.spec.ts b/web/app/components/plugins/marketplace/templates/__tests__/template-language.spec.ts
new file mode 100644
index 00000000000..f4160836db6
--- /dev/null
+++ b/web/app/components/plugins/marketplace/templates/__tests__/template-language.spec.ts
@@ -0,0 +1,68 @@
+import { describe, expect, it } from 'vite-plus/test'
+import { filterTemplatesForLocale } from '../template-language'
+
+const template = (id: string, preferredLanguages?: string[]) => ({
+ id,
+ preferred_languages: preferredLanguages,
+})
+
+const ids = (templates: { id: string }[]) => templates.map(({ id }) => id)
+
+describe('filterTemplatesForLocale', () => {
+ it('keeps templates matching the requested language prefix', () => {
+ const templates = [
+ template('en', ['en-US']),
+ template('zh', ['zh-Hans']),
+ template('ja', ['ja-JP']),
+ ]
+
+ expect(ids(filterTemplatesForLocale(templates, 'zh-Hans'))).toEqual(['zh'])
+ expect(ids(filterTemplatesForLocale(templates, 'en-US'))).toEqual(['en'])
+ })
+
+ it('matches unrelated locales instead of collapsing them into "other"', () => {
+ const templates = [
+ template('en', ['en-US']),
+ template('de', ['de-DE']),
+ template('fr', ['fr-FR']),
+ ]
+
+ expect(ids(filterTemplatesForLocale(templates, 'de-DE'))).toEqual(['de'])
+ })
+
+ it('falls back to English templates when nothing matches the requested language', () => {
+ const templates = [
+ template('en-1', ['en-US']),
+ template('en-2', ['en-GB']),
+ template('ja', ['ja-JP']),
+ ]
+
+ expect(ids(filterTemplatesForLocale(templates, 'de-DE'))).toEqual(['en-1', 'en-2'])
+ })
+
+ it('falls back to the unfiltered list when neither the locale nor English matches', () => {
+ const templates = [template('zh', ['zh-Hans']), template('ja', ['ja-JP'])]
+
+ expect(ids(filterTemplatesForLocale(templates, 'de-DE'))).toEqual(['zh', 'ja'])
+ })
+
+ it('always keeps language-agnostic templates', () => {
+ const templates = [
+ template('agnostic-none'),
+ template('agnostic-empty', []),
+ template('de', ['de-DE']),
+ ]
+
+ expect(ids(filterTemplatesForLocale(templates, 'de-DE'))).toEqual([
+ 'agnostic-none',
+ 'agnostic-empty',
+ 'de',
+ ])
+ })
+
+ it('normalizes underscore locales', () => {
+ const templates = [template('zh', ['zh_Hans']), template('en', ['en_US'])]
+
+ expect(ids(filterTemplatesForLocale(templates, 'zh_Hans'))).toEqual(['zh'])
+ })
+})
diff --git a/web/app/components/plugins/marketplace/templates/index.tsx b/web/app/components/plugins/marketplace/templates/index.tsx
index 96a9c9c6a2c..343c2674642 100644
--- a/web/app/components/plugins/marketplace/templates/index.tsx
+++ b/web/app/components/plugins/marketplace/templates/index.tsx
@@ -8,6 +8,7 @@ import Link from '@/next/link'
import {
getMarketplaceTemplateCollectionsAndTemplates,
searchMarketplaceTemplates,
+ TEMPLATE_SEARCH_PAGE_SIZE,
} from '@/service/marketplace-template-discovery'
import { fetchPluginBanners } from '../home/banners'
import HomeCatalogNavigation from '../home/home-catalog-navigation'
@@ -29,6 +30,7 @@ import { filterTemplatesForLocale } from './template-language'
type EmbeddedTemplatesMarketplaceProps = {
category: TemplateCategory
locale: Locale
+ page?: number
query: string
sortBy?: string
sortOrder?: string
@@ -103,9 +105,81 @@ function TemplateGrid({
)
}
+const PAGE_LINK_CLASS =
+ 'flex h-8 items-center justify-center rounded-lg border-[0.5px] border-divider-regular px-3 system-sm-medium text-text-secondary outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid'
+const PAGE_LINK_DISABLED_CLASS =
+ 'flex h-8 cursor-not-allowed items-center justify-center rounded-lg border-[0.5px] border-divider-subtle px-3 system-sm-medium text-text-quaternary'
+
+// Server-rendered pagination: plain links keep the search results reachable
+// beyond the first page without any client-side state.
+function TemplatePagination({
+ category,
+ navigationLabel,
+ nextLabel,
+ page,
+ pageCount,
+ previousLabel,
+ query,
+ sortBy,
+ sortOrder,
+ view,
+}: {
+ category: TemplateCategory
+ navigationLabel: string
+ nextLabel: string
+ page: number
+ pageCount: number
+ previousLabel: string
+ query: string
+ sortBy?: string
+ sortOrder?: string
+ view?: string
+}) {
+ if (pageCount <= 1) return null
+
+ const buildHref = (targetPage: number) => {
+ const searchParams = new URLSearchParams()
+ if (query) searchParams.set('q', query)
+ if (sortBy) searchParams.set('sort_by', sortBy)
+ if (sortOrder) searchParams.set('sort_order', sortOrder)
+ if (view) searchParams.set('view', view)
+ if (targetPage > 1) searchParams.set('page', String(targetPage))
+ const queryString = searchParams.toString()
+ const basePath = category === 'all' ? '/templates' : `/templates/${category}`
+ return queryString ? `${basePath}?${queryString}` : basePath
+ }
+
+ return (
+
+ )
+}
+
export async function EmbeddedTemplatesMarketplace({
category,
locale,
+ page = 1,
query,
sortBy,
sortOrder,
@@ -118,6 +192,7 @@ export async function EmbeddedTemplatesMarketplace({
{ t: tApp },
{ t: tExplore },
{ t: tPluginTags },
+ { t: tCommon },
collectionsResult,
searchResult,
banners,
@@ -126,11 +201,13 @@ export async function EmbeddedTemplatesMarketplace({
getTranslation(locale, 'app'),
getTranslation(locale, 'explore'),
getTranslation(locale, 'pluginTags'),
+ getTranslation(locale, 'common'),
showCollections ? getMarketplaceTemplateCollectionsAndTemplates() : Promise.resolve(null),
showCollections
? Promise.resolve(null)
: searchMarketplaceTemplates({
category,
+ page,
query: normalizedQuery,
sortBy,
sortOrder,
@@ -214,7 +291,9 @@ export async function EmbeddedTemplatesMarketplace({
/>
}
/>
- {tApp('newApp.noTemplateFound' as never)}
)}
+
>
)}
-
+
diff --git a/web/app/components/plugins/marketplace/templates/template-language.ts b/web/app/components/plugins/marketplace/templates/template-language.ts
index fc2b293eeca..bc82613fed2 100644
--- a/web/app/components/plugins/marketplace/templates/template-language.ts
+++ b/web/app/components/plugins/marketplace/templates/template-language.ts
@@ -1,36 +1,33 @@
import type { MarketplaceTemplate } from '@dify/contracts/marketplace'
-type TemplateLanguageFamily = 'en' | 'ja' | 'other' | 'zh'
-
-function getTemplateLanguageFamily(locale: string): TemplateLanguageFamily {
- const normalizedLocale = locale.toLowerCase()
-
- if (normalizedLocale.startsWith('en')) return 'en'
- if (normalizedLocale.startsWith('zh')) return 'zh'
- if (normalizedLocale.startsWith('ja')) return 'ja'
-
- return 'other'
-}
+const getLanguagePrefix = (locale: string) => locale.toLowerCase().split(/[-_]/)[0] ?? ''
+/**
+ * Keeps the templates matching the requested locale's language. Templates
+ * without language metadata are treated as language-agnostic and always kept.
+ * When no template matches the requested language, the list explicitly falls
+ * back to English templates (and finally to the unfiltered list) so locales
+ * such as German render real content instead of an empty state.
+ */
export function filterTemplatesForLocale<
T extends Pick,
>(templates: T[], locale: string) {
- const languageFamily = getTemplateLanguageFamily(locale)
+ const requestedLanguage = getLanguagePrefix(locale)
- return templates.filter((template) => {
- const preferredLanguages = (template.preferred_languages ?? []).map((language) =>
- language.toLowerCase(),
- )
+ const filterByLanguage = (languagePrefix: string) =>
+ templates.filter((template) => {
+ const preferredLanguages = template.preferred_languages ?? []
+ if (preferredLanguages.length === 0) return true
+ return preferredLanguages.some((language) => getLanguagePrefix(language) === languagePrefix)
+ })
- if (languageFamily === 'other') {
- return !preferredLanguages.some(
- (language) =>
- language.startsWith('en') || language.startsWith('zh') || language.startsWith('ja'),
- )
- }
+ const requestedMatches = filterByLanguage(requestedLanguage)
+ if (requestedMatches.length > 0) return requestedMatches
- return preferredLanguages.some((language) => language.startsWith(languageFamily))
- })
+ const englishMatches = requestedLanguage === 'en' ? [] : filterByLanguage('en')
+ if (englishMatches.length > 0) return englishMatches
+
+ return templates
}
export function getTemplateCollectionText(value: Record, locale: string) {
diff --git a/web/i18n/ar-TN/plugin.json b/web/i18n/ar-TN/plugin.json
index abfcee07c34..d09ca304cc3 100644
--- a/web/i18n/ar-TN/plugin.json
+++ b/web/i18n/ar-TN/plugin.json
@@ -227,6 +227,7 @@
"marketplace.difyMarketplace": "سوق Dify",
"marketplace.discover": "اكتشف",
"marketplace.empower": "تمكين تطوير الذكاء الاصطناعي الخاص بك",
+ "marketplace.home.creatorCenter": "مركز المبدعين",
"marketplace.home.guide": "دليل",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "اكتشف. وسّع. ابنِ",
@@ -238,7 +239,12 @@
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
+ "marketplace.home.trendingPause": "إيقاف مؤقت",
+ "marketplace.home.trendingPlay": "تشغيل",
+ "marketplace.home.trendingReadMore": "اقرأ المزيد",
+ "marketplace.home.trendingReadMoreAbout": "اقرأ المزيد عن {{title}}",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
+ "marketplace.home.trendingView": "عرض",
"marketplace.moreFrom": "المزيد من السوق",
"marketplace.noPluginFound": "لم يتم العثور على إضافة",
"marketplace.partnerTip": "تم التحقق بواسطة شريك Dify",
diff --git a/web/i18n/de-DE/plugin.json b/web/i18n/de-DE/plugin.json
index 4808d3d0a79..71a5b08f3d9 100644
--- a/web/i18n/de-DE/plugin.json
+++ b/web/i18n/de-DE/plugin.json
@@ -227,6 +227,7 @@
"marketplace.difyMarketplace": "Dify Marktplatz",
"marketplace.discover": "Entdecken",
"marketplace.empower": "Unterstützen Sie Ihre KI-Entwicklung",
+ "marketplace.home.creatorCenter": "Creator Center",
"marketplace.home.guide": "Leitfaden",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Entdecken. Erweitern. Entwickeln",
@@ -238,7 +239,12 @@
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
+ "marketplace.home.trendingPause": "Pausieren",
+ "marketplace.home.trendingPlay": "Abspielen",
+ "marketplace.home.trendingReadMore": "Mehr erfahren",
+ "marketplace.home.trendingReadMoreAbout": "Mehr erfahren über {{title}}",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
+ "marketplace.home.trendingView": "Ansehen",
"marketplace.moreFrom": "Mehr aus dem Marketplace",
"marketplace.noPluginFound": "Kein Plugin gefunden",
"marketplace.partnerTip": "Von einem Dify-Partner verifiziert",
diff --git a/web/i18n/en-US/plugin.json b/web/i18n/en-US/plugin.json
index d00be8ff63f..73056b3e335 100644
--- a/web/i18n/en-US/plugin.json
+++ b/web/i18n/en-US/plugin.json
@@ -227,6 +227,7 @@
"marketplace.difyMarketplace": "Dify Marketplace",
"marketplace.discover": "Discover",
"marketplace.empower": "Empower your AI development",
+ "marketplace.home.creatorCenter": "Creator Center",
"marketplace.home.guide": "Guide",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Discover. Extend. Build",
@@ -240,6 +241,8 @@
"marketplace.home.trendingPaginationLabel": "Trending pages",
"marketplace.home.trendingPause": "Pause",
"marketplace.home.trendingPlay": "Play",
+ "marketplace.home.trendingReadMore": "Read more",
+ "marketplace.home.trendingReadMoreAbout": "Read more about {{title}}",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
"marketplace.home.trendingView": "View",
"marketplace.moreFrom": "More from Marketplace",
diff --git a/web/i18n/es-ES/plugin.json b/web/i18n/es-ES/plugin.json
index 7b3bf805585..f38732b37f7 100644
--- a/web/i18n/es-ES/plugin.json
+++ b/web/i18n/es-ES/plugin.json
@@ -227,6 +227,7 @@
"marketplace.difyMarketplace": "Mercado de Dify",
"marketplace.discover": "Descubrir",
"marketplace.empower": "Potencie su desarrollo de IA",
+ "marketplace.home.creatorCenter": "Centro de creadores",
"marketplace.home.guide": "Guía",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Descubre. Amplía. Crea",
@@ -238,7 +239,12 @@
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
+ "marketplace.home.trendingPause": "Pausar",
+ "marketplace.home.trendingPlay": "Reproducir",
+ "marketplace.home.trendingReadMore": "Leer más",
+ "marketplace.home.trendingReadMoreAbout": "Leer más sobre {{title}}",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
+ "marketplace.home.trendingView": "Ver",
"marketplace.moreFrom": "Más de Marketplace",
"marketplace.noPluginFound": "No se ha encontrado ninguna integración",
"marketplace.partnerTip": "Verificado por un socio de Dify",
diff --git a/web/i18n/fa-IR/plugin.json b/web/i18n/fa-IR/plugin.json
index f2c5d292b3b..ec15f1e42bd 100644
--- a/web/i18n/fa-IR/plugin.json
+++ b/web/i18n/fa-IR/plugin.json
@@ -227,6 +227,7 @@
"marketplace.difyMarketplace": "بازار دیفی",
"marketplace.discover": "کشف",
"marketplace.empower": "توسعه هوش مصنوعی خود را توانمند کنید",
+ "marketplace.home.creatorCenter": "مرکز سازندگان",
"marketplace.home.guide": "راهنما",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "کشف کنید. گسترش دهید. بسازید",
@@ -238,7 +239,12 @@
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
+ "marketplace.home.trendingPause": "توقف",
+ "marketplace.home.trendingPlay": "پخش",
+ "marketplace.home.trendingReadMore": "ادامه مطلب",
+ "marketplace.home.trendingReadMoreAbout": "ادامه مطلب درباره {{title}}",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
+ "marketplace.home.trendingView": "مشاهده",
"marketplace.moreFrom": "اطلاعات بیشتر از Marketplace",
"marketplace.noPluginFound": "هیچ افزونهای یافت نشد",
"marketplace.partnerTip": "تأیید شده توسط یک شریک دیفی",
diff --git a/web/i18n/fr-FR/plugin.json b/web/i18n/fr-FR/plugin.json
index 6adaabdf09f..7e39144d228 100644
--- a/web/i18n/fr-FR/plugin.json
+++ b/web/i18n/fr-FR/plugin.json
@@ -227,6 +227,7 @@
"marketplace.difyMarketplace": "Marché Dify",
"marketplace.discover": "Découvrir",
"marketplace.empower": "Renforcez le développement de votre IA",
+ "marketplace.home.creatorCenter": "Centre des créateurs",
"marketplace.home.guide": "Guide",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Découvrez. Étendez. Créez",
@@ -238,7 +239,12 @@
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
+ "marketplace.home.trendingPause": "Mettre en pause",
+ "marketplace.home.trendingPlay": "Lire",
+ "marketplace.home.trendingReadMore": "En savoir plus",
+ "marketplace.home.trendingReadMoreAbout": "En savoir plus sur {{title}}",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
+ "marketplace.home.trendingView": "Voir",
"marketplace.moreFrom": "Plus de Marketplace",
"marketplace.noPluginFound": "Aucune intégration trouvée",
"marketplace.partnerTip": "Vérifié par un partenaire Dify",
diff --git a/web/i18n/hi-IN/plugin.json b/web/i18n/hi-IN/plugin.json
index 76e77fc3bc8..385ceabe507 100644
--- a/web/i18n/hi-IN/plugin.json
+++ b/web/i18n/hi-IN/plugin.json
@@ -227,6 +227,7 @@
"marketplace.difyMarketplace": "डिफाई मार्केटप्लेस",
"marketplace.discover": "खोजें",
"marketplace.empower": "अपने एआई विकास को सशक्त बनाएं",
+ "marketplace.home.creatorCenter": "क्रिएटर केंद्र",
"marketplace.home.guide": "मार्गदर्शिका",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "खोजें। विस्तार करें। बनाएँ",
@@ -238,7 +239,12 @@
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
+ "marketplace.home.trendingPause": "रोकें",
+ "marketplace.home.trendingPlay": "चलाएं",
+ "marketplace.home.trendingReadMore": "और पढ़ें",
+ "marketplace.home.trendingReadMoreAbout": "{{title}} के बारे में और पढ़ें",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
+ "marketplace.home.trendingView": "देखें",
"marketplace.moreFrom": "मार्केटप्लेस से अधिक",
"marketplace.noPluginFound": "कोई इंटीग्रेशन नहीं मिला",
"marketplace.partnerTip": "Dify भागीदार द्वारा सत्यापित",
diff --git a/web/i18n/id-ID/plugin.json b/web/i18n/id-ID/plugin.json
index ca3b6022f7c..9b7e8d2edb5 100644
--- a/web/i18n/id-ID/plugin.json
+++ b/web/i18n/id-ID/plugin.json
@@ -227,6 +227,7 @@
"marketplace.difyMarketplace": "Dify Marketplace",
"marketplace.discover": "Menemukan",
"marketplace.empower": "Berdayakan pengembangan AI Anda",
+ "marketplace.home.creatorCenter": "Pusat Kreator",
"marketplace.home.guide": "Panduan",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Temukan. Perluas. Bangun",
@@ -238,7 +239,12 @@
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
+ "marketplace.home.trendingPause": "Jeda",
+ "marketplace.home.trendingPlay": "Putar",
+ "marketplace.home.trendingReadMore": "Baca selengkapnya",
+ "marketplace.home.trendingReadMoreAbout": "Baca selengkapnya tentang {{title}}",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
+ "marketplace.home.trendingView": "Lihat",
"marketplace.moreFrom": "Selengkapnya dari Marketplace",
"marketplace.noPluginFound": "Tidak ada integrasi yang ditemukan",
"marketplace.partnerTip": "Diverifikasi oleh partner Dify",
diff --git a/web/i18n/it-IT/plugin.json b/web/i18n/it-IT/plugin.json
index 2a21059b0ce..c4507ddc7c4 100644
--- a/web/i18n/it-IT/plugin.json
+++ b/web/i18n/it-IT/plugin.json
@@ -227,6 +227,7 @@
"marketplace.difyMarketplace": "Mercato Dify",
"marketplace.discover": "Scoprire",
"marketplace.empower": "Potenzia lo sviluppo dell'intelligenza artificiale",
+ "marketplace.home.creatorCenter": "Centro creatori",
"marketplace.home.guide": "Guida",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Scopri. Estendi. Crea",
@@ -238,7 +239,12 @@
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
+ "marketplace.home.trendingPause": "Pausa",
+ "marketplace.home.trendingPlay": "Riproduci",
+ "marketplace.home.trendingReadMore": "Scopri di più",
+ "marketplace.home.trendingReadMoreAbout": "Scopri di più su {{title}}",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
+ "marketplace.home.trendingView": "Visualizza",
"marketplace.moreFrom": "Altro da Marketplace",
"marketplace.noPluginFound": "Nessuna integrazione trovata",
"marketplace.partnerTip": "Verificato da un partner Dify",
diff --git a/web/i18n/ja-JP/plugin.json b/web/i18n/ja-JP/plugin.json
index b947ba744a0..0d24a6a3a26 100644
--- a/web/i18n/ja-JP/plugin.json
+++ b/web/i18n/ja-JP/plugin.json
@@ -227,6 +227,7 @@
"marketplace.difyMarketplace": "Dify マーケットプレイス",
"marketplace.discover": "探索",
"marketplace.empower": "AI 開発をサポートする",
+ "marketplace.home.creatorCenter": "クリエイターセンター",
"marketplace.home.guide": "ガイド",
"marketplace.home.heroSubtitle": "Dify Marketplace で、より安全で信頼性の高いプラグインを見つけましょう。",
"marketplace.home.heroTitle": "見つける。拡張する。構築する",
@@ -238,7 +239,12 @@
"marketplace.home.trendingDescription": "実際の利用状況に基づく人気プラグインを2週間ごとに更新。ワークスペースでの実行数によるランキングで、有料掲載や編集部による選定はありません。",
"marketplace.home.trendingEyebrow": "トレンド",
"marketplace.home.trendingPaginationLabel": "トレンドページ",
+ "marketplace.home.trendingPause": "一時停止",
+ "marketplace.home.trendingPlay": "再生",
+ "marketplace.home.trendingReadMore": "続きを読む",
+ "marketplace.home.trendingReadMoreAbout": "{{title}} の続きを読む",
"marketplace.home.trendingTitle": "みんながインストールしているプラグイン",
+ "marketplace.home.trendingView": "表示",
"marketplace.moreFrom": "マーケットプレイスからのさらなる情報",
"marketplace.noPluginFound": "インテグレーションが見つかりません",
"marketplace.partnerTip": "このプラグインは Dify のパートナーによって認証されています",
diff --git a/web/i18n/ko-KR/plugin.json b/web/i18n/ko-KR/plugin.json
index dadc012952b..334b3b3ddc7 100644
--- a/web/i18n/ko-KR/plugin.json
+++ b/web/i18n/ko-KR/plugin.json
@@ -227,6 +227,7 @@
"marketplace.difyMarketplace": "Dify 마켓플레이스",
"marketplace.discover": "발견하다",
"marketplace.empower": "AI 개발 역량 강화",
+ "marketplace.home.creatorCenter": "크리에이터 센터",
"marketplace.home.guide": "가이드",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "발견하고, 확장하고, 구축하세요",
@@ -238,7 +239,12 @@
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
+ "marketplace.home.trendingPause": "일시정지",
+ "marketplace.home.trendingPlay": "재생",
+ "marketplace.home.trendingReadMore": "더 알아보기",
+ "marketplace.home.trendingReadMoreAbout": "{{title}}에 대해 더 알아보기",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
+ "marketplace.home.trendingView": "보기",
"marketplace.moreFrom": "Marketplace 에서 더 보기",
"marketplace.noPluginFound": "플러그인을 찾을 수 없습니다.",
"marketplace.partnerTip": "Dify 파트너에 의해 확인됨",
diff --git a/web/i18n/lo-LA/plugin.json b/web/i18n/lo-LA/plugin.json
index e414a330d68..858e26c2b5f 100644
--- a/web/i18n/lo-LA/plugin.json
+++ b/web/i18n/lo-LA/plugin.json
@@ -227,6 +227,12 @@
"marketplace.difyMarketplace": "Dify Marketplace",
"marketplace.discover": "ຄົ້ນຫາ",
"marketplace.empower": "ເສີມພະລັງການພັດທະນາ AI ຂອງທ່ານ",
+ "marketplace.home.creatorCenter": "ສູນຜູ້ສ້າງ",
+ "marketplace.home.trendingPause": "ຢຸດຊົ່ວຄາວ",
+ "marketplace.home.trendingPlay": "ຫຼິ້ນ",
+ "marketplace.home.trendingReadMore": "ອ່ານເພີ່ມເຕີມ",
+ "marketplace.home.trendingReadMoreAbout": "ອ່ານເພີ່ມເຕີມກ່ຽວກັບ {{title}}",
+ "marketplace.home.trendingView": "ເບິ່ງ",
"marketplace.moreFrom": "ເພີ່ມເຕີມຈາກ Marketplace",
"marketplace.noPluginFound": "ບໍ່ພົບການເຊື່ອມຕໍ່",
"marketplace.partnerTip": "ໄດ້ຮັບການຢືນຢັນໂດຍພັດທະນາມິດຂອງ Dify",
diff --git a/web/i18n/nl-NL/plugin.json b/web/i18n/nl-NL/plugin.json
index 876b9956376..dedc1555812 100644
--- a/web/i18n/nl-NL/plugin.json
+++ b/web/i18n/nl-NL/plugin.json
@@ -227,6 +227,7 @@
"marketplace.difyMarketplace": "Dify Marketplace",
"marketplace.discover": "Discover",
"marketplace.empower": "Empower your AI development",
+ "marketplace.home.creatorCenter": "Creatorcentrum",
"marketplace.home.guide": "Handleiding",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Ontdek. Breid uit. Bouw",
@@ -238,7 +239,12 @@
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
+ "marketplace.home.trendingPause": "Pauzeren",
+ "marketplace.home.trendingPlay": "Afspelen",
+ "marketplace.home.trendingReadMore": "Lees meer",
+ "marketplace.home.trendingReadMoreAbout": "Lees meer over {{title}}",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
+ "marketplace.home.trendingView": "Bekijken",
"marketplace.moreFrom": "More from Marketplace",
"marketplace.noPluginFound": "Geen plugin gevonden",
"marketplace.partnerTip": "Verified by a Dify partner",
diff --git a/web/i18n/pl-PL/plugin.json b/web/i18n/pl-PL/plugin.json
index 292dc084758..e1b8a569d0a 100644
--- a/web/i18n/pl-PL/plugin.json
+++ b/web/i18n/pl-PL/plugin.json
@@ -227,6 +227,7 @@
"marketplace.difyMarketplace": "Rynek Dify",
"marketplace.discover": "Odkryć",
"marketplace.empower": "Zwiększ możliwości rozwoju sztucznej inteligencji",
+ "marketplace.home.creatorCenter": "Centrum twórców",
"marketplace.home.guide": "Przewodnik",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Odkrywaj. Rozszerzaj. Twórz",
@@ -238,7 +239,12 @@
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
+ "marketplace.home.trendingPause": "Wstrzymaj",
+ "marketplace.home.trendingPlay": "Odtwórz",
+ "marketplace.home.trendingReadMore": "Czytaj więcej",
+ "marketplace.home.trendingReadMoreAbout": "Czytaj więcej o {{title}}",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
+ "marketplace.home.trendingView": "Zobacz",
"marketplace.moreFrom": "Więcej z Marketplace",
"marketplace.noPluginFound": "Nie znaleziono integracji",
"marketplace.partnerTip": "Zweryfikowane przez partnera Dify",
diff --git a/web/i18n/pt-BR/plugin.json b/web/i18n/pt-BR/plugin.json
index cc49d792925..98d02f9d617 100644
--- a/web/i18n/pt-BR/plugin.json
+++ b/web/i18n/pt-BR/plugin.json
@@ -227,6 +227,7 @@
"marketplace.difyMarketplace": "Mercado Dify",
"marketplace.discover": "Descobrir",
"marketplace.empower": "Capacite seu desenvolvimento de IA",
+ "marketplace.home.creatorCenter": "Central do criador",
"marketplace.home.guide": "Guia",
"marketplace.home.heroSubtitle": "Crie com plugins mais seguros e confiáveis do Dify Marketplace.",
"marketplace.home.heroTitle": "Descubra. Expanda. Crie",
@@ -238,7 +239,12 @@
"marketplace.home.trendingDescription": "Destaques por uso real, atualizados a cada duas semanas. Classificados pelas execuções reais nos espaços de trabalho — sem promoção paga ou seleção editorial.",
"marketplace.home.trendingEyebrow": "Em alta agora",
"marketplace.home.trendingPaginationLabel": "Páginas em alta",
+ "marketplace.home.trendingPause": "Pausar",
+ "marketplace.home.trendingPlay": "Reproduzir",
+ "marketplace.home.trendingReadMore": "Leia mais",
+ "marketplace.home.trendingReadMoreAbout": "Leia mais sobre {{title}}",
"marketplace.home.trendingTitle": "Os plugins que todos estão instalando",
+ "marketplace.home.trendingView": "Ver",
"marketplace.moreFrom": "Mais do Marketplace",
"marketplace.noPluginFound": "Nenhuma integração encontrada",
"marketplace.partnerTip": "Verificado por um parceiro da Dify",
diff --git a/web/i18n/ro-RO/plugin.json b/web/i18n/ro-RO/plugin.json
index c59bd37d137..cb44e729a0c 100644
--- a/web/i18n/ro-RO/plugin.json
+++ b/web/i18n/ro-RO/plugin.json
@@ -227,6 +227,7 @@
"marketplace.difyMarketplace": "Piața Dify",
"marketplace.discover": "Descoperi",
"marketplace.empower": "Îmbunătățește-ți dezvoltarea AI",
+ "marketplace.home.creatorCenter": "Centrul creatorilor",
"marketplace.home.guide": "Ghid",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Descoperă. Extinde. Construiește",
@@ -238,7 +239,12 @@
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
+ "marketplace.home.trendingPause": "Pauză",
+ "marketplace.home.trendingPlay": "Redare",
+ "marketplace.home.trendingReadMore": "Citește mai mult",
+ "marketplace.home.trendingReadMoreAbout": "Citește mai mult despre {{title}}",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
+ "marketplace.home.trendingView": "Vezi",
"marketplace.moreFrom": "Mai multe din Marketplace",
"marketplace.noPluginFound": "Nu s-a găsit niciun plugin",
"marketplace.partnerTip": "Verificat de un partener Dify",
diff --git a/web/i18n/ru-RU/plugin.json b/web/i18n/ru-RU/plugin.json
index b20334b8d41..70d9539f46c 100644
--- a/web/i18n/ru-RU/plugin.json
+++ b/web/i18n/ru-RU/plugin.json
@@ -227,6 +227,7 @@
"marketplace.difyMarketplace": "Торговая площадка Dify",
"marketplace.discover": "Обнаруживать",
"marketplace.empower": "Расширьте возможности разработки ИИ",
+ "marketplace.home.creatorCenter": "Центр авторов",
"marketplace.home.guide": "Руководство",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Открывайте. Расширяйте. Создавайте",
@@ -238,7 +239,12 @@
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
+ "marketplace.home.trendingPause": "Пауза",
+ "marketplace.home.trendingPlay": "Воспроизвести",
+ "marketplace.home.trendingReadMore": "Читать далее",
+ "marketplace.home.trendingReadMoreAbout": "Подробнее о {{title}}",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
+ "marketplace.home.trendingView": "Открыть",
"marketplace.moreFrom": "Больше из Marketplace",
"marketplace.noPluginFound": "Плагин не найден",
"marketplace.partnerTip": "Подтверждено партнером Dify",
diff --git a/web/i18n/sl-SI/plugin.json b/web/i18n/sl-SI/plugin.json
index ca6a60d474b..ed75a7e9cef 100644
--- a/web/i18n/sl-SI/plugin.json
+++ b/web/i18n/sl-SI/plugin.json
@@ -227,6 +227,7 @@
"marketplace.difyMarketplace": "Dify Marketplace",
"marketplace.discover": "Odkrijte",
"marketplace.empower": "Okrepite svoj razvoj AI",
+ "marketplace.home.creatorCenter": "Središče za ustvarjalce",
"marketplace.home.guide": "Vodnik",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Odkrijte. Razširite. Ustvarite",
@@ -238,7 +239,12 @@
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
+ "marketplace.home.trendingPause": "Premor",
+ "marketplace.home.trendingPlay": "Predvajaj",
+ "marketplace.home.trendingReadMore": "Preberi več",
+ "marketplace.home.trendingReadMoreAbout": "Preberi več o {{title}}",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
+ "marketplace.home.trendingView": "Ogled",
"marketplace.moreFrom": "Več iz tržnice",
"marketplace.noPluginFound": "Nobenega vtičnika ni bilo najti.",
"marketplace.partnerTip": "Potrjeno s strani partnerja Dify",
diff --git a/web/i18n/th-TH/plugin.json b/web/i18n/th-TH/plugin.json
index c31d34ca9ec..7166e3884fc 100644
--- a/web/i18n/th-TH/plugin.json
+++ b/web/i18n/th-TH/plugin.json
@@ -227,6 +227,7 @@
"marketplace.difyMarketplace": "ตลาด Dify",
"marketplace.discover": "ค้นพบ",
"marketplace.empower": "เพิ่มศักยภาพในการพัฒนา AI ของคุณ",
+ "marketplace.home.creatorCenter": "ศูนย์ครีเอเตอร์",
"marketplace.home.guide": "คู่มือ",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "ค้นพบ ขยาย และสร้าง",
@@ -238,7 +239,12 @@
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
+ "marketplace.home.trendingPause": "หยุดชั่วคราว",
+ "marketplace.home.trendingPlay": "เล่น",
+ "marketplace.home.trendingReadMore": "อ่านเพิ่มเติม",
+ "marketplace.home.trendingReadMoreAbout": "อ่านเพิ่มเติมเกี่ยวกับ {{title}}",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
+ "marketplace.home.trendingView": "ดู",
"marketplace.moreFrom": "แอปเพิ่มเติมจาก Marketplace",
"marketplace.noPluginFound": "ไม่พบปลั๊กอิน",
"marketplace.partnerTip": "ได้รับการตรวจสอบโดยพันธมิตรของ Dify",
diff --git a/web/i18n/tr-TR/plugin.json b/web/i18n/tr-TR/plugin.json
index dd4edf2fe55..a6069fb0936 100644
--- a/web/i18n/tr-TR/plugin.json
+++ b/web/i18n/tr-TR/plugin.json
@@ -227,6 +227,7 @@
"marketplace.difyMarketplace": "Dify Pazar Yeri",
"marketplace.discover": "Keşfet",
"marketplace.empower": "Yapay zeka geliştirmenizi güçlendirin",
+ "marketplace.home.creatorCenter": "İçerik Üretici Merkezi",
"marketplace.home.guide": "Kılavuz",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Keşfet. Genişlet. Oluştur",
@@ -238,7 +239,12 @@
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
+ "marketplace.home.trendingPause": "Duraklat",
+ "marketplace.home.trendingPlay": "Oynat",
+ "marketplace.home.trendingReadMore": "Devamını oku",
+ "marketplace.home.trendingReadMoreAbout": "{{title}} hakkında devamını oku",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
+ "marketplace.home.trendingView": "Görüntüle",
"marketplace.moreFrom": "Pazar Yeri'nden daha fazlası",
"marketplace.noPluginFound": "Eklenti bulunamadı",
"marketplace.partnerTip": "Dify partner'ı tarafından doğrulandı",
diff --git a/web/i18n/uk-UA/plugin.json b/web/i18n/uk-UA/plugin.json
index dabbe46e1a2..8c39af1856a 100644
--- a/web/i18n/uk-UA/plugin.json
+++ b/web/i18n/uk-UA/plugin.json
@@ -227,6 +227,7 @@
"marketplace.difyMarketplace": "Dify Marketplace",
"marketplace.discover": "Виявити",
"marketplace.empower": "Розширюйте можливості розробки штучного інтелекту",
+ "marketplace.home.creatorCenter": "Центр авторів",
"marketplace.home.guide": "Посібник",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Відкривайте. Розширюйте. Створюйте",
@@ -238,7 +239,12 @@
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
+ "marketplace.home.trendingPause": "Пауза",
+ "marketplace.home.trendingPlay": "Відтворити",
+ "marketplace.home.trendingReadMore": "Читати далі",
+ "marketplace.home.trendingReadMoreAbout": "Дізнатися більше про {{title}}",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
+ "marketplace.home.trendingView": "Переглянути",
"marketplace.moreFrom": "Більше від Marketplace",
"marketplace.noPluginFound": "Плагін не знайдено",
"marketplace.partnerTip": "Перевірено партнером Dify",
diff --git a/web/i18n/vi-VN/plugin.json b/web/i18n/vi-VN/plugin.json
index 7fa4fceb908..c8bb10aac38 100644
--- a/web/i18n/vi-VN/plugin.json
+++ b/web/i18n/vi-VN/plugin.json
@@ -227,6 +227,7 @@
"marketplace.difyMarketplace": "Thị trường Dify",
"marketplace.discover": "Khám phá",
"marketplace.empower": "Hỗ trợ phát triển AI của bạn",
+ "marketplace.home.creatorCenter": "Trung tâm nhà sáng tạo",
"marketplace.home.guide": "Hướng dẫn",
"marketplace.home.heroSubtitle": "Build with safer, more reliable plugins from the Dify Marketplace.",
"marketplace.home.heroTitle": "Khám phá. Mở rộng. Xây dựng",
@@ -238,7 +239,12 @@
"marketplace.home.trendingDescription": "Top picks by real usage, refreshed every two weeks. Ranked by actual runs across workspaces — no paid placement, no editorial picks.",
"marketplace.home.trendingEyebrow": "Trending Now",
"marketplace.home.trendingPaginationLabel": "Trending pages",
+ "marketplace.home.trendingPause": "Tạm dừng",
+ "marketplace.home.trendingPlay": "Phát",
+ "marketplace.home.trendingReadMore": "Đọc thêm",
+ "marketplace.home.trendingReadMoreAbout": "Đọc thêm về {{title}}",
"marketplace.home.trendingTitle": "The plugins everyone is installing",
+ "marketplace.home.trendingView": "Xem",
"marketplace.moreFrom": "Các ứng dụng khác từ Marketplace",
"marketplace.noPluginFound": "Không tìm thấy plugin nào",
"marketplace.partnerTip": "Được xác nhận bởi một đối tác của Dify",
diff --git a/web/i18n/zh-Hans/plugin.json b/web/i18n/zh-Hans/plugin.json
index 2acf2e9df7b..25fe6feb00f 100644
--- a/web/i18n/zh-Hans/plugin.json
+++ b/web/i18n/zh-Hans/plugin.json
@@ -227,6 +227,7 @@
"marketplace.difyMarketplace": "Dify Marketplace",
"marketplace.discover": "探索",
"marketplace.empower": "助力您的 AI 开发",
+ "marketplace.home.creatorCenter": "创作者中心",
"marketplace.home.guide": "指南",
"marketplace.home.heroSubtitle": "在 Dify Marketplace 探索更安全、更可靠的插件。",
"marketplace.home.heroTitle": "发现。扩展。构建",
@@ -240,6 +241,8 @@
"marketplace.home.trendingPaginationLabel": "热门推荐页码",
"marketplace.home.trendingPause": "暂停",
"marketplace.home.trendingPlay": "播放",
+ "marketplace.home.trendingReadMore": "阅读更多",
+ "marketplace.home.trendingReadMoreAbout": "阅读更多关于 {{title}} 的内容",
"marketplace.home.trendingTitle": "大家都在安装的插件",
"marketplace.home.trendingView": "查看",
"marketplace.moreFrom": "来自 Marketplace 的更多内容",
diff --git a/web/i18n/zh-Hant/plugin.json b/web/i18n/zh-Hant/plugin.json
index ad4caacf8a3..b5c6e57117f 100644
--- a/web/i18n/zh-Hant/plugin.json
+++ b/web/i18n/zh-Hant/plugin.json
@@ -227,6 +227,7 @@
"marketplace.difyMarketplace": "Dify Marketplace",
"marketplace.discover": "發現",
"marketplace.empower": "為您的 AI 開發提供支援",
+ "marketplace.home.creatorCenter": "創作者中心",
"marketplace.home.guide": "指南",
"marketplace.home.heroSubtitle": "在 Dify Marketplace 探索更安全、更可靠的外掛程式。",
"marketplace.home.heroTitle": "探索。擴展。建構",
@@ -240,6 +241,8 @@
"marketplace.home.trendingPaginationLabel": "熱門推薦頁碼",
"marketplace.home.trendingPause": "暫停",
"marketplace.home.trendingPlay": "播放",
+ "marketplace.home.trendingReadMore": "閱讀更多",
+ "marketplace.home.trendingReadMoreAbout": "閱讀更多關於 {{title}} 的內容",
"marketplace.home.trendingTitle": "大家都在安裝的外掛程式",
"marketplace.home.trendingView": "查看",
"marketplace.moreFrom": "來自 Marketplace 的更多內容",
diff --git a/web/service/marketplace-template-discovery.spec.ts b/web/service/marketplace-template-discovery.spec.ts
index 0730b51df5b..f6940080338 100644
--- a/web/service/marketplace-template-discovery.spec.ts
+++ b/web/service/marketplace-template-discovery.spec.ts
@@ -1,8 +1,4 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-import {
- getMarketplaceTemplateCollectionsAndTemplates,
- searchMarketplaceTemplates,
-} from './marketplace-template-discovery'
+import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
const mocks = vi.hoisted(() => ({
templateCollections: vi.fn(),
@@ -18,12 +14,20 @@ vi.mock('./client', () => ({
},
}))
+// The collections helper keeps a module-level cache, so import a fresh copy
+// per test to keep them isolated.
+const importDiscovery = async () => {
+ vi.resetModules()
+ return import('./marketplace-template-discovery')
+}
+
describe('marketplace template discovery', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('loads each template collection and isolates a failed collection', async () => {
+ const { getMarketplaceTemplateCollectionsAndTemplates } = await importDiscovery()
mocks.templateCollections.mockResolvedValue({
data: {
collections: [
@@ -43,7 +47,7 @@ describe('marketplace template discovery', () => {
})
expect(mocks.templateCollectionTemplates).toHaveBeenNthCalledWith(1, {
params: { collectionName: 'featured' },
- body: { limit: 100 },
+ body: { limit: 24 },
})
expect(result.templatesByCollection).toEqual({
featured: [{ id: 'template-1' }],
@@ -51,7 +55,51 @@ describe('marketplace template discovery', () => {
})
})
+ it('serves collections from the cache instead of refetching every render', async () => {
+ const { getMarketplaceTemplateCollectionsAndTemplates } = await importDiscovery()
+ mocks.templateCollections.mockResolvedValue({
+ data: {
+ collections: [{ name: 'featured', label: {}, description: {}, priority: 1 }],
+ },
+ })
+ mocks.templateCollectionTemplates.mockResolvedValue({
+ data: { templates: [{ id: 'template-1' }] },
+ })
+
+ const [first, second] = await Promise.all([
+ getMarketplaceTemplateCollectionsAndTemplates(),
+ getMarketplaceTemplateCollectionsAndTemplates(),
+ ])
+ const third = await getMarketplaceTemplateCollectionsAndTemplates()
+
+ expect(mocks.templateCollections).toHaveBeenCalledOnce()
+ expect(mocks.templateCollectionTemplates).toHaveBeenCalledOnce()
+ expect(second).toBe(first)
+ expect(third).toBe(first)
+ })
+
+ it('does not cache a failed collections fetch', async () => {
+ const { getMarketplaceTemplateCollectionsAndTemplates } = await importDiscovery()
+ mocks.templateCollections.mockRejectedValueOnce(new Error('Unavailable'))
+
+ const failed = await getMarketplaceTemplateCollectionsAndTemplates()
+ expect(failed).toEqual({ collections: [], templatesByCollection: {} })
+
+ mocks.templateCollections.mockResolvedValue({
+ data: {
+ collections: [{ name: 'featured', label: {}, description: {}, priority: 1 }],
+ },
+ })
+ mocks.templateCollectionTemplates.mockResolvedValue({
+ data: { templates: [{ id: 'template-1' }] },
+ })
+
+ const recovered = await getMarketplaceTemplateCollectionsAndTemplates()
+ expect(recovered.templatesByCollection).toEqual({ featured: [{ id: 'template-1' }] })
+ })
+
it('sends category searches through the Marketplace contract', async () => {
+ const { searchMarketplaceTemplates } = await importDiscovery()
mocks.templateSearch.mockResolvedValue({
data: {
templates: [{ id: 'template-1' }],
@@ -61,12 +109,13 @@ describe('marketplace template discovery', () => {
const result = await searchMarketplaceTemplates({
category: 'marketing',
+ page: 2,
query: 'campaign',
})
expect(mocks.templateSearch).toHaveBeenCalledWith({
body: {
- page: 1,
+ page: 2,
page_size: 40,
query: 'campaign',
sort_by: 'usage_count',
@@ -74,6 +123,6 @@ describe('marketplace template discovery', () => {
categories: ['marketing'],
},
})
- expect(result).toEqual({ templates: [{ id: 'template-1' }], total: 1 })
+ expect(result).toEqual({ page: 2, templates: [{ id: 'template-1' }], total: 1 })
})
})
diff --git a/web/service/marketplace-template-discovery.ts b/web/service/marketplace-template-discovery.ts
index 49e93d75ffa..e195cc48645 100644
--- a/web/service/marketplace-template-discovery.ts
+++ b/web/service/marketplace-template-discovery.ts
@@ -9,8 +9,11 @@ export type MarketplaceTemplateCollectionsResult = {
templatesByCollection: Record
}
+export const TEMPLATE_SEARCH_PAGE_SIZE = 40
+
type SearchMarketplaceTemplatesOptions = {
category: string
+ page?: number
query: string
sortBy?: string
sortOrder?: string
@@ -21,41 +24,85 @@ const EMPTY_COLLECTIONS_RESULT: MarketplaceTemplateCollectionsResult = {
templatesByCollection: {},
}
-export async function getMarketplaceTemplateCollectionsAndTemplates(): Promise {
- try {
- const response = await marketplaceClient.templateCollections({
- query: {
- page: 1,
- page_size: 100,
- },
- })
- const collections = response.data?.collections ?? []
- const entries = await Promise.all(
- collections.map(async (collection) => {
- try {
- const collectionResponse = await marketplaceClient.templateCollectionTemplates({
- params: { collectionName: collection.name },
- body: { limit: 100 },
- })
+const COLLECTION_PREVIEW_TEMPLATE_LIMIT = 24
+const COLLECTION_FETCH_BATCH_SIZE = 5
+const COLLECTIONS_CACHE_TTL_MS = 5 * 60 * 1000
- return [collection.name, collectionResponse.data?.templates ?? []] as const
- } catch {
- return [collection.name, []] as const
- }
- }),
+let collectionsCache: {
+ expiresAt: number
+ result: MarketplaceTemplateCollectionsResult
+} | null = null
+let collectionsInFlight: Promise | null = null
+
+async function fetchCollectionsAndTemplates(): Promise {
+ const response = await marketplaceClient.templateCollections({
+ query: {
+ page: 1,
+ page_size: 100,
+ },
+ })
+ const collections = response.data?.collections ?? []
+ const entries: (readonly [string, MarketplaceTemplate[]])[] = []
+
+ // Bounded fan-out: fetch collection previews in small batches instead of
+ // firing one uncached request per collection all at once.
+ for (
+ let batchStart = 0;
+ batchStart < collections.length;
+ batchStart += COLLECTION_FETCH_BATCH_SIZE
+ ) {
+ const batch = collections.slice(batchStart, batchStart + COLLECTION_FETCH_BATCH_SIZE)
+ entries.push(
+ ...(await Promise.all(
+ batch.map(async (collection) => {
+ try {
+ const collectionResponse = await marketplaceClient.templateCollectionTemplates({
+ params: { collectionName: collection.name },
+ body: { limit: COLLECTION_PREVIEW_TEMPLATE_LIMIT },
+ })
+
+ return [collection.name, collectionResponse.data?.templates ?? []] as const
+ } catch {
+ return [collection.name, [] as MarketplaceTemplate[]] as const
+ }
+ }),
+ )),
)
-
- return {
- collections,
- templatesByCollection: Object.fromEntries(entries),
- }
- } catch {
- return EMPTY_COLLECTIONS_RESULT
}
+
+ return {
+ collections,
+ templatesByCollection: Object.fromEntries(entries),
+ }
+}
+
+/**
+ * Server-side cached view of the template collections and their previews.
+ * `marketplaceClient` opts out of the framework fetch cache (`no-store`), so
+ * without this cache every server render of /templates would fan out to up to
+ * 1 + N external requests. Successful results are reused for a few minutes and
+ * concurrent renders share a single in-flight fetch; failures are not cached.
+ */
+export async function getMarketplaceTemplateCollectionsAndTemplates(): Promise {
+ if (collectionsCache && collectionsCache.expiresAt > Date.now()) return collectionsCache.result
+ if (collectionsInFlight) return collectionsInFlight
+
+ collectionsInFlight = fetchCollectionsAndTemplates()
+ .then((result) => {
+ collectionsCache = { expiresAt: Date.now() + COLLECTIONS_CACHE_TTL_MS, result }
+ return result
+ })
+ .catch(() => EMPTY_COLLECTIONS_RESULT)
+ .finally(() => {
+ collectionsInFlight = null
+ })
+
+ return collectionsInFlight
}
export async function searchMarketplaceTemplates({
category,
+ page = 1,
query,
sortBy = 'usage_count',
sortOrder = 'DESC',
@@ -63,8 +110,8 @@ export async function searchMarketplaceTemplates({
try {
const response = await marketplaceClient.templateSearch({
body: {
- page: 1,
- page_size: 40,
+ page,
+ page_size: TEMPLATE_SEARCH_PAGE_SIZE,
query,
sort_by: sortBy,
sort_order: sortOrder,
@@ -73,11 +120,13 @@ export async function searchMarketplaceTemplates({
})
return {
+ page,
templates: response.data?.templates ?? [],
total: response.data?.total ?? 0,
}
} catch {
return {
+ page,
templates: [],
total: 0,
}