chore: corrects dateFNS keys, stricter types

This commit is contained in:
Jarrod Flesch
2024-04-09 11:38:38 -04:00
parent 817d57bd12
commit 35f59a47cc
14 changed files with 354 additions and 100 deletions

View File

@@ -1,5 +1,7 @@
import type { AcceptedLanguages } from '@payloadcms/translations'
import type { SanitizedConfig } from 'payload/types' import type { SanitizedConfig } from 'payload/types'
import { rtlLanguages } from '@payloadcms/translations'
import { initI18n } from '@payloadcms/translations' import { initI18n } from '@payloadcms/translations'
import { RootProvider } from '@payloadcms/ui/providers/Root' import { RootProvider } from '@payloadcms/ui/providers/Root'
import '@payloadcms/ui/scss/app.scss' import '@payloadcms/ui/scss/app.scss'
@@ -19,8 +21,6 @@ export const metadata = {
title: 'Next.js', title: 'Next.js',
} }
const rtlLanguages = ['ar', 'fa', 'ha', 'ku', 'ur', 'ps', 'dv', 'ks', 'khw', 'he', 'yi']
export const RootLayout = async ({ export const RootLayout = async ({
children, children,
config: configPromise, config: configPromise,
@@ -33,17 +33,18 @@ export const RootLayout = async ({
const headers = getHeaders() const headers = getHeaders()
const cookies = parseCookies(headers) const cookies = parseCookies(headers)
const languageCode = const languageCode = getRequestLanguage({
getRequestLanguage({ config,
config, cookies,
cookies, headers,
headers, })
}) ?? config.i18n.fallbackLanguage
const i18n = await initI18n({ config: config.i18n, context: 'client', language: languageCode }) const i18n = await initI18n({ config: config.i18n, context: 'client', language: languageCode })
const clientConfig = await createClientConfig({ config, t: i18n.t }) const clientConfig = await createClientConfig({ config, t: i18n.t })
const dir = rtlLanguages.includes(languageCode) ? 'RTL' : 'LTR' const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)
? 'RTL'
: 'LTR'
const languageOptions = Object.entries(config.i18n.supportedLanguages || {}).reduce( const languageOptions = Object.entries(config.i18n.supportedLanguages || {}).reduce(
(acc, [language, languageConfig]) => { (acc, [language, languageConfig]) => {

View File

@@ -1,4 +1,4 @@
import type { I18n } from '@payloadcms/translations' import type { AcceptedLanguages, I18n } from '@payloadcms/translations'
import type { SanitizedConfig } from 'payload/types' import type { SanitizedConfig } from 'payload/types'
import { initI18n } from '@payloadcms/translations' import { initI18n } from '@payloadcms/translations'
@@ -11,7 +11,7 @@ export const getNextI18n = async ({
language, language,
}: { }: {
config: SanitizedConfig config: SanitizedConfig
language?: string language?: AcceptedLanguages
}): Promise<I18n> => }): Promise<I18n> =>
initI18n({ initI18n({
config: config.i18n, config: config.i18n,

View File

@@ -1,3 +1,4 @@
import type { AcceptedLanguages } from '@payloadcms/translations'
import type { ReadonlyRequestCookies } from 'next/dist/server/web/spec-extension/adapters/request-cookies.js' import type { ReadonlyRequestCookies } from 'next/dist/server/web/spec-extension/adapters/request-cookies.js'
import type { SanitizedConfig } from 'payload/config' import type { SanitizedConfig } from 'payload/config'
@@ -15,13 +16,14 @@ export const getRequestLanguage = ({
cookies, cookies,
defaultLanguage = 'en', defaultLanguage = 'en',
headers, headers,
}: GetRequestLanguageArgs): string => { }: GetRequestLanguageArgs): AcceptedLanguages => {
const acceptLanguage = headers.get('Accept-Language') const acceptLanguage = headers.get('Accept-Language')
const cookieLanguage = cookies.get(`${config.cookiePrefix || 'payload'}-lng`) const cookieLanguage = cookies.get(`${config.cookiePrefix || 'payload'}-lng`)
const reqLanguage = const reqLanguage =
(typeof cookieLanguage === 'string' ? cookieLanguage : cookieLanguage?.value) || (typeof cookieLanguage === 'string' ? cookieLanguage : cookieLanguage?.value) ||
acceptLanguage || acceptLanguage ||
config.i18n.fallbackLanguage ||
defaultLanguage defaultLanguage
return matchLanguage(reqLanguage) return matchLanguage(reqLanguage)

View File

@@ -43,3 +43,121 @@ These are the translations for Payload. Translations are used on both the server
// or // or
pnpm build pnpm build
``` ```
Here is a full list of language keys. Note that these are not all implemented, but if you would like to contribute and add a new language, you can use this list as a reference:
| Language Code | Language Name |
| -------------- | ------------------------------------------ |
| af | Afrikaans |
| am | Amharic |
| ar-sa | Arabic (Saudi Arabia) |
| as | Assamese |
| az-Latn | Azerbaijani (Latin) |
| be | Belarusian |
| bg | Bulgarian |
| bn-BD | Bangla (Bangladesh) |
| bn-IN | Bangla (India) |
| bs | Bosnian (Latin) |
| ca | Catalan Spanish |
| ca-ES-valencia | Valencian |
| cs | Czech |
| cy | Welsh |
| da | Danish |
| de | German (Germany) |
| el | Greek |
| en-GB | English (United Kingdom) |
| en-US | English (United States) |
| es | Spanish (Spain) |
| es-ES | Spanish (Spain) |
| es-US | Spanish (United States) |
| es-MX | Spanish (Mexico) |
| et | Estonian |
| eu | Basque |
| fa | Persian |
| fi | Finnish |
| fil-Latn | Filipino |
| fr | French (France) |
| fr-FR | French (France) |
| fr-CA | French (Canada) |
| ga | Irish |
| gd-Latn | Scottish Gaelic |
| gl | Galician |
| gu | Gujarati |
| ha-Latn | Hausa (Latin) |
| he | Hebrew |
| hi | Hindi |
| hr | Croatian |
| hu | Hungarian |
| hy | Armenian |
| id | Indonesian |
| ig-Latn | Igbo |
| is | Icelandic |
| it | Italian (Italy) |
| it-it | Italian (Italy) |
| ja | Japanese |
| ka | Georgian |
| kk | Kazakh |
| km | Khmer |
| kn | Kannada |
| ko | Korean |
| kok | Konkani |
| ku-Arab | Central Kurdish |
| ky-Cyrl | Kyrgyz |
| lb | Luxembourgish |
| lt | Lithuanian |
| lv | Latvian |
| mi-Latn | Maori |
| mk | Macedonian |
| ml | Malayalam |
| mn-Cyrl | Mongolian (Cyrillic) |
| mr | Marathi |
| ms | Malay (Malaysia) |
| mt | Maltese |
| nb | Norwegian (Bokmål) |
| ne | Nepali (Nepal) |
| nl | Dutch (Netherlands) |
| nl-BE | Dutch (Netherlands) |
| nn | Norwegian (Nynorsk) |
| nso | Sesotho sa Leboa |
| or | Odia |
| pa | Punjabi (Gurmukhi) |
| pa-Arab | Punjabi (Arabic) |
| pl | Polish |
| prs-Arab | Dari |
| pt-BR | Portuguese (Brazil) |
| pt-PT | Portuguese (Portugal) |
| qut-Latn | Kiche |
| quz | Quechua (Peru) |
| ro | Romanian (Romania) |
| ru | Russian |
| rw | Kinyarwanda |
| sd-Arab | Sindhi (Arabic) |
| si | Sinhala |
| sk | Slovak |
| sl | Slovenian |
| sq | Albanian |
| sr-Cyrl-BA | Serbian (Cyrillic, Bosnia and Herzegovina) |
| sr-Cyrl-RS | Serbian (Cyrillic, Serbia) |
| sr-Latn-RS | Serbian (Latin, Serbia) |
| sv | Swedish (Sweden) |
| sw | Kiswahili |
| ta | Tamil |
| te | Telugu |
| tg-Cyrl | Tajik (Cyrillic) |
| th | Thai |
| ti | Tigrinya |
| tk-Latn | Turkmen (Latin) |
| tn | Setswana |
| tr | Turkish |
| tt-Cyrl | Tatar (Cyrillic) |
| ug-Arab | Uyghur |
| uk | Ukrainian |
| ur | Urdu |
| uz-Latn | Uzbek (Latin) |
| vi | Vietnamese |
| wo | Wolof |
| xh | isiXhosa |
| yo-Latn | Yoruba |
| zh-Hans | Chinese (Simplified) |
| zh-Hant | Chinese (Traditional) |
| zu | isiZulu |

View File

@@ -61,5 +61,5 @@ export const translations = {
ua, ua,
vi, vi,
zh, zh,
'zh-tw': zhTw, 'zh-TW': zhTw,
} as SupportedLanguages } as SupportedLanguages

View File

@@ -1,7 +1,7 @@
import type { Language } from '../types.js' import type { Language } from '../types.js'
export const fa: Language = { export const fa: Language = {
dateFNSKey: 'fa', dateFNSKey: 'fa-IR',
translations: { translations: {
authentication: { authentication: {
account: 'نمایه', account: 'نمایه',

View File

@@ -1,7 +1,7 @@
import type { Language } from '../types.js' import type { Language } from '../types.js'
export const rs: Language = { export const rs: Language = {
dateFNSKey: 'rs', dateFNSKey: 'en-US',
translations: { translations: {
authentication: { authentication: {
account: 'Налог', account: 'Налог',

View File

@@ -1,7 +1,7 @@
import type { Language } from '../types.js' import type { Language } from '../types.js'
export const rsLatin: Language = { export const rsLatin: Language = {
dateFNSKey: 'rs-latin', dateFNSKey: 'en-US',
translations: { translations: {
authentication: { authentication: {
account: 'Nalog', account: 'Nalog',

View File

@@ -1,7 +1,7 @@
import type { Language } from '../types.js' import type { Language } from '../types.js'
export const zh: Language = { export const zh: Language = {
dateFNSKey: 'zh', dateFNSKey: 'zh-CN',
translations: { translations: {
authentication: { authentication: {
account: '帐户', account: '帐户',

View File

@@ -1,9 +1,37 @@
import type { Locale } from 'date-fns' import type { Locale } from 'date-fns'
import type { acceptedLanguages } from './utilities/init.js' import type { acceptedLanguages } from './utilities/languages.js'
type DateFNSKeys =
| 'ar'
| 'az'
| 'bg'
| 'cs'
| 'de'
| 'en-US'
| 'es'
| 'fa-IR'
| 'fr'
| 'hr'
| 'hu'
| 'it'
| 'ja'
| 'ko'
| 'nb'
| 'nl'
| 'pl'
| 'pt'
| 'ro'
| 'ru'
| 'sv'
| 'th'
| 'tr'
| 'vi'
| 'zh-CN'
| 'zh-TW'
export type Language = { export type Language = {
dateFNSKey: string dateFNSKey: DateFNSKeys
translations: { translations: {
[namespace: string]: { [namespace: string]: {
[key: string]: string [key: string]: string
@@ -22,7 +50,7 @@ export type TFunction = (key: string, options?: Record<string, any>) => string
export type I18n = { export type I18n = {
dateFNS: Locale dateFNS: Locale
/** Corresponding dateFNS key */ /** Corresponding dateFNS key */
dateFNSKey: string dateFNSKey: DateFNSKeys
/** The fallback language */ /** The fallback language */
fallbackLanguage: string fallbackLanguage: string
/** The language of the request */ /** The language of the request */
@@ -52,5 +80,10 @@ export type InitTFunction = (args: {
export type InitI18n = (args: { export type InitI18n = (args: {
config: I18nOptions config: I18nOptions
context: 'api' | 'client' context: 'api' | 'client'
language?: string language?: AcceptedLanguages
}) => Promise<I18n> }) => Promise<I18n>
export type LanguagePreference = {
language: AcceptedLanguages
quality?: number
}

View File

@@ -54,10 +54,10 @@ function sortObject(obj) {
return sortedObject return sortedObject
} }
export const getTranslationsByContext = (translations: Language, context: 'api' | 'client') => { export const getTranslationsByContext = (selectedLanguage: Language, context: 'api' | 'client') => {
if (context === 'client') { if (context === 'client') {
return sortObject(filterKeys(translations, '', clientTranslationKeys)) return sortObject(filterKeys(selectedLanguage.translations, '', clientTranslationKeys))
} else { } else {
return translations return selectedLanguage.translations
} }
} }

View File

@@ -130,74 +130,9 @@ export const t: TFunctionConstructor = ({ key, translations, vars }) => {
return translationString return translationString
} }
type LanguagePreference = {
language: string
quality?: number
}
function parseAcceptLanguage(header: string): LanguagePreference[] {
return header
.split(',')
.map((lang) => {
const [language, quality] = lang.trim().split(';q=')
return {
language,
quality: quality ? parseFloat(quality) : 1,
}
})
.sort((a, b) => b.quality - a.quality) // Sort by quality, highest to lowest
}
export const acceptedLanguages = [
'ar',
'az',
'bg',
'cs',
'de',
'en',
'es',
'fa',
'fr',
'hr',
'hu',
'it',
'ja',
'ko',
'my',
'nb',
'nl',
'pl',
'pt',
'ro',
'rs',
'rsLatin',
'ru',
'sv',
'th',
'tr',
'ua',
'vi',
'zh',
'zhTw',
] as const
export function matchLanguage(header: string): string | undefined {
const parsedHeader = parseAcceptLanguage(header)
for (const { language } of parsedHeader) {
for (const acceptedLanguage of acceptedLanguages) {
if (language.startsWith(acceptedLanguage)) {
return acceptedLanguage
}
}
}
return undefined
}
const initTFunction: InitTFunction = (args) => { const initTFunction: InitTFunction = (args) => {
const { config, language, translations } = args const { config, language, translations } = args
const mergedTranslations = deepMerge(config?.translations?.[language] ?? {}, translations) const mergedTranslations = deepMerge(translations, config?.translations?.[language] ?? {})
return { return {
t: (key, vars) => { t: (key, vars) => {
@@ -230,10 +165,7 @@ function memoize(fn: (args: unknown) => Promise<I18n>, keys: string[]) {
export const initI18n: InitI18n = memoize( export const initI18n: InitI18n = memoize(
async ({ config, context, language = 'en' }: Parameters<InitI18n>[0]) => { async ({ config, context, language = 'en' }: Parameters<InitI18n>[0]) => {
const translations = getTranslationsByContext( const translations = getTranslationsByContext(config.supportedLanguages[language], context)
config.supportedLanguages[language].translations,
context,
)
const { t, translations: mergedTranslations } = initTFunction({ const { t, translations: mergedTranslations } = initTFunction({
config, config,

View File

@@ -0,0 +1,165 @@
import type { AcceptedLanguages, LanguagePreference } from '../types.js'
export const rtlLanguages = ['ar', 'fa'] as const
export const acceptedLanguages = [
'ar',
'az',
'bg',
'cs',
'de',
'en',
'es',
'fa',
'fr',
'hr',
'hu',
'it',
'ja',
'ko',
'my',
'nb',
'nl',
'pl',
'pt',
'ro',
'rs',
'rsLatin',
'ru',
'sv',
'th',
'tr',
'ua',
'vi',
'zh',
'zh-TW',
/**
* Languages not implemented:
*
* 'af',
* 'am',
* 'ar-sa',
* 'as',
* 'az-latin',
* 'be',
* 'bn-BD',
* 'bn-IN',
* 'bs',
* 'ca',
* 'ca-ES-valencia',
* 'cy',
* 'da',
* 'el',
* 'en-GB',
* 'en-US',
* 'es-ES',
* 'es-US',
* 'es-MX',
* 'et',
* 'eu',
* 'fi',
* 'fil-Latn',
* 'fr-FR',
* 'fr-CA',
* 'ga',
* 'gd-Latn',
* 'gl',
* 'gu',
* 'ha-Latn',
* 'he',
* 'hi',
* 'hr',
* 'hy',
* 'id',
* 'ig-Latn',
* 'is',
* 'it-it',
* 'ka',
* 'kk',
* 'km',
* 'kn',
* 'kok',
* 'ku-Arab',
* 'ky-Cyrl',
* 'lb',
* 'lt',
* 'lv',
* 'mi-Latn',
* 'mk',
* 'ml',
* 'mn-Cyrl',
* 'mr',
* 'ms',
* 'mt',
* 'ne',
* 'nl-BE',
* 'nn',
* 'nso',
* 'or',
* 'pa',
* 'pa-Arab',
* 'prs-Arab',
* 'pt-BR',
* 'pt-PT',
* 'qut-Latn',
* 'quz',
* 'rw',
* 'sd-Arab',
* 'si',
* 'sk',
* 'sl',
* 'sq',
* 'sr-Cyrl-BA',
* 'sr-Cyrl-RS',
* 'sr-Latn-RS',
* 'sw',
* 'ta',
* 'te',
* 'tg-Cyrl',
* 'ti',
* 'tk-Latn',
* 'tn',
* 'tt-Cyrl',
* 'ug-Arab',
* 'uk',
* 'ur',
* 'uz-Latn',
* 'wo',
* 'xh',
* 'yo-Latn',
* 'zh-Hans',
* 'zh-Hant',
* 'zu',
*/
] as const
function parseAcceptLanguage(acceptLanguageHeader: string): LanguagePreference[] {
return acceptLanguageHeader
.split(',')
.map((lang) => {
const [language, quality] = lang.trim().split(';q=') as [
AcceptedLanguages,
string | undefined,
]
return {
language,
quality: quality ? parseFloat(quality) : 1,
}
})
.sort((a, b) => b.quality - a.quality) // Sort by quality, highest to lowest
}
export function matchLanguage(acceptLanguageHeader: string): AcceptedLanguages | undefined {
const parsedHeader = parseAcceptLanguage(acceptLanguageHeader)
let matchedLanguage: AcceptedLanguages
for (const { language } of parsedHeader) {
if (!matchedLanguage && acceptedLanguages.includes(language)) {
matchedLanguage = language
}
}
return matchedLanguage
}

View File

@@ -27,6 +27,7 @@ import {
} from '@payloadcms/richtext-lexical' } from '@payloadcms/richtext-lexical'
import { de } from '@payloadcms/translations/languages/de' import { de } from '@payloadcms/translations/languages/de'
import { en } from '@payloadcms/translations/languages/en' import { en } from '@payloadcms/translations/languages/en'
import { es } from '@payloadcms/translations/languages/es'
// import { slateEditor } from '@payloadcms/richtext-slate' // import { slateEditor } from '@payloadcms/richtext-slate'
import { type Config, buildConfig } from 'payload/config' import { type Config, buildConfig } from 'payload/config'
import sharp from 'sharp' import sharp from 'sharp'
@@ -173,16 +174,18 @@ export async function buildConfigWithDefaults(
}), }),
sharp, sharp,
telemetry: false, telemetry: false,
i18n: {
supportedLanguages: {
en,
de,
},
},
typescript: { typescript: {
declare: false, declare: false,
}, },
...testConfig, ...testConfig,
i18n: {
supportedLanguages: {
en,
es,
de,
},
...(testConfig?.i18n || {}),
},
} }
config.admin = { config.admin = {