diff --git a/packages/fastify-vue/context.js b/packages/fastify-vue/context.js
index 596e7be8..530bbe59 100644
--- a/packages/fastify-vue/context.js
+++ b/packages/fastify-vue/context.js
@@ -25,6 +25,8 @@ export default class RouteContext {
// Populated
this.head = {}
this.data = route.data
+ this.key = route.key
+ this.meta = route.meta
this.state = null
// Route settings
this.layout = route.layout
@@ -51,6 +53,8 @@ export default class RouteContext {
state: this.state,
data: this.data,
head: this.head,
+ key: this.key,
+ meta: this.meta,
layout: this.layout,
getMeta: this.getMeta,
getData: this.getData,
diff --git a/packages/fastify-vue/rendering.js b/packages/fastify-vue/rendering.js
index eb1ebb00..21571124 100644
--- a/packages/fastify-vue/rendering.js
+++ b/packages/fastify-vue/rendering.js
@@ -6,7 +6,7 @@ import { createHtmlTemplates } from './templating.js'
export async function createRenderFunction ({ routes, create }) {
// Used when hydrating Vue Router on the client
- const routeMap = Object.fromEntries(routes.map(_ => [_.path, _]))
+ const routeMap = Object.fromEntries(routes.map(_ => [_.key, _]))
// Registered as reply.render()
return function () {
if (this.request.route.streaming) {
diff --git a/packages/fastify-vue/routing.js b/packages/fastify-vue/routing.js
index 04ffdf46..57b5a3f8 100644
--- a/packages/fastify-vue/routing.js
+++ b/packages/fastify-vue/routing.js
@@ -41,7 +41,7 @@ export async function createRoute ({ client, errorHandler, route }, scope, confi
}
// Used when hydrating Vue Router on the client
- const routeMap = Object.fromEntries(client.routes.map(_ => [_.path, _]))
+ const routeMap = Object.fromEntries(client.routes.map(_ => [_.key, _]))
// Extend with route context initialization module
RouteContext.extend(client.context)
@@ -139,7 +139,8 @@ export async function createRoute ({ client, errorHandler, route }, scope, confi
if (route.getData) {
// If getData is provided, register JSON endpoint for it
- scope.get(`/-/data${routePath}`, {
+ const dataPath = (route.dataPath ?? route.path).replace(/:[^+]+\+/, '*')
+ scope.get(`/-/data${dataPath}`, {
onRequest,
async handler (req, reply) {
reply.send(await route.getData(req.route))
diff --git a/packages/fastify-vue/server.js b/packages/fastify-vue/server.js
index 38f2fd9f..d276cb4c 100644
--- a/packages/fastify-vue/server.js
+++ b/packages/fastify-vue/server.js
@@ -6,6 +6,7 @@ class Routes extends Array {
return {
id: route.id,
path: route.path,
+ key: route.key,
name: route.name,
layout: route.layout,
getData: !!route.getData,
@@ -29,6 +30,7 @@ export async function createRoutes (fromPromise, { param } = { param: /\[([.\w]+
id: routeDef.path,
name: routeDef.path ?? routeModule.path,
path: routeDef.path ?? routeModule.path,
+ key: routeDef.path ?? routeModule.path,
...routeModule,
}
}),
@@ -61,11 +63,13 @@ export async function createRoutes (fromPromise, { param } = { param: /\[([.\w]+
.replace(param, (_, m) => `:${m}`)
// Replace '/index' with '/'
.replace(/\/index$/, '/')
- // Remove trailing slashs
+ // Remove trailing slashes
.replace(/(.+)\/+$/, (...m) => m[1]),
...routeModule,
}
+ route.key = route.path
+
if (route.name === '') {
route.name = 'catch-all'
}
diff --git a/packages/fastify-vue/virtual-ts/create.ts b/packages/fastify-vue/virtual-ts/create.ts
index 9e921b37..142d6f1d 100644
--- a/packages/fastify-vue/virtual-ts/create.ts
+++ b/packages/fastify-vue/virtual-ts/create.ts
@@ -4,8 +4,8 @@ import {
createHistory,
serverRouteContext,
routeLayout,
- createBeforeEachHandler,
} from '@fastify/vue/client'
+import { createClientBeforeEachHandler, createServerBeforeEachHandler } from '$app/index.ts'
import { createHead as createClientHead } from '@unhead/vue/client'
import { createHead as createServerHead } from '@unhead/vue/server'
@@ -40,9 +40,12 @@ export default async function create (ctx) {
}
if (isServer) {
+ if (createServerBeforeEachHandler) {
+ router.beforeEach(createServerBeforeEachHandler(ctx))
+ }
instance.provide(serverRouteContext, ctxHydration)
} else {
- router.beforeEach(createBeforeEachHandler(ctx, layoutRef))
+ router.beforeEach(createClientBeforeEachHandler(ctx, layoutRef))
}
instance.use(router)
diff --git a/packages/fastify-vue/virtual-ts/index.ts b/packages/fastify-vue/virtual-ts/index.ts
index 4f15d300..d755a437 100644
--- a/packages/fastify-vue/virtual-ts/index.ts
+++ b/packages/fastify-vue/virtual-ts/index.ts
@@ -1,4 +1,7 @@
import { createRoutes } from '@fastify/vue/server'
+export { createBeforeEachHandler as createClientBeforeEachHandler } from '@fastify/vue/client'
+
+export const createServerBeforeEachHandler = null
export default {
routes: createRoutes(import('$app/routes.ts')),
diff --git a/packages/fastify-vue/virtual-ts/mount.ts b/packages/fastify-vue/virtual-ts/mount.ts
index 7a82360b..8521646d 100644
--- a/packages/fastify-vue/virtual-ts/mount.ts
+++ b/packages/fastify-vue/virtual-ts/mount.ts
@@ -8,7 +8,7 @@ async function mountApp (...targets) {
const ctxHydration = await extendContext(window.route, context)
const resolvedRoutes = await hydrateRoutes(routes)
const routeMap = Object.fromEntries(
- resolvedRoutes.map((route) => [route.path, route]),
+ resolvedRoutes.map((route) => [route.key, route]),
)
const { instance, router } = await create({
ctxHydration,
diff --git a/packages/fastify-vue/virtual/create.js b/packages/fastify-vue/virtual/create.js
index 9e921b37..f91705af 100644
--- a/packages/fastify-vue/virtual/create.js
+++ b/packages/fastify-vue/virtual/create.js
@@ -4,8 +4,8 @@ import {
createHistory,
serverRouteContext,
routeLayout,
- createBeforeEachHandler,
} from '@fastify/vue/client'
+import { createClientBeforeEachHandler, createServerBeforeEachHandler } from '$app/index.js'
import { createHead as createClientHead } from '@unhead/vue/client'
import { createHead as createServerHead } from '@unhead/vue/server'
@@ -40,9 +40,12 @@ export default async function create (ctx) {
}
if (isServer) {
+ if (createServerBeforeEachHandler) {
+ router.beforeEach(createServerBeforeEachHandler(ctx))
+ }
instance.provide(serverRouteContext, ctxHydration)
} else {
- router.beforeEach(createBeforeEachHandler(ctx, layoutRef))
+ router.beforeEach(createClientBeforeEachHandler(ctx, layoutRef))
}
instance.use(router)
diff --git a/packages/fastify-vue/virtual/index.js b/packages/fastify-vue/virtual/index.js
index 05f4dde6..89f829f2 100644
--- a/packages/fastify-vue/virtual/index.js
+++ b/packages/fastify-vue/virtual/index.js
@@ -1,4 +1,7 @@
import { createRoutes } from '@fastify/vue/server'
+export { createBeforeEachHandler as createClientBeforeEachHandler } from '@fastify/vue/client'
+
+export const createServerBeforeEachHandler = null
export default {
routes: createRoutes(import('$app/routes.js')),
diff --git a/packages/fastify-vue/virtual/mount.js b/packages/fastify-vue/virtual/mount.js
index ca88e463..e0c3702b 100644
--- a/packages/fastify-vue/virtual/mount.js
+++ b/packages/fastify-vue/virtual/mount.js
@@ -9,7 +9,7 @@ async function mountApp (...targets) {
const ctxHydration = await extendContext(window.route, context)
const resolvedRoutes = await hydrateRoutes(routes)
const routeMap = Object.fromEntries(
- resolvedRoutes.map((route) => [route.path, route]),
+ resolvedRoutes.map((route) => [route.key, route]),
)
const { instance, router } = await create({
ctxHydration,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ab154f3a..9d84b77a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1324,6 +1324,55 @@ importers:
specifier: ^6.2.4
version: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.3)(yaml@2.7.1)
+ starters/vue-i18n:
+ dependencies:
+ '@fastify/one-line-logger':
+ specifier: ^2.0.2
+ version: 2.0.2
+ '@fastify/vite':
+ specifier: workspace:^
+ version: link:../../packages/fastify-vite
+ '@fastify/vue':
+ specifier: workspace:^
+ version: link:../../packages/fastify-vue
+ '@unhead/vue':
+ specifier: ^2.0.5
+ version: 2.0.8(vue@3.5.13(typescript@5.8.3))
+ fastify:
+ specifier: ^5.3.2
+ version: 5.3.2
+ vue:
+ specifier: ^3.5.13
+ version: 3.5.13(typescript@5.8.3)
+ vue-i18n:
+ specifier: ^11.1.3
+ version: 11.1.3(vue@3.5.13(typescript@5.8.3))
+ vue-router:
+ specifier: ^4.5.0
+ version: 4.5.0(vue@3.5.13(typescript@5.8.3))
+ devDependencies:
+ '@tailwindcss/postcss':
+ specifier: ^4.1.1
+ version: 4.1.2
+ '@tailwindcss/vite':
+ specifier: ^4.1.1
+ version: 4.1.2(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.3)(yaml@2.7.1))
+ '@vitejs/plugin-vue':
+ specifier: ^5.2.3
+ version: 5.2.3(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.3))
+ postcss:
+ specifier: ^8.5.3
+ version: 8.5.3
+ postcss-preset-env:
+ specifier: ^10.1.5
+ version: 10.1.5(postcss@8.5.3)
+ tailwindcss:
+ specifier: ^4.1.1
+ version: 4.1.2
+ vite:
+ specifier: ^6.2.4
+ version: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.3)(yaml@2.7.1)
+
starters/vue-kitchensink:
dependencies:
'@fastify/formbody':
@@ -2592,6 +2641,7 @@ packages:
'@fastify/vite@6.0.7':
resolution: {integrity: sha512-+dRo9KUkvmbqdmBskG02SwigWl06Mwkw8SBDK1zTNH6vd4DyXbRvI7RmJEmBkLouSU81KTzy1+OzwHSffqSD6w==}
+ bundledDependencies: []
'@fastify/vite@8.1.2':
resolution: {integrity: sha512-W/XC2wmDjGwzQGa1SFphn+7w6KlzOYpK69yWAV4T3c3BZb5JcFgrM/f/ZRQpQ4kgYZfNs5UbfC6CA5Zccw2xtw==}
@@ -2621,6 +2671,18 @@ packages:
resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==}
engines: {node: '>=18.18'}
+ '@intlify/core-base@11.1.3':
+ resolution: {integrity: sha512-cMuHunYO7LE80azTitcvEbs1KJmtd6g7I5pxlApV3Jo547zdO3h31/0uXpqHc+Y3RKt1wo2y68RGSx77Z1klyA==}
+ engines: {node: '>= 16'}
+
+ '@intlify/message-compiler@11.1.3':
+ resolution: {integrity: sha512-7rbqqpo2f5+tIcwZTAG/Ooy9C8NDVwfDkvSeDPWUPQW+Dyzfw2o9H103N5lKBxO7wxX9dgCDjQ8Umz73uYw3hw==}
+ engines: {node: '>= 16'}
+
+ '@intlify/shared@11.1.3':
+ resolution: {integrity: sha512-pTFBgqa/99JRA2H1qfyqv97MKWJrYngXBA/I0elZcYxvJgcCw3mApAoPW3mJ7vx3j+Ti0FyKUFZ4hWxdjKaxvA==}
+ engines: {node: '>= 16'}
+
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
@@ -7347,6 +7409,12 @@ packages:
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
+ vue-i18n@11.1.3:
+ resolution: {integrity: sha512-Pcylh9z9S5+CJAqgbRZ3EKxFIBIrtY5YUppU722GIT65+Nukm0TCqiQegZnNLCZkXGthxe0cpqj0AoM51H+6Gw==}
+ engines: {node: '>= 16'}
+ peerDependencies:
+ vue: ^3.0.0
+
vue-router@4.5.0:
resolution: {integrity: sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==}
peerDependencies:
@@ -8706,6 +8774,18 @@ snapshots:
'@humanwhocodes/retry@0.4.2': {}
+ '@intlify/core-base@11.1.3':
+ dependencies:
+ '@intlify/message-compiler': 11.1.3
+ '@intlify/shared': 11.1.3
+
+ '@intlify/message-compiler@11.1.3':
+ dependencies:
+ '@intlify/shared': 11.1.3
+ source-map-js: 1.2.1
+
+ '@intlify/shared@11.1.3': {}
+
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
@@ -13863,6 +13943,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ vue-i18n@11.1.3(vue@3.5.13(typescript@5.8.3)):
+ dependencies:
+ '@intlify/core-base': 11.1.3
+ '@intlify/shared': 11.1.3
+ '@vue/devtools-api': 6.6.4
+ vue: 3.5.13(typescript@5.8.3)
+
vue-router@4.5.0(vue@3.5.13(typescript@5.8.3)):
dependencies:
'@vue/devtools-api': 6.6.4
diff --git a/starters/vue-i18n/.eslintignore b/starters/vue-i18n/.eslintignore
new file mode 100644
index 00000000..53c37a16
--- /dev/null
+++ b/starters/vue-i18n/.eslintignore
@@ -0,0 +1 @@
+dist
\ No newline at end of file
diff --git a/starters/vue-i18n/.eslintrc b/starters/vue-i18n/.eslintrc
new file mode 100644
index 00000000..be9e0ffa
--- /dev/null
+++ b/starters/vue-i18n/.eslintrc
@@ -0,0 +1,31 @@
+{
+ parser: '@babel/eslint-parser',
+ parserOptions: {
+ requireConfigFile: false,
+ ecmaVersion: 2021,
+ sourceType: 'module',
+ babelOptions: {
+ presets: ['@babel/preset-react'],
+ },
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ extends: [
+ 'plugin:react/recommended',
+ 'standard',
+ ],
+ plugins: [
+ 'react',
+ ],
+ rules: {
+ 'comma-dangle': ['error', 'always-multiline'],
+ 'react/prop-types': 'off',
+ 'import/no-absolute-path': 'off',
+ },
+ settings: {
+ react: {
+ version: '18.0',
+ },
+ },
+}
diff --git a/starters/vue-i18n/README.md b/starters/vue-i18n/README.md
new file mode 100644
index 00000000..3e242159
--- /dev/null
+++ b/starters/vue-i18n/README.md
@@ -0,0 +1,3 @@
+
+
+The official **[@fastify/vue](https://github.com/fastify/fastify-vite/tree/dev/packages/fastify-vue)** i18n starter template.
diff --git a/starters/vue-i18n/client/assets/logo.svg b/starters/vue-i18n/client/assets/logo.svg
new file mode 100644
index 00000000..39c9396a
--- /dev/null
+++ b/starters/vue-i18n/client/assets/logo.svg
@@ -0,0 +1,31 @@
+
\ No newline at end of file
diff --git a/starters/vue-i18n/client/base.css b/starters/vue-i18n/client/base.css
new file mode 100644
index 00000000..6519f04e
--- /dev/null
+++ b/starters/vue-i18n/client/base.css
@@ -0,0 +1,58 @@
+@import 'tailwindcss';
+
+:root {
+ --color-base: #f1f1f1;
+ --color-highlight: #ff80ff;
+}
+html {
+ background: #222;
+}
+#root {
+ width: 800px;
+ margin: 0 auto;
+ padding: 2em;
+ box-shadow: 5px 5px 30px rgba(0,0,0,0.4);
+ border-radius: 10px;
+ background-color: rgba(255, 255, 255, 0.1);
+ font-family: Avenir, Helvetica, Arial, sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ color: var(--color-base);
+ margin-top: 60px;
+ & a {
+ color: var(--color-highlight);
+ text-decoration: none;
+ font-weight: bold;
+ border-bottom: 1px solid var(--color-highlight);
+ &:hover {
+ color: #ffde00;
+ }
+ &:active {
+ color: #eecf00
+ }
+ }
+ & p {
+ font-size: 1.2em;
+ }
+ & ul {
+ & li {
+ &:not(:last-child) {
+ margin-bottom: 0.5em;
+ }
+ break-inside: avoid;
+ font-size: 1em;
+ }
+ }
+ & code {
+ color: #ffde00;
+ font-weight: bold;
+ font-family: 'Consolas', 'Andale Mono', monospace;
+ font-size: 0.9em;
+ }
+ & img {
+ width: 14em;
+ }
+ & button {
+ margin: 0 0.5em;
+ }
+}
diff --git a/starters/vue-i18n/client/composables/i18n.js b/starters/vue-i18n/client/composables/i18n.js
new file mode 100644
index 00000000..86e34083
--- /dev/null
+++ b/starters/vue-i18n/client/composables/i18n.js
@@ -0,0 +1,25 @@
+import { useRoute, useRouter } from 'vue-router';
+
+export function useI18nUtils() {
+ const router = useRouter();
+ const routes = router.getRoutes();
+ const route = useRoute();
+
+ const localePath = (path) => {
+ if ('name' in path) {
+ const nameWithLocalePrefix = `${route.meta.locale}__${path.name}`;
+ for (const route of routes) {
+ if (route.name === nameWithLocalePrefix) {
+ path.name = nameWithLocalePrefix;
+ break;
+ }
+ }
+ }
+
+ return path;
+ };
+
+ return {
+ localePath,
+ };
+}
diff --git a/starters/vue-i18n/client/context.js b/starters/vue-i18n/client/context.js
new file mode 100644
index 00000000..c7238c28
--- /dev/null
+++ b/starters/vue-i18n/client/context.js
@@ -0,0 +1,17 @@
+// The default export function runs exactly once on
+// the server and once on the client during the
+// first render, that is, it's not executed again
+// in subsequent client-side navigation.
+export default async (ctx) => {
+ // Set default params here for fetch/axios or similar XHR library
+ ctx.state.locale = ctx.meta.locale
+}
+
+// State initializer, must be a function called state
+// as this is a special context.js export and has
+// special processing, e.g., serialization and hydration
+export function state() {
+ return {
+ locale: 'sv',
+ }
+}
diff --git a/starters/vue-i18n/client/i18n.config.js b/starters/vue-i18n/client/i18n.config.js
new file mode 100644
index 00000000..07ec1586
--- /dev/null
+++ b/starters/vue-i18n/client/i18n.config.js
@@ -0,0 +1,5 @@
+export default {
+ locales: ['en', 'sv', 'da'], // The first locale is the default
+ localePrefix: true,
+ localeDomains: {},
+}
\ No newline at end of file
diff --git a/starters/vue-i18n/client/i18n.js b/starters/vue-i18n/client/i18n.js
new file mode 100644
index 00000000..59ffa66e
--- /dev/null
+++ b/starters/vue-i18n/client/i18n.js
@@ -0,0 +1,26 @@
+import { createI18n } from 'vue-i18n'
+
+const i18nConfig = createI18n({
+ locale: 'en',
+ legacy: false,
+ fallbackLocale: 'en',
+ messages: {
+ sv: {
+ message: {
+ welcome: "Välkommen till {'@'}fastify/vue!",
+ },
+ },
+ en: {
+ message: {
+ welcome: "Welcome to {'@'}fastify/vue!",
+ },
+ },
+ da: {
+ message: {
+ welcome: "Velkommen til {'@'}fastify/vue!",
+ },
+ },
+ },
+})
+
+export const i18n = i18nConfig
\ No newline at end of file
diff --git a/starters/vue-i18n/client/index.html b/starters/vue-i18n/client/index.html
new file mode 100644
index 00000000..c9155b1a
--- /dev/null
+++ b/starters/vue-i18n/client/index.html
@@ -0,0 +1,13 @@
+
+
+
/wildcard/*
+ Path match: {{ data.pathMatch.join(', ') }}
+Locale: {{ data.locale }}
+
+