diff --git a/package.json b/package.json index a7747b2..e76c610 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ }, "homepage": "https://github.com/aboutbits/internationalization#readme", "devDependencies": { + "@types/node": "^14.11.5", "@types/jest": "^26.0.9", "@typescript-eslint/eslint-plugin": "^3.8.0", "@typescript-eslint/parser": "^3.8.0", diff --git a/readme.md b/readme.md index c01f81a..80227a6 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,8 @@ Internationalization [![npm version](https://badge.fury.io/js/%40aboutbits%2Finternationalization.svg)](https://badge.fury.io/js/%40aboutbits%2Finternationalization) -This package includes utilities for working with different languages in the browser. +This package includes utilities for working with different languages in the browser. +This package works for client side rendered applications as well as for server side rendered applications. ## Table of content @@ -22,15 +23,26 @@ npm install @aboutbits/internationalization Second, you can setup the tool with all supported languages and the fallback language and then detect the given browser language. ```js -import Internationalization from '@aboutbits/internationalization' +import { Internationalization } from '@aboutbits/internationalization' let supportedLanguages = ['en', 'de', 'it'] let fallbackLanguage = 'en' let i18n = new Internationalization(supportedLanguages, fallbackLanguage) -let language = i18n.detectBrowserLanguage() +// Fetches the language only from the user's browser +let browserLanguage = i18n.detectBrowserLanguage() +// First check the cookies to see if a language is set. If not it will look in the browser. +// You can also pass a requiest obejct with which the language can be recognized already during the server side rendering +let language = i18n.detectLanguage() + +// Sets a cookie with the specified language. +// There exists also a static version of this method. +// Ex. Internationalization.setLanguage("de") +i18n.setLanguage("de") + +console.log(browserLanguage) console.log(language) ``` diff --git a/src/index.ts b/src/index.ts index e19895a..5d6a693 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,21 +1,143 @@ +import { IncomingMessage } from 'http' +import { + getCookieFromDocument, + canUseDOM, + setCookie, + getCookieFromRequest, +} from './utilities' + class Internationalization { - supportedLanguages: T[] + private readonly cookieName: string + + supportedLanguages: Array fallbackLanguage: T - constructor(supportedLanguages: T[], fallbackLanguage: T) { + /** + * ClientInternationalization utility package for working with languages + * + * @param supportedLanguages { Array } - List of all supported languages + * @param fallbackLanguage { string } - Fallback language if no language matches + * @param cookieName { string } - Optional: Set a preferred cookie name + */ + constructor( + supportedLanguages: Array, + fallbackLanguage: T, + cookieName?: string + ) { this.supportedLanguages = supportedLanguages this.fallbackLanguage = fallbackLanguage + + this.cookieName = typeof cookieName === 'string' ? cookieName : 'language' } + /** + * Detect the browser language of the client + * If no language was found or if none of the + * supported language matches a fallback language will be returned + * + * @return {string} - Detected language + */ detectBrowserLanguage(): T { - const browserLanguage = - (navigator.languages && navigator.languages[0]) || navigator.language + // Check if the DOM is presented. + // If the DOM isn't accessible the website may be rendered on a server + if (canUseDOM()) { + // Get browser language + const browserLanguage = + (navigator.languages && navigator.languages[0]) || navigator.language + + return ( + this.supportedLanguages.find((lang) => lang === browserLanguage) || + this.fallbackLanguage + ) + } + + // Return fallback language if no DOM was found + return this.fallbackLanguage + } + + /** + * Save a language to a cookie + * If the set language isn't in the list of supported languages + * the fallback language will be returned + * + * @param language { string } - Language to set + */ + setLanguage(language: string): void { + if (canUseDOM()) { + const cookieLanguage = + this.supportedLanguages.find((lang) => lang === language) || + this.fallbackLanguage + + setCookie(this.cookieName, cookieLanguage) + } + } + + /** + * Get the current language + * This method will first check if a language cookie is present. + * If no cookie was found the method will get the language form the browser + * + * For handling cookies on the server side pass the request object + * + * @param request { IncomingMessage } - Optional: Request object for accessing cookies over request headers + * @return { string } - Found language or fallback language + */ + detectLanguage(request?: IncomingMessage): T { + // Check if request is not undefined + if (request) { + // Try to get language form cookie + const cookieFromRequest = getCookieFromRequest(this.cookieName, request) + + if (cookieFromRequest) { + return cookieFromRequest as T + } + + // Get accept language from request header + const acceptLanguage = request.headers['accept-language'] + + if (acceptLanguage) { + // Extract the locale string and the priority from header string + const requestedLocales = acceptLanguage.split(',').map((part) => { + const [locale, priority] = part.trim().split(';q=') + + return { locale, priority: parseInt(priority) } + }) + + // Get the potential locale from requested locales array + const potentialLocale = requestedLocales + .sort((a, b) => b.priority - a.priority) + .find( + ({ locale }) => + locale !== '*' && + this.supportedLanguages.some((lang) => locale === lang) + ) + + // Check if the potential locale is not undefined else return the fallback language + return (potentialLocale + ? potentialLocale.locale + : this.fallbackLanguage) as T + } + } + + // Check for DOM + if (canUseDOM()) { + // Try to get language form cookie + const language = getCookieFromDocument(this.cookieName) + + // If cookie is undefined get the language form the browser + if (language == undefined) { + return this.detectBrowserLanguage() + } + + // Return found language or fallback language + return ( + this.supportedLanguages.find((lang) => lang === language) || + this.fallbackLanguage + ) + } - return ( - this.supportedLanguages.find((lang) => lang === browserLanguage) || - this.fallbackLanguage - ) + return this.fallbackLanguage } } -export { Internationalization as default } +export default Internationalization diff --git a/src/utilities.ts b/src/utilities.ts new file mode 100644 index 0000000..e2ab5ca --- /dev/null +++ b/src/utilities.ts @@ -0,0 +1,68 @@ +/** + * Regex based filter to get cookie values + * This includes the = + * + * @param name { string } - Filter keyword + */ +import { IncomingMessage } from 'http' + +const getCookiePattern = (name: string): RegExp => RegExp(name + '=.[^;]*') + +/** + * Get cookie from document (Local DOM) + * + * @param name { string } - Cookie name + */ +function getCookieFromDocument(name: string): string | undefined { + const pattern = getCookiePattern(name) + const matched = document.cookie.match(pattern) + + if (!matched) { + return undefined + } + + const cookie = matched[0].split('=') + return decodeURIComponent(cookie[1]) +} + +function getCookieFromRequest( + name: string, + request: IncomingMessage +): string | undefined { + if (request.headers && request.headers.cookie) { + const pattern = getCookiePattern(name) + const matched = request.headers.cookie.match(pattern) + + if (!matched) { + return undefined + } + + const cookie = matched[0].split('=') + return decodeURIComponent(cookie[1]) + } +} + +/** + * Set new cookie + * + * @param name { string } - Cookie name + * @param value { string } - Cookie value + */ +function setCookie(name: string, value: string): void { + document.cookie = name + '=' + value + ';path=/' +} + +/** + * Check if the DOM is available + * if this function returns false, it means that the page is not rendered by the client. + * However, it might be pre-rendered for example on a server. + */ +function canUseDOM(): boolean { + return !!( + typeof window != 'undefined' && + window.document && + window.document.createElement + ) +} + +export { getCookieFromDocument, getCookieFromRequest, setCookie, canUseDOM }