Skip to content

Commit

Permalink
feat: experimental switchLocalePathLink SSR component
Browse files Browse the repository at this point in the history
  • Loading branch information
BobbieGoede committed Mar 14, 2024
1 parent fc6fb55 commit 30cc704
Show file tree
Hide file tree
Showing 12 changed files with 68 additions and 40 deletions.
22 changes: 1 addition & 21 deletions playground/layouts/default.vue
Original file line number Diff line number Diff line change
@@ -1,24 +1,10 @@
<script setup lang="ts">
import { useSetI18nParams, useLocaleHead } from '#i18n'
import { useHead } from '#imports'
import SwitchLocalePathLink from '../../src/runtime/components/SwitchLocalePathLink'
import { useLocaleHead } from '#i18n'
const route = useRoute()
const { t } = useI18n()
const head = useLocaleHead({ addDirAttribute: true, addSeoAttributes: true })
const title = computed(() => t('layouts.title', { title: t(route.meta.title ?? 'TBD') }))
const nuxt = useNuxtApp()
const switchLocalePath = useSwitchLocalePath()
nuxt.hook('app:rendered', ctx => {
ctx.renderResult!.html = ctx.renderResult!.html.replaceAll(
/<!--nuxt-i18n-swlp-->(.+)<!--nuxt-i18n-swlp-end-->/g,
(substr: string) => {
const matchedLocale = /data-nuxt-i18n-swlp="([^"]+)"/.exec(substr)?.[0]
return substr.replace(/href="([^"]+)"/, `href="${switchLocalePath(matchedLocale ?? '')}"`)
}
)
// console.log(ctx.renderResult!.html.replace())
})
</script>

<template>
Expand All @@ -35,12 +21,6 @@ nuxt.hook('app:rendered', ctx => {
</Head>
<Body>
<slot />

<div>
<SwitchLocalePathLink locale="nl">To Dutch!</SwitchLocalePathLink>
<SwitchLocalePathLink locale="en">To English!</SwitchLocalePathLink>
<SwitchLocalePathLink locale="fr">To French!</SwitchLocalePathLink>
</div>
<div>
<h2>I18n Head</h2>
<pre>{{ head }}</pre>
Expand Down
3 changes: 2 additions & 1 deletion playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ export default defineNuxtConfig({
// debug: true,
i18n: {
experimental: {
localeDetector: './localeDetector.ts'
localeDetector: './localeDetector.ts',
switchLocalePathLinkSSR: true
},
compilation: {
// jit: false,
Expand Down
3 changes: 1 addition & 2 deletions playground/pages/products.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<script lang="ts" setup>
const { locale, locales } = useI18n()
const localePath = useLocalePath()
const switchLocalePath = useSwitchLocalePath()
const { data } = await useAsyncData('products', () => $fetch(`/api/products`))
definePageMeta({
Expand All @@ -21,7 +20,7 @@ definePageMeta({
<div>
<nav style="padding: 1em">
<span v-for="locale in locales" :key="locale.code">
<NuxtLink :to="switchLocalePath(locale.code) || ''">{{ locale.name }}</NuxtLink> |
<SwitchLocalePathLink :locale="locale.code">{{ locale.name }}</SwitchLocalePathLink> |
</span>
</nav>
<NuxtLink
Expand Down
12 changes: 12 additions & 0 deletions specs/fixtures/basic_usage/components/LangSwitcher.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ const localesExcludingCurrent = computed(() => {
>{{ locale.name }}</NuxtLink
>
</section>
<section id="lang-switcher-with-switch-locale-path-link">
<strong>Using <code>SwitchLocalePathLink</code></strong
>:
<SwitchLocalePathLink
v-for="(locale, index) in localesExcludingCurrent"
:id="`nuxt-locale-link-${locale.code}`"
:key="index"
:exact="true"
:locale="locale.code"
>{{ locale.name }}</SwitchLocalePathLink
>
</section>
<section id="lang-switcher-with-set-locale">
<strong>Using <code>setLocale()</code></strong
>:
Expand Down
3 changes: 2 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export const DEFAULT_COOKIE_KEY = 'i18n_redirected'

export const DEFAULT_OPTIONS = {
experimental: {
localeDetector: ''
localeDetector: '',
switchLocalePathLinkSSR: false
},
bundle: {
compositionOnly: true,
Expand Down
9 changes: 8 additions & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ export default defineNuxtModule<NuxtI18nOptions>({
},
{} as Record<string, { domain: string | undefined }>
),
detectBrowserLanguage: options.detectBrowserLanguage ?? DEFAULT_OPTIONS.detectBrowserLanguage
detectBrowserLanguage: options.detectBrowserLanguage ?? DEFAULT_OPTIONS.detectBrowserLanguage,
experimental: options.experimental
// TODO: we should support more i18n module options. welcome PRs :-)
})

Expand Down Expand Up @@ -361,6 +362,12 @@ export interface ModulePublicRuntimeConfig {
baseUrl: NuxtI18nOptions['baseUrl']
rootRedirect: NuxtI18nOptions['rootRedirect']

/**
* Overwritten at build time, used to pass generated options to runtime
*
* @internal
*/
experimental: NonNullable<NuxtI18nOptions['experimental']>
/**
* Overwritten at build time, used to pass generated options to runtime
*
Expand Down
19 changes: 9 additions & 10 deletions src/runtime/components/SwitchLocalePathLink.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { useSwitchLocalePath } from '#i18n'
import { Fragment, defineComponent, h } from 'vue'
import { Comment, defineComponent, h } from 'vue'
import { defineNuxtLink } from '#imports'

import type { PropType } from 'vue'
import { Comment } from 'vue'

const NuxtLinkLocale = defineNuxtLink({ componentName: 'NuxtLinkLocale' })
const NuxtLink = defineNuxtLink({ componentName: 'NuxtLink' })

export default defineComponent({
name: 'SwitchLocalePathLink',
Expand All @@ -15,14 +14,14 @@ export default defineComponent({
required: true
}
},
setup(props, { slots }) {
inheritAttrs: false,
setup(props, { slots, attrs }) {
const switchLocalePath = useSwitchLocalePath()

return () =>
h(Fragment, [
h(Comment, 'nuxt-i18n-swlp'),
h(NuxtLinkLocale, { to: switchLocalePath(props.locale), 'data-nuxt-i18n-swlp': props.locale }, slots.default),
h(Comment, 'nuxt-i18n-swlp-end')
])
return () => [
h(Comment, 'nuxt-i18n-swlp'),
h(NuxtLink, { ...attrs, to: switchLocalePath(props.locale), 'data-nuxt-i18n-swlp': props.locale }, slots.default),
h(Comment, '/nuxt-i18n-swlp')
]
}
})
6 changes: 4 additions & 2 deletions src/runtime/composables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,14 @@ export function useSetI18nParams(seoAttributes?: SeoAttributesOptions): SetI18nP
const locale = getLocale(i18n)
const locales = getNormalizedLocales(getLocales(i18n))
const _i18nParams = ref({})
const experimentalSSR = common.runtimeConfig.public.i18n.experimental.switchLocalePathLinkSSR

const i18nParams = computed({
get() {
return router.currentRoute.value.meta.nuxtI18n ?? {}
return experimentalSSR ? common.metaState.value : router.currentRoute.value.meta.nuxtI18n ?? {}
},
set(val) {
common.metaState.value = val
_i18nParams.value = val
router.currentRoute.value.meta.nuxtI18n = val
}
Expand All @@ -61,7 +63,7 @@ export function useSetI18nParams(seoAttributes?: SeoAttributesOptions): SetI18nP
const stop = watch(
() => router.currentRoute.value.fullPath,
() => {
router.currentRoute.value.meta.nuxtI18n = _i18nParams.value
router.currentRoute.value.meta.nuxtI18n = experimentalSSR ? common.metaState.value : _i18nParams.value
}
)

Expand Down
18 changes: 18 additions & 0 deletions src/runtime/plugins/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
normalizedLocales
} from '#build/i18n.options.mjs'
import { loadVueI18nOptions, loadInitialMessages, loadLocale } from '../messages'
import { useSwitchLocalePath } from '../composables'
import {
loadAndSetLocale,
detectLocale,
Expand Down Expand Up @@ -391,6 +392,23 @@ export default defineNuxtPlugin({
// inject for nuxt helpers
injectNuxtHelpers(nuxtContext, i18n)

// Replace `SwitchLocalePathLink` href in rendered html for SSR support
if (runtimeI18n.experimental.switchLocalePathLinkSSR === true) {
const switchLocalePath = useSwitchLocalePath()
nuxt.hook('app:rendered', ctx => {
if (ctx.renderResult?.html == null) return

const localeMatcherExpr = new RegExp(`data-nuxt-i18n-swlp="(${localeCodes.join('|')})"`)
ctx.renderResult.html = ctx.renderResult.html.replaceAll(
/<!--nuxt-i18n-swlp-->(.+?)<!--\/nuxt-i18n-swlp-->/g,
(substr: string) => {
const matchedLocale = localeMatcherExpr.exec(substr)?.at(1)
return substr.replace(/href="([^"]+)"/, `href="${switchLocalePath(matchedLocale ?? '')}"`)
}
)
})
}

let routeChangeCount = 0

addRouteMiddleware(
Expand Down
7 changes: 6 additions & 1 deletion src/runtime/routing/compatibles/routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,13 @@ export function resolveRoute(common: CommonComposableOptions, route: RouteLocati
export const DefaultSwitchLocalePathIntercepter: SwitchLocalePathIntercepter = (path: string) => path

function getLocalizableMetaFromDynamicParams(
common: CommonComposableOptions,
route: RouteLocationNormalizedLoaded
): Record<Locale, Record<string, unknown>> {
if (common.runtimeConfig.public.i18n.experimental.switchLocalePathLinkSSR) {
return unref(common.metaState.value) as Record<Locale, any>
}

const meta = route.meta || {}
return (unref(meta)?.[DEFAULT_DYNAMIC_PARAMS_KEY] || {}) as Record<Locale, any>
}
Expand Down Expand Up @@ -247,7 +252,7 @@ export function switchLocalePath(

const switchLocalePathIntercepter = extendSwitchLocalePathIntercepter(common.runtimeConfig)
const routeCopy = routeToObject(route)
const resolvedParams = getLocalizableMetaFromDynamicParams(route)[locale]
const resolvedParams = getLocalizableMetaFromDynamicParams(common, route)[locale]

const baseRoute = { ...routeCopy, name, params: { ...routeCopy.params, ...resolvedParams } }
const path = localePath(common, baseRoute, locale)
Expand Down
5 changes: 4 additions & 1 deletion src/runtime/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { getLocale, setLocale, getLocaleCodes, getI18nTarget } from './routing/u

import type { I18n, Locale, FallbackLocale, Composer, VueI18n } from 'vue-i18n'
import type { NuxtApp } from '#app'
import type { Ref } from '#imports'
import type { Router } from '#vue-router'
import type { DetectLocaleContext } from './internal'
import type { HeadSafe } from '@unhead/vue'
Expand Down Expand Up @@ -90,12 +91,14 @@ export type CommonComposableOptions = {
router: Router
i18n: I18n
runtimeConfig: RuntimeConfig
metaState: Ref<Record<Locale, any>>
}
export function initCommonComposableOptions(i18n?: I18n): CommonComposableOptions {
return {
i18n: i18n ?? useNuxtApp().$i18n,
router: useRouter(),
runtimeConfig: useRuntimeConfig()
runtimeConfig: useRuntimeConfig(),
metaState: useState<Record<Locale, any>>('nuxt-i18n-meta', () => ({}))
}
}

Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export type CustomRoutePages = {

export interface ExperimentalFeatures {
localeDetector?: string
switchLocalePathLinkSSR?: boolean
}

export interface BundleOptions
Expand Down

0 comments on commit 30cc704

Please sign in to comment.