Skip to content

Commit

Permalink
refactor: detect browser language internals (#3063)
Browse files Browse the repository at this point in the history
* refactor: `detectBrowserLanguage` internal logic

* refactor: simplify function calling

* refactor: detect locale and browserlanguage logging

* refactor: disable log tag until further usage of `createLogger`

* fix: logger should preserve correct line number
  • Loading branch information
BobbieGoede authored Aug 18, 2024
1 parent 519287b commit fe262c4
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 203 deletions.
179 changes: 63 additions & 116 deletions src/runtime/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import isHTTPS from 'is-https'
import { useRequestHeaders, useRequestEvent, useCookie as useNuxtCookie, useRuntimeConfig, useNuxtApp } from '#imports'
import { NUXT_I18N_MODULE_ID, DEFAULT_COOKIE_KEY, isSSG, localeCodes, normalizedLocales } from '#build/i18n.options.mjs'
import { findBrowserLocale, getLocalesRegex } from './routing/utils'
import { initCommonComposableOptions, type CommonComposableOptions } from './utils'
import { createLogger, initCommonComposableOptions, type CommonComposableOptions } from './utils'

import type { Locale } from 'vue-i18n'
import type { DetectBrowserLanguageOptions, LocaleObject } from '#build/i18n.options.mjs'
Expand Down Expand Up @@ -141,19 +141,24 @@ export function setLocaleCookie(
cookieRef.value = locale
}

export type DetectBrowserLanguageNotDetectReason =
| 'unknown'
| 'not_found_match'
| 'first_access_only'
| 'not_redirect_on_root'
| 'not_redirect_on_no_prefix'
| 'detect_ignore_on_ssg'
export type DetectBrowserLanguageFrom = 'unknown' | 'cookie' | 'navigator_or_header' | 'fallback'
export type DetectBrowserLanguageFromResult = {
export const enum DetectFailure {
NOT_FOUND = 'not_found_match',
FIRST_ACCESS = 'first_access_only',
NO_REDIRECT_ROOT = 'not_redirect_on_root',
NO_REDIRECT_NO_PREFIX = 'not_redirect_on_no_prefix',
SSG_IGNORE = 'detect_ignore_on_ssg'
}

const enum DetectFrom {
COOKIE = 'cookie',
NAVIGATOR_HEADER = 'navigator_or_header',
FALLBACK = 'fallback'
}

type DetectBrowserLanguageFromResult = {
locale: string
stat: boolean
reason?: DetectBrowserLanguageNotDetectReason
from?: DetectBrowserLanguageFrom
from?: DetectFrom
reason?: DetectFailure
}
export type DetectLocaleForSSGStatus = 'ssg_ignore' | 'ssg_setup' | 'normal'
export type DetectLocaleCallType = 'setup' | 'routing'
Expand All @@ -164,140 +169,82 @@ export type DetectLocaleContext = {
localeCookie: string | undefined
}

export const DefaultDetectBrowserLanguageFromResult: DetectBrowserLanguageFromResult = {
locale: '',
stat: false,
reason: 'unknown',
from: 'unknown'
}
export const DefaultDetectBrowserLanguageFromResult: DetectBrowserLanguageFromResult = { locale: '' }

export function detectBrowserLanguage(
route: string | RouteLocationNormalized | RouteLocationNormalizedLoaded,
vueI18nOptionsLocale: Locale | undefined,
detectLocaleContext: DetectLocaleContext,
locale: Locale = ''
): DetectBrowserLanguageFromResult {
const logger = createLogger('detectBrowserLanguage')
const _detect = runtimeDetectBrowserLanguage()

// feature is disabled
if (!_detect) {
return DefaultDetectBrowserLanguageFromResult
}

const { strategy } = useRuntimeConfig().public.i18n
const { ssg, callType, firstAccess, localeCookie } = detectLocaleContext
__DEBUG__ && console.log('detectBrowserLanguage: (ssg, callType, firstAccess) - ', ssg, callType, firstAccess)

// browser detection is ignored if it's a nuxt generate.
__DEBUG__ && logger.log({ ssg, callType, firstAccess })

// detection ignored during nuxt generate
if (isSSG && strategy === 'no_prefix' && (import.meta.server || ssg === 'ssg_ignore')) {
return { locale: '', stat: true, reason: 'detect_ignore_on_ssg' }
return { locale: '', reason: DetectFailure.SSG_IGNORE }
}

// browser locale detection happens during first access only
// detection only on first access
if (!firstAccess) {
return { locale: strategy === 'no_prefix' ? locale : '', stat: false, reason: 'first_access_only' }
return { locale: strategy === 'no_prefix' ? locale : '', reason: DetectFailure.FIRST_ACCESS }
}

const { redirectOn, alwaysRedirect, useCookie, fallbackLocale } =
runtimeDetectBrowserLanguage() as DetectBrowserLanguageOptions
const { redirectOn, alwaysRedirect, useCookie, fallbackLocale } = _detect

const path = isString(route) ? route : route.path
__DEBUG__ &&
console.log(
'detectBrowserLanguage: (path, strategy, alwaysRedirect, redirectOn, locale) -',
path,
strategy,
alwaysRedirect,
redirectOn,
locale
)
__DEBUG__ && logger.log({ locale, path, strategy, alwaysRedirect, redirectOn })

if (strategy !== 'no_prefix') {
if (redirectOn === 'root') {
if (path !== '/') {
__DEBUG__ && console.log('detectBrowserLanguage: not root')
return { locale: '', stat: false, reason: 'not_redirect_on_root' }
}
} else if (redirectOn === 'no prefix') {
__DEBUG__ && console.log('detectBrowserLanguage: no prefix (path) -', path)
if (!alwaysRedirect && path.match(getLocalesRegex(localeCodes))) {
return { locale: '', stat: false, reason: 'not_redirect_on_no_prefix' }
}
// detection only on root
if (redirectOn === 'root' && path !== '/') {
__DEBUG__ && logger.log('not root', { path })
return { locale: '', reason: DetectFailure.NO_REDIRECT_ROOT }
}

__DEBUG__ && redirectOn === 'no prefix' && logger.log('no prefix -', { path })

// detection only on unprefixed route
if (redirectOn === 'no prefix' && !alwaysRedirect && path.match(getLocalesRegex(localeCodes))) {
return { locale: '', reason: DetectFailure.NO_REDIRECT_NO_PREFIX }
}
}

let localeFrom: DetectBrowserLanguageFrom = 'unknown'
let cookieLocale: string | undefined
let matchedLocale: string | undefined
// track detection match source
let from: DetectFrom | undefined

// get preferred language from cookie if present and enabled
// match locale from cookie if enabled and present
const cookieMatch = (useCookie && localeCookie) || undefined
if (useCookie) {
matchedLocale = cookieLocale = localeCookie
localeFrom = 'cookie'
__DEBUG__ && console.log('detectBrowserLanguage: cookieLocale', cookieLocale)
from = DetectFrom.COOKIE
}
// try to get locale from either navigator or header detection
if (!matchedLocale) {
matchedLocale = getBrowserLocale()
localeFrom = 'navigator_or_header'
__DEBUG__ && console.log('detectBrowserLanguage: browserLocale', matchedLocale)
}
__DEBUG__ &&
console.log(
'detectBrowserLanguage: (matchedLocale, cookieLocale, localeFrom) -',
matchedLocale,
cookieLocale,
localeFrom
)

// set fallback locale if that is not matched locale
const finalLocale = matchedLocale || fallbackLocale
if (!matchedLocale && fallbackLocale) {
localeFrom = 'fallback'
}
__DEBUG__ &&
console.log(
'detectBrowserLanguage: first finaleLocale (finaleLocale, cookieLocale, localeFrom) -',
finalLocale,
cookieLocale,
localeFrom
)

const vueI18nLocale = locale || vueI18nOptionsLocale
__DEBUG__ && console.log('detectBrowserLanguage: vueI18nLocale', vueI18nLocale)

// handle cookie option to prevent multiple redirects
if (finalLocale && (!useCookie || alwaysRedirect || !cookieLocale)) {
if (strategy === 'no_prefix') {
return { locale: finalLocale, stat: true, from: localeFrom }
} else {
if (callType === 'setup') {
if (finalLocale !== vueI18nLocale) {
__DEBUG__ && console.log('detectBrowserLanguage: finalLocale !== vueI18nLocale', finalLocale)
return { locale: finalLocale, stat: true, from: localeFrom }
}
}

if (alwaysRedirect) {
const redirectOnRoot = path === '/'
const redirectOnAll = redirectOn === 'all'
const redirectOnNoPrefix = redirectOn === 'no prefix' && !path.match(getLocalesRegex(localeCodes))
__DEBUG__ &&
console.log(
'detectBrowserLanguage: (redirectOnRoot, redirectOnAll, redirectOnNoPrefix) - ',
redirectOnRoot,
redirectOnAll,
redirectOnNoPrefix
)
if (redirectOnRoot || redirectOnAll || redirectOnNoPrefix) {
return { locale: finalLocale, stat: true, from: localeFrom }
}
}
}
// match locale from either navigator or header detection
const browserMatch = getBrowserLocale()
if (!cookieMatch) {
from = DetectFrom.NAVIGATOR_HEADER
}

if (ssg === 'ssg_setup' && finalLocale) {
return { locale: finalLocale, stat: true, from: localeFrom }
}
const matchedLocale = cookieMatch || browserMatch

if ((localeFrom === 'navigator_or_header' || localeFrom === 'cookie') && finalLocale) {
return { locale: finalLocale, stat: true, from: localeFrom }
// use fallback locale when no locale matched
const resolved = matchedLocale || fallbackLocale || ''
if (!matchedLocale && fallbackLocale) {
from = DetectFrom.FALLBACK
}

return { locale: '', stat: false, reason: 'not_found_match' }
__DEBUG__ && logger.log({ locale: resolved, cookieMatch, browserMatch, from })

return { locale: resolved, from }
}

export function getHost() {
Expand Down
42 changes: 12 additions & 30 deletions src/runtime/plugins/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
getLocaleCookie,
setLocaleCookie,
detectBrowserLanguage,
DefaultDetectBrowserLanguageFromResult,
getI18nCookie,
runtimeDetectBrowserLanguage
} from '../internal'
Expand Down Expand Up @@ -67,7 +66,6 @@ export default defineNuxtPlugin({
let initialLocale = detectLocale(
route,
getLocaleFromRoute,
vueI18nOptions.locale,
getDefaultLocale(runtimeI18n.defaultLocale),
{
ssg: isSSG && runtimeI18n.strategy === 'no_prefix' ? 'ssg_ignore' : 'normal',
Expand Down Expand Up @@ -112,33 +110,18 @@ export default defineNuxtPlugin({
if (isSSGModeInitialSetup() && runtimeI18n.strategy === 'no_prefix' && import.meta.client) {
nuxt.hook('app:mounted', async () => {
__DEBUG__ && console.log('hook app:mounted')
const {
locale: browserLocale,
stat,
reason,
from
} = _detectBrowserLanguage
? detectBrowserLanguage(
route,
vueI18nOptions.locale,
{
ssg: 'ssg_setup',
callType: 'setup',
firstAccess: true,
localeCookie: getLocaleCookie(localeCookie, _detectBrowserLanguage, runtimeI18n.defaultLocale)
},
initialLocale
)
: DefaultDetectBrowserLanguageFromResult
__DEBUG__ &&
console.log(
'app:mounted: detectBrowserLanguage (browserLocale, stat, reason, from) -',
browserLocale,
stat,
reason,
from
)
await setLocale(i18n, browserLocale)
const detected = detectBrowserLanguage(
route,
{
ssg: 'ssg_setup',
callType: 'setup',
firstAccess: true,
localeCookie: getLocaleCookie(localeCookie, _detectBrowserLanguage, runtimeI18n.defaultLocale)
},
initialLocale
)
__DEBUG__ && console.log('app:mounted: detectBrowserLanguage (locale, reason, from) -', Object.values(detected))
await setLocale(i18n, detected.locale)
ssgModeInitialSetup = false
})
}
Expand Down Expand Up @@ -318,7 +301,6 @@ export default defineNuxtPlugin({
const locale = detectLocale(
to,
getLocaleFromRoute,
vueI18nOptions.locale,
() => {
return getLocale(i18n) || getDefaultLocale(runtimeI18n.defaultLocale)
},
Expand Down
Loading

0 comments on commit fe262c4

Please sign in to comment.