Internationalization (i18n)
CDS components carry texts of their own: "No data available" in a select, the table filter labels, the aria-label of a close button, the file counter. They all go through the built-in localization layer, so you can translate them into another language, override a single string for your product, or switch languages on the fly — without rewriting components.
The library ships two dictionaries — Russian and English. ru is active by default, so out of the box everything is in Russian.
Setup
The locale is configured through the locale option of createCds. Switching to the built-in English is a one-liner — you don't need to pass any dictionaries:
import { createApp } from 'vue';
import '@central-design-system/components/styles';
import { createCds, components, directives } from '@central-design-system/components';
import App from './App.vue';
const cds = createCds({
components,
directives,
locale: {
locale: 'en'
}
});
createApp(App).use(cds);Languages the library doesn't have are added as a dictionary in messages:
import de from './locales/cds-de';
createCds({
components,
directives,
locale: {
locale: 'de',
fallback: 'en',
messages: { de }
}
});| Option | Type | Purpose |
|---|---|---|
locale | string | Active language; ru (default) and en are available out of the box |
fallback | string | Language a string is taken from when the active one lacks it; ru by default |
messages | LocaleMessages | Dictionaries of extra languages; a same-named built-in dictionary is replaced entirely |
decimalSeparator | string | Decimal separator; derived from the active locale by default |
rtl | Record<string, boolean> | Map of right-to-left languages (see RTL) |
adapter | LocaleInstance | Your own translation engine instead of the built-in one (see Custom adapter) |
The built-in dictionaries are available to you as well — they live in the locales export. Use them as a base for your own language or to fix a couple of strings:
import { locales } from '@central-design-system/components';
locales.ru; // Russian dictionary
locales.en; // English dictionaryFor the full set of keys, see Message keys.
How components get their text
Component strings are keys prefixed with $cds. that the library runs through a translation function. Some of the keys are declared as default prop values, so you can see them in the API: text on CdsFileUpload is $cds.fileUpload.browse, closeLabel on CdsNotification is $cds.close, noDataText on CdsSelect is $cds.noDataText.
That gives you three ways to change a text.
Switch the whole language — with the locale option:
createCds({ locale: { locale: 'en' } });Change wording across the product — with a dictionary on top of the built-in one:
import { createCds, locales } from '@central-design-system/components';
createCds({
locale: {
messages: {
en: { ...locales.en, noDataText: 'Nothing found' }
}
}
});Important. A language dictionary is replaced entirely, not merged key by key. Without the ...locales.en spread, the English locale is left with a single key and the rest of the strings turn into their own keys ($cds.close instead of "Close"). Spread nested sections separately: table: { ...locales.en.table, emptyText: '…' }.
Change one label in one place — with a prop:
<CdsSelect :items="items" no-data-text="No matching suppliers" />The same prop can be set as the default for every instance of the component — see global configuration:
createCds({
defaults: {
CdsSelect: { noDataText: 'Nothing found' }
}
});The translation function also accepts plain text: a string without the $cds. prefix is returned as is, with the parameters substituted. That's how props with a localizable value work the same way with a key and with a ready-made phrase.
If a key is missing from both the active locale and the fallback, the component shows the key itself, and in dev mode a [CDS: useLocale] Translation key … not found warning appears in the console.
Switching the language at runtime
createCds returns an instance whose locale lives in reactive refs — to switch languages, just write to current:
const cds = createCds({ components, directives });
// for example, following the user's choice in profile settings
cds.locale.current.value = 'en';Inside components, the useLocale composable does the same:
<script setup lang="ts">
import { CdsButton, CdsButtonToggle, useLocale } from '@central-design-system/components';
const { current } = useLocale();
</script>
<template>
<CdsButtonToggle v-model="current" appearance="tertiary">
<CdsButton value="ru" text="Русский" />
<CdsButton value="en" text="English" />
</CdsButtonToggle>
</template>Every mounted component recalculates its strings on its own — no page reload needed.
A locale for part of the application
A single subtree can get its own language — for example, an English-language widget inside a Russian interface. Call provideLocale in a wrapper component: it takes the locale from the context, applies the props you passed and provides the result to the children.
<script setup lang="ts">
import { provideLocale } from '@central-design-system/components';
const props = defineProps<{ locale?: string; fallback?: string }>();
provideLocale(props);
</script>
<template>
<div>
<slot />
</div>
</template><template>
<LocaleProvider locale="en">
<CdsSelect :items="items" />
</LocaleProvider>
</template>Props you don't pass are inherited from the parent locale, so a wrapper with just a locale loses neither the fallback nor the dictionaries.
useLocale
The composable hands you the current locale and its functions — use it in your own components so their texts live in the same dictionaries as the CDS ones.
<script setup lang="ts">
import { useLocale } from '@central-design-system/components';
const { t, n } = useLocale();
</script>
<template>
<p>{{ t('$cds.table.emptyText') }}</p>
<p>{{ n(1234.56) }}</p>
</template>| Property | Type | Purpose |
|---|---|---|
t | (key: string, ...params: unknown[]) => string | Translation of a $cds.… key with parameter substitution |
n | (value: number) => string | Number formatting for the active locale |
current | Ref<string> | Active language; writing to it switches the language |
fallback | Ref<string> | Fallback language |
messages | Ref<LocaleMessages> | Dictionaries; can be extended at runtime |
decimalSeparator | ShallowRef<string> | Decimal separator of the active locale |
Parameters in strings are numbered — {0}, {1}:
const { t } = useLocale();
t('$cds.carousel.delimiter', 3, 8); // "Slide 3 of 8"
t('Days left: {0}', 5); // "Days left: 5"Under the hood n is Intl.NumberFormat with the active locale and the fallback, so group and decimal separators come from the browser. The library derives decimalSeparator from the same locale (, for ru, . for en); if your product uses a different one, set it explicitly in createCds.
Dates
Month and weekday names live in separate date locales, shipped by ru and en. Date and time components pick the date locale by the active language, so after current.value = 'en' the calendar switches by itself.
A single component gets its language from the locale prop — CdsDatePicker, CdsDateInput and CdsTimePicker all have it:
<CdsDatePicker v-model="value" locale="en" :first-day-of-week="0" header />The prop accepts not only a language name but also your own date locale object (type TDateLocale) — that's how you plug in a language the library doesn't have. The built-in date locales, for reference and reuse, live in the same locales export: locales.ruDate, locales.enDate and the locales.dateLocales map.
const de = {
days: 'Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag'.split('_'),
daysShort: 'So_Mo_Di_Mi_Do_Fr_Sa'.split('_'),
months: 'Januar_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember'.split('_'),
monthsLong: 'Januar_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember'.split('_'),
monthsShort: 'Jan_Feb_Mär_Apr_Mai_Jun_Jul_Aug_Sep_Okt_Nov_Dez'.split('_'),
today: 'Heute',
now: 'Jetzt',
firstDayOfWeek: 1,
format24h: true,
pluralDay: (day = 0) => (day === 1 ? 'Tag' : 'Tage')
};<CdsDatePicker v-model="value" :locale="de" />Table
CdsTable builds its labels from the $cds.table.* keys, but it also accepts a pinpoint override through the locale prop — which replaces only the strings you pass:
<CdsTable :columns="columns" :data="data" :locale="{ emptyText: 'No requests yet' }" />RTL
Right-to-left languages are described by the rtl map: the key is the language, the value is the direction.
createCds({
locale: {
locale: 'ar',
messages: { ar },
rtl: { ar: true }
}
});Components read the direction through useRtl and flip whatever depends on the axis: the side menus and tooltips open to, the pagination arrows, the cell order in CdsOtpInput, the fill of CdsProgressBar. Spacing and borders in the styles are mostly written with logical properties (margin-inline, inset-inline) and are mirrored by the browser itself — but the design system does not claim full RTL support yet, and some places will need polishing on your side.
The library does not set the direction attribute for you — put it on the application root yourself:
<script setup lang="ts">
import { useRtl } from '@central-design-system/components';
const { isRtl } = useRtl();
</script>
<template>
<div :dir="isRtl ? 'rtl' : 'ltr'">
<slot />
</div>
</template>useRtl gives you isRtl and rtlClasses — a ready cds-locale--is-rtl / cds-locale--is-ltr class for your own styles.
Custom adapter
If your application already has an i18n library, hand the translations over to it: the adapter option accepts any object implementing the LocaleInstance interface. In that case messages, locale and fallback from createCds are unused — your engine becomes the source of truth, and the CDS strings live in the application's shared dictionaries under the $cds key.
import type { LocaleInstance, LocaleOptions } from '@central-design-system/components';
import { toRef } from 'vue';
import type { I18n } from 'vue-i18n';
export function createVueI18nAdapter(i18n: I18n<any, any, any, string, false>): LocaleInstance {
const format = (value: number) => i18n.global.n(value);
return {
name: 'vue-i18n',
current: i18n.global.locale,
fallback: i18n.global.fallbackLocale as never,
messages: i18n.global.messages as never,
decimalSeparator: toRef(() => (format(0.1).includes(',') ? ',' : '.')),
t: (key, ...params) => i18n.global.t(key, params as never),
n: format,
provide: () => createVueI18nAdapter(i18n)
};
}import { createI18n } from 'vue-i18n';
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: {
$cds: { noDataText: 'No data available' },
profile: { title: 'Profile' }
}
}
});
const cds = createCds({
components,
directives,
locale: {
adapter: createVueI18nAdapter(i18n)
}
});Required members of LocaleInstance: name (that's how the adapter is recognized), current, fallback, messages, decimalSeparator, t, n and provide — a factory for a nested locale, used by provideLocale. If you don't need "a locale in a subtree", provide can return the same instance, as in the example above.
Message keys
The built-in English dictionary is a template for translating into a new language. Copy it, translate the values and pass it to messages under your language's key; the Russian version is available as locales.ru.
export default {
badge: 'Badge',
noDataText: 'No data available',
selectAll: 'Select all',
close: 'Close',
open: 'Open',
dismiss: 'Dismiss',
confirmEdit: {
ok: 'OK',
cancel: 'Cancel'
},
carousel: {
prev: 'Previous slide',
next: 'Next slide',
delimiter: 'Slide {0} of {1}'
},
input: {
clear: 'Clear {0}',
otp: 'Please enter OTP character',
prependAction: '{0} prepended action',
appendAction: '{0} appended action'
},
fileInput: {
counter: '{0} files',
counterSize: '{0} files ({1} in total)'
},
fileUpload: {
title: 'Drag and drop files here',
divider: 'or',
browse: 'Browse files'
},
steps: {
prev: 'Back',
next: 'Next'
},
table: {
filterTitle: 'Filter menu',
filterConfirm: 'OK',
filterReset: 'Reset',
filterEmptyText: 'No filters',
filterCheckAll: 'Select all items',
filterSearchPlaceholder: 'Search in filters',
emptyText: 'No data',
selectAll: 'Select current page',
selectInvert: 'Invert current page',
selectNone: 'Clear all data',
selectionAll: 'Select all data',
sortTitle: 'Sort',
expand: 'Expand row',
collapse: 'Collapse row',
triggerDesc: 'Click to sort descending',
triggerAsc: 'Click to sort ascending',
cancelSort: 'Click to cancel sorting',
showed: 'Showed',
from: 'from',
rowOnPage: 'Row on page'
},
uploader: {
uploading: 'Uploading...',
uploadFile: 'Upload file',
downloadFile: 'Download file',
removeFile: 'Remove file',
uploadError: 'Upload error',
previewFile: 'Preview file'
}
};The dictionary may stay incomplete: missing keys are taken from the fallback — that is, from Russian by default.