From 2782b26dc23f68628e5887556661c19a164db987 Mon Sep 17 00:00:00 2001 From: giancarlo Date: Thu, 4 Apr 2024 09:22:43 +0800 Subject: [PATCH] Refactor i18n handling for language cookie and headers The commit encompasses the aspect of refactoring the i18n handling for language cookies and headers. It also includes the deletion of get-language-cookie file and its transformation into a function inside i18n.server file. Extra functionalities were added to the i18n.server like enhancing the i18n server instance creation to consider the 'accept-language' header and default to environment provided values when necessary. The changes were also adjusted accordingly on the packages/i18n/package.json where deletion of "./cookie" was realized. --- apps/web/lib/i18n/i18n.server.ts | 25 ++++++++++--- packages/i18n/package.json | 1 - packages/i18n/src/get-language-cookie.ts | 9 ----- packages/i18n/src/i18n.server.ts | 46 +++++++++++++++++++++++- packages/i18n/src/i18n.settings.ts | 2 +- 5 files changed, 67 insertions(+), 16 deletions(-) delete mode 100644 packages/i18n/src/get-language-cookie.ts diff --git a/apps/web/lib/i18n/i18n.server.ts b/apps/web/lib/i18n/i18n.server.ts index ad34fbd4d..74ad6146b 100644 --- a/apps/web/lib/i18n/i18n.server.ts +++ b/apps/web/lib/i18n/i18n.server.ts @@ -1,10 +1,27 @@ -import getLanguageCookie from '@kit/i18n/cookie'; -import { initializeServerI18n } from '@kit/i18n/server'; +import { cookies, headers } from 'next/headers'; + +import { + getLanguageCookie, + initializeServerI18n, + parseAcceptLanguageHeader, +} from '@kit/i18n/server'; import { i18nResolver } from './i18n.resolver'; +/** + * @name createI18nServerInstance + * @description Creates an instance of the i18n server. + * It uses the language from the cookie if it exists, otherwise it uses the language from the accept-language header. + * If neither is available, it will default to the provided environment variable. + * + * Initialize the i18n instance for every RSC server request (eg. each page/layout) + */ export function createI18nServerInstance() { - const cookie = getLanguageCookie(); + const acceptLanguage = headers().get('accept-language'); + const cookie = getLanguageCookie(cookies()); - return initializeServerI18n(cookie, i18nResolver); + const language = + cookie ?? parseAcceptLanguageHeader(acceptLanguage)[0] ?? undefined; + + return initializeServerI18n(language, i18nResolver); } diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 219a615d8..e19af3694 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -12,7 +12,6 @@ "exports": { "./server": "./src/i18n.server.ts", "./client": "./src/i18n.client.ts", - "./cookie": "./src/get-language-cookie.ts", "./provider": "./src/i18n-provider.tsx" }, "devDependencies": { diff --git a/packages/i18n/src/get-language-cookie.ts b/packages/i18n/src/get-language-cookie.ts deleted file mode 100644 index 06cd7ac2f..000000000 --- a/packages/i18n/src/get-language-cookie.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { cookies } from 'next/headers'; - -import { I18N_COOKIE_NAME } from './i18n.settings'; - -function getLanguageCookie() { - return cookies().get(I18N_COOKIE_NAME)?.value; -} - -export default getLanguageCookie; diff --git a/packages/i18n/src/i18n.server.ts b/packages/i18n/src/i18n.server.ts index 6815832a9..bd7779ffb 100644 --- a/packages/i18n/src/i18n.server.ts +++ b/packages/i18n/src/i18n.server.ts @@ -2,7 +2,15 @@ import { createInstance } from 'i18next'; import resourcesToBackend from 'i18next-resources-to-backend'; import { initReactI18next } from 'react-i18next/initReactI18next'; -import { getI18nSettings } from './i18n.settings'; +import { I18N_COOKIE_NAME, getI18nSettings, languages } from './i18n.settings'; + +export function getLanguageCookie< + Cookies extends { + get: (name: string) => { value: string } | undefined; + }, +>(cookies: Cookies) { + return cookies.get(I18N_COOKIE_NAME)?.value; +} export async function initializeServerI18n( lang: string | undefined, @@ -33,3 +41,39 @@ export async function initializeServerI18n( return i18nInstance; } + +export function parseAcceptLanguageHeader( + languageHeaderValue: string | null | undefined, + acceptedLanguages = languages, +): string[] { + // Return an empty array if the header value is not provided + if (!languageHeaderValue) return []; + + const ignoreWildcard = true; + + // Split the header value by comma and map each language to its quality value + return languageHeaderValue + .split(',') + .map((lang): [number, string] => { + const [locale, q = 'q=1'] = lang.split(';'); + + if (!locale) return [0, '']; + + const trimmedLocale = locale.trim(); + const numQ = Number(q.replace(/q ?=/, '')); + + return [isNaN(numQ) ? 0 : numQ, trimmedLocale]; + }) + .sort(([q1], [q2]) => q2 - q1) // Sort by quality value in descending order + .flatMap(([_, locale]) => { + // Ignore wildcard '*' if 'ignoreWildcard' is true + if (locale === '*' && ignoreWildcard) return []; + + // Return the locale if it's included in the accepted languages + try { + return acceptedLanguages.includes(locale) ? [locale] : []; + } catch { + return []; + } + }); +} diff --git a/packages/i18n/src/i18n.settings.ts b/packages/i18n/src/i18n.settings.ts index f02ee7539..744fa09a5 100644 --- a/packages/i18n/src/i18n.settings.ts +++ b/packages/i18n/src/i18n.settings.ts @@ -1,7 +1,7 @@ import { InitOptions } from 'i18next'; const fallbackLng = 'en'; -const languages: string[] = [fallbackLng]; +export const languages: string[] = [fallbackLng]; export const I18N_COOKIE_NAME = 'lang';