Installation
Install typesafe-i18n for Vue.js:
npm install typesafe-i18n
npx typesafe-i18n --setup-auto --adapter vue
This will create the necessary folder structure and configuration files.
Project Structure
After setup, you'll have the following structure:
src/
i18n/
en/
index.ts
pl/
index.ts
es/
index.ts
fr/
index.ts
i18n-types.ts
i18n-util.ts
i18n-vue.ts
Translation File Structure
Define your translations with full TypeScript support:
// src/i18n/en/index.ts
import type { BaseTranslation } from '../i18n-types'
const en = {
homepage: {
title: 'Welcome to our website',
description: 'This is a multilingual Vue.js application',
subtitle: 'Built with typesafe-i18n'
},
navigation: {
home: 'Home',
about: 'About',
contact: 'Contact',
products: 'Products'
},
product: {
name: 'Product Name',
price: 'Price: {price:string}',
available: 'Available since: {date:string}',
addToCart: 'Add to Cart',
inStock: 'In Stock',
outOfStock: 'Out of Stock',
count: 'You have {count:number} item{count|s} in your cart'
},
cart: {
title: 'Shopping Cart',
empty: 'Your cart is empty',
total: 'Total: {amount:string}',
checkout: 'Checkout'
},
user: {
welcome: 'Welcome, {name:string}!',
profile: 'Profile',
logout: 'Logout',
login: 'Login'
},
forms: {
email: 'Email',
password: 'Password',
submit: 'Submit',
cancel: 'Cancel',
save: 'Save',
required: 'This field is required'
},
messages: {
loading: 'Loading...',
error: 'Something went wrong',
success: 'Operation completed successfully',
confirm: 'Are you sure?'
}
} satisfies BaseTranslation
export default en
// src/i18n/pl/index.ts
import type { Translation } from '../i18n-types'
const pl = {
homepage: {
title: 'Witamy na naszej stronie',
description: 'To jest wielojęzyczna aplikacja Vue.js',
subtitle: 'Zbudowana z typesafe-i18n'
},
navigation: {
home: 'Strona główna',
about: 'O nas',
contact: 'Kontakt',
products: 'Produkty'
},
product: {
name: 'Nazwa produktu',
price: 'Cena: {price}',
available: 'Dostępne od: {date}',
addToCart: 'Dodaj do koszyka',
inStock: 'Na stanie',
outOfStock: 'Brak na stanie',
count: 'Masz {count} przedmiot{count|ów} w koszyku'
},
cart: {
title: 'Koszyk',
empty: 'Twój koszyk jest pusty',
total: 'Razem: {amount}',
checkout: 'Złóż zamówienie'
},
user: {
welcome: 'Witaj, {name}!',
profile: 'Profil',
logout: 'Wyloguj',
login: 'Zaloguj'
},
forms: {
email: 'Email',
password: 'Hasło',
submit: 'Wyślij',
cancel: 'Anuluj',
save: 'Zapisz',
required: 'To pole jest wymagane'
},
messages: {
loading: 'Ładowanie...',
error: 'Coś poszło nie tak',
success: 'Operacja zakończona pomyślnie',
confirm: 'Czy jesteś pewny?'
}
} satisfies Translation
export default pl
// src/i18n/es/index.ts
import type { Translation } from '../i18n-types'
const es = {
homepage: {
title: 'Bienvenido a nuestro sitio web',
description: 'Esta es una aplicación Vue.js multiidioma',
subtitle: 'Construida con typesafe-i18n'
},
navigation: {
home: 'Inicio',
about: 'Acerca de',
contact: 'Contacto',
products: 'Productos'
},
product: {
name: 'Nombre del producto',
price: 'Precio: {price}',
available: 'Disponible desde: {date}',
addToCart: 'Añadir al carrito',
inStock: 'En stock',
outOfStock: 'Agotado',
count: 'Tienes {count} artículo{count|s} en tu carrito'
},
cart: {
title: 'Carrito de compras',
empty: 'Tu carrito está vacío',
total: 'Total: {amount}',
checkout: 'Finalizar compra'
},
user: {
welcome: '¡Bienvenido, {name}!',
profile: 'Perfil',
logout: 'Cerrar sesión',
login: 'Iniciar sesión'
},
forms: {
email: 'Correo electrónico',
password: 'Contraseña',
submit: 'Enviar',
cancel: 'Cancelar',
save: 'Guardar',
required: 'Este campo es obligatorio'
},
messages: {
loading: 'Cargando...',
error: 'Algo salió mal',
success: 'Operación completada exitosamente',
confirm: '¿Estás seguro?'
}
} satisfies Translation
export default es
// src/i18n/fr/index.ts
import type { Translation } from '../i18n-types'
const fr = {
homepage: {
title: 'Bienvenue sur notre site web',
description: 'Ceci est une application Vue.js multilingue',
subtitle: 'Construite avec typesafe-i18n'
},
navigation: {
home: 'Accueil',
about: 'À propos',
contact: 'Contact',
products: 'Produits'
},
product: {
name: 'Nom du produit',
price: 'Prix: {price}',
available: 'Disponible depuis: {date}',
addToCart: 'Ajouter au panier',
inStock: 'En stock',
outOfStock: 'Rupture de stock',
count: 'Vous avez {count} article{count|s} dans votre panier'
},
cart: {
title: 'Panier',
empty: 'Votre panier est vide',
total: 'Total: {amount}',
checkout: 'Commander'
},
user: {
welcome: 'Bienvenue, {name} !',
profile: 'Profil',
logout: 'Déconnexion',
login: 'Connexion'
},
forms: {
email: 'Email',
password: 'Mot de passe',
submit: 'Soumettre',
cancel: 'Annuler',
save: 'Sauvegarder',
required: 'Ce champ est requis'
},
messages: {
loading: 'Chargement...',
error: 'Quelque chose s\'est mal passé',
success: 'Opération terminée avec succès',
confirm: 'Êtes-vous sûr ?'
}
} satisfies Translation
export default fr
Vue.js Configuration
Configure typesafe-i18n for Vue.js:
// src/i18n/i18n-vue.ts
import type { Locales, TranslationFunctions } from './i18n-types'
import { loadLocaleAsync } from './i18n-util.async'
import { i18nObject } from './i18n-util'
import { ref, computed, App } from 'vue'
const locale = ref<Locales>('en')
const LL = ref<TranslationFunctions>()
export const typesafeI18n = {
locale: computed(() => locale.value),
LL: computed(() => LL.value),
setLocale: async (newLocale: Locales) => {
await loadLocaleAsync(newLocale)
locale.value = newLocale
LL.value = i18nObject(newLocale)
},
init: async (initialLocale: Locales = 'en') => {
await loadLocaleAsync(initialLocale)
locale.value = initialLocale
LL.value = i18nObject(initialLocale)
}
}
export const install = (app: App) => {
app.config.globalProperties.$typesafeI18n = typesafeI18n
app.provide('typesafeI18n', typesafeI18n)
}
export const useTypesafeI18n = () => typesafeI18n
Main Application Setup
// src/main.ts
import { createApp } from 'vue'
import { install as i18nInstall, typesafeI18n } from './i18n/i18n-vue'
import App from './App.vue'
async function initApp() {
const app = createApp(App)
// Install typesafe-i18n
app.use(i18nInstall)
// Initialize with saved or detected locale
const savedLocale = localStorage.getItem('locale') as any
const browserLocale = navigator.language.split('-')[0] as any
const initialLocale = savedLocale || browserLocale || 'en'
await typesafeI18n.init(initialLocale)
app.mount('#app')
}
initApp()
Usage Examples
Basic Component Usage (Composition API)
<!-- HomePage.vue -->
<template>
<div class="homepage">
<h1>{{ LL.homepage.title() }}</h1>
<p>{{ LL.homepage.description() }}</p>
<small>{{ LL.homepage.subtitle() }}</small>
<LanguageSwitcher />
<ProductShowcase :products="products" />
<UserProfile :user="currentUser" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useTypesafeI18n } from '../i18n/i18n-vue'
import LanguageSwitcher from './LanguageSwitcher.vue'
import ProductShowcase from './ProductShowcase.vue'
import UserProfile from './UserProfile.vue'
const { LL } = useTypesafeI18n()
const products = ref([
{
id: 1,
name: 'Sample Product',
price: '$29.99',
availableDate: '2023-01-15',
inStock: true,
count: 3
}
])
const currentUser = ref({
name: 'John Doe',
messageCount: 5
})
</script>
Language Switcher Component
<!-- LanguageSwitcher.vue -->
<template>
<div class="language-switcher">
<label for="language-select">{{ LL.ui.selectLanguage() }}:</label>
<select
id="language-select"
:value="locale"
@change="changeLanguage">
<option
v-for="lang in availableLanguages"
:key="lang.code"
:value="lang.code">
{{ lang.name }}
</option>
</select>
</div>
</template>
<script setup lang="ts">
import { useTypesafeI18n } from '../i18n/i18n-vue'
import type { Locales } from '../i18n/i18n-types'
const { locale, setLocale, LL } = useTypesafeI18n()
const availableLanguages = [
{ code: 'en' as Locales, name: 'English' },
{ code: 'pl' as Locales, name: 'Polski' },
{ code: 'es' as Locales, name: 'Español' },
{ code: 'fr' as Locales, name: 'Français' }
]
const changeLanguage = async (event: Event) => {
const target = event.target as HTMLSelectElement
const newLocale = target.value as Locales
await setLocale(newLocale)
localStorage.setItem('locale', newLocale)
}
</script>
Product Component with Type-Safe Translations
<!-- ProductCard.vue -->
<template>
<div class="product-card">
<h3>{{ product.name }}</h3>
<!-- Type-safe parameter passing -->
<p>{{ LL.product.price({ price: product.price }) }}</p>
<p>{{ LL.product.available({ date: product.availableDate }) }}</p>
<!-- Type-safe pluralization -->
<p>{{ LL.product.count({ count: product.count }) }}</p>
<div class="product-status">
<span :class="stockClass">
{{ product.inStock ? LL.product.inStock() : LL.product.outOfStock() }}
</span>
</div>
<button
@click="addToCart"
:disabled="!product.inStock"
class="btn btn-primary">
{{ LL.product.addToCart() }}
</button>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useTypesafeI18n } from '../i18n/i18n-vue'
interface Product {
id: number
name: string
price: string
availableDate: string
inStock: boolean
count: number
}
const props = defineProps<{
product: Product
}>()
const { LL } = useTypesafeI18n()
const stockClass = computed(() => ({
'in-stock': props.product.inStock,
'out-of-stock': !props.product.inStock
}))
const addToCart = () => {
console.log('Added to cart:', props.product.name)
}
</script>
Shopping Cart Component
<!-- ShoppingCart.vue -->
<template>
<div class="shopping-cart">
<h2>{{ LL.cart.title() }}</h2>
<div v-if="items.length === 0" class="empty-cart">
{{ LL.cart.empty() }}
</div>
<div v-else class="cart-content">
<div class="cart-items">
<div v-for="item in items" :key="item.id" class="cart-item">
<span>{{ item.name }}</span>
<span>{{ item.price }}</span>
</div>
</div>
<!-- Type-safe parameter with formatting -->
<div class="cart-total">
{{ LL.cart.total({ amount: formatTotal() }) }}
</div>
<button @click="checkout" class="btn btn-primary">
{{ LL.cart.checkout() }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useTypesafeI18n } from '../i18n/i18n-vue'
interface CartItem {
id: number
name: string
price: string
amount: number
}
const props = defineProps<{
items: CartItem[]
}>()
const { LL } = useTypesafeI18n()
const formatTotal = () => {
const total = props.items.reduce((sum, item) => sum + item.amount, 0)
return `$${total.toFixed(2)}`
}
const checkout = () => {
console.log('Proceeding to checkout')
}
</script>
User Profile Component
<!-- UserProfile.vue -->
<template>
<div class="user-profile">
<!-- Type-safe string interpolation -->
<h2>{{ LL.user.welcome({ name: user.name }) }}</h2>
<div class="user-actions">
<button @click="viewProfile" class="btn btn-secondary">
{{ LL.user.profile() }}
</button>
<button @click="logout" class="btn btn-outline">
{{ LL.user.logout() }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { useTypesafeI18n } from '../i18n/i18n-vue'
interface User {
name: string
messageCount: number
}
const props = defineProps<{
user: User
}>()
const { LL } = useTypesafeI18n()
const viewProfile = () => {
console.log('Viewing profile for:', props.user.name)
}
const logout = () => {
console.log('Logging out:', props.user.name)
}
</script>
Translation Composable
// src/composables/useTranslation.ts
import { computed } from 'vue'
import { useTypesafeI18n } from '../i18n/i18n-vue'
import type { Locales } from '../i18n/i18n-types'
export function useTranslation() {
const { locale, LL, setLocale } = useTypesafeI18n()
const currentLocale = computed(() => locale.value)
const changeLocale = async (newLocale: Locales) => {
await setLocale(newLocale)
localStorage.setItem('locale', newLocale)
}
// Type-safe translation helpers
const translateMessage = (key: 'loading' | 'error' | 'success' | 'confirm') => {
return LL.value.messages[key]()
}
const translateNavigation = (key: 'home' | 'about' | 'contact' | 'products') => {
return LL.value.navigation[key]()
}
const translateForm = (key: 'email' | 'password' | 'submit' | 'cancel' | 'save' | 'required') => {
return LL.value.forms[key]()
}
return {
currentLocale,
LL: computed(() => LL.value),
changeLocale,
translateMessage,
translateNavigation,
translateForm
}
}
Type-safe Form Component
<!-- ContactForm.vue -->
<template>
<form @submit.prevent="submitForm" class="contact-form">
<h2>{{ LL.forms.title() }}</h2>
<div class="form-group">
<label for="email">{{ LL.forms.email() }}</label>
<input
id="email"
v-model="formData.email"
type="email"
:class="{ 'error': errors.email }"
required>
<span v-if="errors.email" class="error-message">
{{ LL.forms.required() }}
</span>
</div>
<div class="form-group">
<label for="password">{{ LL.forms.password() }}</label>
<input
id="password"
v-model="formData.password"
type="password"
:class="{ 'error': errors.password }"
required>
<span v-if="errors.password" class="error-message">
{{ LL.forms.required() }}
</span>
</div>
<div class="form-actions">
<button type="button" @click="cancel" class="btn btn-secondary">
{{ LL.forms.cancel() }}
</button>
<button type="submit" :disabled="isSubmitting" class="btn btn-primary">
{{ isSubmitting ? LL.messages.loading() : LL.forms.submit() }}
</button>
</div>
</form>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useTypesafeI18n } from '../i18n/i18n-vue'
const { LL } = useTypesafeI18n()
const formData = reactive({
email: '',
password: ''
})
const errors = reactive({
email: false,
password: false
})
const isSubmitting = ref(false)
const validateForm = () => {
errors.email = !formData.email
errors.password = !formData.password
return !errors.email && !errors.password
}
const submitForm = async () => {
if (!validateForm()) return
isSubmitting.value = true
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000))
console.log(LL.value.messages.success())
} catch (error) {
console.error(LL.value.messages.error())
} finally {
isSubmitting.value = false
}
}
const cancel = () => {
formData.email = ''
formData.password = ''
errors.email = false
errors.password = false
}
</script>
SimpleLocalize Configuration
To manage translations with SimpleLocalize, you need to set up the CLI tool.
# macOS / Linux / Windows (WSL)
curl -s https://get.simplelocalize.io/2.9/install | bash
# Windows (PowerShell)
. { iwr -useb https://get.simplelocalize.io/2.9/install-windows } | iex;
# npm
npm install @simplelocalize/cli
# macOS / Linux / Windows (WSL)
curl -s https://get.simplelocalize.io/2.9/install | bash
# Windows (PowerShell)
. { iwr -useb https://get.simplelocalize.io/2.9/install-windows } | iex;
# npm
npm install @simplelocalize/cli
Create simplelocalize.yml
in your project root:
apiKey: YOUR_PROJECT_API_KEY
uploadFormat: single-language-json
uploadPath: ./src/i18n/{lang}/index.ts
uploadLanguageKey: en
uploadOptions:
- REPLACE_TRANSLATION_IF_FOUND
downloadPath: ./src/i18n/{lang}/
downloadLanguageKeys: ['en', 'pl', 'es', 'fr']
downloadFormat: typesafe-i18n
Work with the CLI to manage your translations:
# Upload translations to SimpleLocalize
simplelocalize upload
# Download translations for all languages
simplelocalize download
# Generate TypeScript types
npx typesafe-i18n
# Build your Vue.js application
npm run build
Key Features
- Full Type Safety: Complete TypeScript integration with autocomplete and error checking
- Vue 3 Composition API: Native support for Vue 3's Composition API
- Pluralization: Built-in pluralization with type-safe parameters
- Interpolation: Type-safe variable interpolation
- Tree Shaking: Only include translations you actually use
- Small Bundle Size: Minimal runtime overhead
- IDE Support: Full IntelliSense support in VS Code
Type Safety Benefits
// ✅ This will work - proper parameter types
LL.product.price({ price: '$29.99' })
// ❌ TypeScript error - missing required parameter
LL.product.price()
// ❌ TypeScript error - wrong parameter type
LL.product.count({ count: 'invalid' })
// ✅ Autocomplete suggests available translation keys
LL.navigation. // Will show: home, about, contact, products
Best Practices
1. Organize Translations by Feature
const en = {
pages: {
home: { title: 'Home Page' },
about: { title: 'About Us' }
},
components: {
header: { logo: 'Company Logo' },
footer: { copyright: '© 2025' }
}
} satisfies BaseTranslation
2. Use Type-safe Parameters
// Define with proper types
const en = {
welcome: 'Hello {name:string}, you have {count:number} message{count|s}'
} satisfies BaseTranslation
// Use with type safety
LL.welcome({ name: 'John', count: 3 })
3. Leverage Pluralization
const en = {
items: 'You have {count:number} item{count|s}',
notifications: '{count:number} notification{count|s} received'
} satisfies BaseTranslation
Resources
- typesafe-i18n Documentation
- Vue.js Documentation
- typesafe-i18n Vue Adapter
- SimpleLocalize CLI Documentation
Was this helpful?