fs-translation
Type-safe reactive i18n service with multi-locale support.
npm install @script-development/fs-translationPeer dependencies: vue ^3.5.0
What It Does
fs-translation provides internationalization with two guarantees you don't get from most i18n libraries:
- Compile-time key validation — misspelled translation keys are caught by TypeScript, not by users
- Reactive locale switching — changing the locale updates all translated text automatically via Vue's reactivity system
Basic Usage
Define Your Translations
Translations are organized as a two-level nested object: sections containing keys:
const translations = {
en: {
common: {
save: "Save",
cancel: "Cancel",
delete: "Delete",
},
users: {
title: "Users",
empty: "No users found",
created: "User {name} created",
},
},
nl: {
common: {
save: "Opslaan",
cancel: "Annuleren",
delete: "Verwijderen",
},
users: {
title: "Gebruikers",
empty: "Geen gebruikers gevonden",
created: "Gebruiker {name} aangemaakt",
},
},
};Create the Service
import { createTranslationService } from "@script-development/fs-translation";
const translation = createTranslationService(translations, "en");Use in Components
<script setup lang="ts">
import { translation } from "@/services";
</script>
<template>
<h1>{{ translation.t("users.title").value }}</h1>
<p v-if="!users.length">{{ translation.t("users.empty").value }}</p>
<button>{{ translation.t("common.save").value }}</button>
</template>Type-Safe Keys
Translation keys are validated at compile time using TypeScript's template literal types. The t() function only accepts keys that exist in your translation schema:
translation.t("common.save"); // compiles — "common.save" exists
translation.t("common.delete"); // compiles — "common.delete" exists
translation.t("common.submit"); // compile error — "common.submit" doesn't exist
translation.t("invalid.key"); // compile error — "invalid" section doesn't existKeys use dot notation: "section.key". This catches typos before your code ever runs.
Parameter Interpolation
Use {param} placeholders in translation strings and pass values at call time:
// Translation: "User {name} created"
const message = translation.t("users.created", { name: "Alice" });
// Result: "User Alice created"<template>
<p>{{ translation.t("users.created", { name: userName }).value }}</p>
</template>Switching Locales
The locale property is a reactive Ref. Assign to it to switch languages — all t() results update automatically:
translation.locale.value = "nl";
// Every ComputedRef from t() now returns Dutch translations<script setup lang="ts">
import { translation } from "@/services";
</script>
<template>
<select v-model="translation.locale.value">
<option value="en">English</option>
<option value="nl">Nederlands</option>
</select>
<!-- Updates automatically when locale changes -->
<h1>{{ translation.t("users.title").value }}</h1>
</template>Why is t() a ComputedRef?
Because it needs to update reactively when the locale changes. If t() returned a plain string, you'd have to re-call it every time the locale changes. By returning a ComputedRef, Vue's reactivity system handles it — the template re-renders automatically.
Memoization
The service caches ComputedRef instances by key and params combination. Calling t("common.save") in 10 different components returns the same ComputedRef instance — no duplicate work.
API Reference
createTranslationService(translations, defaultLocale)
| Parameter | Type | Description |
|---|---|---|
translations | Record<TLocale, TSchema> | Translation data per locale |
defaultLocale | TLocale | The initial active locale |
Service Properties
| Property | Type | Description |
|---|---|---|
t(key, params?) | (key, params?) => ComputedRef<string> | Translate a key with optional params |
locale | Ref<TLocale> | Current locale — assign to switch languages |
Translation Schema
Translations must be a two-level nested object:
{
section: {
key: "Translation string with optional {param} placeholders";
}
}Section names and key names must not contain dots (the dot is the separator in t("section.key")).