@nuxtjs/i18n
Version:
i18n for Nuxt
226 lines (199 loc) • 7.31 kB
JavaScript
import { parse as cookieParse, serialize as cookieSerialize } from 'cookie'
import JsCookie from 'js-cookie'
/** @typedef {import('../../types/internal').ResolvedOptions} ResolvedOptions */
/**
* Formats a log message, prefixing module's name to it.
*
* @param {string} text
* @return {string}
*/
export function formatMessage (text) {
return `[@nuxtjs/i18n] ${text}`
}
/**
* Parses locales provided from browser through `accept-language` header.
*
* @param {string} input
* @return {string[]} An array of locale codes. Priority determined by order in array.
*/
export function parseAcceptLanguage (input) {
// Example input: en-US,en;q=0.9,nb;q=0.8,no;q=0.7
// Contains tags separated by comma.
// Each tag consists of locale code (2-3 letter language code) and optionally country code
// after dash. Tag can also contain score after semicolon, that is assumed to match order
// so it's not explicitly used.
return input.split(',').map(tag => tag.split(';')[0])
}
/**
* Find locale code that best matches provided list of browser locales.
*
* @param {ResolvedOptions['normalizedLocales']} appLocales The user-configured locales that are to be matched.
* @param {readonly string[]} browserLocales The locales to match against configured.
* @return {string | undefined}
*/
export function matchBrowserLocale (appLocales, browserLocales) {
/** @type {{ code: string, score: number }[]} */
const matchedLocales = []
// Normalise appLocales input
/** @type {{ code: string, iso: string }[]} */
const normalizedAppLocales = []
for (const appLocale of appLocales) {
const { code } = appLocale
const iso = appLocale.iso || code
normalizedAppLocales.push({ code, iso })
}
// First pass: match exact locale.
for (const [index, browserCode] of browserLocales.entries()) {
const matchedLocale = normalizedAppLocales.find(appLocale => appLocale.iso.toLowerCase() === browserCode.toLowerCase())
if (matchedLocale) {
matchedLocales.push({ code: matchedLocale.code, score: 1 - index / browserLocales.length })
break
}
}
// Second pass: match only locale code part of the browser locale (not including country).
for (const [index, browserCode] of browserLocales.entries()) {
const languageCode = browserCode.split('-')[0].toLowerCase()
const matchedLocale = normalizedAppLocales.find(appLocale => appLocale.iso.split('-')[0].toLowerCase() === languageCode)
if (matchedLocale) {
// Deduct a thousandth for being non-exact match.
matchedLocales.push({ code: matchedLocale.code, score: 0.999 - index / browserLocales.length })
break
}
}
// Sort the list by score (0 - lowest, 1 - highest).
if (matchedLocales.length > 1) {
matchedLocales.sort((localeA, localeB) => {
if (localeA.score === localeB.score) {
// If scores are equal then pick more specific (longer) code.
return localeB.code.length - localeA.code.length
}
return localeB.score - localeA.score
})
}
return matchedLocales.length ? matchedLocales[0].code : undefined
}
/**
* Get locale code that corresponds to current hostname
*
* @param {ResolvedOptions['normalizedLocales']} locales
* @param {import('http').IncomingMessage | undefined} req
* @return {string} Locale code found if any
*/
export function getLocaleDomain (locales, req) {
/** @type {string | undefined} */
let host
if (process.client) {
host = window.location.host
} else if (req) {
const detectedHost = req.headers['x-forwarded-host'] || req.headers.host
host = Array.isArray(detectedHost) ? detectedHost[0] : detectedHost
}
if (host) {
const matchingLocale = locales.find(l => l.domain === host)
if (matchingLocale) {
return matchingLocale.code
}
}
return ''
}
/**
* Creates a RegExp for route paths
*
* @param {readonly string[]} localeCodes
* @return {RegExp}
*/
export function getLocalesRegex (localeCodes) {
return new RegExp(`^/(${localeCodes.join('|')})(?:/|$)`)
}
/**
* Creates getter for getLocaleFromRoute
*
* @param {readonly string[]} localeCodes
* @param {Pick<ResolvedOptions, 'routesNameSeparator' | 'defaultLocaleRouteNameSuffix'>} options
*/
export function createLocaleFromRouteGetter (localeCodes, { routesNameSeparator, defaultLocaleRouteNameSuffix }) {
const localesPattern = `(${localeCodes.join('|')})`
const defaultSuffixPattern = `(?:${routesNameSeparator}${defaultLocaleRouteNameSuffix})?`
const regexpName = new RegExp(`${routesNameSeparator}${localesPattern}${defaultSuffixPattern}$`)
const regexpPath = getLocalesRegex(localeCodes)
/**
* Extract locale code from given route:
* - If route has a name, try to extract locale from it
* - Otherwise, fall back to using the routes'path
* @param {import('vue-router').Route} route
* @return {string} Locale code found if any
*/
const getLocaleFromRoute = route => {
// Extract from route name
if (route.name) {
const matches = route.name.match(regexpName)
if (matches && matches.length > 1) {
return matches[1]
}
} else if (route.path) {
// Extract from path
const matches = route.path.match(regexpPath)
if (matches && matches.length > 1) {
return matches[1]
}
}
return ''
}
return getLocaleFromRoute
}
/**
* @param {import('http').IncomingMessage | undefined} req
* @param {{ useCookie: boolean, cookieKey: string, localeCodes: readonly string[] }} options
* @return {string | undefined}
*/
export function getLocaleCookie (req, { useCookie, cookieKey, localeCodes }) {
if (useCookie) {
let localeCode
if (process.client) {
localeCode = JsCookie.get(cookieKey)
} else if (req && typeof req.headers.cookie !== 'undefined') {
const cookies = req.headers && req.headers.cookie ? cookieParse(req.headers.cookie) : {}
localeCode = cookies[cookieKey]
}
if (localeCode && localeCodes.includes(localeCode)) {
return localeCode
}
}
}
/**
* @param {string} locale
* @param {import('http').ServerResponse | undefined} res
* @param {{ useCookie: boolean, cookieDomain: string | null, cookieKey: string, cookieSecure: boolean, cookieCrossOrigin: boolean}} options
*/
export function setLocaleCookie (locale, res, { useCookie, cookieDomain, cookieKey, cookieSecure, cookieCrossOrigin }) {
if (!useCookie) {
return
}
const date = new Date()
/** @type {import('cookie').CookieSerializeOptions} */
const cookieOptions = {
expires: new Date(date.setDate(date.getDate() + 365)),
path: '/',
sameSite: cookieCrossOrigin ? 'none' : 'lax',
secure: cookieCrossOrigin || cookieSecure
}
if (cookieDomain) {
cookieOptions.domain = cookieDomain
}
if (process.client) {
// @ts-ignore
JsCookie.set(cookieKey, locale, cookieOptions)
} else if (res) {
let headers = res.getHeader('Set-Cookie') || []
if (!Array.isArray(headers)) {
headers = [String(headers)]
}
const redirectCookie = cookieSerialize(cookieKey, locale, cookieOptions)
headers = headers.filter(header => {
const cookie = cookieParse(header)
return !(cookieKey in cookie)
})
headers.push(redirectCookie)
res.setHeader('Set-Cookie', headers)
}
}