-
Notifications
You must be signed in to change notification settings - Fork 0
Extend internationalization package with SSR and CSR and the ability to store selected language in cookies #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 13 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
9450f59
Moved Internationalization class in it's own file
08d3e29
Added utilities functions for handling cookies and DOM verification
03d31ba
Add client side Internationalization class.
33da2ea
Merge remote-tracking branch 'upsource/master'
ba1d1fd
Changed cookie name form private to protected
a3957db
Add ServerInternationalization class to index.ts
1e3e398
Add typescript types for node js as dev dependency
d23de20
Add utility function for reading cookies form request objects
9cca1fb
Add ServerInternationalization class for handling languages on the se…
ba3e102
Fixed typo in ClientInternationalization.ts
4be01a2
Merged ClientInternationalization.ts and ServerInternationalization.t…
76a906b
Add new Internationalization class in index file
0eacc1c
Updated description and example in readme.md
80f1dba
Removed static method setLanguage
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,21 +1,159 @@ | ||
| import { IncomingMessage } from 'http' | ||
| import { | ||
| getCookieFromDocument, | ||
| canUseDOM, | ||
| setCookie, | ||
| getCookieFromRequest, | ||
| } from './utilities' | ||
|
|
||
| class Internationalization<T extends string> { | ||
| supportedLanguages: T[] | ||
| private readonly cookieName: string | ||
|
|
||
| supportedLanguages: Array<T> | ||
| fallbackLanguage: T | ||
|
|
||
| constructor(supportedLanguages: T[], fallbackLanguage: T) { | ||
| /** | ||
| * ClientInternationalization utility package for working with languages | ||
| * | ||
| * @param supportedLanguages { Array<string> } - 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<T>, | ||
| 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) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Save a language to a cookie | ||
| * This method will not check if the language you want to set is supported | ||
| * Please note! If you have changed the cookie name, you must change it here as well | ||
| * | ||
| * @param language { string } - Language to set | ||
| * @param cookieName { string } - Optional: Cookie name | ||
| */ | ||
| static setLanguage(language: string, cookieName?: string): void { | ||
| if (canUseDOM()) { | ||
| cookieName = typeof cookieName === 'string' ? cookieName : 'language' | ||
|
|
||
| setCookie(cookieName, language) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this static function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought this method could be useful to quickly change the language without having to create a new instance
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would leave it out for now and add it only if see a need for it.