feat(plugin-multi-tenant): prompt the user to confirm the change of tenant before actually updating (#12382)

This commit is contained in:
Jarrod Flesch
2025-05-14 09:45:00 -04:00
committed by GitHub
parent 98283ca18c
commit faa7794cc7
69 changed files with 1028 additions and 93 deletions

View File

@@ -74,6 +74,7 @@ export const rootEslintConfig = [
'no-console': 'off', 'no-console': 'off',
'perfectionist/sort-object-types': 'off', 'perfectionist/sort-object-types': 'off',
'perfectionist/sort-objects': 'off', 'perfectionist/sort-objects': 'off',
'payload/no-relative-monorepo-imports': 'off',
}, },
}, },
] ]

View File

@@ -56,6 +56,16 @@
"import": "./src/exports/utilities.ts", "import": "./src/exports/utilities.ts",
"types": "./src/exports/utilities.ts", "types": "./src/exports/utilities.ts",
"default": "./src/exports/utilities.ts" "default": "./src/exports/utilities.ts"
},
"./translations/languages/all": {
"import": "./src/translations/index.ts",
"types": "./src/translations/index.ts",
"default": "./src/translations/index.ts"
},
"./translations/languages/*": {
"import": "./src/translations/languages/*.ts",
"types": "./src/translations/languages/*.ts",
"default": "./src/translations/languages/*.ts"
} }
}, },
"main": "./src/index.ts", "main": "./src/index.ts",
@@ -118,6 +128,16 @@
"import": "./dist/exports/utilities.js", "import": "./dist/exports/utilities.js",
"types": "./dist/exports/utilities.d.ts", "types": "./dist/exports/utilities.d.ts",
"default": "./dist/exports/utilities.js" "default": "./dist/exports/utilities.js"
},
"./translations/languages/all": {
"import": "./dist/translations/index.js",
"types": "./dist/translations/index.d.ts",
"default": "./dist/translations/index.js"
},
"./translations/languages/*": {
"import": "./dist/translations/languages/*.js",
"types": "./dist/translations/languages/*.d.ts",
"default": "./dist/translations/languages/*.js"
} }
}, },
"main": "./dist/index.js", "main": "./dist/index.js",

View File

@@ -3,18 +3,52 @@ import type { ReactSelectOption } from '@payloadcms/ui'
import type { ViewTypes } from 'payload' import type { ViewTypes } from 'payload'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { SelectInput, useTranslation } from '@payloadcms/ui' import {
ConfirmationModal,
SelectInput,
Translation,
useModal,
useTranslation,
} from '@payloadcms/ui'
import React from 'react' import React from 'react'
import type {
PluginMultiTenantTranslationKeys,
PluginMultiTenantTranslations,
} from '../../translations/index.js'
import { useTenantSelection } from '../../providers/TenantSelectionProvider/index.client.js' import { useTenantSelection } from '../../providers/TenantSelectionProvider/index.client.js'
import './index.scss' import './index.scss'
export const TenantSelector = ({ label, viewType }: { label: string; viewType?: ViewTypes }) => { const confirmSwitchTenantSlug = 'confirmSwitchTenant'
const { options, selectedTenantID, setTenant } = useTenantSelection()
const { i18n } = useTranslation()
const handleChange = React.useCallback( export const TenantSelector = ({ label, viewType }: { label: string; viewType?: ViewTypes }) => {
(option: ReactSelectOption | ReactSelectOption[]) => { const { options, preventRefreshOnChange, selectedTenantID, setTenant } = useTenantSelection()
const { openModal } = useModal()
const { i18n, t } = useTranslation<
PluginMultiTenantTranslations,
PluginMultiTenantTranslationKeys
>()
const [tenantSelection, setTenantSelection] = React.useState<
ReactSelectOption | ReactSelectOption[]
>()
const selectedValue = React.useMemo(() => {
if (selectedTenantID) {
return options.find((option) => option.value === selectedTenantID)
}
return undefined
}, [options, selectedTenantID])
const newSelectedValue = React.useMemo(() => {
if (tenantSelection && 'value' in tenantSelection) {
return options.find((option) => option.value === tenantSelection.value)
}
return undefined
}, [options, tenantSelection])
const switchTenant = React.useCallback(
(option: ReactSelectOption | ReactSelectOption[] | undefined) => {
if (option && 'value' in option) { if (option && 'value' in option) {
setTenant({ id: option.value as string, refresh: true }) setTenant({ id: option.value as string, refresh: true })
} else { } else {
@@ -24,6 +58,19 @@ export const TenantSelector = ({ label, viewType }: { label: string; viewType?:
[setTenant], [setTenant],
) )
const onChange = React.useCallback(
(option: ReactSelectOption | ReactSelectOption[]) => {
if (!preventRefreshOnChange) {
switchTenant(option)
return
} else {
setTenantSelection(option)
openModal(confirmSwitchTenantSlug)
}
},
[openModal, preventRefreshOnChange, switchTenant],
)
if (options.length <= 1) { if (options.length <= 1) {
return null return null
} }
@@ -34,11 +81,46 @@ export const TenantSelector = ({ label, viewType }: { label: string; viewType?:
isClearable={viewType === 'list'} isClearable={viewType === 'list'}
label={getTranslation(label, i18n)} label={getTranslation(label, i18n)}
name="setTenant" name="setTenant"
onChange={handleChange} onChange={onChange}
options={options} options={options}
path="setTenant" path="setTenant"
value={selectedTenantID as string | undefined} value={selectedTenantID as string | undefined}
/> />
<ConfirmationModal
body={
<Translation
elements={{
0: ({ children }) => {
return <b>{children}</b>
},
}}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
i18nKey="plugin-multi-tenant:confirm-tenant-switch--body"
t={t}
variables={{
fromTenant: selectedValue?.label,
toTenant: newSelectedValue?.label,
}}
/>
}
heading={
<Translation
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
i18nKey="plugin-multi-tenant:confirm-tenant-switch--heading"
t={t}
variables={{
tenantLabel: label.toLowerCase(),
}}
/>
}
modalSlug={confirmSwitchTenantSlug}
onConfirm={() => {
switchTenant(tenantSelection)
}}
/>
</div> </div>
) )
} }

View File

@@ -0,0 +1,35 @@
'use client'
import type { ClientCollectionConfig } from 'payload'
import { useConfig, useDocumentInfo, useEffectEvent, useFormFields } from '@payloadcms/ui'
import React from 'react'
import { useTenantSelection } from '../../providers/TenantSelectionProvider/index.client.js'
export const WatchTenantCollection = () => {
const { id, collectionSlug, title } = useDocumentInfo()
const { getEntityConfig } = useConfig()
const [useAsTitleName] = React.useState(
() => (getEntityConfig({ collectionSlug }) as ClientCollectionConfig).admin.useAsTitle,
)
const titleField = useFormFields(([fields]) => fields[useAsTitleName])
const { updateTenants } = useTenantSelection()
const syncTenantTitle = useEffectEvent(() => {
if (id) {
updateTenants({ id, label: title })
}
})
React.useEffect(() => {
// only update the tenant selector when the document saves
// → aka when initial value changes
if (id && titleField?.initialValue) {
syncTenantTitle()
}
}, [id, titleField?.initialValue])
return null
}

View File

@@ -1,3 +1,4 @@
export { TenantField } from '../components/TenantField/index.client.js' export { TenantField } from '../components/TenantField/index.client.js'
export { TenantSelector } from '../components/TenantSelector/index.js' export { TenantSelector } from '../components/TenantSelector/index.js'
export { WatchTenantCollection } from '../components/WatchTenantCollection/index.js'
export { useTenantSelection } from '../providers/TenantSelectionProvider/index.client.js' export { useTenantSelection } from '../providers/TenantSelectionProvider/index.client.js'

View File

@@ -1,6 +1,9 @@
import type { AcceptedLanguages } from '@payloadcms/translations' import type { AcceptedLanguages } from '@payloadcms/translations'
import type { CollectionConfig, Config } from 'payload' import type { CollectionConfig, Config } from 'payload'
import { deepMergeSimple } from 'payload'
import type { PluginDefaultTranslationsObject } from './translations/types.js'
import type { MultiTenantPluginConfig } from './types.js' import type { MultiTenantPluginConfig } from './types.js'
import { defaults } from './defaults.js' import { defaults } from './defaults.js'
@@ -10,6 +13,7 @@ import { addTenantCleanup } from './hooks/afterTenantDelete.js'
import { filterDocumentsBySelectedTenant } from './list-filters/filterDocumentsBySelectedTenant.js' import { filterDocumentsBySelectedTenant } from './list-filters/filterDocumentsBySelectedTenant.js'
import { filterTenantsBySelectedTenant } from './list-filters/filterTenantsBySelectedTenant.js' import { filterTenantsBySelectedTenant } from './list-filters/filterTenantsBySelectedTenant.js'
import { filterUsersBySelectedTenant } from './list-filters/filterUsersBySelectedTenant.js' import { filterUsersBySelectedTenant } from './list-filters/filterUsersBySelectedTenant.js'
import { translations } from './translations/index.js'
import { addCollectionAccess } from './utilities/addCollectionAccess.js' import { addCollectionAccess } from './utilities/addCollectionAccess.js'
import { addFilterOptionsToFields } from './utilities/addFilterOptionsToFields.js' import { addFilterOptionsToFields } from './utilities/addFilterOptionsToFields.js'
import { combineListFilters } from './utilities/combineListFilters.js' import { combineListFilters } from './utilities/combineListFilters.js'
@@ -229,6 +233,21 @@ export const multiTenantPlugin =
usersTenantsArrayTenantFieldName: tenantsArrayTenantFieldName, usersTenantsArrayTenantFieldName: tenantsArrayTenantFieldName,
}) })
} }
/**
* Add custom tenant field that watches and dispatches updates to the selector
*/
collection.fields.push({
name: '_watchTenant',
type: 'ui',
admin: {
components: {
Field: {
path: '@payloadcms/plugin-multi-tenant/client#WatchTenantCollection',
},
},
},
})
} else if (pluginConfig.collections?.[collection.slug]) { } else if (pluginConfig.collections?.[collection.slug]) {
const isGlobal = Boolean(pluginConfig.collections[collection.slug]?.isGlobal) const isGlobal = Boolean(pluginConfig.collections[collection.slug]?.isGlobal)
@@ -340,5 +359,25 @@ export const multiTenantPlugin =
path: '@payloadcms/plugin-multi-tenant/client#TenantSelector', path: '@payloadcms/plugin-multi-tenant/client#TenantSelector',
}) })
/**
* Merge plugin translations
*/
const simplifiedTranslations = Object.entries(translations).reduce(
(acc, [key, value]) => {
acc[key] = value.translations
return acc
},
{} as Record<string, PluginDefaultTranslationsObject>,
)
incomingConfig.i18n = {
...incomingConfig.i18n,
translations: deepMergeSimple(
simplifiedTranslations,
incomingConfig.i18n?.translations ?? {},
),
}
return incomingConfig return incomingConfig
} }

View File

@@ -11,6 +11,7 @@ type ContextType = {
* Array of options to select from * Array of options to select from
*/ */
options: OptionObject[] options: OptionObject[]
preventRefreshOnChange: boolean
/** /**
* The currently selected tenant ID * The currently selected tenant ID
*/ */
@@ -28,20 +29,26 @@ type ContextType = {
* @param args.refresh - Whether to refresh the page after changing the tenant * @param args.refresh - Whether to refresh the page after changing the tenant
*/ */
setTenant: (args: { id: number | string | undefined; refresh?: boolean }) => void setTenant: (args: { id: number | string | undefined; refresh?: boolean }) => void
/**
*
*/
updateTenants: (args: { id: number | string; label: string }) => void
} }
const Context = createContext<ContextType>({ const Context = createContext<ContextType>({
options: [], options: [],
preventRefreshOnChange: false,
selectedTenantID: undefined, selectedTenantID: undefined,
setPreventRefreshOnChange: () => null, setPreventRefreshOnChange: () => null,
setTenant: () => null, setTenant: () => null,
updateTenants: () => null,
}) })
export const TenantSelectionProviderClient = ({ export const TenantSelectionProviderClient = ({
children, children,
initialValue, initialValue,
tenantCookie, tenantCookie,
tenantOptions, tenantOptions: tenantOptionsFromProps,
}: { }: {
children: React.ReactNode children: React.ReactNode
initialValue?: number | string initialValue?: number | string
@@ -54,6 +61,9 @@ export const TenantSelectionProviderClient = ({
const [preventRefreshOnChange, setPreventRefreshOnChange] = React.useState(false) const [preventRefreshOnChange, setPreventRefreshOnChange] = React.useState(false)
const { user } = useAuth() const { user } = useAuth()
const userID = React.useMemo(() => user?.id, [user?.id]) const userID = React.useMemo(() => user?.id, [user?.id])
const [tenantOptions, setTenantOptions] = React.useState<OptionObject[]>(
() => tenantOptionsFromProps,
)
const selectedTenantLabel = React.useMemo( const selectedTenantLabel = React.useMemo(
() => tenantOptions.find((option) => option.value === selectedTenantID)?.label, () => tenantOptions.find((option) => option.value === selectedTenantID)?.label,
[selectedTenantID, tenantOptions], [selectedTenantID, tenantOptions],
@@ -91,6 +101,20 @@ export const TenantSelectionProviderClient = ({
[deleteCookie, preventRefreshOnChange, router, setCookie, setSelectedTenantID, tenantOptions], [deleteCookie, preventRefreshOnChange, router, setCookie, setSelectedTenantID, tenantOptions],
) )
const updateTenants = React.useCallback<ContextType['updateTenants']>(({ id, label }) => {
setTenantOptions((prev) => {
return prev.map((currentTenant) => {
if (id === currentTenant.value) {
return {
label,
value: id,
}
}
return currentTenant
})
})
}, [])
React.useEffect(() => { React.useEffect(() => {
if (selectedTenantID && !tenantOptions.find((option) => option.value === selectedTenantID)) { if (selectedTenantID && !tenantOptions.find((option) => option.value === selectedTenantID)) {
if (tenantOptions?.[0]?.value) { if (tenantOptions?.[0]?.value) {
@@ -105,13 +129,14 @@ export const TenantSelectionProviderClient = ({
if (userID && !tenantCookie) { if (userID && !tenantCookie) {
// User is logged in, but does not have a tenant cookie, set it // User is logged in, but does not have a tenant cookie, set it
setSelectedTenantID(initialValue) setSelectedTenantID(initialValue)
setTenantOptions(tenantOptionsFromProps)
if (initialValue) { if (initialValue) {
setCookie(String(initialValue)) setCookie(String(initialValue))
} else { } else {
deleteCookie() deleteCookie()
} }
} }
}, [userID, tenantCookie, initialValue, setCookie, deleteCookie, router]) }, [userID, tenantCookie, initialValue, setCookie, deleteCookie, router, tenantOptionsFromProps])
React.useEffect(() => { React.useEffect(() => {
if (!userID && tenantCookie) { if (!userID && tenantCookie) {
@@ -132,9 +157,11 @@ export const TenantSelectionProviderClient = ({
<Context <Context
value={{ value={{
options: tenantOptions, options: tenantOptions,
preventRefreshOnChange,
selectedTenantID, selectedTenantID,
setPreventRefreshOnChange, setPreventRefreshOnChange,
setTenant, setTenant,
updateTenants,
}} }}
> >
{children} {children}

View File

@@ -0,0 +1,91 @@
import type {
GenericTranslationsObject,
NestedKeysStripped,
SupportedLanguages,
} from '@payloadcms/translations'
import type { PluginDefaultTranslationsObject } from './types.js'
import { ar } from './languages/ar.js'
import { az } from './languages/az.js'
import { bg } from './languages/bg.js'
import { ca } from './languages/ca.js'
import { cs } from './languages/cs.js'
import { da } from './languages/da.js'
import { de } from './languages/de.js'
import { en } from './languages/en.js'
import { es } from './languages/es.js'
import { et } from './languages/et.js'
import { fa } from './languages/fa.js'
import { fr } from './languages/fr.js'
import { he } from './languages/he.js'
import { hr } from './languages/hr.js'
import { hu } from './languages/hu.js'
import { hy } from './languages/hy.js'
import { it } from './languages/it.js'
import { ja } from './languages/ja.js'
import { ko } from './languages/ko.js'
import { lt } from './languages/lt.js'
import { my } from './languages/my.js'
import { nb } from './languages/nb.js'
import { nl } from './languages/nl.js'
import { pl } from './languages/pl.js'
import { pt } from './languages/pt.js'
import { ro } from './languages/ro.js'
import { rs } from './languages/rs.js'
import { rsLatin } from './languages/rsLatin.js'
import { ru } from './languages/ru.js'
import { sk } from './languages/sk.js'
import { sl } from './languages/sl.js'
import { sv } from './languages/sv.js'
import { th } from './languages/th.js'
import { tr } from './languages/tr.js'
import { uk } from './languages/uk.js'
import { vi } from './languages/vi.js'
import { zh } from './languages/zh.js'
import { zhTw } from './languages/zhTw.js'
export const translations = {
ar,
az,
bg,
ca,
cs,
da,
de,
en,
es,
et,
fa,
fr,
he,
hr,
hu,
hy,
it,
ja,
ko,
lt,
my,
nb,
nl,
pl,
pt,
ro,
rs,
'rs-latin': rsLatin,
ru,
sk,
sl,
sv,
th,
tr,
uk,
vi,
zh,
'zh-TW': zhTw,
} as SupportedLanguages<PluginDefaultTranslationsObject>
export type PluginMultiTenantTranslations = GenericTranslationsObject
export type PluginMultiTenantTranslationKeys = NestedKeysStripped<PluginMultiTenantTranslations>

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const arTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'أنت على وشك تغيير الملكية من <0>{{fromTenant}}</0> إلى <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'تأكيد تغيير {{tenantLabel}}',
},
}
export const ar: PluginLanguage = {
dateFNSKey: 'ar',
translations: arTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const azTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Siz <0>{{fromTenant}}</0> mülkiyyətini <0>{{toTenant}}</0> mülkiyyətinə dəyişdirəcəksiniz.',
'confirm-tenant-switch--heading': '{{tenantLabel}} dəyişikliyini təsdiqləyin',
},
}
export const az: PluginLanguage = {
dateFNSKey: 'az',
translations: azTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const bgTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Предстои да промените собствеността от <0>{{fromTenant}}</0> на <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'Потвърдете промяната на {{tenantLabel}}',
},
}
export const bg: PluginLanguage = {
dateFNSKey: 'bg',
translations: bgTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const caTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Estàs a punt de canviar la propietat de <0>{{fromTenant}}</0> a <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'Confirmeu el canvi de {{tenantLabel}}',
},
}
export const ca: PluginLanguage = {
dateFNSKey: 'ca',
translations: caTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const csTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Chystáte se změnit vlastnictví z <0>{{fromTenant}}</0> na <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'Potvrďte změnu {{tenantLabel}}',
},
}
export const cs: PluginLanguage = {
dateFNSKey: 'cs',
translations: csTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const daTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Du er ved at ændre ejerskab fra <0>{{fromTenant}}</0> til <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'Bekræft {{tenantLabel}} ændring',
},
}
export const da: PluginLanguage = {
dateFNSKey: 'da',
translations: daTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const deTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Sie sind dabei, den Besitz von <0>{{fromTenant}}</0> auf <0>{{toTenant}}</0> zu übertragen.',
'confirm-tenant-switch--heading': 'Bestätigen Sie die Änderung von {{tenantLabel}}.',
},
}
export const de: PluginLanguage = {
dateFNSKey: 'de',
translations: deTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginLanguage } from '../types.js'
export const enTranslations = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'You are about to change ownership from <0>{{fromTenant}}</0> to <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'Confirm {{tenantLabel}} change',
},
}
export const en: PluginLanguage = {
dateFNSKey: 'en-US',
translations: enTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const esTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Está a punto de cambiar la propiedad de <0>{{fromTenant}}</0> a <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'Confirme el cambio de {{tenantLabel}}',
},
}
export const es: PluginLanguage = {
dateFNSKey: 'es',
translations: esTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const etTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Te olete tegemas omandiõiguse muudatust <0>{{fromTenant}}</0>lt <0>{{toTenant}}</0>le.',
'confirm-tenant-switch--heading': 'Kinnita {{tenantLabel}} muutus',
},
}
export const et: PluginLanguage = {
dateFNSKey: 'et',
translations: etTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const faTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'شما در حال تغییر مالکیت از <0>{{fromTenant}}</0> به <0>{{toTenant}}</0> هستید',
'confirm-tenant-switch--heading': 'تایید تغییر {{tenantLabel}}',
},
}
export const fa: PluginLanguage = {
dateFNSKey: 'fa-IR',
translations: faTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const frTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Vous êtes sur le point de changer la propriété de <0>{{fromTenant}}</0> à <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'Confirmer le changement de {{tenantLabel}}',
},
}
export const fr: PluginLanguage = {
dateFNSKey: 'fr',
translations: frTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const heTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'אתה עומד לשנות בעלות מ- <0>{{fromTenant}}</0> ל- <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'אשר שינוי {{tenantLabel}}',
},
}
export const he: PluginLanguage = {
dateFNSKey: 'he',
translations: heTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const hrTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Upravo ćete promijeniti vlasništvo sa <0>{{fromTenant}}</0> na <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'Potvrdi promjenu {{tenantLabel}}',
},
}
export const hr: PluginLanguage = {
dateFNSKey: 'hr',
translations: hrTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const huTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Ön azon van, hogy megváltoztassa a tulajdonjogot <0>{{fromTenant}}</0>-ről <0>{{toTenant}}</0>-re.',
'confirm-tenant-switch--heading': 'Erősítse meg a(z) {{tenantLabel}} változtatást',
},
}
export const hu: PluginLanguage = {
dateFNSKey: 'hu',
translations: huTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const hyTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Դուք պատրաստ եք փոխել գերեցդիմատնին ընկերությունը <0>{{fromTenant}}</0>-ից <0>{{toTenant}}</0>-ին',
'confirm-tenant-switch--heading': 'Հաստատեք {{tenantLabel}} փոփոխությունը',
},
}
export const hy: PluginLanguage = {
dateFNSKey: 'hy-AM',
translations: hyTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const itTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Stai per cambiare proprietà da <0>{{fromTenant}}</0> a <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'Conferma il cambiamento di {{tenantLabel}}',
},
}
export const it: PluginLanguage = {
dateFNSKey: 'it',
translations: itTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const jaTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'あなたは所有権を<0>{{fromTenant}}</0>から<0>{{toTenant}}</0>へ変更しようとしています',
'confirm-tenant-switch--heading': '{{tenantLabel}}の変更を確認してください',
},
}
export const ja: PluginLanguage = {
dateFNSKey: 'ja',
translations: jaTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const koTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'<0>{{fromTenant}}</0>에서 <0>{{toTenant}}</0>으로 소유권을 변경하려고 합니다.',
'confirm-tenant-switch--heading': '{{tenantLabel}} 변경을 확인하세요',
},
}
export const ko: PluginLanguage = {
dateFNSKey: 'ko',
translations: koTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const ltTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Jūs ketinate pakeisti nuosavybės teisę iš <0>{{fromTenant}}</0> į <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'Patvirtinkite {{tenantLabel}} pakeitimą',
},
}
export const lt: PluginLanguage = {
dateFNSKey: 'lt',
translations: ltTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const myTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Anda akan mengubah pemilikan dari <0>{{fromTenant}}</0> ke <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'Sahkan perubahan {{tenantLabel}}',
},
}
export const my: PluginLanguage = {
dateFNSKey: 'en-US',
translations: myTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const nbTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Du er i ferd med å endre eierskap fra <0>{{fromTenant}}</0> til <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'Bekreft {{tenantLabel}} endring',
},
}
export const nb: PluginLanguage = {
dateFNSKey: 'nb',
translations: nbTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const nlTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'U staat op het punt het eigendom te wijzigen van <0>{{fromTenant}}</0> naar <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'Bevestig wijziging van {{tenantLabel}}',
},
}
export const nl: PluginLanguage = {
dateFNSKey: 'nl',
translations: nlTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const plTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Za chwilę nastąpi zmiana właściciela z <0>{{fromTenant}}</0> na <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'Potwierdź zmianę {{tenantLabel}}',
},
}
export const pl: PluginLanguage = {
dateFNSKey: 'pl',
translations: plTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const ptTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Você está prestes a alterar a propriedade de <0>{{fromTenant}}</0> para <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'Confirme a alteração de {{tenantLabel}}',
},
}
export const pt: PluginLanguage = {
dateFNSKey: 'pt',
translations: ptTranslations,
}

View File

@@ -0,0 +1,3 @@
for file in *.js; do
mv -- "$file" "${file%.js}.ts"
done

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const roTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Sunteți pe punctul de a schimba proprietatea de la <0>{{fromTenant}}</0> la <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'Confirmați schimbarea {{tenantLabel}}',
},
}
export const ro: PluginLanguage = {
dateFNSKey: 'ro',
translations: roTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const rsTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Upravo ćete promeniti vlasništvo sa <0>{{fromTenant}}</0> na <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'Potvrdi promena {{tenantLabel}}',
},
}
export const rs: PluginLanguage = {
dateFNSKey: 'rs',
translations: rsTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const rsLatinTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Uskoro ćete promeniti vlasništvo sa <0>{{fromTenant}}</0> na <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'Potvrdite promenu {{tenantLabel}}',
},
}
export const rsLatin: PluginLanguage = {
dateFNSKey: 'rs-Latin',
translations: rsLatinTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const ruTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Вы собираетесь изменить владельца с <0>{{fromTenant}}</0> на <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'Подтвердите изменение {{tenantLabel}}',
},
}
export const ru: PluginLanguage = {
dateFNSKey: 'ru',
translations: ruTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const skTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Chystáte sa zmeniť vlastníctvo z <0>{{fromTenant}}</0> na <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'Potvrďte zmenu {{tenantLabel}}',
},
}
export const sk: PluginLanguage = {
dateFNSKey: 'sk',
translations: skTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const slTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Ravno ste pred spremembo lastništva iz <0>{{fromTenant}}</0> na <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'Potrdi spremembo {{tenantLabel}}',
},
}
export const sl: PluginLanguage = {
dateFNSKey: 'sl-SI',
translations: slTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const svTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Du är på väg att ändra ägare från <0>{{fromTenant}}</0> till <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'Bekräfta ändring av {{tenantLabel}}',
},
}
export const sv: PluginLanguage = {
dateFNSKey: 'sv',
translations: svTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const thTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'คุณกำลังจะเปลี่ยนความเป็นเจ้าของจาก <0>{{fromTenant}}</0> เป็น <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'ยืนยันการเปลี่ยนแปลง {{tenantLabel}}',
},
}
export const th: PluginLanguage = {
dateFNSKey: 'th',
translations: thTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const trTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
"Sahipliği <0>{{fromTenant}}</0>'den <0>{{toTenant}}</0>'e değiştirmek üzeresiniz.",
'confirm-tenant-switch--heading': '{{tenantLabel}} değişikliğini onayla',
},
}
export const tr: PluginLanguage = {
dateFNSKey: 'tr',
translations: trTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const ukTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Ви збираєтесь змінити власність з <0>{{fromTenant}}</0> на <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'Підтвердіть зміну {{tenantLabel}}',
},
}
export const uk: PluginLanguage = {
dateFNSKey: 'uk',
translations: ukTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const viTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'Bạn đang chuẩn bị chuyển quyền sở hữu từ <0>{{fromTenant}}</0> sang <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': 'Xác nhận thay đổi {{tenantLabel}}',
},
}
export const vi: PluginLanguage = {
dateFNSKey: 'vi',
translations: viTranslations,
}

View File

@@ -0,0 +1,13 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const zhTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body': '您即将将所有权从<0>{{fromTenant}}</0>更改为<0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': '确认更改{{tenantLabel}}',
},
}
export const zh: PluginLanguage = {
dateFNSKey: 'zh-CN',
translations: zhTranslations,
}

View File

@@ -0,0 +1,14 @@
import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
export const zhTwTranslations: PluginDefaultTranslationsObject = {
'plugin-multi-tenant': {
'confirm-tenant-switch--body':
'您即將將所有權從 <0>{{fromTenant}}</0> 轉移至 <0>{{toTenant}}</0>',
'confirm-tenant-switch--heading': '確認{{tenantLabel}}更改',
},
}
export const zhTw: PluginLanguage = {
dateFNSKey: 'zh-TW',
translations: zhTwTranslations,
}

View File

@@ -0,0 +1,12 @@
import type { Language } from '@payloadcms/translations'
import type { enTranslations } from './languages/en.js'
export type PluginLanguage = Language<{
'plugin-multi-tenant': {
'confirm-tenant-switch--body': string
'confirm-tenant-switch--heading': string
}
}>
export type PluginDefaultTranslationsObject = typeof enTranslations

View File

@@ -344,8 +344,7 @@
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/", "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
"lint": "eslint .", "lint": "eslint .",
"lint:fix": "eslint . --fix", "lint:fix": "eslint . --fix",
"prepublishOnly": "pnpm clean && pnpm turbo build", "prepublishOnly": "pnpm clean && pnpm turbo build"
"translateNewKeys": "node --no-deprecation --import @swc-node/register/esm-register scripts/translateNewKeys.ts"
}, },
"lint-staged": { "lint-staged": {
"**/package.json": "sort-package-json", "**/package.json": "sort-package-json",

View File

@@ -1 +0,0 @@
OPENAI_KEY=sk-

View File

@@ -51,8 +51,7 @@
"clean": "rimraf -g {dist,*.tsbuildinfo}", "clean": "rimraf -g {dist,*.tsbuildinfo}",
"lint": "eslint .", "lint": "eslint .",
"lint:fix": "eslint . --fix", "lint:fix": "eslint . --fix",
"prepublishOnly": "pnpm clean && pnpm turbo build", "prepublishOnly": "pnpm clean && pnpm turbo build"
"translateNewKeys": "node --no-deprecation --import @swc-node/register/esm-register scripts/translateNewKeys/run.ts"
}, },
"dependencies": { "dependencies": {
"date-fns": "4.1.0" "date-fns": "4.1.0"

View File

@@ -1,32 +0,0 @@
import path from 'path'
import { fileURLToPath } from 'url'
import type { AcceptedLanguages, GenericTranslationsObject } from '../../src/types.js'
import { translations } from '../../src/exports/all.js'
import { enTranslations } from '../../src/languages/en.js'
import { translateObject } from './index.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
const allTranslations: {
[key in AcceptedLanguages]?: {
dateFNSKey: string
translations: GenericTranslationsObject
}
} = {}
for (const key of Object.keys(translations)) {
allTranslations[key] = {
dateFNSKey: translations[key].dateFNSKey,
translations: translations[key].translations,
}
}
void translateObject({
allTranslationsObject: allTranslations,
fromTranslationsObject: enTranslations,
//languages: ['de'],
targetFolder: path.resolve(dirname, '../../src/languages'),
})

13
pnpm-lock.yaml generated
View File

@@ -1958,6 +1958,19 @@ importers:
open: open:
specifier: ^10.1.0 specifier: ^10.1.0
version: 10.1.0 version: 10.1.0
payload:
specifier: workspace:*
version: link:../../packages/payload
devDependencies:
'@payloadcms/plugin-multi-tenant':
specifier: workspace:*
version: link:../../packages/plugin-multi-tenant
'@payloadcms/richtext-lexical':
specifier: workspace:*
version: link:../../packages/richtext-lexical
'@payloadcms/translations':
specifier: workspace:*
version: link:../../packages/translations
packages: packages:

View File

@@ -41,7 +41,7 @@ export default buildConfigWithDefaults({
isGlobal: true, isGlobal: true,
}, },
}, },
tenantSelectorLabel: 'Sites', tenantSelectorLabel: 'Site',
}), }),
], ],
typescript: { typescript: {

1
tools/.env.example Normal file
View File

@@ -0,0 +1 @@
OPENAI_KEY=sk-your-key-here

View File

@@ -13,9 +13,11 @@
}, },
"main": "src/index.ts", "main": "src/index.ts",
"scripts": { "scripts": {
"build": "tsc", "build": "tsc --project tsconfig.build.json",
"build-template-with-local-pkgs": "pnpm runts src/build-template-with-local-pkgs.ts", "build-template-with-local-pkgs": "pnpm runts src/build-template-with-local-pkgs.ts",
"gen-templates": "pnpm runts src/generate-template-variations.ts", "gen-templates": "pnpm runts src/generate-template-variations.ts",
"generateTranslations:core": "node --no-deprecation --import @swc-node/register/esm-register src/generateTranslations/core.ts",
"generateTranslations:plugin-multi-tenant": "node --no-deprecation --import @swc-node/register/esm-register src/generateTranslations/plugin-multi-tenant.ts",
"license-check": "pnpm runts src/license-check.ts", "license-check": "pnpm runts src/license-check.ts",
"lint": "eslint .", "lint": "eslint .",
"lint:fix": "eslint . --fix", "lint:fix": "eslint . --fix",
@@ -32,6 +34,12 @@
"create-payload-app": "workspace:*", "create-payload-app": "workspace:*",
"csv-stringify": "^6.5.2", "csv-stringify": "^6.5.2",
"license-checker": "25.0.1", "license-checker": "25.0.1",
"open": "^10.1.0" "open": "^10.1.0",
"payload": "workspace:*"
},
"devDependencies": {
"@payloadcms/plugin-multi-tenant": "workspace:*",
"@payloadcms/richtext-lexical": "workspace:*",
"@payloadcms/translations": "workspace:*"
} }
} }

View File

@@ -0,0 +1,31 @@
import type { AcceptedLanguages, GenericTranslationsObject } from '@payloadcms/translations'
import { translations } from '@payloadcms/translations/all'
import { enTranslations } from '@payloadcms/translations/languages/en'
import path from 'path'
import { fileURLToPath } from 'url'
import { translateObject } from './utils/index.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
const allTranslations: {
[key in AcceptedLanguages]?: {
dateFNSKey: string
translations: GenericTranslationsObject
}
} = {}
for (const key of Object.keys(translations) as AcceptedLanguages[]) {
allTranslations[key] = {
dateFNSKey: translations[key]?.dateFNSKey || 'unknown-date-fns-key',
translations: translations[key]?.translations || {},
}
}
void translateObject({
allTranslationsObject: allTranslations,
fromTranslationsObject: enTranslations,
targetFolder: path.resolve(dirname, '../../../../packages/translations/src/languages'),
})

View File

@@ -0,0 +1,39 @@
import type { AcceptedLanguages, GenericTranslationsObject } from '@payloadcms/translations'
import { translations } from '@payloadcms/plugin-multi-tenant/translations/languages/all'
import { enTranslations } from '@payloadcms/plugin-multi-tenant/translations/languages/en'
import path from 'path'
import { fileURLToPath } from 'url'
import { translateObject } from './utils/index.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
const allTranslations: {
[key in AcceptedLanguages]?: {
dateFNSKey: string
translations: GenericTranslationsObject
}
} = {}
for (const key of Object.keys(translations)) {
allTranslations[key as AcceptedLanguages] = {
dateFNSKey: translations[key as AcceptedLanguages]?.dateFNSKey ?? 'unknown-date-fns-key',
translations: translations[key as AcceptedLanguages]?.translations ?? {},
}
}
void translateObject({
allTranslationsObject: allTranslations,
fromTranslationsObject: enTranslations,
targetFolder: path.resolve(
dirname,
'../../../../packages/plugin-multi-tenant/src/translations/languages',
),
tsFilePrefix: `import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'\n\nexport const {{locale}}Translations: PluginDefaultTranslationsObject = `,
tsFileSuffix: `\n\nexport const {{locale}}: PluginLanguage = {
dateFNSKey: {{dateFNSKey}},
translations: {{locale}}Translations,
} `,
})

View File

@@ -8,13 +8,13 @@ import * as fs from 'node:fs'
import path from 'path' import path from 'path'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import { translateObject } from '../../translations/scripts/translateNewKeys/index.js' import { translateObject } from './utils/index.js'
const filename = fileURLToPath(import.meta.url) const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename) const dirname = path.dirname(filename)
// Function to get all files with a specific name recursively in all subdirectories // Function to get all files with a specific name recursively in all subdirectories
function findFilesRecursively(startPath, filter) { function findFilesRecursively(startPath: string, filter: string): string[] {
let results = [] let results: string[] = []
const entries = fs.readdirSync(startPath, { withFileTypes: true }) const entries = fs.readdirSync(startPath, { withFileTypes: true })
@@ -33,7 +33,10 @@ function findFilesRecursively(startPath, filter) {
return results return results
} }
const i18nFilePaths = findFilesRecursively(path.resolve(dirname, '../src'), 'i18n.ts') const i18nFilePaths = findFilesRecursively(
path.resolve(dirname, '../../../../packages/richtext-lexical/src'),
'i18n.ts',
)
async function translate() { async function translate() {
for (const i18nFilePath of i18nFilePaths) { for (const i18nFilePath of i18nFilePaths) {
@@ -46,22 +49,26 @@ async function translate() {
} }
} = {} } = {}
for (const lang in translationsObject) { for (const lang in translationsObject) {
allTranslations[lang] = { allTranslations[lang as AcceptedLanguages] = {
dateFNSKey: 'en', dateFNSKey: 'en',
translations: translationsObject[lang], translations: translationsObject?.[lang as keyof GenericLanguages] || {},
} }
} }
console.log('Translating', i18nFilePath) if (translationsObject.en) {
await translateObject({ console.log('Translating', i18nFilePath)
allTranslationsObject: allTranslations, await translateObject({
fromTranslationsObject: translationsObject.en, allTranslationsObject: allTranslations,
inlineFile: i18nFilePath, fromTranslationsObject: translationsObject.en,
tsFilePrefix: `import { GenericLanguages } from '@payloadcms/translations' inlineFile: i18nFilePath,
tsFilePrefix: `import { GenericLanguages } from '@payloadcms/translations'
export const i18n: Partial<GenericLanguages> = `,
tsFileSuffix: ``, export const i18n: Partial<GenericLanguages> = `,
}) tsFileSuffix: ``,
})
} else {
console.error(`No English translations found in ${i18nFilePath}`)
}
} }
} }

View File

@@ -1,4 +1,4 @@
import type { GenericTranslationsObject } from '../../src/types.js' import type { GenericTranslationsObject } from '@payloadcms/translations'
/** /**
* Returns keys which are present in baseObj but not in targetObj * Returns keys which are present in baseObj but not in targetObj
@@ -8,7 +8,7 @@ export function findMissingKeys(
targetObj: GenericTranslationsObject, targetObj: GenericTranslationsObject,
prefix = '', prefix = '',
): string[] { ): string[] {
let missingKeys = [] let missingKeys: string[] = []
for (const key in baseObj) { for (const key in baseObj) {
const baseValue = baseObj[key] const baseValue = baseObj[key]

View File

@@ -1,5 +1,7 @@
export function generateTsObjectLiteral(obj: any): string { import type { JsonObject } from 'payload'
const lines = []
export function generateTsObjectLiteral(obj: JsonObject): string {
const lines: string[] = []
const entries = Object.entries(obj) const entries = Object.entries(obj)
for (const [key, value] of entries) { for (const [key, value] of entries) {
const safeKey = /^[\w$]+$/.test(key) ? key : JSON.stringify(key) const safeKey = /^[\w$]+$/.test(key) ? key : JSON.stringify(key)

View File

@@ -1,17 +1,17 @@
/* eslint no-console: 0 */ /* eslint no-console: 0 */
import fs from 'fs'
import path from 'path'
import { format } from 'prettier'
import type { import type {
AcceptedLanguages, AcceptedLanguages,
GenericLanguages, GenericLanguages,
GenericTranslationsObject, GenericTranslationsObject,
} from '../../src/types.js' } from '@payloadcms/translations'
import { acceptedLanguages } from '@payloadcms/translations'
import fs from 'fs'
import path from 'path'
import { deepMergeSimple } from 'payload/shared'
import { format } from 'prettier'
import { deepMergeSimple } from '../../src/utilities/deepMergeSimple.js'
import { acceptedLanguages } from '../../src/utilities/languages.js'
import { applyEslintFixes } from './applyEslintFixes.js' import { applyEslintFixes } from './applyEslintFixes.js'
import { findMissingKeys } from './findMissingKeys.js' import { findMissingKeys } from './findMissingKeys.js'
import { generateTsObjectLiteral } from './generateTsObjectLiteral.js' import { generateTsObjectLiteral } from './generateTsObjectLiteral.js'
@@ -104,16 +104,16 @@ export async function translateObject(props: {
*/ */
for (const key of keysWhichDoNotExistInFromlang) { for (const key of keysWhichDoNotExistInFromlang) {
// Delete those keys in the target language object obj[lang] // Delete those keys in the target language object obj[lang]
const keys = key.split('.') const keys: string[] = key.split('.')
let targetObj = allTranslatedTranslationsObject?.[targetLang].translations let targetObj = allTranslatedTranslationsObject?.[targetLang].translations
for (let i = 0; i < keys.length - 1; i += 1) { for (let i = 0; i < keys.length - 1; i += 1) {
const nextObj = targetObj[keys[i]] const nextObj = targetObj[keys[i] as string]
if (typeof nextObj !== 'object') { if (typeof nextObj !== 'object') {
throw new Error(`Key ${keys[i]} is not an object in ${targetLang} (1)`) throw new Error(`Key ${keys[i]} is not an object in ${targetLang} (1)`)
} }
targetObj = nextObj targetObj = nextObj
} }
delete targetObj[keys[keys.length - 1]] delete targetObj[keys[keys.length - 1] as string]
} }
if (!allTranslatedTranslationsObject?.[targetLang].translations) { if (!allTranslatedTranslationsObject?.[targetLang].translations) {
@@ -128,7 +128,10 @@ export async function translateObject(props: {
for (const missingKey of missingKeys) { for (const missingKey of missingKeys) {
const keys: string[] = missingKey.split('.') const keys: string[] = missingKey.split('.')
const sourceText = keys.reduce((acc, key) => acc[key], fromTranslationsObject) const sourceText = keys.reduce(
(acc, key) => acc[key] as GenericTranslationsObject,
fromTranslationsObject,
)
if (!sourceText || typeof sourceText !== 'string') { if (!sourceText || typeof sourceText !== 'string') {
throw new Error( throw new Error(
`Missing key ${missingKey} or key not "leaf" in fromTranslationsObject for lang ${targetLang}. (2)`, `Missing key ${missingKey} or key not "leaf" in fromTranslationsObject for lang ${targetLang}. (2)`,
@@ -147,20 +150,20 @@ export async function translateObject(props: {
} }
let targetObj = allOnlyNewTranslatedTranslationsObject?.[targetLang] let targetObj = allOnlyNewTranslatedTranslationsObject?.[targetLang]
for (let i = 0; i < keys.length - 1; i += 1) { for (let i = 0; i < keys.length - 1; i += 1) {
if (!targetObj[keys[i]]) { if (!targetObj[keys[i] as string]) {
targetObj[keys[i]] = {} targetObj[keys[i] as string] = {}
} }
const nextObj = targetObj[keys[i]] const nextObj = targetObj[keys[i] as string]
if (typeof nextObj !== 'object') { if (typeof nextObj !== 'object') {
throw new Error(`Key ${keys[i]} is not an object in ${targetLang} (3)`) throw new Error(`Key ${keys[i]} is not an object in ${targetLang} (3)`)
} }
targetObj = nextObj targetObj = nextObj
} }
targetObj[keys[keys.length - 1]] = translated targetObj[keys[keys.length - 1] as string] = translated
allTranslatedTranslationsObject[targetLang].translations = sortKeys( allTranslatedTranslationsObject[targetLang]!.translations = sortKeys(
deepMergeSimple( deepMergeSimple(
allTranslatedTranslationsObject[targetLang].translations, allTranslatedTranslationsObject[targetLang]!.translations,
allOnlyNewTranslatedTranslationsObject[targetLang], allOnlyNewTranslatedTranslationsObject[targetLang],
), ),
) )
@@ -180,9 +183,14 @@ export async function translateObject(props: {
console.log('New translations:', allOnlyNewTranslatedTranslationsObject) console.log('New translations:', allOnlyNewTranslatedTranslationsObject)
if (inlineFile?.length) { if (inlineFile?.length) {
const simpleTranslationsObject = {} const simpleTranslationsObject: GenericTranslationsObject = {}
for (const lang in allTranslatedTranslationsObject) { for (const lang in allTranslatedTranslationsObject) {
simpleTranslationsObject[lang] = allTranslatedTranslationsObject[lang].translations if (lang in allTranslatedTranslationsObject) {
simpleTranslationsObject[lang as keyof typeof allTranslatedTranslationsObject] =
allTranslatedTranslationsObject[
lang as keyof typeof allTranslatedTranslationsObject
]!.translations
}
} }
// write allTranslatedTranslationsObject // write allTranslatedTranslationsObject
@@ -218,10 +226,10 @@ export async function translateObject(props: {
const filePath = path.resolve(targetFolder, `${sanitizedKey}.ts`) const filePath = path.resolve(targetFolder, `${sanitizedKey}.ts`)
// prefix & translations // prefix & translations
let fileContent: string = `${tsFilePrefix.replace('{{locale}}', sanitizedKey)}${generateTsObjectLiteral(allTranslatedTranslationsObject[key].translations)}\n` let fileContent: string = `${tsFilePrefix.replace('{{locale}}', sanitizedKey)}${generateTsObjectLiteral(allTranslatedTranslationsObject[key]?.translations || {})}\n`
// suffix // suffix
fileContent += `${tsFileSuffix.replaceAll('{{locale}}', sanitizedKey).replaceAll('{{dateFNSKey}}', `'${allTranslatedTranslationsObject[key].dateFNSKey}'`)}\n` fileContent += `${tsFileSuffix.replaceAll('{{locale}}', sanitizedKey).replaceAll('{{dateFNSKey}}', `'${allTranslatedTranslationsObject[key]?.dateFNSKey}'`)}\n`
// eslint // eslint
fileContent = await applyEslintFixes(fileContent, filePath) fileContent = await applyEslintFixes(fileContent, filePath)

View File

@@ -4,7 +4,7 @@ import path from 'path'
const filename = fileURLToPath(import.meta.url) const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename) const dirname = path.dirname(filename)
dotenv.config({ path: path.resolve(dirname, '../../', '.env') }) dotenv.config({ path: path.resolve(dirname, '../../../../', '.env') })
export async function translateText(text: string, targetLang: string) { export async function translateText(text: string, targetLang: string) {
const response = await fetch('https://api.openai.com/v1/chat/completions', { const response = await fetch('https://api.openai.com/v1/chat/completions', {

View File

@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"strict": true
},
"references": [
{ "path": "../../packages/translations" },
{ "path": "../../packages/richtext-lexical" },
{ "path": "../../packages/plugin-multi-tenant" }
],
"exclude": ["./src/generateTranslations"]
}

View File

@@ -2,5 +2,6 @@
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"strict": true, "strict": true,
} },
"references": [{ "path": "../../packages/translations" }, { "path": "../../packages/richtext-lexical"}, { "path": "../../packages/plugin-multi-tenant"}]
} }

View File

@@ -31,7 +31,7 @@
} }
], ],
"paths": { "paths": {
"@payload-config": ["./test/admin/config.ts"], "@payload-config": ["./test/plugin-multi-tenant/config.ts"],
"@payloadcms/admin-bar": ["./packages/admin-bar/src"], "@payloadcms/admin-bar": ["./packages/admin-bar/src"],
"@payloadcms/live-preview": ["./packages/live-preview/src"], "@payloadcms/live-preview": ["./packages/live-preview/src"],
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"], "@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
@@ -70,6 +70,12 @@
"./packages/plugin-multi-tenant/src/exports/client.ts" "./packages/plugin-multi-tenant/src/exports/client.ts"
], ],
"@payloadcms/plugin-multi-tenant": ["./packages/plugin-multi-tenant/src/index.ts"], "@payloadcms/plugin-multi-tenant": ["./packages/plugin-multi-tenant/src/index.ts"],
"@payloadcms/plugin-multi-tenant/translations/languages/all": [
"./packages/plugin-multi-tenant/src/translations/index.ts"
],
"@payloadcms/plugin-multi-tenant/translations/languages/*": [
"./packages/plugin-multi-tenant/src/translations/languages/*.ts"
],
"@payloadcms/next": ["./packages/next/src/exports/*"], "@payloadcms/next": ["./packages/next/src/exports/*"],
"@payloadcms/storage-s3/client": ["./packages/storage-s3/src/exports/client.ts"], "@payloadcms/storage-s3/client": ["./packages/storage-s3/src/exports/client.ts"],
"@payloadcms/storage-vercel-blob/client": [ "@payloadcms/storage-vercel-blob/client": [