Skip to content

Commit f97d7d3

Browse files
Merge pull request #2 from stplasim/master
Extend internationalization package with SSR and CSR and the ability to store selected language in cookies
2 parents 8160c72 + 80f1dba commit f97d7d3

4 files changed

Lines changed: 215 additions & 12 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
},
4040
"homepage": "https://github.com/aboutbits/internationalization#readme",
4141
"devDependencies": {
42+
"@types/node": "^14.11.5",
4243
"@types/jest": "^26.0.9",
4344
"@typescript-eslint/eslint-plugin": "^3.8.0",
4445
"@typescript-eslint/parser": "^3.8.0",

readme.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ Internationalization
33

44
[![npm version](https://badge.fury.io/js/%40aboutbits%2Finternationalization.svg)](https://badge.fury.io/js/%40aboutbits%2Finternationalization)
55

6-
This package includes utilities for working with different languages in the browser.
6+
This package includes utilities for working with different languages in the browser.
7+
This package works for client side rendered applications as well as for server side rendered applications.
78

89
## Table of content
910

@@ -22,15 +23,26 @@ npm install @aboutbits/internationalization
2223
Second, you can setup the tool with all supported languages and the fallback language and then detect the given browser language.
2324

2425
```js
25-
import Internationalization from '@aboutbits/internationalization'
26+
import { Internationalization } from '@aboutbits/internationalization'
2627

2728
let supportedLanguages = ['en', 'de', 'it']
2829
let fallbackLanguage = 'en'
2930

3031
let i18n = new Internationalization(supportedLanguages, fallbackLanguage)
3132

32-
let language = i18n.detectBrowserLanguage()
33+
// Fetches the language only from the user's browser
34+
let browserLanguage = i18n.detectBrowserLanguage()
3335

36+
// First check the cookies to see if a language is set. If not it will look in the browser.
37+
// You can also pass a requiest obejct with which the language can be recognized already during the server side rendering
38+
let language = i18n.detectLanguage()
39+
40+
// Sets a cookie with the specified language.
41+
// There exists also a static version of this method.
42+
// Ex. Internationalization.setLanguage("de")
43+
i18n.setLanguage("de")
44+
45+
console.log(browserLanguage)
3446
console.log(language)
3547
```
3648

src/index.ts

Lines changed: 131 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,143 @@
1+
import { IncomingMessage } from 'http'
2+
import {
3+
getCookieFromDocument,
4+
canUseDOM,
5+
setCookie,
6+
getCookieFromRequest,
7+
} from './utilities'
8+
19
class Internationalization<T extends string> {
2-
supportedLanguages: T[]
10+
private readonly cookieName: string
11+
12+
supportedLanguages: Array<T>
313
fallbackLanguage: T
414

5-
constructor(supportedLanguages: T[], fallbackLanguage: T) {
15+
/**
16+
* ClientInternationalization utility package for working with languages
17+
*
18+
* @param supportedLanguages { Array<string> } - List of all supported languages
19+
* @param fallbackLanguage { string } - Fallback language if no language matches
20+
* @param cookieName { string } - Optional: Set a preferred cookie name
21+
*/
22+
constructor(
23+
supportedLanguages: Array<T>,
24+
fallbackLanguage: T,
25+
cookieName?: string
26+
) {
627
this.supportedLanguages = supportedLanguages
728
this.fallbackLanguage = fallbackLanguage
29+
30+
this.cookieName = typeof cookieName === 'string' ? cookieName : 'language'
831
}
932

33+
/**
34+
* Detect the browser language of the client
35+
* If no language was found or if none of the
36+
* supported language matches a fallback language will be returned
37+
*
38+
* @return {string} - Detected language
39+
*/
1040
detectBrowserLanguage(): T {
11-
const browserLanguage =
12-
(navigator.languages && navigator.languages[0]) || navigator.language
41+
// Check if the DOM is presented.
42+
// If the DOM isn't accessible the website may be rendered on a server
43+
if (canUseDOM()) {
44+
// Get browser language
45+
const browserLanguage =
46+
(navigator.languages && navigator.languages[0]) || navigator.language
47+
48+
return (
49+
this.supportedLanguages.find((lang) => lang === browserLanguage) ||
50+
this.fallbackLanguage
51+
)
52+
}
53+
54+
// Return fallback language if no DOM was found
55+
return this.fallbackLanguage
56+
}
57+
58+
/**
59+
* Save a language to a cookie
60+
* If the set language isn't in the list of supported languages
61+
* the fallback language will be returned
62+
*
63+
* @param language { string } - Language to set
64+
*/
65+
setLanguage(language: string): void {
66+
if (canUseDOM()) {
67+
const cookieLanguage =
68+
this.supportedLanguages.find((lang) => lang === language) ||
69+
this.fallbackLanguage
70+
71+
setCookie(this.cookieName, cookieLanguage)
72+
}
73+
}
74+
75+
/**
76+
* Get the current language
77+
* This method will first check if a language cookie is present.
78+
* If no cookie was found the method will get the language form the browser
79+
*
80+
* For handling cookies on the server side pass the request object
81+
*
82+
* @param request { IncomingMessage } - Optional: Request object for accessing cookies over request headers
83+
* @return { string } - Found language or fallback language
84+
*/
85+
detectLanguage(request?: IncomingMessage): T {
86+
// Check if request is not undefined
87+
if (request) {
88+
// Try to get language form cookie
89+
const cookieFromRequest = getCookieFromRequest(this.cookieName, request)
90+
91+
if (cookieFromRequest) {
92+
return cookieFromRequest as T
93+
}
94+
95+
// Get accept language from request header
96+
const acceptLanguage = request.headers['accept-language']
97+
98+
if (acceptLanguage) {
99+
// Extract the locale string and the priority from header string
100+
const requestedLocales = acceptLanguage.split(',').map((part) => {
101+
const [locale, priority] = part.trim().split(';q=')
102+
103+
return { locale, priority: parseInt(priority) }
104+
})
105+
106+
// Get the potential locale from requested locales array
107+
const potentialLocale = requestedLocales
108+
.sort((a, b) => b.priority - a.priority)
109+
.find(
110+
({ locale }) =>
111+
locale !== '*' &&
112+
this.supportedLanguages.some((lang) => locale === lang)
113+
)
114+
115+
// Check if the potential locale is not undefined else return the fallback language
116+
return (potentialLocale
117+
? potentialLocale.locale
118+
: this.fallbackLanguage) as T
119+
}
120+
}
121+
122+
// Check for DOM
123+
if (canUseDOM()) {
124+
// Try to get language form cookie
125+
const language = getCookieFromDocument(this.cookieName)
126+
127+
// If cookie is undefined get the language form the browser
128+
if (language == undefined) {
129+
return this.detectBrowserLanguage()
130+
}
131+
132+
// Return found language or fallback language
133+
return (
134+
this.supportedLanguages.find((lang) => lang === language) ||
135+
this.fallbackLanguage
136+
)
137+
}
13138

14-
return (
15-
this.supportedLanguages.find((lang) => lang === browserLanguage) ||
16-
this.fallbackLanguage
17-
)
139+
return this.fallbackLanguage
18140
}
19141
}
20142

21-
export { Internationalization as default }
143+
export default Internationalization

src/utilities.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* Regex based filter to get cookie values
3+
* This includes the =
4+
*
5+
* @param name { string } - Filter keyword
6+
*/
7+
import { IncomingMessage } from 'http'
8+
9+
const getCookiePattern = (name: string): RegExp => RegExp(name + '=.[^;]*')
10+
11+
/**
12+
* Get cookie from document (Local DOM)
13+
*
14+
* @param name { string } - Cookie name
15+
*/
16+
function getCookieFromDocument(name: string): string | undefined {
17+
const pattern = getCookiePattern(name)
18+
const matched = document.cookie.match(pattern)
19+
20+
if (!matched) {
21+
return undefined
22+
}
23+
24+
const cookie = matched[0].split('=')
25+
return decodeURIComponent(cookie[1])
26+
}
27+
28+
function getCookieFromRequest(
29+
name: string,
30+
request: IncomingMessage
31+
): string | undefined {
32+
if (request.headers && request.headers.cookie) {
33+
const pattern = getCookiePattern(name)
34+
const matched = request.headers.cookie.match(pattern)
35+
36+
if (!matched) {
37+
return undefined
38+
}
39+
40+
const cookie = matched[0].split('=')
41+
return decodeURIComponent(cookie[1])
42+
}
43+
}
44+
45+
/**
46+
* Set new cookie
47+
*
48+
* @param name { string } - Cookie name
49+
* @param value { string } - Cookie value
50+
*/
51+
function setCookie(name: string, value: string): void {
52+
document.cookie = name + '=' + value + ';path=/'
53+
}
54+
55+
/**
56+
* Check if the DOM is available
57+
* if this function returns false, it means that the page is not rendered by the client.
58+
* However, it might be pre-rendered for example on a server.
59+
*/
60+
function canUseDOM(): boolean {
61+
return !!(
62+
typeof window != 'undefined' &&
63+
window.document &&
64+
window.document.createElement
65+
)
66+
}
67+
68+
export { getCookieFromDocument, getCookieFromRequest, setCookie, canUseDOM }

0 commit comments

Comments
 (0)