Nuxtで言語別ルーティングを実装する

# 前振り： 言語別ルーティング

言語別にディレクトリを切ってコンテンツ表示するサイトをnuxtで実装します。
（コンテンツ内容が違うだけで、使用コンポーネントは同一とします）

- /en/index.html
- /en/posts/index.html
- /en/posts/1/index.html
- /ja/index.html
- /ja/posts/index.html
- /ja/posts/2/index.html

## nuxtでのディレクトリ構成

[ルーティング - Nuxt.js](https://ja.nuxtjs.org/guide/routing/)

- pages/
    - _lang/
        - index.vue
        - posts/
            - index.vue
            - _id.vue

nuxtではpages下に配置したコンポーネントの構成からroutesが作られます。
今回は、langとpostIdが動的となるため、langとidにアンダースコアでプレフィックスをつけることで実装できます。

## 生成されるroutes

```.nuxt/router.js
    routes: [
      {
        path: '/:lang',
        component: _4579e935,
        name: 'lang'
      },
      {
        path: '/:lang/posts',
        component: _14cb8c8e,
        name: 'lang-posts'
      },
      {
        path: '/:lang/posts/:id?',
        component: _08a5533e,
        name: 'lang-posts-id'
      }
    ]
```

ビルドして `.nuxt/router.js` を見るとディレクトリ構造から生成されたroutesを確認できます。
pathに `:` がついた部分が動的ルーティングとなり、params.lang、params.idとして値を受け取って表示を切り分けることができます。

# 本題： デフォルト言語の場合にはそのディレクトリを省略する構成

サイトのデフォルト言語の場合には、ディレクトリを切らずに直下に表示したい、とします。

- /index.html
- /posts/index.html
- /posts/1/index.html
- /ja/index.html
- /ja/posts/index.html
- /ja/posts/2/index.html

（enをデフォルトとした場合）

## nuxtでのディレクトリ構成

- pages/
    - _lang.vue
    - _lang/
        - posts/
            - index.vue
            - _id.vue

今回はlang部分が無いケースが出てくるため、lang部分を省略できるようにlang下にindex.vueを置かず、_lang.vueに変更してみます。

## 生成されるroutes
```.nuxt/router.js
    routes: [
      {
        path: '/:lang?',
        component: _655c5f9c,
        name: 'lang',
        children: [
          {
            path: 'posts',
            component: _14cb8c8e,
            name: 'lang-posts'
          },
          {
            path: 'posts/:id?',
            component: _08a5533e,
            name: 'lang-posts-id'
          }
        ]
      }
    ]
```

index.vueを持たない動的ルーティングのディレクトリは `/:lang?` のように `?` がつくので省略された場合でもマッチするようになります。

ただしこれではうまく動きません。

`/:lang?` だと全てのrouteにマッチしてしまうからです。


## 本当に欲しいroutes

```.nuxt/router.js
    routes: [
      {
        path: '/',
        component: _0ff1b892,
        name: 'index'
      },
      {
        path: '/posts',
        component: _5e83ea8a,
        name: 'posts'
      },
      {
        path: '/posts/:id',
        component: _4f95003a,
        name: 'posts-id'
      },
      {
        path: '/:lang',
        component: _0ff1b892,
        name: 'lang-index'
      },
      {
        path: '/:lang/posts',
        component: _5e83ea8a,
        name: 'lang-posts'
      },
      {
        path: '/:lang/posts/:id',
        component: _4f95003a,
        name: 'lang-posts-id'
      }
    ]
```

最終的なroutesから考えるとこうならないといけません。

# どうやるか

## TL;DR

- langディレクトリを作らない
- extendで動的に生やす
- nuxt-linkを拡張する

## nuxtでのディレクトリ構成

- pages/
    - index.vue
    - posts/
        - index.vue
        - _id.vue

とりあえずlang無しの構成を作ります。

```.nuxt/router.js
    routes: [
      {
        path: '/',
        component: _0ff1b892,
        name: 'index'
      },
      {
        path: '/posts',
        component: _5e83ea8a,
        name: 'posts'
      },
      {
        path: '/posts/:id',
        component: _4f95003a,
        name: 'posts-id'
      }
   ]
```

この段階ではこれだけです。

## extendRoutesで言語別ルーティングを追加

```nuxt.config.js
const makeLangRoutes = (routes, paramName) =>
  routes.map(route => ({
    ...route,
    path: `/:${paramName}${route.path}`.replace(/\/$/, ''),
    name: `${paramName}-${route.name}`
  }))

const conf = {
  router: {
    extendRoutes(routes) {
      routes.push(...makeLangRoutes(routes, 'lang'))
    },
    middleware: ['lang']
  },
  plugins: ['@plugins/langLink.js'],
}

module.exports = conf
```

routerのextendRoutesで現在のroutesをベースにして、pathとnameに paramNameとして 'lang' を加えたrouteを作って追加します。

routes生成についてはこれで対応できます。

## nuxt-linkを拡張して表示中の言語で遷移

routesは作れましたが、言語別でrouteのnameが変わるため、ページ遷移する際にはnuxt-linkに対してnameとparamを適切に指定する必要が出てきます。

（デフォルト言語の場合）

```vue
<template lang="pug">
  nuxt-link(
    :to="{ name:'posts-id', params: {id: post.id, lang} }"
  ) {{post.title}}
```

（デフォルト言語以外の場合）

```vue
<template lang="pug">
  nuxt-link(
    :to="{ name:'lang-posts-id', params: {id: post.id, lang} }"
  ) {{post.title}}
```



いちいち場合分けして入れるのは大変なので、nuxt-linkを拡張して自動的に設定されるようにします。

### middleware


```middleware/lang.js
export default function({ params: { lang }, store }) {
  store.commit('lang', { lang })
}
```

まずはページ遷移時にstoreにparamを保存します。

### store

```store/index.js
const state = () => ({
  lang: null,
  defaultLang: 'en'
})
const mutations = {
  lang(state, { lang }) {
    state.lang = lang || state.defaultLang
  }
}
export default {
  state,
  mutations
}
```

store側ではstateに現在のlangを保持するように作っておきます。

### link component

```components/LangLink.js
const newData = (data, { lang, defaultLang }) => {
  const to = data.attrs.to
  const currentLang = (to && to.params && to.params.lang) || lang
  if (currentLang && currentLang !== defaultLang) {
    to.name = `lang-${to.name}`
  }
  return data
}

export default {
  name: 'lang-link',
  functional: true,
  render(h, {data, children, parent}) {
    return h('nuxt-link', newData(data, parent.$store.state), children)
  }
}
```

nuxt-linkを拡張したコンポーネントです。
storeから現在の言語を取得して、デフォルト言語でなければroute先の名前を変更する処理を行います。
（functional componentからstoreへのアクセスはparent経由で行います）

### plugin

```plugins/langLink.js
import Vue from 'vue'
import LangLink from '@/components/LangLink.js'

Vue.component(LangLink.name, LangLink)
```

上で作ったコンポーネントをグローバルで使えるようにpluginで登録します。

### コンポーネントで使用

```vue
<template lang="pug">
  lang-link(
    :to="{ name:'posts-id', params: {id: post.id} }"
  ) {{post.title}}
```

nuxt-linkだった部分をlang-linkに書き換えます。
これで各コンポーネント側からはlang情報を指定せずに遷移できるようになります。

# 参照

- [Facing difficulties with nuxt routing and i18n - Nuxt Community](https://cmty.app/nuxt/nuxt.js/issues/c1690)
- [Define custom routes on nuxt.config.js · Issue #112 · nuxt/nuxt.js](https://github.com/nuxt/nuxt.js/issues/112#issuecomment-340574312)

# （generate時）

```module.js
    const langs = await api.get('langs')
    const posts = await api.get('posts')
    const langRE = new RegExp(`^${defaultLang}$`)
    const omitLangIfDefault = lang => lang.replace(langRE, '')
    this.options.generate.routes = [
      ...langs.reduce(
        (acc, lang) => [
          ...acc,
          ...['', 'posts'].map(route => ({
            route: `${omitLangIfDefault(lang)}/${route}`
          })),
        ],
        []
      ),
      ...posts.map(post => ({
        route: `${omitLangIfDefault(post.lang)}/posts/${post.id}`
      }))
    ]
```

generateする場合は、APIからコンテンツ取得後にroutesを構築しますが、その際にデフォルト言語であればpathを削るという処理をします。