mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
fix(web): deduplicate server locale resolution (#42187)
This commit is contained in:
3 files changed
+51
-28
No files matched your search
@@ -1,8 +1,13 @@
|
||||
// @vitest-environment node
|
||||
|
||||
const mocks = vi.hoisted(() => ({ loadResource: vi.fn() }))
|
||||
const mocks = vi.hoisted(() => ({
|
||||
loadResource: vi.fn(),
|
||||
cookies: vi.fn(),
|
||||
headers: vi.fn(),
|
||||
getCookie: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/next/headers', () => ({ cookies: vi.fn(), headers: vi.fn() }))
|
||||
vi.mock('@/next/headers', () => ({ cookies: mocks.cookies, headers: mocks.headers }))
|
||||
vi.mock('../load-resource', () => ({ loadI18nResource: mocks.loadResource }))
|
||||
|
||||
// Unit tests have no RSC dispatcher; provide its memoization boundary here.
|
||||
@@ -72,3 +77,44 @@ describe('server translations', () => {
|
||||
expect(english.t(($) => $['operation.save'], { ns: 'common' })).toBe('Save')
|
||||
})
|
||||
})
|
||||
|
||||
describe('server locale', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.resetModules()
|
||||
mocks.getCookie.mockReturnValue(undefined)
|
||||
mocks.cookies.mockResolvedValue({ get: mocks.getCookie })
|
||||
mocks.headers.mockResolvedValue(new Headers())
|
||||
})
|
||||
|
||||
it('resolves the request locale once for concurrent and subsequent consumers', async () => {
|
||||
mocks.headers.mockResolvedValue(new Headers({ 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8' }))
|
||||
const { getLocaleOnServer } = await import('../server')
|
||||
|
||||
await expect(
|
||||
Promise.all([getLocaleOnServer(), getLocaleOnServer(), getLocaleOnServer()]),
|
||||
).resolves.toEqual(['zh-Hans', 'zh-Hans', 'zh-Hans'])
|
||||
await expect(getLocaleOnServer()).resolves.toBe('zh-Hans')
|
||||
expect(mocks.getCookie).toHaveBeenCalledExactlyOnceWith('locale')
|
||||
expect(mocks.headers).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('prefers the locale cookie over the browser language', async () => {
|
||||
mocks.getCookie.mockReturnValue({ value: 'zh-Hans' })
|
||||
mocks.headers.mockResolvedValue(new Headers({ 'accept-language': 'en-US' }))
|
||||
const { getLocaleOnServer } = await import('../server')
|
||||
|
||||
await expect(getLocaleOnServer()).resolves.toBe('zh-Hans')
|
||||
expect(mocks.headers).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each([undefined, 'bad value', 'zz-ZZ'])(
|
||||
'uses the default language for an absent or unusable preference: %s',
|
||||
async (locale) => {
|
||||
mocks.getCookie.mockReturnValue(locale ? { value: locale } : undefined)
|
||||
const { getLocaleOnServer } = await import('../server')
|
||||
|
||||
await expect(getLocaleOnServer()).resolves.toBe('en-US')
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -9,14 +9,11 @@ import Negotiator from 'negotiator'
|
||||
import { cache } from 'react'
|
||||
import { initReactI18next } from 'react-i18next/initReactI18next'
|
||||
import { cookies, headers } from '@/next/headers'
|
||||
import { serverOnlyContext } from '@/utils/server-only-context'
|
||||
import { i18n } from '.'
|
||||
import { loadI18nResource } from './load-resource'
|
||||
import { namespacesInFileName } from './resources'
|
||||
import { getInitOptions } from './settings'
|
||||
|
||||
const [getLocaleCache, setLocaleCache] = serverOnlyContext<Locale | null>(null)
|
||||
|
||||
const getOrCreateI18next = cache(async (lng: Locale) => {
|
||||
const instance = createInstance()
|
||||
await instance
|
||||
@@ -44,10 +41,7 @@ export async function getTranslation<T extends Namespace>(lng: Locale, ns?: T) {
|
||||
}
|
||||
}
|
||||
|
||||
export const getLocaleOnServer = async (): Promise<Locale> => {
|
||||
const cached = getLocaleCache()
|
||||
if (cached) return cached
|
||||
|
||||
export const getLocaleOnServer = cache(async (): Promise<Locale> => {
|
||||
const locales: string[] = i18n.locales
|
||||
|
||||
let languages: string[] | undefined
|
||||
@@ -72,10 +66,8 @@ export const getLocaleOnServer = async (): Promise<Locale> => {
|
||||
languages = [i18n.defaultLocale]
|
||||
|
||||
// match locale
|
||||
const matchedLocale = match(languages, locales, i18n.defaultLocale) as Locale
|
||||
setLocaleCache(matchedLocale)
|
||||
return matchedLocale
|
||||
}
|
||||
return match(languages, locales, i18n.defaultLocale) as Locale
|
||||
})
|
||||
|
||||
export const getResources = cache(async (lng: Locale): Promise<Resource> => {
|
||||
const messages = {} as ResourceLanguage
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
// credit: https://github.com/manvalls/server-only-context/blob/main/src/index.ts
|
||||
|
||||
import { cache } from 'react'
|
||||
|
||||
export function serverOnlyContext<T>(defaultValue: T): [() => T, (v: T) => void] {
|
||||
const getRef = cache(() => ({ current: defaultValue }))
|
||||
|
||||
const getValue = (): T => getRef().current
|
||||
|
||||
const setValue = (value: T) => {
|
||||
getRef().current = value
|
||||
}
|
||||
|
||||
return [getValue, setValue]
|
||||
}
|
||||
Reference in new issue
Block a user