Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
danielKugler committed Jan 12, 2022
0 parents commit 1b1155f
Show file tree
Hide file tree
Showing 19 changed files with 3,285 additions and 0 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Vue 3 + Typescript + Vite + Router + i18n

This template should help get you started developing with Vue 3, Typescript, lazy loading translation handling and routing in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.

## Recommended IDE Setup

- [VSCode](https://code.visualstudio.com/)

## Type Support For `.vue` Imports in TS

Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's `.vue` type support plugin by running `Volar: Switch TS Plugin on/off` from VSCode command palette.

## i18n suport for vue3
Using [@intlify/vite-plugin-vue-i18n](https://github.com/intlify/vite-plugin-vue-i18n) for handling lazy-loading translations from JSON files.

## i18n routing
Routing width createWebHistory() (no hash) and language param in url.

"THE BEER-WARE LICENSE" (Revision 42):
<[email protected]> wrote this file. As long as you retain this notice you
can do whatever you want with this stuff. If we meet some day, and you think
this stuff is worth it, you can buy me a beer in return.
13 changes: 13 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
47 changes: 47 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "vue3-ts-vite-i18n-router",
"version": "0.0.0",
"author": "[email protected]",
"license": "Beerware",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"lint:script": "eslint --ext .ts,vue --ignore-path .gitignore .",
"lint:style": "stylelint src/**/*.{css,scss,vue}",
"lint:markup": "vue-tsc --noEmit"
},
"dependencies": {
"@vue/compiler-sfc": "^3.2.26",
"axios": "^0.24.0",
"vue": "^3.2.25",
"vue-i18n": "^9.2.0",
"vue-router": "^4.0.12"
},
"devDependencies": {
"@intlify/vite-plugin-vue-i18n": "^3.2.1",
"@typescript-eslint/eslint-plugin": "^5.9.1",
"@typescript-eslint/parser": "^5.9.1",
"@vitejs/plugin-vue": "^2.0.0",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^10.0.0",
"eslint": "^8.6.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.2.0",
"path": "^0.12.7",
"prettier": "^2.5.1",
"rollup-plugin-copy": "^3.4.0",
"sass": "^1.47.0",
"stylelint": "^14.2.0",
"stylelint-config-recommended": "^6.0.0",
"stylelint-config-standard": "^24.0.0",
"typescript": "^4.4.4",
"vite": "^2.7.2",
"vue-tsc": "^0.29.8"
},
"lint-staged": {
"*.{ts,tsx}": "eslint --fix",
"*.{css,scss,vue}": "stylelint --fix",
"*": "prettier -w -u"
}
}
Binary file added public/favicon.ico
Binary file not shown.
84 changes: 84 additions & 0 deletions src/app/App.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<template>
<div>
<nav>
<div class="navigation">
<router-link :to="{ name: 'home', params: { locale } }">
{{ t('navigations.home') }}
</router-link>
|
<router-link :to="{ name: 'about', params: { locale } }">
{{ t('navigations.about') }}
</router-link>
</div>
<form class="language">
<label for="locale-select">{{ t('labels.language') }}</label>
<select id="locale-select" v-model="currentLocale">
<option
v-for="optionLocale in supportLocales"
:key="optionLocale"
:value="optionLocale"
>
{{ optionLocale }}
</option>
</select>
</form>
</nav>
<router-view />
</div>
</template>

<script lang="ts">
import { defineComponent, watch, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { SUPPORT_LOCALES } from '../i18n'
export default defineComponent({
name: 'App',
setup() {
const router = useRouter()
const { t, locale } = useI18n() // same as `useI18n({ useScope: 'global' })`
/**
* select locale value for language select form
*
* If you use the vue-i18n composer `locale` property directly, it will be re-rendering component when this property is changed,
* before dynamic import was used to asynchronously load and apply locale messages
* To avoid this, use the another locale reactive value.
*/
const currentLocale = ref(locale.value)
// sync to switch locale from router locale path
watch(router.currentRoute, route => {
currentLocale.value = route.params.locale as string
})
/**
* when change the locale, go to locale route
*
* when the changes are detected, load the locale message and set the language via vue-router navigation guard.
* change the vue-i18n locale too.
*/
watch(currentLocale, val => {
router.push({
name: router.currentRoute.value.name!,
params: { locale: val }
})
})
return { t, locale, currentLocale, supportLocales: SUPPORT_LOCALES }
}
})
</script>

<style scoped>
nav {
display: inline-flex;
}
.navigation {
margin-right: 1rem;
}
.language label {
margin-right: 1rem;
}
</style>
Empty file added src/app/components/.gitkeep
Empty file.
13 changes: 13 additions & 0 deletions src/app/pages/About.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<template>
<div class="container">
<h2>{{ $t('pages.about') }}</h2>
</div>
</template>


<script lang="ts">
import { defineComponent } from '@vue/runtime-core';
export default defineComponent({
name: 'About'
});
</script>
12 changes: 12 additions & 0 deletions src/app/pages/Home.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<template>
<div class="container">
<h2>{{ $t('pages.home') }}</h2>
</div>
</template>

<script lang="ts">
import { defineComponent } from '@vue/runtime-core';
export default defineComponent({
name: 'Home'
});
</script>
53 changes: 53 additions & 0 deletions src/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { nextTick } from 'vue'
import { createI18n } from 'vue-i18n'

import type { I18n, I18nOptions, Locale, VueI18n, Composer } from 'vue-i18n'

export const SUPPORT_LOCALES = ['de', 'en']

export function getLocale(i18n: I18n): string {
return i18n.mode === 'legacy'
? (i18n.global as unknown as VueI18n).locale
: (i18n.global as unknown as Composer).locale.value
}

export function setLocale(i18n: I18n, locale: Locale): void {
if (i18n.mode === 'legacy') {
;(i18n.global as unknown as VueI18n).locale = locale
} else {
;(i18n.global as unknown as Composer).locale.value = locale
}
}

export function setupI18n(options: I18nOptions = { locale: 'de' }): I18n {
const i18n = createI18n(options)
setI18nLanguage(i18n, options.locale!)
return i18n
}

export function setI18nLanguage(i18n: I18n, locale: Locale): void {
setLocale(i18n, locale)
/**
* NOTE:
* If you need to specify the language setting for headers, such as the `fetch` API, set it here.
* The following is an example for axios.
*
* axios.defaults.headers.common['Accept-Language'] = locale
*/
document.querySelector('html')!.setAttribute('lang', locale)
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const getResourceMessages = (r: any) => r.default || r

export async function loadLocaleMessages(i18n: I18n, locale: Locale) {
// load locale messages
const messages = await import(
/* @vite-ignore */ `./locales/${locale}.json`
).then(getResourceMessages)

// set locale and locale message
i18n.global.setLocaleMessage(locale, messages)

return nextTick()
}
6 changes: 6 additions & 0 deletions src/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}
13 changes: 13 additions & 0 deletions src/locales/de.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"pages": {
"home": "Startseite",
"about": "Über uns"
},
"navigations": {
"home": "Start",
"about": "Über uns"
},
"labels": {
"language": "Sprachen"
}
}
13 changes: 13 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"pages": {
"home": "Home page",
"about": "About page"
},
"navigations": {
"home": "Home",
"about": "About"
},
"labels": {
"language": "Languages"
}
}
22 changes: 22 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { createApp } from 'vue'
import App from '@/app/App.vue'
import './index.scss'
import { setupRouter } from './router'
import { setupI18n } from './i18n'
import de from './locales/de.json'

const i18n = setupI18n({
legacy: false,
locale: 'de',
globalInjection: true,
fallbackLocale: 'de',
messages: {
de
}
})
const router = setupRouter(i18n)

const app = createApp(App)
app.use(i18n)
app.use(router)
app.mount('#app')
61 changes: 61 additions & 0 deletions src/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { createRouter, createWebHistory } from 'vue-router'
import {
getLocale,
setI18nLanguage,
loadLocaleMessages,
SUPPORT_LOCALES
} from './i18n'

import type { Router, RouteRecordRaw } from 'vue-router'
import type { I18n } from 'vue-i18n'

import Home from '@/app/pages/Home.vue'
import About from '@/app/pages/About.vue'

export function setupRouter(i18n: I18n): Router {
const locale = getLocale(i18n)

// setup routes
const routes: RouteRecordRaw[] = [
{
path: '/:locale/',
name: 'home',
component: Home
},
{
path: '/:locale/about',
name: 'about',
component: About
},
{
path: '/:pathMatch(.*)*',
redirect: () => `/${locale}`
}
]

// create router instance
const router = createRouter({
history: createWebHistory(),
routes
})

// navigation guards
router.beforeEach(async to => {
const paramsLocale = to.params.locale as string

// use locale if paramsLocale is not in SUPPORT_LOCALES
if (!SUPPORT_LOCALES.includes(paramsLocale)) {
return `/${locale}`
}

// load locale messages
if (!i18n.global.availableLocales.includes(paramsLocale)) {
await loadLocaleMessages(i18n, paramsLocale)
}

// set i18n language
setI18nLanguage(i18n, paramsLocale)
})

return router
}
5 changes: 5 additions & 0 deletions src/shims-vue.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
13 changes: 13 additions & 0 deletions src/vue-i18n.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* global type definitions
*/

import { DefineLocaleMessage } from 'vue-i18n'
import en from './locales/en.json'

type MessageSchema = typeof en

declare module 'vue-i18n' {
// define the locale messages schema
export interface DefineLocaleMessage extends MessageSchema {}
}
Loading

0 comments on commit 1b1155f

Please sign in to comment.