Skip to content

Commit

Permalink
feat!: add useSetI18nParams composable (#2580)
Browse files Browse the repository at this point in the history
* feat: add `useSetI18nParams` composable

* test: test `useI18nParams` composable

* chore: add `useSetI18nParams` to playground

* docs: remove and replace dynamic parameter usage with `useSetI18nParams`

* fix: add `useSetI18nParams` to auto imports

* test: fix dynamic parameters test

* fix: prevent hydration mismatch by not using `useState`

* fix: add deprecation warning for `nuxtI18n` usage

* fix: revert docs and refer to composable

* docs: improve `useSetI18nParams` documentation

* test: improve `useSetI18nParams` test

* chore: cleanup playground

* fix: invert `useSetI18nParams` defaults

* fix: playground products page not changing locale

* fix: use `useLogger` for meta deprecation bundler plugin

* fix: change `useSetI18nParams` parameters
  • Loading branch information
BobbieGoede committed Dec 10, 2023
1 parent 7d5971f commit 898d32a
Show file tree
Hide file tree
Showing 33 changed files with 838 additions and 51 deletions.
3 changes: 1 addition & 2 deletions docs/content/2.guide/19.migrating.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ Use the `customRoutes` option. because the option name `parsePages` is not intui
### Deprecated `vuex` option
Use `dynamicRouteParams` option. Because Vuex is no longer required by the Nuxt i18n module.
Vuex is no longer required by the Nuxt i18n module, use the `useSetI18nParams` composable to set dynamic route parameters instead.
```diff {}[nuxt.config.js]
export default defineNuxtConfig({
Expand All @@ -145,7 +145,6 @@ Use `dynamicRouteParams` option. Because Vuex is no longer required by the Nuxt
i18n: {
// ...
- vuex: true,
+ dynamicRouteParams: true,
// ...
}
})
Expand Down
93 changes: 71 additions & 22 deletions docs/content/2.guide/9.lang-switcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,57 +80,57 @@ const availableLocales = computed(() => {

## Dynamic route parameters

Dealing with dynamic route parameters requires a bit more work because you need to provide parameters translations to **Nuxt i18n module**. For this purpose, **Nuxt i18n module** uses params which are configured by `definePageMeta`. These will be merged with route params when generating lang switch routes with `switchLocalePath()`.
Dealing with dynamic route parameters requires a bit more work because you need to provide parameters translations to **Nuxt i18n module**. The composable `useSetI18nParams` can be used to set translations for route parameters, this is used to set SEO tags as well as changing the routes returned by `switchLocalePath`.

::alert{type="warning"}
During SSR it matters when and where you set i18n parameters, since there is no reactivity during SSR.

You have to set the `dynamicRouteParams` option to `true` in **Nuxt i18n module**'s options to enable dynamic route parameters.
:br :br
Components which have already been rendered do not update by changes introduced by pages and components further down the tree. Instead, these links are updated on the client side, so this should not cause any issues.

:br :br
This is not the case for SEO tags, these are updated correctly during SSR regardless of when and where i18n parameters are set.
::

An example (replace `id` with the applicable route parameter):

An example (replace `slug` with the applicable route parameter):
```vue
<script setup>
definePageMeta({
// ...
nuxtI18n: {
en: { id: 'my-post' },
fr: { id: 'mon-article' }
}
// ...
// fetch product from API... (red mug)
const setI18nParams = useSetI18nParams();
setI18nParams({
en: { slug: data.slugs.en }, // slug: 'red-mug'
nl: { slug: data.slugs.nl } // slug: 'rode-mok'
})
const switchLocalePath = useSwitchLocalePath()
switchLocalePath('en') // /products/red-mug
switchLocalePath('nl') // /nl/products/rode-mok
</script>
<template>
<!-- pages/post/[id].vue -->
<!-- pages/products/[slug].vue -->
</template>
```

Note that for the special case of a catch-all route named like `[...pathMatch.vue]`, the key of the object needs to say `pathMatch`. For example:

```vue
<script>
definePageMeta({
// ...
nuxtI18n: {
en: { pathMatch: ['not-found-my-post'] },
fr: { pathMatch: ['not-found-mon-article'] }
}
// ...
const setI18nParams = useSetI18nParams()
setI18nParams({
en: { pathMatch: ['not-found-my-post'] },
fr: { pathMatch: ['not-found-mon-article'] }
})
</script>
<template>
<!-- pages/[...pathMatch].vue -->
</template>
```

Note that a catch all route is defined **as an array**. In this case, there is only one element, but if you want to use a sub-path, for example `/not-found/post`, define multiple elements as in `['not-found', 'post']`. You will need to define more than one, e.g. `['not-found', 'post']`.


::alert{type="info"}
**Nuxt i18n module** won't reset parameters translations for you, this means that if you use identical parameters for different routes, navigating between those routes might result in conflicting parameters. Make sure you always set params translations in such cases.
::
Expand Down Expand Up @@ -240,3 +240,52 @@ route.meta.pageTransition.onBeforeEnter = async () => {
}
</style>
```
## Dynamic route parameters using `definePageMeta` (Deprecated)
::alert{type="warning"}
Dynamic route params using `nuxtI18n` on `definePageMeta` has been deprecated and will be removed in `v8.1`, use the composable [`useSetI18nParams`](/api/composables#useseti18nparams) instead.
::
Dynamic params can be configured uding `definePageMeta`. These will be merged with route params when generating lang switch routes with `switchLocalePath()`.
::alert{type="warning"}
You have to set the `dynamicRouteParams` option to `true` in **Nuxt i18n module**'s options to enable dynamic route parameters.
::
An example (replace `id` with the applicable route parameter):
```vue
<script setup>
definePageMeta({
// ...
nuxtI18n: {
en: { id: 'my-post' },
fr: { id: 'mon-article' }
}
// ...
})
</script>
<template>
<!-- pages/post/[id].vue -->
</template>
```
Note that for the special case of a catch-all route named like `[...pathMatch.vue]`, the key of the object needs to say `pathMatch`. For example:
```vue
<script>
definePageMeta({
// ...
nuxtI18n: {
en: { pathMatch: ['not-found-my-post'] },
fr: { pathMatch: ['not-found-mon-article'] }
}
// ...
})
</script>
<template>
<!-- pages/[...pathMatch].vue -->
</template>
```
4 changes: 4 additions & 0 deletions docs/content/3.options/2.routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ Set to a path to which you want to redirect users accessing the root URL (`/`).
- type: `boolean`
- default: `false`

::alert{type=warning}
The `dynamicRouteParams` is deprecated and will be removed in `v8.1`, use the [`useSetI18nParams`](/api/composables#useseti18nparams) composable instead.
::

Whether to localize dynamic route parameters.

If `true`, you can set the dynamic route parameter to `nuxtI18n` field of `definePageMeta` for each locale:
Expand Down
44 changes: 44 additions & 0 deletions docs/content/4.API/1.composables.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,50 @@ An object accepting the following optional fields:
Identifier attribute of `<meta>` tag, default `'hid'`.



## `useSetI18nParams`

The `useSetI18nParams` returns a function to set translated parameters for the current route. For more details on its usage see the [Lang Switcher guide](/guide/lang-switcher#dynamic-route-parameters).

Example:
```vue
<script setup>
// fetch product from API... (red mug)
const setI18nParams = useSetI18nParams({
canonicalQueries: ['foo']
});
setI18nParams({
en: { slug: data.slugs.en }, // slug: 'red-mug'
nl: { slug: data.slugs.nl } // slug: 'rode-mok'
})
const switchLocalePath = useSwitchLocalePath()
switchLocalePath('en') // /products/red-mug
switchLocalePath('nl') // /nl/products/rode-mok
</script>
<template>
<!-- pages/products/[slug].vue -->
</template>
```

### Type

```ts
declare function useSetI18nParams(options?: SeoAttributesOptions): (locale: Record<Locale, unknown>) => void;
```

### Parameters

#### `options`

**Type**: `SeoAttributesOptions | undefined`

An `SeoAttributesOptions` object, default `undefined`. See the [SEO guide](/guide/seo#feature-details) for more details.



## `useRouteBaseName`

The `useRouteBaseName` composable returns a function that gets the route's base name. `useRouteBaseName` is powered by [vue-i18n-routing](https://github.com/intlify/routing/tree/main/packages/vue-i18n-routing).
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"@types/debug": "^4.1.9",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"@unhead/vue": "^1.8.8",
"bumpp": "^9.2.0",
"changelogithub": "^0.13.0",
"consola": "^3",
Expand All @@ -130,6 +131,7 @@
"unbuild": "^2.0.0",
"undici": "^6.0.1",
"vitest": "^1.0.0",
"unhead": "^1.8.8",
"vue": "^3.3.4",
"vue-router": "^4.2.5"
},
Expand All @@ -152,4 +154,4 @@
"engines": {
"node": "^14.16.0 || >=16.11.0"
}
}
}
7 changes: 5 additions & 2 deletions playground/layouts/default.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<script setup lang="ts">
import { useSetI18nParams, useLocaleHead } from '#i18n'
import { useHead } from '#imports'
const route = useRoute()
const { t } = useI18n()
const head = useLocaleHead({ addSeoAttributes: true })
const head = useLocaleHead({ addDirAttribute: true, addSeoAttributes: true })
const title = computed(() => t('layouts.title', { title: t(route.meta.title ?? 'TBD') }))
</script>

Expand All @@ -21,7 +24,7 @@ const title = computed(() => t('layouts.title', { title: t(route.meta.title ?? '
<slot />
<div>
<h2>I18n Head</h2>
<code>{{ head }}</code>
<pre>{{ head }}</pre>
</div>
</Body>
</Html>
Expand Down
5 changes: 2 additions & 3 deletions playground/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,8 @@ definePageMeta({
<NuxtLink :to="localePath({ name: 'server' })">Server</NuxtLink> |
<NuxtLink :to="localePath({ name: 'category-id', params: { id: 'foo' } })">Category</NuxtLink> |
<NuxtLinkLocale :to="{ name: 'history' }" class="history-link">History</NuxtLinkLocale> |
<NuxtLinkLocale :to="'/'" locale="ja" activeClass="link-active">Home (Japanese)</NuxtLinkLocale>
<nuxt-link to=""></nuxt-link>
<NuxtLinkLocale to=""></NuxtLinkLocale>
<NuxtLinkLocale :to="'/'" locale="ja" activeClass="link-active">Home (Japanese)</NuxtLinkLocale> |
<NuxtLinkLocale :to="{ name: 'products' }" class="products-link">Products</NuxtLinkLocale>
</nav>
<h2>Current Language: {{ getLocaleName(locale) }}</h2>
<h2>Current Strategy: {{ strategy }}</h2>
Expand Down
43 changes: 43 additions & 0 deletions playground/pages/products.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script lang="ts" setup>
const { locale, locales } = useI18n()
const localePath = useLocalePath()
const switchLocalePath = useSwitchLocalePath()
const { data } = await useAsyncData('products', () => $fetch(`/api/products`))
definePageMeta({
pageTransition: {
name: 'page',
duration: 0,
mode: 'default',
onBeforeEnter: async () => {
const { finalizePendingLocaleChange } = useNuxtApp().$i18n
await finalizePendingLocaleChange()
}
}
})
</script>

<template>
<div>
<nav style="padding: 1em">
<span v-for="locale in locales" :key="locale.code">
<NuxtLink :to="switchLocalePath(locale.code) || ''">{{ locale.name }}</NuxtLink> |
</span>
</nav>
<NuxtLink
class="product"
v-for="product in data"
:key="product.id"
:to="localePath({ name: 'products-slug', params: { slug: product?.slugs?.[locale] ?? 'none' } })"
>
{{ product.name?.[locale] }}
</NuxtLink>
<NuxtPage />
</div>
</template>
<style>
.product {
padding: 1em 0.5em;
}
</style>
34 changes: 34 additions & 0 deletions playground/pages/products/[slug].vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script lang="ts" setup>
import { useI18n, useSetI18nParams } from '#i18n'
import { useRoute } from '#imports'
const { locale } = useI18n()
const route = useRoute()
const setI18nParams = useSetI18nParams()
const { data, pending } = await useAsyncData(`products-${route.params.slug}`, () =>
$fetch(`/api/products/${route.params.slug}`)
)
if (data.value != null) {
const availableLocales = Object.keys(data.value.slugs)
const slugs: Record<string, string> = {}
for (const l of availableLocales) {
slugs[l] = { slug: data.value.slugs[l] }
}
setI18nParams(slugs)
}
</script>

<template>
<div>
<section class="product">{{ pending ? 'loading' : data?.name?.[locale] }}</section>
</div>
</template>

<style>
.product {
padding: 1em 0.5em;
}
</style>
8 changes: 7 additions & 1 deletion playground/server/api/locales/[locale].ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import type { LocaleMessages, DefineLocaleMessage } from 'vue-i18n'

const locales: LocaleMessages<DefineLocaleMessage> = {
'en-GB': {
id: new Date().toISOString(),
settings: {
profile: 'Profile'
}
},
ja: {
id: new Date().toISOString(),
layouts: {
title: 'ページ ー {title}'
},
Expand All @@ -26,8 +28,12 @@ const locales: LocaleMessages<DefineLocaleMessage> = {
}
}

export default defineEventHandler(event => {
export default defineEventHandler(async event => {
const locale = event.context.params?.locale
locales['en-GB'].id = new Date().toISOString()
locales['ja'].id = new Date().toISOString()

await new Promise(resolve => setTimeout(resolve, 5000))
if (locale == null) {
return {}
}
Expand Down

0 comments on commit 898d32a

Please sign in to comment.