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>
}): Promise<Metadata> => {
const t = getNextT({
const t = await getNextT({
config: await config,
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,9 +8,9 @@ import { getAuthenticatedUser } from 'payload/auth'
import { getPayload } from 'payload'
import { URL } from 'url'
import { parseCookies } from 'payload/auth'
import { initI18n } from '@payloadcms/translations'
import { getRequestLanguage } from './getRequestLanguage'
import { getRequestLocales } from './getRequestLocales'
import { getNextI18n } from './getNextI18n'
import { getDataAndFile } from './getDataAndFile'
type Args = {
@@ -61,10 +61,10 @@ export const createPayloadRequest = async ({
cookies,
})
const i18n = getNextI18n({
config,
const i18n = await initI18n({
config: config.i18n,
language,
translationContext: 'api',
translationsContext: 'api',
})
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 { SanitizedConfig } from 'payload/types'
import { initTFunction } from '@payloadcms/translations'
import { initI18n } from '@payloadcms/translations'
import { cookies, headers } from 'next/headers'
import { getRequestLanguage } from './getRequestLanguage'
export const getNextT = ({
export const getNextT = async ({
config,
language,
}: {
config: SanitizedConfig
language?: string
}): TFunction => {
const lang = language || getRequestLanguage({ cookies: cookies(), headers: headers() })
return initTFunction({
language: lang,
}): Promise<TFunction> => {
const i18n = await initI18n({
translationsContext: 'client',
language: language || getRequestLanguage({ cookies: cookies(), headers: headers() }),
config: config.i18n,
translations,
})
return i18n.t
}

View File

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

View File

@@ -38,7 +38,7 @@ async function localForgotPassword<T extends keyof GeneratedTypes['collections']
data,
disableEmail,
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,
depth,
overrideAccess,
req: createLocalReq(options, payload),
req: await createLocalReq(options, payload),
showHiddenFields,
}

View File

@@ -38,7 +38,7 @@ async function localResetPassword<T extends keyof GeneratedTypes['collections']>
collection,
data,
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,
data,
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({
collection,
req: createLocalReq(options, payload),
req: await createLocalReq(options, payload),
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))
return createOperation<TSlug>({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -45,7 +45,7 @@ export default async function restoreVersionLocal<T extends keyof GeneratedTypes
depth,
overrideAccess,
payload,
req: createLocalReq(options, payload),
req: await createLocalReq(options, payload),
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))
const args = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,11 @@
"prepublishOnly": "pnpm clean && pnpm build"
},
"exports": {
".": {
"import": "./src/exports/index.ts",
"require": "./src/exports/index.ts",
"types": "./src/exports/index.ts"
},
"./api": {
"import": "./src/all/index.ts",
"require": "./src/all/index.ts",
@@ -19,11 +24,6 @@
"import": "./src/all/index.ts",
"require": "./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": {
@@ -33,13 +33,17 @@
},
"publishConfig": {
"exports": {
".": {
"import": "./dist/exports/index.js",
"require": "./dist/exports/index.js"
},
"./api": {
"import": "./dist/api/index.ts",
"require": "./dist/api/index.ts"
"import": "./dist/api/index.js",
"require": "./dist/api/index.js"
},
"./client": {
"import": "./dist/client/index.ts",
"require": "./dist/client/index.ts"
"import": "./dist/client/index.js",
"require": "./dist/client/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 type * from '../types'

View File

@@ -42,3 +42,9 @@ export type InitTFunction = (args: {
language?: string
translations?: Translations
}) => 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 { initI18n } from './init'
import { I18n } from '../types'
export const getTranslation = (
label: JSX.Element | Record<string, string> | string,
i18n: Pick<ReturnType<typeof initI18n>, 'fallbackLanguage' | 'language'>,
i18n: Pick<I18n, 'fallbackLanguage' | 'language'>,
): string => {
if (typeof label === 'object') {
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'
/**
@@ -191,7 +191,7 @@ export function matchLanguage(header: string): string | undefined {
return undefined
}
export const initTFunction: InitTFunction = (args) => (key, vars) => {
const initTFunction: InitTFunction = (args) => (key, vars) => {
const { config, language, translations } = args
const mergedLanguages = deepMerge(config?.translations ?? {}, translations)
@@ -204,18 +204,58 @@ export const initTFunction: InitTFunction = (args) => (key, vars) => {
})
}
export const initI18n = ({
config,
language = 'en',
translations,
}: Parameters<InitTFunction>[0]): I18n => {
return {
fallbackLanguage: config.fallbackLanguage,
language: language || config.fallbackLanguage,
t: initTFunction({
config,
language: language || config.fallbackLanguage,
translations,
}),
function memoize<T>(fn: Function, keys: string[]): T {
const cacheMap = new Map()
return <T>async function (args) {
const cacheKey = keys.reduce((acc, key) => acc + args[key], '')
if (!cacheMap.has(cacheKey)) {
const result = await fn(args)
cacheMap.set(cacheKey, result)
}
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 { Locale } from 'payload/config'
import { User } from 'payload/auth'
import { initTFunction } from '@payloadcms/translations'
import { translations } from '@payloadcms/translations/api'
import { initI18n } from '@payloadcms/translations'
export const getFormStateFromServer = async (
args: {
@@ -37,8 +36,11 @@ export const getFormStateFromServer = async (
const data = reduceFieldsToValues(formState, true)
// TODO: memoize the creation of this function based on language
const t = initTFunction({ config: payload.config.i18n, language, translations })
const { t } = await initI18n({
translationsContext: 'client',
language: language,
config: payload.config.i18n,
})
const result = await buildStateFromSchema({
id,