Vue Tiny Translation ๐โจ
April 16, 2026 ยท View on GitHub
A minimalist reactive translation plugin for Vue 3. Super lightweight (~0.35 KB gzipped) โ zero dependencies, full TypeScript support, reactive out of the box.
๐ Live Demo โ 35 languages, reactive language switching, no page reload.
Contents
- Features
- Installation
- Quick Start
- Composition API
- Loading translations
- TypeScript
- CDN usage
- API Reference
- vs vue-i18n
- Troubleshooting
- Local development
- License
Features
- ๐ Tiny โ ~0.35 KB gzipped. Zero runtime dependencies.
- โก Reactive โ components re-render automatically when the language changes. No page reload.
- ๐ง Four-function API โ
$t,translate,loadTranslations,setTranslations. Nothing else to learn. - ๐ Any source โ load translations from a static file, an API, a CMS, or a bundled import.
- ๐ฆ TypeScript โ full
.d.tswith$taugmentation forComponentCustomProperties. - ๐ณ Tree-shakeable โ
sideEffects: false, ESM-only, dev warnings stripped from the production build.
Installation
npm install vue-tiny-translation
# or
pnpm add vue-tiny-translation
# or
yarn add vue-tiny-translation
Quick Start
1. Register the plugin
// main.js
import { createApp } from 'vue'
import TinyTranslation from 'vue-tiny-translation'
import App from './App.vue'
createApp(App).use(TinyTranslation).mount('#app')
You can also seed initial translations at install time:
app.use(TinyTranslation, { hello: 'Hello', goodbye: 'Goodbye' })
2. Use $t in templates
<template>
<h1>{{ $t('hello') }}</h1>
<p>{{ $t('goodbye', 'Bye!') }}</p> <!-- second arg is a fallback -->
</template>
3. Load a language
<script>
import { loadTranslations } from 'vue-tiny-translation'
export default {
async mounted() {
const lang = navigator.language.split('-')[0]
await loadTranslations(`/translations/${lang}.json`)
},
}
</script>
Switching languages is just another loadTranslations() call โ every $t re-renders reactively.
Composition API
Use useTranslation() from <script setup>:
<script setup>
import { onMounted } from 'vue'
import { useTranslation } from 'vue-tiny-translation'
const { t, loadTranslations } = useTranslation()
onMounted(() => loadTranslations('/translations/en.json'))
async function switchLang(code) {
await loadTranslations(`/translations/${code}.json`)
}
</script>
<template>
<h1>{{ t('hello') }}</h1>
<button @click="switchLang('fr')">FR</button>
<button @click="switchLang('ja')">JA</button>
</template>
Loading translations
import { loadTranslations, setTranslations } from 'vue-tiny-translation'
// from a static file in /public
await loadTranslations('/translations/en.json')
// from your API
await loadTranslations('/api/i18n?lang=en')
// from an already-imported object (no fetch)
import en from './locales/en.json'
setTranslations(en)
Organizing translation files
Put JSON files wherever your bundler can serve them. A common layout:
public/translations/
en.json
fr.json
ja.json
{
"hello": "Hello",
"goodbye": "Goodbye",
"hero.title": "Welcome",
"hero.subtitle": "Enjoy your stay"
}
Dot-notation keys are strings โ there's no nested-object lookup by design. Keep it flat and searchable.
TypeScript
Types ship with the package, no @types/ install needed. The plugin augments Vue's ComponentCustomProperties so this.$t is typed in every component.
import { translate, useTranslation, setTranslations } from 'vue-tiny-translation'
translate('hello') // string
translate('missing', 'Fallback') // string
const { t } = useTranslation()
const greeting: string = t('hello')
setTranslations({ hello: 'Hello', 'hero.title': 'Welcome' })
CDN usage
Because the package is ESM-only, you can load it directly from a CDN via an import map:
<script type="importmap">
{
"imports": {
"vue": "https://esm.sh/vue@3",
"vue-tiny-translation": "https://esm.sh/vue-tiny-translation"
}
}
</script>
<div id="app">{{ $t('hello') }}</div>
<script type="module">
import { createApp } from 'vue'
import TinyTranslation from 'vue-tiny-translation'
createApp({}).use(TinyTranslation, { hello: 'Hello from CDN ๐' }).mount('#app')
</script>
API Reference
| Export | Signature | Description |
|---|---|---|
default (plugin) | app.use(plugin, initial?) | Registers $t on every component. Optional initial dictionary. |
translate | (key, fallback?) => value | Look up a key. Returns the fallback if provided, otherwise the key itself. |
useTranslation | () => { t, loadTranslations, setTranslations } | Composable for <script setup>. |
loadTranslations | (url?) => Promise<void> | Fetches JSON and replaces the current dictionary. Defaults to /translations/en.json. |
setTranslations | (dict) => void | Replaces the current dictionary without fetching (useful for bundled imports, CMS, SSR hydration). |
Missing keys log Missing translation: <key> once per key in development. The warning is compiled out of the production build (see Troubleshooting).
vs vue-i18n
| vue-tiny-translation | vue-i18n | |
|---|---|---|
| Bundle (gzipped) | ~0.35 KB | ~13 KB |
| Runtime deps | 0 | Several |
| Reactive switching | โ | โ |
| Dot-notation keys | โ (flat strings) | โ (true nesting) |
| Pluralization | โ | โ |
Interpolation ({name}) | โ | โ |
| Number / date formatting | โ | โ (Intl) |
| Lazy loading | Manual | Built-in helpers |
| Learning curve | 4 functions | Larger surface |
Pick vue-tiny-translation when you want small bundles, static strings, and no ceremony. Pick vue-i18n when you need ICU plural rules, message formatting, or locale-aware number/date output.
If all you need later is interpolation, you can build it yourself in ~5 lines on top of translate() without bringing in another library.
Troubleshooting
No missing-key warnings in production. Expected. The production build strips console.warn via terser's pure_funcs โ your consumer's bundler picks the minified build automatically via the production export condition. To see warnings while debugging a production bundle, import the development build explicitly or run your bundler in dev mode.
SSR / Nuxt. Translations live in a module-level reactive object, which is shared across requests on the server. For SSR, initialize translations before rendering (e.g. in a Nuxt plugin with ssr: true) and ensure the same dictionary is serialized into the client payload so hydration matches. If you need per-request isolation, this plugin is not the right fit โ reach for vue-i18n.
HMR not reflecting JSON edits. Translation JSON files served from public/ are not watched. After editing, call loadTranslations() again or add a Vite plugin that invalidates the module on change.
CJS require() no longer works. As of v1.2.0 the package is ESM-only. If you're locked to CommonJS, pin to vue-tiny-translation@1.1.x.
Local development
pnpm install
pnpm test # vitest
pnpm build # rollup + copy .d.ts to dist/
pnpm dev # rollup watch mode
Run the showcase example locally:
cd example
npm install
npm run dev
Then open http://localhost:5173 to see language auto-detection, a 3D background, and 35 translation files in action.
License
MIT ยฉ Makio64