Localisation (i18n)
Aucun libellé destiné à l'utilisateur n'est codé en dur au sein des composants : l'intégralité des chaînes de caractères est résolue dynamiquement via un dictionnaire de traduction. La bibliothèque est configurée en anglais (en) par défaut et fournit nativement la locale française (fr). L'ajout de langues supplémentaires s'effectue directement au niveau de l'application consommatrice.
La localisation repose sur un découplage strict entre le vocabulaire et le formatage. Les libellés textuels sont issus des dictionnaires de traduction, tandis que le formatage des données (dates, nombres, premier jour de la semaine et cycle 12/24h) s'appuie directement sur l'API native Intl à partir du tag de langue. Ainsi, déclarer une locale sans dictionnaire associé applique immédiatement les conventions régionales appropriées pour les données, tout en conservant les libellés d'interface en anglais. Ce comportement constitue une stratégie de dégradation gracieuse (graceful degradation) parfaitement maîtrisée.
Changer de langue
L'activation d'une locale repose sur deux étapes distinctes : l'enregistrement du dictionnaire de traduction, puis le choix de la locale active. Bien que fourni par la bibliothèque, le dictionnaire français (fr) est optionnel : l'omettre de vos imports suffit à l'exclure du bundle final (tree-shaking). Au-delà de l'optimisation du poids (inférieur à 1 Ko gzippé), ce modèle unifie l'intégration : activer la locale française intégrée ou ajouter une langue sur mesure s'effectue via un mécanisme rigoureusement identique, sans distinction de statut entre dictionnaires natifs et tiers.
import { fr, registerMessages, setLocale } from 'vectis-ui'
registerMessages('fr', fr)
setLocale('fr-FR')L'enregistrement des dictionnaires et la sélection initiale de la locale s'effectuent au niveau du module (dans main.ts ou un plugin Nuxt), en dehors du hook setup() des composants. Par la suite, setLocale peut être invoqué dynamiquement depuis n'importe quel point de l'application. La table de traduction s'appuyant sur l'état réactif de Vue, sa mise à jour déclenche le re-rendu immédiat de tous les composants montés, sans nécessiter de navigation ni de rechargement de page.
L'état d'i18n étant conservé au niveau du module, la locale est globale pour un processus d'exécution donné. Ce choix d'architecture implique une contrainte explicite : un même processus Node.js ne maintient qu'une seule locale active à la fois. Le rendu côté serveur (SSR) dynamique et concurrentiel par requête n'est donc pas pris en charge nativement ; dans ce scénario, les libellés doivent être transmis explicitement via les props des composants.
En revanche, cette limitation est sans impact sur le pré-rendu statique (SSG), les routes étant générées de façon séquentielle : la locale est définie juste avant la compilation de chaque page, garantissant la génération conforme de l'interface dans la langue ciblée.
Ajouter une langue
Un dictionnaire sur mesure est un simple objet JavaScript. La déclaration de dictionnaires partiels est totalement valide : toute clé absente retombe automatiquement sur le dictionnaire anglais au lieu d'afficher une clé technique brute. Enregistrez l'objet sous son sous-tag de langue, puis définissez la locale active.
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')En typant l'objet avec MessagesInput, l'éditeur fournit l'autoplétion complète pour les espaces de noms, les clés et les arguments des messages paramétrés. Les entrées textuelles sont formulées sous forme de fonctions TypeScript typées, sans dépendance à un moteur ICU ou de pluralisation complexe : la gestion des pluriels s'effectue par de simples expressions ternaires au sein des fonctions. La fusion des dictionnaires est non récursive par conception, l'arborescence étant strictement limitée à deux niveaux afin de préserver l'intégrité des fonctions de message.
La résolution textuelle s'appuie sur le sous-tag de langue (ex. en-GB et en-US partagent le dictionnaire en et ne diffèrent que par leurs formats Intl). Au sommet de la hiérarchie, les props explicites restent prioritaires : la chaîne de résolution pour le nom accessible d'un composant suit l'ordre de préséance suivant : aria-labelledby -> aria-label -> prop label -> dictionnaire actif -> dictionnaire anglais de secours. L'interface garantit l'absence de chaînes vides, de clés brutes à l'écran ou d'échecs silencieux en mode développement.
Langues et les formats
Le dictionnaire de traduction et la locale de formatage constituent deux réglages strictement indépendants. Tandis que registerMessages et setLocale déterminent la couche lexicale (les chaînes traduites), le code de locale (ainsi que la prop locale disponible sur les composants concernés) pilote les conventions régionales dérivées de l'API Intl (ordonnancement et séparateurs de dates, premier jour de la semaine, format 12/24h). Cette étanchéité permet d'associer librement un dictionnaire linguistique à un code régional distinct : une application peut par exemple afficher ses libellés en français tout en appliquant les formats canadiens anglais (en-CA), ou conserver une interface en anglais avec un formatage adapté à l'Allemagne (de-DE).
Nomenclature et référence des clés de traduction
Le dictionnaire complet de Vectis UI s'articule autour de 134 clés réparties au sein de 22 espaces de noms (namespaces), présentées ci-dessous avec leurs valeurs françaises à titre de référence. L'enregistrement supportant l'injection partielle, vous n'avez besoin de déclarer que les espaces de noms et les clés que vous souhaitez explicitement traduire.
Parmi ces clés, 22 sont des fonctions TypeScript paramétrées. Leur signature expose la liste des arguments attendus ainsi que leur placement dans la chaîne produite. En l'absence de moteur ICU ou de parseur de pluriel dédié, la gestion des formes grammaticales (dont la pluralisation) repose directement sur la logique conditionnelle native (expressions ternaires JS/TS), offrant toute la souplesse nécessaire aux langues complexes.
| Clé | Valeur française |
|---|---|
common.loading | Chargement… |
common.clear | Effacer |
common.close | Fermer |
common.dismiss | Retirer |
common.remove | (name) => `Retirer ${name}` |
common.cancel | Annuler |
common.confirm | OK |
pagination.label | Pagination |
pagination.previous | Page précédente |
pagination.next | Page suivante |
pagination.page | (page) => `Page ${page}` |
tabs.label | Onglets |
tabs.previous | Onglets précédents |
tabs.next | Onglets suivants |
breadcrumb.label | Fil d'Ariane |
breadcrumb.ellipsis | Afficher les pages intermédiaires |
sideNavigation.label | Navigation |
combobox.empty | Aucun résultat |
combobox.clear | Effacer la sélection |
dataTable.empty | Aucune donnée |
dataTable.loading | Chargement des données… |
dataTable.searchLabel | Rechercher dans le tableau |
dataTable.searchPlaceholder | Rechercher… |
dataTable.perPage | Lignes par page |
dataTable.perPageValue | (label, value) => `${label} : ${value}` |
dataTable.selectAll | Tout sélectionner |
dataTable.selectRow | (index) => `Sélectionner la ligne ${index}` |
dataTable.selection | (count) => `${count} élément${count > 1 ? 's' : ''} sélectionné${count > 1 ? 's' : ''}` |
dataTable.range | ({ start, end, total }) => `${start}–${end} sur ${total}` |
dataTable.pagination | Pagination du tableau |
toaster.label | Notifications |
snackbar.label | Confirmation |
snackbar.action | Annuler |
inputOTP.label | Code de vérification |
inputOTP.slot | (index, total) => `Caractère ${index} sur ${total}` |
slider.value | Valeur |
slider.start | Début |
slider.end | Fin |
slider.rangeStart | (label) => `${label} (début)` |
slider.rangeEnd | (label) => `${label} (fin)` |
field.limitExceeded | (max) => `Dépasse la limite de ${max} caractères` |
progress.percent | (percent) => `${percent}\u00A0%` |
progress.label | Progression |
hotkeys.command | Commande |
hotkeys.ctrl | Ctrl |
hotkeys.alt | Alt |
hotkeys.shift | Maj |
hotkeys.windows | Win |
hotkeys.super | Super |
hotkeys.enter | Entrée |
hotkeys.escape | Échap |
hotkeys.space | Espace |
hotkeys.backspace | Retour arrière |
hotkeys.delete | Suppr |
hotkeys.tab | Tab |
hotkeys.up | Flèche haut |
hotkeys.down | Flèche bas |
hotkeys.left | Flèche gauche |
hotkeys.right | Flèche droite |
hotkeys.label | (keys) => `Raccourci clavier : ${keys}` |
datePicker.label | Sélecteur de date |
datePicker.previousMonth | Mois précédent |
datePicker.nextMonth | Mois suivant |
datePicker.previousYear | Année précédente |
datePicker.nextYear | Année suivante |
datePicker.monthPicker | Choix du mois |
datePicker.yearPicker | Choix de l'année |
dateInput.clear | Effacer la date |
dateInput.openPicker | Ouvrir le calendrier |
dateInput.pickerLabel | Choisir une date |
timePicker.label | Sélecteur d'heure |
timePicker.meridiem | AM ou PM |
timePicker.am | AM |
timePicker.pm | PM |
timePicker.selectHour | Sélectionner l’heure |
timePicker.selectMinutes | Sélectionner les minutes |
timePicker.choosingHour | Sélection de l’heure |
timePicker.choosingMinutes | Sélection des minutes |
timePicker.hour | Heure |
timePicker.minutes | Minutes |
timePicker.hourValue | (hour) => `${hour} heures` |
timePicker.minutesValue | (minute) => `${minute} minutes` |
timeInput.clear | Effacer l'heure |
timeInput.openPicker | Ouvrir le sélecteur d’heure |
timeInput.pickerLabel | Choisir une heure |
timeInput.meridiemValue | (value) => `AM ou PM : ${value}` |
timeInput.maskPlaceholder | hh:mm |
timeInput.unavailable | Cette heure n'est pas disponible. |
fileInput.openPicker | Choisir des fichiers |
fileInput.clear | Effacer les fichiers |
fileInput.files | (count) => `${count} fichier${count > 1 ? 's' : ''}` |
fileInput.placeholder | Aucun fichier sélectionné |
filePicker.browse | Parcourir les fichiers |
filePicker.or | ou |
filePicker.list | Fichiers sélectionnés |
carousel.label | Carrousel |
carousel.roleDescription | carrousel |
carousel.slideRoleDescription | diapositive |
carousel.slides | Diapositives |
carousel.slide | (index, total) => `${index} sur ${total}` |
carousel.previous | Diapositive précédente |
carousel.next | Diapositive suivante |
carousel.indicators | Choisir la diapositive à afficher |
calendar.label | Calendrier |
calendar.roleDescription | calendrier |
calendar.today | Aujourd'hui |
calendar.view | Affichage |
calendar.viewDay | Jour |
calendar.view4Days | 4 jours |
calendar.viewWeek | Semaine |
calendar.viewMonth | Mois |
calendar.viewYear | Année |
calendar.viewCustom | (days) => `${days} jours` |
calendar.previousDay | Jour précédent |
calendar.nextDay | Jour suivant |
calendar.previousWeek | Semaine précédente |
calendar.nextWeek | Semaine suivante |
calendar.previousMonth | Mois précédent |
calendar.nextMonth | Mois suivant |
calendar.previousYear | Année précédente |
calendar.nextYear | Année suivante |
calendar.previousPeriod | Période précédente |
calendar.nextPeriod | Période suivante |
calendar.allDay | Journée |
calendar.moreEvents | (count) => `+${count} autre${count > 1 ? 's' : ''}` |
calendar.openDay | (day) => `Ouvrir le ${day}` |
calendar.untitled | (Sans titre) |
calendar.eventRoleDescription | évènement |
calendar.eventHint | Appuyez sur Entrée pour ouvrir cet évènement. Appuyez sur Espace pour le saisir, puis sur les flèches pour le déplacer et sur Maj avec les flèches pour changer son heure de fin. |
calendar.grabbed | Évènement saisi. Utilisez les flèches pour le déplacer, Entrée ou Espace pour le poser, Échap pour annuler. |
calendar.dropped | Évènement posé. |
calendar.reverted | Déplacement annulé. L'évènement est revenu à sa place. |
calendar.movedTo | (title, when) => `${title} déplacé au ${when}.` |