4 Common Routing Traps When Migrating to Nuxt i18n
If you're adding translations to your NuxtJS app using @nuxtjs/i18n, you probably expect it to be a simple config change. But there is a massive gotcha that caught me totally off guard while translating neoncv.ai: your URLs are no longer static.
Depending on your i18n routing strategy (like prefix_except_default or prefix), that predictable /dashboard route dynamically shifts to /fr/dashboard, /ja/dashboard, or /es/dashboard.
This one detail shatters standard Vue routing logic relying on exact path strings. After untangling broken links and missing transitions, I realized most tutorials skip the painful part: retrofitting a large, existing codebase.
Here are 4 architectural traps you will hit, and the senior-level patterns you should use instead.
Trap 1: Brittle Path Checking
It's common to check if a user is inside a specific section of the app to trigger middleware, animations, or load third-party scripts (like a support chat widget only on marketing pages).
The Mistake:
const route = useRoute()
// ❌ Breaks instantly on localized paths like /fr/tracker
const isTrackerChild = route.path.startsWith('/tracker/')
// ❌ A fragile string-sniffing workaround
const routeName = String(route.name || '')
const isTrackerChildWorkaround = routeName.startsWith('tracker-')
Relying on route.path fails because the path changes with locales. Switching to route.name.startsWith() is slightly better, but replaces one brittle hack with another. What if someone renames tracker/[id].vue to tracker/detail-[id].vue? Or what if tracker-settings.vue accidentally triggers it?
The Senior Fix: Route Meta
Use Vue Router's native route.meta. It's immune to locale prefixes, renaming, and string sniffing.
Define it in your pages:
// pages/tracker/[id].vue
definePageMeta({
section: 'tracker',
requiresAuth: true,
loadSupportChat: false
})
Check it in your middleware or computed properties:
// middleware/auth.ts or chat widget logic
const route = useRoute()
const isTrackerChild = route.meta.section === 'tracker'
Trap 2: Context-Blind Navigation
If you have custom "go back" buttons or logic that redirects users, you likely have navigation calls scattered everywhere.
The Mistake:
const router = useRouter()
// ❌ Kicks the user out of their localized environment
function goBack() {
router.push('/pricing')
}
If a user from Japan reading your pricing page triggers this, they are instantly kicked back to the default English route.
The Fix: navigateTo and <NuxtLinkLocale>
For programmatic navigation, wrap the path in useLocalePath() and use Nuxt's navigateTo (which safely handles SSR context and cross-environment navigation, unlike raw router.push).
const localePath = useLocalePath()
// ✅ Safely navigates while preserving locale and SSR context
async function goBack() {
await navigateTo(localePath('/pricing'))
}
For template navigation, stop hardcoding <NuxtLink to="/pricing">. Use the built-in <NuxtLinkLocale> component instead. It automatically wraps your paths in localePath() under the hood.
<!-- ✅ Automatically resolves to /fr/pricing for French users -->
<NuxtLinkLocale to="/pricing">Pricing</NuxtLinkLocale>
Trap 3: Reinventing Active Link States
When building a custom sidebar or navigation menu, you need to highlight the active route.
The Mistake: Manually hacking active link detection by comparing strings.
// ❌ Custom logic that fails on nested routes and edge cases
const isActive = (path) => route.path.startsWith(localePath(path))
This gets messy incredibly fast, and edge cases (like /tracker matching /tracker-archive due to .startsWith()) will inevitably cause bugs.
The Fix: Native Router Link Resolution
Don't reinvent the wheel. <NuxtLinkLocale> natively applies standard router-link-active and router-link-exact-active classes automatically.
If you must build a highly custom wrapper component that can't use <NuxtLinkLocale> directly, leverage Vue Router's useLink with the localized path to tap into its robust resolution engine.
const localePath = useLocalePath()
// Leverage Vue Router's bulletproof logic
const { isActive, isExactActive } = useLink({ to: localePath('/dashboard') })
Trap 4: Locale Switching & Query Preservation
Adding a language toggle seems easy until your users try it mid-session on a dynamic route or a page with active filters.
The Mistake:
<!-- ❌ Loses route parameters and query strings -->
<a href="/fr/search">Français</a>
If a user is on /search?q=nuxt and clicks your language toggle, redirecting them strictly to /fr/search wipes out their search query. The same happens on dynamic routes like /users/123.
The Fix: switchLocalePath()
@nuxtjs/i18n provides a switchLocalePath() composable specifically for this. It takes the current route context (including dynamic parameters and query strings) and safely translates it to the target language.
<script setup>
const switchLocalePath = useSwitchLocalePath()
</script>
<template>
<div>
<!-- ✅ Preserves /search?q=nuxt -> /fr/search?q=nuxt -->
<NuxtLink :to="switchLocalePath('fr')">
Français
</NuxtLink>
</div>
</template>
Wrapping up
Integrating @nuxtjs/i18n into a production codebase taught me these vital rules:
- Ditch
route.pathchecking in favor ofroute.meta. - Replace hardcoded
router.push()withnavigateTo(localePath())and<NuxtLinkLocale>. - Let Vue Router handle active link states.
- Always use
switchLocalePath()for language toggles.
Adopt these architectural patterns early, and you'll save yourself massive headaches taking your app global.
Bonus: The AI Refactoring Prompt
If you are retrofitting a large, existing codebase and don't want to hunt down every single router.push and route.path manually, drop this prompt into your AI coding assistant (like GitHub Copilot, Cursor, or Gemini):
"I am migrating my Nuxt 3 application to
@nuxtjs/i18n. Scan mypages,components, andmiddlewaredirectories for Nuxt routing anti-patterns.Specifically, look for:
- Hardcoded string checks against
route.pathorroute.name.- Direct usage of
router.push()ornavigateTo()with static strings.- Standard
<NuxtLink>components or manual active link logic.Refactor them using Nuxt i18n best practices:
- Replace route path checking with
route.meta.- Wrap programmatic navigation paths in
localePath().- Replace standard
<NuxtLink>with<NuxtLinkLocale>.- Use
switchLocalePath()for language toggles."
Let the AI do the heavy lifting, and enjoy your newly localized app!
