Next.js(next-intl)では、localeを含まないURLにアクセスした場合に、優先言語で表示が切り替わったり、URLが書き換わりましたが、
Nuxt.js(@nuxtjs/i18n)では、defaultLocaleで設定した言語で表示されたので、
両者の挙動を確認して、求める挙動になるようにpluginを作成しました。(テストも作成)

挙動確認

前提

strategyがprefix_expect_default(prefix_and_defaultでも切り替えの挙動は変わらない)
対応言語は、ja(defaultLocale)とen(fallbackLocale)
と設定(下記のコード)しているので、ブラウザの優先言語を変更します。
= 英語(en)が優先される状態に

ja → en-US → en(Chrome/Firefox日本語デフォルト) ※Safari日本語デフォルトは、jaのみ
 ↓
en → en-US → ja

※ちなみに、3文字以上の場合は先頭2文字で判断されるようにしている事が多いようです。

nuxt.config.ts
import { cookieKey, defaultLocale, locales } from './i18n.config'

export default defineNuxtConfig({
  i18n: {
    strategy: 'prefix_except_default', // https://i18n.nuxtjs.org/guide/routing-strategies
    detectBrowserLanguage: { // NOTE: ブラウザ言語の自動検出ができない -> plugins/setLocale.clientで対応
      useCookie: true,
      cookieKey
    },
    defaultLocale,
    locales,
    vueI18n: './i18n.config.ts'
  },

※「alwaysRedirect: true」を設定してみましたが、挙動が変わらなかったので削除。

i18n.config.ts

cookieKey, defaultLocale, localesは、nuxt.config.tsに書いても問題ないですが、
言語の設定を1ヶ所にまとめたいのと、Vitestから参照しやすいように、ここで定義します。

※localesのnameは、言語切り替えで表示する為に追加しています。
layouts/default.vue のv-selectで使用

import { en } from './locales/en'
import { ja } from './locales/ja'

export const cookieKey = 'locale' // <- 'i18n_redirected'
export const defaultLocale = 'ja'
export const locales = [
  { code: 'en', iso: 'en_US', name: 'English' },
  { code: 'ja', iso: 'ja_JP', name: '日本語' }
]
export const fallbackLocale = 'en'

export default {
  legacy: false,
  fallbackLocale,
  silentFallbackWarn: true,
  messages: {
    en,
    ja
  }
}

Cookieなしの場合

URL Next.js(next-intl) Nuxt.js(@nuxtjs/i18n)
/ 英語, URL: /en, Cookie: en 日本語, URL: 変更なし, Cookie: なし
/en 英語, URL: 変更なし, Cookie: en 英語, URL: 変更なし, Cookie: なし
/ja 日本語, URL: /, Cookie: ja 日本語, URL: 変更なし, Cookie: なし
/users/sign_up?a 英語, URL: /en/users/sign_up?a=, Cookie: en 日本語, URL: 変更なし, Cookie: なし
/en/users/sign_up?a 英語, URL: 変更なし, Cookie: en 英語, URL: 変更なし, Cookie: なし
/ja/users/sign_up?a 日本語, URL: /users/sign_up?a=, Cookie: ja 日本語, URL: 変更なし, Cookie: なし

考察

Next.js(next-intl)は、URLが一意(defaultLocaleのパスは消える)で、必ずCookieがセットされる。
Nuxt.js(@nuxtjs/i18n)は、URLやCookieは変わらない。※CSR/SSR共に挙動は変わらない。
※switchLocalePath(Nuxt)やrouter.replaceでlocale指定(Next)を呼び出すと、URLとCookieが変わる。

Next.js(next-intl)は、優先言語で先に一致したもの
Nuxt.js(@nuxtjs/i18n)は、優先言語からjaを消しても、日本語(defaultLocale)が表示されました。

Cookieありの場合

URL Cookie Next.js(next-intl) Nuxt.js(@nuxtjs/i18n)
/ ja 日本語, URL: 変更なし, Cookie: 変更なし ※Next.jsと同じ
en 英語, URL: /en, Cookie: 変更なし ※Next.jsと同じ
/en ja 英語, URL: 変更なし, Cookie: en 英語, URL: 変更なし, Cookie: 変更なし(ja)
en 英語, URL: 変更なし, Cookie: 変更なし ※Next.jsと同じ
/ja ja 日本語, URL: /, Cookie: 変更なし 日本語, URL: 変更なし, Cookie: 変更なし
en 日本語, URL: /, Cookie: ja 日本語, URL: 変更なし, Cookie: 変更なし(en)

考察

共に、URLにlocaleがある場合は、URLのが優先される。
URLの書き換えとCookieの挙動は、上記(Cookieなし)の場合と変わらない。

Next.js(next-intl)は、URLとCookieの一貫性が保たれる。
Nuxt.js(@nuxtjs/i18n)では、URLにlocaleがない場合はCookie使うので、前回localeありでも、次回はそれ以前に切り替えた言語が表示される。

仕様の違いですが、localePathの付け忘れでも言語が切り替わってしまう。Next.js(next-intl)は、URLが書き換わる。

方針

URLは一意にしたい → defaultLocaleが含まないように書き換える
・Cookieなしの場合は、ブラウザの優先言語で表示言語・URLを切り替えたい

URLを一意にした場合、Cookieと異なると誤った言語が表示されてしまう。
ここだけ対応するのもありですが、仕様に一貫性がなくなってしまうので、Cookieも更新する事にします。
= Next.js(next-intl)の挙動に揃える。

middleware: 遷移時に毎回実行される。
 → パラメータのfromとtoが一致するかで判断できますが。。。
 → layoutsが1つなら、layoutsから呼び出すでも行けそうですが。。。
plugin: 初回アクセス時のみ実行される。
 → 素直にpluginを使います。

plugin実装

Cookieをセットしただけでは、localeは変わらない。
既にCookieを参照してlocaleは決まっている。→ $i18n.locale.valueで取得できる。
$i18n.locale.valueを書き換えても、表示言語は変わらない。
switchLocalePathを呼び出しても変わらない。

location.replaceとlocation.reloadを使う事にしました。
画面表示の不自然な描写(一度、表示されてから書き換わる等)もありませんでした。
Cookieが使えない場合、リダイレクトループするので、一部、localStorageも併用します。

最新のコードはこちら → plugins/setLocale.client.ts

import { cookieKey, locales, defaultLocale, fallbackLocale } from '~/i18n.config'

// ブラウザ言語の自動検出、デフォルト言語の場合はURLから言語を削除
export default defineNuxtPlugin(({ $config }): any => {
  const browserLocales = window.navigator.languages
  const $route = useRoute()
  const fullPath = $route.fullPath.match(/^\/([^/]*)(.*)$/)
  const cookieLocale = useCookie(cookieKey)
  /* c8 ignore next */ // eslint-disable-next-line no-console
  if ($config.public.debug) { console.log('plugins/setLocale.client', browserLocales, fullPath, cookieLocale.value) }

  if (browserLocales == null) {
    /* c8 ignore next */ // eslint-disable-next-line no-console
    if ($config.public.debug) { console.log('...Skip') }
    return
  }

  // URLに言語が含まれる -> Cookie更新、デフォルト言語の場合はURLから言語を削除
  if (fullPath != null) {
    const locale = fullPath[1]
    if (locales.filter((item: any) => item.code === locale).length >= 1) {
      /* c8 ignore next */ // eslint-disable-next-line no-console
      if ($config.public.debug) { console.log('URL: cookieLocale', locale) }

      cookieLocale.value = locale

      if (locale === defaultLocale) {
        const path = fullPath[2] === '' ? '/' : fullPath[2]
        /* c8 ignore next */ // eslint-disable-next-line no-console
        if ($config.public.debug) { console.log('URL: location.replace', path) }

        location.replace(path)
      }
      return locale
    }
  }

  // Cookieあり -> デフォルト言語以外の場合はURLに言語を追加
  if (cookieLocale.value != null) {
    const locale = cookieLocale.value
    if (locales.filter((item: any) => item.code === locale).length >= 1) {
      if (locale !== defaultLocale) {
        const path = $route.fullPath === '/' ? `/${locale}` : `/${locale}${$route.fullPath}`
        /* c8 ignore next */ // eslint-disable-next-line no-console
        if ($config.public.debug) { console.log('Cookie: location.replace', path) }

        location.replace(path)
      }
      return
    }
  }

  // ブラウザの優先言語と一致 -> Cookie更新(ブラウザの優先言語)してリロード
  for (const browserLocale of browserLocales) {
    const locale = browserLocale.slice(0, 2) // NOTE: 先頭2文字で判定
    if (locales.filter((item: any) => item.code === locale).length >= 1) {
      /* c8 ignore next */ // eslint-disable-next-line no-console
      if ($config.public.debug) { console.log('browserLocales: cookieLocale', locale) }

      cookieLocale.value = locale

      const storageKey = 'browser-locale'
      if (localStorage.getItem(storageKey) !== locale) { // NOTE: Cookieが使えない場合にリダイレクトループを防ぐ
        /* c8 ignore next */ // eslint-disable-next-line no-console
        if ($config.public.debug) { console.log('browserLocales: location.reload') }

        localStorage.setItem(storageKey, locale)
        location.reload()
      }
      return locale
    }
  }

  // Cookie更新(fallbackLocale)
  /* c8 ignore next */ // eslint-disable-next-line no-console
  if ($config.public.debug) { console.log('fallbackLocale: cookieLocale', fallbackLocale) }
  cookieLocale.value = fallbackLocale
  return fallbackLocale
})

※ログを入れていますが、location.replaceやlocation.reloadすると前のは無くなってしまう。

Vitestで品質担保

長いので、テストコードの記載は省略します。
最新のコードはこちら → test/plugins/setLocale.client.test.ts

実行結果
 ✓ test/plugins/setLocale.client.test.ts (25)
   ✓ setLocale.client.ts (25)
     ✓ [優先言語がない]undefined
     ✓ 優先言語に対応言語が含まれない (8)
       ✓ URLに言語が含まれない (5)
         ✓ [Cookieの言語がない]fallbackLocale
         ✓ [Cookieの言語が対応言語に存在しない]fallbackLocale
         ✓ testExistsCookie (3)
           ✓ [Cookieの言語がデフォルト言語]undefined
           ✓ [URLがルート。Cookieの言語がデフォルト言語以外]undefined、言語付きURLに書き換えられる
           ✓ [URLにパラメータがある。Cookieの言語がデフォルト言語以外]undefined、言語付きURLに書き換えられる
       ✓ testIncludeURL (3)
         ✓ URLの言語がデフォルト言語 (2)
           ✓ [URLがルート]defaultLocale、言語なしURLに書き換えられる
           ✓ [URLにパラメータがある]defaultLocale、言語なしURLに書き換えられる
         ✓ [URLの言語がデフォルト言語以外]URLの言語
     ✓ 優先言語にデフォルト言語が含まれる (8)
       ✓ URLに言語が含まれない (5)
         ✓ [Cookieの言語がない]優先言語が対応言語と最初に一致した言語、リロードされる
         ✓ [Cookieの言語が対応言語に存在しない]優先言語が対応言語と最初に一致した言語、リロードされる
         ✓ testExistsCookie (3)
           ✓ [Cookieの言語がデフォルト言語]undefined
           ✓ [URLがルート。Cookieの言語がデフォルト言語以外]undefined、言語付きURLに書き換えられる
           ✓ [URLにパラメータがある。Cookieの言語がデフォルト言語以外]undefined、言語付きURLに書き換えられる
       ✓ testIncludeURL (3)
         ✓ URLの言語がデフォルト言語 (2)
           ✓ [URLがルート]defaultLocale、言語なしURLに書き換えられる
           ✓ [URLにパラメータがある]defaultLocale、言語なしURLに書き換えられる
         ✓ [URLの言語がデフォルト言語以外]URLの言語
     ✓ 優先言語にデフォルト言語以外の対応言語が含まれる (8)
       ✓ URLに言語が含まれない (5)
         ✓ [Cookieの言語がない]優先言語が対応言語と最初に一致した言語、リロードされる
         ✓ [Cookieの言語が対応言語に存在しない]優先言語が対応言語と最初に一致した言語、リロードされる
         ✓ testExistsCookie (3)
           ✓ [Cookieの言語がデフォルト言語]undefined
           ✓ [URLがルート。Cookieの言語がデフォルト言語以外]undefined、言語付きURLに書き換えられる
           ✓ [URLにパラメータがある。Cookieの言語がデフォルト言語以外]undefined、言語付きURLに書き換えられる
       ✓ testIncludeURL (3)
         ✓ URLの言語がデフォルト言語 (2)
           ✓ [URLがルート]defaultLocale、言語なしURLに書き換えられる
           ✓ [URLにパラメータがある]defaultLocale、言語なしURLに書き換えられる
         ✓ [URLの言語がデフォルト言語以外]URLの言語

 Test Files  1 passed (1)
      Tests  25 passed (25)
   Start at  21:26:51
   Duration  632ms

言語切り替えselect実装

layouts/default.vue
      <v-select
        v-if="locales.length >= 2"
        v-model="switchLocale"
        :items="locales"
        item-title="name"
        item-value="code"
        density="compact"
        variant="underlined"
        hide-details
        class="ml-1 mr-4 mb-2 switch-locale"
        style="max-width: 94px"
        @update:model-value="navigateTo(switchLocalePath(switchLocale))"
      />

const { locale, locales } = useI18n()
const switchLocalePath = useSwitchLocalePath()
const switchLocale = ref(locale.value)

今回のコミット内容

origin#561 ブラウザの優先言語で表示言語を切り替える
origin#561 デザイン修正、リファクタ

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です