├── .npmrc ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml ├── workflows │ ├── github-label-sync.yml │ ├── release.yml │ └── ci.yml ├── PULL_REQUEST_TEMPLATE.md ├── release.yml └── labels.yml ├── .gitattributes ├── pnpm-workspace.yaml ├── .eslintignore ├── .githooks └── pre-commit ├── .prettierignore ├── playground ├── vue3 │ ├── env.d.ts │ ├── public │ │ └── favicon.ico │ ├── README.md │ ├── tsconfig.config.json │ ├── vitest.config.ts │ ├── src │ │ ├── i18n.ts │ │ ├── assets │ │ │ ├── logo.svg │ │ │ ├── main.css │ │ │ └── base.css │ │ ├── components │ │ │ ├── icons │ │ │ │ ├── IconSupport.vue │ │ │ │ ├── IconTooling.vue │ │ │ │ ├── IconCommunity.vue │ │ │ │ ├── IconDocumentation.vue │ │ │ │ └── IconEcosystem.vue │ │ │ ├── HelloWorld.vue │ │ │ ├── WelcomeItem.vue │ │ │ └── TheWelcome.vue │ │ ├── locales │ │ │ ├── schema.d.ts │ │ │ ├── ja.json │ │ │ └── en.json │ │ ├── main.ts │ │ ├── views │ │ │ ├── AboutView.vue │ │ │ └── HomeView.vue │ │ ├── router │ │ │ └── index.ts │ │ └── App.vue │ ├── tsconfig.json │ ├── index.html │ ├── vite.config.ts │ ├── package.json │ └── specs │ │ └── app.spec.ts ├── vue3-options │ ├── env.d.ts │ ├── README.md │ ├── public │ │ └── favicon.ico │ ├── tsconfig.config.json │ ├── vitest.config.ts │ ├── src │ │ ├── i18n.ts │ │ ├── assets │ │ │ ├── logo.svg │ │ │ ├── main.css │ │ │ └── base.css │ │ ├── components │ │ │ ├── icons │ │ │ │ ├── IconSupport.vue │ │ │ │ ├── IconTooling.vue │ │ │ │ ├── IconCommunity.vue │ │ │ │ ├── IconDocumentation.vue │ │ │ │ └── IconEcosystem.vue │ │ │ ├── HelloWorld.vue │ │ │ └── WelcomeItem.vue │ │ ├── locales │ │ │ ├── schema.d.ts │ │ │ ├── ja.json │ │ │ └── en.json │ │ ├── main.ts │ │ ├── views │ │ │ ├── HomeView.vue │ │ │ └── AboutView.vue │ │ ├── router │ │ │ └── index.ts │ │ └── App.vue │ ├── index.html │ ├── tsconfig.json │ ├── vite.config.ts │ ├── package.json │ └── specs │ │ └── app.spec.ts ├── vue2 │ ├── public │ │ └── favicon.ico │ ├── README.md │ ├── vitest.config.ts │ ├── src │ │ ├── assets │ │ │ ├── logo.svg │ │ │ ├── main.css │ │ │ └── base.css │ │ ├── components │ │ │ ├── icons │ │ │ │ ├── IconSupport.vue │ │ │ │ ├── IconTooling.vue │ │ │ │ ├── IconCommunity.vue │ │ │ │ ├── IconDocumentation.vue │ │ │ │ └── IconEcosystem.vue │ │ │ ├── HelloWorld.vue │ │ │ ├── WelcomeItem.vue │ │ │ └── TheWelcome.vue │ │ ├── locales │ │ │ ├── schema.d.ts │ │ │ ├── ja.json │ │ │ └── en.json │ │ ├── i18n.ts │ │ ├── main.ts │ │ ├── views │ │ │ ├── AboutView.vue │ │ │ └── HomeView.vue │ │ ├── router │ │ │ └── index.ts │ │ └── App.vue │ ├── env.d.ts │ ├── index.html │ ├── vite.config.ts │ ├── tsconfig.json │ ├── package.json │ └── specs │ │ └── app.spec.ts └── vue2-options │ ├── README.md │ ├── public │ └── favicon.ico │ ├── vitest.config.ts │ ├── src │ ├── assets │ │ ├── logo.svg │ │ ├── main.css │ │ └── base.css │ ├── views │ │ ├── AboutView.vue │ │ └── HomeView.vue │ ├── components │ │ ├── icons │ │ │ ├── IconSupport.vue │ │ │ ├── IconTooling.vue │ │ │ ├── IconCommunity.vue │ │ │ ├── IconDocumentation.vue │ │ │ └── IconEcosystem.vue │ │ ├── HelloWorld.vue │ │ └── WelcomeItem.vue │ ├── locales │ │ ├── schema.d.ts │ │ ├── ja.json │ │ └── en.json │ ├── i18n.ts │ ├── main.ts │ ├── router │ │ └── index.ts │ └── App.vue │ ├── index.html │ ├── env.d.ts │ ├── vite.config.ts │ ├── tsconfig.json │ ├── package.json │ └── specs │ └── app.spec.ts ├── packages └── vue-i18n-routing │ ├── index.mjs │ ├── src │ ├── extends │ │ ├── index.ts │ │ ├── __test__ │ │ │ ├── router.test.vue2.ts │ │ │ ├── i18n.test.vue2.ts │ │ │ └── router.test.ts │ │ ├── vue-router.d.ts │ │ └── vue-i18n.d.ts │ ├── composables │ │ ├── index.ts │ │ ├── __test__ │ │ │ ├── __snapshots__ │ │ │ │ └── head.test.ts.snap │ │ │ └── head.test.ts │ │ └── head.ts │ ├── compatibles │ │ ├── index.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── global.d.ts │ ├── constants.ts │ ├── index.ts │ ├── vue-i18n.d.ts │ ├── vue.d.ts │ └── __test__ │ │ └── __snapshots__ │ │ └── resolve.test.ts.snap │ ├── types.d.ts │ ├── CHANGELOG.md │ ├── docsgen.config.js │ ├── README.md │ ├── vitest.config.ts │ ├── tsconfig.json │ ├── LICENSE │ ├── scripts │ └── vitest.ts │ ├── vite.config.ts │ └── package.json ├── .npmignore ├── scripts ├── bump.sh ├── preinstall.mjs ├── build.sh ├── e2e.sh ├── release.sh ├── release-edge.sh ├── replaceDeps.ts └── bump.ts ├── .secretlintrc.json ├── .prettierrc ├── .editorconfig ├── renovate.json ├── .vscode └── settings.json ├── .gitignore ├── README.md ├── LICENSE ├── .eslintrc-i18n.cjs ├── .eslintrc.cjs ├── CODE_OF_CONDUCT.md └── CONTRIBUTING.md /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: kazupon 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.json linguist-language=JSON-with-Comments -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | dist_ssr 3 | docsgen.config.js 4 | .nyc_output -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | npx --no-install lint-staged 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | dist-ssr 3 | coverage 4 | tsconfig.json 5 | -------------------------------------------------------------------------------- /playground/vue3/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /playground/vue3-options/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/vue-i18n-routing/index.mjs: -------------------------------------------------------------------------------- 1 | export * from './dist/vue-i18n-routing.js' 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.log 3 | *.swp 4 | coverage 5 | test 6 | temp 7 | scripts 8 | .nyc_output -------------------------------------------------------------------------------- /packages/vue-i18n-routing/src/extends/index.ts: -------------------------------------------------------------------------------- 1 | export * from './router' 2 | export * from './i18n' 3 | -------------------------------------------------------------------------------- /packages/vue-i18n-routing/src/composables/index.ts: -------------------------------------------------------------------------------- 1 | export * from './routing' 2 | export * from './head' 3 | -------------------------------------------------------------------------------- /scripts/bump.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -xe 4 | 5 | # Bump versions 6 | npx jiti ./scripts/bump.ts 7 | -------------------------------------------------------------------------------- /playground/vue2/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intlify/routing/HEAD/playground/vue2/public/favicon.ico -------------------------------------------------------------------------------- /playground/vue3/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intlify/routing/HEAD/playground/vue3/public/favicon.ico -------------------------------------------------------------------------------- /packages/vue-i18n-routing/types.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist/vue-i18n-routing' 2 | import './dist/vue' 3 | import './dist/vue-i18n' 4 | -------------------------------------------------------------------------------- /.secretlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": [ 3 | { 4 | "id": "@secretlint/secretlint-rule-preset-recommend" 5 | } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/vue-i18n-routing/src/compatibles/index.ts: -------------------------------------------------------------------------------- 1 | export * from './routing' 2 | export * from './head' 3 | export * from './types' 4 | -------------------------------------------------------------------------------- /playground/vue2-options/README.md: -------------------------------------------------------------------------------- 1 | # playground-vue2-options 2 | 3 | This example is vue-i18n-routing for Vue 2 + Options API + Vite 4 | -------------------------------------------------------------------------------- /playground/vue2-options/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intlify/routing/HEAD/playground/vue2-options/public/favicon.ico -------------------------------------------------------------------------------- /playground/vue3-options/README.md: -------------------------------------------------------------------------------- 1 | # playground-vue3-options 2 | 3 | This example is vue-i18n-routing for Vue 3 + Options API + Vite 4 | -------------------------------------------------------------------------------- /playground/vue3-options/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intlify/routing/HEAD/playground/vue3-options/public/favicon.ico -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | semi: false 2 | singleQuote: true 3 | printWidth: 120 4 | trailingComma: "none" 5 | endOfLine: "auto" 6 | arrowParens: "avoid" 7 | -------------------------------------------------------------------------------- /playground/vue2/README.md: -------------------------------------------------------------------------------- 1 | # playground-vue2 2 | 3 | This example is vue-i18n-routing for Vue 2 + Composition API (included ` 12 | 13 | 14 | -------------------------------------------------------------------------------- /playground/vue3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /playground/vue2-options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /playground/vue3-options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /playground/vue3-options/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.web.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./src/*"], 8 | }, 9 | "types": ["vue-i18n-routing/types"] 10 | }, 11 | "references": [ 12 | { 13 | "path": "./tsconfig.config.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /scripts/e2e.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Pack packages 6 | for PKG in packages/* ; do 7 | pushd $PKG 8 | echo "⚡ Packing $PKG ..." 9 | npm pack 10 | popd > /dev/null 11 | done 12 | 13 | rm -rf ./playground/**/node_modules/vue-i18n-routing 14 | rm -rf ./playground/**/package-lock.json 15 | 16 | # Replace deps 17 | npx jiti ./scripts/replaceDeps.ts 18 | 19 | pnpm play:setup 20 | 21 | pnpm test:e2e 22 | -------------------------------------------------------------------------------- /playground/vue2-options/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import Vue from 'vue' 5 | export default Vue 6 | } 7 | 8 | declare global { 9 | namespace JSX { 10 | type Element = VNode 11 | type ElementClass = Vue 12 | interface IntrinsicElements { 13 | [elem: string]: any // eslint-disable-line @typescript-eslint/no-explicit-any 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /playground/vue3-options/src/locales/schema.d.ts: -------------------------------------------------------------------------------- 1 | import { DefineLocaleMessage } from 'vue-i18n' // eslint-disable-line @typescript-eslint/no-unused-vars 2 | import en from './en.json' 3 | 4 | type MessageSchema = typeof en 5 | 6 | declare module 'vue-i18n' { 7 | // define the locale messages schema 8 | export interface DefineLocaleMessage extends MessageSchema {} // eslint-disable-line @typescript-eslint/no-empty-interface 9 | } 10 | -------------------------------------------------------------------------------- /playground/vue3/src/locales/schema.d.ts: -------------------------------------------------------------------------------- 1 | import { DefineLocaleMessage } from 'vue-i18n' // eslint-disable-line @typescript-eslint/no-unused-vars 2 | 3 | import type en from './en.json' 4 | 5 | type MessageSchema = typeof en 6 | 7 | declare module 'vue-i18n' { 8 | // define the locale messages schema 9 | export interface DefineLocaleMessage extends MessageSchema {} // eslint-disable-line @typescript-eslint/no-empty-interface 10 | } 11 | -------------------------------------------------------------------------------- /playground/vue3-options/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createHead } from '@vueuse/head' 2 | import { createApp } from 'vue' 3 | 4 | import App from './App.vue' 5 | import i18n from './i18n' 6 | import { createRouter } from './router' 7 | 8 | import './assets/main.css' 9 | 10 | const app = createApp(App) 11 | const router = createRouter(i18n) 12 | const head = createHead() 13 | app.use(router) 14 | app.use(i18n) 15 | app.use(head) 16 | app.mount('#app') 17 | -------------------------------------------------------------------------------- /playground/vue2-options/src/locales/schema.d.ts: -------------------------------------------------------------------------------- 1 | import { DefineLocaleMessage } from 'vue-i18n-bridge' // eslint-disable-line @typescript-eslint/no-unused-vars 2 | import en from './en.json' 3 | 4 | type MessageSchema = typeof en 5 | 6 | declare module 'vue-i18n-bridge' { 7 | // define the locale messages schema 8 | export interface DefineLocaleMessage extends MessageSchema {} // eslint-disable-line @typescript-eslint/no-empty-interface 9 | } 10 | -------------------------------------------------------------------------------- /playground/vue2/src/locales/schema.d.ts: -------------------------------------------------------------------------------- 1 | import { DefineLocaleMessage } from 'vue-i18n-bridge' // eslint-disable-line @typescript-eslint/no-unused-vars 2 | 3 | import type en from './en.json' 4 | 5 | type MessageSchema = typeof en 6 | 7 | declare module 'vue-i18n-bridge' { 8 | // define the locale messages schema 9 | export interface DefineLocaleMessage extends MessageSchema {} // eslint-disable-line @typescript-eslint/no-empty-interface 10 | } 11 | -------------------------------------------------------------------------------- /playground/vue3-options/src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /playground/vue3/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createHead } from '@vueuse/head' 2 | import { createApp } from 'vue' 3 | 4 | import App from './App.vue' 5 | import i18n from './i18n' 6 | import { createRouter } from './router' 7 | 8 | import './assets/main.css' 9 | 10 | const app = createApp(App) 11 | const router = createRouter(i18n) 12 | const head = createHead() 13 | app.use(router) 14 | app.use(i18n, { inject: true }) 15 | app.use(head) 16 | app.mount('#app') 17 | -------------------------------------------------------------------------------- /playground/vue2-options/src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /playground/vue2/src/i18n.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | import { createI18n } from 'vue-i18n-bridge' 4 | 5 | import en from './locales/en.json' 6 | import ja from './locales/ja.json' 7 | 8 | import type { I18n } from 'vue-i18n-bridge' 9 | 10 | Vue.use(VueI18n, { bridge: true }) 11 | 12 | const i18n = createI18n( 13 | { 14 | legacy: false, 15 | locale: 'en', 16 | messages: { 17 | en, 18 | ja 19 | } 20 | }, 21 | VueI18n 22 | ) 23 | 24 | export default i18n as I18n 25 | -------------------------------------------------------------------------------- /playground/vue2-options/src/i18n.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | import { createI18n } from 'vue-i18n-bridge' 4 | 5 | import en from './locales/en.json' 6 | import ja from './locales/ja.json' 7 | 8 | import type { I18n } from 'vue-i18n-bridge' 9 | 10 | Vue.use(VueI18n, { bridge: true }) 11 | 12 | const i18n = createI18n( 13 | { 14 | legacy: true, 15 | locale: 'en', 16 | messages: { 17 | en, 18 | ja 19 | } 20 | }, 21 | VueI18n 22 | ) 23 | 24 | export default i18n as I18n 25 | -------------------------------------------------------------------------------- /playground/vue3-options/src/views/AboutView.vue: -------------------------------------------------------------------------------- 1 | 9 | 14 | 15 | 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | build 13 | dist-ssr 14 | *.local 15 | .vercel 16 | .output 17 | .vite 18 | temp 19 | 20 | # Editor directories and files 21 | .vscode/extensions.json 22 | .idea 23 | .DS_Store 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | 30 | .yarn/* 31 | !.yarn/releases 32 | !.yarn/plugins 33 | 34 | .env 35 | .env.* 36 | *.tgz 37 | coverage 38 | package-lock.json 39 | yarn.lock -------------------------------------------------------------------------------- /playground/vue3/src/views/AboutView.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | 16 | 25 | -------------------------------------------------------------------------------- /playground/vue2/src/main.ts: -------------------------------------------------------------------------------- 1 | import VueCompositionAPI from '@vue/composition-api' 2 | import Vue from 'vue' 3 | import { castToVueI18n } from 'vue-i18n-bridge' 4 | 5 | Vue.use(VueCompositionAPI) 6 | 7 | import App from './App.vue' 8 | import i18n from './i18n' 9 | import { createRouter } from './router' 10 | 11 | import './assets/main.css' 12 | 13 | const router = createRouter(i18n) 14 | 15 | // @ts-ignore 16 | Vue.use(i18n) 17 | 18 | const app = new Vue({ 19 | router, 20 | i18n: castToVueI18n(i18n), 21 | render: h => h(App) 22 | }) 23 | 24 | app.$mount('#app') 25 | -------------------------------------------------------------------------------- /playground/vue3/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'url' 2 | 3 | import vuei18n from '@intlify/vite-plugin-vue-i18n' 4 | import vue from '@vitejs/plugin-vue' 5 | import vueJsx from '@vitejs/plugin-vue-jsx' 6 | import { defineConfig } from 'vite' 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | plugins: [vue(), vueJsx(), vuei18n({ include: fileURLToPath(new URL('./src/locales/**', import.meta.url)) })], 11 | resolve: { 12 | alias: { 13 | '@': fileURLToPath(new URL('./src', import.meta.url)) 14 | } 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /playground/vue2-options/src/main.ts: -------------------------------------------------------------------------------- 1 | import VueCompositionAPI from '@vue/composition-api' 2 | import Vue from 'vue' 3 | import { castToVueI18n } from 'vue-i18n-bridge' 4 | 5 | Vue.use(VueCompositionAPI) 6 | 7 | import App from './App.vue' 8 | import i18n from './i18n' 9 | import { createRouter } from './router' 10 | 11 | import './assets/main.css' 12 | 13 | const router = createRouter(i18n) 14 | 15 | // @ts-ignore 16 | Vue.use(i18n) 17 | 18 | const app = new Vue({ 19 | router, 20 | i18n: castToVueI18n(i18n), 21 | render: h => h(App) 22 | }) 23 | 24 | app.$mount('#app') 25 | -------------------------------------------------------------------------------- /playground/vue3-options/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'url' 2 | 3 | import vuei18n from '@intlify/vite-plugin-vue-i18n' 4 | import vue from '@vitejs/plugin-vue' 5 | import vueJsx from '@vitejs/plugin-vue-jsx' 6 | import { defineConfig } from 'vite' 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | plugins: [vue(), vueJsx(), vuei18n({ include: fileURLToPath(new URL('./src/locales/**', import.meta.url)) })], 11 | resolve: { 12 | alias: { 13 | '@': fileURLToPath(new URL('./src', import.meta.url)) 14 | } 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /packages/vue-i18n-routing/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | const TARGET = process.env.TEST_TARGET || 'vue3' 4 | const include = 5 | TARGET === 'vue3' 6 | ? ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'] 7 | : ['**/*.{test,spec}.vue2.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'] 8 | 9 | export default defineConfig({ 10 | define: { 11 | __VERSION__: JSON.stringify('__VERSION__') 12 | }, 13 | test: { 14 | globals: true, 15 | clearMocks: true, 16 | include, 17 | environment: 'happy-dom', 18 | testTimeout: 5000 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /playground/vue2/src/views/AboutView.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | 19 | 28 | -------------------------------------------------------------------------------- /playground/vue2/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import "./base.css"; 2 | 3 | #app { 4 | max-width: 1280px; 5 | margin: 0 auto; 6 | padding: 2rem; 7 | 8 | font-weight: normal; 9 | } 10 | 11 | a, 12 | .green { 13 | text-decoration: none; 14 | color: hsla(160, 100%, 37%, 1); 15 | transition: 0.4s; 16 | } 17 | 18 | @media (hover: hover) { 19 | a:hover { 20 | background-color: hsla(160, 100%, 37%, 0.2); 21 | } 22 | } 23 | 24 | @media (min-width: 1024px) { 25 | body { 26 | display: flex; 27 | place-items: center; 28 | } 29 | 30 | #app { 31 | display: grid; 32 | grid-template-columns: 1fr 1fr; 33 | padding: 0 2rem; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /playground/vue3/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import "./base.css"; 2 | 3 | #app { 4 | max-width: 1280px; 5 | margin: 0 auto; 6 | padding: 2rem; 7 | 8 | font-weight: normal; 9 | } 10 | 11 | a, 12 | .green { 13 | text-decoration: none; 14 | color: hsla(160, 100%, 37%, 1); 15 | transition: 0.4s; 16 | } 17 | 18 | @media (hover: hover) { 19 | a:hover { 20 | background-color: hsla(160, 100%, 37%, 0.2); 21 | } 22 | } 23 | 24 | @media (min-width: 1024px) { 25 | body { 26 | display: flex; 27 | place-items: center; 28 | } 29 | 30 | #app { 31 | display: grid; 32 | grid-template-columns: 1fr 1fr; 33 | padding: 0 2rem; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /playground/vue2-options/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import "./base.css"; 2 | 3 | #app { 4 | max-width: 1280px; 5 | margin: 0 auto; 6 | padding: 2rem; 7 | 8 | font-weight: normal; 9 | } 10 | 11 | a, 12 | .green { 13 | text-decoration: none; 14 | color: hsla(160, 100%, 37%, 1); 15 | transition: 0.4s; 16 | } 17 | 18 | @media (hover: hover) { 19 | a:hover { 20 | background-color: hsla(160, 100%, 37%, 0.2); 21 | } 22 | } 23 | 24 | @media (min-width: 1024px) { 25 | body { 26 | display: flex; 27 | place-items: center; 28 | } 29 | 30 | #app { 31 | display: grid; 32 | grid-template-columns: 1fr 1fr; 33 | padding: 0 2rem; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /playground/vue3-options/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import "./base.css"; 2 | 3 | #app { 4 | max-width: 1280px; 5 | margin: 0 auto; 6 | padding: 2rem; 7 | 8 | font-weight: normal; 9 | } 10 | 11 | a, 12 | .green { 13 | text-decoration: none; 14 | color: hsla(160, 100%, 37%, 1); 15 | transition: 0.4s; 16 | } 17 | 18 | @media (hover: hover) { 19 | a:hover { 20 | background-color: hsla(160, 100%, 37%, 0.2); 21 | } 22 | } 23 | 24 | @media (min-width: 1024px) { 25 | body { 26 | display: flex; 27 | place-items: center; 28 | } 29 | 30 | #app { 31 | display: grid; 32 | grid-template-columns: 1fr 1fr; 33 | padding: 0 2rem; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Restore all git changes 6 | git restore -s@ -SW -- packages 7 | 8 | # Build packages 9 | pnpm build 10 | 11 | # Update token 12 | if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then 13 | echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc 14 | echo "registry=https://registry.npmjs.org/" >> ~/.npmrc 15 | echo "always-auth=true" >> ~/.npmrc 16 | npm whoami 17 | fi 18 | 19 | # Release packages 20 | for PKG in packages/* ; do 21 | pushd $PKG 22 | TAG="latest" 23 | echo "⚡ Publishing $PKG with tag $TAG" 24 | npx npm@8.17.0 publish --tag $TAG --access public --tolerate-republish 25 | popd > /dev/null 26 | done 27 | -------------------------------------------------------------------------------- /scripts/release-edge.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -xe 4 | 5 | # Restore all git changes 6 | git restore -s@ -SW -- packages 7 | 8 | # Build packages 9 | pnpm build 10 | 11 | # Bump versions 12 | npx jiti ./scripts/bump.ts 13 | 14 | # Update token 15 | if [[ ! -z ${NODE_AUTH_TOKEN} ]] ; then 16 | echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc 17 | echo "registry=https://registry.npmjs.org/" >> ~/.npmrc 18 | echo "always-auth=true" >> ~/.npmrc 19 | npm whoami 20 | fi 21 | 22 | # Release packages 23 | for PKG in packages/* ; do 24 | pushd $PKG 25 | echo "⚡ Publishing $PKG with edge tag" 26 | pnpm publish --access public --no-git-checks --tag edge 27 | popd 28 | done 29 | -------------------------------------------------------------------------------- /playground/vue2/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'url' 2 | 3 | import legacy from '@vitejs/plugin-legacy' 4 | import scriptSetup from 'unplugin-vue2-script-setup/vite' 5 | import { defineConfig } from 'vite' 6 | import { createVuePlugin as vue2 } from 'vite-plugin-vue2' 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | plugins: [ 11 | vue2({ 12 | jsx: true 13 | }), 14 | scriptSetup(), 15 | legacy({ 16 | targets: ['ie >= 11'], 17 | additionalLegacyPolyfills: ['regenerator-runtime/runtime'] 18 | }) 19 | ], 20 | resolve: { 21 | alias: { 22 | '@': fileURLToPath(new URL('./src', import.meta.url)) 23 | } 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /packages/vue-i18n-routing/src/extends/vue-router.d.ts: -------------------------------------------------------------------------------- 1 | import type { Strategies, Directions } from '../types' 2 | 3 | interface VueRouterCustomProperties { 4 | __defaultLocale?: string 5 | __localeCodes?: string[] 6 | __strategy?: Strategies 7 | __trailingSlash?: boolean 8 | __routesNameSeparator?: string 9 | __defaultLocaleRouteNameSuffix?: string 10 | __defaultDirection?: Directions 11 | } 12 | declare module '@intlify/vue-router-bridge' { 13 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 14 | export interface VueRouter extends VueRouterCustomProperties {} 15 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 16 | export interface Router extends VueRouterCustomProperties {} 17 | } 18 | -------------------------------------------------------------------------------- /playground/vue2-options/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'url' 2 | 3 | import legacy from '@vitejs/plugin-legacy' 4 | import scriptSetup from 'unplugin-vue2-script-setup/vite' 5 | import { defineConfig } from 'vite' 6 | import { createVuePlugin as vue2 } from 'vite-plugin-vue2' 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | plugins: [ 11 | vue2({ 12 | jsx: true 13 | }), 14 | scriptSetup(), 15 | legacy({ 16 | targets: ['ie >= 11'], 17 | additionalLegacyPolyfills: ['regenerator-runtime/runtime'] 18 | }) 19 | ], 20 | resolve: { 21 | alias: { 22 | '@': fileURLToPath(new URL('./src', import.meta.url)) 23 | }, 24 | dedupe: ['vue'] 25 | } 26 | }) 27 | -------------------------------------------------------------------------------- /packages/vue-i18n-routing/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "useDefineForClassFields": true, 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "sourceMap": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "types": ["vitest/globals"], 13 | "paths": { 14 | "@intlify/vue-router-bridge": ["./node_modules/@intlify/vue-router-bridge/lib/index.d.ts"], 15 | "@intlify/vue-i18n-bridge": ["./node_modules/@intlify/vue-i18n-bridge/lib/index.d.ts"] 16 | } 17 | }, 18 | "include": ["src/**/*.ts", "src/**/*.d.ts", "test/**/*.ts", "test/**/*.d.ts", "scripts/**/*.ts", "scripts/**/*.d.ts"], 19 | } 20 | -------------------------------------------------------------------------------- /playground/vue2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "target": "esnext", 5 | "useDefineForClassFields": true, 6 | "module": "esnext", 7 | "moduleResolution": "node", 8 | "isolatedModules": true, 9 | "strict": true, 10 | "jsx": "preserve", 11 | "sourceMap": true, 12 | "resolveJsonModule": true, 13 | "esModuleInterop": true, 14 | "paths": { 15 | "@/*": ["src/*"] 16 | }, 17 | "types": [ 18 | "unplugin-vue2-script-setup/types", 19 | "vue-i18n-routing/types" 20 | ], 21 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"], 22 | "skipLibCheck": true 23 | }, 24 | "vueCompilerOptions": { 25 | "experimentalCompatMode": 2 26 | }, 27 | "include": ["vite.config.*", "env.d.ts", "src/**/*", "src/**/*.vue"] 28 | } 29 | -------------------------------------------------------------------------------- /playground/vue2-options/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "target": "esnext", 5 | "useDefineForClassFields": true, 6 | "module": "esnext", 7 | "moduleResolution": "node", 8 | "isolatedModules": true, 9 | "strict": true, 10 | "jsx": "preserve", 11 | "sourceMap": true, 12 | "resolveJsonModule": true, 13 | "esModuleInterop": true, 14 | "paths": { 15 | "@/*": ["src/*"] 16 | }, 17 | "types": [ 18 | "unplugin-vue2-script-setup/types", 19 | "vue-i18n-routing/types" 20 | ], 21 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"], 22 | "skipLibCheck": true 23 | }, 24 | "vueCompilerOptions": { 25 | "experimentalCompatMode": 2 26 | }, 27 | "include": ["vite.config.*", "env.d.ts", "src/**/*", "src/**/*.vue"] 28 | } 29 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | ### Description 15 | 16 | 17 | 18 | ### Linked Issues 19 | 20 | ### Additional context 21 | 22 | 23 | -------------------------------------------------------------------------------- /playground/vue3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground-vue3", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview --port 4173", 9 | "test": "vitest run specs", 10 | "typecheck": "vue-tsc --noEmit" 11 | }, 12 | "dependencies": { 13 | "vue": "3.2.31", 14 | "vue-router": "4.1.2", 15 | "vue-i18n": "^9.3.0", 16 | "@vueuse/head": "0.9.8", 17 | "vue-i18n-routing": "0.13.4" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "^18.0.4", 21 | "@vitejs/plugin-vue": "3.2.0", 22 | "@vitejs/plugin-vue-jsx": "2.1.1", 23 | "@intlify/vite-plugin-vue-i18n": "next", 24 | "@vue/tsconfig": "0.1.3", 25 | "typescript": "^4.8.3", 26 | "vite": "3.2.10", 27 | "playwright": "1.25.2", 28 | "vite-test-utils": "0.6.0", 29 | "vitest": "^0.34.0", 30 | "vue-tsc": "0.40.13" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /playground/vue3-options/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground-vue3-options", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview --port 4173", 9 | "test": "vitest run specs", 10 | "typecheck": "vue-tsc --noEmit" 11 | }, 12 | "dependencies": { 13 | "vue": "3.2.31", 14 | "vue-router": "4.1.2", 15 | "vue-i18n": "^9.3.0", 16 | "@vueuse/head": "0.9.8", 17 | "vue-i18n-routing": "0.13.4" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "^18.0.4", 21 | "@vitejs/plugin-vue": "3.2.0", 22 | "@vitejs/plugin-vue-jsx": "2.1.1", 23 | "@intlify/vite-plugin-vue-i18n": "next", 24 | "@vue/tsconfig": "0.1.3", 25 | "typescript": "^4.8.3", 26 | "vite": "3.2.10", 27 | "playwright": "1.25.2", 28 | "vite-test-utils": "0.6.0", 29 | "vitest": "^0.34.0", 30 | "vue-tsc": "0.40.13" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /playground/vue3/src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🛣️ @intlify/routing 2 | 3 | The i18n routing libraries 4 | 5 | ## 🍭 Playgrounds 6 | 7 | - Vue: [playground](./playground) 8 | 9 | ## 💖 Special thanks! 10 | 11 | - [`vue-i18n-routing`](https://github.com/intlify/routing/tree/main/packages/vue-i18n-routing) i18n routing logics was based on [`@nuxtjs/i18n`](https://github.com/nuxt-community/i18n-module). [@paulgv](https://github.com/paulgv), [@rchl](https://github.com/rchl) great works! 12 | 13 | We're also grateful for the contributions of the Nuxt community 💚! 14 | 15 | ## 🙌 Contributing guidelines 16 | 17 | If you are interested in contributing to `@intlify/routing` project, I highly recommend checking out [the contributing guidelines](/CONTRIBUTING.md) here. You'll find all the relevant information such as [how to make a PR](/CONTRIBUTING.md#pull-request-guidelines), [how to setup development](/CONTRIBUTING.md#development-setup) etc., there. 18 | 19 | ## ©️ License 20 | 21 | [MIT](http://opensource.org/licenses/MIT) 22 | -------------------------------------------------------------------------------- /playground/vue2/src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 32 | -------------------------------------------------------------------------------- /playground/vue2/src/components/icons/IconTooling.vue: -------------------------------------------------------------------------------- 1 | 2 | 20 | -------------------------------------------------------------------------------- /playground/vue3/src/components/icons/IconTooling.vue: -------------------------------------------------------------------------------- 1 | 2 | 20 | -------------------------------------------------------------------------------- /playground/vue2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground-vue2", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite --port 5173", 7 | "build": "vite build", 8 | "preview": "vite preview --port 4173", 9 | "test": "vitest run specs", 10 | "typecheck": "vue-tsc --noEmit" 11 | }, 12 | "dependencies": { 13 | "@vue/composition-api": "^1.7.0", 14 | "vue": "2.6.14", 15 | "vue-router": "3.5.3", 16 | "vue-i18n": "^8", 17 | "vue-i18n-bridge": "^9.3.0", 18 | "vue-i18n-routing": "0.13.4" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "^18.0.4", 22 | "@vitejs/plugin-legacy": "1.8.2", 23 | "@vue/runtime-dom": "3.2.31", 24 | "typescript": "^4.7.4", 25 | "unplugin-vue2-script-setup": "0.11.4", 26 | "vite": "2.9.18", 27 | "playwright": "1.25.2", 28 | "vite-test-utils": "0.6.0", 29 | "vitest": "^0.34.0", 30 | "vite-plugin-vue2": "2.0.3", 31 | "vue-template-compiler": "2.6.14", 32 | "vue-tsc": "0.40.13" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /playground/vue2-options/src/components/icons/IconTooling.vue: -------------------------------------------------------------------------------- 1 | 2 | 20 | -------------------------------------------------------------------------------- /playground/vue3-options/src/components/icons/IconTooling.vue: -------------------------------------------------------------------------------- 1 | 2 | 20 | -------------------------------------------------------------------------------- /playground/vue2-options/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground-vue2-options", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite --port 5173", 7 | "build": "vite build", 8 | "preview": "vite preview --port 4173", 9 | "test": "vitest run specs", 10 | "typecheck": "vue-tsc --noEmit" 11 | }, 12 | "dependencies": { 13 | "@vue/composition-api": "^1.7.0", 14 | "vue": "2.6.14", 15 | "vue-router": "3.5.3", 16 | "vue-i18n": "^8", 17 | "vue-i18n-bridge": "^9.3.0", 18 | "vue-i18n-routing": "0.13.4" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "^18.0.4", 22 | "@vitejs/plugin-legacy": "1.8.2", 23 | "@vue/runtime-dom": "3.2.31", 24 | "typescript": "^4.7.4", 25 | "unplugin-vue2-script-setup": "0.11.4", 26 | "vite": "2.9.18", 27 | "playwright": "1.25.2", 28 | "vite-test-utils": "0.6.0", 29 | "vitest": "^0.34.0", 30 | "vite-plugin-vue2": "2.0.3", 31 | "vue-template-compiler": "2.6.14", 32 | "vue-tsc": "0.40.13" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /playground/vue2/src/components/icons/IconCommunity.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /playground/vue3/src/components/icons/IconCommunity.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /playground/vue2-options/src/components/icons/IconCommunity.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /playground/vue2/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | 23 | 47 | -------------------------------------------------------------------------------- /playground/vue3-options/src/components/icons/IconCommunity.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /playground/vue3/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | 23 | 47 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | authors: 6 | - octocat 7 | - renovate[bot] 8 | categories: 9 | - title: 🌟 Features 10 | labels: 11 | - feature 12 | - title: 🐛 Bug Fixes 13 | labels: 14 | - bug 15 | - title: 💥 Breaking Changes 16 | labels: 17 | - breaking 18 | - title: ⚠️ Deprecated Features 19 | labels: 20 | - deprecated 21 | - title: ⚡ Improvement Features 22 | labels: 23 | - improvement 24 | - title: 🔒 Security Fixes 25 | labels: 26 | - security 27 | - title: 📈 Performance Fixes 28 | labels: 29 | - performance 30 | - title: 📝️ Documentations 31 | labels: 32 | - documentation 33 | - title: 👕 Refactoring 34 | labels: 35 | - refactoring 36 | - title: 🍭 Examples 37 | labels: 38 | - example 39 | - title: 🌐 ♿ Internationalization or Accessibility Fixes 40 | labels: 41 | - a11y 42 | - i18n 43 | - title: 🪄 Others 44 | labels: 45 | - chore 46 | -------------------------------------------------------------------------------- /playground/vue3-options/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter as _createRouter } from 'vue-i18n-routing' 2 | import { createWebHistory } from 'vue-router' 3 | 4 | import HomeView from '../views/HomeView.vue' 5 | 6 | import type { I18n } from 'vue-i18n' 7 | 8 | export function createRouter(i18n: I18n) { 9 | return _createRouter(i18n, { 10 | version: 4, 11 | defaultLocale: 'en', 12 | baseUrl: 'https://localhost:3000', 13 | locales: [ 14 | { 15 | code: 'en', 16 | iso: 'en-US', 17 | name: 'English' 18 | }, 19 | { 20 | code: 'ja', 21 | iso: 'ja-JP', 22 | name: '日本語' 23 | } 24 | ], 25 | history: createWebHistory(import.meta.env.BASE_URL), 26 | routes: [ 27 | { 28 | path: '/', 29 | name: 'home', 30 | component: HomeView 31 | }, 32 | { 33 | path: '/about', 34 | name: 'about', 35 | // route level code-splitting 36 | // this generates a separate chunk (About.[hash].js) for this route 37 | // which is lazy-loaded when the route is visited. 38 | component: () => import('../views/AboutView.vue') 39 | } 40 | ] 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /packages/vue-i18n-routing/src/extends/vue-i18n.d.ts: -------------------------------------------------------------------------------- 1 | import type { LocaleObject } from '../types' 2 | import type { ComputedRef } from 'vue-demi' 3 | 4 | export interface ComposerCustomProperties { 5 | /** 6 | * List of locales 7 | * 8 | * @remarks 9 | * Can either be an array of string codes (e.g. `['en', 'fr']`) or an array of {@link LocaleObject} for more complex configurations 10 | */ 11 | locales: ComputedRef 12 | /** 13 | * List of locale codes 14 | */ 15 | localeCodes: ComputedRef 16 | /** 17 | * Base URL that is used in generating canonical links 18 | */ 19 | baseUrl: ComputedRef 20 | } 21 | declare module 'vue-i18n' { 22 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 23 | export interface ComposerCustom extends ComposerCustomProperties {} 24 | } 25 | 26 | declare module 'vue-i18n-bridge' { 27 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 28 | export interface ComposerCustom extends ComposerCustomProperties {} 29 | } 30 | declare module '@intlify/vue-i18n-bridge' { 31 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 32 | export interface ComposerCustom extends ComposerCustomProperties {} 33 | } 34 | -------------------------------------------------------------------------------- /playground/vue3-options/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 29 | 30 | 54 | -------------------------------------------------------------------------------- /playground/vue2-options/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 29 | 30 | 54 | -------------------------------------------------------------------------------- /playground/vue3/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter as _createRouter } from 'vue-i18n-routing' 2 | import { createWebHistory } from 'vue-router' 3 | 4 | import HomeView from '../views/HomeView.vue' 5 | 6 | import type { I18n } from '@intlify/vue-i18n-bridge' 7 | 8 | export function createRouter(i18n: I18n) { 9 | return _createRouter(i18n, { 10 | version: 4, 11 | defaultLocale: 'en', 12 | // strategy: 'prefix', 13 | // trailingSlash: true, 14 | baseUrl: 'https://localhost:3000', 15 | locales: [ 16 | { 17 | code: 'en', 18 | iso: 'en-US', 19 | name: 'English' 20 | }, 21 | { 22 | code: 'ja', 23 | iso: 'ja-JP', 24 | name: '日本語' 25 | } 26 | ], 27 | history: createWebHistory(import.meta.env.BASE_URL), 28 | routes: [ 29 | { 30 | path: '/', 31 | name: 'home', 32 | component: HomeView 33 | }, 34 | { 35 | path: '/about', 36 | name: 'about', 37 | // route level code-splitting 38 | // this generates a separate chunk (About.[hash].js) for this route 39 | // which is lazy-loaded when the route is visited. 40 | component: () => import('../views/AboutView.vue') 41 | } 42 | ] 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Nuxt Community 4 | Copyright (c) Rafał Chłodnicki (@rchl) 5 | Copyright (c) Paul Gascou-Vaillancourt (@paulgv) 6 | Copyright (c) kazuya kawaguchi (@kazupon) 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | this software and associated documentation files (the "Software"), to deal in 10 | the Software without restriction, including without limitation the rights to 11 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 12 | the Software, and to permit persons to whom the Software is furnished to do so, 13 | subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 20 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 21 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 22 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /playground/vue2-options/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { createRouter as _createRouter } from 'vue-i18n-routing' 3 | import VueRouter from 'vue-router' 4 | 5 | import HomeView from '../views/HomeView.vue' 6 | 7 | import type { I18n } from 'vue-i18n-bridge' 8 | 9 | Vue.use(VueRouter) 10 | 11 | export function createRouter(i18n: I18n) { 12 | return _createRouter(i18n, { 13 | version: 3, 14 | defaultLocale: 'en', 15 | baseUrl: 'https://localhost:3000', 16 | locales: [ 17 | { 18 | code: 'en', 19 | iso: 'en-US', 20 | name: 'English' 21 | }, 22 | { 23 | code: 'ja', 24 | iso: 'ja-JP', 25 | name: '日本語' 26 | } 27 | ], 28 | // @ts-ignore TODO: 29 | mode: 'history', 30 | base: import.meta.env.BASE_URL, 31 | routes: [ 32 | { 33 | path: '/', 34 | name: 'home', 35 | component: HomeView 36 | }, 37 | { 38 | path: '/about', 39 | name: 'about', 40 | // route level code-splitting 41 | // this generates a separate chunk (About.[hash].js) for this route 42 | // which is lazy-loaded when the route is visited. 43 | component: () => import('../views/AboutView.vue') 44 | } 45 | ] 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /playground/vue2/src/components/icons/IconDocumentation.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /playground/vue2/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { createRouter as _createRouter } from 'vue-i18n-routing' 3 | import VueRouter from 'vue-router' 4 | 5 | import HomeView from '../views/HomeView.vue' 6 | 7 | import type { I18n } from '@intlify/vue-i18n-bridge' 8 | 9 | Vue.use(VueRouter) 10 | 11 | export function createRouter(i18n: I18n) { 12 | return _createRouter(i18n, { 13 | version: 3, 14 | defaultLocale: 'en', 15 | // strategy: 'prefix', 16 | baseUrl: 'https://localhost:3000', 17 | locales: [ 18 | { 19 | code: 'en', 20 | iso: 'en-US', 21 | name: 'English' 22 | }, 23 | { 24 | code: 'ja', 25 | iso: 'ja-JP', 26 | name: '日本語' 27 | } 28 | ], 29 | mode: 'history', 30 | base: import.meta.env.BASE_URL, 31 | routes: [ 32 | { 33 | path: '/', 34 | name: 'home', 35 | component: HomeView 36 | }, 37 | { 38 | path: '/about', 39 | name: 'about', 40 | // route level code-splitting 41 | // this generates a separate chunk (About.[hash].js) for this route 42 | // which is lazy-loaded when the route is visited. 43 | component: () => import('../views/AboutView.vue') 44 | } 45 | ] 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /playground/vue3/src/components/icons/IconDocumentation.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /playground/vue2-options/src/components/icons/IconDocumentation.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /playground/vue3-options/src/components/icons/IconDocumentation.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /packages/vue-i18n-routing/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Nuxt Community 4 | Copyright (c) Rafał Chłodnicki (@rchl) 5 | Copyright (c) Paul Gascou-Vaillancourt (@paulgv) 6 | Copyright (c) kazuya kawaguchi (@kazupon) 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | this software and associated documentation files (the "Software"), to deal in 10 | the Software without restriction, including without limitation the rights to 11 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 12 | the Software, and to permit persons to whom the Software is furnished to do so, 13 | subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 20 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 21 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 22 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /packages/vue-i18n-routing/src/constants.ts: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | export const STRATEGIES = { 3 | PREFIX: 'prefix', 4 | PREFIX_EXCEPT_DEFAULT: 'prefix_except_default', 5 | PREFIX_AND_DEFAULT: 'prefix_and_default', 6 | NO_PREFIX: 'no_prefix' 7 | } as const 8 | 9 | // NOTE: 10 | // we avoid SSR issue with the temp variables 11 | // (I think it seem to not be able to handle the MemberExpression case) 12 | // - https://github.com/vitejs/vite/pull/6171 13 | // - https://github.com/vitejs/vite/pull/3848 14 | /* 15 | export const VUE_I18N_ROUTING_DEFAULTS = { 16 | defaultLocale: '', 17 | strategy: 'prefix_except_default', 18 | trailingSlash: false, 19 | routesNameSeparator: '___', 20 | defaultLocaleRouteNameSuffix: 'default' 21 | } 22 | */ 23 | 24 | /** @internal */ 25 | export const DEFAULT_LOCALE = '' 26 | /** @internal */ 27 | export const DEFAULT_STRATEGY = STRATEGIES.PREFIX_EXCEPT_DEFAULT 28 | /** @internal */ 29 | export const DEFAULT_TRAILING_SLASH = false 30 | /** @internal */ 31 | export const DEFAULT_ROUTES_NAME_SEPARATOR = '___' 32 | /** @internal */ 33 | export const DEFAULT_LOCALE_ROUTE_NAME_SUFFIX = 'default' 34 | /** @internal */ 35 | export const DEFAULT_DETECTION_DIRECTION = 'ltr' 36 | /** @internal */ 37 | export const DEFAULT_BASE_URL = '' 38 | /** @internal */ 39 | export const DEFAULT_DYNAMIC_PARAMS_KEY = '' 40 | -------------------------------------------------------------------------------- /packages/vue-i18n-routing/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Vue I18n Routing Version 3 | * 4 | * @remarks 5 | * Semver format. Same format as the package.json `version` field. 6 | */ 7 | export const VERSION = __VERSION__ 8 | 9 | export * from './constants' 10 | export * from './compatibles' 11 | export * from './composables' 12 | export * from './resolve' 13 | export * from './types' 14 | export { 15 | getLocale, 16 | getLocales, 17 | getLocaleCodes, 18 | setLocale, 19 | resolveBaseUrl, 20 | findBrowserLocale, 21 | isComposer, 22 | getComposer, 23 | isExportedGlobalComposer, 24 | isI18nInstance, 25 | isLegacyVueI18n, 26 | isVueI18n 27 | } from './utils' 28 | export type { 29 | I18nCommonRoutingOptions, 30 | I18nCommonRoutingOptionsWithComposable, 31 | ComposableOptions, 32 | FindBrowserLocaleOptions, 33 | BrowserLocale, 34 | TargetLocale, 35 | BrowserLocaleMatcher 36 | } from './utils' 37 | export { 38 | createRouter, 39 | createLocaleFromRouteGetter, 40 | registerGlobalOptions, 41 | getGlobalOptions, 42 | extendI18n, 43 | getLocalesRegex, 44 | proxyVueInstance 45 | } from './extends' 46 | export type { 47 | I18nRoutingGlobalOptions, 48 | VueI18nRoutingPluginOptions, 49 | VueI18nExtendOptions, 50 | ExtendComposerHook, 51 | ExtendExportedGlobalHook, 52 | ExtendVueI18nHook, 53 | ExtendProperyDescripters, 54 | ExtendHooks 55 | } from './extends' 56 | -------------------------------------------------------------------------------- /playground/vue2/src/locales/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "App": { 3 | "msg": "それをしました!", 4 | "home": "ホーム", 5 | "about": "アバウト" 6 | }, 7 | "views": { 8 | "AboutView": "これはアバウトページです" 9 | }, 10 | "components": { 11 | "HelloWorld": "{vite} + {vue} でプロジェクトを作成しました。次は何をすればいいですか?", 12 | "TheWelcome": { 13 | "documentation": { 14 | "title": "ドキュメント", 15 | "official": "公式ドキュメント", 16 | "message": "Vue {official} はあなたが始めるために必要なすべての情報を提供します。" 17 | }, 18 | "tooling": { 19 | "title": "ツール", 20 | "message": "このプロジェクトは、{vite} によってバンドルされ提供されます。推奨される IDE のセットアップは、{vscode} + {volar} です。もし、コンポーネントとページのテストが必要である場合、{cypress} と {testing} をよく調べてください。 より詳しい説明は、{readme} にあります。" 21 | }, 22 | "ecosystem": { 23 | "title": "エコシステム", 24 | "message": "{state}、{router}、{test} そして {devtools} のような公式ツールやライブラリをプロジェクトに導入しましょう。もしもっとリソースが必要ならば、{awesome} に行ってご覧になってください。" 25 | }, 26 | "community": { 27 | "title": "コミュニティ", 28 | "mailing": "メーリングリスト", 29 | "message": "詰まりましたか? 私達の公式 Discord サーバーである {discord} 、または {stackoverflow} で質問してみましょう。また、{news}を購読したり、公式 Twitter アカウント {twitter} をフォローしたりすると、Vue の世界の最新情報を得ることが出来ます。" 30 | }, 31 | "support": { 32 | "title": "Vue をサポートする", 33 | "sponsor": "スポンサーになっていただく", 34 | "message": "独立したプロジェクトである Vue は、その持続性のためにコミュニティの支援に依存しています。{sponsor}ことで、私たちを支援していただけます。" 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /playground/vue3/src/locales/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "App": { 3 | "msg": "それをしました!", 4 | "home": "ホーム", 5 | "about": "アバウト" 6 | }, 7 | "views": { 8 | "AboutView": "これはアバウトページです" 9 | }, 10 | "components": { 11 | "HelloWorld": "{vite} + {vue} でプロジェクトを作成しました。次は何をすればいいですか?", 12 | "TheWelcome": { 13 | "documentation": { 14 | "title": "ドキュメント", 15 | "official": "公式ドキュメント", 16 | "message": "Vue {official} はあなたが始めるために必要なすべての情報を提供します。" 17 | }, 18 | "tooling": { 19 | "title": "ツール", 20 | "message": "このプロジェクトは、{vite} によってバンドルされ提供されます。推奨される IDE のセットアップは、{vscode} + {volar} です。もし、コンポーネントとページのテストが必要である場合、{cypress} と {testing} をよく調べてください。 より詳しい説明は、{readme} にあります。" 21 | }, 22 | "ecosystem": { 23 | "title": "エコシステム", 24 | "message": "{state}、{router}、{test} そして {devtools} のような公式ツールやライブラリをプロジェクトに導入しましょう。もしもっとリソースが必要ならば、{awesome} に行ってご覧になってください。" 25 | }, 26 | "community": { 27 | "title": "コミュニティ", 28 | "mailing": "メーリングリスト", 29 | "message": "詰まりましたか? 私達の公式 Discord サーバーである {discord} 、または {stackoverflow} で質問してみましょう。また、{news}を購読したり、公式 Twitter アカウント {twitter} をフォローしたりすると、Vue の世界の最新情報を得ることが出来ます。" 30 | }, 31 | "support": { 32 | "title": "Vue をサポートする", 33 | "sponsor": "スポンサーになっていただく", 34 | "message": "独立したプロジェクトである Vue は、その持続性のためにコミュニティの支援に依存しています。{sponsor}ことで、私たちを支援していただけます。" 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /playground/vue2-options/src/locales/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "App": { 3 | "msg": "それをしました!", 4 | "home": "ホーム", 5 | "about": "アバウト" 6 | }, 7 | "views": { 8 | "AboutView": "これはアバウトページです" 9 | }, 10 | "components": { 11 | "HelloWorld": "{vite} + {vue} でプロジェクトを作成しました。次は何をすればいいですか?", 12 | "TheWelcome": { 13 | "documentation": { 14 | "title": "ドキュメント", 15 | "official": "公式ドキュメント", 16 | "message": "Vue {official} はあなたが始めるために必要なすべての情報を提供します。" 17 | }, 18 | "tooling": { 19 | "title": "ツール", 20 | "message": "このプロジェクトは、{vite} によってバンドルされ提供されます。推奨される IDE のセットアップは、{vscode} + {volar} です。もし、コンポーネントとページのテストが必要である場合、{cypress} と {testing} をよく調べてください。 より詳しい説明は、{readme} にあります。" 21 | }, 22 | "ecosystem": { 23 | "title": "エコシステム", 24 | "message": "{state}、{router}、{test} そして {devtools} のような公式ツールやライブラリをプロジェクトに導入しましょう。もしもっとリソースが必要ならば、{awesome} に行ってご覧になってください。" 25 | }, 26 | "community": { 27 | "title": "コミュニティ", 28 | "mailing": "メーリングリスト", 29 | "message": "詰まりましたか? 私達の公式 Discord サーバーである {discord} 、または {stackoverflow} で質問してみましょう。また、{news}を購読したり、公式 Twitter アカウント {twitter} をフォローしたりすると、Vue の世界の最新情報を得ることが出来ます。" 30 | }, 31 | "support": { 32 | "title": "Vue をサポートする", 33 | "sponsor": "スポンサーになっていただく", 34 | "message": "独立したプロジェクトである Vue は、その持続性のためにコミュニティの支援に依存しています。{sponsor}ことで、私たちを支援していただけます。" 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /playground/vue3-options/src/locales/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "App": { 3 | "msg": "それをしました!", 4 | "home": "ホーム", 5 | "about": "アバウト" 6 | }, 7 | "views": { 8 | "AboutView": "これはアバウトページです" 9 | }, 10 | "components": { 11 | "HelloWorld": "{vite} + {vue} でプロジェクトを作成しました。次は何をすればいいですか?", 12 | "TheWelcome": { 13 | "documentation": { 14 | "title": "ドキュメント", 15 | "official": "公式ドキュメント", 16 | "message": "Vue {official} はあなたが始めるために必要なすべての情報を提供します。" 17 | }, 18 | "tooling": { 19 | "title": "ツール", 20 | "message": "このプロジェクトは、{vite} によってバンドルされ提供されます。推奨される IDE のセットアップは、{vscode} + {volar} です。もし、コンポーネントとページのテストが必要である場合、{cypress} と {testing} をよく調べてください。 より詳しい説明は、{readme} にあります。" 21 | }, 22 | "ecosystem": { 23 | "title": "エコシステム", 24 | "message": "{state}、{router}、{test} そして {devtools} のような公式ツールやライブラリをプロジェクトに導入しましょう。もしもっとリソースが必要ならば、{awesome} に行ってご覧になってください。" 25 | }, 26 | "community": { 27 | "title": "コミュニティ", 28 | "mailing": "メーリングリスト", 29 | "message": "詰まりましたか? 私達の公式 Discord サーバーである {discord} 、または {stackoverflow} で質問してみましょう。また、{news}を購読したり、公式 Twitter アカウント {twitter} をフォローしたりすると、Vue の世界の最新情報を得ることが出来ます。" 30 | }, 31 | "support": { 32 | "title": "Vue をサポートする", 33 | "sponsor": "スポンサーになっていただく", 34 | "message": "独立したプロジェクトである Vue は、その持続性のためにコミュニティの支援に依存しています。{sponsor}ことで、私たちを支援していただけます。" 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /playground/vue2/specs/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { setup, url, createPage } from 'vite-test-utils' 2 | import { test, expect } from 'vitest' 3 | 4 | await setup({ 5 | browser: true 6 | }) 7 | 8 | test('i18n & routing', async () => { 9 | const page = await createPage() 10 | await page.goto(url('/')) 11 | 12 | page.on('console', (...args) => console.log(...args)) 13 | 14 | // initial title 15 | const header = await page.locator('h1.green') 16 | expect(await header.textContent()).toBe('You did it!') 17 | 18 | // check `localeHead` 19 | const localeHead = await page.locator('code#head') 20 | let head = JSON.parse((await localeHead.textContent()) || '{}') 21 | expect(head.htmlAttrs.lang).toBe('en-US') 22 | 23 | // go to about page 24 | const aboutMenu = await page.locator('header a[href="/about"]') 25 | await aboutMenu.click() 26 | const aboutHeader = await page.locator('.about h1') 27 | expect(await aboutHeader.textContent()).toBe('This is an about page') 28 | 29 | // change locale 30 | const jaAbout = await page.locator('header a[href="/ja/about"]') 31 | await jaAbout.click() 32 | expect(await header.textContent()).toBe('それをしました!') 33 | expect(await aboutHeader.textContent()).toBe('これはアバウトページです') 34 | 35 | // go to home page 36 | const jaHome = await page.locator('header a[href="/ja"]') 37 | await jaHome.click() 38 | head = JSON.parse((await localeHead.textContent()) || '{}') 39 | expect(head.htmlAttrs.lang).toBe('ja-JP') 40 | }) 41 | -------------------------------------------------------------------------------- /playground/vue3/specs/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { setup, url, createPage } from 'vite-test-utils' 2 | import { test, expect } from 'vitest' 3 | 4 | await setup({ 5 | browser: true 6 | }) 7 | 8 | test('i18n & routing', async () => { 9 | const page = await createPage() 10 | await page.goto(url('/')) 11 | 12 | page.on('console', (...args) => console.log(...args)) 13 | 14 | // initial title 15 | const header = await page.locator('h1.green') 16 | expect(await header.textContent()).toBe('You did it!') 17 | 18 | // check `localeHead` 19 | const localeHead = await page.locator('code#head') 20 | let head = JSON.parse((await localeHead.textContent()) || '{}') 21 | expect(head.htmlAttrs.lang).toBe('en-US') 22 | 23 | // go to about page 24 | const aboutMenu = await page.locator('header a[href="/about"]') 25 | await aboutMenu.click() 26 | const aboutHeader = await page.locator('.about h1') 27 | expect(await aboutHeader.textContent()).toBe('This is an about page') 28 | 29 | // change locale 30 | const jaAbout = await page.locator('header a[href="/ja/about"]') 31 | await jaAbout.click() 32 | expect(await header.textContent()).toBe('それをしました!') 33 | expect(await aboutHeader.textContent()).toBe('これはアバウトページです') 34 | 35 | // go to home page 36 | const jaHome = await page.locator('header a[href="/ja"]') 37 | await jaHome.click() 38 | head = JSON.parse((await localeHead.textContent()) || '{}') 39 | expect(head.htmlAttrs.lang).toBe('ja-JP') 40 | }) 41 | -------------------------------------------------------------------------------- /playground/vue2-options/specs/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { setup, url, createPage } from 'vite-test-utils' 2 | import { test, expect } from 'vitest' 3 | 4 | await setup({ 5 | browser: true 6 | }) 7 | 8 | test('i18n & routing', async () => { 9 | const page = await createPage() 10 | await page.goto(url('/')) 11 | 12 | page.on('console', (...args) => console.log(...args)) 13 | 14 | // initial title 15 | const header = await page.locator('h1.green') 16 | expect(await header.textContent()).toBe('You did it!') 17 | 18 | // check `localeHead` 19 | const localeHead = await page.locator('code#head') 20 | let head = JSON.parse((await localeHead.textContent()) || '{}') 21 | expect(head.htmlAttrs.lang).toBe('en-US') 22 | 23 | // go to about page 24 | const aboutMenu = await page.locator('header a[href="/about"]') 25 | await aboutMenu.click() 26 | const aboutHeader = await page.locator('.about h1') 27 | expect(await aboutHeader.textContent()).toBe('This is an about page') 28 | 29 | // change locale 30 | const jaAbout = await page.locator('header a[href="/ja/about"]') 31 | await jaAbout.click() 32 | expect(await header.textContent()).toBe('それをしました!') 33 | expect(await aboutHeader.textContent()).toBe('これはアバウトページです') 34 | 35 | // go to home page 36 | const jaHome = await page.locator('header a[href="/ja"]') 37 | await jaHome.click() 38 | head = JSON.parse((await localeHead.textContent()) || '{}') 39 | expect(head.htmlAttrs.lang).toBe('ja-JP') 40 | }) 41 | -------------------------------------------------------------------------------- /playground/vue3-options/specs/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { setup, url, createPage } from 'vite-test-utils' 2 | import { test, expect } from 'vitest' 3 | 4 | await setup({ 5 | browser: true 6 | }) 7 | 8 | test('i18n & routing', async () => { 9 | const page = await createPage() 10 | await page.goto(url('/')) 11 | 12 | page.on('console', (...args) => console.log(...args)) 13 | 14 | // initial title 15 | const header = await page.locator('h1.green') 16 | expect(await header.textContent()).toBe('You did it!') 17 | 18 | // check `localeHead` 19 | const localeHead = await page.locator('code#head') 20 | let head = JSON.parse((await localeHead.textContent()) || '{}') 21 | expect(head.htmlAttrs.lang).toBe('en-US') 22 | 23 | // go to about page 24 | const aboutMenu = await page.locator('header a[href="/about"]') 25 | await aboutMenu.click() 26 | const aboutHeader = await page.locator('.about h1') 27 | expect(await aboutHeader.textContent()).toBe('This is an about page') 28 | 29 | // change locale 30 | const jaAbout = await page.locator('header a[href="/ja/about"]') 31 | await jaAbout.click() 32 | expect(await header.textContent()).toBe('それをしました!') 33 | expect(await aboutHeader.textContent()).toBe('これはアバウトページです') 34 | 35 | // go to home page 36 | const jaHome = await page.locator('header a[href="/ja"]') 37 | await jaHome.click() 38 | head = JSON.parse((await localeHead.textContent()) || '{}') 39 | expect(head.htmlAttrs.lang).toBe('ja-JP') 40 | }) 41 | -------------------------------------------------------------------------------- /scripts/replaceDeps.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs, constants as FS_CONSTANTS } from 'node:fs' 2 | import { resolve } from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | 5 | import { readPackageJSON, writePackageJSON } from 'pkg-types' 6 | 7 | const __dirname = fileURLToPath(new URL('.', import.meta.url)) 8 | const PKG_MAP = new Map() 9 | 10 | export async function isExists(path: string) { 11 | try { 12 | await fs.access(path, FS_CONSTANTS.F_OK) 13 | return true 14 | } catch { 15 | return false 16 | } 17 | } 18 | 19 | async function main() { 20 | const packagesPath = resolve(__dirname, '../packages') 21 | for (const pkg of await fs.readdir(packagesPath)) { 22 | const pkgJson = await readPackageJSON(resolve(packagesPath, pkg, 'package.json')) 23 | const tgzPath = resolve(packagesPath, pkg, `${pkg}-${pkgJson.version}.tgz`) 24 | if (await isExists(tgzPath)) { 25 | PKG_MAP.set(pkg, tgzPath) 26 | } 27 | } 28 | const playgroundPath = resolve(__dirname, '../playground') 29 | for (const ex of await fs.readdir(playgroundPath)) { 30 | const examplePath = resolve(playgroundPath, ex, 'package.json') 31 | const pkgJson = await readPackageJSON(examplePath) 32 | for (const [pkg, tgzPath] of PKG_MAP.entries()) { 33 | if (tgzPath && pkgJson.dependencies) { 34 | pkgJson.dependencies[`${pkg}`] = `file:${tgzPath}` 35 | } 36 | } 37 | await writePackageJSON(examplePath, pkgJson) 38 | } 39 | } 40 | 41 | main().catch(err => { 42 | console.error(err) 43 | process.exit(1) 44 | }) 45 | -------------------------------------------------------------------------------- /packages/vue-i18n-routing/src/vue-i18n.d.ts: -------------------------------------------------------------------------------- 1 | import type { LocaleObject } from 'vue-i18n-routing' 2 | 3 | export interface I18nRoutingCustomProperties { 4 | /** 5 | * List of locales 6 | * 7 | * @remarks 8 | * Can either be an array of string codes (e.g. `['en', 'fr']`) or an array of {@link LocaleObject} for more complex configurations 9 | */ 10 | readonly locales: string[] | LocaleObject[] 11 | /** 12 | * List of locale codes 13 | */ 14 | readonly localeCodes: string[] 15 | /** 16 | * Base URL that is used in generating canonical links 17 | */ 18 | baseUrl: string 19 | } 20 | 21 | declare module 'vue-i18n' { 22 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 23 | export interface ExportedGlobalComposer extends I18nRoutingCustomProperties {} 24 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 25 | export interface VueI18n extends I18nRoutingCustomProperties {} 26 | } 27 | 28 | declare module 'vue-i18n-bridge' { 29 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 30 | export interface ExportedGlobalComposer extends I18nRoutingCustomProperties {} 31 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 32 | export interface VueI18n extends I18nRoutingCustomProperties {} 33 | } 34 | 35 | declare module '@intlify/vue-i18n-bridge' { 36 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 37 | export interface ExportedGlobalComposer extends I18nRoutingCustomProperties {} 38 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 39 | export interface VueI18n extends I18nRoutingCustomProperties {} 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - '**' 7 | tags: 8 | - 'v*' 9 | permissions: 10 | contents: write 11 | packages: write 12 | 13 | jobs: 14 | release: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout codes 18 | uses: actions/checkout@v4 19 | 20 | - name: Enable corepack 21 | run: corepack enable 22 | 23 | - name: Setup Node 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 18 27 | 28 | - name: Install dependencies 29 | run: pnpm install --no-frozen-lockfile 30 | 31 | - name: Extract version tag 32 | if: startsWith( github.ref, 'refs/tags/v' ) 33 | uses: jungwinter/split@v2 34 | id: split 35 | with: 36 | msg: ${{ github.ref }} 37 | separator: '/' 38 | 39 | - name: Create Github Release 40 | run: gh release create ${{ steps.split.outputs._2 }} --generate-notes 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | 44 | - name: Generate changelog 45 | run: npx gh-changelogen --repo=intlify/routing --tag=${{ steps.split.outputs._2 }} 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | 49 | - name: Commit changelog 50 | uses: stefanzweifel/git-auto-commit-action@v4 51 | with: 52 | branch: main 53 | file_pattern: '*.md' 54 | commit_message: 'chore: sync changelog' 55 | 56 | - name: Publish package 57 | run: | 58 | ./scripts/release.sh 59 | env: 60 | NODE_AUTH_TOKEN: ${{secrets.NODE_AUTH_TOKEN}} 61 | -------------------------------------------------------------------------------- /packages/vue-i18n-routing/src/extends/__test__/router.test.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from '@intlify/vue-i18n-bridge' 2 | import { createMemoryHistory, createRouter as _createRouter } from '@intlify/vue-router-bridge' 3 | import { vi, describe, it, assert, afterEach, expect } from 'vitest' 4 | 5 | import { registerGlobalOptions, createRouter, getGlobalOptions } from '../router' 6 | 7 | describe('registerGlobalOptions', () => { 8 | afterEach(() => { 9 | vi.restoreAllMocks() 10 | }) 11 | 12 | it('should be worked', () => { 13 | const spy = vi.spyOn(console, 'warn') 14 | 15 | const router = _createRouter({ 16 | routes: [], 17 | history: createMemoryHistory() 18 | }) 19 | 20 | registerGlobalOptions(router, { localeCodes: ['en', 'ja'] }) 21 | const { localeCodes } = getGlobalOptions(router) 22 | assert.deepEqual(localeCodes, ['en', 'ja']) 23 | 24 | registerGlobalOptions(router, { localeCodes: ['en', 'ja'] }) 25 | expect(spy).toHaveBeenCalledOnce() 26 | expect(spy.mock.calls[0][0]).toContain('already registered global options') 27 | }) 28 | }) 29 | 30 | describe('createRouter', () => { 31 | it('should be created Vue Router v4 instance', () => { 32 | const i18n = createI18n({ legacy: false, locale: 'en' }) 33 | const router = createRouter(i18n, { 34 | version: 4, 35 | locales: ['en', 'ja'], 36 | routes: [], 37 | history: createMemoryHistory() 38 | }) 39 | 40 | // check router instance 41 | assert.isNotNull(router) 42 | assert.isDefined(router.isReady) 43 | // check router extending for internal 44 | const { localeCodes } = getGlobalOptions(router) 45 | assert.deepEqual(localeCodes, ['en', 'ja']) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /packages/vue-i18n-routing/scripts/vitest.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any, vue/one-component-per-file */ 2 | 3 | import { defineComponent, createApp, h, provide, ref } from 'vue-demi' 4 | 5 | import type { InjectionKey, Ref } from 'vue-demi' 6 | 7 | type InstanceType = V extends { new (...arg: any[]): infer X } ? X : never 8 | type VM = InstanceType & { unmount(): void } 9 | type Plugins = [...any] 10 | 11 | export function mount(Comp: V, plugins: Plugins[] = []) { 12 | const el = document.createElement('div') 13 | const app = createApp(Comp as any) 14 | 15 | for (const plugin of plugins) { 16 | app.use(plugin[0], plugin[1]) 17 | } 18 | 19 | // @ts-ignore 20 | const unmount = () => app.unmount(el) 21 | const comp = app.mount(el) as any as VM 22 | comp.unmount = unmount 23 | return comp 24 | } 25 | 26 | export function useSetup(setup: () => V, plugins: Plugins[] = []) { 27 | const Comp = defineComponent({ 28 | setup, 29 | render() { 30 | return h('div', []) 31 | } 32 | }) 33 | 34 | return mount(Comp, plugins) 35 | } 36 | 37 | export const Key: InjectionKey> = Symbol('num') 38 | 39 | export function useInjectedSetup(setup: () => V, plugins: any[] = []) { 40 | const Comp = defineComponent({ 41 | setup, 42 | render() { 43 | return h('div', []) 44 | } 45 | }) 46 | 47 | const Provider = defineComponent({ 48 | components: { Comp }, 49 | setup() { 50 | provide(Key, ref(1)) 51 | }, 52 | render() { 53 | return h('div', []) 54 | } 55 | }) 56 | 57 | return mount(Provider, plugins) 58 | } 59 | 60 | /* eslint-enable @typescript-eslint/no-explicit-any, vue/one-component-per-file */ 61 | -------------------------------------------------------------------------------- /playground/vue2/src/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "App": { 3 | "msg": "You did it!", 4 | "home": "Home", 5 | "about": "About" 6 | }, 7 | "views": { 8 | "AboutView": "This is an about page" 9 | }, 10 | "components": { 11 | "HelloWorld": "You've successfully created a project with {vite} + {vue}. What's next?", 12 | "TheWelcome": { 13 | "documentation": { 14 | "title": "Documentation", 15 | "official": "official documentation", 16 | "message": "Vue's {official} provides you with all information you need to get started." 17 | }, 18 | "tooling": { 19 | "title": "tooling", 20 | "message": "This project is served and bundled with {vite}. The recommended IDE setup is {vscode} + {volar}. If you need to test your components and web pages, check out {cypress} and {testing}.{newline} More instructions are available in {readme}." 21 | }, 22 | "ecosystem": { 23 | "title": "Ecosystem", 24 | "message": "Get official tools and libraries for your project: {state}, {router}, {test}, and {devtools}. If you need more resources, we suggest paying {awesome} a visit." 25 | }, 26 | "community": { 27 | "title": "Community", 28 | "mailing": "our mailing list", 29 | "message": "Got stuck? Ask your question on {discord}, our official Discord server, or {stackoverflow}. You should also subscribe to {news} and follow the official {twitter} twitter account for latest news in the Vue world." 30 | }, 31 | "support": { 32 | "title": "Support Vue", 33 | "sponsor": "becoming a sponsor", 34 | "message": "As an independent project, Vue relies on community backing for its sustainability. You can help us by {sponsor}." 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /playground/vue3/src/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "App": { 3 | "msg": "You did it!", 4 | "home": "Home", 5 | "about": "About" 6 | }, 7 | "views": { 8 | "AboutView": "This is an about page" 9 | }, 10 | "components": { 11 | "HelloWorld": "You've successfully created a project with {vite} + {vue}. What's next?", 12 | "TheWelcome": { 13 | "documentation": { 14 | "title": "Documentation", 15 | "official": "official documentation", 16 | "message": "Vue's {official} provides you with all information you need to get started." 17 | }, 18 | "tooling": { 19 | "title": "tooling", 20 | "message": "This project is served and bundled with {vite}. The recommended IDE setup is {vscode} + {volar}. If you need to test your components and web pages, check out {cypress} and {testing}.{newline} More instructions are available in {readme}." 21 | }, 22 | "ecosystem": { 23 | "title": "Ecosystem", 24 | "message": "Get official tools and libraries for your project: {state}, {router}, {test}, and {devtools}. If you need more resources, we suggest paying {awesome} a visit." 25 | }, 26 | "community": { 27 | "title": "Community", 28 | "mailing": "our mailing list", 29 | "message": "Got stuck? Ask your question on {discord}, our official Discord server, or {stackoverflow}. You should also subscribe to {news} and follow the official {twitter} twitter account for latest news in the Vue world." 30 | }, 31 | "support": { 32 | "title": "Support Vue", 33 | "sponsor": "becoming a sponsor", 34 | "message": "As an independent project, Vue relies on community backing for its sustainability. You can help us by {sponsor}." 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /playground/vue2-options/src/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "App": { 3 | "msg": "You did it!", 4 | "home": "Home", 5 | "about": "About" 6 | }, 7 | "views": { 8 | "AboutView": "This is an about page" 9 | }, 10 | "components": { 11 | "HelloWorld": "You've successfully created a project with {vite} + {vue}. What's next?", 12 | "TheWelcome": { 13 | "documentation": { 14 | "title": "Documentation", 15 | "official": "official documentation", 16 | "message": "Vue's {official} provides you with all information you need to get started." 17 | }, 18 | "tooling": { 19 | "title": "tooling", 20 | "message": "This project is served and bundled with {vite}. The recommended IDE setup is {vscode} + {volar}. If you need to test your components and web pages, check out {cypress} and {testing}.{newline} More instructions are available in {readme}." 21 | }, 22 | "ecosystem": { 23 | "title": "Ecosystem", 24 | "message": "Get official tools and libraries for your project: {state}, {router}, {test}, and {devtools}. If you need more resources, we suggest paying {awesome} a visit." 25 | }, 26 | "community": { 27 | "title": "Community", 28 | "mailing": "our mailing list", 29 | "message": "Got stuck? Ask your question on {discord}, our official Discord server, or {stackoverflow}. You should also subscribe to {news} and follow the official {twitter} twitter account for latest news in the Vue world." 30 | }, 31 | "support": { 32 | "title": "Support Vue", 33 | "sponsor": "becoming a sponsor", 34 | "message": "As an independent project, Vue relies on community backing for its sustainability. You can help us by {sponsor}." 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /playground/vue3-options/src/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "App": { 3 | "msg": "You did it!", 4 | "home": "Home", 5 | "about": "About" 6 | }, 7 | "views": { 8 | "AboutView": "This is an about page" 9 | }, 10 | "components": { 11 | "HelloWorld": "You've successfully created a project with {vite} + {vue}. What's next?", 12 | "TheWelcome": { 13 | "documentation": { 14 | "title": "Documentation", 15 | "official": "official documentation", 16 | "message": "Vue's {official} provides you with all information you need to get started." 17 | }, 18 | "tooling": { 19 | "title": "tooling", 20 | "message": "This project is served and bundled with {vite}. The recommended IDE setup is {vscode} + {volar}. If you need to test your components and web pages, check out {cypress} and {testing}.{newline} More instructions are available in {readme}." 21 | }, 22 | "ecosystem": { 23 | "title": "Ecosystem", 24 | "message": "Get official tools and libraries for your project: {state}, {router}, {test}, and {devtools}. If you need more resources, we suggest paying {awesome} a visit." 25 | }, 26 | "community": { 27 | "title": "Community", 28 | "mailing": "our mailing list", 29 | "message": "Got stuck? Ask your question on {discord}, our official Discord server, or {stackoverflow}. You should also subscribe to {news} and follow the official {twitter} twitter account for latest news in the Vue world." 30 | }, 31 | "support": { 32 | "title": "Support Vue", 33 | "sponsor": "becoming a sponsor", 34 | "message": "As an independent project, Vue relies on community backing for its sustainability. You can help us by {sponsor}." 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /playground/vue2/src/components/WelcomeItem.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 87 | -------------------------------------------------------------------------------- /playground/vue3/src/components/WelcomeItem.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 87 | -------------------------------------------------------------------------------- /playground/vue2-options/src/components/WelcomeItem.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 87 | -------------------------------------------------------------------------------- /playground/vue3-options/src/components/WelcomeItem.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 87 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 New feature proposal 2 | description: Propose a new feature 3 | labels: [feature] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for your interest in the project and taking the time to fill out this feature report! 9 | - type: textarea 10 | id: feature-description 11 | attributes: 12 | label: Clear and concise description of the problem 13 | description: 'As a developer using @intlify/routing I want [goal / wish] so that [benefit]. If you intend to submit a PR for this issue, tell us in the description. Thanks!' 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: suggested-solution 18 | attributes: 19 | label: Suggested solution 20 | description: 'In module [xy] we could provide following implementation...' 21 | validations: 22 | required: true 23 | - type: textarea 24 | id: alternative 25 | attributes: 26 | label: Alternative 27 | description: Clear and concise description of any alternative solutions or features you've considered. 28 | - type: textarea 29 | id: additional-context 30 | attributes: 31 | label: Additional context 32 | description: Any other context or screenshots about the feature request here. 33 | - type: checkboxes 34 | id: checkboxes 35 | attributes: 36 | label: Validations 37 | description: Before submitting the issue, please make sure you do the following 38 | options: 39 | - label: Follow our [Code of Conduct](https://github.com/intlify/routing/blob/main/CODE_OF_CONDUCT.md) 40 | required: true 41 | - label: Read the [Contributing Guide](https://github.com/intlify/routing/blob/main/CONTRIBUTING.md). 42 | required: true 43 | - label: Check that there isn't already an issue that request the same feature to avoid creating a duplicate. 44 | required: true 45 | -------------------------------------------------------------------------------- /playground/vue2/src/components/icons/IconEcosystem.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /playground/vue3/src/components/icons/IconEcosystem.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /playground/vue2-options/src/components/icons/IconEcosystem.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /playground/vue3-options/src/components/icons/IconEcosystem.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | - name: feature 2 | description: Includes new features 3 | color: 'ffff00' 4 | - name: bug 5 | description: Includes new features 6 | color: 'ee0701' 7 | - name: improvement 8 | description: Includes backwards-compatible fixes 9 | color: '1d76db' 10 | - name: breaking 11 | description: Includes backwards-incompatible fixes 12 | color: 'b60205' 13 | - name: refactoring 14 | description: A code change that neither fixes a bug nor adds a feature 15 | color: 'fbca04' 16 | - name: security 17 | description: Security fixes 18 | color: 'b60205' 19 | - name: documentation 20 | description: Includes documetation fixes 21 | color: '5319e7' 22 | - name: example 23 | description: Includes example and demo code fixes 24 | color: 'db0875' 25 | - name: deprecated 26 | description: Includes deprecate fixes 27 | color: 'f7ffa8' 28 | - name: performance 29 | description: Includes performance fixes 30 | color: 'cc317c' 31 | - name: i18n 32 | description: Includes internationalization fixes 33 | color: 'ffd412' 34 | - name: a11y 35 | description: Inlucdes accessibility fixes 36 | color: '0000ff' 37 | - name: dependency 38 | description: Includes dependency fixes 39 | color: 'ffbce7' 40 | - name: todo 41 | description: todo tasks 42 | color: 'c2e0c6' 43 | - name: duplicate 44 | description: This issue or Pull Request already exists 45 | color: 'ededed' 46 | - name: help wanted 47 | description: Extra attention is needed 48 | color: 'e99695' 49 | - name: good first issue 50 | description: Good for newcomers 51 | color: '7057ff' 52 | - name: 'status: abandoned' 53 | description: The issue or Pull Request is wontfix 54 | color: '000000' 55 | - name: 'status: blocked' 56 | description: Progress on the issue is Blocked 57 | color: 'ee0701' 58 | - name: 'status: in progress' 59 | description: Work in Progress 60 | color: 'cccccc' 61 | - name: 'status: proposal' 62 | description: Request for comments 63 | color: 'd4c5f9' 64 | - name: 'status: pull request welcome' 65 | description: Welcome to Pull Request 66 | color: '2E7733' 67 | - name: 'status: review needed' 68 | description: Request for review 69 | color: 'fbca04' 70 | - name: 'status: need more repro codes or info' 71 | description: Lacks enough info to make progress 72 | color: 'F9C90A' 73 | -------------------------------------------------------------------------------- /playground/vue3/src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | position: relative; 59 | font-weight: normal; 60 | } 61 | 62 | body { 63 | min-height: 100vh; 64 | color: var(--color-text); 65 | background: var(--color-background); 66 | transition: color 0.5s, background-color 0.5s; 67 | line-height: 1.6; 68 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 69 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 70 | font-size: 15px; 71 | text-rendering: optimizeLegibility; 72 | -webkit-font-smoothing: antialiased; 73 | -moz-osx-font-smoothing: grayscale; 74 | } 75 | -------------------------------------------------------------------------------- /playground/vue2/src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dadarkrk-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | position: relative; 59 | font-weight: normal; 60 | } 61 | 62 | body { 63 | min-height: 100vh; 64 | color: var(--color-text); 65 | background: var(--color-background); 66 | transition: color 0.5s, background-color 0.5s; 67 | line-height: 1.6; 68 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 69 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 70 | font-size: 15px; 71 | text-rendering: optimizeLegibility; 72 | -webkit-font-smoothing: antialiased; 73 | -moz-osx-font-smoothing: grayscale; 74 | } 75 | -------------------------------------------------------------------------------- /playground/vue3-options/src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | position: relative; 59 | font-weight: normal; 60 | } 61 | 62 | body { 63 | min-height: 100vh; 64 | color: var(--color-text); 65 | background: var(--color-background); 66 | transition: color 0.5s, background-color 0.5s; 67 | line-height: 1.6; 68 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 69 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 70 | font-size: 15px; 71 | text-rendering: optimizeLegibility; 72 | -webkit-font-smoothing: antialiased; 73 | -moz-osx-font-smoothing: grayscale; 74 | } 75 | -------------------------------------------------------------------------------- /playground/vue2-options/src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dadarkrk-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | position: relative; 59 | font-weight: normal; 60 | } 61 | 62 | body { 63 | min-height: 100vh; 64 | color: var(--color-text); 65 | background: var(--color-background); 66 | transition: color 0.5s, background-color 0.5s; 67 | line-height: 1.6; 68 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 69 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 70 | font-size: 15px; 71 | text-rendering: optimizeLegibility; 72 | -webkit-font-smoothing: antialiased; 73 | -moz-osx-font-smoothing: grayscale; 74 | } 75 | -------------------------------------------------------------------------------- /packages/vue-i18n-routing/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | 4 | import { Extractor, ExtractorConfig } from '@microsoft/api-extractor' 5 | import rimraf from 'rimraf' 6 | import { defineConfig } from 'vite' 7 | import dts from 'vite-plugin-dts' 8 | 9 | import pkg from './package.json' 10 | 11 | // like `__dirname` of Node.js 12 | const currentDir = fileURLToPath(new URL('.', import.meta.url)) 13 | 14 | // https://vitejs.dev/config/ 15 | export default defineConfig({ 16 | define: { 17 | // https://github.com/vitejs/vite/pull/9130/files#diff-4a49b457ff0fce41061261711fc1d5e95385af670880cebae5fca2d4807103f0R193 18 | 'process.env.NODE_ENV': JSON.stringify('production'), 19 | __VERSION__: JSON.stringify(pkg.version) 20 | }, 21 | build: { 22 | minify: false, 23 | lib: { 24 | entry: resolve(currentDir, './src/index.ts'), 25 | name: 'VueI18nRouting', 26 | formats: ['es', 'cjs', 'iife'] 27 | }, 28 | rollupOptions: { 29 | external: ['vue-demi', '@intlify/vue-router-bridge', '@intlify/vue-i18n-bridge'], 30 | output: { 31 | globals: { 32 | 'vue-demi': 'VueDemi', 33 | '@intlify/vue-i18n-bridge': 'VueI18n', 34 | '@intlify/vue-router-bridge': 'VueRouter' 35 | } 36 | } 37 | } 38 | }, 39 | plugins: [ 40 | dts({ 41 | // NOTE: 42 | // if vite-plugin-dts will fix the hoisting for multiple files, we should switch to it. 43 | // we're facing the broken file when some d.ts .files is hosted. 44 | // rollupTypes: true, 45 | afterBuild: () => { 46 | const extractorConfigPath = resolve(currentDir, `api-extractor.json`) 47 | const extractorConfig = ExtractorConfig.loadFileAndPrepare(extractorConfigPath) 48 | const extractorResult = Extractor.invoke(extractorConfig, { 49 | localBuild: true, 50 | showVerboseMessages: true 51 | }) 52 | 53 | if (extractorResult.succeeded) { 54 | console.log(`API Extractor completed successfully.`) 55 | } else { 56 | console.error( 57 | `API Extractor completed with ${extractorResult.errorCount} errors` + 58 | ` and ${extractorResult.warningCount} warnings` 59 | ) 60 | } 61 | 62 | rimraf.sync(resolve(currentDir, './dist/src')) 63 | } 64 | }) 65 | ] 66 | }) 67 | -------------------------------------------------------------------------------- /.eslintrc-i18n.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const localeDir = 4 | process.env.LINT_I18N === 'vue2' ? './playground/vue2/src/locales/*.json' : './playground/vue3/src/locales/*.json' 5 | 6 | const messageSyntaxVersion = process.env.LINT_I18N === 'vue2' ? '^8.0.0' : '^9.0.0' 7 | console.log(`lint i18n target: ${process.env.LINT_I18N}`) 8 | console.log(`localeDir: ${localeDir}`) 9 | console.log(`messageSyntaxVersion: ${messageSyntaxVersion}`) 10 | 11 | module.exports = { 12 | root: true, 13 | env: { 14 | node: true 15 | }, 16 | extends: ['plugin:@intlify/vue-i18n/recommended', 'plugin:import/recommended', 'plugin:import/typescript'], 17 | plugins: ['@typescript-eslint'], 18 | parser: 'vue-eslint-parser', 19 | parserOptions: { 20 | parser: '@typescript-eslint/parser', 21 | sourceType: 'module' 22 | }, 23 | rules: { 24 | '@intlify/vue-i18n/no-raw-text': 'error', 25 | '@intlify/vue-i18n/no-dynamic-keys': 'warn', 26 | '@intlify/vue-i18n/no-missing-keys-in-other-locales': 'error', 27 | '@intlify/vue-i18n/no-unused-keys': 'error', 28 | 'import/order': [ 29 | 'warn', 30 | { 31 | groups: ['builtin', 'external', 'parent', 'sibling', 'index', 'object', 'type'], 32 | pathGroups: [ 33 | { 34 | pattern: '{vue**}', 35 | group: 'builtin', 36 | position: 'before' 37 | }, 38 | { 39 | pattern: '{vite**}', 40 | group: 'builtin', 41 | position: 'before' 42 | }, 43 | { 44 | pattern: '@/**', 45 | group: 'parent', 46 | position: 'before' 47 | } 48 | ], 49 | pathGroupsExcludedImportTypes: ['builtin'], 50 | alphabetize: { 51 | order: 'asc' 52 | }, 53 | 'newlines-between': 'always' 54 | } 55 | ] 56 | }, 57 | settings: { 58 | 'vue-i18n': { 59 | localeDir, 60 | messageSyntaxVersion 61 | }, 62 | 'import/resolver': { 63 | /** 64 | * NOTE: https://github.com/import-js/eslint-import-resolver-typescript#configuration 65 | */ 66 | typescript: { 67 | project: ['playground/**/tsconfig.json'] 68 | } 69 | } 70 | }, 71 | overrides: [ 72 | { 73 | files: ['*.json', '*.json5'], 74 | extends: ['plugin:@intlify/vue-i18n/base'] 75 | }, 76 | { 77 | files: ['*.yaml', '*.yml'], 78 | extends: ['plugin:@intlify/vue-i18n/base'] 79 | } 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /playground/vue3-options/src/App.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 40 | 41 | 104 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | root: true, 5 | env: { 6 | node: true 7 | }, 8 | extends: [ 9 | 'plugin:vue/vue3-recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:@typescript-eslint/eslint-recommended', 12 | 'plugin:import/recommended', 13 | 'plugin:import/typescript', 14 | 'plugin:prettier/recommended', 15 | 'prettier' 16 | ], 17 | plugins: ['@typescript-eslint', 'eslint-plugin-tsdoc'], 18 | parser: 'vue-eslint-parser', 19 | parserOptions: { 20 | parser: '@typescript-eslint/parser', 21 | sourceType: 'module' 22 | }, 23 | rules: { 24 | /** 25 | * NOTE: https://typescript-eslint.io/rules/ 26 | */ 27 | '@typescript-eslint/consistent-type-assertions': 'warn', 28 | '@typescript-eslint/consistent-type-definitions': 'warn', 29 | '@typescript-eslint/consistent-generic-constructors': 'warn', 30 | // '@typescript-eslint/consistent-type-exports': 'warn', 31 | '@typescript-eslint/consistent-type-imports': ['warn', { prefer: 'type-imports', disallowTypeAnnotations: false }], 32 | '@typescript-eslint/ban-ts-comment': 'off', 33 | '@typescript-eslint/triple-slash-reference': 'off', 34 | '@typescript-eslint/no-non-null-assertion': 'off', 35 | '@typescript-eslint/no-empty-function': 'off', 36 | /** 37 | * NOTE: https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/order.md#importorder-enforce-a-convention-in-module-import-order 38 | */ 39 | 'import/order': [ 40 | 'warn', 41 | { 42 | groups: ['builtin', 'external', 'parent', 'sibling', 'index', 'object', 'type'], 43 | pathGroups: [ 44 | { 45 | pattern: '{vue**}', 46 | group: 'builtin', 47 | position: 'before' 48 | }, 49 | { 50 | pattern: '{vite**}', 51 | group: 'builtin', 52 | position: 'before' 53 | } 54 | ], 55 | pathGroupsExcludedImportTypes: ['builtin'], 56 | alphabetize: { 57 | order: 'asc' 58 | }, 59 | 'newlines-between': 'always' 60 | } 61 | ], 62 | 'tsdoc/syntax': 'warn', 63 | }, 64 | settings: { 65 | 'import/resolver': { 66 | /** 67 | * NOTE: https://github.com/import-js/eslint-import-resolver-typescript#configuration 68 | */ 69 | typescript: { 70 | // project: ['tsconfig.json', 'packages/**/tsconfig.json', 'playground/**/tsconfig.json'] 71 | project: ['tsconfig.json', 'packages/**/tsconfig.json'] 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /playground/vue2-options/src/App.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 42 | 43 | 106 | -------------------------------------------------------------------------------- /playground/vue2/src/App.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 40 | 41 | 104 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug report 2 | description: Report an issue 3 | labels: ['status: review needed'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to fill out this bug report! 9 | - type: textarea 10 | id: bug-description 11 | attributes: 12 | label: Describe the bug 13 | description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description. Thanks! 14 | placeholder: Bug description 15 | validations: 16 | required: true 17 | - type: input 18 | id: reproduction 19 | attributes: 20 | label: Reproduction 21 | description: A [minimal reproduction](https://stackoverflow.com/help/minimal-reproducible-example) is **required**, otherwise the issue might be closed without further notice. [**Why & How?**](https://antfu.me/posts/why-reproductions-are-required) 22 | placeholder: Reproduction 23 | validations: 24 | required: true 25 | - type: textarea 26 | id: system-info 27 | attributes: 28 | label: System Info 29 | description: Output of `npx envinfo --system --binaries --browsers` 30 | render: Shell 31 | placeholder: System, Binaries, Browsers 32 | validations: 33 | required: true 34 | - type: dropdown 35 | id: package-manager 36 | attributes: 37 | label: Used Package Manager 38 | description: Select the used package manager 39 | options: 40 | - npm 41 | - yarn 42 | - pnpm 43 | - other (if you use other package manager, please describe at the `Additional context`) 44 | - n/a 45 | validations: 46 | required: true 47 | - type: textarea 48 | id: additional-context 49 | attributes: 50 | label: Additional context 51 | description: Any other context or screenshots about the bug report here. 52 | - type: checkboxes 53 | id: checkboxes 54 | attributes: 55 | label: Validations 56 | description: Before submitting the issue, please make sure you do the following 57 | options: 58 | - label: Follow our [Code of Conduct](https://github.com/intlify/routing/blob/main/CODE_OF_CONDUCT.md) 59 | required: true 60 | - label: Read the [Contributing Guide](https://github.com/intlify/routing/blob/main/CONTRIBUTING.md). 61 | required: true 62 | - label: Check that there isn't already an issue that reports the same bug to avoid creating a duplicate. 63 | required: true 64 | - label: Check that this is a concrete bug. For Q&A, please open a GitHub Discussion instead. 65 | required: true 66 | - label: The provided reproduction is a [minimal reproducible](https://stackoverflow.com/help/minimal-reproducible-example) of the bug. 67 | required: true 68 | -------------------------------------------------------------------------------- /packages/vue-i18n-routing/src/vue.d.ts: -------------------------------------------------------------------------------- 1 | import type { Route, RouteLocationNormalizedLoaded } from '@intlify/vue-router-bridge' 2 | import type { 3 | LocalePathFunction, 4 | LocaleRouteFunction, 5 | LocaleLocationFunction, 6 | SwitchLocalePathFunction, 7 | I18nHeadOptions, 8 | I18nHeadMetaInfo 9 | } from 'vue-i18n-routing' 10 | 11 | interface I18nMixinAPI { 12 | /** 13 | * Get route base name 14 | * 15 | * @param givenRoute - A route object, optional, if not provided, the current route will be used 16 | * 17 | * @returns The route base name, if route name is not defined, return `undefined` 18 | */ 19 | getRouteBaseName: (givenRoute?: Route | RouteLocationNormalizedLoaded) => string | undefined 20 | /** 21 | * Resolve locale path 22 | * 23 | * @param route - A route location. The path or name of the route or an object for more complex routes 24 | * @param locale - A locale code, if not specified, uses the current locale 25 | * 26 | * @returns Returns the localized URL for a given route 27 | */ 28 | localePath: LocalePathFunction 29 | /** 30 | * Resolve locale route 31 | * 32 | * @param route - A route location. The path or name of the route or an object for more complex routes 33 | * @param locale - A locale code, if not specified, uses the current locale 34 | * 35 | * @returns Returns the route object for a given route, the route object is resolved by vue-router rather than just a full route path. 36 | */ 37 | localeRoute: LocaleRouteFunction 38 | /** 39 | * Resolve locale location 40 | * 41 | * @param route - A route location. The path or name of the route or an object for more complex routes 42 | * @param locale - A locale code, if not specified, uses the current locale 43 | * 44 | * @returns Returns the location object for a given route, the location object is resolved by vue-router rather than just a full route path. 45 | */ 46 | localeLocation: LocaleLocationFunction 47 | /** 48 | * Switch locale path 49 | * 50 | * @param locale - A locale code, if not specified, uses the current locale 51 | * 52 | * @returns Returns a link to the current route in another language 53 | */ 54 | switchLocalePath: SwitchLocalePathFunction 55 | /** 56 | * Resolve locale head meta info 57 | * 58 | * @param options - A options of locale head meta info, optional, see about {@link I18nHeadOptions} 59 | * 60 | * @returns Returns a {@link I18nHeadMetaInfo | head meta info} 61 | */ 62 | localeHead: (options?: I18nHeadOptions) => I18nHeadMetaInfo 63 | } 64 | 65 | declare module 'vue/types/vue' { 66 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 67 | export interface Vue extends I18nMixinAPI {} 68 | } 69 | 70 | declare module '@vue/runtime-core' { 71 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 72 | export interface ComponentCustomProperties extends I18nMixinAPI {} 73 | } 74 | -------------------------------------------------------------------------------- /packages/vue-i18n-routing/src/composables/__test__/__snapshots__/head.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`useLocaleHead > basic > should be worked > en 1`] = ` 4 | { 5 | "htmlAttrs": { 6 | "dir": "ltr", 7 | "lang": "en-US", 8 | }, 9 | "link": [ 10 | { 11 | "hid": "i18n-alt-en", 12 | "href": "http://localhost:8080/en/about", 13 | "hreflang": "en", 14 | "rel": "alternate", 15 | }, 16 | { 17 | "hid": "i18n-alt-en-US", 18 | "href": "http://localhost:8080/en/about", 19 | "hreflang": "en-US", 20 | "rel": "alternate", 21 | }, 22 | { 23 | "hid": "i18n-alt-ja", 24 | "href": "http://localhost:8080/ja/about", 25 | "hreflang": "ja", 26 | "rel": "alternate", 27 | }, 28 | { 29 | "hid": "i18n-alt-ja-JP", 30 | "href": "http://localhost:8080/ja/about", 31 | "hreflang": "ja-JP", 32 | "rel": "alternate", 33 | }, 34 | { 35 | "hid": "i18n-can", 36 | "href": "http://localhost:8080/en/about", 37 | "rel": "canonical", 38 | }, 39 | ], 40 | "meta": [ 41 | { 42 | "content": "http://localhost:8080/en/about", 43 | "hid": "i18n-og-url", 44 | "property": "og:url", 45 | }, 46 | { 47 | "content": "en_US", 48 | "hid": "i18n-og", 49 | "property": "og:locale", 50 | }, 51 | { 52 | "content": "ja_JP", 53 | "hid": "i18n-og-alt-ja-JP", 54 | "property": "og:locale:alternate", 55 | }, 56 | ], 57 | } 58 | `; 59 | 60 | exports[`useLocaleHead > basic > should be worked > ja 1`] = ` 61 | { 62 | "htmlAttrs": { 63 | "dir": "ltr", 64 | "lang": "ja-JP", 65 | }, 66 | "link": [ 67 | { 68 | "hid": "i18n-alt-en", 69 | "href": "http://localhost:8080/en", 70 | "hreflang": "en", 71 | "rel": "alternate", 72 | }, 73 | { 74 | "hid": "i18n-alt-en-US", 75 | "href": "http://localhost:8080/en", 76 | "hreflang": "en-US", 77 | "rel": "alternate", 78 | }, 79 | { 80 | "hid": "i18n-alt-ja", 81 | "href": "http://localhost:8080/ja", 82 | "hreflang": "ja", 83 | "rel": "alternate", 84 | }, 85 | { 86 | "hid": "i18n-alt-ja-JP", 87 | "href": "http://localhost:8080/ja", 88 | "hreflang": "ja-JP", 89 | "rel": "alternate", 90 | }, 91 | { 92 | "hid": "i18n-can", 93 | "href": "http://localhost:8080/ja", 94 | "rel": "canonical", 95 | }, 96 | ], 97 | "meta": [ 98 | { 99 | "content": "http://localhost:8080/ja", 100 | "hid": "i18n-og-url", 101 | "property": "og:url", 102 | }, 103 | { 104 | "content": "ja_JP", 105 | "hid": "i18n-og", 106 | "property": "og:locale", 107 | }, 108 | { 109 | "content": "en_US", 110 | "hid": "i18n-og-alt-en-US", 111 | "property": "og:locale:alternate", 112 | }, 113 | ], 114 | } 115 | `; 116 | -------------------------------------------------------------------------------- /packages/vue-i18n-routing/src/compatibles/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import type { Strategies, Directions, LocalizeRoutesPrefixableOptions } from '../types' 3 | import type { Locale } from '@intlify/vue-i18n-bridge' 4 | import type { Route, RouteLocationNormalizedLoaded, Router, VueRouter } from '@intlify/vue-router-bridge' 5 | 6 | /** 7 | * Routing Proxy 8 | */ 9 | export interface RoutingProxy { 10 | i18n: any 11 | getRouteBaseName: any 12 | localePath: any 13 | localeRoute: any 14 | localeLocation: any 15 | resolveRoute: any 16 | switchLocalePath: any 17 | localeHead: any 18 | route: Route | RouteLocationNormalizedLoaded 19 | router: Router | VueRouter 20 | defaultLocale?: string 21 | localeCodes?: string[] 22 | strategy?: Strategies 23 | defaultDirection?: Directions 24 | defaultLocaleRouteNameSuffix?: string 25 | trailingSlash?: boolean 26 | routesNameSeparator?: string 27 | prefixable?: Prefixable 28 | switchLocalePathIntercepter?: SwitchLocalePathIntercepter 29 | dynamicRouteParamsKey?: string | symbol 30 | } 31 | 32 | /** 33 | * SEO Attribute options. 34 | * 35 | * @public 36 | */ 37 | export interface SeoAttributesOptions { 38 | /** 39 | * An array of strings corresponding to query params you would like to include in your canonical URL. 40 | * 41 | * @defaultValue [] 42 | */ 43 | canonicalQueries?: string[] 44 | } 45 | 46 | /** 47 | * Options for {@link localeHead} function. 48 | * 49 | * @public 50 | */ 51 | export interface I18nHeadOptions { 52 | /** 53 | * Adds a `dir` attribute to the HTML element. 54 | * 55 | * @defaultValue false 56 | */ 57 | addDirAttribute?: boolean 58 | /** 59 | * Adds various SEO attributes. 60 | * 61 | * @defaultValue false 62 | */ 63 | addSeoAttributes?: boolean | SeoAttributesOptions 64 | /** 65 | * Identifier attribute of `` tag 66 | * 67 | * @defaultValue 'hid' 68 | */ 69 | identifierAttribute?: string 70 | } 71 | 72 | /** 73 | * Meta attributes for head properties. 74 | * 75 | * @public 76 | */ 77 | export type MetaAttrs = Record 78 | 79 | /** 80 | * I18n header meta info. 81 | * 82 | * @public 83 | */ 84 | export interface I18nHeadMetaInfo { 85 | htmlAttrs?: MetaAttrs 86 | meta?: MetaAttrs[] 87 | link?: MetaAttrs[] 88 | } 89 | 90 | /** 91 | * Route path prefix judgment options used in {@link Prefixable} 92 | */ 93 | export type PrefixableOptions = Pick 94 | 95 | /** 96 | * Route path prefix judgment logic in {@link resolveRoute} function 97 | */ 98 | export type Prefixable = (optons: PrefixableOptions) => boolean 99 | 100 | /** 101 | * The intercept handler which is called in {@link switchLocalePath} function 102 | */ 103 | export type SwitchLocalePathIntercepter = (path: string, locale: Locale) => string 104 | 105 | /* eslint-enable @typescript-eslint/no-explicit-any */ 106 | -------------------------------------------------------------------------------- /packages/vue-i18n-routing/src/composables/head.ts: -------------------------------------------------------------------------------- 1 | import { useI18n } from '@intlify/vue-i18n-bridge' 2 | import { useRoute, useRouter } from '@intlify/vue-router-bridge' 3 | import { ref, watchEffect, isVue3, onUnmounted } from 'vue-demi' 4 | 5 | import { localeHead } from '../compatibles' 6 | import { inBrowser, toRawRoute } from '../utils' 7 | 8 | import type { I18nHeadOptions, I18nHeadMetaInfo } from '../compatibles' 9 | import type { I18nCommonRoutingOptionsWithComposable } from '../utils' 10 | import type { 11 | Router, 12 | VueRouter, 13 | RouteLocationNormalizedLoaded, 14 | RouteLocationNormalized, 15 | Route 16 | } from '@intlify/vue-router-bridge' 17 | import type { Ref } from 'vue-demi' 18 | 19 | /** 20 | * The `useLocaleHead` composable returns localized head properties for locale-related aspects. 21 | * 22 | * @param options - An options, see about details {@link I18nHeadOptions} and {@link I18nCommonRoutingOptionsWithComposable} 23 | * 24 | * @returns The localized {@link I18nHeadMetaInfo | head properties} with Vue `ref`. 25 | * 26 | * @public 27 | */ 28 | export function useLocaleHead({ 29 | addDirAttribute = false, 30 | addSeoAttributes = false, 31 | identifierAttribute = 'hid', 32 | strategy = undefined, 33 | defaultLocale = undefined, 34 | route = useRoute(), 35 | router = useRouter(), 36 | i18n = useI18n() 37 | }: Pick & 38 | I18nHeadOptions = {}): Ref { 39 | type R = Router | VueRouter 40 | const _router = router as R 41 | 42 | const metaObject: Ref = ref({ 43 | htmlAttrs: {}, 44 | link: [], 45 | meta: [] 46 | }) 47 | 48 | function cleanMeta() { 49 | metaObject.value = { 50 | htmlAttrs: {}, 51 | link: [], 52 | meta: [] 53 | } 54 | } 55 | 56 | function updateMeta(_route: RouteLocationNormalizedLoaded | Route) { 57 | metaObject.value = Reflect.apply( 58 | localeHead, 59 | { 60 | router, 61 | route: _route, 62 | i18n, 63 | defaultLocale, 64 | strategy 65 | }, 66 | [{ addDirAttribute, addSeoAttributes, identifierAttribute }] 67 | ) as I18nHeadMetaInfo 68 | } 69 | 70 | if (inBrowser) { 71 | if (isVue3) { 72 | const stop = watchEffect(() => { 73 | cleanMeta() 74 | updateMeta(toRawRoute(_router.currentRoute)) 75 | }) 76 | onUnmounted(() => stop()) 77 | } else { 78 | /** 79 | * NOTE: 80 | * In vue 2 + `@vue/compoistion-api`, useRoute (`$route`) cannot be watched. 81 | * For this reason, use `afterEach` to work around it. 82 | */ 83 | const handler = _router.afterEach( 84 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 85 | (to: Route | RouteLocationNormalized, from: Route | RouteLocationNormalized) => { 86 | cleanMeta() 87 | updateMeta(to) 88 | } 89 | ) 90 | onUnmounted(() => handler()) 91 | updateMeta(route as Route) 92 | } 93 | } else { 94 | updateMeta(toRawRoute(_router.currentRoute)) 95 | } 96 | 97 | return metaObject 98 | } 99 | -------------------------------------------------------------------------------- /playground/vue3/src/App.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 52 | 53 | 116 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | permissions: 11 | contents: read 12 | packages: read 13 | 14 | jobs: 15 | lint: 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest] 20 | node: [18] 21 | steps: 22 | - name: Checkout codes 23 | uses: actions/checkout@v4 24 | 25 | - name: Enable corepack 26 | run: corepack enable 27 | 28 | - name: Setup node 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: ${{ matrix.node }} 32 | cache: 'pnpm' 33 | 34 | - name: Install dependencies 35 | run: pnpm install --no-frozen-lockfile 36 | 37 | - name: Build 38 | run: pnpm build 39 | 40 | - name: Lint codes 41 | run: pnpm lint 42 | 43 | unit: 44 | runs-on: ${{ matrix.os }} 45 | strategy: 46 | matrix: 47 | os: [ubuntu-latest] 48 | node: [18] 49 | steps: 50 | - name: Checkout codes 51 | uses: actions/checkout@v4 52 | 53 | - name: Enable corepack 54 | run: corepack enable 55 | 56 | - name: Setup node 57 | uses: actions/setup-node@v4 58 | with: 59 | node-version: ${{ matrix.node }} 60 | cache: 'pnpm' 61 | 62 | - name: Install dependencies 63 | run: pnpm install --no-frozen-lockfile 64 | 65 | - name: Build 66 | run: pnpm build 67 | 68 | - name: Test 69 | run: pnpm test 70 | 71 | e2e: 72 | runs-on: ${{ matrix.os }} 73 | strategy: 74 | matrix: 75 | os: [ubuntu-latest] 76 | node: [18] 77 | steps: 78 | - name: Checkout codes 79 | uses: actions/checkout@v4 80 | 81 | - name: Enable corepack 82 | run: corepack enable 83 | 84 | - name: Setup node 85 | uses: actions/setup-node@v4 86 | with: 87 | node-version: ${{ matrix.node }} 88 | cache: 'pnpm' 89 | 90 | - name: Install dependencies 91 | run: pnpm install --no-frozen-lockfile 92 | 93 | - name: Build 94 | run: pnpm build 95 | 96 | - name: Run e2e test 97 | run: ./scripts/e2e.sh 98 | 99 | edge-release: 100 | needs: 101 | - lint 102 | - unit 103 | - e2e 104 | runs-on: ${{ matrix.os }} 105 | strategy: 106 | matrix: 107 | os: [ubuntu-latest] 108 | node: [18] 109 | steps: 110 | - name: Checkout codes 111 | uses: actions/checkout@v4 112 | 113 | - name: Enable corepack 114 | run: corepack enable 115 | 116 | - name: Setup node 117 | uses: actions/setup-node@v4 118 | with: 119 | node-version: ${{ matrix.node }} 120 | cache: 'pnpm' 121 | 122 | - name: Install dependencies 123 | run: pnpm install --no-frozen-lockfile 124 | 125 | - name: Release Edge version 126 | if: | 127 | github.event_name == 'push' && 128 | !startsWith(github.event.head_commit.message, '[skip-release]') && 129 | !startsWith(github.event.head_commit.message, 'chore') && 130 | !startsWith(github.event.head_commit.message, 'docs') 131 | run: ./scripts/release-edge.sh 132 | env: 133 | NODE_AUTH_TOKEN: ${{secrets.NODE_AUTH_TOKEN}} 134 | -------------------------------------------------------------------------------- /packages/vue-i18n-routing/src/composables/__test__/head.test.ts: -------------------------------------------------------------------------------- 1 | import { createI18n, useI18n } from '@intlify/vue-i18n-bridge' 2 | import { createMemoryHistory, useRoute, useRouter } from '@intlify/vue-router-bridge' 3 | import { describe, it, expect, assert } from 'vitest' 4 | 5 | import { useSetup } from '../../../scripts/vitest' 6 | import { createRouter } from '../../extends/router' 7 | import { useLocaleHead } from '../head' 8 | 9 | describe('useLocaleHead', () => { 10 | describe('basic', () => { 11 | it('should be worked', async () => { 12 | const i18n = createI18n({ legacy: false, locale: 'en' }) 13 | const router = createRouter(i18n, { 14 | version: 4, 15 | locales: [ 16 | { 17 | code: 'en', 18 | iso: 'en-US', 19 | dir: 'ltr' 20 | }, 21 | { 22 | code: 'ja', 23 | iso: 'ja-JP' 24 | } 25 | ], 26 | baseUrl: 'http://localhost:8080', 27 | routes: [ 28 | { path: '/', name: 'index', component: { template: '
index
' } }, 29 | { path: '/about', name: 'about', component: { template: '
About
' } }, 30 | { path: '/:pathMatch(.*)*', name: 'not-found', component: { template: '
Not Found
' } } 31 | ], 32 | history: createMemoryHistory() 33 | }) 34 | await router.push('/en/about') 35 | 36 | const vm = useSetup(() => { 37 | const route = useRoute() 38 | const router = useRouter() 39 | const i18n = useI18n() 40 | const head = useLocaleHead({ addDirAttribute: true, addSeoAttributes: true, route, router, i18n }) 41 | expect(head.value).toMatchSnapshot(i18n.locale.value) 42 | assert.equal(head.value.htmlAttrs!.lang, 'en-US') 43 | return { 44 | i18n, 45 | head 46 | } 47 | }, [[router], [i18n]]) 48 | 49 | await router.push('/ja') 50 | expect(vm.head).toMatchSnapshot(vm.i18n.locale.value) 51 | assert.equal(vm.head.htmlAttrs!.lang, 'ja-JP') 52 | }) 53 | }) 54 | 55 | describe('identifierAttribute option', () => { 56 | it('should be worked', async () => { 57 | const i18n = createI18n({ legacy: false, locale: 'en' }) 58 | const router = createRouter(i18n, { 59 | version: 4, 60 | locales: [ 61 | { 62 | code: 'en', 63 | iso: 'en-US', 64 | dir: 'ltr' 65 | }, 66 | { 67 | code: 'ja', 68 | iso: 'ja-JP' 69 | } 70 | ], 71 | baseUrl: 'http://localhost:8080', 72 | routes: [ 73 | { path: '/', name: 'index', component: { template: '
index
' } }, 74 | { path: '/about', name: 'about', component: { template: '
About
' } }, 75 | { path: '/:pathMatch(.*)*', name: 'not-found', component: { template: '
Not Found
' } } 76 | ], 77 | history: createMemoryHistory() 78 | }) 79 | await router.push('/en/about') 80 | 81 | const vm = useSetup(() => { 82 | const route = useRoute() 83 | const router = useRouter() 84 | const i18n = useI18n() 85 | const head = useLocaleHead({ addSeoAttributes: true, identifierAttribute: 'key', route, router, i18n }) 86 | return { 87 | i18n, 88 | head 89 | } 90 | }, [[router], [i18n]]) 91 | 92 | await router.push('/ja') 93 | for (const m of vm.head.meta || []) { 94 | expect(m).toHaveProperty('key') 95 | } 96 | }) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at kawakazu80@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /packages/vue-i18n-routing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-i18n-routing", 3 | "description": "The i18n routing with using vue-i18n", 4 | "version": "1.2.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "typecheck": "tsc -p . --noEmit", 9 | "switch:2": "vue-demi-switch 2 vue2 && vue-router-switch 3 vue-router3 && vue-i18n-switch 8 vue-i18n-legacy", 10 | "switch:3": "vue-demi-switch 3 && vue-router-switch 4 && vue-i18n-switch 9", 11 | "test": "vitest run", 12 | "test:watch": "vitest", 13 | "test:cover": "vitest --coverage", 14 | "build:docs": "api-docs-gen ./temp/vue-i18n-routing.api.json -c ./docsgen.config.js -o ./ -g noprefix" 15 | }, 16 | "dependencies": { 17 | "@intlify/shared": "^9.4.1", 18 | "@intlify/vue-i18n-bridge": "^1.1.0", 19 | "@intlify/vue-router-bridge": "^1.1.0", 20 | "ufo": "^1.2.0", 21 | "vue-demi": ">=0.13.5 < 1.0.0" 22 | }, 23 | "devDependencies": { 24 | "@microsoft/api-extractor": "^7.31.1", 25 | "api-docs-gen": "^0.4.0", 26 | "typescript": "^5.1.6", 27 | "vite": "^4.4.9", 28 | "vite-plugin-dts": "^3.5.1", 29 | "vitest": "^0.34.1", 30 | "vue": "^3.2.27", 31 | "vue-i18n": "npm:vue-i18n@9.4.0", 32 | "vue-i18n-bridge": "next", 33 | "vue-i18n-legacy": "npm:vue-i18n@8", 34 | "vue-router": "^4.1.5", 35 | "vue-router3": "npm:vue-router@3.6.5", 36 | "vue-template-compiler": "^2.6.14", 37 | "vue2": "npm:vue@2.6.14" 38 | }, 39 | "peerDependencies": { 40 | "@vue/composition-api": "^1.0.0-rc.1", 41 | "vue": "^2.6.14 || ^2.7.0 || ^3.2.0", 42 | "vue-i18n": "^8.26.1 || >=9.2.0", 43 | "vue-i18n-bridge": ">=9.2.0", 44 | "vue-router": "^3.5.3 || ^3.6.0 || ^4.0.0" 45 | }, 46 | "peerDependenciesMeta": { 47 | "vue": { 48 | "optional": true 49 | }, 50 | "vue-i18n": { 51 | "optional": true 52 | }, 53 | "vue-i18n-bridge": { 54 | "optional": true 55 | }, 56 | "vue-router": { 57 | "optional": true 58 | }, 59 | "@vue/composition-api": { 60 | "optional": true 61 | } 62 | }, 63 | "keywords": [ 64 | "i18n", 65 | "internationalization", 66 | "intlify", 67 | "routing" 68 | ], 69 | "files": [ 70 | "dist", 71 | "index.mjs", 72 | "types.d.ts", 73 | "LICENSE", 74 | "README.md" 75 | ], 76 | "main": "./dist/vue-i18n-routing.js", 77 | "module": "./dist/vue-i18n-routing.mjs", 78 | "unpkg": "dist/vue-i18n-routing.iife.js", 79 | "jsdelivr": "dist/vue-i18n-routing.iife.js", 80 | "types": "dist/vue-i18n-routing.d.ts", 81 | "exports": { 82 | ".": { 83 | "import": { 84 | "node": "./index.mjs", 85 | "default": "./dist/vue-i18n-routing.mjs" 86 | }, 87 | "require": "./dist/vue-i18n-routing.js", 88 | "types": "./dist/vue-i18n-routing.d.ts" 89 | }, 90 | "./vue-i18n": { 91 | "types": "./dist/vue-i18n.d.ts" 92 | }, 93 | "./types": { 94 | "types": "./types.d.ts" 95 | }, 96 | "./package.json": "./package.json", 97 | "./dist/*": "./dist/*" 98 | }, 99 | "license": "MIT", 100 | "author": { 101 | "name": "kazuya kawaguchi", 102 | "email": "kawakazu80@gmail.com" 103 | }, 104 | "funding": "https://github.com/sponsors/kazupon", 105 | "homepage": "https://github.com/intlify/routing/tree/main/packages/vue-i18n-routing#readme", 106 | "repository": { 107 | "type": "git", 108 | "url": "git+https://github.com/intlify/routing.git", 109 | "directory": "packages/vue-i18n-routing" 110 | }, 111 | "bugs": { 112 | "url": "https://github.com/intlify/routing/issues" 113 | }, 114 | "engines": { 115 | "node": ">= 14.6" 116 | }, 117 | "publishConfig": { 118 | "access": "public" 119 | }, 120 | "sideEffects": false 121 | } 122 | -------------------------------------------------------------------------------- /packages/vue-i18n-routing/src/__test__/__snapshots__/resolve.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`localizeRoutes > Route optiosn resolver: routing disable > should be disabled routing 1`] = ` 4 | [ 5 | { 6 | "name": "home", 7 | "path": "/", 8 | }, 9 | { 10 | "name": "about", 11 | "path": "/about", 12 | }, 13 | ] 14 | `; 15 | 16 | exports[`localizeRoutes > basic > should be localized routing 1`] = ` 17 | [ 18 | { 19 | "name": "home___en", 20 | "path": "/en", 21 | }, 22 | { 23 | "name": "home___ja", 24 | "path": "/ja", 25 | }, 26 | { 27 | "name": "about___en", 28 | "path": "/en/about", 29 | }, 30 | { 31 | "name": "about___ja", 32 | "path": "/ja/about", 33 | }, 34 | ] 35 | `; 36 | 37 | exports[`localizeRoutes > has children > should be localized routing 1`] = ` 38 | [ 39 | { 40 | "children": [ 41 | { 42 | "name": "user-profile___en", 43 | "path": "profile", 44 | }, 45 | { 46 | "name": "user-posts___en", 47 | "path": "posts", 48 | }, 49 | ], 50 | "name": "user___en", 51 | "path": "/en/user/:id", 52 | }, 53 | { 54 | "children": [ 55 | { 56 | "name": "user-profile___ja", 57 | "path": "profile", 58 | }, 59 | { 60 | "name": "user-posts___ja", 61 | "path": "posts", 62 | }, 63 | ], 64 | "name": "user___ja", 65 | "path": "/ja/user/:id", 66 | }, 67 | ] 68 | `; 69 | 70 | exports[`localizeRoutes > route name separator > should be localized routing 1`] = ` 71 | [ 72 | { 73 | "name": "home__en", 74 | "path": "/en", 75 | }, 76 | { 77 | "name": "home__ja", 78 | "path": "/ja", 79 | }, 80 | { 81 | "name": "about__en", 82 | "path": "/en/about", 83 | }, 84 | { 85 | "name": "about__ja", 86 | "path": "/ja/about", 87 | }, 88 | ] 89 | `; 90 | 91 | exports[`localizeRoutes > strategy: "no_prefix" > should be localized routing 1`] = ` 92 | [ 93 | { 94 | "name": "home", 95 | "path": "/", 96 | }, 97 | { 98 | "name": "about", 99 | "path": "/about", 100 | }, 101 | ] 102 | `; 103 | 104 | exports[`localizeRoutes > strategy: "prefix" > should be localized routing 1`] = ` 105 | [ 106 | { 107 | "name": "home", 108 | "path": "/", 109 | }, 110 | { 111 | "name": "home___en", 112 | "path": "/en", 113 | }, 114 | { 115 | "name": "home___ja", 116 | "path": "/ja", 117 | }, 118 | { 119 | "name": "about", 120 | "path": "/about", 121 | }, 122 | { 123 | "name": "about___en", 124 | "path": "/en/about", 125 | }, 126 | { 127 | "name": "about___ja", 128 | "path": "/ja/about", 129 | }, 130 | ] 131 | `; 132 | 133 | exports[`localizeRoutes > strategy: "prefix_and_default" > should be localized routing 1`] = ` 134 | [ 135 | { 136 | "name": "home___en___default", 137 | "path": "/", 138 | }, 139 | { 140 | "name": "home___en", 141 | "path": "/en", 142 | }, 143 | { 144 | "name": "home___ja", 145 | "path": "/ja", 146 | }, 147 | { 148 | "name": "about___en___default", 149 | "path": "/about", 150 | }, 151 | { 152 | "name": "about___en", 153 | "path": "/en/about", 154 | }, 155 | { 156 | "name": "about___ja", 157 | "path": "/ja/about", 158 | }, 159 | ] 160 | `; 161 | 162 | exports[`localizeRoutes > strategy: "prefix_except_default" > should be localized routing 1`] = ` 163 | [ 164 | { 165 | "name": "home___en", 166 | "path": "/", 167 | }, 168 | { 169 | "name": "home___ja", 170 | "path": "/ja", 171 | }, 172 | { 173 | "name": "about___en", 174 | "path": "/about", 175 | }, 176 | { 177 | "name": "about___ja", 178 | "path": "/ja/about", 179 | }, 180 | ] 181 | `; 182 | 183 | exports[`localizeRoutes > trailing slash > should be localized routing 1`] = ` 184 | [ 185 | { 186 | "name": "home___en", 187 | "path": "/en/", 188 | }, 189 | { 190 | "name": "home___ja", 191 | "path": "/ja/", 192 | }, 193 | { 194 | "name": "about___en", 195 | "path": "/en/about/", 196 | }, 197 | { 198 | "name": "about___ja", 199 | "path": "/ja/about/", 200 | }, 201 | ] 202 | `; 203 | -------------------------------------------------------------------------------- /scripts/bump.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs' 2 | import { execSync } from 'child_process' 3 | import { resolve, dirname } from 'pathe' 4 | import { globby } from 'globby' 5 | import yaml from 'js-yaml' 6 | 7 | const _dirname = dirname(new URL(import.meta.url).pathname) 8 | 9 | type Deps = { 10 | name: string 11 | range: string 12 | type: string 13 | } 14 | type DepsReviver = (deps: Deps) => Deps | undefined 15 | 16 | async function loadPackage(dir: string) { 17 | const pkgPath = resolve(dir, 'package.json') 18 | const data = JSON.parse(await fs.readFile(pkgPath, 'utf-8').catch(() => '{}')) 19 | const save = () => fs.writeFile(pkgPath, JSON.stringify(data, null, 2) + '\n') 20 | // const save = () => 21 | // console.log(`package: ${dir}`, JSON.stringify(data, null, 2)) 22 | 23 | const updateDeps = (reviver: DepsReviver) => { 24 | for (const type of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) { 25 | if (!data[type]) { 26 | continue 27 | } 28 | for (const e of Object.entries(data[type])) { 29 | const dep = { name: e[0], range: e[1] as string, type } 30 | delete data[type][dep.name] 31 | const updated = reviver(dep) || dep 32 | data[updated.type] = data[updated.type] || {} 33 | data[updated.type][updated.name] = updated.range 34 | } 35 | } 36 | } 37 | 38 | return { 39 | dir, 40 | data, 41 | save, 42 | updateDeps 43 | } 44 | } 45 | 46 | type ThenArg = T extends PromiseLike ? U : T 47 | type Package = ThenArg> 48 | 49 | async function loadWorkspaceData(path: string): Promise { 50 | const workspacesYaml = await fs.readFile(path, 'utf-8') 51 | const data = (yaml.load(workspacesYaml) as any).packages 52 | return Array.isArray(data) ? (data as string[]) : [] 53 | } 54 | 55 | async function loadWorkspace(dir: string, workspaces: string[] = []) { 56 | const workspacePkg = await loadPackage(dir) 57 | const workspacesYaml = await loadWorkspaceData(resolve(_dirname, '../pnpm-workspace.yaml')) 58 | workspacePkg.data.workspaces = [...workspaces, ...workspacesYaml] 59 | 60 | const pkgDirs = await globby([...(workspacePkg.data.workspaces || []), ...workspaces], { 61 | onlyDirectories: true 62 | }) 63 | 64 | const packages: Package[] = [] 65 | 66 | for (const pkgDir of pkgDirs) { 67 | const pkg = await loadPackage(pkgDir) 68 | if (!pkg.data.name) { 69 | continue 70 | } 71 | packages.push(pkg) 72 | } 73 | 74 | const find = (name: string) => { 75 | const pkg = packages.find(pkg => pkg.data.name === name) 76 | if (!pkg) { 77 | throw new Error('Workspace package not found: ' + name) 78 | } 79 | return pkg 80 | } 81 | 82 | const rename = (from: string, to: string) => { 83 | find(from).data.name = to 84 | for (const pkg of packages) { 85 | pkg.updateDeps(dep => { 86 | return undefined 87 | }) 88 | } 89 | } 90 | 91 | const setVersion = (name: string, newVersion: string) => { 92 | find(name).data.version = newVersion 93 | for (const pkg of packages) { 94 | pkg.updateDeps(dep => { 95 | if (dep.name === name) { 96 | dep.range = newVersion 97 | } 98 | return undefined 99 | }) 100 | } 101 | } 102 | 103 | const save = async () => { 104 | await workspacePkg.save() 105 | return Promise.all(packages.map(pkg => pkg.save())) 106 | } 107 | 108 | return { 109 | dir, 110 | workspacePkg, 111 | packages, 112 | save, 113 | find, 114 | rename, 115 | setVersion 116 | } 117 | } 118 | 119 | async function main() { 120 | const workspace = await loadWorkspace(process.cwd()) 121 | 122 | const commit = execSync('git rev-parse --short HEAD').toString('utf-8').trim() 123 | const release = `${workspace.workspacePkg.data.version}-${commit}` 124 | 125 | for (const pkg of workspace.packages.filter(p => !p.data.private)) { 126 | workspace.setVersion(pkg.data.name, release) 127 | if (pkg.data.name !== 'vue-i18n-routing') { 128 | workspace.rename(pkg.data.name, pkg.data.name) 129 | } 130 | } 131 | 132 | await workspace.save() 133 | } 134 | 135 | main().catch(err => { 136 | console.error(err) 137 | process.exit(1) 138 | }) 139 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | - [Contributing Guide](#contributing-guide) 4 | - [General Guidelines](#general-guidelines) 5 | - [Pull Request Guidelines](#pull-request-guidelines) 6 | - [Work Step Example](#work-step-example) 7 | - [Development Setup](#development-setup) 8 | - [Scripts](#scripts) 9 | - [`pnpm build`](#pnpm-build) 10 | - [`pnpm test`](#pnpm-test) 11 | - [Project Structure](#project-structure) 12 | - [Contributing Tests](#contributing-tests) 13 | - [Financial Contribution](#financial-contribution) 14 | - [Credits](#credits) 15 | 16 | ## General Guidelines 17 | 18 | Thanks for understanding that English is used as a shared language in this repository. 19 | Maintainers do not use machine translation to avoid miscommunication due to error in translation. 20 | If description of issue / PR are written in non-English languages, those may be closed. 21 | 22 | It is of course fine to use non-English language, when you open a PR to translate documents and communicates with other users in same language. 23 | 24 | ## Pull Request Guidelines 25 | 26 | - The `main` branch is the latest stable version release. All development should be done in dedicated branches. 27 | 28 | - Checkout a topic branch from the relevant branch, e.g. `main`, and merge back against that branch. 29 | 30 | - Work in the `src` folder and **DO NOT** checkin `dist` in the commits. 31 | 32 | - If adding new feature: 33 | 34 | - Add accompanying test case. 35 | - Provide convincing reason to add this feature. Ideally you should open a suggestion issue first and have it greenlighted before working on it. 36 | 37 | - If fixing a bug: 38 | 39 | - Provide detailed description of the bug in the PR. Live demo preferred. 40 | - Add appropriate test coverage if applicable. 41 | 42 | - It's OK to have multiple small commits as you work on the PR - we will let GitHub automatically squash it before merging. 43 | 44 | - Make sure `pnpm test` passes. (see [development setup](#development-setup)) 45 | 46 | ### Work Step Example 47 | 48 | - Fork the repository from [intlify/routing](https://github.com/intlify/routing) ! 49 | - Create your topic branch from `main`: `git branch my-new-topic origin/main` 50 | - Add codes and pass tests ! 51 | - Commit your changes: `git commit -am 'Add some topic'` 52 | - Push to the branch: `git push origin my-new-topic` 53 | - Submit a pull request to `main` branch of `intlify/routing` repository ! 54 | 55 | ## Development Setup 56 | 57 | You will need [Node.js](http://nodejs.org) **version 14.16+**, and [PNPM](https://pnpm.io). 58 | 59 | After cloning the repo, run: 60 | 61 | ```sh 62 | $ pnpm i # install the dependencies of the project 63 | ``` 64 | 65 | A high level overview of tools used: 66 | 67 | - [TypeScript](https://www.typescriptlang.org/) as the development language 68 | - [Vite](https://vitejs.dev/) for bundling 69 | - [Vitest](https://vitest.dev/) for testing 70 | - [ESLint](https://eslint.org/) for code linting 71 | - [Prettier](https://prettier.io/) for code formatting 72 | 73 | ## Scripts 74 | 75 | ### `pnpm build` 76 | 77 | The `build` script builds all public packages (currently `packages/vue-i18n-routing` only) via `scripts/build.ts`. 78 | 79 | ### `pnpm test` 80 | 81 | The `test` script calls the following npm scripts: 82 | 83 | - `typecheck`: type check in `vue-i18n-routing` (currently) 84 | - `test:unit`: test in `vue-i18n-routing` (currently) 85 | - `test:unit:vue2`: test in `vue-i18n-routing` (currently) for Vue 2 86 | 87 | ## Project Structure 88 | 89 | This repository employs a [monorepo](https://en.wikipedia.org/wiki/Monorepo) setup which hosts a number of associated packages under the `packages` directory: 90 | 91 | - `vue-i18n-routing`: The i18n routing with using vue-i18n 92 | 93 | ## Contributing Tests 94 | 95 | Unit tests are collocated with directories named `__test__`. Consult the [Vitest docs](https://vitest.dev/api/) and existing test cases for how to write new test specs. 96 | 97 | ## Financial Contribution 98 | 99 | As a pure community-driven project without major corporate backing, we also welcome financial contributions via GitHub Sponsors 100 | 101 | - [Become a backer or sponsor on GitHub Sponsors](https://github.com/sponsors/kazupon) 102 | 103 | Funds donated via GitHub Sponsors and Patreon go to support kazuya kawaguchi full-time work on Intlify. 104 | 105 | ## Credits 106 | 107 | Thank you to all the people who have already contributed to Intlify project and my OSS work ! 108 | -------------------------------------------------------------------------------- /packages/vue-i18n-routing/src/compatibles/utils.ts: -------------------------------------------------------------------------------- 1 | import { assign } from '@intlify/shared' 2 | import { isVue3 } from 'vue-demi' 3 | 4 | import { 5 | DEFAULT_ROUTES_NAME_SEPARATOR, 6 | DEFAULT_LOCALE_ROUTE_NAME_SUFFIX, 7 | DEFAULT_LOCALE, 8 | DEFAULT_DETECTION_DIRECTION, 9 | DEFAULT_TRAILING_SLASH, 10 | DEFAULT_STRATEGY, 11 | DEFAULT_DYNAMIC_PARAMS_KEY 12 | } from '../constants' 13 | import { getGlobalOptions } from '../extends/router' 14 | 15 | import { DefaultPrefixable, DefaultSwitchLocalePathIntercepter } from './routing' 16 | 17 | import type { RoutingProxy } from './types' 18 | import type { I18nRoutingGlobalOptions } from '../extends/router' 19 | import type { Strategies } from '../types' 20 | import type { Locale } from '@intlify/vue-i18n-bridge' 21 | import type { 22 | VueRouter, 23 | Router, 24 | Route, 25 | RouteLocationNormalizedLoaded, 26 | RouteLocationPathRaw 27 | } from '@intlify/vue-router-bridge' 28 | 29 | export function getI18nRoutingOptions( 30 | router: Router | VueRouter, 31 | proxy: RoutingProxy, 32 | { 33 | defaultLocale = DEFAULT_LOCALE, 34 | defaultDirection = DEFAULT_DETECTION_DIRECTION, 35 | defaultLocaleRouteNameSuffix = DEFAULT_LOCALE_ROUTE_NAME_SUFFIX, 36 | routesNameSeparator = DEFAULT_ROUTES_NAME_SEPARATOR, 37 | strategy = DEFAULT_STRATEGY, 38 | trailingSlash = DEFAULT_TRAILING_SLASH, 39 | localeCodes = [], 40 | prefixable = DefaultPrefixable, 41 | switchLocalePathIntercepter = DefaultSwitchLocalePathIntercepter, 42 | dynamicRouteParamsKey = DEFAULT_DYNAMIC_PARAMS_KEY 43 | }: I18nRoutingGlobalOptions = {} 44 | ): Required> & 45 | Pick { 46 | const options = getGlobalOptions(router) 47 | return { 48 | defaultLocale: proxy.defaultLocale || options.defaultLocale || defaultLocale, 49 | defaultDirection: proxy.defaultDirection || options.defaultDirection || defaultDirection, 50 | defaultLocaleRouteNameSuffix: 51 | proxy.defaultLocaleRouteNameSuffix || options.defaultLocaleRouteNameSuffix || defaultLocaleRouteNameSuffix, 52 | routesNameSeparator: proxy.routesNameSeparator || options.routesNameSeparator || routesNameSeparator, 53 | strategy: proxy.strategy || options.strategy || strategy, 54 | trailingSlash: proxy.trailingSlash || options.trailingSlash || trailingSlash, 55 | localeCodes: proxy.localeCodes || options.localeCodes || localeCodes, 56 | prefixable: proxy.prefixable || options.prefixable || prefixable, 57 | switchLocalePathIntercepter: 58 | proxy.switchLocalePathIntercepter || options.switchLocalePathIntercepter || switchLocalePathIntercepter, 59 | dynamicRouteParamsKey: proxy.dynamicRouteParamsKey || options.dynamicRouteParamsKey || dynamicRouteParamsKey, 60 | dynamicParamsInterceptor: options.dynamicParamsInterceptor || undefined 61 | } 62 | } 63 | 64 | function split(str: string, index: number) { 65 | const result = [str.slice(0, index), str.slice(index)] 66 | return result 67 | } 68 | 69 | /** 70 | * NOTE: 71 | * Nuxt route uses a proxy with getters for performance reasons (https://github.com/nuxt/nuxt/pull/21957). 72 | * Spreading will result in an empty object, so we make a copy of the route by accessing each getter property by name. 73 | */ 74 | export function routeToObject(route: Route | RouteLocationNormalizedLoaded) { 75 | const { fullPath, query, hash, name, path, params, meta, redirectedFrom, matched } = route 76 | return { 77 | fullPath, 78 | params, 79 | query, 80 | hash, 81 | name, 82 | path, 83 | meta, 84 | matched, 85 | redirectedFrom 86 | } 87 | } 88 | 89 | export type ResolveV3 = ReturnType 90 | export type ResolveV4 = ReturnType 91 | type ResolvedRoute = ResolveV3 | ResolveV4 92 | 93 | export function isV4Route(val: ResolvedRoute): val is ReturnType { 94 | return isVue3 95 | } 96 | 97 | export function isV4Router(val: Router | VueRouter): val is Router { 98 | return isVue3 99 | } 100 | 101 | /** 102 | * NOTE: 103 | * vue-router v4.x `router.resolve` for a non exists path will output a warning. 104 | * `router.hasRoute`, which checks for the route can only be a named route. 105 | * When using the `prefix` strategy, the path specified by `localePath` is specified as a path not prefixed with a locale. 106 | * This will cause vue-router to issue a warning, so we can work-around by using `router.options.routes`. 107 | */ 108 | export function resolve(router: Router | VueRouter, route: RouteLocationPathRaw, strategy: Strategies, locale: Locale) { 109 | if (!isV4Router(router)) { 110 | return router.resolve(route) 111 | } 112 | 113 | if (strategy !== 'prefix') { 114 | return router.resolve(route) 115 | } 116 | 117 | // if (isArray(route.matched) && route.matched.length > 0) { 118 | // return route.matched[0] 119 | // } 120 | 121 | const [rootSlash, restPath] = split(route.path, 1) 122 | const targetPath = `${rootSlash}${locale}${restPath === '' ? restPath : `/${restPath}`}` 123 | const _route = router.options?.routes?.find(r => r.path === targetPath) 124 | 125 | if (_route == null) { 126 | return route 127 | } 128 | 129 | const _resolvableRoute = assign({}, route, _route) 130 | _resolvableRoute.path = targetPath 131 | return router.resolve(_resolvableRoute) 132 | } 133 | -------------------------------------------------------------------------------- /playground/vue3/src/components/TheWelcome.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 131 | -------------------------------------------------------------------------------- /playground/vue2/src/components/TheWelcome.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 131 | --------------------------------------------------------------------------------