feat: init rsc support for translation (#30596)

This commit is contained in:
Stephen Zhou 2026-01-06 13:23:03 +08:00 committed by GitHub
parent 89463cc11d
commit b2124a7358
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 202 additions and 214 deletions

View File

@ -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 (
<div className="h-full overflow-y-auto">

View File

@ -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<string, string> = {
'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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
// 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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
// 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(<Description />)
// 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(<Description />)
// 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(<Description />)
// 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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
// 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(<Description />)
// 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(<Description />)
// 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(<Description />)
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(<Description />)
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(<Description />)
// 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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
// 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(<Description />)
// 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(<Description />)
// 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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
// 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(<Description />)
// 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(<Description />)
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(<Description />)
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(<Description />)
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(<Description />)
const zhCategoryCount = zhContainer.querySelectorAll('.body-md-medium').length
// Both should have same number of styled category spans

View File

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

View File

@ -42,7 +42,7 @@ const Marketplace = async ({
scrollContainerId={scrollContainerId}
showSearchParams={showSearchParams}
>
<Description locale={locale} />
<Description />
<StickySearchAndSwitchWrapper
locale={locale}
pluginTypeSwitchClassName={pluginTypeSwitchClassName}

View File

@ -179,7 +179,7 @@ export default antfu(
// 'dify-i18n/no-as-any-in-t': ['error', { mode: 'all' }],
'dify-i18n/no-as-any-in-t': 'error',
// 'dify-i18n/no-legacy-namespace-prefix': 'error',
'dify-i18n/require-ns-option': 'error',
// 'dify-i18n/require-ns-option': 'error',
},
},
// i18n JSON validation rules

View File

@ -0,0 +1,10 @@
'use client'
import type { NamespaceCamelCase } from './i18next-config'
import { useTranslation as useTranslationOriginal } from 'react-i18next'
export function useTranslation(ns?: NamespaceCamelCase) {
return useTranslationOriginal(ns)
}
export { useLocale } from '@/context/i18n'

View File

@ -0,0 +1,16 @@
import type { NamespaceCamelCase } from './i18next-config'
import { use } from 'react'
import { getLocaleOnServer, getTranslation } from './server'
async function getI18nConfig(ns?: NamespaceCamelCase) {
const lang = await getLocaleOnServer()
return getTranslation(lang, ns)
}
export function useTranslation(ns?: NamespaceCamelCase) {
return use(getI18nConfig(ns))
}
export function useLocale() {
return use(getLocaleOnServer())
}

View File

@ -2,13 +2,13 @@ import type { i18n as I18nInstance } from 'i18next'
import type { Locale } from '.'
import type { NamespaceCamelCase, NamespaceKebabCase } from './i18next-config'
import { match } from '@formatjs/intl-localematcher'
import { camelCase, kebabCase } from 'es-toolkit/compat'
import { kebabCase } from 'es-toolkit/compat'
import { createInstance } from 'i18next'
import resourcesToBackend from 'i18next-resources-to-backend'
import Negotiator from 'negotiator'
import { cookies, headers } from 'next/headers'
import { initReactI18next } from 'react-i18next/initReactI18next'
import serverOnlyContext from '@/utils/server-only-context'
import { serverOnlyContext } from '@/utils/server-only-context'
import { i18n } from '.'
const [getLocaleCache, setLocaleCache] = serverOnlyContext<Locale | null>(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,
}
}

View File

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

View File

@ -2,7 +2,7 @@
import { cache } from 'react'
export default <T>(defaultValue: T): [() => T, (v: T) => void] => {
export function serverOnlyContext<T>(defaultValue: T): [() => T, (v: T) => void] {
const getRef = cache(() => ({ current: defaultValue }))
const getValue = (): T => getRef().current