feat: wires up server-action for language translation loading (#5346)
This commit is contained in:
@@ -133,7 +133,7 @@ export const CollectionArchiveByCollection: React.FC<Props> = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
makeRequest()
|
||||
void makeRequest()
|
||||
|
||||
return () => {
|
||||
if (timer) clearTimeout(timer)
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { SanitizedConfig } from 'payload/types'
|
||||
import { translations } from '@payloadcms/translations/client'
|
||||
import { RootProvider, buildComponentMap } from '@payloadcms/ui'
|
||||
import '@payloadcms/ui/scss/app.scss'
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js'
|
||||
import { parseCookies } from 'payload/auth'
|
||||
import { createClientConfig } from 'payload/config'
|
||||
import { deepMerge } from 'payload/utilities'
|
||||
@@ -37,6 +37,7 @@ export const RootLayout = async ({
|
||||
|
||||
const lang =
|
||||
getRequestLanguage({
|
||||
config,
|
||||
cookies,
|
||||
headers,
|
||||
}) ?? clientConfig.i18n.fallbackLanguage
|
||||
@@ -50,6 +51,16 @@ export const RootLayout = async ({
|
||||
value: language,
|
||||
}))
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
async function switchLanguageServerAction(lang: string): Promise<void> {
|
||||
'use server'
|
||||
nextCookies().set({
|
||||
name: `${config.cookiePrefix || 'payload'}-lng'`,
|
||||
path: '/',
|
||||
value: lang,
|
||||
})
|
||||
}
|
||||
|
||||
const { componentMap, wrappedChildren } = buildComponentMap({
|
||||
DefaultCell,
|
||||
DefaultEditView,
|
||||
@@ -67,6 +78,8 @@ export const RootLayout = async ({
|
||||
fallbackLang={clientConfig.i18n.fallbackLanguage}
|
||||
lang={lang}
|
||||
languageOptions={languageOptions}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
switchLanguageServerAction={switchLanguageServerAction}
|
||||
translations={mergedTranslations[lang]}
|
||||
>
|
||||
{wrappedChildren}
|
||||
|
||||
@@ -67,6 +67,7 @@ export const createPayloadRequest = async ({
|
||||
}
|
||||
|
||||
const language = getRequestLanguage({
|
||||
config,
|
||||
cookies,
|
||||
headers: request.headers,
|
||||
})
|
||||
|
||||
@@ -7,19 +7,16 @@ import { cookies, headers } from 'next/headers.js'
|
||||
|
||||
import { getRequestLanguage } from './getRequestLanguage.js'
|
||||
|
||||
export const getNextI18n = async ({
|
||||
export const getNextI18n = ({
|
||||
config,
|
||||
language,
|
||||
}: {
|
||||
config: SanitizedConfig
|
||||
language?: string
|
||||
}): Promise<I18n> => {
|
||||
const i18n = initI18n({
|
||||
}): I18n =>
|
||||
initI18n({
|
||||
config: config.i18n,
|
||||
context: 'client',
|
||||
language: language || getRequestLanguage({ cookies: cookies(), headers: headers() }),
|
||||
language: language || getRequestLanguage({ config, cookies: cookies(), headers: headers() }),
|
||||
translations,
|
||||
})
|
||||
|
||||
return i18n
|
||||
}
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
import type { ReadonlyRequestCookies } from 'next/dist/server/web/spec-extension/adapters/request-cookies.js'
|
||||
import type { SanitizedConfig } from 'packages/payload/src/exports/types.js'
|
||||
|
||||
import { matchLanguage } from '@payloadcms/translations'
|
||||
|
||||
type GetRequestLanguageArgs = {
|
||||
config: SanitizedConfig
|
||||
cookies: Map<string, string> | ReadonlyRequestCookies
|
||||
defaultLanguage?: string
|
||||
headers: Request['headers']
|
||||
}
|
||||
|
||||
export const getRequestLanguage = ({
|
||||
config,
|
||||
cookies,
|
||||
defaultLanguage = 'en',
|
||||
headers,
|
||||
}: GetRequestLanguageArgs): string => {
|
||||
const acceptLanguage = headers.get('Accept-Language')
|
||||
const cookieLanguage = cookies.get('lng')
|
||||
const cookieLanguage = cookies.get(`${config.cookiePrefix || 'payload'}-lng'`)
|
||||
|
||||
const reqLanguage =
|
||||
acceptLanguage ||
|
||||
(typeof cookieLanguage === 'string' ? cookieLanguage : cookieLanguage?.value) ||
|
||||
acceptLanguage ||
|
||||
defaultLanguage
|
||||
|
||||
return matchLanguage(reqLanguage)
|
||||
|
||||
@@ -62,7 +62,7 @@ export const initPage = async ({
|
||||
localization && localization.defaultLocale ? localization.defaultLocale : 'en'
|
||||
const localeCode = localeParam || defaultLocale
|
||||
const locale = localization && findLocaleFromCode(localization, localeCode)
|
||||
const language = getRequestLanguage({ cookies, headers })
|
||||
const language = getRequestLanguage({ config: payload.config, cookies, headers })
|
||||
|
||||
const i18n = initI18n({
|
||||
config: payload.config.i18n,
|
||||
|
||||
@@ -13,7 +13,7 @@ export const Settings: React.FC<{
|
||||
}> = (props) => {
|
||||
const { className } = props
|
||||
|
||||
const { i18n, languageOptions, t } = useTranslation()
|
||||
const { i18n, languageOptions, switchLanguage, t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={[baseClass, className].filter(Boolean).join(' ')}>
|
||||
@@ -22,8 +22,9 @@ export const Settings: React.FC<{
|
||||
<Label htmlFor="language-select" label={t('general:language')} />
|
||||
<ReactSelect
|
||||
inputId="language-select"
|
||||
// TODO(i18n): wire up onChange / changeLanguage fn
|
||||
// onChange={({ value }) => i18n.changeLanguage(value)}
|
||||
onChange={async ({ value }) => {
|
||||
await switchLanguage(value)
|
||||
}}
|
||||
options={languageOptions}
|
||||
value={languageOptions.find((language) => language.value === i18n.language)}
|
||||
/>
|
||||
|
||||
@@ -61,7 +61,7 @@ type GetCookieExpirationArgs = {
|
||||
*/
|
||||
seconds: number
|
||||
}
|
||||
const getCookieExpiration = ({ seconds = 7200 }: GetCookieExpirationArgs) => {
|
||||
export const getCookieExpiration = ({ seconds = 7200 }: GetCookieExpirationArgs) => {
|
||||
const currentTime = new Date()
|
||||
currentTime.setSeconds(currentTime.getSeconds() + seconds)
|
||||
return currentTime
|
||||
|
||||
@@ -12,20 +12,37 @@
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
"access": "public",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/exports/index.ts",
|
||||
"require": "./dist/exports/index.ts",
|
||||
"types": "./dist/exports/*.d.ts"
|
||||
},
|
||||
"./api": {
|
||||
"import": "./dist/_generatedFiles_/api/index.ts",
|
||||
"require": "./dist/_generatedFiles_/api/index.ts",
|
||||
"types": "./dist/_generatedFiles_/exports/*.d.ts"
|
||||
},
|
||||
"./client": {
|
||||
"import": "./dist/_generatedFiles_/client/index.ts",
|
||||
"require": "./dist/_generatedFiles_/client/index.ts",
|
||||
"types": "./dist/_generatedFiles_/exports/*.d.ts"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/exports/index.js",
|
||||
"require": "./dist/exports/index.js"
|
||||
"import": "./src/exports/index.ts",
|
||||
"require": "./src/exports/index.ts"
|
||||
},
|
||||
"./api": {
|
||||
"import": "./dist/_generatedFiles_/api/index.js",
|
||||
"require": "./dist/_generatedFiles_/api/index.js"
|
||||
"import": "./src/_generatedFiles_/api/index.ts",
|
||||
"require": "./src/_generatedFiles_/api/index.ts"
|
||||
},
|
||||
"./client": {
|
||||
"import": "./dist/_generatedFiles_/client/index.js",
|
||||
"require": "./dist/_generatedFiles_/client/index.js"
|
||||
"import": "./src/_generatedFiles_/client/index.ts",
|
||||
"require": "./src/_generatedFiles_/client/index.ts"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -5,11 +5,7 @@ export type LanguageTranslations = {
|
||||
}
|
||||
|
||||
export type Translations = {
|
||||
[language: string]:
|
||||
| {
|
||||
$schema: string
|
||||
}
|
||||
| LanguageTranslations
|
||||
[language: string]: LanguageTranslations
|
||||
}
|
||||
|
||||
export type TFunction = (key: string, options?: Record<string, any>) => string
|
||||
|
||||
@@ -228,12 +228,7 @@ function memoize(fn: Function, keys: string[]) {
|
||||
}
|
||||
|
||||
export const initI18n: InitI18n = memoize(
|
||||
({
|
||||
config,
|
||||
context,
|
||||
language = 'en',
|
||||
translations: incomingTranslations,
|
||||
}: Parameters<InitI18n>[0]) => {
|
||||
({ config, language = 'en', translations: incomingTranslations }: Parameters<InitI18n>[0]) => {
|
||||
const { t, translations } = initTFunction({
|
||||
config,
|
||||
language: language || config.fallbackLanguage,
|
||||
|
||||
@@ -14,8 +14,7 @@ export const LocalizerLabel: React.FC<{
|
||||
}> = (props) => {
|
||||
const { ariaLabel, className } = props
|
||||
const locale = useLocale()
|
||||
const { t } = useTranslation()
|
||||
const { i18n } = useTranslation()
|
||||
const { i18n, t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -37,18 +37,18 @@ const Relationship: React.FC<RelationshipFieldProps> = (props) => {
|
||||
Description,
|
||||
Error,
|
||||
Label,
|
||||
allowCreate = true,
|
||||
className,
|
||||
hasMany,
|
||||
isSortable = true,
|
||||
path: pathFromProps,
|
||||
readOnly,
|
||||
relationTo,
|
||||
required,
|
||||
sortOptions,
|
||||
style,
|
||||
validate,
|
||||
width,
|
||||
relationTo,
|
||||
hasMany,
|
||||
sortOptions,
|
||||
isSortable = true,
|
||||
allowCreate = true,
|
||||
} = props
|
||||
|
||||
const config = useConfig()
|
||||
|
||||
@@ -36,18 +36,18 @@ export const Select: React.FC<SelectFieldProps> = (props) => {
|
||||
Error,
|
||||
Label: LabelFromProps,
|
||||
className,
|
||||
hasMany = false,
|
||||
isClearable = true,
|
||||
isSortable = true,
|
||||
label,
|
||||
onChange: onChangeFromProps,
|
||||
options: optionsFromProps = [],
|
||||
path: pathFromProps,
|
||||
readOnly,
|
||||
required,
|
||||
style,
|
||||
validate,
|
||||
width,
|
||||
options: optionsFromProps = [],
|
||||
hasMany = false,
|
||||
isClearable = true,
|
||||
isSortable = true,
|
||||
} = props
|
||||
|
||||
const Label = LabelFromProps || <LabelComp label={label} required={required} />
|
||||
|
||||
@@ -33,11 +33,11 @@ const Textarea: React.FC<TextareaFieldProps> = (props) => {
|
||||
path: pathFromProps,
|
||||
placeholder,
|
||||
required,
|
||||
rows,
|
||||
rtl,
|
||||
style,
|
||||
validate,
|
||||
width,
|
||||
rows,
|
||||
} = props
|
||||
|
||||
const Label = LabelFromProps || <LabelComp label={label} required={required} />
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
'use client'
|
||||
import type { FieldTypes } from 'payload/config.js'
|
||||
|
||||
import React, { createContext, useContext } from 'react'
|
||||
|
||||
import { fieldComponents } from '../../forms/fields/index.js'
|
||||
import { FieldTypes } from 'payload/config.js'
|
||||
|
||||
export type IFieldComponentsContext = FieldTypes
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import { ClientFunctionProvider } from '../ClientFunction/index.js'
|
||||
import { ComponentMapProvider } from '../ComponentMapProvider/index.js'
|
||||
import { ConfigProvider } from '../Config/index.js'
|
||||
import { DocumentEventsProvider } from '../DocumentEvents/index.js'
|
||||
import { FieldComponentsProvider } from '../FieldComponentsProvider/index.js'
|
||||
import { LocaleProvider } from '../Locale/index.js'
|
||||
import { ParamsProvider } from '../Params/index.js'
|
||||
import { PreferencesProvider } from '../Preferences/index.js'
|
||||
@@ -27,7 +28,6 @@ import { RouteCache } from '../RouteCache/index.js'
|
||||
import { SearchParamsProvider } from '../SearchParams/index.js'
|
||||
import { ThemeProvider } from '../Theme/index.js'
|
||||
import { TranslationProvider } from '../Translation/index.js'
|
||||
import { FieldComponentsProvider } from '../FieldComponentsProvider/index.js'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
@@ -36,6 +36,7 @@ type Props = {
|
||||
fallbackLang: ClientConfig['i18n']['fallbackLanguage']
|
||||
lang: string
|
||||
languageOptions: LanguageOptions
|
||||
switchLanguageServerAction?: (lang: string) => Promise<void>
|
||||
translations: LanguageTranslations
|
||||
}
|
||||
|
||||
@@ -46,6 +47,7 @@ export const RootProvider: React.FC<Props> = ({
|
||||
fallbackLang,
|
||||
lang,
|
||||
languageOptions,
|
||||
switchLanguageServerAction,
|
||||
translations,
|
||||
}) => {
|
||||
const { ModalContainer, ModalProvider } = facelessUIImport || {
|
||||
@@ -66,6 +68,7 @@ export const RootProvider: React.FC<Props> = ({
|
||||
fallbackLang={fallbackLang}
|
||||
lang={lang}
|
||||
languageOptions={languageOptions}
|
||||
switchLanguageServerAction={switchLanguageServerAction}
|
||||
translations={translations}
|
||||
>
|
||||
<WindowInfoProvider
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
'use client'
|
||||
import type { I18n, LanguageTranslations, Translations } from '@payloadcms/translations'
|
||||
import type { I18n, LanguageTranslations } from '@payloadcms/translations'
|
||||
import type { ClientConfig } from 'payload/types'
|
||||
|
||||
import { t } from '@payloadcms/translations'
|
||||
import React, { createContext, useContext } from 'react'
|
||||
|
||||
import { useRouteCache } from '../RouteCache/index.js'
|
||||
|
||||
export type LanguageOptions = {
|
||||
label: string
|
||||
value: string
|
||||
@@ -13,6 +15,7 @@ export type LanguageOptions = {
|
||||
const Context = createContext<{
|
||||
i18n: I18n
|
||||
languageOptions: LanguageOptions
|
||||
switchLanguage?: (lang: string) => Promise<void>
|
||||
t: (key: string, vars?: Record<string, any>) => string
|
||||
}>({
|
||||
i18n: {
|
||||
@@ -22,23 +25,49 @@ const Context = createContext<{
|
||||
translations: {},
|
||||
},
|
||||
languageOptions: undefined,
|
||||
switchLanguage: undefined,
|
||||
t: (key) => key,
|
||||
})
|
||||
|
||||
export const TranslationProvider: React.FC<{
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
fallbackLang: ClientConfig['i18n']['fallbackLanguage']
|
||||
lang: string
|
||||
languageOptions: LanguageOptions
|
||||
switchLanguageServerAction: (lang: string) => Promise<void>
|
||||
translations: LanguageTranslations
|
||||
}> = ({ children, fallbackLang, lang, languageOptions, translations }) => {
|
||||
const nextT = (key: string, vars?: Record<string, any>): string =>
|
||||
}
|
||||
|
||||
export const TranslationProvider: React.FC<Props> = ({
|
||||
children,
|
||||
fallbackLang,
|
||||
lang,
|
||||
languageOptions,
|
||||
switchLanguageServerAction,
|
||||
translations,
|
||||
}) => {
|
||||
const { clearRouteCache } = useRouteCache()
|
||||
|
||||
const nextT = (key: string, vars?: Record<string, unknown>): string =>
|
||||
t({
|
||||
key,
|
||||
translations,
|
||||
vars,
|
||||
})
|
||||
|
||||
const switchLanguage = React.useCallback(
|
||||
async (lang: string) => {
|
||||
try {
|
||||
await switchLanguageServerAction(lang)
|
||||
clearRouteCache()
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Error loading language: "${lang}"`, error)
|
||||
}
|
||||
},
|
||||
[switchLanguageServerAction, clearRouteCache],
|
||||
)
|
||||
|
||||
return (
|
||||
<Context.Provider
|
||||
value={{
|
||||
@@ -46,9 +75,12 @@ export const TranslationProvider: React.FC<{
|
||||
fallbackLanguage: fallbackLang,
|
||||
language: lang,
|
||||
t: nextT,
|
||||
translations: translations as Translations,
|
||||
translations: {
|
||||
[lang]: translations,
|
||||
},
|
||||
},
|
||||
languageOptions,
|
||||
switchLanguage,
|
||||
t: nextT,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Fragment } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
import type { Props } from './types.js'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user