From 35f59a47cc51554d08903ce36eda0f63f59386e7 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Tue, 9 Apr 2024 11:38:38 -0400 Subject: [PATCH] chore: corrects dateFNS keys, stricter types --- packages/next/src/layouts/Root/index.tsx | 19 +- packages/next/src/utilities/getNextI18n.ts | 4 +- .../next/src/utilities/getRequestLanguage.ts | 4 +- packages/translations/README.md | 118 +++++++++++++ packages/translations/src/exports/all.ts | 2 +- packages/translations/src/languages/fa.ts | 2 +- packages/translations/src/languages/rs.ts | 2 +- .../translations/src/languages/rsLatin.ts | 2 +- packages/translations/src/languages/zh.ts | 2 +- packages/translations/src/types.ts | 41 ++++- .../src/utilities/getTranslationsByContext.ts | 6 +- packages/translations/src/utilities/init.ts | 72 +------- .../translations/src/utilities/languages.ts | 165 ++++++++++++++++++ test/buildConfigWithDefaults.ts | 15 +- 14 files changed, 354 insertions(+), 100 deletions(-) create mode 100644 packages/translations/src/utilities/languages.ts diff --git a/packages/next/src/layouts/Root/index.tsx b/packages/next/src/layouts/Root/index.tsx index 9565b78af..059920498 100644 --- a/packages/next/src/layouts/Root/index.tsx +++ b/packages/next/src/layouts/Root/index.tsx @@ -1,5 +1,7 @@ +import type { AcceptedLanguages } from '@payloadcms/translations' import type { SanitizedConfig } from 'payload/types' +import { rtlLanguages } from '@payloadcms/translations' import { initI18n } from '@payloadcms/translations' import { RootProvider } from '@payloadcms/ui/providers/Root' import '@payloadcms/ui/scss/app.scss' @@ -19,8 +21,6 @@ export const metadata = { title: 'Next.js', } -const rtlLanguages = ['ar', 'fa', 'ha', 'ku', 'ur', 'ps', 'dv', 'ks', 'khw', 'he', 'yi'] - export const RootLayout = async ({ children, config: configPromise, @@ -33,17 +33,18 @@ export const RootLayout = async ({ const headers = getHeaders() const cookies = parseCookies(headers) - const languageCode = - getRequestLanguage({ - config, - cookies, - headers, - }) ?? config.i18n.fallbackLanguage + const languageCode = getRequestLanguage({ + config, + cookies, + headers, + }) const i18n = await initI18n({ config: config.i18n, context: 'client', language: languageCode }) 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( (acc, [language, languageConfig]) => { diff --git a/packages/next/src/utilities/getNextI18n.ts b/packages/next/src/utilities/getNextI18n.ts index 82c488c64..e93b50fa2 100644 --- a/packages/next/src/utilities/getNextI18n.ts +++ b/packages/next/src/utilities/getNextI18n.ts @@ -1,4 +1,4 @@ -import type { I18n } from '@payloadcms/translations' +import type { AcceptedLanguages, I18n } from '@payloadcms/translations' import type { SanitizedConfig } from 'payload/types' import { initI18n } from '@payloadcms/translations' @@ -11,7 +11,7 @@ export const getNextI18n = async ({ language, }: { config: SanitizedConfig - language?: string + language?: AcceptedLanguages }): Promise => initI18n({ config: config.i18n, diff --git a/packages/next/src/utilities/getRequestLanguage.ts b/packages/next/src/utilities/getRequestLanguage.ts index 6c6c77c70..6ca0b8b1a 100644 --- a/packages/next/src/utilities/getRequestLanguage.ts +++ b/packages/next/src/utilities/getRequestLanguage.ts @@ -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 { SanitizedConfig } from 'payload/config' @@ -15,13 +16,14 @@ export const getRequestLanguage = ({ cookies, defaultLanguage = 'en', headers, -}: GetRequestLanguageArgs): string => { +}: GetRequestLanguageArgs): AcceptedLanguages => { const acceptLanguage = headers.get('Accept-Language') const cookieLanguage = cookies.get(`${config.cookiePrefix || 'payload'}-lng`) const reqLanguage = (typeof cookieLanguage === 'string' ? cookieLanguage : cookieLanguage?.value) || acceptLanguage || + config.i18n.fallbackLanguage || defaultLanguage return matchLanguage(reqLanguage) diff --git a/packages/translations/README.md b/packages/translations/README.md index 5f069af5f..22c328f79 100644 --- a/packages/translations/README.md +++ b/packages/translations/README.md @@ -43,3 +43,121 @@ These are the translations for Payload. Translations are used on both the server // or 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 | K’iche’ | +| 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 | diff --git a/packages/translations/src/exports/all.ts b/packages/translations/src/exports/all.ts index fc651f4ce..04919e7bd 100644 --- a/packages/translations/src/exports/all.ts +++ b/packages/translations/src/exports/all.ts @@ -61,5 +61,5 @@ export const translations = { ua, vi, zh, - 'zh-tw': zhTw, + 'zh-TW': zhTw, } as SupportedLanguages diff --git a/packages/translations/src/languages/fa.ts b/packages/translations/src/languages/fa.ts index 914b9ff46..e05e44d54 100644 --- a/packages/translations/src/languages/fa.ts +++ b/packages/translations/src/languages/fa.ts @@ -1,7 +1,7 @@ import type { Language } from '../types.js' export const fa: Language = { - dateFNSKey: 'fa', + dateFNSKey: 'fa-IR', translations: { authentication: { account: 'نمایه', diff --git a/packages/translations/src/languages/rs.ts b/packages/translations/src/languages/rs.ts index 81017cb28..4a0cbc891 100644 --- a/packages/translations/src/languages/rs.ts +++ b/packages/translations/src/languages/rs.ts @@ -1,7 +1,7 @@ import type { Language } from '../types.js' export const rs: Language = { - dateFNSKey: 'rs', + dateFNSKey: 'en-US', translations: { authentication: { account: 'Налог', diff --git a/packages/translations/src/languages/rsLatin.ts b/packages/translations/src/languages/rsLatin.ts index b9433a9fd..145c65e8b 100644 --- a/packages/translations/src/languages/rsLatin.ts +++ b/packages/translations/src/languages/rsLatin.ts @@ -1,7 +1,7 @@ import type { Language } from '../types.js' export const rsLatin: Language = { - dateFNSKey: 'rs-latin', + dateFNSKey: 'en-US', translations: { authentication: { account: 'Nalog', diff --git a/packages/translations/src/languages/zh.ts b/packages/translations/src/languages/zh.ts index e55b8e300..4e4aba714 100644 --- a/packages/translations/src/languages/zh.ts +++ b/packages/translations/src/languages/zh.ts @@ -1,7 +1,7 @@ import type { Language } from '../types.js' export const zh: Language = { - dateFNSKey: 'zh', + dateFNSKey: 'zh-CN', translations: { authentication: { account: '帐户', diff --git a/packages/translations/src/types.ts b/packages/translations/src/types.ts index cac0ddbd7..937b252b4 100644 --- a/packages/translations/src/types.ts +++ b/packages/translations/src/types.ts @@ -1,9 +1,37 @@ 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 = { - dateFNSKey: string + dateFNSKey: DateFNSKeys translations: { [namespace: string]: { [key: string]: string @@ -22,7 +50,7 @@ export type TFunction = (key: string, options?: Record) => string export type I18n = { dateFNS: Locale /** Corresponding dateFNS key */ - dateFNSKey: string + dateFNSKey: DateFNSKeys /** The fallback language */ fallbackLanguage: string /** The language of the request */ @@ -52,5 +80,10 @@ export type InitTFunction = (args: { export type InitI18n = (args: { config: I18nOptions context: 'api' | 'client' - language?: string + language?: AcceptedLanguages }) => Promise + +export type LanguagePreference = { + language: AcceptedLanguages + quality?: number +} diff --git a/packages/translations/src/utilities/getTranslationsByContext.ts b/packages/translations/src/utilities/getTranslationsByContext.ts index a8126d181..952bb971d 100644 --- a/packages/translations/src/utilities/getTranslationsByContext.ts +++ b/packages/translations/src/utilities/getTranslationsByContext.ts @@ -54,10 +54,10 @@ function sortObject(obj) { return sortedObject } -export const getTranslationsByContext = (translations: Language, context: 'api' | 'client') => { +export const getTranslationsByContext = (selectedLanguage: Language, context: 'api' | 'client') => { if (context === 'client') { - return sortObject(filterKeys(translations, '', clientTranslationKeys)) + return sortObject(filterKeys(selectedLanguage.translations, '', clientTranslationKeys)) } else { - return translations + return selectedLanguage.translations } } diff --git a/packages/translations/src/utilities/init.ts b/packages/translations/src/utilities/init.ts index 7101203db..5d84dbd21 100644 --- a/packages/translations/src/utilities/init.ts +++ b/packages/translations/src/utilities/init.ts @@ -130,74 +130,9 @@ export const t: TFunctionConstructor = ({ key, translations, vars }) => { 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 { config, language, translations } = args - const mergedTranslations = deepMerge(config?.translations?.[language] ?? {}, translations) + const mergedTranslations = deepMerge(translations, config?.translations?.[language] ?? {}) return { t: (key, vars) => { @@ -230,10 +165,7 @@ function memoize(fn: (args: unknown) => Promise, keys: string[]) { export const initI18n: InitI18n = memoize( async ({ config, context, language = 'en' }: Parameters[0]) => { - const translations = getTranslationsByContext( - config.supportedLanguages[language].translations, - context, - ) + const translations = getTranslationsByContext(config.supportedLanguages[language], context) const { t, translations: mergedTranslations } = initTFunction({ config, diff --git a/packages/translations/src/utilities/languages.ts b/packages/translations/src/utilities/languages.ts new file mode 100644 index 000000000..4e0427400 --- /dev/null +++ b/packages/translations/src/utilities/languages.ts @@ -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 +} diff --git a/test/buildConfigWithDefaults.ts b/test/buildConfigWithDefaults.ts index faaf1ed88..c10b1d831 100644 --- a/test/buildConfigWithDefaults.ts +++ b/test/buildConfigWithDefaults.ts @@ -27,6 +27,7 @@ import { } from '@payloadcms/richtext-lexical' import { de } from '@payloadcms/translations/languages/de' import { en } from '@payloadcms/translations/languages/en' +import { es } from '@payloadcms/translations/languages/es' // import { slateEditor } from '@payloadcms/richtext-slate' import { type Config, buildConfig } from 'payload/config' import sharp from 'sharp' @@ -173,16 +174,18 @@ export async function buildConfigWithDefaults( }), sharp, telemetry: false, - i18n: { - supportedLanguages: { - en, - de, - }, - }, typescript: { declare: false, }, ...testConfig, + i18n: { + supportedLanguages: { + en, + es, + de, + }, + ...(testConfig?.i18n || {}), + }, } config.admin = {