├── store ├── mutation-types.js ├── i18n │ ├── getters.js │ ├── actions.js │ ├── mutations.js │ └── index.js └── index.js ├── static └── favicon.ico ├── .gitignore ├── pages ├── about.vue ├── category │ ├── index.vue │ └── _slug.vue └── index.vue ├── .editorconfig ├── .eslintrc.js ├── lang ├── en-US │ └── index.js └── fr-FR │ └── index.js ├── plugins ├── vue-i18n.js └── global-mixin.js ├── components ├── LangSwitcher.vue └── Logo.vue ├── config └── index.js ├── package.json ├── nuxt.config.js ├── middleware └── i18n.js ├── utils └── router.js ├── layouts └── default.vue └── README.md /store/mutation-types.js: -------------------------------------------------------------------------------- 1 | export const I18N_SET_LOCALE = 'I18N_SET_LOCALE' 2 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulgv/nuxt-i18n-routing/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /store/i18n/getters.js: -------------------------------------------------------------------------------- 1 | export default { 2 | currentLocale: state => state.currentLocale 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # logs 5 | npm-debug.log 6 | 7 | # Nuxt build 8 | .nuxt 9 | 10 | # Nuxt generate 11 | dist 12 | -------------------------------------------------------------------------------- /store/i18n/actions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../mutation-types' 2 | 3 | export default { 4 | setLocale ({ commit }, { locale }) { 5 | commit(types.I18N_SET_LOCALE, { locale }) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /store/i18n/mutations.js: -------------------------------------------------------------------------------- 1 | import * as types from '../mutation-types' 2 | 3 | export default { 4 | [types.I18N_SET_LOCALE] (state, { locale }) { 5 | state.currentLocale = locale 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /pages/about.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | env: { 5 | browser: true, 6 | node: true 7 | }, 8 | extends: 'standard', 9 | // required to lint *.vue files 10 | plugins: [ 11 | 'html' 12 | ], 13 | // add your custom rules here 14 | rules: {}, 15 | globals: {} 16 | } 17 | -------------------------------------------------------------------------------- /lang/en-US/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | labels: { 3 | home: 'Homepage', 4 | about: 'About', 5 | back: 'Back', 6 | select_category: 'Select category', 7 | showing_category: 'Showing category', 8 | see_categories: 'See categories' 9 | }, 10 | categories: { 11 | animals: 'Animals', 12 | landscapes: 'Landscapes' 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lang/fr-FR/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | labels: { 3 | home: 'Accueil', 4 | about: 'À propos', 5 | back: 'Retour', 6 | select_category: 'Selectionnez une catégorie', 7 | showing_category: 'Catégorie actuelle: ', 8 | see_categories: 'Voir les catégorie' 9 | }, 10 | categories: { 11 | animals: 'Animaux', 12 | landscapes: 'Paysages' 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /plugins/vue-i18n.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | 4 | import { DEFAULT_LOCALE, I18N } from '~/config' 5 | 6 | Vue.use(VueI18n) 7 | 8 | export default ({ app, store }) => { 9 | const i18n = new VueI18n({ 10 | fallbackLocale: DEFAULT_LOCALE, 11 | messages: I18N 12 | }) 13 | i18n.locale = store.state.i18n.currentLocale 14 | app.i18n = i18n 15 | } 16 | -------------------------------------------------------------------------------- /store/i18n/index.js: -------------------------------------------------------------------------------- 1 | import { LOCALES, DEFAULT_LOCALE } from '~~/config' 2 | 3 | import getters from './getters' 4 | import actions from './actions' 5 | import mutations from './mutations' 6 | 7 | // Default module's state 8 | const state = { 9 | locales: LOCALES, 10 | currentLocale: DEFAULT_LOCALE 11 | } 12 | 13 | export default { 14 | namespaced: true, 15 | state, 16 | getters, 17 | actions, 18 | mutations 19 | } 20 | -------------------------------------------------------------------------------- /components/LangSwitcher.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 22 | -------------------------------------------------------------------------------- /pages/category/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 22 | 23 | 28 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | // i18n messages 2 | const en = require('../lang/en-US') 3 | const fr = require('../lang/fr-FR') 4 | 5 | // i18n config 6 | const LOCALES = [ 7 | { 8 | code: 'en', 9 | iso: 'en-US', 10 | name: 'English' 11 | }, 12 | { 13 | code: 'fr', 14 | iso: 'fr-FR', 15 | name: 'Français' 16 | } 17 | ] 18 | const DEFAULT_LOCALE = 'en' 19 | const I18N = { 20 | en, 21 | fr 22 | } 23 | 24 | // Define custom paths for localized routes 25 | // If a route/locale is omitted, defaults to Nuxt's generated path 26 | const ROUTES_ALIASES = { 27 | about: { 28 | fr: '/a-propos', 29 | en: '/about-us' 30 | }, 31 | category: { 32 | fr: '/categorie' 33 | }, 34 | 'category-slug': { 35 | fr: '/categorie/:slug' 36 | } 37 | } 38 | 39 | module.exports = { 40 | LOCALES, 41 | DEFAULT_LOCALE, 42 | I18N, 43 | ROUTES_ALIASES 44 | } 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-i18n-routing", 3 | "version": "1.0.0", 4 | "description": "Localized routing with Nuxt.js", 5 | "scripts": { 6 | "dev": "nuxt", 7 | "build": "nuxt build", 8 | "start": "nuxt start", 9 | "generate": "nuxt generate", 10 | "lint": "eslint --ext .js,.vue --ignore-path .gitignore .", 11 | "precommit": "npm run lint" 12 | }, 13 | "dependencies": { 14 | "lodash": "^4.17.4", 15 | "nuxt": "1.0.0-rc11", 16 | "vue-i18n": "^7.3.2" 17 | }, 18 | "devDependencies": { 19 | "babel-eslint": "^7.2.3", 20 | "babel-register": "^6.26.0", 21 | "eslint": "^4.3.0", 22 | "eslint-config-standard": "^10.2.1", 23 | "eslint-loader": "^1.9.0", 24 | "eslint-plugin-html": "^3.1.1", 25 | "eslint-plugin-import": "^2.7.0", 26 | "eslint-plugin-node": "^5.1.1", 27 | "eslint-plugin-promise": "^3.5.0", 28 | "eslint-plugin-standard": "^3.0.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /store/index.js: -------------------------------------------------------------------------------- 1 | import Vuex from 'vuex' 2 | import createLogger from 'vuex/dist/logger' 3 | 4 | import i18n from './i18n' 5 | 6 | const debug = process.env.NODE_ENV !== 'production' 7 | 8 | // Common plugins for store 9 | let plugins = [] 10 | if (debug) { 11 | // Dev plugins 12 | const devPlugins = [] 13 | if (process.browser) { 14 | devPlugins.push(createLogger()) 15 | } 16 | plugins = devPlugins 17 | } else { 18 | // Prod plugins 19 | plugins = plugins.concat([]) 20 | } 21 | 22 | const store = () => new Vuex.Store({ 23 | modules: { 24 | i18n 25 | }, 26 | actions: { 27 | /** 28 | * Action triggered on server served pages (first load) 29 | * @param {Function} options.dispatch Dispatch method 30 | * @param {Object} options.req Request object 31 | * @return {void} 32 | */ 33 | // nuxtServerInit ({ commit, dispatch, rootState }, { req, res }) {} 34 | }, 35 | plugins 36 | }) 37 | 38 | export default store 39 | -------------------------------------------------------------------------------- /pages/category/_slug.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 30 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 28 | 29 | 51 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | const { generateRoutes } = require('./utils/router') 2 | 3 | module.exports = { 4 | /* 5 | ** Headers of the page 6 | */ 7 | head: { 8 | title: 'nuxt-i18n-routing', 9 | meta: [ 10 | { charset: 'utf-8' }, 11 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 12 | { hid: 'description', name: 'description', content: 'Localized routing with Nuxt.js' } 13 | ], 14 | link: [ 15 | { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' } 16 | ] 17 | }, 18 | /** 19 | * Router configuration 20 | */ 21 | router: { 22 | middleware: ['i18n'], 23 | extendRoutes (routes) { 24 | const newRoutes = generateRoutes(routes) 25 | routes.splice(0, routes.length) 26 | routes.unshift(...newRoutes) 27 | } 28 | }, 29 | /* 30 | ** Customize the progress bar color 31 | */ 32 | loading: { color: '#3B8070' }, 33 | /* 34 | ** Build configuration 35 | */ 36 | build: { 37 | /* 38 | ** Run ESLint on save 39 | */ 40 | extend (config, ctx) { 41 | if (ctx.dev && ctx.isClient) { 42 | config.module.rules.push({ 43 | enforce: 'pre', 44 | test: /\.(js|vue)$/, 45 | loader: 'eslint-loader', 46 | exclude: /(node_modules)/ 47 | }) 48 | } 49 | } 50 | }, 51 | plugins: [ 52 | { src: '~/plugins/global-mixin.js' }, 53 | { src: '~/plugins/vue-i18n.js', injectAs: 'i18n' } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /middleware/i18n.js: -------------------------------------------------------------------------------- 1 | import { DEFAULT_LOCALE, LOCALES } from '~~/config' 2 | 3 | /** 4 | * i18n middleware - Handles lang switching and redirections to default lang 5 | * Inspired by Nuxt.js i18n example: https://nuxtjs.org/examples/i18n 6 | * @param {Object} options.app Vue app 7 | * @param {Object} options.store Vuex store 8 | * @param {Object} options.route Current route 9 | * @param {Function} options.error Error function 10 | * @param {Function} options.redirect Redirect function 11 | * @param {Boolean} options.hotReload True if middleware was called by hotreload 12 | * @return {void} 13 | */ 14 | export default function ({ app, store, route, error, redirect, hotReload }) { 15 | // Check if middleware called from hot-reloading, ignore 16 | if (hotReload) return 17 | // Get locale from params 18 | let locale = DEFAULT_LOCALE 19 | LOCALES.forEach(l => { 20 | const regexp = new RegExp(`^/${l.code}/`) 21 | if (route.path.match(regexp)) { 22 | locale = l.code 23 | } 24 | }) 25 | if (LOCALES.findIndex(l => l.code === locale) === -1) { 26 | return error({ message: 'Page not found.', statusCode: 404 }) 27 | } 28 | if (locale === store.state.i18n.currentLocale) return 29 | // Set locale 30 | store.dispatch('i18n/setLocale', { locale }) 31 | app.i18n.locale = locale 32 | // If route is //... -> redirect to /... 33 | if (locale === DEFAULT_LOCALE && route.fullPath.indexOf(`/${DEFAULT_LOCALE}`) === 0) { 34 | const regexp = new RegExp(`^/${DEFAULT_LOCALE}/`) 35 | return redirect(route.fullPath.replace(regexp, '/')) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /plugins/global-mixin.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { mapState, mapActions } from 'vuex' 3 | 4 | import { LOCALES } from '~~/config' 5 | 6 | Vue.mixin({ 7 | data: () => ({ 8 | locales: LOCALES 9 | }), 10 | computed: { 11 | ...mapState('i18n', ['currentLocale']) 12 | }, 13 | methods: { 14 | ...mapActions({ 15 | setLocale: 'i18n/setLocale' 16 | }), 17 | getLocalizedRoute (route, locale) { 18 | locale = locale || this.$i18n.locale 19 | // If route parameters is a string, consider it as the route's name 20 | if (typeof route === 'string') { 21 | route = { name: route } 22 | } 23 | // Build localized route options 24 | const baseRoute = Object.assign({}, route, { name: `${route.name}-${locale}` }) 25 | // Resolve localized route 26 | const resolved = this.$router.resolve(baseRoute) 27 | let { href } = resolved 28 | // Handle exception for homepage 29 | if (route.name === 'index') { 30 | href += '/' 31 | } 32 | // Cleanup href 33 | href = (href.match(/^\/\/+$/)) ? '/' : href 34 | return href 35 | }, 36 | getRouteBaseName (route) { 37 | route = route || this.$route 38 | if (!route.name) { 39 | return null 40 | } 41 | for (let i = LOCALES.length - 1; i >= 0; i--) { 42 | const regexp = new RegExp(`-${LOCALES[i].code}$`) 43 | if (route.name.match(regexp)) { 44 | return route.name.replace(regexp, '') 45 | } 46 | } 47 | }, 48 | getSwitchLocaleRoute (locale) { 49 | const name = this.getRouteBaseName() 50 | if (!name) { 51 | return '' 52 | } 53 | const baseRoute = Object.assign({}, this.$route, { name }) 54 | return this.getLocalizedRoute(baseRoute, locale) 55 | } 56 | } 57 | }) 58 | -------------------------------------------------------------------------------- /components/Logo.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 80 | -------------------------------------------------------------------------------- /utils/router.js: -------------------------------------------------------------------------------- 1 | const { has } = require('lodash') 2 | 3 | const { ROUTES_ALIASES, DEFAULT_LOCALE, LOCALES } = require('../config') 4 | 5 | /** 6 | * Generate localized route using Nuxt's generated routes and i18n config 7 | * @param {Array} baseRoutes Nuxt's default routes based on pages/ directory 8 | * @param {Array} locales Locales to use for route generation, should be 9 | * used when recursively generating children routes, 10 | * defaults to app's configured LOCALES 11 | * @return {Array} Localized routes to be used in Nuxt config 12 | */ 13 | function generateRoutes (baseRoutes, locales = []) { 14 | const newRoutes = [] 15 | locales = locales.length ? locales : LOCALES 16 | baseRoutes.forEach((baseRoute) => { 17 | locales.forEach((locale) => { 18 | const { component } = baseRoute 19 | let { path, name, children } = baseRoute 20 | if (children) { 21 | children = generateRoutes(children, [locale]) 22 | } 23 | const { code } = locale 24 | if (has(ROUTES_ALIASES, `${name}.${code}`)) { 25 | path = ROUTES_ALIASES[name][code] 26 | } 27 | if (code !== DEFAULT_LOCALE) { 28 | // Add leading / if needed (ie. children routes) 29 | if (path.match(/^\//) === null) { 30 | path = `/${path}` 31 | } 32 | // Prefix path with locale code if not default locale 33 | path = `/${code}${path}` 34 | } 35 | const route = { path, component } 36 | if (name) { 37 | name += `-${code}` 38 | route.name = name 39 | } 40 | if (children) { 41 | route.children = children 42 | } 43 | newRoutes.push(route) 44 | }) 45 | }) 46 | return newRoutes 47 | }; 48 | 49 | /** 50 | * Make a copy of a route 51 | * @param {Object} route Route to be cloned 52 | * @return {Objet} Route copy 53 | */ 54 | function cloneRoute (route) { 55 | const clonedRoute = Object.assign({}, route) 56 | if (route.meta) { 57 | clonedRoute.meta = Object.assign({}, route.meta) 58 | } 59 | if (route.params) { 60 | clonedRoute.params = Object.assign({}, route.params) 61 | } 62 | if (route.query) { 63 | clonedRoute.query = Object.assign({}, route.query) 64 | } 65 | return clonedRoute 66 | }; 67 | 68 | module.exports = { 69 | generateRoutes, 70 | cloneRoute 71 | } 72 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 30 | 31 | 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nuxt-i18n-routing 2 | 3 | > Localized routing with Nuxt.js 4 | 5 | ## nuxt-i18n module 6 | 7 | Check out [nuxt-i18n](https://github.com/paulgv/nuxt-i18n) module which is based on the work you'll find here. 8 | 9 | ## Info 10 | 11 | This project showcases a way of achieving localized routing with Nuxt.js. 12 | It's not fully tested and you should expect that this might not work for your specific needs. In particular, nested routing has not been tried at all and might require additional work to be properly supported. 13 | 14 | ### Configuration 15 | 16 | In [`~/config/index.js`](config/index.js), we define the i18n configuration: 17 | 18 | - `LOCALES` is an array of languages available in the app 19 | - `DEFAULT_LOCALE` is the app's main language, routes for this language won't have a lang prefix 20 | - `ROUTES_ALIASES` is an object where custom paths can be defined for generated routes, each key should match one of Nuxt's original routes name. Use this if you want to translate some URLs in the app, visit the "About" page to see this in action. 21 | 22 | ### Routes generator 23 | 24 | In order to "localize" the routes, a generator function overrides all the routes that Nuxt generates by reading the `pages/` directory contents. 25 | The generator is imported in `nuxt.config.js` and used in the `extendRoutes` method. 26 | 27 | See the code in [`~/utils/router.js`](utils/router.js). 28 | 29 | ### Store 30 | 31 | A small Vuex store module is used to persist user's locale preference accross pages. 32 | 33 | See the code in [`~/store/i18n/`](store/i18n/). 34 | 35 | ### Plugin 36 | 37 | A simple plugins is used to inialize [vue-i18n](https://github.com/kazupon/vue-i18n) which provides all translation features in the app. 38 | 39 | See the code in [`~/plugins/vue-i18n.js`](plugins/vue-i18n.js). 40 | 41 | ### Middleware 42 | 43 | A middleware, heavily inspired by [Nuxt's i18n example](https://nuxtjs.org/examples/i18n), handles language switching using the language from the URL. 44 | 45 | See the code in [`~/middleware/i18n.js`](middleware/i18n.js). 46 | 47 | ### Mixin 48 | 49 | A global mixin provides 2 methods responsible for generating links in the app: 50 | 51 | - `getLocalizedRoute (route, locale)` – Returns the path corresponding to a given route for the request language. If `route` is a string, it will be used as the route's name. The route's name should correspond to the original name that was generated by Nuxt. Refer to [Nuxt doc](https://nuxtjs.org/guide/routing#basic-routes) to see how it generates routes names. If `locale` is omitted, the app's current locale is used. 52 | - `getSwitchLocaleRoute (locale)` – Returns the URL of the current page for the requested language. 53 | 54 | In Vue components, display links as follows: 55 | 56 | ```vue 57 | 58 | 59 | {{ $t('labels.home') }} 60 | 61 | 62 | 63 | 64 | {{ $t('categories.landscapes') }} 65 | 66 | ``` 67 | 68 | See the code in [`~/plugins/global-mixin.js`](plugins/global-mixin.js). 69 | 70 | ## Build Setup 71 | 72 | ``` bash 73 | # install dependencies 74 | $ yarn 75 | 76 | # serve with hot reload at localhost:3000 77 | $ yarn dev 78 | 79 | # build for production and launch server 80 | $ yarn build 81 | $ yarn start 82 | 83 | # generate static project 84 | $ yarn generate 85 | ``` 86 | 87 | For detailed explanation on how things work, checkout the [Nuxt.js docs](https://github.com/nuxt/nuxt.js). 88 | --------------------------------------------------------------------------------