Skip to content

Commit

Permalink
fix: redirect without appending extra chars (#1214)
Browse files Browse the repository at this point in the history
Co-authored-by: Rafal Chlodnicki <rchl2k@gmail.com>
  • Loading branch information
divine and rchl committed Jul 4, 2021
1 parent 16105c6 commit 0527d63
Show file tree
Hide file tree
Showing 10 changed files with 320 additions and 260 deletions.
3 changes: 2 additions & 1 deletion src/helpers/routes.js
@@ -1,3 +1,4 @@
import { withoutTrailingSlash, withTrailingSlash } from 'ufo'
import { STRATEGIES } from './constants'
import { extractComponentOptions } from './components'
import { getPageOptions } from './utils'
Expand Down Expand Up @@ -149,7 +150,7 @@ export function makeRoutes (baseRoutes, {
// - If "router.trailingSlash" is not specified then default to no trailing slash (like Nuxt)
// - Children with relative paths must not start with slash so don't append if path is empty.
if (path.length) { // Don't replace empty (child) path with a slash!
path = path.replace(/\/+$/, '') + (trailingSlash ? '/' : '') || (isChildWithRelativePath ? '' : '/')
path = (trailingSlash ? withTrailingSlash(path, true) : withoutTrailingSlash(path, true)) || (isChildWithRelativePath ? '' : '/')
}

if (shouldAddPrefix && isDefaultLocale && strategy === STRATEGIES.PREFIX && includeUprefixedFallback) {
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Expand Up @@ -93,6 +93,7 @@ export default function (moduleOptions) {
this.nuxt.hook('build:before', () => buildHook.call(this, options))

this.options.alias['~i18n-klona'] = require.resolve('klona/full').replace(/\.js$/, '.mjs')
this.options.alias['~i18n-ufo'] = require.resolve('ufo').replace(/\.js$/, '.mjs')

if (!Array.isArray(this.options.router.middleware)) {
throw new TypeError(formatMessage('options.router.middleware is not an array.'))
Expand Down
10 changes: 4 additions & 6 deletions src/templates/plugin.main.js
@@ -1,21 +1,19 @@
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import { joinURL } from 'ufo'
import { nuxtI18nHead, nuxtI18nSeo } from './head-meta'
import { Constants, nuxtOptions, options } from './options'
import {
createLocaleFromRouteGetter,
getLocaleCookie,
getLocaleDomain,
getLocalesRegex,
resolveBaseUrl,
matchBrowserLocale,
parseAcceptLanguage,
registerStore,
setLocaleCookie,
syncVuex
setLocaleCookie
} from './utils-common'
import { loadLanguageAsync } from './utils'
import { loadLanguageAsync, resolveBaseUrl, registerStore, syncVuex } from './plugin.utils'
// @ts-ignore
import { joinURL } from '~i18n-ufo'
// @ts-ignore
import { klona } from '~i18n-klona'

Expand Down
6 changes: 4 additions & 2 deletions src/templates/plugin.routing.js
@@ -1,7 +1,9 @@
import './middleware'
import Vue from 'vue'
import { Constants, nuxtOptions, options } from './options'
import { getDomainFromLocale } from './utils-common'
import { getDomainFromLocale } from './plugin.utils'
// @ts-ignore
import { withoutTrailingSlash, withTrailingSlash } from '~i18n-ufo'

/**
* @this {import('../../types/internal').PluginProxy}
Expand Down Expand Up @@ -86,7 +88,7 @@ function resolveRoute (route, locale) {
if (isPrefixed) {
localizedRoute.path = `/${locale}${localizedRoute.path}`
}
localizedRoute.path = localizedRoute.path.replace(/\/+$/, '') + (nuxtOptions.trailingSlash ? '/' : '') || '/'
localizedRoute.path = nuxtOptions.trailingSlash ? withTrailingSlash(localizedRoute.path, true) : withoutTrailingSlash(localizedRoute.path, true)
}
} else {
if (!localizedRoute.name && !localizedRoute.path) {
Expand Down
254 changes: 254 additions & 0 deletions src/templates/plugin.utils.js
@@ -0,0 +1,254 @@
import isHTTPS from 'is-https'
import { localeMessages, options } from './options'
import { formatMessage } from './utils-common'
// @ts-ignore
import { hasProtocol } from '~i18n-ufo'

/** @typedef {import('../../types/internal').ResolvedOptions} ResolvedOptions */

/**
* Asynchronously load messages from translation files
*
* @param {import('@nuxt/types').Context} context
* @param {string} locale Language code to load
* @return {Promise<void>}
*/
export async function loadLanguageAsync (context, locale) {
const { app } = context
const { i18n } = app

if (!i18n.loadedLanguages) {
i18n.loadedLanguages = []
}

if (!i18n.loadedLanguages.includes(locale)) {
const localeObject = options.normalizedLocales.find(l => l.code === locale)
if (localeObject) {
const { file } = localeObject
if (file) {
/* <% if (options.options.langDir) { %> */
/** @type {import('vue-i18n').LocaleMessageObject | undefined} */
let messages
if (process.client) {
const { nuxtState } = context
if (nuxtState && nuxtState.__i18n && nuxtState.__i18n.langs[locale]) {
messages = nuxtState.__i18n.langs[locale]
// Even if already cached in Nuxt state, trigger locale import so that HMR kicks-in on changes to that file.
if (context.isDev) {
localeMessages[file]()
}
}
}
if (!messages) {
try {
// @ts-ignore
const getter = await localeMessages[file]().then(m => m.default || m)
messages = typeof getter === 'function' ? await Promise.resolve(getter(context, locale)) : getter
} catch (error) {
// eslint-disable-next-line no-console
console.error(formatMessage(`Failed loading async locale export: ${error.message}`))
}
}
if (messages) {
i18n.setLocaleMessage(locale, messages)
i18n.loadedLanguages.push(locale)
}
/* <% } %> */
} else {
console.warn(formatMessage(`Could not find lang file for locale ${locale}`))
}
} else {
console.warn(formatMessage(`Attempted to load messages for non-existant locale code "${locale}"`))
}
}
}

/**
* Resolves base URL value if provided as function. Otherwise just returns verbatim.
*
* @param {string | ((context: import('@nuxt/types').Context) => string)} baseUrl
* @param {import('@nuxt/types').Context} context
* @param {import('../../types').Locale} localeCode
* @param {Pick<ResolvedOptions, 'differentDomains' | 'normalizedLocales'>} options
* @return {string}
*/
export function resolveBaseUrl (baseUrl, context, localeCode, { differentDomains, normalizedLocales }) {
if (typeof baseUrl === 'function') {
return baseUrl(context)
}

if (differentDomains && localeCode) {
// Lookup the `differentDomain` origin associated with given locale.
const domain = getDomainFromLocale(localeCode, context.req, { normalizedLocales })
if (domain) {
return domain
}
}

return baseUrl
}

/**
* Gets the `differentDomain` domain from locale.
*
* @param {string} localeCode
* @param {import('http').IncomingMessage | undefined} req
* @param {Pick<ResolvedOptions, 'normalizedLocales'>} options
* @return {string | undefined}
*/
export function getDomainFromLocale (localeCode, req, { normalizedLocales }) {
// Lookup the `differentDomain` origin associated with given locale.
const lang = normalizedLocales.find(locale => locale.code === localeCode)
if (lang && lang.domain) {
if (hasProtocol(lang.domain)) {
return lang.domain
}
let protocol
if (process.server) {
protocol = (req && isHTTPS(req)) ? 'https' : 'http'
} else {
protocol = window.location.protocol.split(':')[0]
}
return `${protocol}://${lang.domain}`
}

// eslint-disable-next-line no-console
console.warn(formatMessage(`Could not find domain name for locale ${localeCode}`))
}

/**
* @param {import('vuex').Store<Record<string, boolean>>} store
* @param {Required<import('../../types').VuexOptions>} vuex
* @param {readonly string[]} localeCodes
*/
export function registerStore (store, vuex, localeCodes) {
/** @typedef {{
* locale?: string
* messages?: Record<string, string>
* routeParams?: Record<string, Record<string, string>>
* }} ModuleStore
*
* @type {import('vuex').Module<ModuleStore, {}>}
*/
const storeModule = {
namespaced: true,
state: () => ({
...(vuex.syncLocale ? { locale: '' } : {}),
...(vuex.syncMessages ? { messages: {} } : {}),
...(vuex.syncRouteParams ? { routeParams: {} } : {})
}),
actions: {
...(vuex.syncLocale
? {
setLocale ({ commit }, locale) {
commit('setLocale', locale)
}
}
: {}),
...(vuex.syncMessages
? {
setMessages ({ commit }, messages) {
commit('setMessages', messages)
}
}
: {}),
...(vuex.syncRouteParams
? {
setRouteParams ({ commit }, params) {
if (process.env.NODE_ENV === 'development') {
validateRouteParams(params, localeCodes)
}
commit('setRouteParams', params)
}
}
: {})
},
mutations: {
...(vuex.syncLocale
? {
setLocale (state, locale) {
state.locale = locale
}
}
: {}),
...(vuex.syncMessages
? {
setMessages (state, messages) {
state.messages = messages
}
}
: {}),
...(vuex.syncRouteParams
? {
setRouteParams (state, params) {
state.routeParams = params
}
}
: {})
},
getters: {
...(vuex.syncRouteParams
? {
localeRouteParams: ({ routeParams }) => {
/** @type {(locale: string) => Record<string, string>} */
const paramsGetter = locale => (routeParams && routeParams[locale]) || {}
return paramsGetter
}
}
: {})
}
}
store.registerModule(vuex.moduleName, storeModule, { preserveState: !!store.state[vuex.moduleName] })
}

/**
* Dispatch store module actions to keep it in sync with app's locale data
*
* @param {import('vuex').Store<void>} store
* @param {string | null} locale The current locale
* @param {object | null} messages Current messages
* @param {ResolvedOptions['vuex']} vuex
* @return {Promise<void>}
*/
export async function syncVuex (store, locale = null, messages = null, vuex) {
if (vuex && store) {
if (locale !== null && vuex.syncLocale) {
await store.dispatch(vuex.moduleName + '/setLocale', locale)
}
if (messages !== null && vuex.syncMessages) {
await store.dispatch(vuex.moduleName + '/setMessages', messages)
}
}
}

/**
* Validate setRouteParams action's payload
*
* @param {object} routeParams The action's payload
* @param {readonly string[]} localeCodes
*/
export function validateRouteParams (routeParams, localeCodes) {
if (!isObject(routeParams)) {
// eslint-disable-next-line no-console
console.warn(formatMessage('Route params should be an object'))
return
}

for (const [key, value] of Object.entries(routeParams)) {
if (!localeCodes.includes(key)) {
// eslint-disable-next-line no-console
console.warn(formatMessage(`Trying to set route params for key ${key} which is not a valid locale`))
} else if (!isObject(value)) {
// eslint-disable-next-line no-console
console.warn(formatMessage(`Trying to set route params for locale ${key} with a non-object value`))
}
}
}

/**
* @param {any} value
* @return {boolean}
*/
function isObject (value) {
return value && !Array.isArray(value) && typeof value === 'object'
}

0 comments on commit 0527d63

Please sign in to comment.