feat: adds memoization to translation functions (#5036)

This commit is contained in:
Jarrod Flesch
2024-02-08 15:23:50 -05:00
committed by GitHub
parent 78a45fc92d
commit a8ac42037b
37 changed files with 134 additions and 99 deletions

View File

@@ -24,7 +24,7 @@ export const generateMetadata = async ({
}: { }: {
config: Promise<SanitizedConfig> config: Promise<SanitizedConfig>
}): Promise<Metadata> => { }): Promise<Metadata> => {
const t = getNextT({ const t = await getNextT({
config: await config, config: await config,
}) })

View File

@@ -15,7 +15,7 @@ export const generateMetadata = async ({
}: { }: {
config: Promise<SanitizedConfig> config: Promise<SanitizedConfig>
}): Promise<Metadata> => { }): Promise<Metadata> => {
const t = getNextT({ const t = await getNextT({
config: await config, config: await config,
}) })

View File

@@ -18,7 +18,7 @@ export const generateMetadata = async ({
}: { }: {
config: Promise<SanitizedConfig> config: Promise<SanitizedConfig>
}): Promise<Metadata> => { }): Promise<Metadata> => {
const t = getNextT({ const t = await getNextT({
config: await config, config: await config,
}) })

View File

@@ -15,7 +15,7 @@ export const generateMetadata = async ({
}: { }: {
config: Promise<SanitizedConfig> config: Promise<SanitizedConfig>
}): Promise<Metadata> => { }): Promise<Metadata> => {
const t = getNextT({ const t = await getNextT({
config: await config, config: await config,
}) })

View File

@@ -25,7 +25,7 @@ export const generateMetadata = async ({
}: { }: {
config: Promise<SanitizedConfig> config: Promise<SanitizedConfig>
}): Promise<Metadata> => { }): Promise<Metadata> => {
const t = getNextT({ const t = await getNextT({
config: await config, config: await config,
}) })

View File

@@ -12,7 +12,7 @@ export const generateMetadata = async ({
}: { }: {
config: Promise<SanitizedConfig> config: Promise<SanitizedConfig>
}): Promise<Metadata> => { }): Promise<Metadata> => {
const t = getNextT({ const t = await getNextT({
config: await config, config: await config,
}) })

View File

@@ -16,7 +16,7 @@ export const generateMetadata = async ({
}: { }: {
config: Promise<SanitizedConfig> config: Promise<SanitizedConfig>
}): Promise<Metadata> => { }): Promise<Metadata> => {
const t = getNextT({ const t = await getNextT({
config: await config, config: await config,
}) })

View File

@@ -8,9 +8,9 @@ import { getAuthenticatedUser } from 'payload/auth'
import { getPayload } from 'payload' import { getPayload } from 'payload'
import { URL } from 'url' import { URL } from 'url'
import { parseCookies } from 'payload/auth' import { parseCookies } from 'payload/auth'
import { initI18n } from '@payloadcms/translations'
import { getRequestLanguage } from './getRequestLanguage' import { getRequestLanguage } from './getRequestLanguage'
import { getRequestLocales } from './getRequestLocales' import { getRequestLocales } from './getRequestLocales'
import { getNextI18n } from './getNextI18n'
import { getDataAndFile } from './getDataAndFile' import { getDataAndFile } from './getDataAndFile'
type Args = { type Args = {
@@ -61,10 +61,10 @@ export const createPayloadRequest = async ({
cookies, cookies,
}) })
const i18n = getNextI18n({ const i18n = await initI18n({
config, config: config.i18n,
language, language,
translationContext: 'api', translationsContext: 'api',
}) })
const customRequest: CustomPayloadRequest = { const customRequest: CustomPayloadRequest = {

View File

@@ -1,20 +0,0 @@
import { SanitizedConfig } from 'payload/types'
import { initI18n } from '@payloadcms/translations'
import { translations as clientTranslations } from '@payloadcms/translations/client'
import { translations as apiTranslations } from '@payloadcms/translations/api'
export const getNextI18n = ({
config,
language,
translationContext = 'client',
}: {
config: SanitizedConfig
language: string
translationContext?: 'api' | 'client'
}): ReturnType<typeof initI18n> => {
return initI18n({
config: config.i18n,
language,
translations: translationContext === 'api' ? apiTranslations : clientTranslations,
})
}

View File

@@ -1,24 +1,23 @@
import { translations } from '@payloadcms/translations/client'
import type { TFunction } from '@payloadcms/translations' import type { TFunction } from '@payloadcms/translations'
import type { SanitizedConfig } from 'payload/types' import type { SanitizedConfig } from 'payload/types'
import { initTFunction } from '@payloadcms/translations' import { initI18n } from '@payloadcms/translations'
import { cookies, headers } from 'next/headers' import { cookies, headers } from 'next/headers'
import { getRequestLanguage } from './getRequestLanguage' import { getRequestLanguage } from './getRequestLanguage'
export const getNextT = ({ export const getNextT = async ({
config, config,
language, language,
}: { }: {
config: SanitizedConfig config: SanitizedConfig
language?: string language?: string
}): TFunction => { }): Promise<TFunction> => {
const lang = language || getRequestLanguage({ cookies: cookies(), headers: headers() }) const i18n = await initI18n({
translationsContext: 'client',
return initTFunction({ language: language || getRequestLanguage({ cookies: cookies(), headers: headers() }),
language: lang,
config: config.i18n, config: config.i18n,
translations,
}) })
return i18n.t
} }

View File

@@ -10,9 +10,10 @@ import type {
} from 'payload/types' } from 'payload/types'
import { redirect } from 'next/navigation' import { redirect } from 'next/navigation'
import { parseCookies } from 'payload/auth' import { parseCookies } from 'payload/auth'
import { getNextI18n } from './getNextI18n'
import { getRequestLanguage } from './getRequestLanguage' import { getRequestLanguage } from './getRequestLanguage'
import { findLocaleFromCode } from '../../../ui/src/utilities/findLocaleFromCode' import { findLocaleFromCode } from '../../../ui/src/utilities/findLocaleFromCode'
import { I18n } from '@payloadcms/translations/types'
import { initI18n } from '@payloadcms/translations'
export const initPage = async ({ export const initPage = async ({
configPromise, configPromise,
@@ -31,7 +32,7 @@ export const initPage = async ({
permissions: Awaited<ReturnType<typeof auth>>['permissions'] permissions: Awaited<ReturnType<typeof auth>>['permissions']
user: Awaited<ReturnType<typeof auth>>['user'] user: Awaited<ReturnType<typeof auth>>['user']
config: SanitizedConfig config: SanitizedConfig
i18n: ReturnType<typeof getNextI18n> i18n: I18n
collectionConfig?: SanitizedCollectionConfig collectionConfig?: SanitizedCollectionConfig
globalConfig?: SanitizedGlobalConfig globalConfig?: SanitizedGlobalConfig
locale: ReturnType<typeof findLocaleFromCode> locale: ReturnType<typeof findLocaleFromCode>
@@ -59,7 +60,11 @@ export const initPage = async ({
config: configPromise, config: configPromise,
}) })
const i18n = getNextI18n({ config, language }) const i18n = await initI18n({
config: config.i18n,
language,
translationsContext: 'client',
})
let collectionConfig: SanitizedCollectionConfig let collectionConfig: SanitizedCollectionConfig
let globalConfig: SanitizedGlobalConfig let globalConfig: SanitizedGlobalConfig

View File

@@ -38,7 +38,7 @@ async function localForgotPassword<T extends keyof GeneratedTypes['collections']
data, data,
disableEmail, disableEmail,
expiration, expiration,
req: createLocalReq(options, payload), req: await createLocalReq(options, payload),
}) })
} }

View File

@@ -47,7 +47,7 @@ async function localLogin<TSlug extends keyof GeneratedTypes['collections']>(
data, data,
depth, depth,
overrideAccess, overrideAccess,
req: createLocalReq(options, payload), req: await createLocalReq(options, payload),
showHiddenFields, showHiddenFields,
} }

View File

@@ -38,7 +38,7 @@ async function localResetPassword<T extends keyof GeneratedTypes['collections']>
collection, collection,
data, data,
overrideAccess, overrideAccess,
req: createLocalReq(options, payload), req: await createLocalReq(options, payload),
}) })
} }

View File

@@ -34,7 +34,7 @@ async function localUnlock<T extends keyof GeneratedTypes['collections']>(
collection, collection,
data, data,
overrideAccess, overrideAccess,
req: createLocalReq(options, payload), req: await createLocalReq(options, payload),
}) })
} }

View File

@@ -29,7 +29,7 @@ async function localVerifyEmail<T extends keyof GeneratedTypes['collections']>(
return verifyEmailOperation({ return verifyEmailOperation({
collection, collection,
req: createLocalReq(options, payload), req: await createLocalReq(options, payload),
token, token,
}) })
} }

View File

@@ -59,7 +59,7 @@ export default async function createLocal<TSlug extends keyof GeneratedTypes['co
) )
} }
const req = createLocalReq(options, payload) const req = await createLocalReq(options, payload)
req.file = file ?? (await getFileByPath(filePath)) req.file = file ?? (await getFileByPath(filePath))
return createOperation<TSlug>({ return createOperation<TSlug>({

View File

@@ -76,7 +76,7 @@ async function deleteLocal<TSlug extends keyof GeneratedTypes['collections']>(
collection, collection,
depth, depth,
overrideAccess, overrideAccess,
req: createLocalReq(options, payload), req: await createLocalReq(options, payload),
showHiddenFields, showHiddenFields,
where, where,
} }

View File

@@ -67,7 +67,7 @@ export default async function findLocal<T extends keyof GeneratedTypes['collecti
overrideAccess, overrideAccess,
page, page,
pagination, pagination,
req: createLocalReq(options, payload), req: await createLocalReq(options, payload),
showHiddenFields, showHiddenFields,
sort, sort,
where, where,

View File

@@ -57,7 +57,7 @@ export default async function findByIDLocal<T extends keyof GeneratedTypes['coll
disableErrors, disableErrors,
draft, draft,
overrideAccess, overrideAccess,
req: createLocalReq(options, payload), req: await createLocalReq(options, payload),
showHiddenFields, showHiddenFields,
}) })
} }

View File

@@ -54,7 +54,7 @@ export default async function findVersionByIDLocal<T extends keyof GeneratedType
depth, depth,
disableErrors, disableErrors,
overrideAccess, overrideAccess,
req: createLocalReq(options, payload), req: await createLocalReq(options, payload),
showHiddenFields, showHiddenFields,
}) })
} }

View File

@@ -57,7 +57,7 @@ export default async function findVersionsLocal<T extends keyof GeneratedTypes['
limit, limit,
overrideAccess, overrideAccess,
page, page,
req: createLocalReq(options, payload), req: await createLocalReq(options, payload),
showHiddenFields, showHiddenFields,
sort, sort,
where, where,

View File

@@ -45,7 +45,7 @@ export default async function restoreVersionLocal<T extends keyof GeneratedTypes
depth, depth,
overrideAccess, overrideAccess,
payload, payload,
req: createLocalReq(options, payload), req: await createLocalReq(options, payload),
showHiddenFields, showHiddenFields,
} }

View File

@@ -86,7 +86,7 @@ async function updateLocal<TSlug extends keyof GeneratedTypes['collections']>(
) )
} }
const req = createLocalReq(options, payload) const req = await createLocalReq(options, payload)
req.file = file ?? (await getFileByPath(filePath)) req.file = file ?? (await getFileByPath(filePath))
const args = { const args = {

View File

@@ -43,7 +43,7 @@ export default async function findOneLocal<T extends keyof GeneratedTypes['globa
draft, draft,
globalConfig, globalConfig,
overrideAccess, overrideAccess,
req: createLocalReq(options, payload), req: await createLocalReq(options, payload),
showHiddenFields, showHiddenFields,
slug: globalSlug as string, slug: globalSlug as string,
}) })

View File

@@ -47,7 +47,7 @@ export default async function findVersionByIDLocal<T extends keyof GeneratedType
disableErrors, disableErrors,
globalConfig, globalConfig,
overrideAccess, overrideAccess,
req: createLocalReq(options, payload), req: await createLocalReq(options, payload),
showHiddenFields, showHiddenFields,
}) })
} }

View File

@@ -52,7 +52,7 @@ export default async function findVersionsLocal<T extends keyof GeneratedTypes['
limit, limit,
overrideAccess, overrideAccess,
page, page,
req: createLocalReq(options, payload), req: await createLocalReq(options, payload),
showHiddenFields, showHiddenFields,
sort, sort,
where, where,

View File

@@ -37,7 +37,7 @@ export default async function restoreVersionLocal<T extends keyof GeneratedTypes
depth, depth,
globalConfig, globalConfig,
overrideAccess, overrideAccess,
req: createLocalReq(options, payload), req: await createLocalReq(options, payload),
showHiddenFields, showHiddenFields,
}) })
} }

View File

@@ -41,7 +41,7 @@ export default async function updateLocal<TSlug extends keyof GeneratedTypes['gl
draft, draft,
globalConfig, globalConfig,
overrideAccess, overrideAccess,
req: createLocalReq(options, payload), req: await createLocalReq(options, payload),
showHiddenFields, showHiddenFields,
slug: globalSlug as string, slug: globalSlug as string,
}) })

View File

@@ -1,17 +1,16 @@
import { initI18n } from '@payloadcms/translations' import { initI18n } from '@payloadcms/translations'
import { translations } from '@payloadcms/translations/api'
import type { SanitizedConfig } from '../exports/types' import type { SanitizedConfig } from '../exports/types'
export const getLocalI18n = ({ export const getLocalI18n = async ({
config, config,
language = 'en', language = 'en',
}: { }: {
config: SanitizedConfig config: SanitizedConfig
language?: string language?: string
}): ReturnType<typeof initI18n> => }) =>
initI18n({ initI18n({
config: config.i18n, config: config.i18n,
language, language,
translations, translationsContext: 'api',
}) })

View File

@@ -29,12 +29,12 @@ type CreateLocalReq = (
user?: Document user?: Document
}, },
payload: Payload, payload: Payload,
) => PayloadRequest ) => Promise<PayloadRequest>
export const createLocalReq: CreateLocalReq = ( export const createLocalReq: CreateLocalReq = async (
{ context, fallbackLocale, locale, req = {} as PayloadRequest, user }, { context, fallbackLocale, locale, req = {} as PayloadRequest, user },
payload, payload,
) => { ) => {
const i18n = req?.i18n || getLocalI18n({ config: payload.config }) const i18n = req?.i18n || (await getLocalI18n({ config: payload.config }))
if (payload.config?.localization) { if (payload.config?.localization) {
const defaultLocale = payload.config.localization.defaultLocale const defaultLocale = payload.config.localization.defaultLocale

View File

@@ -10,6 +10,11 @@
"prepublishOnly": "pnpm clean && pnpm build" "prepublishOnly": "pnpm clean && pnpm build"
}, },
"exports": { "exports": {
".": {
"import": "./src/exports/index.ts",
"require": "./src/exports/index.ts",
"types": "./src/exports/index.ts"
},
"./api": { "./api": {
"import": "./src/all/index.ts", "import": "./src/all/index.ts",
"require": "./src/all/index.ts", "require": "./src/all/index.ts",
@@ -19,11 +24,6 @@
"import": "./src/all/index.ts", "import": "./src/all/index.ts",
"require": "./src/all/index.ts", "require": "./src/all/index.ts",
"types": "./src/all/index.ts" "types": "./src/all/index.ts"
},
".": {
"import": "./src/exports/index.ts",
"require": "./src/exports/index.ts",
"types": "./src/exports/index.ts"
} }
}, },
"devDependencies": { "devDependencies": {
@@ -33,13 +33,17 @@
}, },
"publishConfig": { "publishConfig": {
"exports": { "exports": {
".": {
"import": "./dist/exports/index.js",
"require": "./dist/exports/index.js"
},
"./api": { "./api": {
"import": "./dist/api/index.ts", "import": "./dist/api/index.js",
"require": "./dist/api/index.ts" "require": "./dist/api/index.js"
}, },
"./client": { "./client": {
"import": "./dist/client/index.ts", "import": "./dist/client/index.js",
"require": "./dist/client/index.ts" "require": "./dist/client/index.js"
} }
}, },
"main": "./dist/exports/index.js", "main": "./dist/exports/index.js",

View File

@@ -1,3 +1,3 @@
export { initI18n, t, initTFunction, matchLanguage } from '../utilities/init' export { initI18n, t, matchLanguage } from '../utilities/init'
export { getTranslation } from '../utilities/getTranslation' export { getTranslation } from '../utilities/getTranslation'
export type * from '../types' export type * from '../types'

View File

@@ -42,3 +42,9 @@ export type InitTFunction = (args: {
language?: string language?: string
translations?: Translations translations?: Translations
}) => TFunction }) => TFunction
export type InitI18n = (args: {
config: I18nOptions
language?: string
translationsContext: 'client' | 'api'
}) => Promise<I18n>

View File

@@ -1,9 +1,9 @@
import type { JSX } from 'react' import type { JSX } from 'react'
import type { initI18n } from './init' import { I18n } from '../types'
export const getTranslation = ( export const getTranslation = (
label: JSX.Element | Record<string, string> | string, label: JSX.Element | Record<string, string> | string,
i18n: Pick<ReturnType<typeof initI18n>, 'fallbackLanguage' | 'language'>, i18n: Pick<I18n, 'fallbackLanguage' | 'language'>,
): string => { ): string => {
if (typeof label === 'object') { if (typeof label === 'object') {
if (label[i18n.language]) { if (label[i18n.language]) {

View File

@@ -1,4 +1,4 @@
import { I18n, Translations, InitTFunction } from '../types' import { Translations, InitTFunction, InitI18n, I18n } from '../types'
import { deepMerge } from './deepMerge' import { deepMerge } from './deepMerge'
/** /**
@@ -191,7 +191,7 @@ export function matchLanguage(header: string): string | undefined {
return undefined return undefined
} }
export const initTFunction: InitTFunction = (args) => (key, vars) => { const initTFunction: InitTFunction = (args) => (key, vars) => {
const { config, language, translations } = args const { config, language, translations } = args
const mergedLanguages = deepMerge(config?.translations ?? {}, translations) const mergedLanguages = deepMerge(config?.translations ?? {}, translations)
@@ -204,18 +204,58 @@ export const initTFunction: InitTFunction = (args) => (key, vars) => {
}) })
} }
export const initI18n = ({ function memoize<T>(fn: Function, keys: string[]): T {
config, const cacheMap = new Map()
language = 'en',
translations, return <T>async function (args) {
}: Parameters<InitTFunction>[0]): I18n => { const cacheKey = keys.reduce((acc, key) => acc + args[key], '')
return {
fallbackLanguage: config.fallbackLanguage, if (!cacheMap.has(cacheKey)) {
language: language || config.fallbackLanguage, const result = await fn(args)
t: initTFunction({ cacheMap.set(cacheKey, result)
config, }
language: language || config.fallbackLanguage,
translations, return cacheMap.get(cacheKey)!
}),
} }
} }
type GetTranslationsByKey = ({ context }: { context: 'client' | 'api' }) => Promise<Translations>
const getTranslationsByKey: GetTranslationsByKey = memoize(
<GetTranslationsByKey>(async ({ context }): Promise<Translations> => {
const cachedTranslations = new Map<string, Translations>()
if (cachedTranslations.has(context)) {
return cachedTranslations.get(context)
}
let translations = {}
if (context === 'api') {
translations = await import('@payloadcms/translations/api')
cachedTranslations.set(context, translations)
} else if (context === 'client') {
translations = await import('@payloadcms/translations/client')
cachedTranslations.set(context, translations)
}
return translations
}),
['context'] satisfies Array<keyof Parameters<GetTranslationsByKey>[0]>,
)
export const initI18n: InitI18n = memoize(
<InitI18n>(async ({ config, language = 'en', translationsContext }) => {
const translations = await getTranslationsByKey({ context: translationsContext })
const i18n = {
fallbackLanguage: config.fallbackLanguage,
language: language || config.fallbackLanguage,
t: initTFunction({
config,
language: language || config.fallbackLanguage,
translations,
}),
}
return i18n
}),
['language', 'translationsContext'] satisfies Array<keyof Parameters<InitI18n>[0]>,
)

View File

@@ -8,8 +8,7 @@ import { reduceFieldsToValues } from '../..'
import { DocumentPreferences } from 'payload/types' import { DocumentPreferences } from 'payload/types'
import { Locale } from 'payload/config' import { Locale } from 'payload/config'
import { User } from 'payload/auth' import { User } from 'payload/auth'
import { initTFunction } from '@payloadcms/translations' import { initI18n } from '@payloadcms/translations'
import { translations } from '@payloadcms/translations/api'
export const getFormStateFromServer = async ( export const getFormStateFromServer = async (
args: { args: {
@@ -37,8 +36,11 @@ export const getFormStateFromServer = async (
const data = reduceFieldsToValues(formState, true) const data = reduceFieldsToValues(formState, true)
// TODO: memoize the creation of this function based on language const { t } = await initI18n({
const t = initTFunction({ config: payload.config.i18n, language, translations }) translationsContext: 'client',
language: language,
config: payload.config.i18n,
})
const result = await buildStateFromSchema({ const result = await buildStateFromSchema({
id, id,