feat!: typed i18n (#6343)

This commit is contained in:
Alessio Gravili
2024-05-14 11:10:31 -04:00
committed by GitHub
parent 353c2b0be2
commit f716122eab
84 changed files with 1232 additions and 581 deletions

View File

@@ -301,6 +301,7 @@ jobs:
- fields__collections__Lexical
- live-preview
- localization
- i18n
- plugin-cloud-storage
- plugin-form-builder
- plugin-nested-docs

View File

@@ -565,10 +565,11 @@ These are the props that will be passed to your custom Label.
#### Example
```tsx
'use client'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { getTranslation } from '@payloadcms/translations'
import { useTranslation } from '@payloadcms/ui/providers/Translation'
import { getTranslation } from 'payload/utilities/getTranslation'
type Props = {
htmlFor?: string
@@ -680,21 +681,21 @@ To make use of Payload SCSS variables / mixins to use directly in your own compo
### Getting the current language
When developing custom components you can support multiple languages to be consistent with Payload's i18n support. The best way to do this is to add your translation resources to the [i18n configuration](https://payloadcms.com/docs/configuration/i18n) and import `useTranslation` from `react-i18next` in your components.
When developing custom components you can support multiple languages to be consistent with Payload's i18n support. The best way to do this is to add your translation resources to the [i18n configuration](https://payloadcms.com/docs/configuration/i18n) and import `useTranslation` from `@payloadcms/ui/providers/Translation` in your components.
For example:
```tsx
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@payloadcms/ui/providers/Translation'
const CustomComponent: React.FC = () => {
// highlight-start
const { t, i18n } = useTranslation('namespace1')
const { t, i18n } = useTranslation()
// highlight-end
return (
<ul>
<li>{t('key', { variable: 'value' })}</li>
<li>{t('namespace1:key', { variable: 'value' })}</li>
<li>{t('namespace2:key', { variable: 'value' })}</li>
<li>{i18n.language}</li>
</ul>

View File

@@ -6,7 +6,7 @@ desc: Manage and customize internationalization support in your CMS editor exper
keywords: internationalization, i18n, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
---
Not only does Payload support managing localized content, it also has internationalization support so that admin users can work in their preferred language. Payload's i18n support is built on top of [i18next](https://www.i18next.com). It comes included by default and can be extended in your config.
Not only does Payload support managing localized content, it also has internationalization support so that admin users can work in their preferred language. It comes included by default and can be extended in your config.
While Payload's built-in features come translated, you may want to also translate parts of your project's configuration too. This is possible in places like collections and globals labels and groups, field labels, descriptions and input placeholder text. The admin UI will display all the correct translations you provide based on the user's language.
@@ -72,9 +72,7 @@ After a user logs in, they can change their language selection in the `/account`
Payload's backend uses express middleware to set the language on incoming requests before they are handled. This allows backend validation to return error messages in the user's own language or system generated emails to be sent using the correct translation. You can make HTTP requests with the `accept-language` header and Payload will use that language.
Anywhere in your Payload app that you have access to the `req` object, you can access i18next's extensive internationalization features assigned to `req.i18n`. To access text translations you can use `req.t('namespace:key')`.
Read the i18next [API documentation](https://www.i18next.com/overview/api) to learn more.
Anywhere in your Payload app that you have access to the `req` object, you can access payload's extensive internationalization features assigned to `req.i18n`. To access text translations you can use `req.t('namespace:key')`.
### Configuration Options
@@ -88,9 +86,8 @@ import { buildConfig } from 'payload/config'
export default buildConfig({
//...
i18n: {
fallbackLng: 'en', // default
debug: false, // default
resources: {
fallbackLanguage: 'en', // default
translations: {
en: {
custom: {
// namespace can be anything you want
@@ -107,4 +104,63 @@ export default buildConfig({
})
```
See the i18next [configuration options](https://www.i18next.com/overview/configuration-options) to learn more.
## Types for custom translations
In order to use custom translations in your project, you need to provide the types for the translations. Here is an example of how you can define the types for the custom translations in a custom react component:
```ts
'use client'
import type { NestedKeysStripped } from '@payloadcms/translations'
import type React from 'react'
import { useTranslation } from '@payloadcms/ui/providers/Translation'
const customTranslations = {
en: {
general: {
test: 'Custom Translation',
},
},
}
type CustomTranslationObject = typeof customTranslations.en
type CustomTranslationKeys = NestedKeysStripped<CustomTranslationObject>
export const MyComponent: React.FC = () => {
const { i18n, t } = useTranslation<CustomTranslationObject, CustomTranslationKeys>() // These generics merge your custom translations with the default client translations
return t('general:test')
}
```
Additionally, payload exposes the `t` function in various places, for example in labels. Here is how you would type those:
```ts
import type {
DefaultTranslationKeys,
NestedKeysStripped,
TFunction,
} from '@payloadcms/translations'
import type { Field } from 'payload/types'
const customTranslations = {
en: {
general: {
test: 'Custom Translation',
},
},
}
type CustomTranslationObject = typeof customTranslations.en
type CustomTranslationKeys = NestedKeysStripped<CustomTranslationObject>
const field: Field = {
name: 'myField',
type: 'text',
label: (
{ t }: { t: TFunction<CustomTranslationKeys | DefaultTranslationKeys> }, // The generic passed to TFunction does not automatically merge the custom translations with the default translations. We need to merge them ourselves here
) => t('fields:addLabel'),
}
```

View File

@@ -54,7 +54,6 @@
"docker:start": "docker compose -f packages/plugin-cloud-storage/docker-compose.yml up -d",
"docker:stop": "docker compose -f packages/plugin-cloud-storage/docker-compose.yml down",
"fix": "eslint \"packages/**/*.ts\" --fix",
"generate:types": "PAYLOAD_CONFIG_PATH=./test/_community/config.ts node --no-deprecation ./packages/payload/bin.js generate:types",
"lint": "eslint \"packages/**/*.ts\"",
"lint-staged": "lint-staged",
"obliterate-playwright-cache": "rm -rf ~/Library/Caches/ms-playwright && find /System/Volumes/Data/private/var/folders -type d -name 'playwright*' -exec rm -rf {} +",

View File

@@ -1,4 +1,4 @@
import type { AcceptedLanguages } from '@payloadcms/translations'
import type { AcceptedLanguages, I18nClient } from '@payloadcms/translations'
import type { SanitizedConfig } from 'payload/types'
import { rtlLanguages } from '@payloadcms/translations'
@@ -50,7 +50,11 @@ export const RootLayout = async ({
})
const payload = await getPayloadHMR({ config })
const i18n = await initI18n({ config: config.i18n, context: 'client', language: languageCode })
const i18n: I18nClient = await initI18n({
config: config.i18n,
context: 'client',
language: languageCode,
})
const clientConfig = await createClientConfig({ config, t: i18n.t })
const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)

View File

@@ -8,7 +8,7 @@ import type { FieldSchemaMap } from './types.js'
type Args = {
config: SanitizedConfig
fields: Field[]
i18n: I18n
i18n: I18n<any, any>
schemaMap: FieldSchemaMap
schemaPath: string
validRelationships: string[]

View File

@@ -7,7 +7,7 @@ import { cookies, headers } from 'next/headers.js'
import { getRequestLanguage } from './getRequestLanguage.js'
/**
* In the context of NextJS, this function initializes the i18n object for the current request.
* In the context of Next.js, this function initializes the i18n object for the current request.
*
* It must be called on the server side, and within the lifecycle of a request since it relies on the request headers and cookies.
*/

View File

@@ -1,3 +1,4 @@
import type { I18nClient } from '@payloadcms/translations'
import type { InitPageResult, PayloadRequestWithData, VisibleEntities } from 'payload/types'
import { initI18n } from '@payloadcms/translations'
@@ -40,7 +41,7 @@ export const initPage = async ({
const cookies = parseCookies(headers)
const language = getRequestLanguage({ config: payload.config, cookies, headers })
const i18n = await initI18n({
const i18n: I18nClient = await initI18n({
config: i18nConfig,
context: 'client',
language,

View File

@@ -1,4 +1,4 @@
import type { I18n } from '@payloadcms/translations'
import type { I18nClient } from '@payloadcms/translations'
import type { SelectFieldProps } from '@payloadcms/ui/fields/Select'
import type { MappedField } from '@payloadcms/ui/utilities/buildComponentMap'
import type { OptionObject, SelectField } from 'payload/types'
@@ -35,7 +35,7 @@ const getOptionsToRender = (
const getTranslatedOptions = (
options: (OptionObject | string)[] | OptionObject | string,
i18n: I18n,
i18n: I18nClient,
): string => {
if (Array.isArray(options)) {
return options

View File

@@ -1,4 +1,4 @@
import type { I18n } from '@payloadcms/translations'
import type { I18nClient } from '@payloadcms/translations'
import type { FieldMap, MappedField } from '@payloadcms/ui/utilities/buildComponentMap'
import type { FieldPermissions } from 'payload/auth'
import type React from 'react'
@@ -13,7 +13,7 @@ export type Props = {
disableGutter?: boolean
field: MappedField
fieldMap: FieldMap
i18n: I18n
i18n: I18nClient
isRichText?: boolean
locale?: string
locales?: string[]

View File

@@ -1,4 +1,4 @@
import type { I18n } from '@payloadcms/translations'
import type { I18nClient } from '@payloadcms/translations'
import type { FieldMap, MappedField } from '@payloadcms/ui/utilities/buildComponentMap'
import type { FieldPermissions } from 'payload/auth'
import type { DiffMethod } from 'react-diff-viewer-continued'
@@ -10,7 +10,7 @@ export type Props = {
diffComponents: DiffComponents
fieldMap: FieldMap
fieldPermissions: Record<string, FieldPermissions>
i18n: I18n
i18n: I18nClient
locales: string[]
version: Record<string, any>
}

View File

@@ -1,7 +1,7 @@
import type { I18n } from '@payloadcms/translations'
import type { I18nClient } from '@payloadcms/translations'
import type { JSONSchema4 } from 'json-schema'
import type { Config, SanitizedConfig } from '../config/types.js'
import type { SanitizedConfig } from '../config/types.js'
import type { Field, FieldBase, RichTextField, Validate } from '../fields/config/types.js'
import type { PayloadRequestWithData, RequestContext } from '../types/index.js'
import type { WithServerSideProps } from './elements/WithServerSideProps.js'
@@ -22,12 +22,12 @@ type RichTextAdapterBase<
generateComponentMap: (args: {
WithServerSideProps: WithServerSideProps
config: SanitizedConfig
i18n: I18n
i18n: I18nClient
schemaPath: string
}) => Map<string, React.ReactNode>
generateSchemaMap?: (args: {
config: SanitizedConfig
i18n: I18n
i18n: I18nClient
schemaMap: Map<string, Field[]>
schemaPath: string
}) => Map<string, Field[]>

View File

@@ -1,4 +1,4 @@
import type { SupportedLanguages } from '@payloadcms/translations'
import type { ClientTranslationsObject } from '@payloadcms/translations'
import type { Permissions } from '../../auth/index.js'
import type { SanitizedCollectionConfig } from '../../collections/config/types.js'
@@ -43,7 +43,7 @@ export type InitPageResult = {
locale: Locale
permissions: Permissions
req: PayloadRequestWithData
translations: SupportedLanguages
translations: ClientTranslationsObject
visibleEntities: VisibleEntities
}

View File

@@ -108,7 +108,9 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
i18nConfig.fallbackLanguage = supportedLangKeys.includes(fallbackLang)
? fallbackLang
: supportedLangKeys[0]
i18nConfig.translations = incomingConfig.i18n?.translations || i18nConfig.translations
i18nConfig.translations =
(incomingConfig.i18n?.translations as SanitizedConfig['i18n']['translations']) ||
i18nConfig.translations
}
config.i18n = i18nConfig

View File

@@ -1,4 +1,4 @@
import type { I18nOptions, TFunction } from '@payloadcms/translations'
import type { DefaultTranslationsObject, I18nOptions, TFunction } from '@payloadcms/translations'
import type { Options as ExpressFileUploadOptions } from 'express-fileupload'
import type GraphQL from 'graphql'
import type { Metadata as NextMetadata } from 'next'
@@ -580,7 +580,7 @@ export type Config = {
afterError?: AfterErrorHook
}
/** i18n config settings */
i18n?: I18nOptions
i18n?: I18nOptions<{} | DefaultTranslationsObject> // loosen the type here to allow for custom translations
/** Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. */
indexSortableFields?: boolean
/**

View File

@@ -65,11 +65,11 @@ export const text: Validate<string | string[], unknown, unknown, TextField> = (
const length = stringValue?.length || 0
if (typeof maxLength === 'number' && length > maxLength) {
return t('validation:shorterThanMax', { label: t('value'), maxLength, stringValue })
return t('validation:shorterThanMax', { label: t('general:value'), maxLength, stringValue })
}
if (typeof minLength === 'number' && length < minLength) {
return t('validation:longerThanMin', { label: t('value'), minLength, stringValue })
return t('validation:longerThanMin', { label: t('general:value'), minLength, stringValue })
}
}

View File

@@ -13,6 +13,7 @@ import { useLocale } from '@payloadcms/ui/providers/Locale'
import { useTranslation } from '@payloadcms/ui/providers/Translation'
import React, { useCallback } from 'react'
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js'
import type { GenerateDescription } from '../types.js'
import { defaults } from '../defaults.js'
@@ -30,7 +31,7 @@ export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
const { CustomLabel, hasGenerateDescriptionFn, label, labelProps, path, required } = props
const { path: pathFromContext } = useFieldProps()
const { t } = useTranslation()
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
const locale = useLocale()
const [fields] = useAllFormFields()

View File

@@ -13,6 +13,7 @@ import { useLocale } from '@payloadcms/ui/providers/Locale'
import { useTranslation } from '@payloadcms/ui/providers/Translation'
import React, { useCallback } from 'react'
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js'
import type { GenerateImage } from '../types.js'
import { Pill } from '../ui/Pill.js'
@@ -27,7 +28,7 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
const field: FieldType<string> = useField(props as Options)
const { t } = useTranslation()
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
const locale = useLocale()
const [fields] = useAllFormFields()

View File

@@ -14,6 +14,7 @@ import { useLocale } from '@payloadcms/ui/providers/Locale'
import { useTranslation } from '@payloadcms/ui/providers/Translation'
import React, { useCallback } from 'react'
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js'
import type { GenerateTitle } from '../types.js'
import { defaults } from '../defaults.js'
@@ -31,7 +32,7 @@ export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
const { CustomLabel, hasGenerateTitleFn, label, labelProps, path, required } = props || {}
const { path: pathFromContext } = useFieldProps()
const { t } = useTranslation()
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
const field: FieldType<string> = useField({
path,

View File

@@ -1,3 +1,5 @@
import type { NestedKeysStripped } from '@payloadcms/translations'
export const translations = {
en: {
$schema: './translation-schema.json',
@@ -162,13 +164,20 @@ export const translations = {
checksPassing: '{{current}}/{{max}} перевірок пройдено',
good: 'Чудово',
imageAutoGenerationTip: 'Автоматична генерація використає зображення з головного блоку',
lengthTipDescription: 'Має бути від {{minLength}} до {{maxLength}} символів. Щоб дізнатися, як писати якісні метаописи — перегляньте ',
lengthTipTitle: 'Має бути від {{minLength}} до {{maxLength}} символів. Щоб дізнатися, як писати якісні метазаголовки — перегляньте ',
lengthTipDescription:
'Має бути від {{minLength}} до {{maxLength}} символів. Щоб дізнатися, як писати якісні метаописи — перегляньте ',
lengthTipTitle:
'Має бути від {{minLength}} до {{maxLength}} символів. Щоб дізнатися, як писати якісні метазаголовки — перегляньте ',
noImage: 'Немає зображення',
preview: 'Попередній перегляд',
previewDescription: 'Реальне відображення може відрізнятися в залежності від вмісту та релевантності пошуку.',
previewDescription:
'Реальне відображення може відрізнятися в залежності від вмісту та релевантності пошуку.',
tooLong: 'Задовгий',
tooShort: 'Закороткий',
},
},
}
export type PluginSEOTranslations = typeof translations.en
export type PluginSEOTranslationKeys = NestedKeysStripped<PluginSEOTranslations>

View File

@@ -3,6 +3,8 @@
import { useTranslation } from '@payloadcms/ui/providers/Translation'
import React, { Fragment, useEffect, useState } from 'react'
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js'
import { Pill } from './Pill.js'
export const LengthIndicator: React.FC<{
@@ -19,7 +21,7 @@ export const LengthIndicator: React.FC<{
const [label, setLabel] = useState('')
const [barWidth, setBarWidth] = useState<number>(0)
const { t } = useTranslation()
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
useEffect(() => {
const textLength = text?.length || 0

View File

@@ -6,6 +6,8 @@ import { useAllFormFields, useForm } from '@payloadcms/ui/forms/Form'
import { useTranslation } from '@payloadcms/ui/providers/Translation'
import React, { useCallback, useEffect, useState } from 'react'
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js'
import { defaults } from '../defaults.js'
const {
@@ -26,7 +28,7 @@ export const Overview: React.FC = () => {
'meta.title': { value: metaTitle } = {} as FormField,
},
] = useAllFormFields()
const { t } = useTranslation()
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
const [titleIsValid, setTitleIsValid] = useState<boolean | undefined>()
const [descIsValid, setDescIsValid] = useState<boolean | undefined>()

View File

@@ -8,15 +8,15 @@ import { useLocale } from '@payloadcms/ui/providers/Locale'
import { useTranslation } from '@payloadcms/ui/providers/Translation'
import React, { useEffect, useState } from 'react'
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js'
import type { GenerateURL } from '../types.js'
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
type PreviewProps = UIField & {
hasGenerateURLFn: boolean
}
export const Preview: React.FC<PreviewProps> = ({ hasGenerateURLFn }) => {
const { t } = useTranslation()
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
const locale = useLocale()
const [fields] = useAllFormFields()

View File

@@ -1,4 +1,4 @@
import type { I18n } from '@payloadcms/translations'
import type { I18nClient } from '@payloadcms/translations'
import type { BaseSelection, LexicalEditor } from 'lexical'
import type React from 'react'
@@ -49,7 +49,7 @@ export type ToolbarGroupItem = {
}) => boolean
key: string
/** The label is displayed as text if the item is part of a dropdown group */
label?: (({ i18n }: { i18n: I18n }) => string) | string
label?: (({ i18n }: { i18n: I18nClient }) => string) | string
onSelect?: ({ editor, isActive }: { editor: LexicalEditor; isActive: boolean }) => void
order?: number
}

View File

@@ -1,5 +1,5 @@
import type { Transformer } from '@lexical/markdown'
import type { I18n } from '@payloadcms/translations'
import type { I18n, I18nClient } from '@payloadcms/translations'
import type { JSONSchema4 } from 'json-schema'
import type { Klass, LexicalEditor, LexicalNode, SerializedEditorState } from 'lexical'
import type { SerializedLexicalNode } from 'lexical'
@@ -274,7 +274,7 @@ export type ServerFeature<ServerProps, ClientFeatureProps> = {
clientFeatureProps?: ClientFeatureProps
generateComponentMap?: (args: {
config: SanitizedConfig
i18n: I18n
i18n: I18nClient
props: ServerProps
schemaPath: string
}) => {
@@ -282,7 +282,7 @@ export type ServerFeature<ServerProps, ClientFeatureProps> = {
}
generateSchemaMap?: (args: {
config: SanitizedConfig
i18n: I18n
i18n: I18nClient
props: ServerProps
schemaMap: Map<string, Field[]>
schemaPath: string

View File

@@ -1,4 +1,4 @@
import type { I18n } from '@payloadcms/translations'
import type { I18n, I18nClient } from '@payloadcms/translations'
import type { LexicalEditor } from 'lexical'
import type { MutableRefObject } from 'react'
import type React from 'react'
@@ -13,7 +13,7 @@ export type SlashMenuItem = {
keyboardShortcut?: string
// For extra searching.
keywords?: Array<string>
label?: (({ i18n }: { i18n: I18n }) => string) | string
label?: (({ i18n }: { i18n: I18nClient }) => string) | string
// What happens when you select this item?
onSelect: ({ editor, queryString }: { editor: LexicalEditor; queryString: string }) => void
}
@@ -22,7 +22,7 @@ export type SlashMenuGroup = {
items: Array<SlashMenuItem>
key: string
// Used for class names and, if label is not provided, for display.
label?: (({ i18n }: { i18n: I18n }) => string) | string
label?: (({ i18n }: { i18n: I18nClient }) => string) | string
}
export type SlashMenuItemInternal = SlashMenuItem & {

View File

@@ -1,4 +1,10 @@
export const clientTranslationKeys = [
import type { DefaultTranslationKeys } from './types.js'
function createClientTranslationKeys<T extends DefaultTranslationKeys[]>(keys: T) {
return keys
}
export const clientTranslationKeys = createClientTranslationKeys([
'authentication:account',
'authentication:accountOfCurrentUser',
'authentication:alreadyActivated',
@@ -22,6 +28,7 @@ export const clientTranslationKeys = [
'authentication:forgotPasswordQuestion',
'authentication:generate',
'authentication:generateNewAPIKey',
'authentication:generatingNewAPIKeyWillInvalidate',
'authentication:logBackIn',
'authentication:loggedOutInactivity',
'authentication:loggedOutSuccessfully',
@@ -63,6 +70,7 @@ export const clientTranslationKeys = [
'error:unknown',
'error:unspecific',
'error:tokenNotProvided',
'error:unPublishingDocument',
'fields:addLabel',
'fields:addLink',
@@ -135,10 +143,11 @@ export const clientTranslationKeys = [
'general:dark',
'general:dashboard',
'general:delete',
'general:deletedSuccessfully',
'general:deletedCountSuccessfully',
'general:deleting',
'general:depth',
'general:descending',
'general:depth',
'general:deselectAllRows',
'general:document',
'general:documents',
@@ -205,7 +214,7 @@ export const clientTranslationKeys = [
'general:submit',
'general:success',
'general:successfullyCreated',
'general:successfullyDeleted',
'general:successfullyDuplicated',
'general:thisLanguage',
'general:titleDeleted',
'general:true',
@@ -267,6 +276,7 @@ export const clientTranslationKeys = [
'version:aboutToUnpublishSelection',
'version:autosave',
'version:autosavedSuccessfully',
'version:autosavedVersion',
'version:changed',
'version:confirmRevertToSaved',
'version:compareVersion',
@@ -287,6 +297,7 @@ export const clientTranslationKeys = [
'version:restoredSuccessfully',
'version:restoreThisVersion',
'version:restoring',
'version:reverting',
'version:revertToPublished',
'version:saveDraft',
'version:selectLocales',
@@ -304,4 +315,4 @@ export const clientTranslationKeys = [
'version:viewingVersionGlobal',
'version:viewingVersions',
'version:viewingVersionsGlobal',
]
])

View File

@@ -1,3 +1,4 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const ar: Language = {

View File

@@ -1,3 +1,4 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const az: Language = {

View File

@@ -1,3 +1,4 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const bg: Language = {

View File

@@ -1,3 +1,4 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const cs: Language = {
@@ -8,8 +9,8 @@ export const cs: Language = {
accountOfCurrentUser: 'Účet současného uživatele',
alreadyActivated: 'Již aktivováno',
alreadyLoggedIn: 'Již přihlášen',
authenticated: 'Ověřený',
apiKey: 'API klíč',
authenticated: 'Ověřený',
backToLogin: 'Zpět na přihlášení',
beginCreateFirstUser: 'Začněte vytvořením svého prvního uživatele.',
changePassword: 'Změnit heslo',
@@ -306,7 +307,8 @@ export const cs: Language = {
fileName: 'Název souboru',
fileSize: 'Velikost souboru',
focalPoint: 'Středobod',
focalPointDescription: 'Přetáhněte bod zaměření přímo na náhled nebo upravte níže uvedené hodnoty.',
focalPointDescription:
'Přetáhněte bod zaměření přímo na náhled nebo upravte níže uvedené hodnoty.',
height: 'Výška',
lessInfo: 'Méně informací',
moreInfo: 'Více informací',

View File

@@ -1,3 +1,4 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const de: Language = {

View File

@@ -1,416 +1,417 @@
import type { Language } from '../types.js'
export const enTranslations = {
authentication: {
account: 'Account',
accountOfCurrentUser: 'Account of current user',
alreadyActivated: 'Already Activated',
alreadyLoggedIn: 'Already logged in',
apiKey: 'API Key',
authenticated: 'Authenticated',
backToLogin: 'Back to login',
beginCreateFirstUser: 'To begin, create your first user.',
changePassword: 'Change Password',
checkYourEmailForPasswordReset:
'Check your email for a link that will allow you to securely reset your password.',
confirmGeneration: 'Confirm Generation',
confirmPassword: 'Confirm Password',
createFirstUser: 'Create first user',
emailNotValid: 'The email provided is not valid',
emailSent: 'Email Sent',
emailVerified: 'Email verified successfully.',
enableAPIKey: 'Enable API Key',
failedToUnlock: 'Failed to unlock',
forceUnlock: 'Force Unlock',
forgotPassword: 'Forgot Password',
forgotPasswordEmailInstructions:
'Please enter your email below. You will receive an email message with instructions on how to reset your password.',
forgotPasswordQuestion: 'Forgot password?',
generate: 'Generate',
generateNewAPIKey: 'Generate new API key',
generatingNewAPIKeyWillInvalidate:
'Generating a new API key will <1>invalidate</1> the previous key. Are you sure you wish to continue?',
lockUntil: 'Lock Until',
logBackIn: 'Log back in',
logOut: 'Log out',
loggedIn: 'To log in with another user, you should <0>log out</0> first.',
loggedInChangePassword:
'To change your password, go to your <0>account</0> and edit your password there.',
loggedOutInactivity: 'You have been logged out due to inactivity.',
loggedOutSuccessfully: 'You have been logged out successfully.',
loggingOut: 'Logging out...',
login: 'Login',
loginAttempts: 'Login Attempts',
loginUser: 'Login user',
loginWithAnotherUser: 'To log in with another user, you should <0>log out</0> first.',
logout: 'Logout',
logoutSuccessful: 'Logout successful.',
logoutUser: 'Logout user',
newAPIKeyGenerated: 'New API Key Generated.',
newAccountCreated:
'A new account has just been created for you to access <a href="{{serverURL}}">{{serverURL}}</a> Please click on the following link or paste the URL below into your browser to verify your email: <a href="{{verificationURL}}">{{verificationURL}}</a><br> After verifying your email, you will be able to log in successfully.',
newPassword: 'New Password',
passed: 'Authentication Passed',
passwordResetSuccessfully: 'Password reset successfully.',
resetPassword: 'Reset Password',
resetPasswordExpiration: 'Reset Password Expiration',
resetPasswordToken: 'Reset Password Token',
resetYourPassword: 'Reset Your Password',
stayLoggedIn: 'Stay logged in',
successfullyRegisteredFirstUser: 'Successfully registered first user.',
successfullyUnlocked: 'Successfully unlocked',
tokenRefreshSuccessful: 'Token refresh successful.',
unableToVerify: 'Unable to Verify',
verified: 'Verified',
verifiedSuccessfully: 'Verified Successfully',
verify: 'Verify',
verifyUser: 'Verify User',
verifyYourEmail: 'Verify your email',
youAreInactive:
"You haven't been active in a little while and will shortly be automatically logged out for your own security. Would you like to stay logged in?",
youAreReceivingResetPassword:
'You are receiving this because you (or someone else) have requested the reset of the password for your account. Please click on the following link, or paste this into your browser to complete the process:',
youDidNotRequestPassword:
'If you did not request this, please ignore this email and your password will remain unchanged.',
},
error: {
accountAlreadyActivated: 'This account has already been activated.',
autosaving: 'There was a problem while autosaving this document.',
correctInvalidFields: 'Please correct invalid fields.',
deletingFile: 'There was an error deleting file.',
deletingTitle:
'There was an error while deleting {{title}}. Please check your connection and try again.',
emailOrPasswordIncorrect: 'The email or password provided is incorrect.',
followingFieldsInvalid_one: 'The following field is invalid:',
followingFieldsInvalid_other: 'The following fields are invalid:',
incorrectCollection: 'Incorrect Collection',
invalidFileType: 'Invalid file type',
invalidFileTypeValue: 'Invalid file type: {{value}}',
loadingDocument: 'There was a problem loading the document with ID of {{id}}.',
localesNotSaved_one: 'The following locale could not be saved:',
localesNotSaved_other: 'The following locales could not be saved:',
logoutFailed: 'Logout failed.',
missingEmail: 'Missing email.',
missingIDOfDocument: 'Missing ID of document to update.',
missingIDOfVersion: 'Missing ID of version.',
missingRequiredData: 'Missing required data.',
noFilesUploaded: 'No files were uploaded.',
noMatchedField: 'No matched field found for "{{label}}"',
noUser: 'No User',
notAllowedToAccessPage: 'You are not allowed to access this page.',
notAllowedToPerformAction: 'You are not allowed to perform this action.',
notFound: 'The requested resource was not found.',
previewing: 'There was a problem previewing this document.',
problemUploadingFile: 'There was a problem while uploading the file.',
tokenInvalidOrExpired: 'Token is either invalid or has expired.',
tokenNotProvided: 'Token not provided.',
unPublishingDocument: 'There was a problem while un-publishing this document.',
unableToDeleteCount: 'Unable to delete {{count}} out of {{total}} {{label}}.',
unableToUpdateCount: 'Unable to update {{count}} out of {{total}} {{label}}.',
unauthorized: 'Unauthorized, you must be logged in to make this request.',
unknown: 'An unknown error has occurred.',
unspecific: 'An error has occurred.',
userLocked: 'This user is locked due to having too many failed login attempts.',
valueMustBeUnique: 'Value must be unique',
verificationTokenInvalid: 'Verification token is invalid.',
},
fields: {
addLabel: 'Add {{label}}',
addLink: 'Add Link',
addNew: 'Add new',
addNewLabel: 'Add new {{label}}',
addRelationship: 'Add Relationship',
addUpload: 'Add Upload',
block: 'block',
blockType: 'Block Type',
blocks: 'blocks',
chooseBetweenCustomTextOrDocument:
'Choose between entering a custom text URL or linking to another document.',
chooseDocumentToLink: 'Choose a document to link to',
chooseFromExisting: 'Choose from existing',
chooseLabel: 'Choose {{label}}',
collapseAll: 'Collapse All',
customURL: 'Custom URL',
editLabelData: 'Edit {{label}} data',
editLink: 'Edit Link',
editRelationship: 'Edit Relationship',
enterURL: 'Enter a URL',
internalLink: 'Internal Link',
itemsAndMore: '{{items}} and {{count}} more',
labelRelationship: '{{label}} Relationship',
latitude: 'Latitude',
linkType: 'Link Type',
linkedTo: 'Linked to <0>{{label}}</0>',
longitude: 'Longitude',
newLabel: 'New {{label}}',
openInNewTab: 'Open in new tab',
passwordsDoNotMatch: 'Passwords do not match.',
relatedDocument: 'Related Document',
relationTo: 'Relation To',
removeRelationship: 'Remove Relationship',
removeUpload: 'Remove Upload',
saveChanges: 'Save changes',
searchForBlock: 'Search for a block',
selectExistingLabel: 'Select existing {{label}}',
selectFieldsToEdit: 'Select fields to edit',
showAll: 'Show All',
swapRelationship: 'Swap Relationship',
swapUpload: 'Swap Upload',
textToDisplay: 'Text to display',
toggleBlock: 'Toggle block',
uploadNewLabel: 'Upload new {{label}}',
},
general: {
aboutToDelete: 'You are about to delete the {{label}} <1>{{title}}</1>. Are you sure?',
aboutToDeleteCount_many: 'You are about to delete {{count}} {{label}}',
aboutToDeleteCount_one: 'You are about to delete {{count}} {{label}}',
aboutToDeleteCount_other: 'You are about to delete {{count}} {{label}}',
addBelow: 'Add Below',
addFilter: 'Add Filter',
adminTheme: 'Admin Theme',
and: 'And',
applyChanges: 'Apply Changes',
ascending: 'Ascending',
automatic: 'Automatic',
backToDashboard: 'Back to Dashboard',
cancel: 'Cancel',
changesNotSaved:
'Your changes have not been saved. If you leave now, you will lose your changes.',
close: 'Close',
collapse: 'Collapse',
collections: 'Collections',
columnToSort: 'Column to Sort',
columns: 'Columns',
confirm: 'Confirm',
confirmDeletion: 'Confirm deletion',
confirmDuplication: 'Confirm duplication',
copied: 'Copied',
copy: 'Copy',
create: 'Create',
createNew: 'Create New',
createNewLabel: 'Create new {{label}}',
created: 'Created',
createdAt: 'Created At',
creating: 'Creating',
creatingNewLabel: 'Creating new {{label}}',
custom: 'Custom',
dark: 'Dark',
dashboard: 'Dashboard',
delete: 'Delete',
deletedCountSuccessfully: 'Deleted {{count}} {{label}} successfully.',
deletedSuccessfully: 'Deleted successfully.',
deleting: 'Deleting...',
depth: 'Depth',
descending: 'Descending',
deselectAllRows: 'Deselect all rows',
document: 'Document',
documents: 'Documents',
duplicate: 'Duplicate',
duplicateWithoutSaving: 'Duplicate without saving changes',
edit: 'Edit',
editLabel: 'Edit {{label}}',
editing: 'Editing',
editingLabel_many: 'Editing {{count}} {{label}}',
editingLabel_one: 'Editing {{count}} {{label}}',
editingLabel_other: 'Editing {{count}} {{label}}',
email: 'Email',
emailAddress: 'Email Address',
enterAValue: 'Enter a value',
error: 'Error',
errors: 'Errors',
fallbackToDefaultLocale: 'Fallback to default locale',
false: 'False',
filter: 'Filter',
filterWhere: 'Filter {{label}} where',
filters: 'Filters',
globals: 'Globals',
language: 'Language',
lastModified: 'Last Modified',
leaveAnyway: 'Leave anyway',
leaveWithoutSaving: 'Leave without saving',
light: 'Light',
livePreview: 'Live Preview',
loading: 'Loading',
locale: 'Locale',
locales: 'Locales',
menu: 'Menu',
moveDown: 'Move Down',
moveUp: 'Move Up',
newPassword: 'New Password',
noFiltersSet: 'No filters set',
noLabel: '<No {{label}}>',
noOptions: 'No options',
noResults:
"No {{label}} found. Either no {{label}} exist yet or none match the filters you've specified above.",
noValue: 'No value',
none: 'None',
notFound: 'Not Found',
nothingFound: 'Nothing found',
of: 'of',
open: 'Open',
or: 'Or',
order: 'Order',
pageNotFound: 'Page not found',
password: 'Password',
payloadSettings: 'Payload Settings',
perPage: 'Per Page: {{limit}}',
remove: 'Remove',
reset: 'Reset',
row: 'Row',
rows: 'Rows',
save: 'Save',
saving: 'Saving...',
searchBy: 'Search by {{label}}',
selectAll: 'Select all {{count}} {{label}}',
selectAllRows: 'Select all rows',
selectValue: 'Select a value',
selectedCount: '{{count}} {{label}} selected',
showAllLabel: 'Show all {{label}}',
sorryNotFound: 'Sorry—there is nothing to correspond with your request.',
sort: 'Sort',
sortByLabelDirection: 'Sort by {{label}} {{direction}}',
stayOnThisPage: 'Stay on this page',
submissionSuccessful: 'Submission Successful.',
submit: 'Submit',
success: 'Success',
successfullyCreated: '{{label}} successfully created.',
successfullyDuplicated: '{{label}} successfully duplicated.',
thisLanguage: 'English',
titleDeleted: '{{label}} "{{title}}" successfully deleted.',
true: 'True',
unauthorized: 'Unauthorized',
unsavedChangesDuplicate: 'You have unsaved changes. Would you like to continue to duplicate?',
untitled: 'Untitled',
updatedAt: 'Updated At',
updatedCountSuccessfully: 'Updated {{count}} {{label}} successfully.',
updatedSuccessfully: 'Updated successfully.',
updating: 'Updating',
uploading: 'Uploading',
user: 'User',
users: 'Users',
value: 'Value',
welcome: 'Welcome',
},
operators: {
contains: 'contains',
equals: 'equals',
exists: 'exists',
isGreaterThan: 'is greater than',
isGreaterThanOrEqualTo: 'is greater than or equal to',
isIn: 'is in',
isLessThan: 'is less than',
isLessThanOrEqualTo: 'is less than or equal to',
isLike: 'is like',
isNotEqualTo: 'is not equal to',
isNotIn: 'is not in',
near: 'near',
},
upload: {
crop: 'Crop',
cropToolDescription:
'Drag the corners of the selected area, draw a new area or adjust the values below.',
dragAndDrop: 'Drag and drop a file',
dragAndDropHere: 'or drag and drop a file here',
editImage: 'Edit Image',
fileName: 'File Name',
fileSize: 'File Size',
focalPoint: 'Focal Point',
focalPointDescription:
'Drag the focal point directly on the preview or adjust the values below.',
height: 'Height',
lessInfo: 'Less info',
moreInfo: 'More info',
previewSizes: 'Preview Sizes',
selectCollectionToBrowse: 'Select a Collection to Browse',
selectFile: 'Select a file',
setCropArea: 'Set crop area',
setFocalPoint: 'Set focal point',
sizes: 'Sizes',
sizesFor: 'Sizes for {{label}}',
width: 'Width',
},
validation: {
emailAddress: 'Please enter a valid email address.',
enterNumber: 'Please enter a valid number.',
fieldHasNo: 'This field has no {{label}}',
greaterThanMax: '{{value}} is greater than the max allowed {{label}} of {{max}}.',
invalidInput: 'This field has an invalid input.',
invalidSelection: 'This field has an invalid selection.',
invalidSelections: 'This field has the following invalid selections:',
lessThanMin: '{{value}} is less than the min allowed {{label}} of {{min}}.',
limitReached: 'Limit reached, only {{max}} items can be added.',
longerThanMin: 'This value must be longer than the minimum length of {{minLength}} characters.',
notValidDate: '"{{value}}" is not a valid date.',
required: 'This field is required.',
requiresAtLeast: 'This field requires at least {{count}} {{label}}.',
requiresNoMoreThan: 'This field requires no more than {{count}} {{label}}.',
requiresTwoNumbers: 'This field requires two numbers.',
shorterThanMax: 'This value must be shorter than the max length of {{maxLength}} characters.',
trueOrFalse: 'This field can only be equal to true or false.',
validUploadID: 'This field is not a valid upload ID.',
},
version: {
type: 'Type',
aboutToPublishSelection:
'You are about to publish all {{label}} in the selection. Are you sure?',
aboutToRestore:
'You are about to restore this {{label}} document to the state that it was in on {{versionDate}}.',
aboutToRestoreGlobal:
'You are about to restore the global {{label}} to the state that it was in on {{versionDate}}.',
aboutToRevertToPublished:
"You are about to revert this document's changes to its published state. Are you sure?",
aboutToUnpublish: 'You are about to unpublish this document. Are you sure?',
aboutToUnpublishSelection:
'You are about to unpublish all {{label}} in the selection. Are you sure?',
autosave: 'Autosave',
autosavedSuccessfully: 'Autosaved successfully.',
autosavedVersion: 'Autosaved version',
changed: 'Changed',
compareVersion: 'Compare version against:',
confirmPublish: 'Confirm publish',
confirmRevertToSaved: 'Confirm revert to saved',
confirmUnpublish: 'Confirm unpublish',
confirmVersionRestoration: 'Confirm version Restoration',
currentDocumentStatus: 'Current {{docStatus}} document',
draft: 'Draft',
draftSavedSuccessfully: 'Draft saved successfully.',
lastSavedAgo: 'Last saved {{distance}} ago',
noFurtherVersionsFound: 'No further versions found',
noRowsFound: 'No {{label}} found',
preview: 'Preview',
problemRestoringVersion: 'There was a problem restoring this version',
publish: 'Publish',
publishChanges: 'Publish changes',
published: 'Published',
publishing: 'Publishing',
restoreThisVersion: 'Restore this version',
restoredSuccessfully: 'Restored Successfully.',
restoring: 'Restoring...',
revertToPublished: 'Revert to published',
reverting: 'Reverting...',
saveDraft: 'Save Draft',
selectLocales: 'Select locales to display',
selectVersionToCompare: 'Select a version to compare',
showLocales: 'Show locales:',
showingVersionsFor: 'Showing versions for:',
status: 'Status',
unpublish: 'Unpublish',
unpublishing: 'Unpublishing...',
version: 'Version',
versionCount_many: '{{count}} versions found',
versionCount_none: 'No versions found',
versionCount_one: '{{count}} version found',
versionCount_other: '{{count}} versions found',
versionCreatedOn: '{{version}} created on:',
versionID: 'Version ID',
versions: 'Versions',
viewingVersion: 'Viewing version for the {{entityLabel}} {{documentTitle}}',
viewingVersionGlobal: 'Viewing version for the global {{entityLabel}}',
viewingVersions: 'Viewing versions for the {{entityLabel}} {{documentTitle}}',
viewingVersionsGlobal: 'Viewing versions for the global {{entityLabel}}',
},
}
export const en: Language = {
dateFNSKey: 'en-US',
translations: {
authentication: {
account: 'Account',
accountOfCurrentUser: 'Account of current user',
alreadyActivated: 'Already Activated',
alreadyLoggedIn: 'Already logged in',
apiKey: 'API Key',
authenticated: 'Authenticated',
backToLogin: 'Back to login',
beginCreateFirstUser: 'To begin, create your first user.',
changePassword: 'Change Password',
checkYourEmailForPasswordReset:
'Check your email for a link that will allow you to securely reset your password.',
confirmGeneration: 'Confirm Generation',
confirmPassword: 'Confirm Password',
createFirstUser: 'Create first user',
emailNotValid: 'The email provided is not valid',
emailSent: 'Email Sent',
emailVerified: 'Email verified successfully.',
enableAPIKey: 'Enable API Key',
failedToUnlock: 'Failed to unlock',
forceUnlock: 'Force Unlock',
forgotPassword: 'Forgot Password',
forgotPasswordEmailInstructions:
'Please enter your email below. You will receive an email message with instructions on how to reset your password.',
forgotPasswordQuestion: 'Forgot password?',
generate: 'Generate',
generateNewAPIKey: 'Generate new API key',
generatingNewAPIKeyWillInvalidate:
'Generating a new API key will <1>invalidate</1> the previous key. Are you sure you wish to continue?',
lockUntil: 'Lock Until',
logBackIn: 'Log back in',
logOut: 'Log out',
loggedIn: 'To log in with another user, you should <0>log out</0> first.',
loggedInChangePassword:
'To change your password, go to your <0>account</0> and edit your password there.',
loggedOutInactivity: 'You have been logged out due to inactivity.',
loggedOutSuccessfully: 'You have been logged out successfully.',
loggingOut: 'Logging out...',
login: 'Login',
loginAttempts: 'Login Attempts',
loginUser: 'Login user',
loginWithAnotherUser: 'To log in with another user, you should <0>log out</0> first.',
logout: 'Logout',
logoutSuccessful: 'Logout successful.',
logoutUser: 'Logout user',
newAPIKeyGenerated: 'New API Key Generated.',
newAccountCreated:
'A new account has just been created for you to access <a href="{{serverURL}}">{{serverURL}}</a> Please click on the following link or paste the URL below into your browser to verify your email: <a href="{{verificationURL}}">{{verificationURL}}</a><br> After verifying your email, you will be able to log in successfully.',
newPassword: 'New Password',
passed: 'Authentication Passed',
passwordResetSuccessfully: 'Password reset successfully.',
resetPassword: 'Reset Password',
resetPasswordExpiration: 'Reset Password Expiration',
resetPasswordToken: 'Reset Password Token',
resetYourPassword: 'Reset Your Password',
stayLoggedIn: 'Stay logged in',
successfullyRegisteredFirstUser: 'Successfully registered first user.',
successfullyUnlocked: 'Successfully unlocked',
tokenRefreshSuccessful: 'Token refresh successful.',
unableToVerify: 'Unable to Verify',
verified: 'Verified',
verifiedSuccessfully: 'Verified Successfully',
verify: 'Verify',
verifyUser: 'Verify User',
verifyYourEmail: 'Verify your email',
youAreInactive:
"You haven't been active in a little while and will shortly be automatically logged out for your own security. Would you like to stay logged in?",
youAreReceivingResetPassword:
'You are receiving this because you (or someone else) have requested the reset of the password for your account. Please click on the following link, or paste this into your browser to complete the process:',
youDidNotRequestPassword:
'If you did not request this, please ignore this email and your password will remain unchanged.',
},
error: {
accountAlreadyActivated: 'This account has already been activated.',
autosaving: 'There was a problem while autosaving this document.',
correctInvalidFields: 'Please correct invalid fields.',
deletingFile: 'There was an error deleting file.',
deletingTitle:
'There was an error while deleting {{title}}. Please check your connection and try again.',
emailOrPasswordIncorrect: 'The email or password provided is incorrect.',
followingFieldsInvalid_one: 'The following field is invalid:',
followingFieldsInvalid_other: 'The following fields are invalid:',
incorrectCollection: 'Incorrect Collection',
invalidFileType: 'Invalid file type',
invalidFileTypeValue: 'Invalid file type: {{value}}',
loadingDocument: 'There was a problem loading the document with ID of {{id}}.',
localesNotSaved_one: 'The following locale could not be saved:',
localesNotSaved_other: 'The following locales could not be saved:',
logoutFailed: 'Logout failed.',
missingEmail: 'Missing email.',
missingIDOfDocument: 'Missing ID of document to update.',
missingIDOfVersion: 'Missing ID of version.',
missingRequiredData: 'Missing required data.',
noFilesUploaded: 'No files were uploaded.',
noMatchedField: 'No matched field found for "{{label}}"',
noUser: 'No User',
notAllowedToAccessPage: 'You are not allowed to access this page.',
notAllowedToPerformAction: 'You are not allowed to perform this action.',
notFound: 'The requested resource was not found.',
previewing: 'There was a problem previewing this document.',
problemUploadingFile: 'There was a problem while uploading the file.',
tokenInvalidOrExpired: 'Token is either invalid or has expired.',
tokenNotProvided: 'Token not provided.',
unPublishingDocument: 'There was a problem while un-publishing this document.',
unableToDeleteCount: 'Unable to delete {{count}} out of {{total}} {{label}}.',
unableToUpdateCount: 'Unable to update {{count}} out of {{total}} {{label}}.',
unauthorized: 'Unauthorized, you must be logged in to make this request.',
unknown: 'An unknown error has occurred.',
unspecific: 'An error has occurred.',
userLocked: 'This user is locked due to having too many failed login attempts.',
valueMustBeUnique: 'Value must be unique',
verificationTokenInvalid: 'Verification token is invalid.',
},
fields: {
addLabel: 'Add {{label}}',
addLink: 'Add Link',
addNew: 'Add new',
addNewLabel: 'Add new {{label}}',
addRelationship: 'Add Relationship',
addUpload: 'Add Upload',
block: 'block',
blockType: 'Block Type',
blocks: 'blocks',
chooseBetweenCustomTextOrDocument:
'Choose between entering a custom text URL or linking to another document.',
chooseDocumentToLink: 'Choose a document to link to',
chooseFromExisting: 'Choose from existing',
chooseLabel: 'Choose {{label}}',
collapseAll: 'Collapse All',
customURL: 'Custom URL',
editLabelData: 'Edit {{label}} data',
editLink: 'Edit Link',
editRelationship: 'Edit Relationship',
enterURL: 'Enter a URL',
internalLink: 'Internal Link',
itemsAndMore: '{{items}} and {{count}} more',
labelRelationship: '{{label}} Relationship',
latitude: 'Latitude',
linkType: 'Link Type',
linkedTo: 'Linked to <0>{{label}}</0>',
longitude: 'Longitude',
newLabel: 'New {{label}}',
openInNewTab: 'Open in new tab',
passwordsDoNotMatch: 'Passwords do not match.',
relatedDocument: 'Related Document',
relationTo: 'Relation To',
removeRelationship: 'Remove Relationship',
removeUpload: 'Remove Upload',
saveChanges: 'Save changes',
searchForBlock: 'Search for a block',
selectExistingLabel: 'Select existing {{label}}',
selectFieldsToEdit: 'Select fields to edit',
showAll: 'Show All',
swapRelationship: 'Swap Relationship',
swapUpload: 'Swap Upload',
textToDisplay: 'Text to display',
toggleBlock: 'Toggle block',
uploadNewLabel: 'Upload new {{label}}',
},
general: {
aboutToDelete: 'You are about to delete the {{label}} <1>{{title}}</1>. Are you sure?',
aboutToDeleteCount_many: 'You are about to delete {{count}} {{label}}',
aboutToDeleteCount_one: 'You are about to delete {{count}} {{label}}',
aboutToDeleteCount_other: 'You are about to delete {{count}} {{label}}',
addBelow: 'Add Below',
addFilter: 'Add Filter',
adminTheme: 'Admin Theme',
and: 'And',
applyChanges: 'Apply Changes',
ascending: 'Ascending',
automatic: 'Automatic',
backToDashboard: 'Back to Dashboard',
cancel: 'Cancel',
changesNotSaved:
'Your changes have not been saved. If you leave now, you will lose your changes.',
close: 'Close',
collapse: 'Collapse',
collections: 'Collections',
columnToSort: 'Column to Sort',
columns: 'Columns',
confirm: 'Confirm',
confirmDeletion: 'Confirm deletion',
confirmDuplication: 'Confirm duplication',
copied: 'Copied',
copy: 'Copy',
create: 'Create',
createNew: 'Create New',
createNewLabel: 'Create new {{label}}',
created: 'Created',
createdAt: 'Created At',
creating: 'Creating',
creatingNewLabel: 'Creating new {{label}}',
custom: 'Custom',
dark: 'Dark',
dashboard: 'Dashboard',
delete: 'Delete',
deletedCountSuccessfully: 'Deleted {{count}} {{label}} successfully.',
deletedSuccessfully: 'Deleted successfully.',
deleting: 'Deleting...',
depth: 'Depth',
descending: 'Descending',
deselectAllRows: 'Deselect all rows',
document: 'Document',
documents: 'Documents',
duplicate: 'Duplicate',
duplicateWithoutSaving: 'Duplicate without saving changes',
edit: 'Edit',
editLabel: 'Edit {{label}}',
editing: 'Editing',
editingLabel_many: 'Editing {{count}} {{label}}',
editingLabel_one: 'Editing {{count}} {{label}}',
editingLabel_other: 'Editing {{count}} {{label}}',
email: 'Email',
emailAddress: 'Email Address',
enterAValue: 'Enter a value',
error: 'Error',
errors: 'Errors',
fallbackToDefaultLocale: 'Fallback to default locale',
false: 'False',
filter: 'Filter',
filterWhere: 'Filter {{label}} where',
filters: 'Filters',
globals: 'Globals',
language: 'Language',
lastModified: 'Last Modified',
leaveAnyway: 'Leave anyway',
leaveWithoutSaving: 'Leave without saving',
light: 'Light',
livePreview: 'Live Preview',
loading: 'Loading',
locale: 'Locale',
locales: 'Locales',
menu: 'Menu',
moveDown: 'Move Down',
moveUp: 'Move Up',
newPassword: 'New Password',
noFiltersSet: 'No filters set',
noLabel: '<No {{label}}>',
noOptions: 'No options',
noResults:
"No {{label}} found. Either no {{label}} exist yet or none match the filters you've specified above.",
noValue: 'No value',
none: 'None',
notFound: 'Not Found',
nothingFound: 'Nothing found',
of: 'of',
open: 'Open',
or: 'Or',
order: 'Order',
pageNotFound: 'Page not found',
password: 'Password',
payloadSettings: 'Payload Settings',
perPage: 'Per Page: {{limit}}',
remove: 'Remove',
reset: 'Reset',
row: 'Row',
rows: 'Rows',
save: 'Save',
saving: 'Saving...',
searchBy: 'Search by {{label}}',
selectAll: 'Select all {{count}} {{label}}',
selectAllRows: 'Select all rows',
selectValue: 'Select a value',
selectedCount: '{{count}} {{label}} selected',
showAllLabel: 'Show all {{label}}',
sorryNotFound: 'Sorry—there is nothing to correspond with your request.',
sort: 'Sort',
sortByLabelDirection: 'Sort by {{label}} {{direction}}',
stayOnThisPage: 'Stay on this page',
submissionSuccessful: 'Submission Successful.',
submit: 'Submit',
success: 'Success',
successfullyCreated: '{{label}} successfully created.',
successfullyDuplicated: '{{label}} successfully duplicated.',
thisLanguage: 'English',
titleDeleted: '{{label}} "{{title}}" successfully deleted.',
true: 'True',
unauthorized: 'Unauthorized',
unsavedChangesDuplicate: 'You have unsaved changes. Would you like to continue to duplicate?',
untitled: 'Untitled',
updatedAt: 'Updated At',
updatedCountSuccessfully: 'Updated {{count}} {{label}} successfully.',
updatedSuccessfully: 'Updated successfully.',
updating: 'Updating',
uploading: 'Uploading',
user: 'User',
users: 'Users',
value: 'Value',
welcome: 'Welcome',
},
operators: {
contains: 'contains',
equals: 'equals',
exists: 'exists',
isGreaterThan: 'is greater than',
isGreaterThanOrEqualTo: 'is greater than or equal to',
isIn: 'is in',
isLessThan: 'is less than',
isLessThanOrEqualTo: 'is less than or equal to',
isLike: 'is like',
isNotEqualTo: 'is not equal to',
isNotIn: 'is not in',
near: 'near',
},
upload: {
crop: 'Crop',
cropToolDescription:
'Drag the corners of the selected area, draw a new area or adjust the values below.',
dragAndDrop: 'Drag and drop a file',
dragAndDropHere: 'or drag and drop a file here',
editImage: 'Edit Image',
fileName: 'File Name',
fileSize: 'File Size',
focalPoint: 'Focal Point',
focalPointDescription:
'Drag the focal point directly on the preview or adjust the values below.',
height: 'Height',
lessInfo: 'Less info',
moreInfo: 'More info',
previewSizes: 'Preview Sizes',
selectCollectionToBrowse: 'Select a Collection to Browse',
selectFile: 'Select a file',
setCropArea: 'Set crop area',
setFocalPoint: 'Set focal point',
sizes: 'Sizes',
sizesFor: 'Sizes for {{label}}',
width: 'Width',
},
validation: {
emailAddress: 'Please enter a valid email address.',
enterNumber: 'Please enter a valid number.',
fieldHasNo: 'This field has no {{label}}',
greaterThanMax: '{{value}} is greater than the max allowed {{label}} of {{max}}.',
invalidInput: 'This field has an invalid input.',
invalidSelection: 'This field has an invalid selection.',
invalidSelections: 'This field has the following invalid selections:',
lessThanMin: '{{value}} is less than the min allowed {{label}} of {{min}}.',
limitReached: 'Limit reached, only {{max}} items can be added.',
longerThanMin:
'This value must be longer than the minimum length of {{minLength}} characters.',
notValidDate: '"{{value}}" is not a valid date.',
required: 'This field is required.',
requiresAtLeast: 'This field requires at least {{count}} {{label}}.',
requiresNoMoreThan: 'This field requires no more than {{count}} {{label}}.',
requiresTwoNumbers: 'This field requires two numbers.',
shorterThanMax: 'This value must be shorter than the max length of {{maxLength}} characters.',
trueOrFalse: 'This field can only be equal to true or false.',
validUploadID: 'This field is not a valid upload ID.',
},
version: {
type: 'Type',
aboutToPublishSelection:
'You are about to publish all {{label}} in the selection. Are you sure?',
aboutToRestore:
'You are about to restore this {{label}} document to the state that it was in on {{versionDate}}.',
aboutToRestoreGlobal:
'You are about to restore the global {{label}} to the state that it was in on {{versionDate}}.',
aboutToRevertToPublished:
"You are about to revert this document's changes to its published state. Are you sure?",
aboutToUnpublish: 'You are about to unpublish this document. Are you sure?',
aboutToUnpublishSelection:
'You are about to unpublish all {{label}} in the selection. Are you sure?',
autosave: 'Autosave',
autosavedSuccessfully: 'Autosaved successfully.',
autosavedVersion: 'Autosaved version',
changed: 'Changed',
compareVersion: 'Compare version against:',
confirmPublish: 'Confirm publish',
confirmRevertToSaved: 'Confirm revert to saved',
confirmUnpublish: 'Confirm unpublish',
confirmVersionRestoration: 'Confirm version Restoration',
currentDocumentStatus: 'Current {{docStatus}} document',
draft: 'Draft',
draftSavedSuccessfully: 'Draft saved successfully.',
lastSavedAgo: 'Last saved {{distance}} ago',
noFurtherVersionsFound: 'No further versions found',
noRowsFound: 'No {{label}} found',
preview: 'Preview',
problemRestoringVersion: 'There was a problem restoring this version',
publish: 'Publish',
publishChanges: 'Publish changes',
published: 'Published',
publishing: 'Publishing',
restoreThisVersion: 'Restore this version',
restoredSuccessfully: 'Restored Successfully.',
restoring: 'Restoring...',
revertToPublished: 'Revert to published',
reverting: 'Reverting...',
saveDraft: 'Save Draft',
selectLocales: 'Select locales to display',
selectVersionToCompare: 'Select a version to compare',
showLocales: 'Show locales:',
showingVersionsFor: 'Showing versions for:',
status: 'Status',
unpublish: 'Unpublish',
unpublishing: 'Unpublishing...',
version: 'Version',
versionCount_many: '{{count}} versions found',
versionCount_none: 'No versions found',
versionCount_one: '{{count}} version found',
versionCount_other: '{{count}} versions found',
versionCreatedOn: '{{version}} created on:',
versionID: 'Version ID',
versions: 'Versions',
viewingVersion: 'Viewing version for the {{entityLabel}} {{documentTitle}}',
viewingVersionGlobal: 'Viewing version for the global {{entityLabel}}',
viewingVersions: 'Viewing versions for the {{entityLabel}} {{documentTitle}}',
viewingVersionsGlobal: 'Viewing versions for the global {{entityLabel}}',
},
},
translations: enTranslations,
}

View File

@@ -1,3 +1,4 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const es: Language = {

View File

@@ -1,3 +1,5 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const fa: Language = {

View File

@@ -1,3 +1,5 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const fr: Language = {

View File

@@ -1,3 +1,5 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const hr: Language = {

View File

@@ -1,3 +1,5 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const hu: Language = {

View File

@@ -1,3 +1,5 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const it: Language = {

View File

@@ -1,3 +1,5 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const ja: Language = {

View File

@@ -1,3 +1,5 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const ko: Language = {

View File

@@ -1,3 +1,5 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const my: Language = {

View File

@@ -1,3 +1,5 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const nb: Language = {

View File

@@ -1,3 +1,5 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const nl: Language = {

View File

@@ -1,3 +1,5 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const pl: Language = {

View File

@@ -1,3 +1,5 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const pt: Language = {

View File

@@ -1,3 +1,5 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const ro: Language = {

View File

@@ -1,3 +1,5 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const rs: Language = {

View File

@@ -1,3 +1,5 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const rsLatin: Language = {

View File

@@ -1,3 +1,5 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const ru: Language = {

View File

@@ -1,3 +1,4 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const sk: Language = {
@@ -9,7 +10,7 @@ export const sk: Language = {
alreadyActivated: 'Už aktivované',
alreadyLoggedIn: 'Už prihlásený',
apiKey: 'API kľúč',
authenticated: "Overený",
authenticated: 'Overený',
backToLogin: 'Späť na prihlásenie',
beginCreateFirstUser: 'Začnite vytvorením prvého používateľa.',
changePassword: 'Zmeniť heslo',
@@ -104,7 +105,8 @@ export const sk: Language = {
unauthorized: 'Neautorizováno, pro zadání tohoto požadavku musíte být přihlášeni.',
unknown: 'Došlo k neznámej chybe.',
unspecific: 'Došlo k chybe.',
userLocked: 'Tento používateľ je uzamknutý kvôli príliš mnohým neúspešným pokusom o prihlásenie.',
userLocked:
'Tento používateľ je uzamknutý kvôli príliš mnohým neúspešným pokusom o prihlásenie.',
valueMustBeUnique: 'Hodnota musí byť jedinečná',
verificationTokenInvalid: 'Overovací token je neplatný.',
},
@@ -192,7 +194,7 @@ export const sk: Language = {
deletedCountSuccessfully: 'Úspešne zmazané {{count}} {{label}}.',
deletedSuccessfully: 'Úspešne odstránené.',
deleting: 'Odstraňovanie...',
depth: "Hĺbka",
depth: 'Hĺbka',
descending: 'Zostupne',
deselectAllRows: 'Zrušiť výber všetkých riadkov',
document: 'Dokument',
@@ -231,7 +233,8 @@ export const sk: Language = {
noFiltersSet: 'Nie sú nastavené žiadne filtre',
noLabel: '<Žiadny {{label}}>',
noOptions: 'Žiadne možnosti',
noResults: 'Neboli nájdené žiadne {{label}}. Buď neexistujú žiadne {{label}}, alebo žiadne nespĺňajú filtre, ktoré ste zadali vyššie.',
noResults:
'Neboli nájdené žiadne {{label}}. Buď neexistujú žiadne {{label}}, alebo žiadne nespĺňajú filtre, ktoré ste zadali vyššie.',
noValue: 'Žiadna hodnota',
none: 'Žiadny',
notFound: 'Nenájdené',
@@ -295,14 +298,16 @@ export const sk: Language = {
},
upload: {
crop: 'Orezať',
cropToolDescription: 'Potiahnite rohy vybranej oblasti, nakreslite novú oblasť alebo upravte hodnoty nižšie.',
cropToolDescription:
'Potiahnite rohy vybranej oblasti, nakreslite novú oblasť alebo upravte hodnoty nižšie.',
dragAndDrop: 'Potiahnite a pusťte súbor',
dragAndDropHere: 'alebo sem potiahnite a pusťte súbor',
editImage: 'Upraviť obrázok',
fileName: 'Názov súboru',
fileSize: 'Veľkosť súboru',
focalPoint: 'Stredobod',
focalPointDescription: 'Potiahnite bod stredobodu priamo na náhľad alebo upravte hodnoty nižšie.',
focalPointDescription:
'Potiahnite bod stredobodu priamo na náhľad alebo upravte hodnoty nižšie.',
height: 'Výška',
lessInfo: 'Menej informácií',
moreInfo: 'Viac informácií',
@@ -338,11 +343,15 @@ export const sk: Language = {
version: {
type: 'Typ',
aboutToPublishSelection: 'Chystáte sa publikovať všetky {{label}} vo výbere. Ste si istý?',
aboutToRestore: 'Chystáte sa obnoviť tento {{label}} dokument do stavu, v akom bol {{versionDate}}.',
aboutToRestoreGlobal: 'Chystáte sa obnoviť globálne {{label}} do stavu, v akom bol {{versionDate}}.',
aboutToRevertToPublished: 'Chystáte sa vrátiť zmeny tohto dokumentu do jeho publikovaného stavu. Ste si istý?',
aboutToRestore:
'Chystáte sa obnoviť tento {{label}} dokument do stavu, v akom bol {{versionDate}}.',
aboutToRestoreGlobal:
'Chystáte sa obnoviť globálne {{label}} do stavu, v akom bol {{versionDate}}.',
aboutToRevertToPublished:
'Chystáte sa vrátiť zmeny tohto dokumentu do jeho publikovaného stavu. Ste si istý?',
aboutToUnpublish: 'Chystáte sa zrušiť publikovanie tohto dokumentu. Ste si istý?',
aboutToUnpublishSelection: 'Chystáte sa zrušiť publikovanie všetkých {{label}} vo výbere. Ste si istý?',
aboutToUnpublishSelection:
'Chystáte sa zrušiť publikovanie všetkých {{label}} vo výbere. Ste si istý?',
autosave: 'Automatické uloženie',
autosavedSuccessfully: 'Úspešne uložené automaticky.',
autosavedVersion: 'Verzia automatického uloženia',

View File

@@ -1,3 +1,4 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const sv: Language = {

View File

@@ -1,3 +1,4 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const th: Language = {

View File

@@ -1,3 +1,4 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const tr: Language = {

View File

@@ -1,3 +1,4 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const uk: Language = {

View File

@@ -1,3 +1,4 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const vi: Language = {

View File

@@ -1,3 +1,4 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const zh: Language = {

View File

@@ -1,3 +1,4 @@
// @ts-nocheck // TODO: Remove this and add missing translations
import type { Language } from '../types.js'
export const zhTw: Language = {

View File

@@ -1,5 +1,7 @@
import type { Locale } from 'date-fns'
import type { clientTranslationKeys } from './clientKeys.js'
import type { enTranslations } from './languages/en.js'
import type { acceptedLanguages } from './utilities/languages.js'
type DateFNSKeys =
@@ -32,24 +34,85 @@ type DateFNSKeys =
| 'zh-CN'
| 'zh-TW'
export type Language = {
export type Language<TDefaultTranslations = DefaultTranslationsObject> = {
dateFNSKey: DateFNSKeys
translations: {
[namespace: string]: {
[key: string]: string
}
}
translations: TDefaultTranslations
}
export type AcceptedLanguages = (typeof acceptedLanguages)[number]
export type SupportedLanguages = {
[key in AcceptedLanguages]?: Language
export type SupportedLanguages<TDefaultTranslations = DefaultTranslationsObject> = {
[key in AcceptedLanguages]?: Language<TDefaultTranslations>
}
export type TFunction = (key: string, options?: Record<string, any>) => string
/**
* Type utilities for converting between translation objects ( e.g. general: { createNew: 'Create New' } ) and translations keys ( e.g. general:createNew )
*/
export type I18n = {
export type NestedKeysUnSanitized<T> = T extends object
? {
[K in keyof T]-?: K extends string
? T[K] extends object
? `${K}:${NestedKeysUnSanitized<T[K]>}` | null
: `${K}`
: never
}[keyof T]
: ''
// Utility type to strip specific suffixes
export type StripCountVariants<TKey> = TKey extends
| `${infer Base}_many`
| `${infer Base}_one`
| `${infer Base}_other`
? Base
: TKey
export type NestedKeysStripped<T> = T extends object
? {
[K in keyof T]-?: K extends string
? T[K] extends object
? `${K}:${StripCountVariants<NestedKeysStripped<T[K]>>}` | null
: `${StripCountVariants<K>}`
: never
}[keyof T]
: ''
export type ReconstructObjectFromTranslationKeys<
TPath extends string,
TValue = string,
> = TPath extends `${infer First}:${infer Rest}`
? { [K in First]: ReconstructObjectFromTranslationKeys<Rest, TValue> }
: { [K in TPath]: TValue }
/**
* Default nested translations object
*/
export type DefaultTranslationsObject = typeof enTranslations
/**
* All translation keys unSanitized. E.g. 'general:aboutToDeleteCount_many'
*/
export type DefaultTranslationKeysUnSanitized = NestedKeysUnSanitized<DefaultTranslationsObject>
/**
* All translation keys sanitized. E.g. 'general:aboutToDeleteCount'
*/
export type DefaultTranslationKeys = NestedKeysStripped<DefaultTranslationsObject>
export type ClientTranslationKeys<TExtraProps = (typeof clientTranslationKeys)[number]> =
TExtraProps
export type ClientTranslationsObject = ReconstructObjectFromTranslationKeys<ClientTranslationKeys>
export type TFunction<TTranslationKeys = DefaultTranslationKeys> = (
key: TTranslationKeys,
options?: Record<string, any>,
) => string
export type I18n<
TTranslations = DefaultTranslationsObject,
TTranslationKeys = DefaultTranslationKeys,
> = {
dateFNS: Locale
/** Corresponding dateFNS key */
dateFNSKey: DateFNSKeys
@@ -58,34 +121,56 @@ export type I18n = {
/** The language of the request */
language: string
/** Translate function */
t: TFunction
translations: Language['translations']
t: TFunction<TTranslationKeys>
translations: Language<TTranslations>['translations']
}
export type I18nOptions = {
export type I18nOptions<TTranslations = DefaultTranslationsObject> = {
fallbackLanguage?: AcceptedLanguages
supportedLanguages?: SupportedLanguages
translations?: Partial<{
[key in AcceptedLanguages]?: Language['translations']
[key in AcceptedLanguages]?: Language<TTranslations>['translations']
}>
}
export type InitTFunction = (args: {
config: I18nOptions
export type InitTFunction<
TTranslations = DefaultTranslationsObject,
TTranslationKeys = DefaultTranslationKeys,
> = (args: {
config: I18nOptions<TTranslations>
language?: string
translations: Language['translations']
translations: Language<TTranslations>['translations']
}) => {
t: TFunction
translations: Language['translations']
t: TFunction<TTranslationKeys>
translations: Language<TTranslations>['translations']
}
export type InitI18n = (args: {
config: I18nOptions
context: 'api' | 'client'
language: AcceptedLanguages
}) => Promise<I18n>
export type InitI18n =
| ((args: {
config: I18nOptions<ClientTranslationsObject>
context: 'client'
language: AcceptedLanguages
}) => Promise<I18n<ClientTranslationsObject, ClientTranslationKeys>>)
| ((args: { config: I18nOptions; context: 'api'; language: AcceptedLanguages }) => Promise<I18n>)
export type LanguagePreference = {
language: AcceptedLanguages
quality?: number
}
export type I18nClient<TAdditionalTranslations = {}, TAdditionalKeys extends string = never> = I18n<
TAdditionalTranslations extends object
? ClientTranslationsObject & TAdditionalTranslations
: ClientTranslationsObject,
[TAdditionalKeys] extends [never]
? ClientTranslationKeys
: ClientTranslationKeys | TAdditionalKeys
>
export type I18nServer<TAdditionalTranslations = {}, TAdditionalKeys extends string = never> = I18n<
TAdditionalTranslations extends object
? DefaultTranslationsObject & TAdditionalTranslations
: DefaultTranslationsObject,
[TAdditionalKeys] extends [never]
? DefaultTranslationKeys
: DefaultTranslationKeys | TAdditionalKeys
>

View File

@@ -10,7 +10,7 @@ type LabelType =
export const getTranslation = <T extends LabelType>(
label: T,
i18n: Pick<I18n, 'fallbackLanguage' | 'language' | 't'>,
i18n: Pick<I18n<any, any>, 'fallbackLanguage' | 'language' | 't'>,
): T extends JSX.Element ? JSX.Element : string => {
// If it's a Record, look for translation. If string or React Element, pass through
if (typeof label === 'object' && !Object.prototype.hasOwnProperty.call(label, '$$typeof')) {

View File

@@ -1,4 +1,11 @@
import type { I18n, InitI18n, InitTFunction, Language } from '../types.js'
import type {
DefaultTranslationsObject,
I18n,
InitI18n,
InitTFunction,
Language,
} from '../types.js'
import type { DefaultTranslationKeys } from '../types.js'
import { importDateFNSLocale } from '../importDateFNSLocale.js'
import { deepMerge } from './deepMerge.js'
@@ -11,16 +18,19 @@ import { getTranslationsByContext } from './getTranslationsByContext.js'
*
* @returns string
*/
export const getTranslationString = ({
export const getTranslationString = <
TTranslations = DefaultTranslationsObject,
TTranslationKeys = DefaultTranslationKeys,
>({
count,
key,
translations,
}: {
count?: number
key: string
translations: Language['translations']
}) => {
const keys = key.split(':')
key: TTranslationKeys
translations: Language<TTranslations>['translations']
}): string => {
const keys = (key as DefaultTranslationKeys).split(':')
let keySuffix = ''
const translation: string = keys.reduce((acc: any, key, index) => {
@@ -54,10 +64,10 @@ export const getTranslationString = ({
}, translations)
if (!translation) {
console.log('key not found: ', key)
console.log('key not found:', key)
}
return translation || key
return translation || (key as string)
}
/**
@@ -99,17 +109,18 @@ const replaceVars = ({
*
* @returns string
*/
type TFunctionConstructor = ({
export function t<
TTranslations = DefaultTranslationsObject,
TTranslationKeys = DefaultTranslationKeys,
>({
key,
translations,
vars,
}: {
key: string
translations?: Language['translations']
key: TTranslationKeys
translations?: Language<TTranslations>['translations']
vars?: Record<string, any>
}) => string
export const t: TFunctionConstructor = ({ key, translations, vars }) => {
}): string {
let translationString = getTranslationString({
count: typeof vars?.count === 'number' ? vars.count : undefined,
key,
@@ -124,7 +135,7 @@ export const t: TFunctionConstructor = ({ key, translations, vars }) => {
}
if (!translationString) {
translationString = key
translationString = key as string
}
return translationString
@@ -146,7 +157,14 @@ const initTFunction: InitTFunction = (args) => {
}
}
function memoize(fn: (args: unknown) => Promise<I18n>, keys: string[]) {
function memoize(
fn: (args: Parameters<InitI18n>[0]) => Promise<I18n>,
keys: string[],
): (
args: Parameters<InitI18n>[0] & {
context: 'api' | 'client'
},
) => Promise<I18n> {
const cacheMap = new Map()
const memoized = async (args) => {
@@ -163,14 +181,14 @@ function memoize(fn: (args: unknown) => Promise<I18n>, keys: string[]) {
return memoized
}
export const initI18n: InitI18n = memoize(
async ({ config, context, language = config.fallbackLanguage }: Parameters<InitI18n>[0]) => {
export const initI18n = memoize(
async ({ config, context, language = config.fallbackLanguage }) => {
const translations = getTranslationsByContext(config.supportedLanguages[language], context)
const { t, translations: mergedTranslations } = initTFunction({
config,
config: config as any,
language: language || config.fallbackLanguage,
translations,
translations: translations as any,
})
const dateFNSKey = config.supportedLanguages[language]?.dateFNSKey || 'en-US'

View File

@@ -1,4 +1,4 @@
import type { I18n } from '@payloadcms/translations'
import type { I18nClient } from '@payloadcms/translations'
import React from 'react'
@@ -9,7 +9,7 @@ const baseClass = 'error-pill'
export type ErrorPillProps = {
className?: string
count: number
i18n: I18n
i18n: I18nClient
withMessage?: boolean
}

View File

@@ -40,7 +40,7 @@ export const Status: React.FC = () => {
const unPublishModalSlug = `confirm-un-publish-${id}`
const revertModalSlug = `confirm-revert-${id}`
let statusToRender
let statusToRender: 'changed' | 'draft' | 'published'
if (unpublishedVersions?.docs?.length > 0 && publishedDoc) {
statusToRender = 'changed'

View File

@@ -1,4 +1,4 @@
import type { TFunction } from '@payloadcms/translations'
import type { ClientTranslationKeys, TFunction } from '@payloadcms/translations'
import * as React from 'react'
@@ -36,7 +36,7 @@ const RecursiveTranslation: React.FC<{
export type TranslationProps = {
elements?: Record<string, React.FC<{ children: React.ReactNode }>>
i18nKey: string
i18nKey: ClientTranslationKeys
t: TFunction
variables?: Record<string, unknown>
}

View File

@@ -1,4 +1,4 @@
import type { I18n } from '@payloadcms/translations'
import type { I18nClient } from '@payloadcms/translations'
import type { PaginatedDocs } from 'payload/database'
import type { ClientCollectionConfig, RelationshipField } from 'payload/types'
@@ -16,7 +16,7 @@ export type Option = {
}
type CLEAR = {
i18n: I18n
i18n: I18nClient
required: boolean
type: 'CLEAR'
}
@@ -25,7 +25,7 @@ type ADD = {
collection: ClientCollectionConfig
data: PaginatedDocs<any>
hasMultipleRelations: boolean
i18n: I18n
i18n: I18nClient
relation: string
type: 'ADD'
}

View File

@@ -1,5 +1,5 @@
'use client'
import type { I18n } from '@payloadcms/translations'
import type { I18nClient } from '@payloadcms/translations'
import type { Labels } from 'payload/types'
import * as facelessUIImport from '@faceless-ui/modal'
@@ -25,7 +25,7 @@ export type Props = {
const baseClass = 'blocks-drawer'
const getBlockLabel = (block: ReducedBlock, i18n: I18n) => {
const getBlockLabel = (block: ReducedBlock, i18n: I18nClient) => {
if (typeof block.labels.singular === 'string') return block.labels.singular.toLowerCase()
if (typeof block.labels.singular === 'object') {
return getTranslation(block.labels.singular, i18n).toLowerCase()

View File

@@ -1,4 +1,4 @@
import type { I18n } from '@payloadcms/translations'
import type { I18nClient } from '@payloadcms/translations'
import type { ClientCollectionConfig, FieldBase, RelationshipField } from 'payload/types'
import type { SanitizedConfig } from 'payload/types'
@@ -41,7 +41,7 @@ type UPDATE = {
collection: ClientCollectionConfig
config: SanitizedConfig
doc: any
i18n: I18n
i18n: I18nClient
type: 'UPDATE'
}
@@ -49,7 +49,7 @@ type ADD = {
collection: ClientCollectionConfig
config: SanitizedConfig
docs: any[]
i18n: I18n
i18n: I18nClient
ids?: (number | string)[]
sort?: boolean
type: 'ADD'

View File

@@ -1,11 +1,11 @@
import type { I18n } from '@payloadcms/translations'
import type { I18nClient } from '@payloadcms/translations'
import type { LabelProps } from 'payload/types'
import type React from 'react'
export type Props = {
RowLabelComponent?: React.ReactNode
className?: string
i18n: I18n
i18n: I18nClient
path: string
rowLabel?: LabelProps['label']
rowNumber?: number

View File

@@ -1,4 +1,4 @@
import type { I18n } from '@payloadcms/translations'
import type { I18nClient } from '@payloadcms/translations'
import type { ViewDescriptionProps } from '@payloadcms/ui/elements/ViewDescription'
import type {
AdminViewProps,
@@ -22,7 +22,7 @@ export const mapCollections = (args: {
WithServerSideProps: WithServerSidePropsType
collections: SanitizedCollectionConfig[]
config: SanitizedConfig
i18n: I18n
i18n: I18nClient
readOnly?: boolean
}): {
[key: SanitizedCollectionConfig['slug']]: CollectionComponentMap

View File

@@ -1,4 +1,4 @@
import type { I18n } from '@payloadcms/translations'
import type { I18nClient } from '@payloadcms/translations'
import type { CustomComponent } from 'payload/config'
import type {
CellComponentProps,
@@ -55,7 +55,7 @@ export const mapFields = (args: {
disableAddingID?: boolean
fieldSchema: FieldWithPath[]
filter?: (field: Field) => boolean
i18n: I18n
i18n: I18nClient
parentPath?: string
readOnly?: boolean
}): FieldMap => {

View File

@@ -1,9 +1,9 @@
import type { I18n } from '@payloadcms/translations'
import type { I18nClient } from '@payloadcms/translations'
import type {
EditViewProps,
SanitizedConfig,
SanitizedGlobalConfig,
WithServerSideProps as WithServerSidePropsType,
WithServerSideProps,
} from 'payload/types'
import { ViewDescription, type ViewDescriptionProps } from '@payloadcms/ui/elements/ViewDescription'
@@ -14,13 +14,17 @@ import type { GlobalComponentMap } from './types.js'
import { mapActions } from './actions.js'
import { mapFields } from './fields.js'
export const mapGlobals = (args: {
DefaultEditView: React.FC<EditViewProps>
WithServerSideProps: WithServerSidePropsType
config: SanitizedConfig
globals: SanitizedGlobalConfig[]
i18n: I18n
readOnly?: boolean
export const mapGlobals = ({
args,
}: {
args: {
DefaultEditView: React.FC<EditViewProps>
WithServerSideProps: WithServerSideProps
config: SanitizedConfig
globals: SanitizedGlobalConfig[]
i18n: I18nClient
readOnly?: boolean
}
}): {
[key: SanitizedGlobalConfig['slug']]: GlobalComponentMap
} => {

View File

@@ -1,4 +1,4 @@
import type { I18n } from '@payloadcms/translations'
import type { I18nClient } from '@payloadcms/translations'
import type {
AdminViewProps,
EditViewProps,
@@ -20,7 +20,7 @@ export const buildComponentMap = (args: {
DefaultListView: React.FC<AdminViewProps>
children: React.ReactNode
config: SanitizedConfig
i18n: I18n
i18n: I18nClient
payload: Payload
readOnly?: boolean
}): {
@@ -44,12 +44,14 @@ export const buildComponentMap = (args: {
})
const globals = mapGlobals({
DefaultEditView,
WithServerSideProps,
config,
globals: config.globals,
i18n,
readOnly,
args: {
DefaultEditView,
WithServerSideProps,
config,
globals: config.globals,
i18n,
readOnly,
},
})
const NestProviders = ({ children, providers }) => {

View File

@@ -1,5 +1,5 @@
'use client'
import type { Language } from '@payloadcms/translations'
import type { I18nClient, Language } from '@payloadcms/translations'
import type { ClientConfig } from 'payload/types'
import * as facelessUIImport from '@faceless-ui/modal'
@@ -39,7 +39,7 @@ type Props = {
languageCode: string
languageOptions: LanguageOptions
switchLanguageServerAction?: (lang: string) => Promise<void>
translations: Language['translations']
translations: I18nClient['translations']
}
export const RootProvider: React.FC<Props> = ({

View File

@@ -1,5 +1,11 @@
'use client'
import type { I18n, Language } from '@payloadcms/translations'
import type { ClientTranslationKeys, I18nClient } from '@payloadcms/translations'
import type {
AcceptedLanguages,
ClientTranslationsObject,
Language,
TFunction,
} from '@payloadcms/translations'
import type { Locale } from 'date-fns'
import type { ClientConfig } from 'payload/types'
@@ -15,23 +21,33 @@ export type LanguageOptions = {
value: string
}[]
const Context = createContext<{
i18n: I18n
type ContextType<
TAdditionalTranslations = {},
TAdditionalClientTranslationKeys extends string = never,
> = {
i18n: [TAdditionalClientTranslationKeys] extends [never]
? I18nClient
: TAdditionalTranslations extends object
? I18nClient<TAdditionalTranslations, TAdditionalClientTranslationKeys>
: I18nClient<ClientTranslationsObject, TAdditionalClientTranslationKeys>
languageOptions: LanguageOptions
switchLanguage?: (lang: string) => Promise<void>
t: (key: string, vars?: Record<string, any>) => string
}>({
switchLanguage?: (lang: AcceptedLanguages) => Promise<void>
t: TFunction<ClientTranslationKeys | Extract<TAdditionalClientTranslationKeys, string>>
}
const Context = createContext<ContextType<any, any>>({
// Use `any` here to be replaced later with a more specific type when used
i18n: {
dateFNS: enUS,
dateFNSKey: 'en-US',
fallbackLanguage: 'en',
language: 'en',
t: (key) => key,
translations: {},
translations: {} as any,
},
languageOptions: undefined,
switchLanguage: undefined,
t: (key) => key,
t: (key) => undefined,
})
type Props = {
@@ -41,7 +57,7 @@ type Props = {
language: string
languageOptions: LanguageOptions
switchLanguageServerAction: (lang: string) => Promise<void>
translations: Language['translations']
translations: I18nClient['translations']
}
export const TranslationProvider: React.FC<Props> = ({
@@ -56,7 +72,7 @@ export const TranslationProvider: React.FC<Props> = ({
const { clearRouteCache } = useRouteCache()
const [dateFNS, setDateFNS] = useState<Locale>()
const nextT = (key: string, vars?: Record<string, unknown>): string =>
const nextT: ContextType['t'] = (key, vars): string =>
t({
key,
translations,
@@ -107,4 +123,7 @@ export const TranslationProvider: React.FC<Props> = ({
)
}
export const useTranslation = () => useContext(Context)
export const useTranslation = <
TAdditionalTranslations = {},
TAdditionalClientTranslationKeys extends string = never,
>() => useContext<ContextType<TAdditionalTranslations, TAdditionalClientTranslationKeys>>(Context)

View File

@@ -4,7 +4,7 @@ import { format, formatDistanceToNow } from 'date-fns'
type FormatDateArgs = {
date: Date | number | string | undefined
i18n: I18n
i18n: I18n<any, any>
pattern: string
}
@@ -15,7 +15,7 @@ export const formatDate = ({ date, i18n, pattern }: FormatDateArgs): string => {
type FormatTimeToNowArgs = {
date: Date | number | string | undefined
i18n: I18n
i18n: I18n<any, any>
}
export const formatTimeToNow = ({ date, i18n }: FormatTimeToNowArgs): string => {

View File

@@ -23,7 +23,7 @@ export const formatDocTitle = ({
dateFormat: SanitizedConfig['admin']['dateFormat']
fallback?: string
globalConfig?: ClientGlobalConfig
i18n: I18n
i18n: I18n<any, any>
}): string => {
let title: string

View File

@@ -1,4 +1,4 @@
import type { I18n } from '@payloadcms/translations'
import type { I18nClient } from '@payloadcms/translations'
import type { Permissions } from 'payload/auth'
import type { ClientCollectionConfig, ClientGlobalConfig } from 'payload/types'
@@ -27,7 +27,7 @@ export type Group = {
export function groupNavItems(
entities: EntityToGroup[],
permissions: Permissions,
i18n: I18n,
i18n: I18nClient,
): Group[] {
const result = entities.reduce(
(groups, entityToGroup) => {

View File

@@ -426,28 +426,41 @@ describe('relationship', () => {
await page.goto(url.list)
await page.waitForURL(url.list)
await wait(400)
await page.locator('.list-controls__toggle-columns').click()
await wait(400)
await page.locator('.list-controls__toggle-where').click()
await expect(page.locator('.list-controls__where.rah-static--height-auto')).toBeVisible()
await wait(400)
await page.locator('.where-builder__add-first-filter').click()
await wait(400)
const conditionField = page.locator('.condition__field')
await conditionField.click()
await wait(400)
const dropdownFieldOptions = conditionField.locator('.rs__option')
await dropdownFieldOptions.locator('text=Relationship').nth(0).click()
await wait(400)
const operatorField = page.locator('.condition__operator')
await operatorField.click()
await wait(400)
const dropdownOperatorOptions = operatorField.locator('.rs__option')
await dropdownOperatorOptions.locator('text=equals').click()
await wait(400)
const valueField = page.locator('.condition__value')
await valueField.click()
await wait(400)
const dropdownValueOptions = valueField.locator('.rs__option')
await dropdownValueOptions.locator('text=some text').click()
await wait(400)
await expect(page.locator(tableRowLocator)).toHaveCount(1)
})

View File

@@ -0,0 +1,59 @@
'use client'
import { useTranslation } from '@payloadcms/ui/providers/Translation'
import React from 'react'
import type { CustomTranslationsKeys, CustomTranslationsObject } from './config.js'
export const ComponentWithCustomI18n = () => {
const { i18n, t } = useTranslation<CustomTranslationsObject, CustomTranslationsKeys>()
const componentWithCustomI18nDefaultValidT = t('fields:addLink')
// @ts-expect-error // Keep the ts-expect-error comment. This NEEDS to throw an error
const componentWithCustomI18nDefaultInvalidT = t('fields:addLink2')
const componentWithCustomI18nDefaultValidI18nT = i18n.t('fields:addLink')
// @ts-expect-error // Keep the ts-expect-error comment. This NEEDS to throw an error
const componentWithCustomI18nDefaultInvalidI18nT = i18n.t('fields:addLink2')
const componentWithCustomI18nCustomValidT = t('general:test')
const componentWithCustomI18nCustomValidI18nT = i18n.t('general:test')
return (
<div className="componentWithCustomI18n">
<p>ComponentWithCustomI18n Default :</p>
ComponentWithCustomI18n Default Valid t:{' '}
<span className="componentWithCustomI18nDefaultValidT">
{componentWithCustomI18nDefaultValidT}
</span>
<br />
ComponentWithCustomI18n Default Valid i18n.t:{' '}
<span className="componentWithCustomI18nDefaultValidI18nT">
{componentWithCustomI18nDefaultValidI18nT}
</span>
<br />
ComponentWithCustomI18n Default Invalid t:{' '}
<span className="componentWithCustomI18nDefaultInvalidT">
{componentWithCustomI18nDefaultInvalidT}
</span>
<br />
ComponentWithCustomI18n Default Invalid i18n.t:
<span className="componentWithCustomI18nDefaultInvalidI18nT">
{' '}
{componentWithCustomI18nDefaultInvalidI18nT}
</span>
<br />
<br />
<p>ComponentWithCustomI18n Custom:</p>
<br />
ComponentWithCustomI18n Custom Valid t:{' '}
<span className="componentWithCustomI18nCustomValidT">
{componentWithCustomI18nCustomValidT}
</span>
<br />
ComponentWithCustomI18n Custom Valid i18n.t:{' '}
<span className="componentWithCustomI18nCustomValidI18nT">
{componentWithCustomI18nCustomValidI18nT}
</span>
</div>
)
}

View File

@@ -0,0 +1,36 @@
'use client'
import { useTranslation } from '@payloadcms/ui/providers/Translation'
import React from 'react'
export const ComponentWithDefaultI18n = () => {
const { i18n, t } = useTranslation()
const componentWithDefaultI18nValidT = t('fields:addLink')
// @ts-expect-error // Keep the ts-expect-error comment. This NEEDS to throw an error
const componentWithDefaultI18nInvalidT = t('fields:addLink2')
const componentWithDefaultI18nValidI18nT = i18n.t('fields:addLink')
// @ts-expect-error // Keep the ts-expect-error comment. This NEEDS to throw an error
const componentWithDefaultI18nInvalidI18nT = i18n.t('fields:addLink2')
return (
<div className="componentWithDefaultI18n">
<p>ComponentWithDefaultI18n </p>
ComponentWithDefaultI18n Valid t:{' '}
<span className="componentWithDefaultI18nValidT">{componentWithDefaultI18nValidT}</span>
<br />
ComponentWithDefaultI18n Valid i18n.t:{' '}
<span className="componentWithDefaultI18nValidI18nT">
{componentWithDefaultI18nValidI18nT}
</span>
<br />
ComponentWithDefaultI18n Invalid t:{' '}
<span className="componentWithDefaultI18nInvalidT">{componentWithDefaultI18nInvalidT}</span>
<br />
ComponentWithDefaultI18n Invalid i18n.t:{' '}
<span className="componentWithDefaultI18nInvalidI18nT">
{componentWithDefaultI18nInvalidI18nT}
</span>
</div>
)
}

84
test/i18n/config.ts Normal file
View File

@@ -0,0 +1,84 @@
/**
* This test suites primarily tests the i18n types (in this config.ts, the ComponentWithCustomI18n.tsx and ComponentWithDefaultI18n.tsx) and the i18n functionality in the admin UI.
*
* Thus, please do not remove any ts-expect-error comments in this test suite. This test suite will eventually have to be compiled to ensure there are no type errors.
*/
import type {
DefaultTranslationKeys,
NestedKeysStripped,
TFunction,
} from '@payloadcms/translations'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.js'
import { ComponentWithCustomI18n } from './ComponentWithCustomI18n.js'
import { ComponentWithDefaultI18n } from './ComponentWithDefaultI18n.js'
const customTranslationsObject = {
en: {
general: {
test: 'My custom translation',
},
},
}
export type CustomTranslationsObject = typeof customTranslationsObject.en
export type CustomTranslationsKeys = NestedKeysStripped<CustomTranslationsObject>
export default buildConfigWithDefaults({
collections: [
{
slug: 'collection1',
fields: [
{
name: 'fieldDefaultI18nValid',
type: 'text',
label: ({ t }) => t('fields:addLabel'),
},
{
name: 'fieldDefaultI18nInvalid',
type: 'text',
// @ts-expect-error // Keep the ts-expect-error comment. This NEEDS to throw an error
label: ({ t }) => t('fields:addLabel2'),
},
{
name: 'fieldCustomI18nValidDefault',
type: 'text',
label: ({ t }: { t: TFunction<CustomTranslationsKeys | DefaultTranslationKeys> }) =>
t('fields:addLabel'),
},
{
name: 'fieldCustomI18nValidCustom',
type: 'text',
label: ({ t }: { t: TFunction<CustomTranslationsKeys | DefaultTranslationKeys> }) =>
t('general:test'),
},
{
name: 'fieldCustomI18nInvalid',
type: 'text',
label: ({ t }: { t: TFunction<CustomTranslationsKeys | DefaultTranslationKeys> }) =>
// @ts-expect-error // Keep the ts-expect-error comment. This NEEDS to throw an error
t('fields:addLabel2'),
},
],
},
],
admin: {
components: {
afterDashboard: [ComponentWithDefaultI18n, ComponentWithCustomI18n],
},
},
i18n: {
translations: customTranslationsObject,
},
onInit: async (payload) => {
await payload.create({
collection: 'users',
data: {
email: devUser.email,
password: devUser.password,
},
})
},
})

89
test/i18n/e2e.spec.ts Normal file
View File

@@ -0,0 +1,89 @@
import type { Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import type { Config } from './payload-types.js'
const { beforeAll, beforeEach, describe } = test
import path from 'path'
import { fileURLToPath } from 'url'
import { ensureAutoLoginAndCompilationIsDone, initPageConsoleErrorCatch } from '../helpers.js'
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
import { reInitializeDB } from '../helpers/reInitializeDB.js'
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
describe('i18n', () => {
let page: Page
let serverURL: string
beforeAll(async ({ browser }, testInfo) => {
const prebuild = Boolean(process.env.CI)
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
prebuild,
}))
const context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
await reInitializeDB({
serverURL,
snapshotKey: 'i18nTests',
})
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
})
beforeEach(async () => {
await reInitializeDB({
serverURL,
snapshotKey: 'i18nTests',
})
await ensureAutoLoginAndCompilationIsDone({ page, serverURL })
})
test('ensure i18n labels and useTranslation hooks display correct translation', async () => {
await page.goto(serverURL + '/admin')
await page.waitForURL(serverURL + '/admin')
await expect(
page.locator('.componentWithDefaultI18n .componentWithDefaultI18nValidT'),
).toHaveText('Add Link')
await expect(
page.locator('.componentWithDefaultI18n .componentWithDefaultI18nValidI18nT'),
).toHaveText('Add Link')
await expect(
page.locator('.componentWithDefaultI18n .componentWithDefaultI18nInvalidT'),
).toHaveText('fields:addLink2')
await expect(
page.locator('.componentWithDefaultI18n .componentWithDefaultI18nInvalidI18nT'),
).toHaveText('fields:addLink2')
await expect(
page.locator('.componentWithCustomI18n .componentWithCustomI18nDefaultValidT'),
).toHaveText('Add Link')
await expect(
page.locator('.componentWithCustomI18n .componentWithCustomI18nDefaultValidI18nT'),
).toHaveText('Add Link')
await expect(
page.locator('.componentWithCustomI18n .componentWithCustomI18nDefaultInvalidT'),
).toHaveText('fields:addLink2')
await expect(
page.locator('.componentWithCustomI18n .componentWithCustomI18nDefaultInvalidI18nT'),
).toHaveText('fields:addLink2')
await expect(
page.locator('.componentWithCustomI18n .componentWithCustomI18nCustomValidT'),
).toHaveText('My custom translation')
await expect(
page.locator('.componentWithCustomI18n .componentWithCustomI18nCustomValidI18nT'),
).toHaveText('My custom translation')
})
})

View File

@@ -0,0 +1,92 @@
/* tslint:disable */
/* eslint-disable */
/**
* This file was automatically generated by Payload.
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
* and re-run `payload generate:types` to regenerate this file.
*/
export interface Config {
collections: {
collection1: Collection1;
users: User;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
globals: {};
locale: null;
user: User & {
collection: 'users';
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "collection1".
*/
export interface Collection1 {
id: string;
fieldDefaultI18nValid?: string | null;
fieldDefaultI18nInvalid?: string | null;
fieldCustomI18nValidDefault?: string | null;
fieldCustomI18nValidCustom?: string | null;
fieldCustomI18nInvalid?: string | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
*/
export interface User {
id: string;
updatedAt: string;
createdAt: string;
email: string;
resetPasswordToken?: string | null;
resetPasswordExpiration?: string | null;
salt?: string | null;
hash?: string | null;
loginAttempts?: number | null;
lockUntil?: string | null;
password?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: string;
user: {
relationTo: 'users';
value: string | User;
};
key?: string | null;
value?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: string;
name?: string | null;
batch?: number | null;
updatedAt: string;
createdAt: string;
}
declare module 'payload' {
// @ts-ignore
export interface GeneratedTypes extends Config {}
}

3
test/i18n/tsconfig.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "../tsconfig.json",
}