Skip to content

Commit 1b5c639

Browse files
committed
localize routes first implementation
1 parent 30de13b commit 1b5c639

File tree

13 files changed

+377
-29
lines changed

13 files changed

+377
-29
lines changed

.prettierrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
semi: false
22
singleQuote: true
3-
printWidth: 80
3+
printWidth: 120
44
trailingComma: "none"
55
endOfLine: "auto"
66
arrowParens: "avoid"

package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,9 @@
103103
"release": "./scripts/release.sh",
104104
"typecheck": "pnpm typecheck --filter vue-i18n-routing",
105105
"test": "pnpm typecheck && pnpm test:cover",
106-
"test:cover": "nyc mocha -r jiti/register -r ./test/hooks/chai.ts 'test/**/*.test.ts'",
107-
"test:unit": "mocha -r jiti/register -r ./test/hooks/chai.ts 'test/**/*.test.ts'",
108-
"test:snap": "UPDATE_SNAPSHOT=* mocha -r jiti/register -r ./test/hooks/chai.ts 'test/**/*.test.ts'"
106+
"test:cover": "nyc mocha -r jiti/register -r ./test/hooks/chai.ts './**/test/**/*.test.ts'",
107+
"test:unit": "mocha -r jiti/register -r ./test/hooks/chai.ts './**/test/**/*.test.ts'",
108+
"test:snap": "UPDATE_SNAPSHOT=* mocha -r jiti/register -r ./test/hooks/chai.ts './**/test/**/*.test.ts'"
109109
},
110110
"workspaces": [
111111
"packages/*"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const VUE_I18N_ROUTING_DEFAULTS = {
2+
defaultLocale: 'en-US',
3+
trailingSlash: false,
4+
routesNameSeparator: '___',
5+
defaultLocaleRouteNameSuffix: 'default'
6+
}

packages/vue-i18n-routing/src/index.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1+
import { VUE_I18N_ROUTING_DEFAULTS } from './constants'
2+
import { localizeRoutes } from './resolve'
3+
14
import type { Plugin } from 'vue-demi'
5+
import type { VueI18nRoute, VueI18nRoutingOptions } from './types'
26

3-
export interface VueI18nRoutingOptions {
4-
routes: []
5-
}
7+
export { localizeRoutes, VueI18nRoutingOptions, VueI18nRoute }
68

79
export const VueI18nRoutingPlugin = function (
810
VueOrApp: any, // eslint-disable-line @typescript-eslint/no-explicit-any
9-
options: any = {} // eslint-disable-line @typescript-eslint/no-explicit-any
11+
options: VueI18nRoutingOptions = {
12+
trailingSlash: VUE_I18N_ROUTING_DEFAULTS.trailingSlash,
13+
routesNameSeparator: VUE_I18N_ROUTING_DEFAULTS.routesNameSeparator
14+
}
1015
) {
1116
// TODO:
1217
console.log('install vue-i18n-rouging!')
+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { adjustRoutePathForTrailingSlash } from './utils'
2+
import { VUE_I18N_ROUTING_DEFAULTS } from './constants'
3+
4+
import type { VueI18nRoute, VueI18nRoutingOptions } from './types'
5+
6+
// type RouteOptions = {
7+
// locales: string[]
8+
// paths: Record<string, any> // eslint-disable-line @typescript-eslint/no-explicit-any
9+
// }
10+
11+
export function localizeRoutes(
12+
routes: VueI18nRoute[],
13+
{
14+
defaultLocale = VUE_I18N_ROUTING_DEFAULTS.defaultLocale,
15+
trailingSlash = VUE_I18N_ROUTING_DEFAULTS.trailingSlash,
16+
routesNameSeparator = VUE_I18N_ROUTING_DEFAULTS.routesNameSeparator,
17+
defaultLocaleRouteNameSuffix = VUE_I18N_ROUTING_DEFAULTS.defaultLocaleRouteNameSuffix,
18+
localeCodes = []
19+
}: VueI18nRoutingOptions = {}
20+
): VueI18nRoute[] {
21+
function makeLocalizedRoutes(
22+
route: VueI18nRoute,
23+
allowedLocaleCodes: string[],
24+
isChild = false,
25+
isExtraPageTree = false
26+
): VueI18nRoute[] {
27+
// skip route localization
28+
if (route.redirect && (!route.component || !route.file)) {
29+
return [route]
30+
}
31+
32+
// TODO: route from options
33+
34+
const targetLocales = allowedLocaleCodes
35+
36+
// TODO: component options
37+
38+
return targetLocales.reduce((_routes, locale) => {
39+
const { name } = route
40+
let { path } = route
41+
const localizedRoute = { ...route }
42+
43+
// make localized page name
44+
if (name) {
45+
localizedRoute.name = `${name}${routesNameSeparator}${locale}`
46+
}
47+
48+
// generate localized children routes
49+
if (route.children) {
50+
localizedRoute.children = route.children.reduce(
51+
(children, child) => [...children, ...makeLocalizedRoutes(child, [locale], true, isExtraPageTree)],
52+
[] as NonNullable<VueI18nRoute['children']>
53+
)
54+
}
55+
56+
// TODO: custom paths
57+
58+
const isDefaultLocale = locale === defaultLocale
59+
if (isDefaultLocale) {
60+
if (!isChild) {
61+
const defaultRoute = { ...localizedRoute, path }
62+
63+
if (name) {
64+
defaultRoute.name = `${localizedRoute.name}${routesNameSeparator}${defaultLocaleRouteNameSuffix}`
65+
}
66+
67+
if (route.children) {
68+
// recreate child routes with default suffix added
69+
defaultRoute.children = []
70+
for (const childRoute of route.children) {
71+
defaultRoute.children = defaultRoute.children.concat(
72+
makeLocalizedRoutes(childRoute as VueI18nRoute, [locale], true, true)
73+
)
74+
}
75+
}
76+
77+
_routes.push(defaultRoute)
78+
} else if (isChild && isExtraPageTree && name) {
79+
localizedRoute.name += `${routesNameSeparator}${defaultLocaleRouteNameSuffix}`
80+
}
81+
}
82+
83+
const isChildWithRelativePath = isChild && !path.startsWith('/')
84+
85+
// add route prefix
86+
const shouldAddPrefix = !isChildWithRelativePath && !isDefaultLocale
87+
if (shouldAddPrefix) {
88+
path = `/${locale}${path}`
89+
}
90+
91+
if (path) {
92+
path = adjustRoutePathForTrailingSlash(path, trailingSlash, isChildWithRelativePath)
93+
}
94+
95+
localizedRoute.path = path
96+
_routes.push(localizedRoute)
97+
98+
return _routes
99+
}, [] as VueI18nRoute[])
100+
}
101+
102+
return routes.reduce(
103+
(localized, route) => [...localized, ...makeLocalizedRoutes(route, localeCodes || [])],
104+
[] as VueI18nRoute[]
105+
)
106+
}
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { RouteConfig as __Route } from 'vue-router3'
2+
3+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4+
type UnionToIntersection<T> = (T extends any ? (k: T) => void : never) extends (k: infer U) => void ? U : never
5+
type _Route = UnionToIntersection<__Route>
6+
7+
/**
8+
* Route config for lagacy vue-router v3
9+
*/
10+
export interface RouteLegacy extends Pick<_Route, Exclude<keyof _Route, 'children' | 'component'>> {
11+
chunkName?: string
12+
chunkNames?: Record<string, string>
13+
component?: _Route['component'] | string
14+
children?: RouteLegacy[]
15+
}
16+
17+
/**
18+
* Route config for vue-router v4
19+
*/
20+
export interface Route {
21+
name?: string
22+
path: string
23+
file?: string // for nuxt bridge & nuxt 3
24+
children?: Route[]
25+
}
26+
27+
/**
28+
* Route config for vue-i18n-routing
29+
*/
30+
export type VueI18nRoute = Route & RouteLegacy & { redirect?: string }
31+
32+
/**
33+
* Vue I18n routing options
34+
*/
35+
export interface VueI18nRoutingOptions {
36+
defaultLocale?: string
37+
localeCodes?: string[]
38+
trailingSlash?: boolean
39+
routesNameSeparator?: string
40+
defaultLocaleRouteNameSuffix?: string
41+
}
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function adjustRoutePathForTrailingSlash(
2+
pagePath: string,
3+
trailingSlash: boolean,
4+
isChildWithRelativePath: boolean
5+
) {
6+
return pagePath.replace(/\/+$/, '') + (trailingSlash ? '/' : '') || (isChildWithRelativePath ? '' : '/')
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { expect } from 'chai'
2+
import { localizeRoutes } from '../src/resolve'
3+
import { VUE_I18N_ROUTING_DEFAULTS } from '../src/constants'
4+
5+
import type { VueI18nRoute } from '../src/types'
6+
7+
describe('localizeRoutes', function () {
8+
describe('basic', function () {
9+
it('should be localized routing', function () {
10+
const routes: VueI18nRoute[] = [
11+
{
12+
path: '/',
13+
name: 'home'
14+
},
15+
{
16+
path: '/about',
17+
name: 'about'
18+
}
19+
]
20+
const localeCodes = ['en', 'ja']
21+
const localizedRoutes = localizeRoutes(routes, { localeCodes })
22+
23+
expect(localizedRoutes.length).to.equal(4)
24+
localeCodes.forEach(locale => {
25+
routes.forEach(route => {
26+
expect(localizedRoutes).to.deep.include({
27+
path: `/${locale}${route.path === '/' ? '' : route.path}`,
28+
name: `${route.name}${VUE_I18N_ROUTING_DEFAULTS.routesNameSeparator}${locale}`
29+
})
30+
})
31+
})
32+
})
33+
})
34+
35+
describe('has children', function () {
36+
it('should be localized routing', function () {
37+
const routes: VueI18nRoute[] = [
38+
{
39+
path: '/user/:id',
40+
name: 'user',
41+
children: [
42+
{
43+
path: 'profile',
44+
name: 'user-profile'
45+
},
46+
{
47+
path: 'posts',
48+
name: 'user-posts'
49+
}
50+
]
51+
}
52+
]
53+
const children: VueI18nRoute[] = routes[0].children
54+
55+
const localeCodes = ['en', 'ja']
56+
const localizedRoutes = localizeRoutes(routes, { localeCodes })
57+
58+
expect(localizedRoutes.length).to.equal(2)
59+
localeCodes.forEach(locale => {
60+
routes.forEach(route => {
61+
expect(localizedRoutes).to.deep.include({
62+
path: `/${locale}${route.path === '/' ? '' : route.path}`,
63+
name: `${route.name}${VUE_I18N_ROUTING_DEFAULTS.routesNameSeparator}${locale}`,
64+
children: children.map(child => ({
65+
path: child.path,
66+
name: `${child.name}${VUE_I18N_ROUTING_DEFAULTS.routesNameSeparator}${locale}`
67+
}))
68+
})
69+
})
70+
})
71+
})
72+
})
73+
74+
describe('trailing slash', function () {
75+
it('should be localized routing', function () {
76+
const routes: VueI18nRoute[] = [
77+
{
78+
path: '/',
79+
name: 'home'
80+
},
81+
{
82+
path: '/about',
83+
name: 'about'
84+
}
85+
]
86+
const localeCodes = ['en', 'ja']
87+
const localizedRoutes = localizeRoutes(routes, { localeCodes, trailingSlash: true })
88+
89+
expect(localizedRoutes.length).to.equal(4)
90+
localeCodes.forEach(locale => {
91+
routes.forEach(route => {
92+
expect(localizedRoutes).to.deep.include({
93+
path: `/${locale}${route.path === '/' ? '' : route.path}/`,
94+
name: `${route.name}${VUE_I18N_ROUTING_DEFAULTS.routesNameSeparator}${locale}`
95+
})
96+
})
97+
})
98+
})
99+
})
100+
101+
describe('route name separator', function () {
102+
it('should be localized routing', function () {
103+
const routes: VueI18nRoute[] = [
104+
{
105+
path: '/',
106+
name: 'home'
107+
},
108+
{
109+
path: '/about',
110+
name: 'about'
111+
}
112+
]
113+
const localeCodes = ['en', 'ja']
114+
const localizedRoutes = localizeRoutes(routes, { localeCodes, routesNameSeparator: '__' })
115+
116+
expect(localizedRoutes.length).to.equal(4)
117+
localeCodes.forEach(locale => {
118+
routes.forEach(route => {
119+
expect(localizedRoutes).to.deep.include({
120+
path: `/${locale}${route.path === '/' ? '' : route.path}`,
121+
name: `${route.name}${'__'}${locale}`
122+
})
123+
})
124+
})
125+
})
126+
})
127+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { assert } from 'chai'
2+
import { adjustRoutePathForTrailingSlash } from '../../src/utils'
3+
4+
describe('adjustRouteDefinitionForTrailingSlash', function () {
5+
describe('pagePath: /foo/bar', function () {
6+
describe('trailingSlash: faawklse, isChildWithRelativePath: true', function () {
7+
it('should be trailed with slash: /foo/bar/', function () {
8+
assert.equal(adjustRoutePathForTrailingSlash('/foo/bar', true, true), '/foo/bar/')
9+
})
10+
})
11+
12+
describe('trailingSlash: false, isChildWithRelativePath: true', function () {
13+
it('should not be trailed with slash: /foo/bar/', function () {
14+
assert.equal(adjustRoutePathForTrailingSlash('/foo/bar', false, true), '/foo/bar')
15+
})
16+
})
17+
18+
describe('trailingSlash: false, isChildWithRelativePath: false', function () {
19+
it('should be trailed with slash: /foo/bar/', function () {
20+
assert.equal(adjustRoutePathForTrailingSlash('/foo/bar', true, false), '/foo/bar/')
21+
})
22+
})
23+
24+
describe('trailingSlash: false, isChildWithRelativePath: false', function () {
25+
it('should not be trailed with slash: /foo/bar/', function () {
26+
assert.equal(adjustRoutePathForTrailingSlash('/foo/bar', false, false), '/foo/bar')
27+
})
28+
})
29+
})
30+
31+
describe('pagePath: /', function () {
32+
describe('trailingSlash: false, isChildWithRelativePath: true', function () {
33+
it('should not be trailed with slash: empty', function () {
34+
assert.equal(adjustRoutePathForTrailingSlash('/', false, true), '')
35+
})
36+
})
37+
})
38+
39+
describe('pagePath: empty', function () {
40+
describe('trailingSlash: true, isChildWithRelativePath: true', function () {
41+
it('should not be trailed with slash: /', function () {
42+
assert.equal(adjustRoutePathForTrailingSlash('', true, true), '/')
43+
})
44+
})
45+
})
46+
})

packages/vue-i18n-routing/vite.config.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ export default defineConfig({
2222
dts({
2323
afterBuild: () => {
2424
const extractorConfigPath = resolve(__dirname, `api-extractor.json`)
25-
const extractorConfig =
26-
ExtractorConfig.loadFileAndPrepare(extractorConfigPath)
25+
const extractorConfig = ExtractorConfig.loadFileAndPrepare(extractorConfigPath)
2726
const extractorResult = Extractor.invoke(extractorConfig, {
2827
localBuild: true,
2928
showVerboseMessages: true

0 commit comments

Comments
 (0)