Skip to content

Localization

A complete localization system for Vue 3 covering locale-aware formatting (l10n), translations (i18n), and timezone detection. Provided by the optional @cocoar/vue-localization package.

Separate Package

The localization system is shipped separately from @cocoar/vue-ui to keep the core bundle lean. Install it only when you need it.

bash
pnpm add @cocoar/vue-localization

Setup

Register the plugin in your app entry point. The createCoarLocalization() factory creates both the plugin and the underlying service instance.

ts
// main.ts
import { createApp } from 'vue';
import { createCoarLocalization } from '@cocoar/vue-localization';
import App from './App.vue';

const app = createApp(App);

app.use(createCoarLocalization({
  defaultLanguage: 'en',
  // Optional: load locale data from your server
  l10nUrl: (lang) => `/locales/${lang}.json`,
  // Optional: load translations from your server
  i18nUrl: (lang) => `/i18n/${lang}.json`,
}));

app.mount('#app');

The defaultLanguage is loaded automatically on startup using the browser's Intl API as a data source. No JSON files are needed for basic formatting -- the system derives number separators, date patterns, currency symbols, and more directly from Intl.

Configuration Options

OptionTypeDefaultDescription
defaultLanguagestring'en'Initial language code
l10nUrl(lang: string) => stringundefinedURL builder for locale data JSON (overrides Intl defaults)
i18nUrl(lang: string) => stringundefinedURL builder for translation JSON
timezoneProvidersCoarTimezoneProvider[][]Custom timezone providers (checked before browser default)

Number and Currency Formatting

The useL10n() composable provides locale-aware formatting for numbers, currencies, and percentages. All formatters react to language changes automatically. Toggle between locales to see how separators, grouping, and currency symbols adapt.

FormatInputOutput
fmtNumber1234567.891234567.89
fmtNumber (0 decimals)1234567.891234568
fmtCurrency9999.999999.99
fmtCurrency (EUR)9999.999999.99
fmtPercent0.25626%
fmtPercent (2 decimals)0.25625.60%

Current language: en

Usage

vue
<script setup lang="ts">
import { useL10n } from '@cocoar/vue-localization';

const { language, fmtNumber, fmtCurrency, fmtPercent } = useL10n();
</script>

<template>
  <p>{{ fmtNumber(1234567.89) }}</p>       <!-- "1,234,567.89" in en -->
  <p>{{ fmtNumber(1234567.89, 0) }}</p>    <!-- "1,234,568" in en -->
  <p>{{ fmtCurrency(9999.99) }}</p>         <!-- "$9,999.99" in en -->
  <p>{{ fmtCurrency(9999.99, 'EUR') }}</p>  <!-- "EUR9,999.99" in en -->
  <p>{{ fmtPercent(0.256) }}</p>            <!-- "26%" in en -->
  <p>{{ fmtPercent(0.256, 2) }}</p>         <!-- "25.60%" in en -->
</template>

useL10n() API

PropertyTypeDescription
languageRef<string>Current language (reactive)
localeDataComputedRef<CoarLocalizationData | undefined>Full locale data for the current language
fmtNumber(value, decimals?)(number, number?) => stringFormat a number with locale separators
fmtCurrency(value, currencyCode?)(number, string?) => stringFormat as currency (defaults to locale's currency)
fmtPercent(value, decimals?)(number, number?) => stringFormat as percentage (0.25 becomes "25%")
fmtDate(value, includeTime?)(Date | string, boolean?) => stringFormat a date (optionally with time)

Date Formatting

Dates are formatted according to the locale's date pattern. The system detects whether the locale uses dd/mm/yyyy, mm/dd/yyyy, dd.mm.yyyy, or yyyy-mm-dd from the browser's Intl API. Switch locales to see both the formatted output and the underlying locale metadata.

DescriptionOutput
Date onlySat Mar 22 2025 14:30:00 GMT+0000 (Coordinated Universal Time)
Date + timeSat Mar 22 2025 14:30:00 GMT+0000 (Coordinated Universal Time)
From ISO string2025-12-31T23:59:00

Usage

vue
<script setup lang="ts">
import { useL10n } from '@cocoar/vue-localization';

const { fmtDate } = useL10n();
</script>

<template>
  <p>{{ fmtDate(new Date()) }}</p>                   <!-- date only -->
  <p>{{ fmtDate(new Date(), true) }}</p>              <!-- date + time -->
  <p>{{ fmtDate('2025-12-31T23:59:00') }}</p>         <!-- from ISO string -->
</template>

Regional Locales

Same language, different region — en-US vs en-GB, de-DE vs de-AT, fr-FR vs fr-CH. The system loads the base locale first, then merges regional overrides on top. This means currency symbols, date patterns, and number formatting automatically adapt to the user's region without duplicating the entire locale definition.

WhatOutput
Number (1234567.89)1234567.89
Currency (9999.99)9999.99
DateMon Mar 23 2026 19:21:11 GMT+0000 (Coordinated Universal Time)
Date pattern
Default currency
Decimal separator

Notice how de-DE and de-AT share the same language but differ in currency (EUR vs EUR with different formatting). The system loads the base locale (de) first, then merges regional overrides on top.

Translations (i18n)

The useI18n() composable provides translation lookup with parameter interpolation. Translations can come from HTTP sources (configured via i18nUrl) or be registered directly in code using the service's i18nStore.

Parameters use {name} syntax and are replaced at runtime. Nested translation objects are automatically flattened to dot-separated keys.

greeting
items.count
status.lastSaved
t('greeting', { name: 'Alice' })greeting

Usage

vue
<script setup lang="ts">
import { useI18n } from '@cocoar/vue-localization';

const { t, tRef, language } = useI18n();

// Immediate value (call in template for reactivity)
// t('greeting', { name: 'Alice' })  →  "Hello, Alice!"

// Computed ref (reacts to language changes automatically)
const title = tRef('app.title');
</script>

<template>
  <h1>{{ title }}</h1>
  <p>{{ t('greeting', { name: 'Alice' }) }}</p>
  <p>{{ t('missing.key', {}, 'Fallback text') }}</p>
</template>

Registering Translations in Code

For cases where HTTP loading is not appropriate (tests, demos, embedded apps), you can register translations directly on the service.

ts
import { useLocalization } from '@cocoar/vue-localization';

const service = useLocalization()!;

service.i18nStore.setTranslations('en', {
  greeting: 'Hello, {name}!',
  actions: {
    save: 'Save',
    cancel: 'Cancel',
  },
});

service.i18nStore.setTranslations('de', {
  greeting: 'Hallo, {name}!',
  actions: {
    save: 'Speichern',
    cancel: 'Abbrechen',
  },
});

Nested keys are automatically flattened: actions.save resolves 'Save' for English.

useI18n() API

PropertyTypeDescription
languageRef<string>Current language (reactive)
t(key, params?, fallback?)(string, Record?, string?) => stringTranslate a key with optional interpolation
tRef(key, params?, fallback?)(string, Record?, string?) => ComputedRef<string>Computed translation that reacts to language changes

Translation Fallback Behavior

  1. Look up the key in the current language (e.g. de-AT)
  2. If not found and locale is regional, try the base language (de)
  3. If still not found, use the provided fallback argument
  4. If no fallback, return the key itself

Translating Component Strings

All built-in text in Cocoar UI components -- aria-labels, button labels, empty state messages, screen-reader announcements -- defaults to English and can be translated by providing a coar.ui.* namespace in your translation JSON.

If the localization plugin is not installed, every string falls back to its English default automatically. Nothing breaks, nothing needs to be configured.

Plugin not installed        →  English fallbacks (default)
Plugin installed            →  Translated strings, if the key exists in your JSON
Plugin installed, key missing  →  English fallback

Your translation file only needs to contain the keys you want to override:

json
{
  "coar": {
    "ui": {
      "dialog": { "dialog": "Dialog", "close": "Schließen" },
      "select": { "noResults": "Keine Ergebnisse", "noOptions": "Keine Optionen verfügbar" },
      "datePicker": {
        "dialog": "Datumsauswahl",
        "clearDate": "Datum löschen",
        "previousYear": "Vorheriges Jahr",
        "nextYear": "Nächstes Jahr",
        "months": "Monate"
      },
      "toast": { "dismiss": "Benachrichtigung schließen" }
    }
  }
}

Each component's documentation page lists its translatable keys under an i18n Keys section. Check the component page you're working with for the exact keys available.

Props as alternative

If you only need to change a single string in one place, some components offer direct props -- for example CoarSpinner has a label prop, CoarPopconfirm has confirmText and cancelText. Check the component's Props table first before reaching for i18n.

Timezone Detection

The useTimezone() composable provides the browser's detected IANA timezone identifier as a reactive ref. The date/time picker components in @cocoar/vue-ui use this automatically.

Detected timezoneUTC
UTC offsetUTC+00:00
Current time19:21:11

The timezone is auto-detected from your browser using Intl.DateTimeFormat. Call refresh() to re-detect if it changes (e.g. VPN or system settings update).

Usage

vue
<script setup lang="ts">
import { useTimezone } from '@cocoar/vue-localization';

const { timezone, refresh } = useTimezone();

// timezone.value → "Europe/Berlin", "America/New_York", etc.
</script>

<template>
  <p>Your timezone: {{ timezone }}</p>
  <button @click="refresh()">Re-detect</button>
</template>

Custom Timezone Providers

You can supply custom providers (e.g. from a user profile API) that are checked before the browser default.

ts
import { createCoarLocalization } from '@cocoar/vue-localization';
import type { CoarTimezoneProvider } from '@cocoar/vue-localization';

const userTimezone: CoarTimezoneProvider = {
  getTimezone() {
    // Return from user settings, or null to defer to next provider
    return userStore.timezone ?? null;
  },
};

app.use(createCoarLocalization({
  timezoneProviders: [userTimezone],
}));

useTimezone() API

PropertyTypeDescription
timezoneRef<string>Current IANA timezone identifier (reactive)
refresh()() => voidRe-resolve timezone from providers

Changing Language at Runtime

Use the service directly to switch languages. Data is loaded on demand and cached.

ts
import { useLocalization } from '@cocoar/vue-localization';

const service = useLocalization()!;

// Switch language (loads locale data + translations if not cached)
await service.setLanguage('de');

// Preload without switching (useful for instant switching later)
await service.preloadLanguage('fr');

// Force reload from all sources (e.g. after server-side data changes)
await service.reloadLanguage();

Service API

The useLocalization() composable returns the full CoarLocalizationService instance for advanced use cases.

Method / PropertyTypeDescription
languageRef<string>Current language (reactive)
loadingRef<boolean>Whether data is currently being loaded
localeDataComputedRef<CoarLocalizationData | undefined>Locale data for the current language
setLanguage(lang)(string) => Promise<void>Switch language and load data
preloadLanguage(lang)(string) => Promise<void>Preload data without switching
reloadLanguage(lang?)(string?) => Promise<void>Force reload data from all sources
t(key, params?, fallback?)(string, Record?, string?) => stringTranslate a key
getDefaultLanguage()() => stringGet the configured default language
i18nStoreCoarTranslationStoreDirect access to the translation store
l10nStoreCoarLocalizationDataStoreDirect access to the locale data store
timezoneServiceCoarTimezoneServiceDirect access to the timezone service
addLocaleDataSource(source)(CoarLocaleDataSource) => voidAdd a custom locale data source
addTranslationSource(source)(CoarTranslationSource) => voidAdd a custom translation source

Locale Data Structure

When providing locale data via HTTP (the l10nUrl option), the JSON should follow the CoarLocalizationData shape:

json
{
  "code": "de",
  "date": {
    "pattern": "dd.mm.yyyy",
    "firstDayOfWeek": 1,
    "monthNames": ["Januar", "Februar", "..."],
    "monthNamesShort": ["Jan", "Feb", "..."],
    "dayNames": ["Sonntag", "Montag", "..."],
    "dayNamesShort": ["So", "Mo", "..."],
    "amPm": ["AM", "PM"]
  },
  "number": {
    "decimal": ",",
    "group": ".",
    "grouping": [3]
  },
  "currency": {
    "default": "EUR",
    "symbols": { "EUR": "\u20ac", "USD": "$" },
    "position": "after",
    "spacing": true,
    "decimals": 2
  },
  "percent": {
    "symbol": "%",
    "spacing": true,
    "decimals": 0
  }
}

TIP

You typically do not need to provide locale JSON files. The built-in IntlLocaleDataSource derives all of this from the browser's Intl API. HTTP sources are useful when you need to override specific values (e.g. custom currency symbols or non-standard grouping).

Released under the Apache-2.0 License.