From b2124a7358658b75130b3cd7940dd5bf5e071ef0 Mon Sep 17 00:00:00 2001
From: Stephen Zhou <38493346+hyoban@users.noreply.github.com>
Date: Tue, 6 Jan 2026 13:23:03 +0800
Subject: [PATCH] feat: init rsc support for translation (#30596)
---
.../[datasetId]/settings/page.tsx | 9 +-
.../marketplace/description/index.spec.tsx | 336 ++++++++----------
.../plugins/marketplace/description/index.tsx | 20 +-
.../components/plugins/marketplace/index.tsx | 2 +-
web/eslint.config.mjs | 2 +-
web/i18n-config/lib.client.ts | 10 +
web/i18n-config/lib.server.ts | 16 +
web/i18n-config/server.ts | 13 +-
web/package.json | 6 +
web/utils/server-only-context.ts | 2 +-
10 files changed, 202 insertions(+), 214 deletions(-)
create mode 100644 web/i18n-config/lib.client.ts
create mode 100644 web/i18n-config/lib.server.ts
diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx
index 8080b565cd..1d65e4de53 100644
--- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx
+++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx
@@ -1,11 +1,8 @@
-/* eslint-disable dify-i18n/require-ns-option */
-import * as React from 'react'
+import { useTranslation } from '#i18n'
import Form from '@/app/components/datasets/settings/form'
-import { getLocaleOnServer, getTranslation } from '@/i18n-config/server'
-const Settings = async () => {
- const locale = await getLocaleOnServer()
- const { t } = await getTranslation(locale, 'dataset-settings')
+const Settings = () => {
+ const { t } = useTranslation('datasetSettings')
return (
diff --git a/web/app/components/plugins/marketplace/description/index.spec.tsx b/web/app/components/plugins/marketplace/description/index.spec.tsx
index b5c8cb716b..054949ee1f 100644
--- a/web/app/components/plugins/marketplace/description/index.spec.tsx
+++ b/web/app/components/plugins/marketplace/description/index.spec.tsx
@@ -1,7 +1,5 @@
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
-
-// Import component after mocks are set up
import Description from './index'
// ================================
@@ -30,20 +28,18 @@ const commonTranslations: Record = {
'operation.in': 'in',
}
-// Mock getLocaleOnServer and translate
-vi.mock('@/i18n-config/server', () => ({
- getLocaleOnServer: vi.fn(() => Promise.resolve(mockDefaultLocale)),
- getTranslation: vi.fn((locale: string, ns: string) => {
- return Promise.resolve({
- t: (key: string) => {
- if (ns === 'plugin')
- return pluginTranslations[key] || key
- if (ns === 'common')
- return commonTranslations[key] || key
- return key
- },
- })
- }),
+// Mock i18n hooks
+vi.mock('#i18n', () => ({
+ useLocale: vi.fn(() => mockDefaultLocale),
+ useTranslation: vi.fn((ns: string) => ({
+ t: (key: string) => {
+ if (ns === 'plugin')
+ return pluginTranslations[key] || key
+ if (ns === 'common')
+ return commonTranslations[key] || key
+ return key
+ },
+ })),
}))
// ================================
@@ -59,29 +55,29 @@ describe('Description', () => {
// Rendering Tests
// ================================
describe('Rendering', () => {
- it('should render without crashing', async () => {
- const { container } = render(await Description({}))
+ it('should render without crashing', () => {
+ const { container } = render()
expect(container.firstChild).toBeInTheDocument()
})
- it('should render h1 heading with empower text', async () => {
- render(await Description({}))
+ it('should render h1 heading with empower text', () => {
+ render()
const heading = screen.getByRole('heading', { level: 1 })
expect(heading).toBeInTheDocument()
expect(heading).toHaveTextContent('Empower your AI development')
})
- it('should render h2 subheading', async () => {
- render(await Description({}))
+ it('should render h2 subheading', () => {
+ render()
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading).toBeInTheDocument()
})
- it('should apply correct CSS classes to h1', async () => {
- render(await Description({}))
+ it('should apply correct CSS classes to h1', () => {
+ render()
const heading = screen.getByRole('heading', { level: 1 })
expect(heading).toHaveClass('title-4xl-semi-bold')
@@ -90,8 +86,8 @@ describe('Description', () => {
expect(heading).toHaveClass('text-text-primary')
})
- it('should apply correct CSS classes to h2', async () => {
- render(await Description({}))
+ it('should apply correct CSS classes to h2', () => {
+ render()
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading).toHaveClass('body-md-regular')
@@ -104,14 +100,18 @@ describe('Description', () => {
// Non-Chinese Locale Rendering Tests
// ================================
describe('Non-Chinese Locale Rendering', () => {
- it('should render discover text for en-US locale', async () => {
- render(await Description({ locale: 'en-US' }))
+ beforeEach(() => {
+ mockDefaultLocale = 'en-US'
+ })
+
+ it('should render discover text for en-US locale', () => {
+ render()
expect(screen.getByText(/Discover/)).toBeInTheDocument()
})
- it('should render all category names', async () => {
- render(await Description({ locale: 'en-US' }))
+ it('should render all category names', () => {
+ render()
expect(screen.getByText('Models')).toBeInTheDocument()
expect(screen.getByText('Tools')).toBeInTheDocument()
@@ -122,36 +122,36 @@ describe('Description', () => {
expect(screen.getByText('Bundles')).toBeInTheDocument()
})
- it('should render "and" conjunction text', async () => {
- render(await Description({ locale: 'en-US' }))
+ it('should render "and" conjunction text', () => {
+ render()
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading.textContent).toContain('and')
})
- it('should render "in" preposition at the end for non-Chinese locales', async () => {
- render(await Description({ locale: 'en-US' }))
+ it('should render "in" preposition at the end for non-Chinese locales', () => {
+ render()
expect(screen.getByText('in')).toBeInTheDocument()
})
- it('should render Dify Marketplace text at the end for non-Chinese locales', async () => {
- render(await Description({ locale: 'en-US' }))
+ it('should render Dify Marketplace text at the end for non-Chinese locales', () => {
+ render()
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading.textContent).toContain('Dify Marketplace')
})
- it('should render category spans with styled underline effect', async () => {
- const { container } = render(await Description({ locale: 'en-US' }))
+ it('should render category spans with styled underline effect', () => {
+ const { container } = render()
const styledSpans = container.querySelectorAll('.body-md-medium.relative.z-\\[1\\]')
// 7 category spans (models, tools, datasources, triggers, agents, extensions, bundles)
expect(styledSpans.length).toBe(7)
})
- it('should apply text-text-secondary class to category spans', async () => {
- const { container } = render(await Description({ locale: 'en-US' }))
+ it('should apply text-text-secondary class to category spans', () => {
+ const { container } = render()
const styledSpans = container.querySelectorAll('.text-text-secondary')
expect(styledSpans.length).toBeGreaterThanOrEqual(7)
@@ -162,29 +162,33 @@ describe('Description', () => {
// Chinese (zh-Hans) Locale Rendering Tests
// ================================
describe('Chinese (zh-Hans) Locale Rendering', () => {
- it('should render "in" text at the beginning for zh-Hans locale', async () => {
- render(await Description({ locale: 'zh-Hans' }))
+ beforeEach(() => {
+ mockDefaultLocale = 'zh-Hans'
+ })
+
+ it('should render "in" text at the beginning for zh-Hans locale', () => {
+ render()
// In zh-Hans mode, "in" appears at the beginning
const inElements = screen.getAllByText('in')
expect(inElements.length).toBeGreaterThanOrEqual(1)
})
- it('should render Dify Marketplace text for zh-Hans locale', async () => {
- render(await Description({ locale: 'zh-Hans' }))
+ it('should render Dify Marketplace text for zh-Hans locale', () => {
+ render()
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading.textContent).toContain('Dify Marketplace')
})
- it('should render discover text for zh-Hans locale', async () => {
- render(await Description({ locale: 'zh-Hans' }))
+ it('should render discover text for zh-Hans locale', () => {
+ render()
expect(screen.getByText(/Discover/)).toBeInTheDocument()
})
- it('should render all categories for zh-Hans locale', async () => {
- render(await Description({ locale: 'zh-Hans' }))
+ it('should render all categories for zh-Hans locale', () => {
+ render()
expect(screen.getByText('Models')).toBeInTheDocument()
expect(screen.getByText('Tools')).toBeInTheDocument()
@@ -195,8 +199,8 @@ describe('Description', () => {
expect(screen.getByText('Bundles')).toBeInTheDocument()
})
- it('should render both zh-Hans specific elements and shared elements', async () => {
- render(await Description({ locale: 'zh-Hans' }))
+ it('should render both zh-Hans specific elements and shared elements', () => {
+ render()
// zh-Hans has specific element order: "in" -> Dify Marketplace -> Discover
// then the same category list with "and" -> Bundles
@@ -206,61 +210,57 @@ describe('Description', () => {
})
// ================================
- // Locale Prop Variations Tests
+ // Locale Variations Tests
// ================================
- describe('Locale Prop Variations', () => {
- it('should use default locale when locale prop is undefined', async () => {
+ describe('Locale Variations', () => {
+ it('should use en-US locale by default', () => {
mockDefaultLocale = 'en-US'
- render(await Description({}))
+ render()
- // Should use the default locale from getLocaleOnServer
expect(screen.getByText('Empower your AI development')).toBeInTheDocument()
})
- it('should use provided locale prop instead of default', async () => {
+ it('should handle ja-JP locale as non-Chinese', () => {
mockDefaultLocale = 'ja-JP'
- render(await Description({ locale: 'en-US' }))
-
- // The locale prop should be used, triggering non-Chinese rendering
- const subheading = screen.getByRole('heading', { level: 2 })
- expect(subheading).toBeInTheDocument()
- })
-
- it('should handle ja-JP locale as non-Chinese', async () => {
- render(await Description({ locale: 'ja-JP' }))
+ render()
// Should render in non-Chinese format (discover first, then "in Dify Marketplace" at end)
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading.textContent).toContain('Dify Marketplace')
})
- it('should handle ko-KR locale as non-Chinese', async () => {
- render(await Description({ locale: 'ko-KR' }))
+ it('should handle ko-KR locale as non-Chinese', () => {
+ mockDefaultLocale = 'ko-KR'
+ render()
// Should render in non-Chinese format
expect(screen.getByText('Empower your AI development')).toBeInTheDocument()
})
- it('should handle de-DE locale as non-Chinese', async () => {
- render(await Description({ locale: 'de-DE' }))
+ it('should handle de-DE locale as non-Chinese', () => {
+ mockDefaultLocale = 'de-DE'
+ render()
expect(screen.getByText('Empower your AI development')).toBeInTheDocument()
})
- it('should handle fr-FR locale as non-Chinese', async () => {
- render(await Description({ locale: 'fr-FR' }))
+ it('should handle fr-FR locale as non-Chinese', () => {
+ mockDefaultLocale = 'fr-FR'
+ render()
expect(screen.getByText('Empower your AI development')).toBeInTheDocument()
})
- it('should handle pt-BR locale as non-Chinese', async () => {
- render(await Description({ locale: 'pt-BR' }))
+ it('should handle pt-BR locale as non-Chinese', () => {
+ mockDefaultLocale = 'pt-BR'
+ render()
expect(screen.getByText('Empower your AI development')).toBeInTheDocument()
})
- it('should handle es-ES locale as non-Chinese', async () => {
- render(await Description({ locale: 'es-ES' }))
+ it('should handle es-ES locale as non-Chinese', () => {
+ mockDefaultLocale = 'es-ES'
+ render()
expect(screen.getByText('Empower your AI development')).toBeInTheDocument()
})
@@ -270,24 +270,27 @@ describe('Description', () => {
// Conditional Rendering Tests
// ================================
describe('Conditional Rendering', () => {
- it('should render zh-Hans specific content when locale is zh-Hans', async () => {
- const { container } = render(await Description({ locale: 'zh-Hans' }))
+ it('should render zh-Hans specific content when locale is zh-Hans', () => {
+ mockDefaultLocale = 'zh-Hans'
+ const { container } = render()
// zh-Hans has additional span with mr-1 before "in" text at the start
const mrSpan = container.querySelector('span.mr-1')
expect(mrSpan).toBeInTheDocument()
})
- it('should render non-Chinese specific content when locale is not zh-Hans', async () => {
- render(await Description({ locale: 'en-US' }))
+ it('should render non-Chinese specific content when locale is not zh-Hans', () => {
+ mockDefaultLocale = 'en-US'
+ render()
// Non-Chinese has "in" and "Dify Marketplace" at the end
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading.textContent).toContain('Dify Marketplace')
})
- it('should not render zh-Hans intro content for non-Chinese locales', async () => {
- render(await Description({ locale: 'en-US' }))
+ it('should not render zh-Hans intro content for non-Chinese locales', () => {
+ mockDefaultLocale = 'en-US'
+ render()
// For en-US, the order should be Discover ... in Dify Marketplace
// The "in" text should only appear once at the end
@@ -303,8 +306,9 @@ describe('Description', () => {
expect(inIndex).toBeLessThan(marketplaceIndex)
})
- it('should render zh-Hans with proper word order', async () => {
- render(await Description({ locale: 'zh-Hans' }))
+ it('should render zh-Hans with proper word order', () => {
+ mockDefaultLocale = 'zh-Hans'
+ render()
const subheading = screen.getByRole('heading', { level: 2 })
const content = subheading.textContent || ''
@@ -323,58 +327,58 @@ describe('Description', () => {
// Category Styling Tests
// ================================
describe('Category Styling', () => {
- it('should apply underline effect with after pseudo-element styling', async () => {
- const { container } = render(await Description({}))
+ it('should apply underline effect with after pseudo-element styling', () => {
+ const { container } = render()
const categorySpan = container.querySelector('.after\\:absolute')
expect(categorySpan).toBeInTheDocument()
})
- it('should apply correct after pseudo-element classes', async () => {
- const { container } = render(await Description({}))
+ it('should apply correct after pseudo-element classes', () => {
+ const { container } = render()
// Check for the specific after pseudo-element classes
const categorySpans = container.querySelectorAll('.after\\:bottom-\\[1\\.5px\\]')
expect(categorySpans.length).toBe(7)
})
- it('should apply full width to after element', async () => {
- const { container } = render(await Description({}))
+ it('should apply full width to after element', () => {
+ const { container } = render()
const categorySpans = container.querySelectorAll('.after\\:w-full')
expect(categorySpans.length).toBe(7)
})
- it('should apply correct height to after element', async () => {
- const { container } = render(await Description({}))
+ it('should apply correct height to after element', () => {
+ const { container } = render()
const categorySpans = container.querySelectorAll('.after\\:h-2')
expect(categorySpans.length).toBe(7)
})
- it('should apply bg-text-text-selected to after element', async () => {
- const { container } = render(await Description({}))
+ it('should apply bg-text-text-selected to after element', () => {
+ const { container } = render()
const categorySpans = container.querySelectorAll('.after\\:bg-text-text-selected')
expect(categorySpans.length).toBe(7)
})
- it('should have z-index 1 on category spans', async () => {
- const { container } = render(await Description({}))
+ it('should have z-index 1 on category spans', () => {
+ const { container } = render()
const categorySpans = container.querySelectorAll('.z-\\[1\\]')
expect(categorySpans.length).toBe(7)
})
- it('should apply left margin to category spans', async () => {
- const { container } = render(await Description({}))
+ it('should apply left margin to category spans', () => {
+ const { container } = render()
const categorySpans = container.querySelectorAll('.ml-1')
expect(categorySpans.length).toBeGreaterThanOrEqual(7)
})
- it('should apply both left and right margin to specific spans', async () => {
- const { container } = render(await Description({}))
+ it('should apply both left and right margin to specific spans', () => {
+ const { container } = render()
// Extensions and Bundles spans have both ml-1 and mr-1
const extensionsBundlesSpans = container.querySelectorAll('.ml-1.mr-1')
@@ -386,28 +390,17 @@ describe('Description', () => {
// Edge Cases Tests
// ================================
describe('Edge Cases', () => {
- it('should handle empty props object', async () => {
- const { container } = render(await Description({}))
-
- expect(container.firstChild).toBeInTheDocument()
- })
-
- it('should render fragment as root element', async () => {
- const { container } = render(await Description({}))
+ it('should render fragment as root element', () => {
+ const { container } = render()
// Fragment renders h1 and h2 as direct children
expect(container.querySelector('h1')).toBeInTheDocument()
expect(container.querySelector('h2')).toBeInTheDocument()
})
- it('should handle locale prop with undefined value', async () => {
- render(await Description({ locale: undefined }))
-
- expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument()
- })
-
- it('should handle zh-Hant as non-Chinese simplified', async () => {
- render(await Description({ locale: 'zh-Hant' }))
+ it('should handle zh-Hant as non-Chinese simplified', () => {
+ mockDefaultLocale = 'zh-Hant'
+ render()
// zh-Hant is different from zh-Hans, should use non-Chinese format
const subheading = screen.getByRole('heading', { level: 2 })
@@ -426,8 +419,8 @@ describe('Description', () => {
// Content Structure Tests
// ================================
describe('Content Structure', () => {
- it('should have comma separators between categories', async () => {
- render(await Description({}))
+ it('should have comma separators between categories', () => {
+ render()
const subheading = screen.getByRole('heading', { level: 2 })
const content = subheading.textContent || ''
@@ -436,8 +429,8 @@ describe('Description', () => {
expect(content).toMatch(/Models[^\n\r,\u2028\u2029]*,.*Tools[^\n\r,\u2028\u2029]*,.*Data Sources[^\n\r,\u2028\u2029]*,.*Triggers[^\n\r,\u2028\u2029]*,.*Agent Strategies[^\n\r,\u2028\u2029]*,.*Extensions/)
})
- it('should have "and" before last category (Bundles)', async () => {
- render(await Description({}))
+ it('should have "and" before last category (Bundles)', () => {
+ render()
const subheading = screen.getByRole('heading', { level: 2 })
const content = subheading.textContent || ''
@@ -449,8 +442,9 @@ describe('Description', () => {
expect(andIndex).toBeLessThan(bundlesIndex)
})
- it('should render all text elements in correct order for en-US', async () => {
- render(await Description({ locale: 'en-US' }))
+ it('should render all text elements in correct order for en-US', () => {
+ mockDefaultLocale = 'en-US'
+ render()
const subheading = screen.getByRole('heading', { level: 2 })
const content = subheading.textContent || ''
@@ -477,8 +471,9 @@ describe('Description', () => {
}
})
- it('should render all text elements in correct order for zh-Hans', async () => {
- render(await Description({ locale: 'zh-Hans' }))
+ it('should render all text elements in correct order for zh-Hans', () => {
+ mockDefaultLocale = 'zh-Hans'
+ render()
const subheading = screen.getByRole('heading', { level: 2 })
const content = subheading.textContent || ''
@@ -499,82 +494,48 @@ describe('Description', () => {
// Layout Tests
// ================================
describe('Layout', () => {
- it('should have shrink-0 on h1 heading', async () => {
- render(await Description({}))
+ it('should have shrink-0 on h1 heading', () => {
+ render()
const heading = screen.getByRole('heading', { level: 1 })
expect(heading).toHaveClass('shrink-0')
})
- it('should have shrink-0 on h2 subheading', async () => {
- render(await Description({}))
+ it('should have shrink-0 on h2 subheading', () => {
+ render()
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading).toHaveClass('shrink-0')
})
- it('should have flex layout on h2', async () => {
- render(await Description({}))
+ it('should have flex layout on h2', () => {
+ render()
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading).toHaveClass('flex')
})
- it('should have items-center on h2', async () => {
- render(await Description({}))
+ it('should have items-center on h2', () => {
+ render()
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading).toHaveClass('items-center')
})
- it('should have justify-center on h2', async () => {
- render(await Description({}))
+ it('should have justify-center on h2', () => {
+ render()
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading).toHaveClass('justify-center')
})
})
- // ================================
- // Translation Function Tests
- // ================================
- describe('Translation Functions', () => {
- it('should call getTranslation for plugin namespace', async () => {
- const { getTranslation } = await import('@/i18n-config/server')
- render(await Description({ locale: 'en-US' }))
-
- expect(getTranslation).toHaveBeenCalledWith('en-US', 'plugin')
- })
-
- it('should call getTranslation for common namespace', async () => {
- const { getTranslation } = await import('@/i18n-config/server')
- render(await Description({ locale: 'en-US' }))
-
- expect(getTranslation).toHaveBeenCalledWith('en-US', 'common')
- })
-
- it('should call getLocaleOnServer when locale prop is undefined', async () => {
- const { getLocaleOnServer } = await import('@/i18n-config/server')
- render(await Description({}))
-
- expect(getLocaleOnServer).toHaveBeenCalled()
- })
-
- it('should use locale prop when provided', async () => {
- const { getTranslation } = await import('@/i18n-config/server')
- render(await Description({ locale: 'ja-JP' }))
-
- expect(getTranslation).toHaveBeenCalledWith('ja-JP', 'plugin')
- expect(getTranslation).toHaveBeenCalledWith('ja-JP', 'common')
- })
- })
-
// ================================
// Accessibility Tests
// ================================
describe('Accessibility', () => {
- it('should have proper heading hierarchy', async () => {
- render(await Description({}))
+ it('should have proper heading hierarchy', () => {
+ render()
const h1 = screen.getByRole('heading', { level: 1 })
const h2 = screen.getByRole('heading', { level: 2 })
@@ -583,22 +544,22 @@ describe('Description', () => {
expect(h2).toBeInTheDocument()
})
- it('should have readable text content', async () => {
- render(await Description({}))
+ it('should have readable text content', () => {
+ render()
const h1 = screen.getByRole('heading', { level: 1 })
expect(h1.textContent).not.toBe('')
})
- it('should have visible h1 heading', async () => {
- render(await Description({}))
+ it('should have visible h1 heading', () => {
+ render()
const heading = screen.getByRole('heading', { level: 1 })
expect(heading).toBeVisible()
})
- it('should have visible h2 heading', async () => {
- render(await Description({}))
+ it('should have visible h2 heading', () => {
+ render()
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading).toBeVisible()
@@ -615,8 +576,8 @@ describe('Description Integration', () => {
mockDefaultLocale = 'en-US'
})
- it('should render complete component structure', async () => {
- const { container } = render(await Description({ locale: 'en-US' }))
+ it('should render complete component structure', () => {
+ const { container } = render()
// Main headings
expect(container.querySelector('h1')).toBeInTheDocument()
@@ -627,8 +588,9 @@ describe('Description Integration', () => {
expect(categorySpans.length).toBe(7)
})
- it('should render complete zh-Hans structure', async () => {
- const { container } = render(await Description({ locale: 'zh-Hans' }))
+ it('should render complete zh-Hans structure', () => {
+ mockDefaultLocale = 'zh-Hans'
+ const { container } = render()
// Main headings
expect(container.querySelector('h1')).toBeInTheDocument()
@@ -639,14 +601,16 @@ describe('Description Integration', () => {
expect(categorySpans.length).toBe(7)
})
- it('should correctly switch between zh-Hans and en-US layouts', async () => {
+ it('should correctly differentiate between zh-Hans and en-US layouts', () => {
// Render en-US
- const { container: enContainer, unmount: unmountEn } = render(await Description({ locale: 'en-US' }))
+ mockDefaultLocale = 'en-US'
+ const { container: enContainer, unmount: unmountEn } = render()
const enContent = enContainer.querySelector('h2')?.textContent || ''
unmountEn()
// Render zh-Hans
- const { container: zhContainer } = render(await Description({ locale: 'zh-Hans' }))
+ mockDefaultLocale = 'zh-Hans'
+ const { container: zhContainer } = render()
const zhContent = zhContainer.querySelector('h2')?.textContent || ''
// Both should have all categories
@@ -666,14 +630,16 @@ describe('Description Integration', () => {
expect(zhMarketplaceIndex).toBeLessThan(zhDiscoverIndex)
})
- it('should maintain consistent styling across locales', async () => {
+ it('should maintain consistent styling across locales', () => {
// Render en-US
- const { container: enContainer, unmount: unmountEn } = render(await Description({ locale: 'en-US' }))
+ mockDefaultLocale = 'en-US'
+ const { container: enContainer, unmount: unmountEn } = render()
const enCategoryCount = enContainer.querySelectorAll('.body-md-medium').length
unmountEn()
// Render zh-Hans
- const { container: zhContainer } = render(await Description({ locale: 'zh-Hans' }))
+ mockDefaultLocale = 'zh-Hans'
+ const { container: zhContainer } = render()
const zhCategoryCount = zhContainer.querySelectorAll('.body-md-medium').length
// Both should have same number of styled category spans
diff --git a/web/app/components/plugins/marketplace/description/index.tsx b/web/app/components/plugins/marketplace/description/index.tsx
index d3ca964538..30ccbdb76e 100644
--- a/web/app/components/plugins/marketplace/description/index.tsx
+++ b/web/app/components/plugins/marketplace/description/index.tsx
@@ -1,17 +1,11 @@
-/* eslint-disable dify-i18n/require-ns-option */
-import type { Locale } from '@/i18n-config'
-import { getLocaleOnServer, getTranslation } from '@/i18n-config/server'
+import { useLocale, useTranslation } from '#i18n'
-type DescriptionProps = {
- locale?: Locale
-}
-const Description = async ({
- locale: localeFromProps,
-}: DescriptionProps) => {
- const localeDefault = await getLocaleOnServer()
- const { t } = await getTranslation(localeFromProps || localeDefault, 'plugin')
- const { t: tCommon } = await getTranslation(localeFromProps || localeDefault, 'common')
- const isZhHans = localeFromProps === 'zh-Hans'
+const Description = () => {
+ const { t } = useTranslation('plugin')
+ const { t: tCommon } = useTranslation('common')
+ const locale = useLocale()
+
+ const isZhHans = locale === 'zh-Hans'
return (
<>
diff --git a/web/app/components/plugins/marketplace/index.tsx b/web/app/components/plugins/marketplace/index.tsx
index ff9a4d60bc..f9f7e86b9a 100644
--- a/web/app/components/plugins/marketplace/index.tsx
+++ b/web/app/components/plugins/marketplace/index.tsx
@@ -42,7 +42,7 @@ const Marketplace = async ({
scrollContainerId={scrollContainerId}
showSearchParams={showSearchParams}
>
-
+
(null)
@@ -35,15 +35,14 @@ const getOrCreateI18next = async (lng: Locale) => {
return instance
}
-export async function getTranslation(lng: Locale, ns: NamespaceKebabCase) {
- const camelNs = camelCase(ns) as NamespaceCamelCase
+export async function getTranslation(lng: Locale, ns?: NamespaceCamelCase) {
const i18nextInstance = await getOrCreateI18next(lng)
- if (!i18nextInstance.hasLoadedNamespace(camelNs))
- await i18nextInstance.loadNamespaces(camelNs)
+ if (ns && !i18nextInstance.hasLoadedNamespace(ns))
+ await i18nextInstance.loadNamespaces(ns)
return {
- t: i18nextInstance.getFixedT(lng, camelNs),
+ t: i18nextInstance.getFixedT(lng, ns),
i18n: i18nextInstance,
}
}
diff --git a/web/package.json b/web/package.json
index 05933c08f7..2385a463b6 100644
--- a/web/package.json
+++ b/web/package.json
@@ -4,6 +4,12 @@
"version": "1.11.2",
"private": true,
"packageManager": "pnpm@10.26.2+sha512.0e308ff2005fc7410366f154f625f6631ab2b16b1d2e70238444dd6ae9d630a8482d92a451144debc492416896ed16f7b114a86ec68b8404b2443869e68ffda6",
+ "imports": {
+ "#i18n": {
+ "react-server": "./i18n-config/lib.server.ts",
+ "default": "./i18n-config/lib.client.ts"
+ }
+ },
"engines": {
"node": ">=v22.11.0"
},
diff --git a/web/utils/server-only-context.ts b/web/utils/server-only-context.ts
index e58dbfe98b..43fe743811 100644
--- a/web/utils/server-only-context.ts
+++ b/web/utils/server-only-context.ts
@@ -2,7 +2,7 @@
import { cache } from 'react'
-export default (defaultValue: T): [() => T, (v: T) => void] => {
+export function serverOnlyContext(defaultValue: T): [() => T, (v: T) => void] {
const getRef = cache(() => ({ current: defaultValue }))
const getValue = (): T => getRef().current