Keyboard shortcut: Ctrl + K
Get started

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.

ts
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.

ts
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.

KeyEnglish Value
common.loadingLoading…
common.clearClear
common.closeClose
common.dismissRemove
common.remove(name) => `Remove ${name}`
common.cancelCancel
common.confirmOK
pagination.labelPagination
pagination.previousPrevious page
pagination.nextNext page
pagination.page(page) => `Page ${page}`
tabs.labelTabs
tabs.previousPrevious tabs
tabs.nextNext tabs
breadcrumb.labelBreadcrumb
breadcrumb.ellipsisShow intermediate pages
sideNavigation.labelNavigation
combobox.emptyNo results
combobox.clearClear selection
dataTable.emptyNo data
dataTable.loadingLoading data…
dataTable.searchLabelSearch the table
dataTable.searchPlaceholderSearch…
dataTable.perPageRows per page
dataTable.perPageValue(label, value) => `${label}: ${value}`
dataTable.selectAllSelect 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.paginationTable pagination
toaster.labelNotifications
snackbar.labelConfirmation
snackbar.actionUndo
inputOTP.labelVerification code
inputOTP.slot(index, total) => `Character ${index} of ${total}`
slider.valueValue
slider.startStart
slider.endEnd
slider.rangeStart(label) => `${label} (start)`
slider.rangeEnd(label) => `${label} (end)`
field.limitExceeded(max) => `Exceeds the limit of ${max} characters`
progress.percent(percent) => `${percent}%`
progress.labelProgress
hotkeys.commandCommand
hotkeys.ctrlCtrl
hotkeys.altAlt
hotkeys.shiftShift
hotkeys.windowsWin
hotkeys.superSuper
hotkeys.enterEnter
hotkeys.escapeEsc
hotkeys.spaceSpace
hotkeys.backspaceBackspace
hotkeys.deleteDel
hotkeys.tabTab
hotkeys.upUp arrow
hotkeys.downDown arrow
hotkeys.leftLeft arrow
hotkeys.rightRight arrow
hotkeys.label(keys) => `Keyboard shortcut: ${keys}`
datePicker.labelDate picker
datePicker.previousMonthPrevious month
datePicker.nextMonthNext month
datePicker.previousYearPrevious year
datePicker.nextYearNext year
datePicker.monthPickerChoose month
datePicker.yearPickerChoose year
dateInput.clearClear date
dateInput.openPickerOpen calendar
dateInput.pickerLabelChoose a date
timePicker.labelTime picker
timePicker.meridiemAM or PM
timePicker.amAM
timePicker.pmPM
timePicker.selectHourSelect hour
timePicker.selectMinutesSelect minutes
timePicker.choosingHourSelecting the hour
timePicker.choosingMinutesSelecting the minutes
timePicker.hourHour
timePicker.minutesMinutes
timePicker.hourValue(hour) => `${hour} o'clock`
timePicker.minutesValue(minute) => `${minute} minutes`
timeInput.clearClear time
timeInput.openPickerOpen time picker
timeInput.pickerLabelChoose a time
timeInput.meridiemValue(value) => `AM or PM: ${value}`
timeInput.maskPlaceholderhh:mm
timeInput.unavailableThis time is not available.
fileInput.openPickerChoose files
fileInput.clearClear files
fileInput.files(count) => `${count} file${count === 1 ? '' : 's'}`
fileInput.placeholderNo file selected
filePicker.browseBrowse files
filePicker.oror
filePicker.listSelected files
carousel.labelCarousel
carousel.roleDescriptioncarousel
carousel.slideRoleDescriptionslide
carousel.slidesSlides
carousel.slide(index, total) => `${index} of ${total}`
carousel.previousPrevious slide
carousel.nextNext slide
carousel.indicatorsChoose slide to display
calendar.labelCalendar
calendar.roleDescriptioncalendar
calendar.todayToday
calendar.viewView
calendar.viewDayDay
calendar.view4Days4 days
calendar.viewWeekWeek
calendar.viewMonthMonth
calendar.viewYearYear
calendar.viewCustom(days) => `${days} days`
calendar.previousDayPrevious day
calendar.nextDayNext day
calendar.previousWeekPrevious week
calendar.nextWeekNext week
calendar.previousMonthPrevious month
calendar.nextMonthNext month
calendar.previousYearPrevious year
calendar.nextYearNext year
calendar.previousPeriodPrevious period
calendar.nextPeriodNext period
calendar.allDayAll day
calendar.moreEvents(count) => `+${count} more`
calendar.openDay(day) => `Open ${day}`
calendar.untitled(No title)
calendar.eventRoleDescriptionevent
calendar.eventHintPress 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.grabbedEvent held. Use the arrow keys to move it, Enter or Space to place it, Escape to cancel.
calendar.droppedEvent placed.
calendar.revertedMove cancelled. The event is back where it was.
calendar.movedTo(title, when) => `${title} moved to ${when}.`