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)