Localization (i18n)
No user-facing strings are hardcoded within the components: all character strings are resolved dynamically via a translation dictionary. The library is configured in English (en) by default and natively provides the French locale (fr). Additional languages can be added directly at the consuming application level.
Localization relies on a strict decoupling between vocabulary and formatting. Text labels come from translation dictionaries, while data formatting (dates, numbers, first day of the week, and 12/24h cycles) relies directly on the native Intl API using the language tag. Thus, declaring a locale without an associated dictionary immediately applies the appropriate regional conventions for data while keeping interface labels in English. This behavior constitutes a perfectly managed graceful degradation strategy.
Changing Language
Activating a locale relies on two distinct steps: registering the translation dictionary, and choosing the active locale. Although provided by the library, the French dictionary (fr) is optional: omitting it from your imports is enough to exclude it from the final bundle (tree-shaking). Beyond bundle size optimization (under 1 kB gzipped), this model unifies integration: activating the built-in French locale or adding a custom language is done via a rigorously identical mechanism, with no status distinction between native and third-party dictionaries.
import { fr, registerMessages, setLocale } from 'vectis-ui'
registerMessages('fr', fr)
setLocale('fr-FR')Dictionary registration and initial locale selection take place at the module level (in main.ts or a Nuxt plugin), outside of component setup() hooks. Subsequently, setLocale can be invoked dynamically from any point in the application. Since the translation table relies on Vue's reactive state, updating it triggers an immediate re-render of all mounted components, without requiring navigation or page reloads.
Because the i18n state is maintained at the module level, the locale is global for a given execution process. This architectural choice implies an explicit constraint: a single Node.js process maintains only one active locale at a time. Dynamic and concurrent server-side rendering (SSR) per request is therefore not natively supported; in this scenario, labels must be explicitly passed via component props.
On the other hand, this limitation has no impact on static pre-rendering (SSG), as routes are generated sequentially: the locale is set just before compiling each page, ensuring compliant generation of the interface in the targeted language.
Adding a Language
A custom dictionary is a simple JavaScript object. Declaring partial dictionaries is fully valid: any missing key automatically falls back to the English dictionary instead of displaying a raw technical key. Register the object under its language sub-tag, then set the active locale.
import { registerMessages, setLocale, type MessagesInput } from 'vectis-ui'
// Partial is legitimate: what is missing falls back to English.
const de: MessagesInput = {
common: { clear: 'Leeren', close: 'Schließen' },
dataTable: { empty: 'Keine Daten' },
}
registerMessages('de', de)
setLocale('de-DE')By typing the object with MessagesInput, the editor provides full autocompletion for namespaces, keys, and parameterized message arguments. Text entries are formulated as typed TypeScript functions, without dependency on an ICU engine or complex pluralization: plural management is handled via simple ternary expressions within the functions. Dictionary merging is non-recursive by design, with the tree structure strictly limited to two levels to preserve the integrity of message functions.
Text resolution relies on the language sub-tag (e.g., en-GB and en-US share the en dictionary and differ only in their Intl formats). At the top of the hierarchy, explicit props remain paramount: the resolution chain for a component's accessible name follows this precedence order: aria-labelledby -> aria-label -> label prop -> active dictionary -> fallback English dictionary. The interface guarantees the absence of empty strings, raw keys on screen, or silent failures in development mode.
Languages and Formats
The translation dictionary and formatting locale constitute two strictly independent settings. While registerMessages and setLocale determine the lexical layer (translated strings), the locale code (along with the locale prop available on relevant components) drives regional conventions derived from the Intl API (date ordering and separators, first day of the week, 12/24h format). This isolation allows freely combining a linguistic dictionary with a distinct regional code: an application can, for example, display its labels in French while applying English Canadian formats (en-CA), or keep an English interface formatted for Germany (de-DE).
Translation Key Nomenclature and Reference
The complete Vectis UI dictionary spans 134 keys distributed across 22 namespaces, presented below with their French values for reference. Since registration supports partial injection, you only need to declare the namespaces and keys you explicitly wish to translate.
Among these keys, 22 are parameterized TypeScript functions. Their signature exposes the expected arguments and their placement within the generated string. In the absence of an ICU engine or dedicated plural parser, grammatical forms (including pluralization) rely directly on native conditional logic (JS/TS ternary expressions), offering the flexibility needed for complex languages.
| Key | English Value |
|---|---|
common.loading | Loading… |
common.clear | Clear |
common.close | Close |
common.dismiss | Remove |
common.remove | (name) => `Remove ${name}` |
common.cancel | Cancel |
common.confirm | OK |
pagination.label | Pagination |
pagination.previous | Previous page |
pagination.next | Next page |
pagination.page | (page) => `Page ${page}` |
tabs.label | Tabs |
tabs.previous | Previous tabs |
tabs.next | Next tabs |
breadcrumb.label | Breadcrumb |
breadcrumb.ellipsis | Show intermediate pages |
sideNavigation.label | Navigation |
combobox.empty | No results |
combobox.clear | Clear selection |
dataTable.empty | No data |
dataTable.loading | Loading data… |
dataTable.searchLabel | Search the table |
dataTable.searchPlaceholder | Search… |
dataTable.perPage | Rows per page |
dataTable.perPageValue | (label, value) => `${label}: ${value}` |
dataTable.selectAll | Select all |
dataTable.selectRow | (index) => `Select row ${index}` |
dataTable.selection | (count) => `${count} item${count === 1 ? '' : 's'} selected` |
dataTable.range | ({ start, end, total }) => `${start}–${end} of ${total}` |
dataTable.pagination | Table pagination |
toaster.label | Notifications |
snackbar.label | Confirmation |
snackbar.action | Undo |
inputOTP.label | Verification code |
inputOTP.slot | (index, total) => `Character ${index} of ${total}` |
slider.value | Value |
slider.start | Start |
slider.end | End |
slider.rangeStart | (label) => `${label} (start)` |
slider.rangeEnd | (label) => `${label} (end)` |
field.limitExceeded | (max) => `Exceeds the limit of ${max} characters` |
progress.percent | (percent) => `${percent}%` |
progress.label | Progress |
hotkeys.command | Command |
hotkeys.ctrl | Ctrl |
hotkeys.alt | Alt |
hotkeys.shift | Shift |
hotkeys.windows | Win |
hotkeys.super | Super |
hotkeys.enter | Enter |
hotkeys.escape | Esc |
hotkeys.space | Space |
hotkeys.backspace | Backspace |
hotkeys.delete | Del |
hotkeys.tab | Tab |
hotkeys.up | Up arrow |
hotkeys.down | Down arrow |
hotkeys.left | Left arrow |
hotkeys.right | Right arrow |
hotkeys.label | (keys) => `Keyboard shortcut: ${keys}` |
datePicker.label | Date picker |
datePicker.previousMonth | Previous month |
datePicker.nextMonth | Next month |
datePicker.previousYear | Previous year |
datePicker.nextYear | Next year |
datePicker.monthPicker | Choose month |
datePicker.yearPicker | Choose year |
dateInput.clear | Clear date |
dateInput.openPicker | Open calendar |
dateInput.pickerLabel | Choose a date |
timePicker.label | Time picker |
timePicker.meridiem | AM or PM |
timePicker.am | AM |
timePicker.pm | PM |
timePicker.selectHour | Select hour |
timePicker.selectMinutes | Select minutes |
timePicker.choosingHour | Selecting the hour |
timePicker.choosingMinutes | Selecting the minutes |
timePicker.hour | Hour |
timePicker.minutes | Minutes |
timePicker.hourValue | (hour) => `${hour} o'clock` |
timePicker.minutesValue | (minute) => `${minute} minutes` |
timeInput.clear | Clear time |
timeInput.openPicker | Open time picker |
timeInput.pickerLabel | Choose a time |
timeInput.meridiemValue | (value) => `AM or PM: ${value}` |
timeInput.maskPlaceholder | hh:mm |
timeInput.unavailable | This time is not available. |
fileInput.openPicker | Choose files |
fileInput.clear | Clear files |
fileInput.files | (count) => `${count} file${count === 1 ? '' : 's'}` |
fileInput.placeholder | No file selected |
filePicker.browse | Browse files |
filePicker.or | or |
filePicker.list | Selected files |
carousel.label | Carousel |
carousel.roleDescription | carousel |
carousel.slideRoleDescription | slide |
carousel.slides | Slides |
carousel.slide | (index, total) => `${index} of ${total}` |
carousel.previous | Previous slide |
carousel.next | Next slide |
carousel.indicators | Choose slide to display |
calendar.label | Calendar |
calendar.roleDescription | calendar |
calendar.today | Today |
calendar.view | View |
calendar.viewDay | Day |
calendar.view4Days | 4 days |
calendar.viewWeek | Week |
calendar.viewMonth | Month |
calendar.viewYear | Year |
calendar.viewCustom | (days) => `${days} days` |
calendar.previousDay | Previous day |
calendar.nextDay | Next day |
calendar.previousWeek | Previous week |
calendar.nextWeek | Next week |
calendar.previousMonth | Previous month |
calendar.nextMonth | Next month |
calendar.previousYear | Previous year |
calendar.nextYear | Next year |
calendar.previousPeriod | Previous period |
calendar.nextPeriod | Next period |
calendar.allDay | All day |
calendar.moreEvents | (count) => `+${count} more` |
calendar.openDay | (day) => `Open ${day}` |
calendar.untitled | (No title) |
calendar.eventRoleDescription | event |
calendar.eventHint | Press Enter to open this event. Press Space to take hold of it, then the arrow keys to move it and Shift with the arrow keys to change when it ends. |
calendar.grabbed | Event held. Use the arrow keys to move it, Enter or Space to place it, Escape to cancel. |
calendar.dropped | Event placed. |
calendar.reverted | Move cancelled. The event is back where it was. |
calendar.movedTo | (title, when) => `${title} moved to ${when}.` |