Skip to content

fs-translation

Type-safe reactive i18n service with multi-locale support.

bash
npm install @script-development/fs-translation

Peer dependencies: vue ^3.5.0

What It Does

fs-translation provides internationalization with two guarantees you don't get from most i18n libraries:

  1. Compile-time key validation — misspelled translation keys are caught by TypeScript, not by users
  2. 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:

typescript
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

typescript
import {createTranslationService} from '@script-development/fs-translation';

const translation = createTranslationService(translations, 'en');

Use in Components

vue
<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:

typescript
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 exist

Keys 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:

typescript
// Translation: "User {name} created"
const message = translation.t('users.created', {name: 'Alice'});
// Result: "User Alice created"
vue
<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:

typescript
translation.locale.value = 'nl';
// Every ComputedRef from t() now returns Dutch translations
vue
<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)

ParameterTypeDescription
translationsRecord<TLocale, TSchema>Translation data per locale
defaultLocaleTLocaleThe initial active locale

Service Properties

PropertyTypeDescription
t(key, params?)(key, params?) => ComputedRef<string>Translate a key with optional params
localeRef<TLocale>Current locale — assign to switch languages

Translation Schema

Translations must be a two-level nested object:

typescript
{
    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")).

Built by Script Development & Back to Code