diff --git a/.changeset/30512-platform-objects-fr.md b/.changeset/30512-platform-objects-fr.md new file mode 100644 index 00000000000..a9b30574df1 --- /dev/null +++ b/.changeset/30512-platform-objects-fr.md @@ -0,0 +1,10 @@ +--- +'@objectstack/platform-objects': patch +--- + +i18n(platform-objects): the platform objects, Setup, Studio and Account ship in French + +Clause-②: no +Metadata-protocol: none + +`@objectstack/platform-objects` now carries `fr` beside `en`, `zh-CN`, `ja-JP`, `es-ES`, `zh-Hant` and `ar`: every `sys_*` object (labels, fields, help, options, views, actions and their dialogs), the metadata-type configuration forms Studio opens, the Setup, Studio and Account navigation, the System Overview dashboard and its datasets, the platform pages' titles, and the inline copy on the user and organization record pages. A user whose locale is `fr` no longer reads English `sys_user` field labels inside French sentences, as the Home feed showed them (`A modifié Utilisateur « … » : Banned et Ban Reason`). The new bundles pass through the same source-hash fallback as the existing ones, and every hand-authored leaf records the digest of the source it was translated from. The extract config documents `fr` in its `--locales`, so `check:i18n` now gates the new bundles. Package 11 of #30512, third of seven locales; `pt-BR`, `ru`, `de` and `ko` follow. diff --git a/packages/platform-objects/scripts/i18n-extract.config.ts b/packages/platform-objects/scripts/i18n-extract.config.ts index 60276d909cb..cada35f7e05 100644 --- a/packages/platform-objects/scripts/i18n-extract.config.ts +++ b/packages/platform-objects/scripts/i18n-extract.config.ts @@ -8,7 +8,7 @@ * pointed at this file via `bundle-require`: * * os i18n extract packages/platform-objects/scripts/i18n-extract.config.ts \ - * --locales=zh-CN,ja-JP,es-ES,zh-Hant,ar \ + * --locales=zh-CN,ja-JP,es-ES,zh-Hant,ar,fr \ * --fill=default \ * --source-hashes \ * --out=packages/platform-objects/src/apps/translations @@ -223,12 +223,14 @@ import { jaJPObjects } from '../src/apps/translations/ja-JP.objects.generated.js import { esESObjects } from '../src/apps/translations/es-ES.objects.generated.js'; import { zhHantObjects } from '../src/apps/translations/zh-Hant.objects.generated.js'; import { arObjects } from '../src/apps/translations/ar.objects.generated.js'; +import { frObjects } from '../src/apps/translations/fr.objects.generated.js'; import { enMetadataForms } from '../src/apps/translations/en.metadata-forms.generated.js'; import { zhCNMetadataForms } from '../src/apps/translations/zh-CN.metadata-forms.generated.js'; import { jaJPMetadataForms } from '../src/apps/translations/ja-JP.metadata-forms.generated.js'; import { esESMetadataForms } from '../src/apps/translations/es-ES.metadata-forms.generated.js'; import { zhHantMetadataForms } from '../src/apps/translations/zh-Hant.metadata-forms.generated.js'; import { arMetadataForms } from '../src/apps/translations/ar.metadata-forms.generated.js'; +import { frMetadataForms } from '../src/apps/translations/fr.metadata-forms.generated.js'; // ── Existing hand-authored app/dashboard/page translations ──────────────── // The per-locale assemblers, NOT the `SetupAppTranslations` wrapper. Each one @@ -253,6 +255,7 @@ import { jaJP } from '../src/apps/translations/ja-JP.js'; import { esES } from '../src/apps/translations/es-ES.js'; import { zhHant } from '../src/apps/translations/zh-Hant.js'; import { ar } from '../src/apps/translations/ar.js'; +import { fr } from '../src/apps/translations/fr.js'; /** * The annotation is load-bearing, not decoration (#10868). `defineStack` @@ -355,6 +358,7 @@ const config: ObjectStackDefinition = defineStack({ { 'es-ES': { ...esES, objects: esESObjects, metadataForms: esESMetadataForms } }, { 'zh-Hant': { ...zhHant, objects: zhHantObjects, metadataForms: zhHantMetadataForms } }, { 'ar': { ...ar, objects: arObjects, metadataForms: arMetadataForms } }, + { 'fr': { ...fr, objects: frObjects, metadataForms: frMetadataForms } }, ], }); diff --git a/packages/platform-objects/src/apps/dashboards/system-overview-null-label.test.ts b/packages/platform-objects/src/apps/dashboards/system-overview-null-label.test.ts index afe7dcffab3..0d4e4cf5c2e 100644 --- a/packages/platform-objects/src/apps/dashboards/system-overview-null-label.test.ts +++ b/packages/platform-objects/src/apps/dashboards/system-overview-null-label.test.ts @@ -32,6 +32,7 @@ describe('System Overview: the null-user bucket reads "System"', () => { 'es-ES': 'Sistema', 'zh-Hant': '系統', ar: 'النظام', + fr: 'Système', }); }); diff --git a/packages/platform-objects/src/apps/dashboards/system.datasets.ts b/packages/platform-objects/src/apps/dashboards/system.datasets.ts index 4096eb43121..97a29f2a4f1 100644 --- a/packages/platform-objects/src/apps/dashboards/system.datasets.ts +++ b/packages/platform-objects/src/apps/dashboards/system.datasets.ts @@ -57,7 +57,7 @@ export const SysAuditLogDataset = defineDataset({ label: 'User', field: 'user_id', type: 'lookup', - nullLabel: { en: 'System', 'zh-CN': '系统', 'ja-JP': 'システム', 'es-ES': 'Sistema', 'zh-Hant': '系統', ar: 'النظام' }, + nullLabel: { en: 'System', 'zh-CN': '系统', 'ja-JP': 'システム', 'es-ES': 'Sistema', 'zh-Hant': '系統', ar: 'النظام', fr: 'Système' }, }, ], measures: [{ name: 'event_count', label: 'Events', aggregate: 'count' }], diff --git a/packages/platform-objects/src/apps/setup-copy-vocabulary.test.ts b/packages/platform-objects/src/apps/setup-copy-vocabulary.test.ts index 069ebefcd76..152f91d611b 100644 --- a/packages/platform-objects/src/apps/setup-copy-vocabulary.test.ts +++ b/packages/platform-objects/src/apps/setup-copy-vocabulary.test.ts @@ -47,18 +47,21 @@ import { jaJP } from './translations/ja-JP.js'; import { esES } from './translations/es-ES.js'; import { zhHant } from './translations/zh-Hant.js'; import { ar } from './translations/ar.js'; +import { fr } from './translations/fr.js'; import { enObjects } from './translations/en.objects.generated.js'; import { zhCNObjects } from './translations/zh-CN.objects.generated.js'; import { jaJPObjects } from './translations/ja-JP.objects.generated.js'; import { esESObjects } from './translations/es-ES.objects.generated.js'; import { zhHantObjects } from './translations/zh-Hant.objects.generated.js'; import { arObjects } from './translations/ar.objects.generated.js'; +import { frObjects } from './translations/fr.objects.generated.js'; import { enMetadataForms } from './translations/en.metadata-forms.generated.js'; import { zhCNMetadataForms } from './translations/zh-CN.metadata-forms.generated.js'; import { jaJPMetadataForms } from './translations/ja-JP.metadata-forms.generated.js'; import { esESMetadataForms } from './translations/es-ES.metadata-forms.generated.js'; import { zhHantMetadataForms } from './translations/zh-Hant.metadata-forms.generated.js'; import { arMetadataForms } from './translations/ar.metadata-forms.generated.js'; +import { frMetadataForms } from './translations/fr.metadata-forms.generated.js'; /** The design-record vocabulary an admin-facing string must not carry. */ const DESIGN_RECORD_VOCABULARY: ReadonlyArray = [ @@ -133,14 +136,15 @@ function objectSchemas(...modules: Array>): Map { diff --git a/packages/platform-objects/src/apps/setup-nav-distinct-labels.test.ts b/packages/platform-objects/src/apps/setup-nav-distinct-labels.test.ts index fce376c27e8..74c0aac8b72 100644 --- a/packages/platform-objects/src/apps/setup-nav-distinct-labels.test.ts +++ b/packages/platform-objects/src/apps/setup-nav-distinct-labels.test.ts @@ -17,8 +17,9 @@ import { jaJP } from './translations/ja-JP.js'; import { esES } from './translations/es-ES.js'; import { zhHant } from './translations/zh-Hant.js'; import { ar } from './translations/ar.js'; +import { fr } from './translations/fr.js'; -const LOCALES = { en, 'zh-CN': zhCN, 'ja-JP': jaJP, 'es-ES': esES, 'zh-Hant': zhHant, ar } as const; +const LOCALES = { en, 'zh-CN': zhCN, 'ja-JP': jaJP, 'es-ES': esES, 'zh-Hant': zhHant, ar, fr } as const; type NavItem = { id?: string; label?: string }; diff --git a/packages/platform-objects/src/apps/setup-nav-per-user-stores.test.ts b/packages/platform-objects/src/apps/setup-nav-per-user-stores.test.ts index ced8657c2da..df579277ca1 100644 --- a/packages/platform-objects/src/apps/setup-nav-per-user-stores.test.ts +++ b/packages/platform-objects/src/apps/setup-nav-per-user-stores.test.ts @@ -112,9 +112,10 @@ describe('(2) the User page lists each per-user store, keyed to its record (#238 'es-ES': 'Preferencias', 'zh-Hant': '偏好設定', ar: 'التفضيلات', + fr: 'Préférences', }); expect((tabOf('sys_account')?.label as Rec)?.en).toBe('Linked Accounts'); - expect(Object.keys((tabOf('sys_account')?.label as Rec) ?? {}).sort()).toEqual(['ar', 'en', 'es-ES', 'ja-JP', 'zh-CN', 'zh-Hant']); + expect(Object.keys((tabOf('sys_account')?.label as Rec) ?? {}).sort()).toEqual(['ar', 'en', 'es-ES', 'fr', 'ja-JP', 'zh-CN', 'zh-Hant']); }); }); diff --git a/packages/platform-objects/src/apps/translations/app-nav-translation-parity.test.ts b/packages/platform-objects/src/apps/translations/app-nav-translation-parity.test.ts index a0f17a40753..2d2f4db35d9 100644 --- a/packages/platform-objects/src/apps/translations/app-nav-translation-parity.test.ts +++ b/packages/platform-objects/src/apps/translations/app-nav-translation-parity.test.ts @@ -60,8 +60,9 @@ import { jaJP } from './ja-JP.js'; import { esES } from './es-ES.js'; import { zhHant } from './zh-Hant.js'; import { ar } from './ar.js'; +import { fr } from './fr.js'; -const LOCALES = { en, 'zh-CN': zhCN, 'ja-JP': jaJP, 'es-ES': esES, 'zh-Hant': zhHant, ar } as const; +const LOCALES = { en, 'zh-CN': zhCN, 'ja-JP': jaJP, 'es-ES': esES, 'zh-Hant': zhHant, ar, fr } as const; /** Every statically declared nav item of an app, depth-first, with its label. */ function navItems(app: { navigation?: unknown[] }): Array<{ id: string; label?: string }> { diff --git a/packages/platform-objects/src/apps/translations/bu-rename-consistency.test.ts b/packages/platform-objects/src/apps/translations/bu-rename-consistency.test.ts index 962c7893df0..91cf92a3ee0 100644 --- a/packages/platform-objects/src/apps/translations/bu-rename-consistency.test.ts +++ b/packages/platform-objects/src/apps/translations/bu-rename-consistency.test.ts @@ -16,6 +16,7 @@ import { jaJPObjects } from './ja-JP.objects.generated.js'; import { esESObjects } from './es-ES.objects.generated.js'; import { zhHantObjects } from './zh-Hant.objects.generated.js'; import { arObjects } from './ar.objects.generated.js'; +import { frObjects } from './fr.objects.generated.js'; const LOCALES = [ { name: 'en', objs: enObjects as Record, old: /\bDepartment\b/ }, @@ -24,6 +25,7 @@ const LOCALES = [ { name: 'es-ES', objs: esESObjects as Record, old: /Departamento/ }, { name: 'zh-Hant', objs: zhHantObjects as Record, old: /部門/ }, { name: 'ar', objs: arObjects as Record, old: /قسم/ }, + { name: 'fr', objs: frObjects as Record, old: /Département/ }, ]; const OBJECTS = ['sys_business_unit', 'sys_business_unit_member']; diff --git a/packages/platform-objects/src/apps/translations/confirm-question-carryover.test.ts b/packages/platform-objects/src/apps/translations/confirm-question-carryover.test.ts index 893f16b6a97..6f2bdaa5145 100644 --- a/packages/platform-objects/src/apps/translations/confirm-question-carryover.test.ts +++ b/packages/platform-objects/src/apps/translations/confirm-question-carryover.test.ts @@ -27,6 +27,7 @@ import { jaJPObjects } from './ja-JP.objects.generated.js'; import { esESObjects } from './es-ES.objects.generated.js'; import { zhHantObjects } from './zh-Hant.objects.generated.js'; import { arObjects } from './ar.objects.generated.js'; +import { frObjects } from './fr.objects.generated.js'; const LOCALES = [ ['zh-CN', zhCNObjects], @@ -34,6 +35,7 @@ const LOCALES = [ ['es-ES', esESObjects], ['zh-Hant', zhHantObjects], ['ar', arObjects], + ['fr', frObjects], ] as const; /** diff --git a/packages/platform-objects/src/apps/translations/dashboard-header-children.test.ts b/packages/platform-objects/src/apps/translations/dashboard-header-children.test.ts index ecd9a1f2525..cfbd7da2671 100644 --- a/packages/platform-objects/src/apps/translations/dashboard-header-children.test.ts +++ b/packages/platform-objects/src/apps/translations/dashboard-header-children.test.ts @@ -26,6 +26,7 @@ import { jaJPMetadataForms } from './ja-JP.metadata-forms.generated.js'; import { esESMetadataForms } from './es-ES.metadata-forms.generated.js'; import { zhHantMetadataForms } from './zh-Hant.metadata-forms.generated.js'; import { arMetadataForms } from './ar.metadata-forms.generated.js'; +import { frMetadataForms } from './fr.metadata-forms.generated.js'; const LOCALES = [ { name: 'en', forms: enMetadataForms as Record }, @@ -34,6 +35,7 @@ const LOCALES = [ { name: 'es-ES', forms: esESMetadataForms as Record }, { name: 'zh-Hant', forms: zhHantMetadataForms as Record }, { name: 'ar', forms: arMetadataForms as Record }, + { name: 'fr', forms: frMetadataForms as Record }, ]; const HEADER_CHILDREN = ['header.showTitle', 'header.showDescription', 'header.actions']; diff --git a/packages/platform-objects/src/apps/translations/fr.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/fr.metadata-forms.generated.ts new file mode 100644 index 00000000000..dd02ba240d9 --- /dev/null +++ b/packages/platform-objects/src/apps/translations/fr.metadata-forms.generated.ts @@ -0,0 +1,2815 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'fr'. + * Edit translations in place; re-run extract (with --merge) to fill new gaps. + * Do not hand-edit the structure — only the leaf string values. + * Merge only fills gaps: correcting a source label/description does not + * push the correction into a leaf here that already holds a translation — + * a present-but-stale string is not a gap, so it is left as-is. Re-translate + * it by hand when its source changes; nothing here or in `os i18n check` + * tells a leaf a translator updated on purpose from one nobody has looked + * at since the source moved. + */ + +import type { TranslationData } from '@objectstack/spec/system'; + +export const frMetadataForms: NonNullable = { + object: { + label: "Objet", + sections: { + basics: { + label: "Informations de base", + description: "Identité, libellés et taxonomie." + }, + fields: { + label: "Champs", + description: "Définissez le modèle de données — chaque entrée devient une colonne de la table de la base de données." + }, + capabilities: { + label: "Capacités", + description: "Fonctionnalités système et exposition via l’API." + }, + advanced: { + label: "Avancé", + description: "Machines à états, actions et stockage." + } + }, + fields: { + name: { + label: "Nom", + helpText: "Identifiant unique snake_case (non modifiable après la création)" + }, + label: { + label: "Libellé", + helpText: "Nom d’affichage au singulier (par ex. « Compte »)" + }, + pluralLabel: { + label: "Libellé au pluriel", + helpText: "Nom d’affichage au pluriel (par ex. « Comptes »)" + }, + icon: { + label: "Icône", + helpText: "Nom d’icône Lucide (par ex. « building », « users »)" + }, + description: { + label: "Description", + helpText: "Documentation pour les développeurs" + }, + nameField: { + label: "Champ de nom", + helpText: "Champ dont la valeur sert de titre à chaque enregistrement (par ex. « name », « subject »). Pointeur canonique ADR-0079 — lu par l’affichage des enregistrements, la recherche ObjectQL et les aperçus d’enregistrements associés." + }, + imageField: { + label: "Champ d’image", + helpText: "Champ dont la valeur est l’image de chaque enregistrement, dessinée à côté du titre dans l’en-tête de la page de l’enregistrement. Doit désigner un champ de cet objet de type image ou avatar ; tout autre nom est refusé lors de l’enregistrement de l’objet. Vide : aucune image d’enregistrement, et aucun espace réservé n’est dessiné." + }, + isSystem: { + label: "Objet système", + helpText: "Objet système (protégé contre la suppression ; partage public par défaut)" + }, + highlightFields: { + label: "Champs mis en avant", + helpText: "Noms des champs de cet objet, du plus important au moins important — la première entrée l’emporte là où une seule peut figurer (ADR-0085). Détermine les colonnes de liste par défaut, les cartes, les aperçus d’enregistrements enfants et la bande de mise en avant du détail. Un nom qui n’est pas un champ de cet objet est refusé à la publication." + }, + relatedLists: { + label: "Listes associées", + helpText: "Ordre métier des listes associées de cet objet sur sa page d’enregistrement, de la plus importante à la moins importante. Chaque entrée est un nom d’objet enfant (par ex. task), ou object.field lorsque cet enfant a plusieurs relations avec cet objet. Les listes indiquées viennent en premier ; les autres suivent dans l’ordre dérivé. Un nom qui ne correspond à aucune relation pointant vers cet objet ne place rien : l’enregistrement ou la validation de l’objet le refuse donc." + }, + searchableFields: { + label: "Champs de recherche", + helpText: "Noms des champs auxquels correspond la requête $search (ADR-0061) : la valeur par défaut pour le sélecteur d’enregistrements, la recherche rapide dans les listes et la recherche globale ; une vue peut la restreindre. Si elle n’est pas définie, la recherche utilise le champ de nom/titre ainsi que les champs de texte court. Chaque entrée doit désigner un champ stocké de cet objet — un nom inconnu ou un champ de formule virtuel est refusé à la publication." + }, + fieldGroups: { + label: "Groupes de champs", + helpText: "Sections ordonnées qui regroupent les champs de cet objet sur le formulaire de saisie et la page de détail de l’enregistrement (ADR-0085) ; l’ordre du tableau est l’ordre d’affichage. Un champ rejoint un groupe en indiquant sa clé dans son propre paramètre de groupe. Les champs sans groupe suivent les groupes, et un groupe qu’aucun champ ne rejoint n’est pas dessiné." + }, + "fieldGroups.key": { + label: "Clé", + helpText: "Clé technique en snake_case, unique au sein de cet objet — le schéma refuse tout autre format. Les champs rejoignent le groupe en indiquant cette clé : la renommer les laisse donc sans groupe." + }, + "fieldGroups.label": { + label: "Libellé", + helpText: "Texte de l’en-tête de la section du groupe." + }, + "fieldGroups.icon": { + label: "Icône", + helpText: "Nom d’icône Lucide affiché à côté de l’en-tête sur la page de détail de l’enregistrement (par ex. « banknote »). Le formulaire de saisie ne l’affiche pas." + }, + "fieldGroups.description": { + label: "Description du groupe", + helpText: "Texte affiché sous l’en-tête, sur le formulaire de saisie et la page de détail de l’enregistrement." + }, + "fieldGroups.relatedList": { + label: "Liste associée", + helpText: "Fait de cette entrée le tableau d’une liste associée au lieu d’une section de champs : indiquez l’objet enfant (ou object.field) dont la liste est dessinée à cette position dans la colonne de détails de l’enregistrement. Un tel groupe ne contient aucun champ ; un champ qui le désigne comme groupe est refusé, de même qu’un nom qui ne correspond à aucune relation pointant vers cet objet. La page de l’enregistrement ne dessine pas encore cet emplacement." + }, + "fieldGroups.collapse": { + label: "Réduire", + helpText: "Indique si la section peut être réduite, sur le formulaire de saisie et la page de détail de l’enregistrement. Non défini : non." + }, + "fieldGroups.visibleWhen": { + label: "Visible si", + helpText: "Prédicat CEL sur l’enregistrement (par ex. record.type == 'invoice') — le formulaire de saisie affiche l’ensemble du groupe, en-tête compris, uniquement tant qu’il est TRUE." + }, + "fieldGroups.requiresFeature": { + label: "Fonctionnalité requise", + helpText: "Fonctionnalité du déploiement dont le groupe a besoin (par ex. multiOrgEnabled) : là où elle est désactivée, le formulaire de saisie et la page de détail de l’enregistrement masquent l’ensemble du groupe. Combiné à Visible si, le groupe ne s’affiche que lorsque les deux conditions sont remplies. Les champs restent lisibles via l’API." + }, + fields: { + label: "Champs", + helpText: "Ajoutez les colonnes que cet objet stockera" + }, + "fields.label": { + label: "Libellé", + helpText: "Libellé d’affichage" + }, + "fields.type": { + label: "Type", + helpText: "Type de champ" + }, + "fields.description": { + label: "Description", + helpText: "Documentation pour les développeurs de cette colonne" + }, + "fields.required": { + label: "Obligatoire", + helpText: "Doit être renseigné sur chaque enregistrement" + }, + "fields.unique": { + label: "Unique", + helpText: "Interdire les valeurs en double" + }, + "fields.readonly": { + label: "Lecture seule", + helpText: "Visible mais jamais modifiable par l’utilisateur" + }, + "fields.hidden": { + label: "Masqué", + helpText: "Masqué dans l’interface par défaut" + }, + "fields.maskingRule": { + label: "Règle de masquage", + helpText: "Masquage partiel : préréglage ('phone', 'id_card', 'bank_account', 'email', 'name') ou {\"keepHead\": n, \"keepTail\": m}. Masqué sauf si l’appelant détient les requiredPermissions de ce champ" + }, + "fields.searchable": { + label: "Recherchable", + helpText: "Inclure dans la recherche plein texte" + }, + "fields.sortable": { + label: "Triable", + helpText: "Autoriser le tri sur cette colonne" + }, + "fields.defaultValue": { + label: "Valeur par défaut", + helpText: "Valeur par défaut des nouveaux enregistrements (littéral JSON)" + }, + "fields.placeholder": { + label: "Espace réservé", + helpText: "Texte d’indication affiché dans le champ de saisie vide ; disparaît dès qu’une valeur est saisie" + }, + "fields.maxLength": { + label: "Longueur maximale", + helpText: "Nombre maximal de caractères" + }, + "fields.minLength": { + label: "Longueur minimale", + helpText: "Nombre minimal de caractères" + }, + "fields.valueDomain": { + label: "Domaine de valeurs", + helpText: "Norme à laquelle doit appartenir la valeur écrite ; une écriture portant une valeur hors du domaine est refusée" + }, + "fields.rows": { + label: "Lignes", + helpText: "Hauteur de l’éditeur en ligne (lignes de texte)" + }, + "fields.min": { + label: "Min", + helpText: "Valeur minimale" + }, + "fields.max": { + label: "Max", + helpText: "Valeur maximale" + }, + "fields.precision": { + label: "Précision", + helpText: "Nombre total de chiffres" + }, + "fields.scale": { + label: "Échelle", + helpText: "Nombre de décimales" + }, + "fields.options": { + label: "Options", + helpText: "Choix disponibles" + }, + "fields.options.label": { + label: "Libellé" + }, + "fields.options.value": { + label: "Valeur" + }, + "fields.options.color": { + label: "Couleur" + }, + "fields.options.description": { + label: "Description de l’option" + }, + "fields.options.kind": { + label: "Nature", + helpText: "Catégorie du cycle de vie (sélection à valeur unique / bouton radio uniquement) : une étape du parcours, la fin réussie ou la fin non réussie. Obligatoire sur chaque option du champ d’étape." + }, + "fields.reference": { + label: "Référence", + helpText: "Nom de l’objet cible. Pour un champ arborescent, il est facultatif et, s’il est indiqué, doit être cet objet (une arborescence est une hiérarchie au sein de son propre objet — liez un autre objet avec une recherche)" + }, + "fields.lookupFilters": { + label: "Filtres de recherche", + helpText: "Règles de filtrage appliquées au sélecteur ({field, operator, value})" + }, + "fields.deleteBehavior": { + label: "Comportement à la suppression", + helpText: "Ce qui se passe lorsque l’enregistrement référencé est supprimé" + }, + "fields.multiple": { + label: "Multiple", + helpText: "Autoriser la sélection de plusieurs enregistrements" + }, + "fields.expression": { + label: "Expression", + helpText: "Expression CEL à partir de laquelle la valeur du champ est calculée. Sur un champ de formule : calculée à la lecture, sans colonne stockée. Sur tout autre champ : calculée par le moteur à partir de la ligne que chaque écriture stocke, puis stockée, de sorte qu’une valeur envoyée par le client n’est jamais conservée." + }, + "fields.returnType": { + label: "Type de retour", + helpText: "Type de résultat des formules" + }, + "fields.summaryOperations": { + label: "Opérations de synthèse", + helpText: "Agrégation : quel objet enfant, quel champ, quelle agrégation" + }, + "fields.summaryOperations.object": { + label: "Objet", + helpText: "Nom de l’objet enfant source" + }, + "fields.summaryOperations.field": { + label: "Champ", + helpText: "Champ de l’objet enfant à agréger (ignoré pour count)" + }, + "fields.summaryOperations.function": { + label: "Fonction", + helpText: "Fonction d’agrégation" + }, + "fields.autonumberFormat": { + label: "Format de numérotation automatique", + helpText: "par ex. « INV-{0000} » ; les jetons de date {YYYY}/{MM}/{DD} et l’interpolation {field_name} sont pris en charge" + }, + "fields.language": { + label: "Langue", + helpText: "Langage de l’éditeur (par ex. sql, javascript)" + }, + "fields.trackHistory": { + label: "Suivre l’historique", + helpText: "Résumer ce champ dans la chronologie d’activité de l’enregistrement" + }, + "fields.visibleWhen": { + label: "Visible si", + helpText: "Prédicat CEL — le champ n’est affiché que lorsqu’il est TRUE" + }, + "fields.readonlyWhen": { + label: "Lecture seule si", + helpText: "Prédicat CEL — le champ est en lecture seule lorsqu’il est TRUE (appliqué côté serveur)" + }, + "fields.requiredWhen": { + label: "Obligatoire si", + helpText: "Prédicat CEL — le champ est obligatoire lorsqu’il est TRUE (appliqué côté serveur)" + }, + enable: { + label: "Activer", + helpText: "Activer/désactiver les fonctionnalités système" + }, + "enable.trackHistory": { + label: "Suivre l’historique" + }, + "enable.searchable": { + label: "Recherchable" + }, + "enable.apiEnabled": { + label: "API activée" + }, + "enable.files": { + label: "Fichiers" + }, + "enable.feeds": { + label: "Fils d’actualité" + }, + "enable.activities": { + label: "Activités" + }, + "enable.clone": { + label: "Cloner" + }, + "enable.approvalsVisibleToReaders": { + label: "Approbations visibles par les lecteurs", + helpText: "Les lecteurs d’un enregistrement voient ses demandes d’approbation et son historique, en lecture seule, sans action d’approbation" + }, + validations: { + label: "Validations", + helpText: "Règles de validation au niveau de l’objet — un tableau d’objets de règle, par ex. [{ \"type\": \"script\", \"name\": \"amount_positive\", \"condition\": \"amount > 0\", \"message\": \"Amount must be positive\" }]. Les tables de transition des machines à états sont également déclarées ici (ADR-0020)" + }, + activityMilestones: { + label: "Jalons d’activité", + helpText: "Entrées de chronologie déclenchées lorsqu’un champ atteint une valeur (ADR-0052 §5b.2) : lorsqu’une mise à jour fait passer le champ surveillé à cette valeur, le plugin d’audit écrit le résumé du jalon dans la chronologie d’activité de l’enregistrement au lieu de l’entrée de modification de champ. Le premier jalon qui correspond l’emporte." + }, + "activityMilestones.field": { + label: "Champ", + helpText: "Nom du champ à surveiller sur cet objet (par ex. status). Rien ne le vérifie lors de l’enregistrement ou de la publication : un nom qui n’est pas un champ de cet objet ne se déclenche jamais." + }, + "activityMilestones.value": { + label: "Valeur", + helpText: "La valeur stockée vers laquelle le champ doit passer, comparée exactement sous forme de texte — pour un champ de sélection, la valeur de l’option et non son libellé (par ex. done). Un jalon sur un champ numérique ou booléen ne se déclenche jamais." + }, + "activityMilestones.summary": { + label: "Résumé", + helpText: "Texte de la chronologie (par ex. « Affaire gagnée : {name} »). Un jeton {field_name} prend la valeur de l’enregistrement après la mise à jour, et le jeton d’un champ de recherche, maître-détail ou utilisateur affiche le titre de l’enregistrement référencé ; un jeton qui ne désigne aucun champ s’affiche vide." + }, + "activityMilestones.type": { + label: "Type de jalon", + helpText: "Type d’activité de l’entrée de chronologie : une nature intégrée comme completed, ou votre propre mot, stocké tel quel. Non défini : updated." + }, + datasource: { + label: "Source de données", + helpText: "ID de la source de données cible (par défaut : « default »)" + }, + indexes: { + label: "Index", + helpText: "Index de base de données sur la table de cet objet. Le pilote SQL crée chacun de ceux qui manquent à la table lorsqu’il la synchronise ; une synchronisation ne supprime jamais d’index." + }, + "indexes.name": { + label: "Nom", + helpText: "Nom physique de l’index. Non défini : généré à partir de la table et des colonnes (par ex. idx_task_status)." + }, + "indexes.fields": { + label: "Champs", + helpText: "Noms des colonnes de cet objet, dans l’ordre de la clé (par ex. status, owner). L’enregistrement ne les vérifie pas ; la publication et os validate refusent un nom qui n’est pas un champ de cet objet. Un champ qui n’est pas une colonne stockée (une formule, par exemple) fait ignorer l’index entier par le pilote SQL, avec une erreur dans le journal du serveur." + }, + "indexes.unique": { + label: "Index unique", + helpText: "Portée d’unicité (ADR-0120). Non défini : non unique. L’ancienne valeur nue true (qui signifie global) n’est pas proposée ; un index qui la porte la conserve jusqu’à ce que vous choisissiez une portée." + }, + ownership: { + label: "Propriété", + helpText: "Modèle de propriété des enregistrements. En l’absence de valeur, user s’applique." + }, + sharingModel: { + label: "Modèle de partage", + helpText: "Visibilité par défaut des enregistrements à l’échelle de l’organisation pour les utilisateurs internes. Un objet personnalisé qui l’omet est résolu en private à l’exécution (ADR-0090 D1)." + }, + sharing: { + label: "Partage", + helpText: "Options de partage par objet qui affinent la valeur par défaut à l’échelle de l’organisation ci-dessus ; elles ne la remplacent jamais. Non défini : la valeur par défaut de chaque option s’applique." + }, + "sharing.controlledByParent": { + label: "Contrôlé par le parent", + helpText: "Lu uniquement lorsque sharingModel vaut « controlled by parent » ; refusé avec tout autre modèle de partage." + }, + "sharing.controlledByParent.ownerMayEdit": { + label: "Le propriétaire peut modifier", + helpText: "Activé : le propriétaire d’une ligne de détail (son owner_id) peut mettre à jour cette ligne par son id avec un accès en lecture au maître ; toute autre personne, ainsi que toute insertion, suppression, mise à jour multiligne et écriture pour le compte d’autrui, nécessite toujours un accès en modification au maître. Désactivé (par défaut) : toute écriture d’un détail nécessite un accès en modification à son maître." + }, + access: { + label: "Accès", + helpText: "Position vis-à-vis des octrois génériques (ADR-0066 D2). En l’absence de valeur, public s’applique. Détermine si l’octroi d’objet '*' d’un ensemble d’autorisations couvre cet objet ; la visibilité des enregistrements entre utilisateurs relève de sharingModel." + }, + "access.default": { + label: "Par défaut", + helpText: "public : couvert par les octrois génériques '*'. private : nécessite un octroi explicite par objet, et est exempté de la sécurité au niveau des lignes générique." + }, + requiredPermissions: { + label: "Autorisations requises", + helpText: "Capacités (systemPermissions d’un ensemble d’autorisations) qu’un appelant doit détenir pour accéder à cet objet, vérifiées en plus des octrois CRUD (ADR-0066 D3). Une liste conditionne toutes les opérations ; une table {read, create, update, delete} ne conditionne que les opérations qu’elle liste. Absent ou vide : aucun contrôle de capacité." + }, + publicSharing: { + label: "Partage public", + helpText: "Politique des liens de partage : indique si les enregistrements de cet objet peuvent être publiés via un lien que toute personne qui le détient peut ouvrir, et à quelles conditions. Distinct de sharingModel, qui partage avec des utilisateurs et des équipes nommés. Non défini ou désactivé : aucun lien ne peut être créé, et aucun ne s’ouvre." + }, + "publicSharing.enabled": { + label: "Activé", + helpText: "Autoriser les liens de partage pour les enregistrements de cet objet. Vérifié à chaque utilisation : le désactiver empêche tous les liens existants de s’ouvrir, et le réactiver les sert de nouveau. Désactivé (par défaut) : rien d’autre ici ne s’applique." + }, + "publicSharing.allowedAudiences": { + label: "Audiences autorisées", + helpText: "Audiences qu’un nouveau lien peut désigner ; toute autre est refusée. Non défini : lien uniquement. Chaque audience nécessite toujours le lien lui-même : connecté nécessite en plus un utilisateur connecté, et e-mail nécessite en plus que l’adresse du destinataire figure sur la liste du lien." + }, + "publicSharing.allowedPermissions": { + label: "Autorisations permises", + helpText: "Niveaux d’autorisation qu’un nouveau lien peut accorder ; tout autre est refusé. Non défini : consultation uniquement." + }, + "publicSharing.maxExpiryDays": { + label: "Expiration maximale (jours)", + helpText: "Expiration la plus tardive qu’un nouveau lien peut demander, en jours à partir de maintenant ; une expiration plus tardive est refusée. Non défini : 365. N’impose pas d’expiration : un lien créé sans expiration n’expire jamais." + }, + "publicSharing.redactFields": { + label: "Champs masqués", + helpText: "Noms des champs de cet objet retirés de chaque enregistrement servi par un lien, quelle que soit l’audience ; l’accès propre du propriétaire n’est pas affecté. Un nom qui n’est pas un champ de cet objet est refusé à la publication." + }, + "publicSharing.eligibility": { + label: "Éligibilité", + helpText: "Prédicat CEL sur l’enregistrement (par ex. record.status == 'published') : un lien n’est créé que tant qu’il est TRUE, et un lien existant cesse de s’ouvrir dès que son enregistrement ne remplit plus la condition. Un prédicat qui ne se compile pas, ou qui échoue, entraîne le refus du lien." + }, + managedBy: { + label: "Géré par", + helpText: "Catégorie de cycle de vie : platform (CRUD utilisateur), config (créé par un administrateur), system-data (schéma défini par la plateforme avec des données modifiables par les administrateurs/utilisateurs), engine-owned (aucune écriture utilisateur), append-only (audit), better-auth (identité). Les clients d’interface en déduisent leurs possibilités CRUD : cela détermine donc ce qui est proposé à un utilisateur sur les enregistrements de cet objet." + }, + userActions: { + label: "Actions utilisateur", + helpText: "Les entrées génériques (Nouveau, Importer, Modifier, Supprimer, Exporter) que les clients d’interface proposent sur les enregistrements de cet objet, en remplaçant la valeur par défaut de managedBy entrée par entrée. Une entrée non définie conserve cette valeur par défaut : platform propose les cinq ; config et system-data toutes sauf Importer ; engine-owned, append-only et better-auth uniquement Exporter. Un interrupteur non modifié n’écrit rien : il apparaît donc désactivé même là où la valeur par défaut propose l’entrée. Sur un objet engine-owned ou append-only, activer une entrée permet aussi aux utilisateurs d’effectuer cette écriture via l’API de données. Les utilisateurs ont toujours besoin de l’autorisation correspondante." + }, + "userActions.create": { + label: "Créer", + helpText: "Le bouton Nouveau : activé l’affiche, désactivé le masque. Un objet {enabled, visibleWhen, disabledWhen} stocké se modifie clé par clé ; écrivez-en un dans la source pour conditionner le bouton à l’enregistrement concerné, évalué une fois par barre d’outils (l’enregistrement hôte sur une liste associée)." + }, + "userActions.import": { + label: "Import", + helpText: "L’entrée d’import CSV : activé l’affiche, désactivé la masque. Un objet {enabled, visibleWhen, disabledWhen} stocké se modifie clé par clé ; écrivez-en un dans la source pour conditionner l’entrée à l’enregistrement concerné, évalué une fois par barre d’outils." + }, + "userActions.edit": { + label: "Modifier", + helpText: "Modification des enregistrements existants, en ligne et dans le formulaire : activé la propose, désactivé la masque. Un objet {enabled, visibleWhen, disabledWhen} stocké se modifie clé par clé ; écrivez-en un dans la source pour conditionner chaque ligne à son propre enregistrement." + }, + "userActions.delete": { + label: "Supprimer", + helpText: "Suppression par ligne et en masse : activé la propose, désactivé la masque. Un objet {enabled, visibleWhen, disabledWhen} stocké se modifie clé par clé ; écrivez-en un dans la source pour conditionner chaque ligne à son propre enregistrement." + }, + "userActions.exportCsv": { + label: "Exporter en CSV", + helpText: "L’entrée d’export CSV. Non défini : affichée, puisque chaque catégorie managedBy propose l’export." + }, + editMode: { + label: "Mode de modification", + helpText: "Intention d’interaction de modification pour les enregistrements de cet objet. En l’absence de valeur, le moteur de rendu choisit sa propre valeur par défaut. Intention commune à tous les moteurs de rendu, et non un style." + }, + fileAccessDelegate: { + label: "Délégué d’accès aux fichiers", + helpText: "Service du noyau qui autorise le téléchargement des fichiers appartenant aux champs multimédias de cet objet, et décide si un lecteur qui ne peut pas lire sys_file voit leur nom, leur taille et leur type lors de la lecture d’un enregistrement de cet objet (interrogé une fois par enregistrement propriétaire à chacune de ces lectures), au lieu de vérifier si l’appelant peut lire la ligne propriétaire. Pour les objets dont l’accès passe par un service. Échoue en mode fermé." + }, + lifecycle: { + label: "Cycle de vie", + helpText: "Contrat de cycle de vie des données (ADR-0057) : durée de vie des lignes et récupération de l’espace. Laissez vide pour une sémantique d’enregistrement permanent. Les classes autres que record exigent au moins une politique de limitation (rétention, TTL ou rotation)." + }, + "lifecycle.class": { + label: "Classe", + helpText: "Contrat de persistance des lignes de cet objet" + }, + "lifecycle.retention": { + label: "Rétention", + helpText: "Fenêtre de rétention basée sur l’âge" + }, + "lifecycle.retention.maxAge": { + label: "Âge maximal", + helpText: "Les lignes plus anciennes que cette durée (selon created_at) sont purgées. Littéral de durée : h/d/w/y, par ex. « 30d »" + }, + "lifecycle.ttl": { + label: "TTL", + helpText: "Expiration TTL par ligne" + }, + "lifecycle.ttl.field": { + label: "Champ", + helpText: "Champ d’horodatage à partir duquel le TTL est mesuré (par ex. expires_at)" + }, + "lifecycle.ttl.expireAfter": { + label: "Expire après", + helpText: "Les lignes expirent ce délai après la valeur du champ, par ex. « 1d »" + }, + "lifecycle.storage": { + label: "Stockage", + helpText: "Rotation physique pour la télémétrie à haute fréquence (SQLite : DROP de fragment en O(1))" + }, + "lifecycle.storage.strategy": { + label: "Stratégie", + helpText: "Stratégie de stockage" + }, + "lifecycle.storage.shards": { + label: "Fragments", + helpText: "Fragments conservés ; fenêtre totale = fragments × unité" + }, + "lifecycle.storage.unit": { + label: "Unité", + helpText: "Durée couverte par un fragment" + }, + "lifecycle.archive": { + label: "Archive", + helpText: "Transfert vers le stockage à froid (classe audit). Les lignes ne sont jamais supprimées à chaud avant la réussite de la copie d’archive." + }, + "lifecycle.archive.after": { + label: "Après", + helpText: "Archiver les lignes plus anciennes que cette durée — doit être égal à retention.maxAge" + }, + "lifecycle.archive.to": { + label: "À", + helpText: "Nom de la source de données cible pour le stockage à froid" + }, + "lifecycle.archive.keep": { + label: "Conservation", + helpText: "Durée de conservation des lignes dans l’archive (vide = indéfiniment), par ex. « 7y »" + }, + "lifecycle.reclaim": { + label: "Récupération", + helpText: "Récupérer l’espace du pilote après les purges (activé par défaut pour les classes autres que record)" + } + } + }, + object_extension: { + label: "Extension d’objet", + description: "Champs et règles qu’un package ajoute à un objet défini par un autre package" + }, + field: { + label: "Champ", + sections: { + basics: { + label: "Informations de base", + description: "Identité et contraintes de base du champ." + }, + configuration: { + label: "Configuration", + description: "Paramètres propres au type de champ (les blocs visibles dépendent du type choisi)." + }, + formula: { + label: "Formule et calcul", + description: "Valeurs calculées et synthèses agrégées." + }, + advanced: { + label: "Avancé", + description: "Paramètres de base de données, d’interface, d’audit et de sécurité." + } + }, + fields: { + name: { + label: "Nom", + helpText: "Identifiant unique (snake_case, non modifiable après la création)" + }, + label: { + label: "Libellé", + helpText: "Nom affiché aux utilisateurs" + }, + type: { + label: "Type", + helpText: "Type de données de ce champ" + }, + group: { + label: "Groupe", + helpText: "Nom du groupe pour la mise en page du formulaire" + }, + description: { + label: "Description", + helpText: "Texte d’aide affiché aux utilisateurs" + }, + required: { + label: "Obligatoire", + helpText: "L’utilisateur doit fournir une valeur" + }, + unique: { + label: "Unique", + helpText: "Deux enregistrements ne peuvent pas avoir la même valeur" + }, + multiple: { + label: "Multiple", + helpText: "Autoriser plusieurs valeurs (pour sélection/recherche)" + }, + defaultValue: { + label: "Valeur par défaut", + helpText: "Valeur par défaut des nouveaux enregistrements" + }, + placeholder: { + label: "Espace réservé", + helpText: "Texte d’indication affiché dans le champ de saisie vide (disparaît dès qu’une valeur est saisie) ; utilisez inlineHelpText pour une aide toujours visible" + }, + inlineHelpText: { + label: "Texte d’aide en ligne", + helpText: "Aide toujours visible affichée sous le champ de saisie, contrairement à `placeholder`, qui disparaît dès qu’une valeur est saisie." + }, + minLength: { + label: "Longueur minimale", + helpText: "Longueur minimale en caractères" + }, + maxLength: { + label: "Longueur maximale", + helpText: "Longueur maximale en caractères" + }, + valueDomain: { + label: "Domaine de valeurs", + helpText: "Norme à laquelle doit appartenir la valeur écrite : iana_time_zone, iso_4217_currency ou iso_3166_alpha2. Une écriture portant une valeur hors du domaine est refusée" + }, + rows: { + label: "Lignes", + helpText: "Hauteur de l’éditeur en ligne, en lignes de texte" + }, + min: { + label: "Min", + helpText: "Valeur minimale" + }, + max: { + label: "Max", + helpText: "Valeur maximale" + }, + precision: { + label: "Précision", + helpText: "Nombre total de chiffres" + }, + scale: { + label: "Échelle", + helpText: "Nombre de décimales" + }, + useGrouping: { + label: "Groupement des chiffres", + helpText: "Groupement des chiffres (séparateurs de milliers) dans les valeurs affichées. Non défini : le moteur de rendu décide ; un interrupteur non modifié n’écrit rien, il apparaît donc désactivé même là où le moteur de rendu groupe les chiffres. Désactivé : jamais groupé, par ex. une année ou un ID. Activé : toujours groupé." + }, + percentDisplay: { + label: "Affichage du pourcentage", + helpText: "Comment la valeur est affichée en lecture seule, dans les cellules de liste et sur la page de l’enregistrement. Non défini : une barre de progression à côté du pourcentage. Bar : toujours la barre de progression, pour une progression. Plain : le pourcentage seul, pour un taux ou une probabilité." + }, + currencyConfig: { + label: "Configuration de la devise", + helpText: "La devise de ce champ — un champ devise, ou une formule dont le type de retour est Currency. Non défini : mode dynamique. La valeur est un simple nombre dans tous les modes." + }, + "currencyConfig.currencyMode": { + label: "Mode de devise", + helpText: "dynamic (par défaut) : le champ n’a pas de devise propre, et les montants s’affichent dans la devise par défaut du locataire (le paramètre localization.currency). fixed : le champ a une seule devise, defaultCurrency. record : chaque enregistrement a sa propre devise, stockée dans le champ désigné par currencyField." + }, + "currencyConfig.currencyField": { + label: "Champ de devise", + helpText: "Mode record uniquement, et obligatoire dans ce mode : le champ de cet objet qui contient le code ISO 4217 de chaque enregistrement — un champ texte avec valueDomain iso_4217_currency, par convention currency_code." + }, + "currencyConfig.defaultCurrency": { + label: "Devise par défaut", + helpText: "L’unique devise d’un champ en mode fixed, sous forme de code ISO 4217 à trois caractères (par ex. USD, EUR). Obligatoire en mode fixed ; rien ne la renseigne automatiquement. Non lue en mode dynamic ; refusée en mode record." + }, + step: { + label: "Pas", + helpText: "Incrément du curseur (1 par défaut). Côté rendu uniquement : le chemin d’écriture ne rejette pas une valeur hors de la grille de pas." + }, + maxSize: { + label: "Taille maximale", + helpText: "Taille de fichier maximale autorisée en OCTETS (entier positif). Appliquée côté serveur à l’écriture par rapport à la taille du fichier stocké — un fichier sans taille enregistrée ne peut pas la dépasser." + }, + accept: { + label: "Types acceptés", + helpText: "Types de fichiers autorisés au téléversement, sous forme de types MIME, de jokers type/* ou de suffixes .ext (par ex. image/*, .pdf). Proposés au sélecteur de fichiers et revérifiés côté serveur à l’écriture par rapport au fichier stocké. Non défini : tout type." + }, + dimensions: { + label: "Dimensions", + helpText: "Dimension du vecteur — un entier de 1 à 10000 (par ex. 1536 pour les embeddings OpenAI)." + }, + language: { + label: "Langue", + helpText: "Langage de l’éditeur pour la coloration syntaxique (par ex. javascript, python, sql)." + }, + autonumberFormat: { + label: "Format de numérotation automatique", + helpText: "Texte littéral plus un compteur {0000}, des jetons de date {YYYY}/{MM}/{DD}/{YYYYMMDD} dans le fuseau horaire de l’entreprise, et l’interpolation {field_name}. Le compteur repart à zéro pour chaque préfixe rendu. S’il est omis sur un champ de numérotation automatique, il vaut {0000} par défaut." + }, + referenceVia: { + label: "Référence via", + helpText: "Fait de ce champ texte la moitié identifiant d’un pointeur polymorphe : désigne le champ FRÈRE du même objet qui contient, ligne par ligne, le nom de l’objet cible (ADR-0052 §5). snake_case ; champs texte uniquement, et incompatible avec `reference`." + }, + referenceDeletedWhen: { + label: "Référence supprimée si", + helpText: "Uniquement avec referenceVia : sur les lignes où un champ de sélection frère contient l’une de ses options, l’enregistrement désigné par ce pointeur a été supprimé par la plateforme, et le pointeur est affiché comme une pierre tombale libellée par cette option. Non défini : aucune ligne n’est marquée comme supprimée." + }, + "referenceDeletedWhen.field": { + label: "Champ", + helpText: "Le champ de sélection frère de cet objet (snake_case), par ex. cancel_reason. Il doit s’agir d’une sélection à valeur unique déclarant des options en ligne." + }, + "referenceDeletedWhen.value": { + label: "Valeur", + helpText: "La valeur d’option de ce champ de sélection qui signifie que la cible a été supprimée, par ex. record_deleted. Elle doit faire partie des valeurs d’option déclarées." + }, + mailAssociation: { + label: "Associer le courrier entrant", + helpText: "Associe le courrier entrant aux enregistrements de cet objet dont la valeur de ce champ correspond à l’expéditeur ou à un destinataire du message (sans tenir compte de la casse ; l’adresse propre de la boîte aux lettres exclue). Champs e-mail uniquement. Ne prend effet que là où un service de boîte aux lettres est configuré." + }, + dueLike: { + label: "Échéance", + helpText: "Marque cette date comme une échéance : une fois passée, les vues peuvent l’afficher comme en retard (formulation relative et couleur de retard). Désactivé ou non défini : pas une échéance, et rien n’est déduit du nom du champ." + }, + settledWhen: { + label: "Réglé si", + helpText: "Prédicat CEL sur l’enregistrement (par ex. record.status == 'done') — tant qu’il est TRUE, l’échéance est réglée, et aucune formulation ni couleur de retard ne s’applique à cet enregistrement. Affichage uniquement." + }, + options: { + label: "Options", + helpText: "Options disponibles (paires libellé/valeur)" + }, + "options.label": { + label: "Libellé" + }, + "options.value": { + label: "Valeur" + }, + "options.description": { + label: "Description de l’option" + }, + "options.color": { + label: "Couleur" + }, + "options.default": { + label: "Par défaut" + }, + "options.visibleWhen": { + label: "Visible si" + }, + "options.kind": { + label: "Nature" + }, + reference: { + label: "Référence", + helpText: "Nom de l’objet référencé" + }, + referenceNameVisible: { + label: "Afficher le nom si illisible", + helpText: "Permet à un lecteur de cet enregistrement de voir le nom de l’enregistrement référencé, en texte brut sans lien, même s’il ne peut pas ouvrir cet enregistrement. Rien d’autre n’en est affiché, et une règle au niveau du champ sur le nom continue de s’appliquer (le nom s’affiche alors sous la forme Record # suivi de l’id). Non défini : une telle référence s’affiche comme un enregistrement restreint." + }, + deleteBehavior: { + label: "Comportement à la suppression", + helpText: "Ce qui se passe lorsque l’enregistrement référencé est supprimé" + }, + relatedListFilter: { + label: "Filtre de la liste associée", + helpText: "Filtre par défaut de la liste associée de cette relation sur la page de détail du parent — combiné par ET avec la correspondance à l’enregistrement parent, et le badge de l’onglet compte le même ensemble" + }, + displayField: { + label: "Champ d’affichage", + helpText: "Champ affiché comme libellé de chaque candidat dans le sélecteur. S’il est omis, le champ de titre propre à l’objet référencé est utilisé." + }, + descriptionField: { + label: "Champ de description", + helpText: "Champ secondaire affiché sous le libellé dans la fenêtre de sélection rapide." + }, + allowCreate: { + label: "Autoriser la création", + helpText: "Permet à l’utilisateur de créer un enregistrement à partir du texte saisi lorsque le sélecteur ne trouve aucune correspondance. Idéal pour les objets dont le seul champ obligatoire est le champ d’affichage." + }, + lookupPageSize: { + label: "Taille de page de la recherche", + helpText: "Lignes par page dans la boîte de dialogue de sélection d’enregistrements — un entier positif ; 10 par défaut." + }, + lookupColumns: { + label: "Colonnes de la recherche", + helpText: "Colonnes du tableau de sélection d’enregistrements : noms de champs de l’objet référencé, ou entrées {field, label, width, type} (par ex. [\"name\", {\"field\": \"status\", \"label\": \"Stage\"}]). Non défini : dérivées de l’objet référencé." + }, + lookupFilters: { + label: "Filtres de recherche", + helpText: "Filtre de base sur les candidats du sélecteur, sous forme de règles {field, operator, value} sur l’objet référencé — operator parmi eq, ne, gt, lt, gte, lte, contains, in, notIn (par ex. [{\"field\": \"status\", \"operator\": \"eq\", \"value\": \"active\"}]). Appliqué à toutes les surfaces de sélection, combiné par ET avec tout filtre dependsOn." + }, + dependsOn: { + label: "Dépend de", + helpText: "Champs du même enregistrement dont dépendent les choix de ce champ : le formulaire bloque ce champ tant que chacun n’est pas renseigné, et le réévalue lorsque l’un d’eux change. Une recherche filtre ses candidats en fonction d’eux — un nom filtre le champ de même nom de l’objet référencé, {field, param} en désigne un autre. Sur un champ à options, indiquez les noms des champs parents ; la règle par option se trouve dans le visibleWhen de chaque option." + }, + relatedListTitle: { + label: "Titre de la liste associée", + helpText: "Titre de la liste associée de cette relation sur la page de détail du parent." + }, + relatedListColumns: { + label: "Colonnes de la liste associée", + helpText: "Colonnes de la liste associée de cette relation sur la page de détail du parent, sous forme de noms de champs de cet objet (l’enfant), par ex. name, status. Non défini : dérivées de l’objet enfant. Noms uniquement — les libellés, types de cellule et formats proviennent des définitions de champs de l’enfant." + }, + inlineTitle: { + label: "Titre en ligne", + helpText: "Titre de la grille maître-détail en ligne sur l’enregistrement parent." + }, + inlineColumns: { + label: "Colonnes en ligne", + helpText: "Colonnes de la grille en ligne sur le formulaire du parent, dans l’ordre d’affichage ; utilisées uniquement lorsque ce champ définit inlineEdit, qui s’écrit dans la source. Non défini : dérivées des champs modifiables de cet objet, et au-delà de six, les suivantes démarrent dans le sélecteur de colonnes de la grille. Une entrée qui ne désigne qu’un champ reprend le type, les options et les règles de ce champ ; les autres clés de colonne, à commencer par type, s’écrivent dans la source." + }, + "inlineColumns.name": { + label: "Nom", + helpText: "Champ de cet objet (l’enfant) que la colonne affiche et modifie (par ex. quantity). Rien ne le vérifie lors de l’enregistrement ou de la publication : un nom qui n’est pas un champ de cet objet produit une simple colonne de texte." + }, + "inlineColumns.label": { + label: "Libellé", + helpText: "En-tête de colonne. Non défini : le libellé propre au champ." + }, + "inlineColumns.width": { + label: "Largeur", + helpText: "Largeur de colonne fixe en pixels. Non défini : dimensionnée selon le type de cellule, les colonnes de texte s’ajustant et les colonnes numériques, de date et de sélection restant étroites." + }, + "inlineColumns.defaultHidden": { + label: "Masquée par défaut", + helpText: "Placer la colonne au départ dans le sélecteur de colonnes de la grille plutôt qu’à l’écran ; l’utilisateur peut l’afficher. Une colonne dont le champ est obligatoire est toujours affichée." + }, + inlineAmountField: { + label: "Champ de montant en ligne", + helpText: "Champ numérique enfant additionné pour le total de la grille en ligne." + }, + expression: { + label: "Expression", + helpText: "Expression CEL à partir de laquelle la valeur du champ est calculée. Sur un champ de formule : calculée à la lecture, sans colonne stockée. Sur tout autre champ : calculée par le moteur à partir de la ligne que chaque écriture stocke, puis stockée, de sorte qu’une valeur envoyée par le client n’est jamais conservée." + }, + returnType: { + label: "Type de retour", + helpText: "Type de valeur déclaré de la formule, renseigné à partir du type CEL déduit. Les consommateurs le lisent au lieu de réanalyser l’expression. Currency : un montant d’argent, dans la devise déclarée par la Configuration de la devise de ce champ (dynamic si non définie)." + }, + summaryOperations: { + label: "Opérations de synthèse", + helpText: "Configuration de la synthèse agrégée (pour les relations parent-enfant)" + }, + "summaryOperations.object": { + label: "Objet", + helpText: "Objet enfant à agréger" + }, + "summaryOperations.function": { + label: "Fonction", + helpText: "Fonction d’agrégation" + }, + "summaryOperations.field": { + label: "Champ", + helpText: "Champ enfant à agréger (ignoré pour count)" + }, + "summaryOperations.relationshipField": { + label: "Champ de relation", + helpText: "Clé étrangère de l’enfant vers ce parent (détectée automatiquement si omise)" + }, + "summaryOperations.filter": { + label: "Filtre", + helpText: "Seules les lignes enfants correspondant à ce prédicat sont agrégées (par ex. status == received)" + }, + storage: { + label: "Stockage", + helpText: "Contraintes de stockage physique (ADR-0113) : le DDL que le contrat d’écriture n’implique délibérément pas. Non défini : aucune n’est demandée." + }, + "storage.notNull": { + label: "Non nul", + helpText: "Émet un NOT NULL de base de données sur la colonne. Non défini, la colonne reste nullable même avec required — le moteur applique required à l’écriture. Le déclarer sur des lignes nulles existantes est une migration destructive conditionnée par la dérive du schéma (remplissez d’abord). Refusé avec requiredWhen." + }, + readonly: { + label: "Lecture seule", + helpText: "Le champ est en lecture seule dans les formulaires" + }, + hidden: { + label: "Masqué", + helpText: "Masquer le champ dans les vues d’interface par défaut" + }, + searchable: { + label: "Recherchable", + helpText: "Inclure dans les résultats de la recherche globale" + }, + sortable: { + label: "Triable", + helpText: "Autoriser le tri des listes par ce champ" + }, + visibleWhen: { + label: "Visible si", + helpText: "Prédicat CEL sur l’enregistrement (par ex. record.type == 'invoice') — le formulaire n’affiche ce champ que tant qu’il est TRUE." + }, + readonlyWhen: { + label: "Lecture seule si", + helpText: "Prédicat CEL sur l’enregistrement (par ex. record.status == 'paid') — le champ est en lecture seule tant qu’il est TRUE, ce qui est appliqué côté serveur : la modification d’un champ verrouillé par une mise à jour est ignorée et la valeur stockée est conservée. Lit les colonnes propres de l’enregistrement ; objectstack validate refuse une lecture via un champ de référence." + }, + requiredWhen: { + label: "Obligatoire si", + helpText: "Prédicat CEL sur l’enregistrement — le champ est obligatoire tant qu’il est TRUE, ce qui est appliqué côté serveur comme un contrôle de transition : une écriture qui laisse la valeur manquante est refusée lorsque l’enregistrement était conforme avant elle, de sorte qu’une ligne à laquelle la valeur manque déjà continue d’accepter les modifications sans rapport. Pour une règle que chaque écriture doit respecter, utilisez une règle de script de validations. Refusé avec storage.notNull." + }, + maskingRule: { + label: "Règle de masquage", + helpText: "Masquage partiel : préréglage ('phone', 'id_card', 'bank_account', 'email', 'name') ou {\"keepHead\": n, \"keepTail\": m}. Masqué pour les appelants qui ne détiennent pas les requiredPermissions de ce champ" + }, + requiredPermissions: { + label: "Autorisations requises", + helpText: "Capacités (systemPermissions d’un ensemble d’autorisations) qu’un appelant doit détenir — toutes celles listées — pour lire ou modifier ce champ (ADR-0066 D3). Sans elles, la valeur est masquée à la lecture (partiellement, si une maskingRule est définie) et les modifications sont refusées. Vide ou non défini : aucun contrôle de capacité." + }, + internal: { + label: "Interne", + helpText: "Ne jamais renvoyer la valeur de ce champ par le chemin de données générique : le moteur omet la clé des résultats find/findOne et des corps de réponse de création et de mise à jour, avec la projection par défaut comme lorsqu’un client nomme le champ dans ?select=. Le stockage, le filtrage et l’indexation ne sont pas affectés." + }, + trackHistory: { + label: "Suivre l’historique", + helpText: "Afficher les modifications de valeur de ce champ sous forme d’entrées dans la chronologie d’activité de l’enregistrement (ADR-0052 §5b). Activation par champ." + }, + widget: { + label: "Widget", + helpText: "Remplacement du widget de formulaire — désigne un composant de champ enregistré, recherché sous la forme `field:` suivi de ce nom, pour afficher le champ à la place du rendu par défaut du type. Un nom non enregistré se replie sur le rendu du type." + }, + ackPlaintextMasking: { + label: "Confirmer le stockage en clair", + helpText: "Confirme que le contrat de ce champ de mot de passe générique — stocké en clair, masqué à la lecture — est voulu, ce qui fait taire l’avertissement à la création (ADR-0100). Sans effet sur tout autre type." + } + } + }, + hook: { + label: "Hook", + sections: { + identity: { + label: "Identité", + description: "Ce qu’est ce hook et quand il se déclenche." + }, + body: { + label: "Corps", + description: "Expression en ligne ou JavaScript en bac à sable exécuté lorsque le hook se déclenche." + }, + legacy_handler: { + label: "Gestionnaire hérité", + description: "Référence à un nom de fonction — obsolète au profit de body." + }, + execution: { + label: "Exécution" + } + }, + fields: { + name: { + label: "Nom", + helpText: "Identifiant snake_case (non modifiable après la création)" + }, + label: { + label: "Libellé" + }, + description: { + label: "Description" + }, + object: { + label: "Objet", + helpText: "Nom de l’objet cible (ou « * » pour global)" + }, + events: { + label: "Événements", + helpText: "Événements du cycle de vie (par ex. beforeInsert, afterUpdate)" + }, + priority: { + label: "Priorité", + helpText: "Les nombres les plus bas s’exécutent en premier" + }, + body: { + label: "Corps", + helpText: "Soit une expression L1, soit un corps JS L2 en bac à sable" + }, + "body.language": { + label: "Langue", + helpText: "expression = formule pure ; js = JavaScript en bac à sable" + }, + "body.source": { + label: "Source", + helpText: "Source du corps de la fonction — pas d’imports au niveau supérieur" + }, + "body.capabilities": { + label: "Capacités", + helpText: "API ctx autorisées (api.read, api.write, crypto.uuid, log, …)" + }, + "body.timeoutMs": { + label: "Délai d’expiration (ms)", + helpText: "Délai d’expiration par appel (ms)" + }, + "body.memoryMb": { + label: "Mémoire (Mo)", + helpText: "Plafond de mémoire par appel (Mo, 256 max.)" + }, + refusals: { + label: "Refus", + helpText: "Catalogue des refus, indexé par le code SCREAMING_SNAKE que lève le corps (par ex. CLEAN_VERSION_REQUIRED). L’appelant lit le message déclaré, traduit dans sa langue, avec le statut déclaré. Corps JavaScript (L2) uniquement" + }, + "refusals.message": { + label: "Message", + helpText: "La phrase que lit l’utilisateur lorsque le corps lève ce code — texte brut, sans espaces réservés" + }, + "refusals.status": { + label: "Statut", + helpText: "Statut HTTP du refus (400–499). Absent : 400" + }, + handler: { + label: "Gestionnaire", + helpText: "Nom de la fonction de gestion (obsolète — préférez `body`)" + }, + async: { + label: "Asynchrone", + helpText: "Exécuter en arrière-plan, sans bloquer la transaction" + }, + onError: { + label: "En cas d’erreur" + }, + timeoutMs: { + label: "Délai d’expiration (ms)", + helpText: "Interrompre le hook après N millisecondes" + }, + runAs: { + label: "Exécuter en tant que", + helpText: "Identité des opérations de données ctx.api : hériter de l’écriture déclenchante (par défaut), system (privilèges élevés) ou user (l’utilisateur déclencheur)" + }, + condition: { + label: "Condition", + helpText: "Prédicat CEL — le hook ne s’exécute que lorsqu’il est TRUE" + }, + retryPolicy: { + label: "Politique de nouvelle tentative", + helpText: "Nouvelle tentative en cas d’échec — surtout utile pour les hooks asynchrones" + }, + "retryPolicy.maxRetries": { + label: "Nombre maximal de nouvelles tentatives", + helpText: "Nombre maximal de nouvelles tentatives" + }, + "retryPolicy.backoffMs": { + label: "Temporisation (ms)", + helpText: "Délai entre les nouvelles tentatives (ms)" + } + } + }, + seed: { + label: "Données d’amorçage", + description: "Données de fixture / d’initialisation appliquées à la publication" + }, + picklist: { + label: "Liste de sélection", + description: "Liste d’options partagée que les champs de sélection référencent par son nom" + }, + mapping: { + label: "Mappage d’import", + description: "Mappage de champs d’import/export réutilisable (renommage + transformations), référencé par son nom lors de l’import" + }, + view: { + label: "Vue", + sections: { + basics: { + label: "Informations de base", + description: "Identité et surface principale." + }, + columns_filters: { + label: "Colonnes et filtres", + description: "Les lignes affichées et la manière dont les utilisateurs les filtrent." + }, + table_options: { + label: "Options du tableau", + description: "Options d’affichage propres à la grille." + }, + pagination: { + label: "Pagination", + description: "Taille de page et options de taille de page — tous les types de vue les acceptent, pas seulement les grilles." + }, + kanban: { + label: "Kanban", + description: "Configuration propre au tableau Kanban." + }, + calendar: { + label: "Calendrier", + description: "Configuration propre au calendrier." + }, + gantt: { + label: "Gantt", + description: "Configuration propre au diagramme de Gantt." + }, + gallery: { + label: "Galerie", + description: "Configuration propre à la galerie." + }, + timeline: { + label: "Chronologie", + description: "Configuration propre à la chronologie." + }, + chart: { + label: "Graphique", + description: "Configuration propre au graphique." + }, + end_user_controls: { + label: "Contrôles de l’utilisateur final", + description: "Ce que les utilisateurs finaux peuvent faire sur cette vue — filtres rapides, onglets de filtre, changement de visualisation." + }, + navigation_sharing: { + label: "Navigation et partage", + description: "Où cette vue apparaît et qui peut la voir." + } + }, + fields: { + name: { + label: "Nom", + helpText: "snake_case, unique par environnement" + }, + label: { + label: "Libellé" + }, + description: { + label: "Description" + }, + type: { + label: "Type", + helpText: "Surface de vue principale" + }, + data: { + label: "Données", + helpText: "Source de données — par ex. {\"provider\":\"object\",\"object\":\"task\"}" + }, + columns: { + label: "Colonnes", + helpText: "Colonnes à afficher (noms de champs de l’objet sélectionné)" + }, + "columns.field": { + label: "Champ" + }, + "columns.label": { + label: "Libellé" + }, + "columns.width": { + label: "Largeur (px)" + }, + "columns.align": { + label: "Alignement" + }, + "columns.hidden": { + label: "Masquée" + }, + "columns.sortable": { + label: "Triable" + }, + "columns.resizable": { + label: "Redimensionnable" + }, + "columns.wrap": { + label: "Renvoi à la ligne" + }, + "columns.type": { + label: "Type de rendu" + }, + "columns.pinned": { + label: "Épinglée" + }, + "columns.summary": { + label: "Résumé" + }, + "columns.prefix": { + label: "Préfixe" + }, + "columns.link": { + label: "Lien principal" + }, + "columns.action": { + label: "Action au clic" + }, + filter: { + label: "Filtre", + helpText: "Conditions de filtrage — même générateur visuel que la barre d’outils de la liste, avec des opérateurs et des saisies de valeur adaptés au type de champ" + }, + sort: { + label: "Tri", + helpText: "Ordre de tri par défaut" + }, + "sort.field": { + label: "Champ" + }, + "sort.order": { + label: "Sens" + }, + searchableFields: { + label: "Champs de recherche", + helpText: "Noms des champs disponibles pour la recherche rapide" + }, + resizable: { + label: "Redimensionnable", + helpText: "Activer le redimensionnement des colonnes" + }, + compactToolbar: { + label: "Barre d’outils compacte", + helpText: "Regrouper Groupe/Couleur/Densité/Masquer les champs dans une seule fenêtre de paramètres de la vue" + }, + rowHeight: { + label: "Hauteur de ligne", + helpText: "Paramètre de hauteur de ligne / densité" + }, + selection: { + label: "Sélection", + helpText: "Configuration de la sélection des lignes" + }, + "selection.type": { + label: "Type", + helpText: "Mode de sélection" + }, + pagination: { + label: "Pagination", + helpText: "Configuration de la pagination" + }, + "pagination.pageSize": { + label: "Taille de page", + helpText: "Nombre d’enregistrements par page. Sur une vue sans pagination (kanban, galerie, chronologie), c’est le plafond de récupération, et le moteur de rendu doit deux choses : limiter sa récupération à ce nombre et, lorsque l’ensemble filtré est plus grand, afficher un signal de troncature visible indiquant que ce qui est à l’écran n’est pas l’ensemble complet" + }, + "pagination.pageSizeOptions": { + label: "Options de taille de page", + helpText: "Options de taille de page disponibles" + }, + kanban: { + label: "Kanban" + }, + calendar: { + label: "Calendrier" + }, + gantt: { + label: "Gantt" + }, + gallery: { + label: "Galerie" + }, + timeline: { + label: "Chronologie" + }, + chart: { + label: "Graphique" + }, + userFilters: { + label: "Filtres utilisateur", + helpText: "Barre de filtres rapides : style d’élément (dropdown / tabs / toggle) + champs exposés ou préréglages d’onglets" + }, + "userFilters.element": { + label: "Élément", + helpText: "Style du contrôle de filtre sur les vues d’objet : \"dropdown\" (jetons de valeur par champ). \"toggle\" est obsolète. \"tabs\" est réservé aux pages — utilisez `listViews` pour les préréglages nommés." + }, + "userFilters.fields": { + label: "Champs", + helpText: "Champs exposés comme filtres rapides (éléments dropdown/toggle)" + }, + "userFilters.fields.field": { + label: "Champ" + }, + "userFilters.fields.label": { + label: "Libellé" + }, + "userFilters.fields.type": { + label: "Type de filtre" + }, + "userFilters.fields.options": { + label: "Options du filtre" + }, + "userFilters.fields.showCount": { + label: "Afficher le nombre" + }, + "userFilters.fields.defaultValues": { + label: "Valeurs par défaut" + }, + appearance: { + label: "Apparence", + helpText: "allowedVisualizations : les moteurs de rendu entre lesquels les utilisateurs peuvent basculer" + }, + "appearance.showDescription": { + label: "Afficher la description", + helpText: "Afficher le texte de description de la vue" + }, + "appearance.allowedVisualizations": { + label: "Visualisations autorisées", + helpText: "Liste blanche des types de visualisation entre lesquels les utilisateurs peuvent basculer (par ex. [\"grid\", \"gallery\", \"kanban\"])" + }, + userActions: { + label: "Actions utilisateur", + helpText: "Bascules de la barre d’outils : tri / recherche / filtre / hauteur de ligne" + }, + "userActions.sort": { + label: "Tri", + helpText: "Autoriser les utilisateurs à trier les enregistrements" + }, + "userActions.search": { + label: "Recherche", + helpText: "Autoriser les utilisateurs à rechercher des enregistrements" + }, + "userActions.filter": { + label: "Filtre", + helpText: "Autoriser les utilisateurs à filtrer les enregistrements" + }, + "userActions.refresh": { + label: "Actualiser", + helpText: "Autoriser les utilisateurs à recharger les données de la vue depuis le serveur sans recharger toute la page" + }, + "userActions.rowHeight": { + label: "Hauteur de ligne", + helpText: "Autoriser les utilisateurs à changer la hauteur de ligne / la densité" + }, + "userActions.group": { + label: "Groupe", + helpText: "Autoriser les utilisateurs à modifier le regroupement des enregistrements depuis la barre d’outils. Bascule uniquement — le regroupement lui-même se configure dans le bloc `grouping` au niveau de la vue." + }, + "userActions.addRecordForm": { + label: "Formulaire d’ajout d’enregistrement", + helpText: "Ajouter des enregistrements via un formulaire plutôt qu’en ligne" + }, + "userActions.editInline": { + label: "Modification en ligne", + helpText: "Autoriser les utilisateurs à modifier les enregistrements en ligne. Activé par défaut : un utilisateur autorisé à mettre à jour l’objet modifie une cellule sur place avec le widget adapté au type du champ (le même contrôle que celui du formulaire), sous le contrôle d’autorisation qui décide déjà si l’objet est modifiable. Déclarez `editInline: false` pour rendre la liste non modifiable sur place." + }, + "userActions.hideFields": { + label: "Masquer les champs", + helpText: "Autoriser les utilisateurs à masquer/afficher des champs depuis la barre d’outils (la fonctionnalité derrière la liste `hiddenFields` au niveau de la vue). Bascule booléenne — distincte du `hideFields` du composant de détails d’enregistrement, qui est un tableau de noms de champs à omettre. Activé par défaut : un lecteur masque et affiche les colonnes pour lui-même ; déclarez `hideFields: false` pour retirer le contrôle." + }, + "userActions.rowColor": { + label: "Couleur de ligne", + helpText: "Autoriser les utilisateurs à configurer la coloration des lignes depuis la barre d’outils. Bascule booléenne — les règles de couleur elles-mêmes se trouvent dans le bloc `rowColor` au niveau de la vue. Désactivé par défaut : la coloration des lignes est facultative." + }, + "userActions.buttons": { + label: "Boutons", + helpText: "ID des boutons d’action personnalisés à afficher dans la barre d’outils" + }, + addRecord: { + label: "Ajouter un enregistrement", + helpText: "Configuration du point d’entrée d’ajout d’enregistrement" + }, + "addRecord.enabled": { + label: "Activé", + helpText: "Afficher le point d’entrée d’ajout d’enregistrement — OBLIGATOIRE : `true` dessine le bouton Ajouter, `false` le déclare désactivé" + }, + "addRecord.position": { + label: "Position", + helpText: "Position du bouton d’ajout d’enregistrement : `top` (la barre d’outils, par défaut), `bottom` (sous la liste) ou `both`" + }, + showRecordCount: { + label: "Afficher le nombre d’enregistrements", + helpText: "Afficher le nombre d’enregistrements en bas de la liste" + }, + navigation: { + label: "Navigation", + helpText: "Configuration de la navigation au clic sur un élément (tiroir par défaut, page, new_window ou none)" + }, + "navigation.mode": { + label: "Mode" + }, + "navigation.preventNavigation": { + label: "Empêcher la navigation", + helpText: "Désactiver entièrement la navigation standard" + }, + "navigation.openNewTab": { + label: "Ouvrir dans un nouvel onglet", + helpText: "Forcer l’ouverture dans un nouvel onglet (s’applique au mode page)" + }, + "navigation.size": { + label: "Taille", + helpText: "Catégorie de taille de la superposition pour le détail en tiroir : 'auto' (par défaut — le moteur de rendu la déduit du nombre de champs et de la fenêtre d’affichage ; l’IA n’écrit rien) ou un remplacement grossier sm/md/lg/xl/full. Préférez ceci à la `width` en pixels ; le mode page l’ignore." + }, + "navigation.width": { + label: "Largeur", + helpText: "[OBSOLÈTE → size] Largeur en pixels/pourcentage du tiroir (par ex. « 600px »). Une largeur en pixels ne peut pas être choisie lors de la création sans connaître la fenêtre d’affichage du client — utilisez la catégorie `size`." + }, + sharing: { + label: "Partage", + helpText: "Configuration du partage de la vue : qui a verrouillé la configuration de la vue" + }, + "sharing.lockedBy": { + label: "Verrouillé par", + helpText: "Utilisateur qui a verrouillé la configuration de la vue" + } + } + }, + page: { + label: "Page", + sections: { + basics: { + label: "Informations de base", + description: "Identité et modèle de la page." + }, + data_context: { + label: "Contexte de données", + description: "Liaison à l’enregistrement et état local de la page." + }, + layout: { + label: "Disposition", + description: "Régions de la page et composants placés à l’intérieur." + }, + interface: { + label: "Interface (pages de liste)", + description: "Mode interface (parité avec Airtable) : la page définit directement sa propre surface de données — colonnes, filtres, visualisations et barre d’outils — sans hériter d’une vue distincte." + }, + advanced: { + label: "Avancé", + description: "Activation, audience et accessibilité." + } + }, + fields: { + name: { + label: "Nom", + helpText: "Identifiant unique (snake_case)" + }, + label: { + label: "Libellé", + helpText: "Titre de la page affiché aux utilisateurs" + }, + icon: { + label: "Icône", + helpText: "Icône du menu de navigation" + }, + type: { + label: "Type", + helpText: "Nature de la page. « Liste / Interface » lie une vue source à une surface organisée — son apparence (grille / kanban / calendrier / …) est une visualisation définie sous Interface, et non un type de page." + }, + template: { + label: "Modèle", + helpText: "Modèle de disposition (par ex. « header-sidebar-main »)" + }, + description: { + label: "Description", + helpText: "Description de la page pour la navigation" + }, + source: { + label: "Source", + helpText: "Texte source de la page. Pour la nature 'html', il s’agit de JSX restreint compilé en arbre de composants lors de l’enregistrement — analysé, jamais exécuté. Pour la nature 'react', il s’agit de véritable React exécuté au rendu par l’environnement d’exécution de confiance." + }, + object: { + label: "Objet", + helpText: "Objet lié (pour les pages d’enregistrement)" + }, + variables: { + label: "Variables", + helpText: "Variables d’état local de la page" + }, + "variables.name": { + label: "Nom", + helpText: "Nom de la variable — exposé aux expressions sous la forme `page.`" + }, + "variables.type": { + label: "Type de variable", + helpText: "Type de valeur" + }, + "variables.defaultValue": { + label: "Valeur par défaut", + helpText: "Valeur initiale (par défaut, une valeur vide adaptée au type)" + }, + "variables.source": { + label: "Écrite par", + helpText: "Composant (par son id) qui écrit cette variable — par ex. un element:record_picker" + }, + regions: { + label: "Régions", + helpText: "Régions de disposition (header, main, sidebar, footer) avec leurs composants" + }, + "regions.name": { + label: "Région" + }, + "regions.width": { + label: "Largeur" + }, + "regions.components": { + label: "Composants" + }, + interfaceConfig: { + label: "Configuration de l’interface", + helpText: "La page EST la vue : source choisit l’objet, columns/filterBy sont définis directement ici ; appearance.allowedVisualizations établit la liste blanche des moteurs de rendu (une seule entrée = verrouillé) ; userActions active ou désactive les éléments de la barre d’outils." + }, + "interfaceConfig.source": { + label: "Source", + helpText: "Objet lu par cette page" + }, + "interfaceConfig.columns": { + label: "Colonnes", + helpText: "Colonnes à afficher — définies directement sur la page (vide = tous les champs de l’objet)" + }, + "interfaceConfig.filterBy": { + label: "Filtrer par", + helpText: "Filtre de base toujours actif pour la page — même générateur visuel que la barre d’outils de la liste." + }, + "interfaceConfig.sort": { + label: "Tri", + helpText: "Ordre de tri par défaut de la page, défini directement sur la page." + }, + "interfaceConfig.sort.field": { + label: "Champ" + }, + "interfaceConfig.sort.order": { + label: "Sens" + }, + "interfaceConfig.levels": { + label: "Niveaux", + helpText: "Niveaux hiérarchiques à afficher (sources arborescentes)" + }, + "interfaceConfig.appearance": { + label: "Apparence", + helpText: "Visualisations autorisées (Grille / Kanban / Calendrier / …) et visibilité de la description" + }, + "interfaceConfig.userFilters": { + label: "Filtres utilisateur", + helpText: "Barre de filtres de l’utilisateur final : Aucun (pas de barre) / Onglets (préréglages nommés) / Liste déroulante (par champ). Aucun supprime la configuration." + }, + "interfaceConfig.userActions": { + label: "Actions utilisateur", + helpText: "Bascules de la barre d’outils (recherche, tri, filtre, hauteur de ligne)" + }, + "interfaceConfig.addRecord": { + label: "Ajouter un enregistrement", + helpText: "Point d’entrée d’ajout d’enregistrement" + }, + "interfaceConfig.addRecord.enabled": { + label: "Activé" + }, + "interfaceConfig.addRecord.position": { + label: "Position" + }, + "interfaceConfig.emptyState": { + label: "État vide", + helpText: "Ce que la page affiche lorsque sa liste ne contient aucune ligne : un titre, un message, une icône et un lien. Défini ici, il remplace l’état vide de la vue source." + }, + "interfaceConfig.buttons": { + label: "Boutons", + helpText: "Boutons de la barre d’outils — à choisir parmi les actions de cet objet" + }, + "interfaceConfig.recordAction": { + label: "Action sur l’enregistrement", + helpText: "Comment un clic sur un enregistrement ouvre son détail" + }, + "interfaceConfig.showRecordCount": { + label: "Afficher le nombre d’enregistrements", + helpText: "Afficher la barre du nombre d’enregistrements" + }, + "interfaceConfig.allowPrinting": { + label: "Autoriser l’impression", + helpText: "Autoriser les utilisateurs à imprimer cette page" + }, + isDefault: { + label: "Par défaut", + helpText: "Ouvrir cette page d’enregistrement pour les enregistrements de son objet. Une seule page d’enregistrement par défaut par objet : une seconde est refusée." + }, + kind: { + label: "Nature", + helpText: "Mode de remplacement de la page : full ou slotted (pour les pages d’enregistrement)" + }, + aria: { + label: "Aria", + helpText: "Attributs d’accessibilité (libellés ARIA, rôles)" + } + } + }, + dashboard: { + label: "Tableau de bord", + sections: { + basics: { + label: "Informations de base", + description: "Identité et description du tableau de bord." + }, + layout: { + label: "Disposition", + description: "Dimensionnement de la grille et cadence d’actualisation." + }, + widgets: { + label: "Widgets", + description: "Cartes et graphiques placés sur la grille." + }, + filters: { + label: "Filtres", + description: "Filtres par défaut et globaux appliqués à l’ensemble des widgets." + }, + advanced: { + label: "Avancé", + description: "Réglages d’accessibilité et de performances." + } + }, + fields: { + name: { + label: "Nom", + helpText: "Identifiant unique snake_case" + }, + label: { + label: "Libellé", + helpText: "Nom d’affichage" + }, + description: { + label: "Description" + }, + columns: { + label: "Colonnes", + helpText: "Colonnes de la grille (12 par défaut)" + }, + gap: { + label: "Espacement", + helpText: "Espace entre les widgets, par pas de 0,25rem (4 = 1rem)" + }, + refreshIntervalSeconds: { + label: "Intervalle d’actualisation (secondes)", + helpText: "Actualisation automatique (secondes)" + }, + header: { + label: "En-tête", + helpText: "Configuration de l’en-tête du tableau de bord (titre, sous-titre, actions)" + }, + "header.showTitle": { + label: "Afficher le titre", + helpText: "Afficher le titre du tableau de bord dans l’en-tête" + }, + "header.showDescription": { + label: "Afficher la description", + helpText: "Afficher la description du tableau de bord dans l’en-tête" + }, + "header.actions": { + label: "Boutons d’action", + helpText: "Boutons d’action de l’en-tête" + }, + "header.actions.label": { + label: "Libellé" + }, + "header.actions.actionUrl": { + label: "URL d’action" + }, + "header.actions.actionType": { + label: "Type d’action" + }, + "header.actions.icon": { + label: "Icône" + }, + widgets: { + label: "Widgets", + helpText: "Widgets du tableau de bord avec leur position et leur taille" + }, + "widgets.id": { + label: "ID du widget" + }, + "widgets.title": { + label: "Titre" + }, + "widgets.description": { + label: "Description du widget" + }, + "widgets.type": { + label: "Type de visualisation" + }, + "widgets.chartConfig": { + label: "Configuration du graphique" + }, + "widgets.colorVariant": { + label: "Variante de couleur" + }, + "widgets.requiresObject": { + label: "Objet requis" + }, + "widgets.requiresService": { + label: "Service requis" + }, + "widgets.filter": { + label: "Filtre" + }, + "widgets.compareTo": { + label: "Comparer à" + }, + "widgets.dataset": { + label: "Jeu de données" + }, + "widgets.dimensions": { + label: "Dimensions du widget" + }, + "widgets.values": { + label: "Valeurs" + }, + "widgets.layout": { + label: "Disposition" + }, + "widgets.options": { + label: "Options du widget" + }, + "widgets.filterBindings": { + label: "Liaisons de filtres" + }, + "widgets.suppressWarnings": { + label: "Masquer les avertissements" + }, + dateRange: { + label: "Plage de dates", + helpText: "Sélecteur de plage de dates par défaut" + }, + globalFilters: { + label: "Filtres globaux", + helpText: "Filtres appliqués à tous les widgets" + }, + "globalFilters.name": { + label: "Nom" + }, + "globalFilters.field": { + label: "Champ" + }, + "globalFilters.object": { + label: "Objet" + }, + "globalFilters.label": { + label: "Libellé" + }, + "globalFilters.type": { + label: "Type de saisie" + }, + "globalFilters.options": { + label: "Options du filtre" + }, + "globalFilters.optionsFrom": { + label: "Options issues de" + }, + "globalFilters.defaultValue": { + label: "Valeur par défaut" + }, + "globalFilters.scope": { + label: "Portée" + }, + "globalFilters.targetWidgets": { + label: "Widgets cibles" + } + } + }, + app: { + label: "Application", + sections: { + basics: { + label: "Informations de base", + description: "Identité et activation de l’application." + }, + navigation: { + label: "Navigation", + description: "Éléments de la barre latérale et regroupement par zones." + }, + content: { + label: "Contenu", + description: "Liaison de l’assistant intégré. Les objets et les points de terminaison sont déclarés sur la pile, et non sur l’application." + }, + branding: { + label: "Image de marque", + description: "Couleurs du thème et logo." + }, + access_and_sharing: { + label: "Accès et partage", + description: "Qui peut accéder à cette application. L’accès public et l’intégration se définissent par vue de formulaire, et non au niveau de l’application." + } + }, + fields: { + name: { + label: "Nom", + helpText: "snake_case, unique" + }, + label: { + label: "Libellé" + }, + description: { + label: "Description" + }, + icon: { + label: "Icône", + helpText: "Nom d’icône Lucide (par ex. « users », « briefcase »)" + }, + active: { + label: "Active" + }, + isDefault: { + label: "Par défaut", + helpText: "Faire de cette application l’application par défaut des nouveaux utilisateurs" + }, + hidden: { + label: "Masquée", + helpText: "Exclure cette application du sélecteur d’applications — l’interface la propose plutôt depuis le menu de l’avatar. Navigation uniquement : une application masquée reste entièrement accessible par son URL et soumise aux contrôles d’autorisation." + }, + navigation: { + label: "Navigation", + helpText: "Arborescence de navigation — structure récursive" + }, + areas: { + label: "Zones", + helpText: "Regrouper les éléments dans des zones repliables" + }, + "areas.id": { + label: "ID de la zone" + }, + "areas.label": { + label: "Libellé" + }, + "areas.icon": { + label: "Icône" + }, + "areas.description": { + label: "Description de la zone" + }, + "areas.navigation": { + label: "Navigation de la zone" + }, + defaultAgent: { + label: "Agent par défaut", + helpText: "Agent de plateforme de l’assistant intégré ('ask' par défaut ; 'build' pour les surfaces de création)" + }, + branding: { + label: "Image de marque", + helpText: "Couleurs principale/secondaire, logo, thème" + }, + requiredPermissions: { + label: "Autorisations requises", + helpText: "Autorisations nécessaires pour accéder à cette application" + } + } + }, + action: { + label: "Action", + sections: { + basics: { + label: "Informations de base", + description: "Identité et présentation de l’action." + }, + behavior: { + label: "Comportement", + description: "Configurez ce qui se passe lorsque l’action est déclenchée." + }, + placement: { + label: "Emplacement", + description: "Où et quand le bouton d’action apparaît." + }, + advanced: { + label: "Avancé", + description: "Exposition à l’IA et forme de la requête API." + } + }, + fields: { + name: { + label: "Nom", + helpText: "Identifiant unique (snake_case)" + }, + label: { + label: "Libellé", + helpText: "Texte du bouton affiché aux utilisateurs" + }, + objectName: { + label: "Nom de l’objet", + helpText: "Objet auquel appartient cette action (facultatif)" + }, + icon: { + label: "Icône", + helpText: "Nom d’icône Lucide (par ex. « check », « x-circle »)" + }, + type: { + label: "Type", + helpText: "Ce qui se passe au clic" + }, + variant: { + label: "Variante", + helpText: "Style du bouton (primary = bleu, danger = rouge, ghost = transparent)" + }, + mode: { + label: "Mode", + helpText: "Mode sémantique de l’action. Lu aujourd’hui uniquement par l’heuristique d’intervention humaine de l’IA — aucun moteur de rendu ne s’en sert." + }, + order: { + label: "Ordre", + helpText: "Ordre de tri au sein d’un groupe d’emplacement — une valeur plus basse place l’action plus haut, et l’en-tête de l’enregistrement prend la première comme bouton principal. Non défini : l’ordre d’enregistrement est conservé." + }, + target: { + label: "Cible", + helpText: "URL, nom de flux ou point de terminaison d’API à appeler" + }, + method: { + label: "Méthode", + helpText: "Méthode HTTP (GET, POST, PUT, DELETE)" + }, + body: { + label: "Corps", + helpText: "Soit une expression L1, soit un corps JS L2 en bac à sable" + }, + "body.language": { + label: "Langue", + helpText: "expression = formule pure ; js = JavaScript en bac à sable" + }, + "body.source": { + label: "Source", + helpText: "Source du corps de la fonction — pas d’imports au niveau supérieur" + }, + "body.capabilities": { + label: "Capacités", + helpText: "API ctx autorisées (api.read, api.write, crypto.uuid, log, …)" + }, + "body.timeoutMs": { + label: "Délai d’expiration (ms)", + helpText: "Délai d’expiration par appel (ms)" + }, + "body.memoryMb": { + label: "Mémoire (Mo)", + helpText: "Plafond de mémoire par appel (Mo, 256 max.)" + }, + params: { + label: "Paramètres", + helpText: "Paramètres saisis par l’utilisateur (afficher un formulaire avant l’exécution)" + }, + "params.name": { + label: "Nom" + }, + "params.field": { + label: "Champ" + }, + "params.objectOverride": { + label: "Remplacement de l’objet" + }, + "params.label": { + label: "Libellé" + }, + "params.type": { + label: "Type de paramètre" + }, + "params.widget": { + label: "Contrôle de saisie" + }, + "params.required": { + label: "Obligatoire" + }, + "params.options": { + label: "Options proposées" + }, + "params.placeholder": { + label: "Espace réservé" + }, + "params.helpText": { + label: "Texte d’aide" + }, + "params.defaultValue": { + label: "Valeur par défaut" + }, + "params.multiple": { + label: "Sélection multiple" + }, + "params.accept": { + label: "Types acceptés" + }, + "params.maxSize": { + label: "Taille maximale (octets)" + }, + "params.reference": { + label: "Objet de référence" + }, + "params.defaultFromRow": { + label: "Valeur par défaut issue de la ligne" + }, + "params.carryOver": { + label: "Reporter" + }, + "params.visible": { + label: "Visible si" + }, + "params.requiresFeature": { + label: "Fonctionnalité requise" + }, + operation: { + label: "Opération", + helpText: "Écriture déclarative de champs sur un seul enregistrement : 'update' applique `patch`, fusionné sous les `params` collectés, à l’enregistrement courant EN TANT QU’APPELANT — jamais avec des privilèges système, de sorte que les autorisations de l’appelant, les hooks de l’objet et ses validations s’appliquent comme pour une modification par un utilisateur." + }, + patch: { + label: "Correctif", + helpText: "Valeurs de champs statiques que la mise à jour écrit dans l’enregistrement courant, par ex. {\"status\": \"done\"} — fusionnées SOUS les valeurs que collectent les `params`, de sorte qu’un paramètre de même nom l’emporte. Écrit en tant qu’appelant : les autorisations, hooks et validations de l’objet s’appliquent comme pour une modification par un utilisateur." + }, + undoable: { + label: "Annulable", + helpText: "Proposer une possibilité d’annulation après la réussite de cette mise à jour. L’annulation capture la valeur antérieure de chaque champ que l’action écrit — l’ensemble fusionné, `patch` sous les `params` collectés. Une action sans `operation` ne déclare aucun ensemble d’écriture : il n’y a donc rien à capturer." + }, + execution: { + label: "Exécution", + helpText: "Le contrat de distribution en masse pour lequel le corps de cette action est écrit : 'perRecord' envoie une distribution par ligne sélectionnée, portant le recordId de cette ligne ; 'aggregate' envoie UNE seule distribution pour toute la sélection, avec tous les id dans params._selectedIds. S’il est omis, l’action est distribuée par enregistrement." + }, + description: { + label: "Description", + helpText: "Ligne explicative sous le titre de la boîte de dialogue des paramètres de cette action. Sur une action qui collecte des paramètres, la question de confirmation se place ici plutôt que dans confirmText — une seule boîte de dialogue, pas deux. Ne pas confondre avec ai.description, qui est le texte que lit un agent IA." + }, + submitText: { + label: "Libellé du bouton de validation", + helpText: "Libellé du bouton de validation de la boîte de dialogue des paramètres de cette action, le verbe qu’exécute le bouton (par ex. « Envoyer », « Consigner l’appel »). Uniquement pour une action qui collecte des paramètres. Laissez vide pour afficher « Confirmer »." + }, + cancelText: { + label: "Libellé du bouton d’annulation", + helpText: "Libellé du bouton de fermeture de la boîte de dialogue que cette action ouvre avant de s’exécuter, sa boîte de confirmation ou sa boîte de paramètres. Nommez ce que la fermeture conserve lorsque le verbe propre à l’action est « Annuler … » (par ex. « Conserver l’invitation »). Uniquement pour une action qui demande une confirmation ou collecte des paramètres. Laissez vide pour afficher « Annuler »." + }, + confirmText: { + label: "Texte de confirmation", + helpText: "Message de confirmation (par ex. « Êtes-vous sûr ? »)" + }, + successMessage: { + label: "Message de réussite", + helpText: "Message de réussite après l’exécution" + }, + errorMessage: { + label: "Message d’erreur", + helpText: "Message d’erreur affiché en cas d’échec de l’action, à la place de l’erreur brute." + }, + outcomeMessages: { + label: "Messages par résultat", + helpText: "Message de réussite par résultat du gestionnaire, au format JSON : chaque clé est une valeur `outcome` en snake_case que le gestionnaire renvoie dans sa charge utile de réussite, et chaque valeur est le libellé affiché pour ce résultat, par ex. {\"archived\": \"Archived\", \"already_archived\": \"Already archived\"}. Un libellé peut contenir des emplacements {{ result.* }}. Un résultat sans entrée affiche le message de réussite ; une clé que le gestionnaire ne renvoie jamais n’est jamais affichée. Non autorisé avec une boîte de dialogue de résultat ou `operation: 'update'`." + }, + onSuccess: { + label: "En cas de réussite", + helpText: "Navigation après la réussite de l’action : la route à ouvrir, et où l’ouvrir." + }, + "onSuccess.navigate": { + label: "Naviguer", + helpText: "Route ou modèle d’URL ouvert après la réussite de l’action. Il peut interpoler ${param.*} (les valeurs collectées par la boîte de dialogue des paramètres), ${ctx.*} (origin, apiBase, user, org, recordId, selection) et ${result.*} (la réponse du serveur, par ex. ${result.id}). Une valeur relative est une route interne à l’application." + }, + "onSuccess.openIn": { + label: "Ouvrir dans", + helpText: "Où s’ouvre la navigation : 'self' (par défaut) navigue sur place ; 'newTab' ouvre un nouvel onglet du navigateur." + }, + refreshAfter: { + label: "Actualiser ensuite", + helpText: "Actualiser la liste/la page une fois l’action terminée" + }, + openIn: { + label: "Ouvrir dans", + helpText: "Où ouvrir une URL `target` statique — 'self' navigue sur place, 'new-tab' ouvre un nouvel onglet du navigateur. S’il est omis, une URL absolue ou externe s’ouvre dans un nouvel onglet et une URL relative navigue sur place." + }, + opensInNewTab: { + label: "S’ouvre dans un nouvel onglet", + helpText: "Ouvrir le RÉSULTAT de l’action dans un nouvel onglet : le moteur de rendu pré-ouvre l’onglet de manière synchrone au clic (compatible avec les bloqueurs de fenêtres contextuelles) et le dirige vers la redirectUrl du gestionnaire. Distinct de `openIn`, qui achemine une cible d’URL statique." + }, + newTabUrl: { + label: "Chemin du nouvel onglet", + helpText: "Chemin sur ce serveur, commençant par / (par ex. /sso-open/{recordId}), avec un espace réservé {recordId}. Défini avec `opensInNewTab`, le moteur de rendu dirige immédiatement l’onglet pré-ouvert vers l’origine du serveur + ce chemin et n’envoie rien — le point de terminaison doit donc appliquer lui-même l’authentification. Une URL absolue est refusée ; pour une page externe, utilisez une action url avec Ouvrir dans défini sur new-tab." + }, + locations: { + label: "Emplacements", + helpText: "Où afficher cette action (barre d’outils, menu de ligne, etc.)" + }, + component: { + label: "Composant", + helpText: "Mode de rendu (bouton, icône, élément de menu)" + }, + visible: { + label: "Visible", + helpText: "Expression CEL : afficher uniquement lorsque la condition est vraie" + }, + disabled: { + label: "Désactivé", + helpText: "Expression CEL : désactiver lorsque la condition est vraie" + }, + disabledReason: { + label: "Motif de désactivation", + helpText: "Pourquoi l’action est grisée, affiché au survol et lu par les lecteurs d’écran tant que Désactivé est vrai (par ex. « Une revue juridique approuvée est d’abord requise. »). Affichage uniquement : le serveur refuse toujours l’appel selon ses propres règles. Laissez vide pour afficher « Non disponible pour cet enregistrement »." + }, + requiresFeature: { + label: "Fonctionnalité requise", + helpText: "Indicateur de fonctionnalité d’authentification public qui conditionne cette action. Il est converti en prédicat `visible` lors de l’analyse et retiré de la sortie : aucun consommateur en aval ne voit donc jamais la clé." + }, + requiresMembershipReach: { + label: "Portée d’appartenance requise", + helpText: "Le point de terminaison d’organisation qu’appelle cette action, afin qu’elle ne soit proposée qu’aux membres dont le niveau atteint ce point de terminaison. Il est converti en prédicat `visible` lors de l’analyse et retiré de la sortie : aucun consommateur en aval ne voit donc jamais la clé." + }, + requiredPermissions: { + label: "Autorisations requises", + helpText: "Capacités (systemPermissions d’un ensemble d’autorisations) qu’un appelant doit détenir — toutes celles listées — pour invoquer cette action (ADR-0066 D4). La porte d’action de la plateforme refuse toute autre personne avec un 403 sur chaque appel qu’elle distribue, chemin MCP/IA compris, et le bouton lui est masqué. Le point de terminaison d’une action de type api reste directement accessible : il doit donc vérifier lui-même ces capacités ; pour une action api dont la cible est hors origine, la plateforme ne vérifie rien côté serveur." + }, + ai: { + label: "IA", + helpText: "Exposition à l’IA (sur activation) : définissez ai.exposed=true et rédigez ai.description (≥40 caractères) pour que les agents puissent l’appeler." + }, + recordIdParam: { + label: "Paramètre d’ID d’enregistrement", + helpText: "Nom du paramètre de corps pour l’ID d’enregistrement" + }, + recordIdField: { + label: "Champ d’ID d’enregistrement", + helpText: "Champ à utiliser comme ID d’enregistrement (par défaut : « id »)" + }, + bodyShape: { + label: "Forme du corps", + helpText: "Structure du corps de la requête (plate ou imbriquée)" + }, + bodyExtra: { + label: "Corps supplémentaire", + helpText: "Champs statiques du corps de requête pour cette action api, fusionnés en dernier afin de remplacer les paramètres collectés (par ex. {\"resend\": true}). Les jetons de variable de page (page.NAME entre doubles accolades) sont résolus par l’environnement d’exécution. La charge utile se place ici, jamais dans params." + } + } + }, + report: { + label: "Rapport", + sections: { + basics: { + label: "Informations de base", + description: "Identité et type de rapport." + }, + dataset_binding: { + label: "Liaison au jeu de données", + description: "Le jeu de données de la couche sémantique que ce rapport affiche. Les valeurs sont les mesures du jeu de données ; les lignes en sont les dimensions." + }, + joined_blocks: { + label: "Blocs joints", + description: "Blocs supplémentaires liés à un jeu de données, empilés dans un rapport unique (rapports joints uniquement)." + }, + filter_and_chart: { + label: "Filtre et graphique", + description: "Filtre de portée au rendu et présentation du graphique." + }, + period_and_export: { + label: "Période et export", + description: "La période par défaut du rapport et les formats dans lesquels son tableau peut être téléchargé." + } + }, + fields: { + name: { + label: "Nom", + helpText: "Identifiant unique snake_case" + }, + label: { + label: "Libellé" + }, + description: { + label: "Description" + }, + type: { + label: "Type", + helpText: "Type de rapport : tabular/summary/matrix/joined" + }, + dataset: { + label: "Jeu de données", + helpText: "Jeu de données à lier (les mesures/dimensions proviennent de sa couche sémantique)" + }, + values: { + label: "Valeurs", + helpText: "Noms des mesures (du jeu de données) à afficher" + }, + rows: { + label: "Lignes", + helpText: "Noms des dimensions (du jeu de données) selon lesquelles regrouper les lignes" + }, + columns: { + label: "Colonnes", + helpText: "Noms des dimensions en colonnes (matrice uniquement)" + }, + order: { + label: "Ordre", + helpText: "Clés de tri, de la plus significative à la moins significative (une dimension de lignes/colonnes ou une mesure de valeurs). Les dimensions temporelles sont chronologiques par défaut." + }, + "order.by": { + label: "Trier par" + }, + "order.direction": { + label: "Sens" + }, + drilldown: { + label: "Exploration", + helpText: "Cliquer sur une ligne/cellule agrégée pour ouvrir les enregistrements sous-jacents" + }, + blocks: { + label: "Blocs", + helpText: "Sous-rapports liés à un jeu de données (rapport joint uniquement)" + }, + "blocks.name": { + label: "Nom" + }, + "blocks.label": { + label: "Libellé" + }, + "blocks.description": { + label: "Description du bloc" + }, + "blocks.type": { + label: "Type de bloc" + }, + "blocks.dataset": { + label: "Jeu de données" + }, + "blocks.rows": { + label: "Lignes" + }, + "blocks.columns": { + label: "Colonnes" + }, + "blocks.values": { + label: "Valeurs" + }, + "blocks.runtimeFilter": { + label: "Filtre d’exécution" + }, + "blocks.order": { + label: "Ordre" + }, + runtimeFilter: { + label: "Filtre d’exécution", + helpText: "Filtre de portée au rendu, combiné par ET au moment de la requête" + }, + chart: { + label: "Graphique", + helpText: "Configuration du graphique (type, légende, couleurs)" + }, + dateRange: { + label: "Plage de dates", + helpText: "Période par défaut et sélecteur de plage personnalisée. La dimension doit être une dimension de date du jeu de données ; laissez vide lorsque le jeu de données en a exactement une." + }, + exportOptions: { + label: "Options d’export", + helpText: "Formats de téléchargement (csv, xlsx) et nom de fichier. Qui peut télécharger dépend de l’autorisation d’export sur l’objet du jeu de données." + } + } + }, + dataset: { + label: "Jeu de données", + description: "Couche sémantique d’analyse — dimensions et mesures", + sections: { + basics: { + label: "Informations de base", + description: "Identité du jeu de données." + }, + source: { + label: "Source", + description: "L’objet de base, les relations à joindre et la portée intrinsèque du jeu de données. Les jointures sont dérivées du graphe d’objets — choisissez des noms de relations (lookup / master_detail), n’écrivez jamais de clause ON." + }, + dimensions: { + label: "Dimensions", + description: "Axes de regroupement. Utilisez un champ de base, ou `relationship.field` (par ex. account.region) pour une relation incluse ci-dessus." + }, + measures: { + label: "Mesures", + description: "Valeurs agrégeables définies une fois et référencées par leur nom. Une mesure est un sum/avg/count/… d’un champ ; une mesure dérivée combine d’autres mesures (ratio/sum/difference/product). Les filtres propres aux mesures et les opérations dérivées se modifient ligne par ligne dans le concepteur de jeux de données." + } + }, + fields: { + name: { + label: "Nom", + helpText: "Identifiant unique snake_case" + }, + label: { + label: "Libellé", + helpText: "Nom d’affichage" + }, + description: { + label: "Description", + helpText: "Ce que mesure ce jeu de données" + }, + object: { + label: "Objet", + helpText: "Objet de base — la clause FROM" + }, + include: { + label: "Inclure", + helpText: "Noms des champs de relation (lookup / master_detail) à joindre — permet les dimensions/mesures `relationship.field` (par ex. inclure « account » → regrouper par account.region)" + }, + filter: { + label: "Filtre", + helpText: "Filtre de portée intrinsèque (par ex. exclure les enregistrements supprimés logiquement), combiné par ET dans chaque requête" + }, + dimensions: { + label: "Dimensions", + helpText: "Pour chacune : nom (référencé par les présentations), champ, type et — pour les dates — une granularité de regroupement par défaut" + }, + "dimensions.name": { + label: "Nom" + }, + "dimensions.label": { + label: "Libellé" + }, + "dimensions.field": { + label: "Champ" + }, + "dimensions.type": { + label: "Type de dimension" + }, + "dimensions.dateGranularity": { + label: "Granularité de date" + }, + "dimensions.nullLabel": { + label: "Libellé des valeurs nulles" + }, + measures: { + label: "Mesures", + helpText: "Pour chacune : nom, agrégat, champ (facultatif pour count) et format d’affichage/devise" + }, + "measures.name": { + label: "Nom" + }, + "measures.label": { + label: "Libellé" + }, + "measures.aggregate": { + label: "Agrégat" + }, + "measures.field": { + label: "Champ" + }, + "measures.filter": { + label: "Filtre" + }, + "measures.format": { + label: "Format de la mesure" + }, + "measures.currency": { + label: "Devise" + }, + "measures.polarity": { + label: "Polarité" + }, + "measures.derived": { + label: "Dérivée de" + }, + "measures.duration": { + label: "Durée" + } + } + }, + flow: { + label: "Flux", + sections: { + basics: { + label: "Informations de base", + description: "Identité du flux et manière dont il démarre." + }, + canvas: { + label: "Canevas", + description: "Nœuds, liaisons et variables du flux — envisagez le concepteur visuel pour les flux complexes." + }, + execution: { + label: "Exécution", + description: "Statut de déploiement, identité et gestion des erreurs." + } + }, + fields: { + name: { + label: "Nom", + helpText: "Identifiant unique (snake_case)" + }, + label: { + label: "Libellé", + helpText: "Nom affiché aux utilisateurs" + }, + type: { + label: "Type", + helpText: "Comment le flux démarre (autolaunched, record_change, schedule, screen, api)" + }, + description: { + label: "Description", + helpText: "Ce que fait ce flux" + }, + nodes: { + label: "Nœuds", + helpText: "⚠️ Envisagez d’utiliser l’éditeur visuel Flow Designer plutôt que le JSON" + }, + "nodes.id": { + label: "ID du nœud" + }, + "nodes.type": { + label: "Type de nœud" + }, + "nodes.label": { + label: "Libellé" + }, + "nodes.config": { + label: "Configuration du nœud" + }, + "nodes.connectorConfig": { + label: "Action de connecteur" + }, + "nodes.position": { + label: "Position sur le canevas" + }, + "nodes.timeoutMs": { + label: "Délai d’expiration (ms)" + }, + "nodes.inputSchema": { + label: "Schéma d’entrée" + }, + "nodes.waitEventConfig": { + label: "Événement d’attente" + }, + "nodes.boundaryConfig": { + label: "Événement de bordure" + }, + edges: { + label: "Liaisons", + helpText: "Connexions entre les nœuds — utilisez Flow Designer pour une modification plus simple" + }, + "edges.id": { + label: "ID de la liaison" + }, + "edges.source": { + label: "Nœud de départ" + }, + "edges.target": { + label: "Nœud d’arrivée" + }, + "edges.condition": { + label: "Condition de la liaison" + }, + "edges.type": { + label: "Type de connexion" + }, + "edges.label": { + label: "Libellé" + }, + "edges.isDefault": { + label: "Chemin par défaut" + }, + variables: { + label: "Variables", + helpText: "Variables du flux (entrées/sorties)" + }, + "variables.name": { + label: "Nom" + }, + "variables.type": { + label: "Type de variable" + }, + "variables.isInput": { + label: "Entrée" + }, + "variables.isOutput": { + label: "Sortie" + }, + "variables.defaultValue": { + label: "Valeur par défaut" + }, + "variables.objectName": { + label: "Objet de l’enregistrement" + }, + status: { + label: "Statut", + helpText: "Statut de déploiement : draft → active → obsolete" + }, + version: { + label: "Version", + helpText: "Numéro de version (incrémenté automatiquement)" + }, + runAs: { + label: "Exécuter en tant que", + helpText: "Exécuter en tant que system (administrateur) ou user (autorisations de l’utilisateur courant)" + }, + requiredPermissions: { + label: "Autorisations requises", + helpText: "Capacités (systemPermissions d’un ensemble d’autorisations) qu’un appelant doit détenir — toutes celles listées — pour démarrer ce flux. Chaque porte de démarrage (la route de déclenchement, une action de flux via REST et MCP, un point de terminaison de flux) refuse toute autre personne avec un 403, et une action qui démarre ce flux hérite de la liste. L’étape de sous-flux d’un flux parent n’est pas revérifiée." + }, + errorHandling: { + label: "Gestion des erreurs", + helpText: "Que faire lorsqu’un nœud échoue (fail, retry, continue)" + }, + successMessage: { + label: "Message de réussite", + helpText: "Message porté par le résultat de l’exécution lorsque le flux se termine ; l’interface des flux d’écran l’affiche sous forme de notification au lieu d’un « Terminé » générique." + }, + errorMessage: { + label: "Message d’erreur", + helpText: "Message porté par le résultat de l’exécution lorsque le flux échoue ; l’interface des flux d’écran l’affiche sous forme de notification au lieu de l’erreur brute." + } + } + }, + job: { + label: "Tâche en arrière-plan" + }, + datasource: { + label: "Source de données" + }, + external_catalog: { + label: "Catalogue externe" + }, + api: { + label: "Point de terminaison d’API", + description: "Une URL HTTP stable que d’autres systèmes appellent pour exécuter un flux ou manipuler les enregistrements de l’application" + }, + translation: { + label: "Traduction" + }, + email_template: { + label: "Modèle d’e-mail", + sections: { + identity: { + label: "Identité", + description: "Les expéditeurs désignent ce modèle par son nom ; la langue détermine quelle version linguistique est envoyée." + }, + subject: { + label: "Objet", + description: "Ligne d’objet. Prend en charge l’interpolation {{var.path}}." + }, + html_body: { + label: "Corps HTML", + description: "Corps HTML enrichi. La plupart des clients suppriment : utilisez donc des styles en ligne." + }, + plain_text_body: { + label: "Corps en texte brut", + description: "Variante facultative en texte brut. Si elle est omise, le service supprime les balises du corps HTML pour en dériver une. En fournir une améliore le score anti-spam." + }, + variables: { + label: "Variables", + description: "Variables déclarées. Affichées comme indications dans Studio et validées par sendTemplate() lorsqu’elles sont obligatoires." + }, + delivery_overrides: { + label: "Remplacements d’envoi", + description: "Remplacements facultatifs par modèle pour From / Reply-To." + }, + status: { + label: "Statut" + } + }, + fields: { + name: { + label: "Nom", + helpText: "snake_case à points (par ex. auth.password_reset, crm.welcome)" + }, + label: { + label: "Libellé" + }, + locale: { + label: "Langue", + helpText: "Étiquette BCP-47 — par ex. en-US, zh-CN" + }, + description: { + label: "Description" + }, + subject: { + label: "Objet" + }, + bodyHtml: { + label: "Corps HTML" + }, + bodyText: { + label: "Corps texte" + }, + variables: { + label: "Variables", + helpText: "[{ \"name\": \"user.name\", \"type\": \"string\", \"required\": true, \"description\": \"...\" }]" + }, + fromOverride: { + label: "Remplacement de l’expéditeur", + helpText: "{ \"name\": \"Acme Sales\", \"address\": \"sales@acme.com\" }" + }, + replyTo: { + label: "Répondre à", + helpText: "Adresse e-mail Reply-To" + }, + active: { + label: "Actif", + helpText: "Lorsque la case est décochée, sendTemplate() renvoie TEMPLATE_INACTIVE." + } + } + }, + doc: { + label: "Documentation", + description: "Une page d’aide rédigée en Markdown, fournie par un package ou écrite par vous-même" + }, + book: { + label: "Livre de documentation", + description: "Une table des matières qui regroupe les pages d’aide en sections ordonnées" + }, + permission: { + label: "Ensemble d’autorisations", + sections: { + identity: { + label: "Identité", + description: "Les ensembles d’autorisations sont l’unique conteneur de capacités : un utilisateur obtient l’union de tous les ensembles qu’il détient, de sorte que les ensembles ne font jamais qu’ajouter des accès. Les postes distribuent les ensembles aux personnes." + }, + system_permissions: { + label: "Autorisations système", + description: "Capacités de haut niveau non liées à un objet particulier — par ex. manage_users, view_audit_logs." + }, + object_and_field_permissions: { + label: "Autorisations sur les objets et les champs", + description: "CRUD par objet + FLS par champ. Modifiez-les via l’éditeur matriciel ou collez du JSON ici." + }, + tab_and_row_level_security: { + label: "Onglets et sécurité au niveau des lignes", + description: "Visibilité des onglets et politiques RLS." + } + }, + fields: { + label: { + label: "Libellé", + helpText: "Libellé affiché aux administrateurs" + }, + name: { + label: "Nom", + helpText: "Rempli à partir des lettres latines et des chiffres du libellé lorsque vous créez l’ensemble ; si le libellé n’en contient aucun, saisissez-le vous-même. snake_case" + }, + description: { + label: "Description", + helpText: "Description lisible affichée dans Configuration." + }, + isDefault: { + label: "Par défaut", + helpText: "Accorde cet ensemble à toute personne qui se connecte, via le poste intégré everyone. Un ensemble déclaré par l’application elle-même est accordé au démarrage ; un ensemble provenant d’un package installé devient une suggestion qu’un administrateur confirme dans Configuration. Un ensemble doté d’autorisations à privilèges élevés n’est jamais accordé de cette manière. Par défaut : false." + }, + packageId: { + label: "ID du package", + helpText: "Le package qui fournit cet ensemble ; une valeur désignant un autre package est refusée. Laissez vide pour un ensemble créé dans ce système." + }, + systemPermissions: { + label: "Autorisations système", + helpText: "Liste des clés de capacités système" + }, + adminScope: { + label: "Portée d’administration", + helpText: "Administration déléguée : permet aux détenteurs de cet ensemble d’administrer une sous-arborescence d’unités commerciales. businessUnit est la racine de la sous-arborescence (sys_business_unit.name) ; il est obligatoire et non vide ; includeSubtree vaut true par défaut ; manageAssignments vaut false par défaut et permet au détenteur d’affecter des personnes à des postes, et d’accorder directement des ensembles, au sein de cette sous-arborescence ; un délégué ne peut distribuer que les ensembles nommés dans assignablePermissionSets. Laissez vide pour un ensemble qui ne délègue rien." + }, + objects: { + label: "Objets", + helpText: "{ \"account\": { allowRead: true, allowEdit: true, ... } }" + }, + fields: { + label: "Champs", + helpText: "{ \"account.amount\": { readable: true, editable: false } }" + }, + tabPermissions: { + label: "Autorisations d’onglets", + helpText: "{ \"app_crm\": \"visible\", \"app_admin\": \"hidden\" }" + }, + rowLevelSecurity: { + label: "Sécurité au niveau des lignes", + helpText: "Tableau de politiques RLS (voir rls.zod.ts)" + } + } + }, + position: { + label: "Poste", + sections: { + position: { + label: "Poste", + description: "Un poste est un ensemble plat et attribuable d’ensembles d’autorisations (par ex. sales_rep, sales_manager) : il nomme les ensembles qu’il distribue. Les capacités se trouvent sur les ensembles d’autorisations ; la profondeur de visibilité se trouve sur l’arborescence des unités commerciales." + } + }, + fields: { + name: { + label: "Nom", + helpText: "snake_case" + }, + label: { + label: "Libellé" + }, + description: { + label: "Description" + }, + permissionSets: { + label: "Ensembles d’autorisations", + helpText: "Ensembles d’autorisations que ce poste distribue, par leur nom (snake_case)." + }, + delegatable: { + label: "Délégable", + helpText: "Les titulaires peuvent déléguer eux-mêmes ce poste, pour une durée limitée et avec un motif. Par défaut : false — la délégation est réservée aux administrateurs." + } + } + }, + capability: { + label: "Capacité", + description: "Une autorisation définie par un package ; vous l’accordez via des ensembles d’autorisations" + }, + sharing_rule: { + label: "Règle de partage", + description: "Une règle qu’un package déclare pour partager des enregistrements ; les organisations créent les leurs dans Configuration" + }, + queue: { + label: "File d’attente", + description: "Un groupe nommé de personnes dans lequel des enregistrements peuvent attendre jusqu’à ce que l’une d’elles prenne l’enregistrement ; les organisations y affectent leurs propres membres dans Configuration" + }, + assignment_rule: { + label: "Règle d’attribution", + description: "Une règle qu’un package déclare pour acheminer les nouveaux enregistrements vers une file d’attente ou une personne" + }, + agent: { + label: "Agent IA", + sections: { + identity: { + label: "Identité", + description: "Comment les utilisateurs voient et désignent cet agent." + }, + ai_configuration: { + label: "Configuration de l’IA", + description: "Choix du modèle, instructions, planification et mémoire." + }, + capabilities: { + label: "Capacités", + description: "Compétences et sources de connaissances que l’agent peut utiliser." + }, + access: { + label: "Accès et sécurité", + description: "Qui peut utiliser cet agent et quelles garanties s’appliquent." + } + }, + fields: { + name: { + label: "Nom", + helpText: "Identifiant unique (snake_case)" + }, + label: { + label: "Libellé", + helpText: "Nom d’affichage (par ex. « Assistant commercial »)" + }, + role: { + label: "Rôle", + helpText: "Personnage de l’agent (par ex. « Spécialiste du support client »)" + }, + avatar: { + label: "Avatar", + helpText: "URL de l’image d’avatar" + }, + active: { + label: "Actif", + helpText: "Activer/désactiver cet agent" + }, + surface: { + label: "Surface", + helpText: "Surface produit à laquelle cet agent se rattache (ADR-0063 §1). Seules les compétences dont la surface correspond — ou vaut 'both' — s’y rattachent, et l’ensemble d’outils de l’agent est l’union des outils de ces compétences. Par défaut : 'ask'." + }, + instructions: { + label: "Instructions", + helpText: "Prompt système — indiquez à l’agent comment se comporter et ce qu’il peut faire" + }, + model: { + label: "Modèle", + helpText: "Configuration du modèle d’IA (fournisseur, nom du modèle, température, etc.)" + }, + planning: { + label: "Planification", + helpText: "Configuration du raisonnement autonome : le nombre maximal d’itérations de raisonnement avant que l’agent ne s’arrête (1–100, 10 par défaut)." + }, + memory: { + label: "Mémoire", + helpText: "Mémoire à long terme : notes synthétisées conservées par utilisateur, rappelées avant chaque conversation et rédigées par une réflexion toutes les reflectionInterval interactions menées à terme. Lorsque la mémoire à long terme est activée, maxEntries et reflectionInterval sont obligatoires. Appliqué par l’environnement d’exécution IA cloud." + }, + structuredOutput: { + label: "Sortie structurée", + helpText: "Contrat de sortie de la réponse finale de l’agent : format JSON, le JSON Schema par rapport auquel elle est vérifiée, les nouvelles tentatives, le format de repli et les étapes de transformation. Appliqué par l’environnement d’exécution IA cloud." + }, + skills: { + label: "Compétences", + helpText: "Noms des compétences (architecture Agent→Compétence→Outil)" + }, + access: { + label: "Accès", + helpText: "ID d’utilisateurs ou noms de rôles autorisés à discuter avec cet agent" + }, + permissions: { + label: "Autorisations", + helpText: "Autorisations requises pour utiliser cet agent" + }, + guardrails: { + label: "Garde-fous", + helpText: "Règles de sécurité et politiques de contenu" + } + } + }, + tool: { + label: "Outil IA", + sections: { + basics: { + label: "Informations de base", + description: "Identité de l’outil et description destinée à l’IA." + }, + schemas: { + label: "Schémas", + description: "Entrées acceptées par l’outil et forme de sa sortie." + } + }, + fields: { + name: { + label: "Nom", + helpText: "Identifiant unique (snake_case)" + }, + label: { + label: "Libellé", + helpText: "Nom affiché dans l’interface Studio" + }, + description: { + label: "Description", + helpText: "Indiquez à l’IA quand utiliser cet outil — soyez précis !" + }, + objectName: { + label: "Nom de l’objet", + helpText: "Objet associé (si cet outil agit sur un objet particulier)" + }, + parameters: { + label: "Paramètres", + helpText: "Paramètres d’entrée — définissez des propriétés comme : {name: {type: \"string\", description: \"...\"}}" + }, + outputSchema: { + label: "Schéma de sortie", + helpText: "Schéma de sortie pour la validation (facultatif)" + } + } + }, + skill: { + label: "Compétence IA", + sections: { + basics: { + label: "Informations de base", + description: "Identité de la compétence et description lisible." + }, + ai_instructions: { + label: "Instructions pour l’IA", + description: "Comment l’agent doit raisonner avec cette compétence." + }, + triggers: { + label: "Déclencheurs", + description: "Quand cette compétence doit s’activer." + } + }, + fields: { + name: { + label: "Nom", + helpText: "Identifiant unique (snake_case)" + }, + label: { + label: "Libellé", + helpText: "Nom d’affichage (par ex. « Gestion des dossiers »)" + }, + description: { + label: "Description", + helpText: "Ce que fait cette compétence" + }, + active: { + label: "Active", + helpText: "Activer/désactiver cette compétence" + }, + surface: { + label: "Surface", + helpText: "Surface d’agent à laquelle cette compétence se rattache (ADR-0063 §3). La rattacher à un agent dont la surface diffère provoque un échec franc lors de la résolution, et non un saut silencieux. Par défaut : 'ask'." + }, + instructions: { + label: "Instructions", + helpText: "Instructions pour l’IA — indiquez-lui comment utiliser ces outils ensemble" + }, + tools: { + label: "Outils", + helpText: "Noms des outils (prend en charge le joker : action_*)" + }, + triggerConditions: { + label: "Conditions de déclenchement", + helpText: "Conditions programmatiques (par ex. objectName == \"case\")" + }, + "triggerConditions.field": { + label: "Champ de contexte" + }, + "triggerConditions.operator": { + label: "Opérateur" + }, + "triggerConditions.value": { + label: "Valeur" + } + } + } +}; diff --git a/packages/platform-objects/src/apps/translations/fr.objects.generated.ts b/packages/platform-objects/src/apps/translations/fr.objects.generated.ts new file mode 100644 index 00000000000..095ab6f9b6a --- /dev/null +++ b/packages/platform-objects/src/apps/translations/fr.objects.generated.ts @@ -0,0 +1,3857 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'fr'. + * Edit translations in place; re-run extract (with --merge) to fill new gaps. + * Do not hand-edit the structure — only the leaf string values. + * Merge only fills gaps: correcting a source label/description does not + * push the correction into a leaf here that already holds a translation — + * a present-but-stale string is not a gap, so it is left as-is. Re-translate + * it by hand when its source changes; nothing here or in `os i18n check` + * tells a leaf a translator updated on purpose from one nobody has looked + * at since the source moved. + */ + +import type { TranslationData } from '@objectstack/spec/system'; + +export const frObjects: NonNullable = { + sys_user: { + label: "Utilisateur", + pluralLabel: "Utilisateurs", + description: "Comptes utilisateur pour l’authentification", + fields: { + name: { + label: "Nom" + }, + email: { + label: "E-mail" + }, + email_verified: { + label: "E-mail vérifié" + }, + two_factor_enabled: { + label: "Authentification à deux facteurs activée", + help: "Indique si l’authentification à deux facteurs est activée pour cet utilisateur. Géré par le plugin better-auth `twoFactor`." + }, + role: { + label: "Rôle de plateforme", + help: "Valeur de rôle héritée de better-auth (admin, user, …). ObjectStack ne l’écrit plus. Pour accorder le statut d’administrateur de la plateforme, ajoutez l’e-mail vérifié de l’utilisateur à `OS_PLATFORM_OWNER_EMAIL` ; avec le mode de location `single`, une attribution `admin_full_access` sans portée dans `sys_user_permission_set` le confère également." + }, + banned: { + label: "Banni", + help: "Lorsque cette option est activée, l’utilisateur ne peut pas se connecter. Utilisez les actions Bannir l’utilisateur / Lever le bannissement pour la modifier." + }, + ban_reason: { + label: "Motif du bannissement" + }, + ban_expires: { + label: "Fin du bannissement", + help: "Si renseigné, le bannissement est levé automatiquement à cette date." + }, + failed_login_count: { + label: "Nombre d’échecs de connexion", + help: "Tentatives de connexion échouées consécutives ; remis à 0 en cas de réussite. Géré par le gestionnaire d’authentification." + }, + locked_until: { + label: "Verrouillé jusqu’au", + help: "Si renseigné et dans le futur, la connexion est refusée (verrouillage contre la force brute). Se lève automatiquement passé cette date ; un administrateur peut le lever plus tôt via Déverrouiller." + }, + password_changed_at: { + label: "Mot de passe modifié le", + help: "Horodatage du dernier changement de mot de passe. Sert de base à password_expiry_days ; géré par le système." + }, + phone_number: { + label: "Numéro de téléphone", + help: "Numéro de téléphone de connexion (format E.164 recommandé). Unique par utilisateur ; géré par better-auth lorsque le plugin phoneNumber est activé." + }, + phone_number_verified: { + label: "Téléphone vérifié", + help: "Indique si le numéro de téléphone a été vérifié (la vérification par OTP nécessite une infrastructure SMS ; reste à false tant qu’elle n’est pas disponible). Géré par le système." + }, + must_change_password: { + label: "Doit changer de mot de passe", + help: "Lorsque cette option est activée, l’utilisateur doit changer son mot de passe avant de pouvoir continuer. Activée lorsqu’un administrateur crée ou importe le compte, ou en réinitialise le mot de passe ; gérée par le système." + }, + mfa_required_at: { + label: "MFA exigée le", + help: "Date à laquelle la MFA obligatoire s’est appliquée pour la première fois à cet utilisateur (point de départ du délai de grâce). Sert de base à mfa_required ; géré par le système." + }, + last_login_at: { + label: "Dernière connexion le", + help: "Horodatage de la dernière connexion réussie. Renseigné par le gestionnaire d’authentification ; géré par le système." + }, + last_login_ip: { + label: "IP de dernière connexion", + help: "Adresse IP du client lors de la dernière connexion réussie (issue de l’en-tête transmis par le proxy de confiance). Géré par le système." + }, + ai_access: { + label: "Accès IA", + help: "Indique si cet utilisateur dispose d’un siège IA. Un siège accorde la capacité `ai_seat`, qui ouvre les assistants IA dans l’application (créer et demander). Le nombre d’utilisateurs pouvant en détenir un est limité par les sièges inclus dans votre licence." + }, + image: { + label: "Photo de profil" + }, + locale: { + label: "Langue", + help: "La langue dans laquelle cet utilisateur voit l’interface et reçoit ses notifications, sous forme d’étiquette de langue telle que zh-CN ou ja-JP. Si elle n’est pas définie, la langue par défaut du déploiement s’applique." + }, + manager_id: { + label: "Responsable", + help: "Le responsable direct de cet utilisateur. Forme la chaîne hiérarchique que parcourt la portée hiérarchique `own_and_reports`." + }, + primary_business_unit_id: { + label: "Unité commerciale principale", + help: "L’unité commerciale principale de l’utilisateur — une projection dénormalisée de sys_business_unit_member.is_primary, maintenue par plugin-sharing. Permet à un filtre de recherche d’utilisateurs de filtrer les candidats par unité commerciale sans parcourir la table de jonction des appartenances. Ne pas modifier directement ; définissez-la via l’appartenance à une unité commerciale." + }, + source: { + label: "Source de l’identité", + help: "Comment cette identité a été créée — idp_provisioned (JIT par SSO fédéré) ou env_native (inscription locale / utilisateur final de l’application). Géré par le système ; ne pas modifier.", + options: { + idp_provisioned: "Provisionné par l’IdP", + env_native: "Compte local" + } + }, + id: { + label: "ID utilisateur" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + }, + _views: { + all_users: { + label: "Tous les utilisateurs" + }, + me: { + label: "Mon profil" + }, + unverified: { + label: "Non vérifiés" + }, + banned: { + label: "Banni" + } + }, + _actions: { + invite_user: { + label: "Inviter un utilisateur", + description: "Invitez une personne par adresse e-mail. Elle rejoint cette organisation avec le rôle choisi lorsqu’elle accepte l’invitation.", + successMessage: "Invitation envoyée à {{ result.email }}", + outcomeMessages: { + sent: "Invitation envoyée à {{ result.email }}", + link_only: "Invitation créée pour {{ result.email }}. L’envoi d’e-mails n’est pas configuré sur ce serveur, aucun e-mail n’a donc été envoyé. Utilisez Copier le lien d’invitation sur l’invitation pour la partager." + }, + params: { + role: { + helpText: "Propriétaire : contrôle total de l’organisation, y compris sa suppression.\nAdministrateur : gère les membres, les invitations, les équipes et les paramètres de l’organisation.\nAdministrateur délégué : invite des personnes et les place uniquement là où un administrateur le lui a délégué.\nMembre : travaille dans les applications de l’organisation avec l’accès que lui donnent ses ensembles d’autorisations.\nExterne : une personne extérieure à l’organisation, comme un fournisseur ou un client, qui ne voit que ce qui est partagé avec elle.", + options: { + owner: "Propriétaire", + admin: "Administrateur", + delegated_admin: "Administrateur délégué", + member: "Membre", + external: "Externe" + } + } + } + }, + ban_user: { + label: "Bannir l’utilisateur", + description: "Bannir cet utilisateur ? Il sera déconnecté, ne pourra plus se connecter et ses clés API seront refusées jusqu’à la levée du bannissement, tout comme ses jetons d’accès OAuth et leurs renouvellements. Les clés fonctionnent de nouveau après la levée du bannissement.", + successMessage: "Utilisateur banni", + params: { + banReason: { + label: "Motif du bannissement" + } + } + }, + unban_user: { + label: "Lever le bannissement", + successMessage: "Bannissement levé" + }, + unlock_user: { + label: "Déverrouiller le compte", + successMessage: "Compte déverrouillé" + }, + create_user: { + label: "Créer un utilisateur", + successMessage: "Utilisateur créé", + params: { + phoneNumber: { + label: "Numéro de téléphone", + helpText: "Numéro de téléphone de connexion (E.164, par ex. +8613800000000). Obligatoire si aucun e-mail n’est fourni." + }, + password: { + label: "Mot de passe (laisser vide pour en générer un)" + }, + mustChangePassword: { + label: "Exiger un changement de mot de passe à la première connexion" + }, + permissionSet: { + label: "Ensemble d’autorisations", + helpText: "Facultatif. Accorde cet ensemble d’autorisations au nouvel utilisateur, afin qu’il puisse ouvrir ses applications dès sa première connexion." + } + }, + resultDialog: { + title: "Utilisateur créé", + acknowledge: "Terminé", + fields: { + "user.email": "E-mail", + "user.phoneNumber": "Numéro de téléphone", + permissionSet: "Ensemble d’autorisations", + temporaryPassword: "Mot de passe temporaire : copiez-le maintenant, il n’est affiché qu’une seule fois et n’est jamais stocké" + } + } + }, + set_user_password: { + label: "Définir le mot de passe", + successMessage: "Mot de passe mis à jour", + params: { + newPassword: { + label: "Nouveau mot de passe (laisser vide pour en générer un)" + }, + mustChangePassword: { + label: "Exiger un changement de mot de passe à la prochaine connexion" + }, + revokeSessionsAndKeys: { + label: "Déconnecter partout et révoquer les clés API", + helpText: "Met fin à toutes les sessions de cet utilisateur et révoque toutes les clés API dont il est propriétaire. Laissez activé si le compte est peut-être compromis ; désactivez pour une réinitialisation de routine. Les clés révoquées le restent après le changement de mot de passe." + } + }, + resultDialog: { + title: "Mot de passe mis à jour", + acknowledge: "Terminé", + fields: { + temporaryPassword: "Mot de passe temporaire : copiez-le maintenant, il n’est affiché qu’une seule fois et n’est jamais stocké" + } + } + }, + impersonate_user: { + label: "Se connecter en tant que l’utilisateur", + confirmText: "Démarrer une session d’usurpation pour cet utilisateur ? À utiliser uniquement pour des cas d’assistance légitimes — les actions seront journalisées.", + successMessage: "Vous incarnez désormais l’utilisateur" + }, + set_user_manager: { + label: "Définir le responsable", + description: "Définit le responsable de cet utilisateur. La chaîne hiérarchique détermine l’acheminement des approbations et la portée d’enregistrements own_and_reports.", + successMessage: "Responsable mis à jour", + params: { + managerId: { + label: "Responsable", + helpText: "L’utilisateur dont cette personne relève. Le serveur refuse un responsable extérieur à son organisation, une auto-attribution et un lien qui fermerait la chaîne hiérarchique ou la rendrait trop profonde." + } + } + }, + list_platform_admins: { + label: "Administrateurs de la plateforme", + resultDialog: { + title: "Administrateurs de la plateforme", + description: "Toutes les personnes qui détiennent actuellement le statut d’administrateur de la plateforme : une attribution admin_full_access en cours de validité détenue par l’organisation par défaut, ou une adresse vérifiée figurant sur la liste des propriétaires du déploiement.", + acknowledge: "Fermer", + fields: { + entries: "Administrateurs de la plateforme" + } + } + }, + update_my_profile: { + label: "Mettre à jour le profil", + successMessage: "Profil mis à jour" + }, + change_my_password: { + label: "Changer le mot de passe", + successMessage: "Mot de passe modifié", + params: { + currentPassword: { + label: "Mot de passe actuel" + }, + newPassword: { + label: "Nouveau mot de passe" + }, + revokeOtherSessions: { + label: "Déconnecter les autres appareils" + } + } + }, + change_my_email: { + label: "Changer d’e-mail", + successMessage: "E-mail de vérification envoyé — consultez la nouvelle adresse pour confirmer.", + params: { + newEmail: { + label: "Nouvel e-mail" + } + } + }, + resend_verification_email: { + label: "Renvoyer l’e-mail de vérification", + successMessage: "E-mail de vérification envoyé — consultez votre boîte de réception." + }, + delete_my_account: { + label: "Supprimer mon compte", + description: "Supprimer définitivement votre compte ? Cette action est irréversible — toutes vos sessions seront fermées et toutes les données dont vous êtes propriétaire seront supprimées selon la politique de conservation configurée.", + successMessage: "Compte supprimé", + params: { + password: { + label: "Mot de passe actuel" + } + } + }, + enable_two_factor: { + label: "Activer l’authentification à deux facteurs", + params: { + password: { + label: "Mot de passe actuel" + } + }, + resultDialog: { + title: "Authentification à deux facteurs activée", + description: "Scannez le code QR avec votre application d’authentification, puis conservez les codes de secours en lieu sûr. Les codes de secours ne sont affichés qu’une seule fois.", + acknowledge: "J’ai enregistré mes codes de secours", + fields: { + totpURI: "URI d’authentification", + backupCodes: "Codes de secours" + } + } + }, + disable_two_factor: { + label: "Désactiver l’authentification à deux facteurs", + description: "Désactiver l’authentification à deux facteurs ? Votre compte sera moins sécurisé.", + successMessage: "Authentification à deux facteurs désactivée.", + params: { + password: { + label: "Mot de passe actuel" + } + } + }, + generate_backup_codes: { + label: "Régénérer les codes de secours", + description: "Générer un nouveau jeu de codes de secours ? Les codes générés précédemment cesseront de fonctionner.", + params: { + password: { + label: "Mot de passe actuel" + } + }, + resultDialog: { + title: "Nouveaux codes de secours générés", + description: "Les codes de secours précédents ne sont plus valides. Conservez ces nouveaux codes en lieu sûr — ils ne sont affichés qu’une seule fois.", + acknowledge: "J’ai enregistré les nouveaux codes", + fields: { + backupCodes: "Codes de secours" + } + } + } + }, + _validations: { + locale_bcp47_shape: { + message: "La langue doit être une étiquette de langue BCP-47, telle que zh-CN ou ja-JP." + } + } + }, + sys_session: { + label: "Session", + pluralLabel: "Sessions", + description: "Sessions utilisateur actives", + fields: { + user_id: { + label: "Utilisateur" + }, + expires_at: { + label: "Expire le" + }, + last_activity_at: { + label: "Dernière activité le", + help: "Horodatage de la dernière requête sur cette session ; détermine l’expiration pour inactivité. Géré par le système." + }, + revoked_at: { + label: "Révoqué le", + help: "Si renseigné, cette session a été révoquée (inactivité / durée maximale absolue / limite de sessions simultanées / administrateur / fin de l’appartenance à l’organisation). Géré par le système." + }, + revoke_reason: { + label: "Motif de la révocation", + help: "Pourquoi la session a été révoquée (idle_timeout, absolute_max, concurrent_cap, user_revoked, admin, organization_membership_ended, admin_password_reset, password_reset, …)." + }, + active_organization_id: { + label: "Organisation active" + }, + active_team_id: { + label: "Équipe active" + }, + ip_address: { + label: "Adresse IP" + }, + user_agent: { + label: "Agent utilisateur" + }, + impersonated_by: { + label: "Usurpée par", + help: "ID utilisateur de l’administrateur qui a démarré cette session d’usurpation, le cas échéant." + }, + token: { + label: "Jeton de session", + help: "Jeton de session opaque — jamais exposé dans l’interface" + }, + id: { + label: "ID de session" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + }, + _views: { + all_sessions: { + label: "Toutes" + }, + mine: { + label: "Mes sessions" + }, + revoked: { + label: "Révoquées" + } + }, + _actions: { + revoke_my_other_sessions: { + label: "Déconnecter les autres appareils", + confirmText: "Se déconnecter de tous les autres appareils sur lesquels vous êtes actuellement connecté ? Votre session actuelle restera active.", + successMessage: "Toutes les autres sessions ont été révoquées" + }, + revoke_session: { + label: "Révoquer la session", + confirmText: "Révoquer cette session ? L’utilisateur sera déconnecté de cet appareil.", + successMessage: "Session révoquée" + } + } + }, + sys_account: { + label: "Compte", + pluralLabel: "Comptes", + description: "Comptes OAuth et de fournisseurs d’authentification", + fields: { + id: { + label: "ID du compte" + }, + display_title: { + label: "Titre", + help: "Titre de l’enregistrement : le fournisseur et l’ID de compte qu’il a émis (calculé à la lecture)" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + }, + provider_id: { + label: "ID du fournisseur", + help: "Identifiant du fournisseur OAuth (google, github, etc.)" + }, + account_id: { + label: "ID du compte chez le fournisseur", + help: "ID de l’utilisateur dans le système du fournisseur" + }, + user_id: { + label: "Utilisateur", + help: "Lien vers la table des utilisateurs" + }, + access_token: { + label: "Jeton d’accès", + help: "Jeton d’accès OAuth actif émis par le fournisseur — jamais renvoyé par l’API de données ; better-auth le relit via l’accesseur privilégié de champs internes du moteur" + }, + refresh_token: { + label: "Jeton d’actualisation", + help: "Jeton d’actualisation OAuth actif — de longue durée et non révoqué par la révocation d’une session ObjectStack ; jamais renvoyé par l’API de données" + }, + id_token: { + label: "Jeton d’identité", + help: "Jeton d’identité OIDC émis par le fournisseur — jamais renvoyé par l’API de données" + }, + access_token_expires_at: { + label: "Expiration du jeton d’accès" + }, + refresh_token_expires_at: { + label: "Expiration du jeton d’actualisation" + }, + scope: { + label: "Portée OAuth" + }, + password: { + label: "Empreinte du mot de passe", + help: "Mot de passe haché pour le fournisseur e-mail/mot de passe" + }, + previous_password_hashes: { + label: "Empreintes des mots de passe précédents", + help: "Tableau JSON des empreintes de mots de passe antérieurs (limité par password_history_count) ; sert uniquement à empêcher leur réutilisation. Géré par le système." + } + }, + _views: { + all_links: { + label: "Toutes" + }, + mine: { + label: "Mes liaisons" + }, + by_provider: { + label: "Par fournisseur" + } + }, + _actions: { + unlink_account: { + label: "Dissocier le compte", + description: "Supprimer cette liaison d’identité ? L’utilisateur ne pourra plus se connecter avec ce fournisseur tant qu’il ne l’aura pas liée de nouveau.", + successMessage: "Liaison d’identité supprimée" + } + } + }, + sys_verification: { + label: "Vérification", + pluralLabel: "Vérifications", + description: "Jetons de vérification d’e-mail et de téléphone", + fields: { + id: { + label: "ID de vérification" + }, + display_title: { + label: "Titre", + help: "Titre de l’enregistrement : un libellé fixe (les colonnes d’identifiant et de jeton sont internes et ne servent jamais de titre)" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + }, + value: { + label: "Jeton de vérification", + help: "Jeton ou code de vérification" + }, + expires_at: { + label: "Expire le" + }, + identifier: { + label: "Identifiant", + help: "Adresse e-mail ou numéro de téléphone" + } + } + }, + sys_organization: { + label: "Organisation", + pluralLabel: "Organisations", + description: "Organisations pour le regroupement multi-locataire", + fields: { + name: { + label: "Nom", + help: "Le nom propre de l’organisation. Modifiez-le avec Renommer l’organisation en haut de cet enregistrement. Le nom de produit affiché par la console est un autre paramètre, défini dans Paramètres → Image de marque." + }, + slug: { + label: "Slug", + help: "Identifiant adapté aux URL" + }, + logo: { + label: "Logo" + }, + metadata: { + label: "Métadonnées", + help: "Métadonnées de l’organisation sérialisées en JSON" + }, + require_mfa: { + label: "Exiger l’authentification multifacteur (retiré)", + help: "Retiré. Définissez l’exigence d’authentification multifacteur de cette organisation dans Paramètres → Politique d’accès. Une valeur activée ici y a été déplacée en tant que « tout le monde » puis effacée." + }, + timezone: { + label: "Fuseau horaire", + help: "Fuseau horaire IANA (par ex. UTC, Asia/Shanghai), conservé comme valeur par défaut racine pour les unités commerciales sans fuseau propre. Il ne modifie aujourd’hui aucun calcul de date : les formules, les rapports et les dates affichées utilisent Paramètres → Localisation → Fuseau horaire par défaut." + }, + parent_organization_id: { + label: "Organisation parente", + help: "Parent de reporting/regroupement. N’accorde RIEN — la visibilité entre organisations provient de l’appartenance, jamais de cette référence. N’a de sens que si le déploiement exécute plusieurs organisations." + }, + sort_order: { + label: "Ordre de tri", + help: "Ordre d’affichage parmi les organisations sœurs. Présentation uniquement. N’a de sens que si le déploiement exécute plusieurs organisations." + }, + id: { + label: "ID de l’organisation" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + }, + _views: { + all_orgs: { + label: "Toutes" + } + }, + _actions: { + invite_user: { + label: "Inviter un utilisateur", + description: "Invitez une personne par adresse e-mail. Elle rejoint cette organisation avec le rôle choisi lorsqu’elle accepte l’invitation.", + successMessage: "Invitation envoyée à {{ result.email }}", + outcomeMessages: { + sent: "Invitation envoyée à {{ result.email }}", + link_only: "Invitation créée pour {{ result.email }}. L’envoi d’e-mails n’est pas configuré sur ce serveur, aucun e-mail n’a donc été envoyé. Utilisez Copier le lien d’invitation sur l’invitation pour la partager." + }, + params: { + role: { + helpText: "Propriétaire : contrôle total de l’organisation, y compris sa suppression.\nAdministrateur : gère les membres, les invitations, les équipes et les paramètres de l’organisation.\nAdministrateur délégué : invite des personnes et les place uniquement là où un administrateur le lui a délégué.\nMembre : travaille dans les applications de l’organisation avec l’accès que lui donnent ses ensembles d’autorisations.\nExterne : une personne extérieure à l’organisation, comme un fournisseur ou un client, qui ne voit que ce qui est partagé avec elle.", + options: { + owner: "Propriétaire", + admin: "Administrateur", + delegated_admin: "Administrateur délégué", + member: "Membre", + external: "Externe" + } + } + } + }, + create_organization: { + label: "Créer une organisation", + successMessage: "Organisation créée" + }, + update_organization: { + label: "Modifier l’organisation", + successMessage: "Organisation mise à jour" + }, + delete_organization: { + label: "Supprimer l’organisation", + confirmText: "Supprimer cette organisation ? Tous les membres perdront immédiatement l’accès. Cette action est irréversible.", + successMessage: "Organisation supprimée" + }, + set_active_organization: { + label: "Rendre active", + successMessage: "Organisation active changée" + }, + rename_organization: { + label: "Renommer l’organisation", + successMessage: "Organisation renommée" + }, + leave_organization: { + label: "Quitter l’organisation", + confirmText: "Quitter cette organisation ? Vous perdrez l’accès à ses environnements et à ses membres.", + successMessage: "Vous avez quitté l’organisation" + } + }, + _sections: { + identity: { + label: "Identité" + }, + branding: { + label: "Image de marque" + }, + configuration: { + label: "Configuration" + }, + group_structure: { + label: "Structure du groupe" + } + } + }, + sys_member: { + label: "Membre", + pluralLabel: "Membres", + description: "Enregistrements d’appartenance aux organisations", + fields: { + id: { + label: "ID du membre" + }, + display_title: { + label: "Titre", + help: "Titre de l’enregistrement : le membre et, s’il est renseigné, son rôle (calculé à la lecture)" + }, + created_at: { + label: "Créé le" + }, + organization_id: { + label: "Organisation" + }, + user_id: { + label: "Utilisateur" + }, + role: { + label: "Rôle", + help: "Rôle du membre au sein de l’organisation", + options: { + owner: "Propriétaire", + admin: "Administrateur", + delegated_admin: "Administrateur délégué", + member: "Membre", + external: "Externe" + } + } + }, + _views: { + mine: { + label: "Mes appartenances", + emptyState: { + title: "Aucune organisation pour le moment", + message: "Vous n’avez rejoint aucune organisation." + } + } + }, + _actions: { + invite_user: { + label: "Inviter un utilisateur", + description: "Invitez une personne par adresse e-mail. Elle rejoint cette organisation avec le rôle choisi lorsqu’elle accepte l’invitation.", + successMessage: "Invitation envoyée à {{ result.email }}", + outcomeMessages: { + sent: "Invitation envoyée à {{ result.email }}", + link_only: "Invitation créée pour {{ result.email }}. L’envoi d’e-mails n’est pas configuré sur ce serveur, aucun e-mail n’a donc été envoyé. Utilisez Copier le lien d’invitation sur l’invitation pour la partager." + }, + params: { + role: { + helpText: "Propriétaire : contrôle total de l’organisation, y compris sa suppression.\nAdministrateur : gère les membres, les invitations, les équipes et les paramètres de l’organisation.\nAdministrateur délégué : invite des personnes et les place uniquement là où un administrateur le lui a délégué.\nMembre : travaille dans les applications de l’organisation avec l’accès que lui donnent ses ensembles d’autorisations.\nExterne : une personne extérieure à l’organisation, comme un fournisseur ou un client, qui ne voit que ce qui est partagé avec elle.", + options: { + owner: "Propriétaire", + admin: "Administrateur", + delegated_admin: "Administrateur délégué", + member: "Membre", + external: "Externe" + } + } + } + }, + add_member: { + label: "Ajouter un membre", + successMessage: "Membre ajouté", + params: { + role: { + helpText: "Propriétaire : contrôle total de l’organisation, y compris sa suppression.\nAdministrateur : gère les membres, les invitations, les équipes et les paramètres de l’organisation.\nAdministrateur délégué : invite des personnes et les place uniquement là où un administrateur le lui a délégué.\nMembre : travaille dans les applications de l’organisation avec l’accès que lui donnent ses ensembles d’autorisations.\nExterne : une personne extérieure à l’organisation, comme un fournisseur ou un client, qui ne voit que ce qui est partagé avec elle." + } + } + }, + update_member_role: { + label: "Changer le rôle", + description: "Choisissez le nouveau rôle dans l’organisation pour {{ record.user_id.name | default:'ce membre' }}.", + successMessage: "Rôle du membre mis à jour", + outcomeMessages: { + permission_set_granted: "Rôle du membre mis à jour. Le nouveau rôle a également accordé l’ensemble d’autorisations {{ result.grantedPermissionSet.label }}." + }, + params: { + role: { + helpText: "Propriétaire : contrôle total de l’organisation, y compris sa suppression.\nAdministrateur : gère les membres, les invitations, les équipes et les paramètres de l’organisation.\nAdministrateur délégué : invite des personnes et les place uniquement là où un administrateur le lui a délégué.\nMembre : travaille dans les applications de l’organisation avec l’accès que lui donnent ses ensembles d’autorisations.\nExterne : une personne extérieure à l’organisation, comme un fournisseur ou un client, qui ne voit que ce qui est partagé avec elle." + } + } + }, + remove_member: { + label: "Retirer le membre", + confirmText: "Retirer ce membre de l’organisation ? Il perdra l’accès à toutes les ressources de l’organisation.", + successMessage: "Membre retiré" + }, + transfer_ownership: { + label: "Nommer propriétaire", + confirmText: "Faire du membre sélectionné un propriétaire de cette organisation ? Il obtient tous les privilèges de propriétaire, y compris la suppression de l’organisation. Vous restez également propriétaire ; pour vous retirer, utilisez Changer le rôle sur votre propre appartenance.", + successMessage: "Le membre est désormais propriétaire", + outcomeMessages: { + permission_set_granted: "Le membre est désormais propriétaire. Le nouveau rôle a également accordé l’ensemble d’autorisations {{ result.grantedPermissionSet.label }}." + } + } + } + }, + sys_invitation: { + label: "Invitation", + pluralLabel: "Invitations", + description: "Invitations à rejoindre une organisation pour l’intégration des utilisateurs", + fields: { + id: { + label: "ID de l’invitation" + }, + display_title: { + label: "Titre", + help: "Titre de l’enregistrement : l’adresse e-mail invitée (calculé à la lecture)" + }, + created_at: { + label: "Créé le" + }, + organization_id: { + label: "Organisation" + }, + email: { + label: "E-mail", + help: "Adresse e-mail de l’utilisateur invité" + }, + role: { + label: "Rôle", + help: "Rôle à attribuer lors de l’acceptation", + options: { + owner: "Propriétaire", + admin: "Administrateur", + delegated_admin: "Administrateur délégué", + member: "Membre", + external: "Externe" + } + }, + status: { + label: "Statut", + options: { + pending: "En attente", + accepted: "Acceptée", + rejected: "Refusée", + expired: "Expirée", + canceled: "Annulée" + } + }, + inviter_id: { + label: "Invité par", + help: "Utilisateur qui a envoyé l’invitation" + }, + expires_at: { + label: "Expire le" + }, + team_id: { + label: "Équipe", + help: "Équipe facultative à attribuer lors de l’acceptation" + }, + business_unit_id: { + label: "Unité commerciale d’affectation", + help: "Unité commerciale sous laquelle la personne invitée est placée lors de l’acceptation. Doit se trouver dans la sous-arborescence déléguée à l’émetteur." + }, + positions: { + label: "Postes d’affectation", + help: "Noms des postes attribués lors de l’acceptation. Les ensembles d’autorisations de chaque poste doivent figurer dans la liste autorisée par l’adminScope de l’émetteur." + } + }, + _views: { + pending: { + label: "En attente", + emptyState: { + title: "Aucune invitation en attente", + message: "Une invitation reste ici jusqu’à ce qu’elle soit acceptée, refusée ou annulée, ou jusqu’à son expiration." + } + }, + accepted: { + label: "Acceptée", + emptyState: { + title: "Aucune invitation acceptée", + message: "Une invitation arrive ici lorsque la personne l’accepte et rejoint l’organisation." + } + }, + expired: { + label: "Expirées / annulées", + emptyState: { + title: "Aucune invitation expirée ou annulée", + message: "Les invitations expirées, refusées ou annulées sont conservées ici." + } + }, + all_invitations: { + label: "Toutes", + emptyState: { + title: "Aucune invitation pour le moment", + message: "Toutes les invitations à rejoindre une organisation sont listées ici, quel que soit leur statut." + } + } + }, + _actions: { + invite_user: { + label: "Inviter un utilisateur", + description: "Invitez une personne par adresse e-mail. Elle rejoint cette organisation avec le rôle choisi lorsqu’elle accepte l’invitation.", + successMessage: "Invitation envoyée à {{ result.email }}", + outcomeMessages: { + sent: "Invitation envoyée à {{ result.email }}", + link_only: "Invitation créée pour {{ result.email }}. L’envoi d’e-mails n’est pas configuré sur ce serveur, aucun e-mail n’a donc été envoyé. Utilisez Copier le lien d’invitation sur l’invitation pour la partager." + }, + params: { + role: { + helpText: "Propriétaire : contrôle total de l’organisation, y compris sa suppression.\nAdministrateur : gère les membres, les invitations, les équipes et les paramètres de l’organisation.\nAdministrateur délégué : invite des personnes et les place uniquement là où un administrateur le lui a délégué.\nMembre : travaille dans les applications de l’organisation avec l’accès que lui donnent ses ensembles d’autorisations.\nExterne : une personne extérieure à l’organisation, comme un fournisseur ou un client, qui ne voit que ce qui est partagé avec elle.", + options: { + owner: "Propriétaire", + admin: "Administrateur", + delegated_admin: "Administrateur délégué", + member: "Membre", + external: "Externe" + } + } + } + }, + cancel_invitation: { + label: "Annuler l’invitation", + confirmText: "Annuler cette invitation ? Le destinataire ne pourra plus l’accepter.", + successMessage: "Invitation annulée", + cancelText: "Conserver l’invitation" + }, + resend_invitation: { + label: "Renvoyer l’invitation", + description: "Renvoyer l’invitation à {{ record.email }} ? La date d’expiration repart de zéro, et une invitation déjà expirée est remplacée par une nouvelle.", + successMessage: "Invitation renvoyée", + outcomeMessages: { + sent: "Invitation renvoyée à {{ result.email }}", + link_only: "Invitation renouvelée pour {{ result.email }}. L’envoi d’e-mails n’est pas configuré sur ce serveur, aucun e-mail n’a donc été envoyé. Utilisez Copier le lien d’invitation sur l’invitation pour la partager." + } + }, + copy_invitation_link: { + label: "Copier le lien d’invitation", + resultDialog: { + title: "Lien d’invitation", + description: "Toute personne qui ouvre ce lien peut accepter l’invitation : ne le partagez donc qu’avec la personne invitée. Il cesse de fonctionner dès que l’invitation est acceptée, refusée, annulée ou qu’elle expire.", + acknowledge: "Terminé", + fields: { + acceptUrl: "Lien d’invitation" + } + } + }, + accept_invitation: { + label: "Accepter l’invitation", + successMessage: "Invitation acceptée" + }, + reject_invitation: { + label: "Refuser l’invitation", + confirmText: "Refuser cette invitation ? La personne qui vous a invité sera notifiée et il vous faudra une nouvelle invitation pour rejoindre l’organisation.", + successMessage: "Invitation refusée" + } + } + }, + sys_team: { + label: "Équipe", + pluralLabel: "Équipes", + description: "Équipes au sein des organisations pour un regroupement plus fin", + fields: { + name: { + label: "Nom" + }, + organization_id: { + label: "Organisation", + help: "Organisation parente de cette équipe. Null en mono-locataire ; renseignée automatiquement en multi-locataire." + }, + member_count: { + label: "Nombre de membres", + help: "Compteur de sièges maintenu par better-auth ; ne pas écrire directement." + }, + id: { + label: "ID de l’équipe" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + }, + _views: { + by_org: { + label: "Par organisation" + }, + all_teams: { + label: "Toutes" + } + }, + _actions: { + create_team: { + label: "Créer une équipe", + successMessage: "Équipe créée" + }, + update_team: { + label: "Modifier l’équipe", + successMessage: "Équipe mise à jour" + }, + add_team_member: { + label: "Ajouter un membre", + successMessage: "Membre ajouté à l’équipe" + }, + remove_team: { + label: "Supprimer l’équipe", + confirmText: "Supprimer cette équipe ? Les membres perdront tout accès limité à l’équipe. Cette action est irréversible.", + successMessage: "Équipe supprimée" + } + } + }, + sys_team_member: { + label: "Membre d’équipe", + pluralLabel: "Membres d’équipe", + description: "Enregistrements d’appartenance qui relient les utilisateurs aux équipes", + fields: { + id: { + label: "ID du membre d’équipe" + }, + display_title: { + label: "Titre", + help: "Titre de l’enregistrement : l’utilisateur et l’équipe (calculé à la lecture)" + }, + created_at: { + label: "Créé le" + }, + team_id: { + label: "Équipe" + }, + user_id: { + label: "Utilisateur" + }, + membership_key: { + label: "Clé d’appartenance", + help: "Empreinte d’appartenance dérivée, maintenue par better-auth ; ne pas écrire directement." + } + }, + _actions: { + add_team_member: { + label: "Ajouter un membre", + successMessage: "Membre ajouté à l’équipe" + }, + remove_team_member: { + label: "Retirer de l’équipe", + description: "Retirer cet utilisateur de l’équipe ? Il perdra tout accès limité à l’équipe.", + successMessage: "Membre retiré de l’équipe" + } + } + }, + sys_business_unit: { + label: "Unité commerciale", + pluralLabel: "Unités commerciales", + description: "La structure de votre entreprise — divisions, départements, régions et bureaux. Les règles de partage peuvent partager des enregistrements avec une unité, ou avec une unité et les unités qui en dépendent : les membres obtiennent cet accès et le perdent lorsqu’ils partent. Permettre à un ensemble d’autorisations de lire tous les enregistrements d’une unité (portée de lecture 'unit') nécessite l’édition Entreprise.", + fields: { + name: { + label: "Nom" + }, + code: { + label: "Code", + help: "Code court et stable (par ex. EMEA-SALES). Unique au sein du locataire." + }, + kind: { + label: "Nature", + help: "Sert uniquement au regroupement et au reporting ; ne modifie ni la place de l’unité dans l’organisation ni les personnes qu’elle couvre.", + options: { + company: "Entreprise", + division: "Division", + department: "Département", + office: "Bureau", + cost_center: "Centre de coûts" + } + }, + parent_business_unit_id: { + label: "Unité commerciale parente", + help: "L’unité sous laquelle celle-ci se trouve. Laissez vide pour une unité de premier niveau. Une unité qui a encore des unités en dessous d’elle ne peut pas être supprimée tant qu’elles n’ont pas été déplacées sous une autre unité ou supprimées." + }, + timezone: { + label: "Fuseau horaire", + help: "Fuseau horaire IANA (par ex. Asia/Shanghai) conservé pour cette unité ; laissez vide pour reprendre celui de l’unité parente, puis celui de l’organisation. Il ne modifie aujourd’hui aucun calcul de date : les formules, les rapports et les dates affichées utilisent Paramètres → Localisation → Fuseau horaire par défaut." + }, + organization_id: { + label: "Organisation", + help: "Portée du locataire. Null en mono-locataire ; renseignée automatiquement en multi-locataire." + }, + manager_user_id: { + label: "Responsable de l’unité commerciale", + help: "Utilisateur responsable de cette unité organisationnelle (responsable ou chef de l’unité commerciale)." + }, + active: { + label: "Active", + help: "Désactivez pour retirer l’unité : le partage et les autres accès accordés à cette unité, ou à une unité au-dessus d’elle, n’atteignent plus ses membres ni les unités qui en dépendent." + }, + effective_from: { + label: "En vigueur depuis le", + help: "Date de création de cette unité commerciale. Généralement renseignée par une synchronisation depuis votre système RH." + }, + effective_to: { + label: "En vigueur jusqu’au", + help: "Date de retrait de cette unité commerciale. Généralement renseignée par une synchronisation depuis votre système RH." + }, + external_ref: { + label: "Référence externe", + help: "L’ID de l’unité dans votre système RH (comme Workday, SAP HR ou 北森), utilisé pour la faire correspondre à chaque synchronisation." + }, + id: { + label: "ID de l’unité commerciale" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + }, + _views: { + org_chart: { + label: "Organigramme" + }, + active: { + label: "Actives" + }, + inactive: { + label: "Inactives" + }, + by_kind: { + label: "Par nature" + }, + all_departments: { + label: "Toutes" + } + } + }, + sys_business_unit_member: { + label: "Membre d’unité commerciale", + pluralLabel: "Membres d’unité commerciale", + description: "Affectation d’un utilisateur à une unité commerciale (compatible avec les organisations matricielles, datée d’effet).", + fields: { + id: { + label: "ID du membre" + }, + display_title: { + label: "Titre", + help: "Titre de l’enregistrement : l’utilisateur et l’unité commerciale à laquelle il est affecté (calculé à la lecture)" + }, + business_unit_id: { + label: "Unité commerciale" + }, + user_id: { + label: "Utilisateur" + }, + function_in_business_unit: { + label: "Fonction dans l’unité commerciale", + help: "`lead` est le responsable au quotidien ; `deputy` peut remplacer le responsable dans l’acheminement des approbations.", + options: { + member: "membre", + lead: "responsable", + deputy: "adjoint" + } + }, + is_primary: { + label: "Affectation principale", + help: "Lorsque l’utilisateur appartient à plusieurs unités commerciales, indique celle qui fait référence pour le reporting." + }, + effective_from: { + label: "En vigueur depuis le" + }, + effective_to: { + label: "En vigueur jusqu’au" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + } + }, + sys_api_key: { + label: "Clé API", + pluralLabel: "Clés API", + description: "Clés API pour l’accès programmatique", + fields: { + name: { + label: "Nom", + help: "Libellé lisible de la clé API" + }, + prefix: { + label: "Préfixe", + help: "Préfixe visible permettant d’identifier la clé (par ex. « osk_ »)" + }, + user_id: { + label: "Propriétaire", + help: "Utilisateur propriétaire de cette clé API" + }, + active_organization_id: { + label: "Organisation active", + help: "Organisation dans laquelle cette clé s’authentifie — héritée de son créateur lors de la création et établie comme organisation active de la requête" + }, + scopes: { + label: "Portées", + help: "Tableau JSON des portées d’autorisation" + }, + expires_at: { + label: "Expire le" + }, + last_used_at: { + label: "Dernière utilisation le", + help: "Enregistré lorsque la clé est utilisée, au plus une fois par heure. Une clé sans utilisation enregistrée pendant la période d’inactivité (90 jours par défaut) est refusée jusqu’à son remplacement" + }, + revoked: { + label: "Révoquée" + }, + revoked_reason: { + label: "Motif de la révocation", + help: "Renseigné lorsque la réinitialisation forcée du mot de passe par un administrateur a révoqué cette clé. Seul un administrateur de la plateforme peut restaurer une telle clé ; son propriétaire en crée une nouvelle. Effacé lorsque la clé est restaurée. Vide pour une clé révoquée depuis la liste Clés API.", + options: { + admin_password_reset: "Réinitialisation du mot de passe par un administrateur" + } + }, + key: { + label: "Clé hachée", + help: "Valeur hachée de la clé API — jamais exposée aux clients" + }, + id: { + label: "ID de la clé API" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + }, + _views: { + all_keys: { + label: "Toutes", + emptyState: { + title: "Aucune clé API pour le moment", + message: "Créez une clé pour vos scripts et intégrations avec Créer une clé ; la clé n’est affichée qu’une fois, lors de sa création." + } + }, + mine: { + label: "Mes clés", + emptyState: { + title: "Vous n’avez encore aucune clé API", + message: "Créez une clé pour vos scripts et intégrations avec Créer une clé ; la clé n’est affichée qu’une fois, lors de sa création." + } + }, + active: { + label: "Actives", + emptyState: { + title: "Aucune clé API active", + message: "Toutes les clés qui n’ont pas été révoquées sont listées ici." + } + }, + revoked: { + label: "Révoquées", + emptyState: { + title: "Aucune clé révoquée", + message: "Une clé arrive ici lorsqu’elle est révoquée ; les clients qui l’utilisent perdent l’accès jusqu’à sa restauration." + } + } + }, + _actions: { + create_api_key: { + label: "Créer une clé", + description: "Créez une clé pour un script ou une intégration. La clé agit en votre nom, avec vos accès, dans l’organisation où vous travaillez actuellement.", + params: { + expires_at: { + helpText: "Laissez vide pour la durée de vie par défaut définie dans Configuration → Authentification → Clés API (Durée de vie par défaut des clés, 90 jours sauf modification). Une date postérieure à la Durée de vie maximale des clés qui y est définie est refusée." + } + }, + resultDialog: { + title: "Clé API créée", + description: "Copiez la clé maintenant — elle n’est affichée qu’une seule fois et ne peut pas être récupérée. En cas de fuite, révoquez-la et créez-en une nouvelle.", + acknowledge: "J’ai enregistré la clé", + fields: { + key: "Clé API", + expires_at: "Expire le" + } + } + }, + revoke_api_key: { + label: "Révoquer la clé API", + confirmText: "Révoquer cette clé API ? Tous les clients qui l’utilisent perdront immédiatement l’accès.", + successMessage: "Clé API « {{ record.name }} » révoquée" + }, + restore_api_key: { + label: "Restaurer la clé API", + confirmText: "Restaurer cette clé API révoquée ? Les clients existants qui détiennent la clé retrouveront l’accès.", + successMessage: "Clé API « {{ record.name }} » restaurée" + } + } + }, + sys_two_factor: { + label: "Double authentification", + pluralLabel: "Identifiants de double authentification", + description: "Identifiants d’authentification à deux facteurs", + fields: { + id: { + label: "ID de double authentification" + }, + display_title: { + label: "Titre", + help: "Titre de l’enregistrement : l’utilisateur auquel appartient l’identifiant (calculé à la lecture)" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + }, + user_id: { + label: "Utilisateur" + }, + secret: { + label: "Secret", + help: "Clé secrète TOTP" + }, + backup_codes: { + label: "Codes de secours", + help: "Codes de récupération de secours, chiffrés au repos (un unique texte chiffré opaque, et non du JSON lisible)" + }, + verified: { + label: "Vérifié", + help: "Indique si l’inscription a été confirmée avec un code TOTP valide (géré par better-auth)" + }, + failed_verification_count: { + label: "Nombre d’échecs de vérification", + help: "Vérifications 2FA échouées consécutives ; remis à zéro en cas de réussite. Géré par better-auth." + }, + locked_until: { + label: "Verrouillé jusqu’au", + help: "Renseigné lorsque les vérifications 2FA échouées dépassent le seuil de verrouillage. Géré par better-auth." + } + }, + _views: { + mine: { + label: "Mon inscription" + }, + all_enrollments: { + label: "Tous" + } + }, + _actions: { + enable_two_factor: { + label: "Activer la 2FA", + params: { + password: { + label: "Mot de passe actuel" + } + }, + resultDialog: { + title: "Authentification à deux facteurs activée", + description: "Scannez le code QR avec votre application d’authentification, puis conservez les codes de secours en lieu sûr. Les codes de secours ne sont affichés qu’une seule fois.", + acknowledge: "J’ai enregistré mes codes de secours", + fields: { + totpURI: "URI d’authentification", + backupCodes: "Codes de secours" + } + } + }, + disable_two_factor: { + label: "Désactiver la 2FA", + description: "Désactiver l’authentification à deux facteurs sur votre compte ?", + successMessage: "2FA désactivée", + params: { + password: { + label: "Mot de passe actuel" + } + } + }, + regenerate_backup_codes: { + label: "Régénérer les codes de secours", + description: "Régénérer les codes de secours ? Tous les codes de secours précédents cesseront immédiatement de fonctionner.", + params: { + password: { + label: "Mot de passe actuel" + } + }, + resultDialog: { + title: "Nouveaux codes de secours générés", + description: "Les codes de secours précédents ne sont plus valides. Conservez ces nouveaux codes en lieu sûr — ils ne sont affichés qu’une seule fois.", + acknowledge: "J’ai enregistré les nouveaux codes", + fields: { + backupCodes: "Codes de secours" + } + } + } + } + }, + sys_device_code: { + label: "Code d’appareil", + pluralLabel: "Codes d’appareil", + description: "Demandes en attente de l’octroi d’autorisation d’appareil OAuth 2.0 (RFC 8628)", + fields: { + id: { + label: "ID du code d’appareil" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + }, + device_code: { + label: "Code d’appareil", + help: "Jeton à forte entropie renvoyé à l’appareil qui interroge le serveur" + }, + user_code: { + label: "Code utilisateur", + help: "Code court présenté à l’utilisateur (par ex. ABCD-EFGH)" + }, + user_id: { + label: "Utilisateur", + help: "Utilisateur qui a approuvé l’autorisation de l’appareil" + }, + expires_at: { + label: "Expire le", + help: "Date à partir de laquelle les codes d’appareil et utilisateur ne sont plus valides" + }, + status: { + label: "Statut", + help: "Statut actuel : 'pending' | 'approved' | 'denied'" + }, + last_polled_at: { + label: "Dernière interrogation le", + help: "Horodatage de la dernière interrogation de /device/token" + }, + polling_interval: { + label: "Intervalle d’interrogation (ms)", + help: "Intervalle d’interrogation minimal recommandé par le serveur, en ms" + }, + client_id: { + label: "ID client", + help: "Identifiant client OAuth de l’appareil demandeur" + }, + scope: { + label: "Portée", + help: "Portées OAuth demandées par l’appareil, séparées par des espaces" + } + } + }, + sys_user_preference: { + label: "Préférence utilisateur", + pluralLabel: "Préférences utilisateur", + description: "Préférences clé-valeur propres à chaque utilisateur (thème, langue, etc.)", + fields: { + id: { + label: "ID de la préférence" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + }, + user_id: { + label: "Utilisateur", + help: "Utilisateur propriétaire de cette préférence" + }, + key: { + label: "Clé", + help: "Clé de préférence (par ex. theme, locale, plugin.ai.auto_save)" + }, + value: { + label: "Valeur", + help: "Valeur de la préférence (tout type sérialisable en JSON)" + } + }, + _views: { + all_preferences: { + label: "Toutes" + }, + mine: { + label: "Mes préférences" + }, + by_user: { + label: "Par utilisateur" + } + } + }, + sys_personal_view: { + label: "Vue personnelle", + pluralLabel: "Vues personnelles", + description: "Les vues de liste nommées propres à un lecteur (Mes vues). Visibles uniquement par leur propriétaire.", + fields: { + id: { + label: "ID de la vue personnelle" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + }, + user_id: { + label: "Propriétaire", + help: "L’utilisateur auquel appartient cette vue. Lui seul peut la lire ou la modifier." + }, + object_name: { + label: "Objet", + help: "L’objet listé par cette vue (son nom technique)." + }, + label: { + label: "Nom", + help: "Le nom que le propriétaire a donné à cette vue." + }, + config: { + label: "Configuration de la vue", + help: "La configuration de la liste, au format ListView de la spécification. Validée à chaque enregistrement." + } + } + }, + sys_oauth_application: { + label: "Application OAuth", + pluralLabel: "Applications OAuth", + description: "Applications clientes OAuth/OIDC enregistrées", + fields: { + id: { + label: "ID" + }, + name: { + label: "Nom" + }, + icon: { + label: "Icône", + help: "URL du logo affiché sur l’écran de consentement" + }, + uri: { + label: "URI de la page d’accueil", + help: "Page d’accueil publique du client enregistré" + }, + contacts: { + label: "Contacts", + help: "Liste sérialisée en JSON des adresses e-mail de contact" + }, + tos: { + label: "Conditions d’utilisation" + }, + policy: { + label: "Politique de confidentialité" + }, + metadata: { + label: "Métadonnées", + help: "Métadonnées de l’application sérialisées en JSON" + }, + client_id: { + label: "ID client", + help: "Identifiant client OAuth public" + }, + client_discovery_id: { + label: "ID de découverte du client", + help: "Identifiant opaque utilisé par le fournisseur pour retrouver ce client sur le chemin de découverte, distinct du `client_id` public" + }, + client_secret: { + label: "Secret client", + help: "Secret client OAuth — stocké sous forme d’empreinte SHA-256, jamais en clair (`storeClientSecret` de `@better-auth/oauth-provider`, haché par défaut dès que le plugin jwt est activé ; câblé dans `AuthManager.buildPluginList()` de plugin-auth, branche oidcProvider). Affiché une seule fois lors de l’enregistrement." + }, + redirect_uris: { + label: "URI de redirection", + help: "Liste sérialisée en JSON des URI de redirection autorisées" + }, + post_logout_redirect_uris: { + label: "URI de redirection après déconnexion", + help: "Liste sérialisée en JSON des URI de redirection autorisées après déconnexion" + }, + type: { + label: "Type de client", + options: { + web: "web", + native: "natif" + } + }, + public: { + label: "Client public", + help: "Indique que le client est un client OAuth public (non confidentiel)" + }, + require_pkce: { + label: "Exiger PKCE" + }, + token_endpoint_auth_method: { + label: "Méthode d’authentification au point de terminaison de jeton", + help: "par ex. client_secret_basic, client_secret_post, none" + }, + grant_types: { + label: "Types d’octroi", + help: "Liste sérialisée en JSON des types d’octroi autorisés" + }, + response_types: { + label: "Types de réponse", + help: "Liste sérialisée en JSON des types de réponse autorisés" + }, + scopes: { + label: "Portées autorisées", + help: "Liste sérialisée en JSON des portées que le client peut demander" + }, + client_credentials_scopes: { + label: "Portées client_credentials", + help: "Liste sérialisée en JSON des portées que le client peut demander avec l’octroi client_credentials, où aucun utilisateur ne donne son consentement — distincte de `scopes`, qui régit les octrois délégués par l’utilisateur" + }, + subject_type: { + label: "Type de sujet", + help: "Type de sujet OIDC (par ex. public, pairwise)" + }, + jwks: { + label: "JWKS", + help: "Jeu de clés web JSON (JWKS) du client (pour private_key_jwt / la vérification des requêtes signées)" + }, + jwks_uri: { + label: "JWKS URI", + help: "URL du jeu de clés web JSON (JWKS) du client" + }, + dpop_bound_access_tokens: { + label: "Jetons d’accès liés à DPoP", + help: "Exige que les jetons d’accès émis pour ce client soient liés à DPoP (RFC 9449)" + }, + disabled: { + label: "Désactivée" + }, + skip_consent: { + label: "Ignorer le consentement", + help: "Traiter comme un client de confiance et contourner l’écran de consentement" + }, + enable_end_session: { + label: "Activer la fin de session", + help: "Autorise le client à appeler le point de terminaison de fin de session OIDC" + }, + backchannel_logout_uri: { + label: "URI de déconnexion back-channel", + help: "Point de terminaison de déconnexion back-channel OIDC du client" + }, + backchannel_logout_session_required: { + label: "Session requise pour la déconnexion back-channel", + help: "Indique si le jeton de déconnexion back-channel doit inclure une revendication sid" + }, + software_id: { + label: "ID du logiciel" + }, + software_version: { + label: "Version du logiciel" + }, + software_statement: { + label: "Déclaration du logiciel", + help: "JWT signé attestant les métadonnées du client (RFC 7591 §2.3)" + }, + user_id: { + label: "Utilisateur propriétaire", + help: "Utilisateur qui a enregistré cette application" + }, + reference_id: { + label: "ID de référence", + help: "Identifiant de corrélation fourni par l’appelant" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + }, + _views: { + all_apps: { + label: "Toutes", + emptyState: { + title: "Aucune application OAuth pour le moment", + message: "Enregistrez une application pour permettre à ses clients de connecter des utilisateurs avec ce système." + } + }, + mine: { + label: "Mes applications", + emptyState: { + title: "Vous n’avez encore aucune application OAuth", + message: "Les applications que vous enregistrez sont listées ici." + } + }, + active: { + label: "Actives", + emptyState: { + title: "Aucune application OAuth active", + message: "Une application reste active jusqu’à sa désactivation ; les applications désactivées sont listées sous Désactivées." + } + }, + disabled_apps: { + label: "Désactivées", + emptyState: { + title: "Aucune application OAuth désactivée", + message: "Une application arrive ici lorsqu’elle est désactivée ; ses clients ne peuvent plus se connecter tant qu’elle n’est pas réactivée." + } + } + }, + _actions: { + disable_oauth_application: { + label: "Désactiver l’application OAuth", + description: "Désactiver cette application OAuth ? Les jetons d’accès et d’actualisation actifs qui lui ont été émis continueront d’être refusés aux points de terminaison de jeton, d’autorisation et d’introspection. Les intégrations existantes cesseront immédiatement de fonctionner.", + successMessage: "Application OAuth désactivée" + }, + enable_oauth_application: { + label: "Activer l’application OAuth", + description: "Réactiver cette application OAuth ? L’émission de jetons, l’autorisation et l’introspection reprendront immédiatement.", + successMessage: "Application OAuth activée" + }, + create_oauth_application: { + label: "Enregistrer une application OAuth", + params: { + name: { + label: "Nom de l’application" + }, + redirectURLs: { + label: "URL de redirection", + helpText: "Une URL par ligne. Les applications web nécessitent https:// sur un hôte autre que localhost ou une autre adresse de bouclage. Les applications natives peuvent aussi utiliser http:// sur exactement localhost, 127.0.0.1 ou [::1], sur n’importe quel port." + }, + type: { + label: "Type d’application", + helpText: "Choisissez Native pour une application de bureau ou mobile, ou pour une application en développement local qui redirige vers http://localhost.", + options: { + web: "Web", + native: "Native" + } + } + }, + resultDialog: { + title: "Application OAuth enregistrée", + description: "Enregistrez le client_secret maintenant — il n’est affiché qu’une seule fois et ne peut pas être récupéré. Vous pourrez le renouveler plus tard en cas de fuite.", + acknowledge: "J’ai enregistré le secret client", + fields: { + "client.client_id": "ID client", + "client.client_secret": "Secret client" + } + } + }, + rotate_client_secret: { + label: "Renouveler le secret client", + description: "Renouveler le secret de ce client OAuth ? L’ancien secret cessera immédiatement de fonctionner et toutes les intégrations qui l’utilisent seront interrompues jusqu’à leur mise à jour avec le nouveau secret. Le nouveau secret n’est affiché qu’une seule fois.", + resultDialog: { + title: "Secret client renouvelé", + description: "Enregistrez le nouveau secret maintenant — il n’est affiché qu’une seule fois. L’ancien secret a déjà cessé de fonctionner : mettez à jour chaque intégration avec le nouveau secret.", + acknowledge: "J’ai mis à jour mes intégrations", + fields: { + client_secret: "Nouveau secret client" + } + } + }, + delete_oauth_application: { + label: "Supprimer l’application OAuth", + description: "Supprimer définitivement cette application OAuth ? Tous les jetons et consentements émis seront invalidés et les intégrations qui utilisent ce client_id cesseront immédiatement de fonctionner. Cette action est irréversible.", + successMessage: "Application OAuth supprimée" + } + } + }, + sys_oauth_access_token: { + label: "Jeton d’accès OAuth", + pluralLabel: "Jetons d’accès OAuth", + description: "Jetons d’accès OAuth opaques émis pour les applications clientes", + fields: { + id: { + label: "ID" + }, + token: { + label: "Jeton", + help: "Valeur opaque du jeton d’accès" + }, + client_id: { + label: "ID client", + help: "L’ID client de l’application OAuth pour laquelle ce jeton a été émis." + }, + session_id: { + label: "Session", + help: "La session de connexion dans laquelle ce jeton a été émis." + }, + user_id: { + label: "Utilisateur", + help: "L’utilisateur pour le compte duquel agit ce jeton." + }, + refresh_id: { + label: "Jeton d’actualisation", + help: "Le jeton d’actualisation à partir duquel ce jeton d’accès a été émis." + }, + reference_id: { + label: "ID de référence", + help: "Organisation liée par le consentement sous lequel ce jeton a été émis ; vide lorsqu’aucune n’a été liée" + }, + authorization_code_id: { + label: "ID du code d’autorisation", + help: "ID de l’octroi par code d’autorisation dont provient ce jeton" + }, + resources: { + label: "Ressources", + help: "Liste sérialisée en JSON des indicateurs de ressource RFC 8707 liés à ce jeton" + }, + requested_user_info_claims: { + label: "Revendications UserInfo demandées", + help: "Liste sérialisée en JSON des revendications OIDC demandées pour le point de terminaison userinfo" + }, + scopes: { + label: "Portées", + help: "Liste sérialisée en JSON des portées accordées à ce jeton" + }, + expires_at: { + label: "Expire le" + }, + created_at: { + label: "Créé le" + }, + revoked: { + label: "Révoqué le", + help: "Horodatage de la révocation de ce jeton d’accès" + }, + confirmation: { + label: "Confirmation", + help: "Revendication cnf JSON RFC 7800 (par ex. empreinte de clé DPoP) qui lie ce jeton à une clé" + } + } + }, + sys_oauth_refresh_token: { + label: "Jeton d’actualisation OAuth", + pluralLabel: "Jetons d’actualisation OAuth", + description: "Jetons d’actualisation OAuth opaques (liés à une session)", + fields: { + id: { + label: "ID" + }, + token: { + label: "Jeton", + help: "Valeur opaque du jeton d’actualisation" + }, + client_id: { + label: "ID client", + help: "L’ID client de l’application OAuth pour laquelle ce jeton a été émis." + }, + session_id: { + label: "Session", + help: "La session de connexion dans laquelle ce jeton a été émis." + }, + user_id: { + label: "Utilisateur", + help: "L’utilisateur pour le compte duquel agit ce jeton." + }, + reference_id: { + label: "ID de référence", + help: "Organisation liée par le consentement à l’origine de cette famille de jetons d’actualisation ; vide lorsqu’aucune n’a été liée" + }, + authorization_code_id: { + label: "ID du code d’autorisation", + help: "ID de l’octroi par code d’autorisation dont provient cette chaîne de jetons" + }, + resources: { + label: "Ressources", + help: "Liste sérialisée en JSON des indicateurs de ressource RFC 8707 liés à ce jeton" + }, + requested_user_info_claims: { + label: "Revendications UserInfo demandées", + help: "Liste sérialisée en JSON des revendications OIDC demandées pour le point de terminaison userinfo" + }, + scopes: { + label: "Portées", + help: "Liste sérialisée en JSON des portées accordées à ce jeton" + }, + expires_at: { + label: "Expire le" + }, + created_at: { + label: "Créé le" + }, + revoked: { + label: "Révoqué le", + help: "Horodatage de la révocation de ce jeton d’actualisation" + }, + rotated_at: { + label: "Renouvelé le", + help: "Horodatage du renouvellement de ce jeton (remplacé par une nouvelle ligne)" + }, + rotation_replay_response: { + label: "Réponse rejouée après renouvellement", + help: "Réponse de jeton mise en cache, rejouée lorsque l’ancien jeton est présenté de nouveau pendant l’intervalle de réutilisation" + }, + rotation_replay_expires_at: { + label: "Expiration du rejeu après renouvellement", + help: "Fin de l’intervalle de réutilisation suivant le renouvellement, pendant lequel la réponse rejouée est servie" + }, + auth_time: { + label: "Heure d’authentification", + help: "Moment où l’utilisateur s’est initialement authentifié pour cette chaîne de jetons" + }, + confirmation: { + label: "Confirmation", + help: "Revendication cnf JSON RFC 7800 (par ex. empreinte de clé DPoP) qui lie ce jeton à une clé" + } + } + }, + sys_oauth_consent: { + label: "Consentement OAuth", + pluralLabel: "Consentements OAuth", + description: "Enregistrements de consentement des utilisateurs pour les applications clientes OAuth", + fields: { + id: { + label: "ID" + }, + client_id: { + label: "ID client", + help: "L’ID client de l’application OAuth à laquelle l’utilisateur a consenti." + }, + user_id: { + label: "Utilisateur", + help: "L’utilisateur qui a donné ce consentement." + }, + reference_id: { + label: "ID de référence", + help: "Organisation que lie ce consentement : l’organisation active de l’utilisateur au moment du consentement ; vide lorsqu’aucune n’était active" + }, + resources: { + label: "Ressources", + help: "Liste sérialisée en JSON des indicateurs de ressource RFC 8707 couverts par le consentement" + }, + requested_user_info_claims: { + label: "Revendications UserInfo demandées", + help: "Liste sérialisée en JSON des revendications OIDC que l’utilisateur a accepté d’exposer" + }, + scopes: { + label: "Portées", + help: "Liste sérialisée en JSON des portées auxquelles l’utilisateur a consenti" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + }, + last_used_at: { + label: "Dernière utilisation le", + help: "Début de l’heure la plus récente au cours de laquelle le client a effectué un appel MCP vérifié sous ce consentement ; vide lorsqu’aucun n’a été enregistré" + } + } + }, + sys_oauth_resource: { + label: "Ressource OAuth", + pluralLabel: "Ressources OAuth", + description: "Ressources protégées OAuth enregistrées (indicateurs de ressource RFC 8707)", + fields: { + id: { + label: "ID" + }, + identifier: { + label: "Identifiant", + help: "URI d’indicateur de ressource présentée dans le paramètre resource de la RFC 8707" + }, + name: { + label: "Nom" + }, + access_token_ttl: { + label: "Durée de vie du jeton d’accès", + help: "Durée de vie du jeton d’accès en secondes pour cette ressource (remplace la valeur par défaut du serveur)" + }, + refresh_token_ttl: { + label: "Durée de vie du jeton d’actualisation", + help: "Durée de vie du jeton d’actualisation en secondes pour cette ressource (remplace la valeur par défaut du serveur)" + }, + signing_algorithm: { + label: "Algorithme de signature", + help: "Algorithme JWS utilisé pour signer les jetons d’accès de cette ressource" + }, + signing_key_id: { + label: "ID de la clé de signature", + help: "ID de clé (kid) utilisé pour signer les jetons d’accès de cette ressource" + }, + allowed_scopes: { + label: "Portées autorisées", + help: "Liste sérialisée en JSON des portées que les clients peuvent demander pour cette ressource" + }, + custom_claims: { + label: "Revendications personnalisées", + help: "Objet JSON de revendications supplémentaires ajoutées aux jetons d’accès de cette ressource" + }, + dpop_bound_access_tokens_required: { + label: "DPoP obligatoire", + help: "Exige que les jetons d’accès de cette ressource soient liés à DPoP (RFC 9449)" + }, + disabled: { + label: "Désactivée" + }, + policy_version: { + label: "Version de la politique", + help: "Version monotone de la politique de jetons de la ressource" + }, + metadata: { + label: "Métadonnées", + help: "Objet JSON de métadonnées supplémentaires de la ressource" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + } + }, + sys_oauth_client_resource: { + label: "Ressource de client OAuth", + pluralLabel: "Ressources de client OAuth", + description: "Octrois permettant à un client OAuth de demander des jetons pour une ressource protégée", + fields: { + id: { + label: "ID" + }, + client_id: { + label: "ID client", + help: "L’ID client de l’application OAuth à laquelle s’applique cet octroi." + }, + resource_id: { + label: "ID de la ressource", + help: "L’identifiant de la ressource protégée pour laquelle l’application peut demander des jetons." + }, + metadata: { + label: "Métadonnées", + help: "Objet JSON de métadonnées supplémentaires de l’octroi" + }, + created_at: { + label: "Créé le" + } + } + }, + sys_oauth_client_assertion: { + label: "Assertion de client OAuth", + pluralLabel: "Assertions de client OAuth", + description: "JTI d’assertions de client OAuth consommés (prévention du rejeu RFC 7523)", + fields: { + id: { + label: "ID" + }, + expires_at: { + label: "Expire le", + help: "Expiration de l’assertion — les lignes dépassant cet instant peuvent être purgées sans risque" + } + } + }, + sys_jwks: { + label: "Clé JWKS", + pluralLabel: "Clés JWKS", + description: "Paires de clés asymétriques utilisées pour signer et vérifier les JWT émis", + fields: { + id: { + label: "ID de la clé", + help: "Valeur `kid` de la JWK" + }, + public_key: { + label: "Clé publique", + help: "Clé publique JWK sérialisée en JSON" + }, + private_key: { + label: "Clé privée", + help: "Clé privée JWK sérialisée en JSON (chiffrée au repos)" + }, + alg: { + label: "Algorithme", + help: "Algorithme de signature JWK, par ex. `EdDSA` (better-auth 1.7+)" + }, + crv: { + label: "Courbe", + help: "Courbe JWK des clés EdDSA/EC, par ex. `Ed25519` (better-auth 1.7+)" + }, + created_at: { + label: "Créé le" + }, + expires_at: { + label: "Expire le", + help: "Date à partir de laquelle la clé ne peut plus servir à vérifier des jetons" + } + } + }, + sys_sso_provider: { + label: "Fournisseur SSO", + pluralLabel: "Fournisseurs SSO", + description: "Fournisseurs d’identité SSO externes (OIDC / SAML) vers lesquels ce système fédère la connexion", + fields: { + id: { + label: "ID" + }, + provider_id: { + label: "ID du fournisseur", + help: "Identifiant stable du fournisseur (unique au sein de l’environnement)" + }, + issuer: { + label: "Émetteur", + help: "URL de l’émetteur de l’IdP" + }, + domain: { + label: "Domaine de messagerie", + help: "Domaine de messagerie acheminé vers cet IdP (par ex. acme.com)" + }, + domain_verified: { + label: "Domaine vérifié", + help: "Indique si la propriété DNS du domaine de messagerie a été prouvée. Définie par « Vérifier le domaine » une fois l’enregistrement DNS TXT résolu. Gérée par better-auth — non modifiable directement. Appliquée uniquement lorsque la vérification de domaine est activée pour l’environnement." + }, + oidc_config: { + label: "Configuration OIDC", + help: "JSON : clientId, clientSecret, points de terminaison, portées, mappage, pkce (géré par better-auth)" + }, + saml_config: { + label: "Configuration SAML", + help: "JSON : entryPoint, cert, identifierFormat, mappage (géré par better-auth)" + }, + user_id: { + label: "Enregistré par", + help: "Utilisateur qui a enregistré ce fournisseur" + }, + organization_id: { + label: "Organisation", + help: "Portée de l’organisation (lorsque le SSO limité à une organisation est utilisé)" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + }, + _views: { + all: { + label: "Tous", + emptyState: { + title: "Aucun fournisseur SSO pour le moment", + message: "Enregistrez l’IdP externe de votre organisation — OIDC (Okta, Entra, Auth0, …) avec « Enregistrer un fournisseur SSO », ou SAML 2.0 avec « Enregistrer un fournisseur SAML ». Les membres dont le domaine de messagerie correspond peuvent ensuite se connecter par son intermédiaire." + } + } + }, + _actions: { + register_sso_provider: { + label: "Enregistrer un fournisseur SSO", + params: { + providerId: { + label: "ID du fournisseur", + helpText: "Identifiant stable, par ex. « okta » ou « acme-entra »." + }, + issuer: { + label: "URL de l’émetteur", + helpText: "Émetteur de l’IdP, par ex. https://acme.okta.com. Le document de découverte est récupéré depuis cette adresse, sauf si une URL explicite est indiquée ci-dessous." + }, + domain: { + label: "Domaine de messagerie", + helpText: "Les utilisateurs de ce domaine de messagerie sont acheminés vers cet IdP, par ex. acme.com." + }, + clientId: { + label: "ID client", + helpText: "ID client OAuth émis par l’IdP pour ce système." + }, + clientSecret: { + label: "Secret client", + helpText: "Secret client OAuth (stocké chiffré par better-auth)." + }, + discoveryEndpoint: { + label: "URL de découverte", + helpText: "Facultatif. URL du document de découverte OIDC. Laissez vide pour utiliser `/.well-known/openid-configuration`." + }, + scopes: { + label: "Portées", + helpText: "Facultatif. Portées OAuth séparées par des espaces ou des virgules. Par défaut : « openid email profile ».", + placeholder: "openid email profile" + }, + mapEmail: { + label: "Mappage : revendication e-mail", + helpText: "Facultatif. Revendication associée à l’e-mail. Par défaut : « email ».", + placeholder: "email" + }, + mapName: { + label: "Mappage : revendication nom", + helpText: "Facultatif. Revendication associée au nom d’affichage. Par défaut : « name ».", + placeholder: "name" + } + } + }, + register_saml_provider: { + label: "Enregistrer un fournisseur SAML", + params: { + providerId: { + label: "ID du fournisseur", + helpText: "Identifiant stable, par ex. « acme-saml »." + }, + issuer: { + label: "ID d’entité de l’IdP", + helpText: "L’EntityID SAML de l’IdP (émetteur), par ex. https://saml.acme.com/entityid." + }, + domain: { + label: "Domaine de messagerie", + helpText: "Les utilisateurs de ce domaine de messagerie sont acheminés vers cet IdP, par ex. acme.com." + }, + entryPoint: { + label: "URL SSO de l’IdP", + helpText: "Le point de terminaison d’authentification unique SAML (redirection) de l’IdP qui reçoit la SAMLRequest." + }, + cert: { + label: "Certificat de signature de l’IdP", + helpText: "Le certificat de signature X.509 de l’IdP (corps PEM). Sert à vérifier les signatures des assertions." + }, + identifierFormat: { + label: "Format du NameID", + helpText: "Facultatif. Format de NameID SAML demandé. Par défaut, le format configuré sur l’IdP.", + placeholder: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + } + } + }, + request_domain_verification: { + label: "Demander la vérification du domaine", + resultDialog: { + title: "Vérifiez votre domaine", + description: "Ajoutez l’enregistrement DNS TXT ci-dessous chez le fournisseur DNS de votre domaine, puis exécutez « Vérifier le domaine ». Le jeton n’est affiché qu’une seule fois.", + acknowledge: "Terminé", + fields: { + dnsRecordType: "Type d’enregistrement", + dnsRecordName: "Nom / hôte", + dnsRecordValue: "Valeur" + } + } + }, + verify_domain: { + label: "Vérifier le domaine", + successMessage: "Propriété du domaine vérifiée" + }, + delete_sso_provider: { + label: "Supprimer le fournisseur SSO", + description: "Supprimer ce fournisseur SSO ? Les utilisateurs de son domaine ne pourront plus se connecter par son intermédiaire.", + successMessage: "Fournisseur SSO supprimé" + } + } + }, + sys_scim_connection_binding: { + label: "Liaison de connexion SCIM", + pluralLabel: "Liaisons de connexion SCIM", + description: "État du cycle de vie d’une connexion de provisionnement SCIM, y compris sa machine à états de mise hors service", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "ID de connexion", + help: "Identifiant immuable de la connexion SCIM qui délimite chaque ressource provisionnée" + }, + connection_key: { + label: "Clé de connexion", + help: "Clé d’unicité de connexion dérivée, maintenue par @better-auth/scim ; ne pas écrire directement." + }, + provisioning_domain_id: { + label: "Domaine de provisionnement", + help: "Périmètre appartenant à l’application qui reçoit les ressources provisionnées (par défaut, l’ID de connexion)" + }, + decommission_status: { + label: "Statut de mise hors service", + help: "État du cycle de vie de la connexion : active | reconciling | complete" + }, + decommissioned_at: { + label: "Mise hors service le" + }, + decommission_cursor_user_id: { + label: "Utilisateur du curseur de mise hors service", + help: "Curseur de réconciliation reprenable, maintenu par la bibliothèque" + }, + decommission_reconciled_user_count: { + label: "Utilisateurs réconciliés" + }, + decommission_batch_count: { + label: "Lots de mise hors service" + }, + decommission_revision: { + label: "Révision de mise hors service", + help: "Révision de concurrence optimiste de la machine à états de mise hors service" + }, + decommission_completed_at: { + label: "Mise hors service terminée le" + }, + decommission_lease_id: { + label: "Bail de mise hors service", + help: "Bail de réconciliation à travailleur unique, maintenu par la bibliothèque" + }, + decommission_lease_expires_at: { + label: "Expiration du bail de mise hors service" + }, + created_at: { + label: "Créé le" + } + }, + _views: { + all: { + label: "Toutes" + } + } + }, + sys_scim_connection_credential: { + label: "Identifiant de connexion SCIM", + pluralLabel: "Identifiants de connexion SCIM", + description: "Identifiants de type bearer (empreintes à sens unique) qui authentifient les connexions de provisionnement SCIM", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "ID de connexion", + help: "La connexion SCIM que cet identifiant authentifie (par ex. « okta-prod ») ; délimite chaque ressource que l’IdP provisionne avec lui" + }, + provisioning_domain_id: { + label: "Domaine de provisionnement", + help: "Périmètre appartenant à l’application qui reçoit les ressources provisionnées ; par défaut, l’ID de connexion s’il est absent" + }, + organization_id: { + label: "Organisation", + help: "Portée de l’organisation de cette connexion, lorsque le provisionnement est limité à une organisation" + }, + label: { + label: "Libellé", + help: "Nom de cet identifiant destiné aux opérateurs (par ex. « rotation 2026-Q3 »)" + }, + token_digest: { + label: "Empreinte du jeton", + help: "HMAC-SHA-256 (base64url) du jeton bearer, avec pour clé le secret d’authentification du déploiement — à sens unique ; le texte en clair n’est affiché qu’une fois lors de la génération et n’est jamais stocké." + }, + active: { + label: "Actif", + help: "Interrupteur de révocation — un identifiant inactif est refusé lors de la vérification" + }, + expires_at: { + label: "Expire le", + help: "Expiration ferme facultative pour une rotation progressive des identifiants ; un identifiant expiré est refusé" + }, + user_id: { + label: "Généré par", + help: "Utilisateur qui a généré cet identifiant" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + }, + _views: { + all: { + label: "Tous" + } + }, + _actions: { + mint_scim_credential: { + label: "Générer un identifiant", + description: "Générez un identifiant bearer pour une connexion SCIM. Fournissez-le au fournisseur d’identité comme jeton SCIM ; il n’est affiché qu’une seule fois.", + params: { + connectionId: { + label: "ID de connexion", + helpText: "La connexion SCIM que cet identifiant authentifie, par ex. « okta-prod ». 246 caractères au maximum. Pour effectuer une rotation, générez un second identifiant pour la même connexion, basculez le fournisseur d’identité dessus, puis révoquez l’ancien." + }, + label: { + label: "Libellé", + helpText: "Facultatif. Un nom pour cet identifiant, par ex. « rotation 2026-Q3 »." + }, + organizationId: { + label: "Organisation", + helpText: "Facultatif. L’organisation propriétaire de la connexion. Laissez vide pour l’organisation par défaut." + }, + provisioningDomainId: { + label: "Domaine de provisionnement", + helpText: "Facultatif. Le périmètre qui reçoit les utilisateurs et groupes provisionnés. Laissez vide pour utiliser l’ID de connexion." + }, + expiresAt: { + label: "Expire le", + helpText: "Facultatif. Passé ce moment, l’identifiant est refusé. Laissez vide pour un identifiant qui n’expire pas." + } + }, + resultDialog: { + title: "Identifiant SCIM généré", + description: "Copiez le jeton maintenant — il n’est affiché qu’une seule fois et ne peut pas être récupéré. Collez-le dans le fournisseur d’identité comme jeton bearer SCIM. En cas de fuite, révoquez-le et générez-en un nouveau.", + acknowledge: "J’ai enregistré le jeton", + fields: { + token: "Jeton", + connectionId: "ID de connexion", + expiresAt: "Expire le" + } + } + }, + revoke_scim_credential: { + label: "Révoquer l’identifiant", + confirmText: "Révoquer cet identifiant SCIM ? Le fournisseur d’identité qui l’utilise est refusé dès sa prochaine requête. Cette action est irréversible : pour rétablir le provisionnement, générez un nouvel identifiant.", + successMessage: "Identifiant SCIM révoqué" + } + } + }, + sys_scim_group: { + label: "Groupe SCIM", + pluralLabel: "Groupes SCIM", + description: "Groupes SCIM 2.0 envoyés par l’IdP, délimités par connexion de provisionnement", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "ID de connexion" + }, + provisioning_domain_id: { + label: "Domaine de provisionnement" + }, + revision: { + label: "Révision", + help: "Révision de concurrence optimiste maintenue par @better-auth/scim" + }, + display_name: { + label: "Nom d’affichage", + help: "displayName SCIM tel qu’envoyé par l’IdP" + }, + display_name_key: { + label: "Clé du nom d’affichage", + help: "Clé d’unicité dérivée du displayName normalisé en casse, maintenue par @better-auth/scim ; ne pas écrire directement." + }, + external_id: { + label: "ID externe", + help: "externalId attribué par l’IdP, lorsque l’IdP en envoie un" + }, + external_id_key: { + label: "Clé d’ID externe", + help: "Clé d’unicité dérivée de l’externalId, maintenue par @better-auth/scim ; ne pas écrire directement." + }, + order_key: { + label: "Clé d’ordre", + help: "Clé de pagination stable dérivée, maintenue par @better-auth/scim ; ne pas écrire directement." + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + }, + _views: { + all: { + label: "Tous" + } + } + }, + sys_scim_group_member: { + label: "Membre de groupe SCIM", + pluralLabel: "Membres de groupe SCIM", + description: "Lignes d’appartenance aux groupes SCIM envoyées par l’IdP (groupe ↔ utilisateur provisionné)", + fields: { + id: { + label: "ID" + }, + display_title: { + label: "Titre", + help: "Titre de l’enregistrement : l’utilisateur provisionné et le groupe (calculé à la lecture)" + }, + connection_id: { + label: "ID de connexion" + }, + group_id: { + label: "Groupe" + }, + scim_user_id: { + label: "Utilisateur SCIM" + }, + membership_key: { + label: "Clé d’appartenance", + help: "Clé d’unicité d’appartenance dérivée, maintenue par @better-auth/scim ; ne pas écrire directement." + }, + created_at: { + label: "Créé le" + } + }, + _views: { + all: { + label: "Tous" + } + } + }, + sys_scim_identity_tombstone: { + label: "Pierre tombale d’identité SCIM", + pluralLabel: "Pierres tombales d’identité SCIM", + description: "Pierres tombales des identités SCIM supprimées par l’IdP, conservées pour qu’un nouveau provisionnement soit reconnu", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "ID de connexion" + }, + provisioning_domain_id: { + label: "Domaine de provisionnement" + }, + external_id: { + label: "ID externe", + help: "L’externalId attribué par l’IdP à l’identité supprimée" + }, + external_id_key: { + label: "Clé d’ID externe", + help: "Clé d’unicité dérivée de l’externalId, maintenue par @better-auth/scim ; ne pas écrire directement." + }, + user_id: { + label: "Utilisateur", + help: "L’utilisateur de la plateforme auquel l’identité supprimée était liée" + }, + profile: { + label: "Profil final", + help: "Profil SCIM final sérialisé au moment de la suppression, maintenu par @better-auth/scim" + }, + deleted_at: { + label: "Supprimé le" + } + }, + _views: { + all: { + label: "Toutes" + } + } + }, + sys_scim_projection_grant: { + label: "Octroi de projection SCIM", + pluralLabel: "Octrois de projection SCIM", + description: "Octrois de rôles/droits projetés sur les utilisateurs de la plateforme par le provisionnement SCIM", + fields: { + id: { + label: "ID" + }, + display_title: { + label: "Titre", + help: "Titre de l’enregistrement : le rôle projeté et l’utilisateur auquel il est accordé (calculé à la lecture)" + }, + connection_id: { + label: "ID de connexion" + }, + provisioning_domain_id: { + label: "Domaine de provisionnement" + }, + scim_user_id: { + label: "Utilisateur SCIM", + help: "La projection d’utilisateur par connexion à laquelle appartient cet octroi" + }, + user_id: { + label: "Utilisateur", + help: "L’utilisateur de la plateforme sur lequel l’octroi est projeté" + }, + source_kind: { + label: "Nature de la source", + help: "Ce qui a projeté l’octroi (par ex. groupe, droit, attribut)" + }, + source_id: { + label: "ID de la source", + help: "Identifiant de la source de projection (par ex. l’ID du groupe SCIM)" + }, + source_value: { + label: "Valeur de la source", + help: "Valeur de l’attribut source, lorsque la nature de la source en porte une" + }, + role: { + label: "Rôle", + help: "Le rôle/droit projeté sur l’utilisateur" + }, + grant_key: { + label: "Clé d’octroi", + help: "Clé d’unicité d’octroi dérivée, maintenue par @better-auth/scim ; ne pas écrire directement." + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + }, + _views: { + all: { + label: "Tous" + } + } + }, + sys_scim_subject: { + label: "Sujet SCIM", + pluralLabel: "Sujets SCIM", + description: "Lien de provisionnement SCIM par utilisateur — une ligne par utilisateur provisionné par une connexion SCIM quelconque", + fields: { + id: { + label: "ID" + }, + user_id: { + label: "Utilisateur", + help: "L’utilisateur de la plateforme suivi par cette ligne de sujet (unique — une ligne par utilisateur)" + }, + profile_source_id: { + label: "Source du profil", + help: "La projection sys_scim_user qui alimente actuellement le profil de cet utilisateur" + }, + revision: { + label: "Révision", + help: "Révision de concurrence optimiste maintenue par @better-auth/scim" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + }, + _views: { + all: { + label: "Tous" + } + } + }, + sys_scim_user: { + label: "Utilisateur SCIM", + pluralLabel: "Utilisateurs SCIM", + description: "Projection par connexion d’un utilisateur provisionné par l’IdP (SCIM 2.0 /Users)", + fields: { + id: { + label: "ID" + }, + connection_id: { + label: "ID de connexion", + help: "Connexion SCIM qui a provisionné cet utilisateur" + }, + provisioning_domain_id: { + label: "Domaine de provisionnement" + }, + user_id: { + label: "Utilisateur", + help: "L’utilisateur de la plateforme en lequel cette projection s’est matérialisée" + }, + connection_user_key: { + label: "Clé connexion-utilisateur", + help: "Clé d’unicité (connexion, utilisateur) dérivée, maintenue par @better-auth/scim ; ne pas écrire directement." + }, + user_name: { + label: "Nom d’utilisateur", + help: "userName SCIM tel qu’envoyé par l’IdP" + }, + user_name_key: { + label: "Clé du nom d’utilisateur", + help: "Clé d’unicité dérivée du userName normalisé en casse, maintenue par @better-auth/scim ; ne pas écrire directement." + }, + primary_email: { + label: "E-mail principal" + }, + work_email_value_index: { + label: "Index de l’e-mail professionnel", + help: "Index de recherche dérivé, maintenu par @better-auth/scim ; ne pas écrire directement." + }, + email_value_index: { + label: "Index de l’e-mail", + help: "Index de recherche dérivé, maintenu par @better-auth/scim ; ne pas écrire directement." + }, + display_name: { + label: "Nom d’affichage" + }, + formatted_name: { + label: "Nom formaté" + }, + given_name: { + label: "Prénom" + }, + family_name: { + label: "Nom de famille" + }, + serialized_emails: { + label: "E-mails (sérialisés)", + help: "Liste canonique sérialisée des e-mails SCIM, maintenue par @better-auth/scim ; ne pas écrire directement." + }, + serialized_attributes: { + label: "Attributs (sérialisés)", + help: "Attributs SCIM canoniques sérialisés, maintenus par @better-auth/scim ; ne pas écrire directement." + }, + external_id: { + label: "ID externe", + help: "externalId attribué par l’IdP, lorsque l’IdP en envoie un" + }, + external_id_key: { + label: "Clé d’ID externe", + help: "Clé d’unicité dérivée de l’externalId, maintenue par @better-auth/scim ; ne pas écrire directement." + }, + active: { + label: "Actif", + help: "Indicateur active de SCIM — false signifie que l’IdP a désactivé cet utilisateur" + }, + order_key: { + label: "Clé d’ordre", + help: "Clé de pagination stable dérivée, maintenue par @better-auth/scim ; ne pas écrire directement." + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + }, + _views: { + all: { + label: "Tous" + } + } + }, + sys_notification: { + label: "Événement de notification", + pluralLabel: "Événements de notification", + description: "Toutes les notifications envoyées par la plateforme, une ligne par événement.", + fields: { + id: { + label: "ID de la notification" + }, + topic: { + label: "Sujet", + help: "Sujet de la notification, par ex. task.assigned, collab.mention" + }, + payload: { + label: "Charge utile", + help: "Entrées de modèle transmises aux canaux (title/body/url/actor/source/…)" + }, + severity: { + label: "Gravité", + help: "Indication de gravité pour l’affichage / le filtrage", + options: { + info: "Information", + warning: "Avertissement", + critical: "Critique" + } + }, + suppressed_channels: { + label: "Canaux supprimés", + help: "Canaux ignorés lors de la diffusion parce qu’ils ne sont pas disponibles pour ce locataire, sous la forme [{channel, reason}] ; reason appartient à l’ensemble fermé : transport_not_configured" + }, + dedup_key: { + label: "Clé de dédoublonnage", + help: "Clé d’idempotence dans une fenêtre de sujet ; une émission répétée n’a aucun effet" + }, + source_object: { + label: "Objet source", + help: "Nom d’objet de l’enregistrement associé (par ex. lead, opportunity)" + }, + source_id: { + label: "Enregistrement source", + help: "ID de l’enregistrement dans source_object" + }, + actor_id: { + label: "Auteur de l’action", + help: "Utilisateur à l’origine de l’événement (auteur de la mention, de l’attribution)" + }, + created_at: { + label: "Créé le" + } + }, + _views: { + recent: { + label: "Récents", + emptyState: { + title: "Aucun événement", + message: "Aucun événement de notification n’a été émis." + } + }, + by_topic: { + label: "Par sujet" + } + } + }, + sys_attachment: { + label: "Pièce jointe", + pluralLabel: "Pièces jointes", + description: "Lien polymorphe entre un sys_file et n’importe quel autre enregistrement", + fields: { + id: { + label: "ID de la pièce jointe" + }, + parent_object: { + label: "Objet parent", + help: "Nom court de l’objet de l’enregistrement auquel le fichier est joint (par ex. account, lead)" + }, + parent_id: { + label: "Enregistrement parent", + help: "Clé primaire de l’enregistrement auquel le fichier est joint" + }, + file_id: { + label: "Fichier", + help: "L’entrée de stockage sys_file jointe" + }, + file_name: { + label: "Nom du fichier", + help: "Copie dénormalisée de sys_file.name pour un affichage rapide des listes" + }, + mime_type: { + label: "Type MIME" + }, + size: { + label: "Taille (octets)" + }, + uploaded_by: { + label: "Téléversé par" + }, + description: { + label: "Description" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + } + }, + sys_email: { + label: "E-mail", + pluralLabel: "E-mails", + description: "Journal d’envoi des e-mails sortants", + fields: { + id: { + label: "ID de l’e-mail" + }, + message_id: { + label: "Message-ID", + help: "Message-ID RFC-5322 attribué par le transport" + }, + from_address: { + label: "De" + }, + to_addresses: { + label: "À", + help: "Adresses des destinataires séparées par des virgules" + }, + cc_addresses: { + label: "Cc" + }, + bcc_addresses: { + label: "Cci" + }, + reply_to: { + label: "Reply-To" + }, + subject: { + label: "Objet" + }, + body_text: { + label: "Corps (texte)" + }, + body_html: { + label: "Corps (HTML)" + }, + headers_json: { + label: "En-têtes (JSON)", + help: "En-têtes personnalisés fournis avec le message lors de son envoi, sous forme d’objet JSON nom → valeur. Écrits dans les deux modes d’envoi (ils constituent une preuve d’audit autant qu’une donnée d’envoi). Absents des lignes écrites avant l’existence de cette colonne, qui se relisent comme « aucun en-tête personnalisé ». Jamais renvoyés par le chemin de données générique — les en-têtes sont l’endroit habituel où se trouve un identifiant secret ; les chemins d’envoi les récupèrent via l’accesseur privilégié du moteur." + }, + attachments_json: { + label: "Pièces jointes (JSON)", + help: "Pièces jointes sous forme de tableau JSON de { filename, contentType?, size, hash, cid?, contentForm, inline?, storageKey?, contentReclaimedAt? }. Un contenu jusqu’au budget de plugin-email (SYS_EMAIL_ATTACHMENT_LIMIT_BYTES, 256 Kio bruts au total — environ 350 Ko de base64 au pire) est encodé en base64 dans `inline` ; un contenu plus volumineux est confié à la capacité de stockage de fichiers et l’élément porte `storageKey` à la place, de sorte que la ligne reste bornée dans les deux cas. filename/contentType/size/hash sont des preuves d’audit PERMANENTES ; le contenu hors ligne est un artefact d’envoi, supprimé après un délai de grâce une fois que la ligne a atteint un état final, moment auquel `storageKey` est remplacé par `contentReclaimedAt`." + }, + status: { + label: "Statut", + help: "État du cycle de vie — queued lorsque le message est soumis pour envoi, avant la première tentative d’envoi", + options: { + queued: "en file d’attente", + sent: "envoyé", + failed: "échec" + } + }, + error: { + label: "Erreur", + help: "Pourquoi le message a échoué — une erreur de transport, ou la validation qui l’a rejeté avant l’envoi." + }, + attempt_count: { + label: "Tentatives", + help: "Nombre de tentatives d’envoi effectuées par le service" + }, + sent_at: { + label: "Envoyé le", + help: "Renseigné lorsque le statut passe à « sent »" + }, + related_object: { + label: "Objet associé", + help: "Nom d’objet de l’enregistrement associé (par ex. lead, opportunity)" + }, + related_id: { + label: "Enregistrement associé", + help: "ID de l’enregistrement dans related_object" + }, + sent_by: { + label: "Envoyé par" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + } + }, + sys_email_template: { + label: "Modèle d’e-mail", + pluralLabel: "Modèles d’e-mail", + description: "Modèle d’e-mail sortant (objet + corps + variables) résolu par nom et langue", + fields: { + id: { + label: "ID" + }, + name: { + label: "Nom", + help: "Identifiant snake_case à points (par ex. auth.password_reset)" + }, + label: { + label: "Libellé", + help: "Nom lisible affiché dans Studio" + }, + category: { + label: "Catégorie", + options: { + auth: "authentification", + notification: "notification", + workflow: "workflow", + marketing: "marketing", + custom: "personnalisé" + } + }, + locale: { + label: "Langue", + help: "Étiquette de langue BCP-47 (en-US, zh-CN, …)" + }, + description: { + label: "Description" + }, + subject: { + label: "Objet", + help: "Modèle de l’objet ; prend en charge les espaces réservés {{var.path}}" + }, + body_html: { + label: "Corps HTML", + help: "Modèle du corps HTML ; prend en charge les espaces réservés {{var.path}}" + }, + body_text: { + label: "Corps en texte brut", + help: "Variante facultative en texte brut (dérivée automatiquement du HTML si vide)" + }, + from_name: { + label: "Nom de l’expéditeur" + }, + from_address: { + label: "Adresse de l’expéditeur" + }, + reply_to: { + label: "Reply-To" + }, + active: { + label: "Actif" + }, + is_system: { + label: "Modèle système", + help: "Fourni par un plugin / la plateforme" + }, + variables_json: { + label: "Variables (JSON)", + help: "Tableau JSON de {name,type,required,description}" + }, + managed_by: { + label: "Géré par", + help: "Provenance de l’enregistrement : platform = intégré au framework / package = déclaré par une application ou un package (amorcé au démarrage à partir des métadonnées email_template déclarées) / admin = créé dans Studio.", + options: { + platform: "plateforme", + package: "package", + admin: "administrateur" + } + }, + customized: { + label: "Personnalisé", + help: "Défini sur un modèle déclaré par un package qu’un administrateur a modifié avant la fermeture de l’édition des modèles au niveau de l’organisation ; l’amorçage au démarrage n’écrase jamais une telle ligne. Plus rien ne le définit désormais. Sans signification sur les lignes admin." + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + } + }, + sys_job: { + label: "Tâche en arrière-plan", + pluralLabel: "Tâches en arrière-plan", + description: "Catalogue des tâches en arrière-plan enregistrées", + fields: { + id: { + label: "ID de la tâche" + }, + name: { + label: "Nom de la tâche", + help: "Identifiant unique de la tâche (snake_case), unique sur l’ensemble de l’installation — le catalogue des tâches est une propriété du déploiement, et non d’une organisation" + }, + schedule_type: { + label: "Type de planification", + options: { + cron: "cron", + interval: "intervalle", + once: "une fois" + } + }, + schedule_expression: { + label: "Expression", + help: "Expression cron / intervalle en ms / date-heure ISO" + }, + timezone: { + label: "Fuseau horaire" + }, + active: { + label: "Active", + help: "Indique si le planificateur exécute actuellement cette tâche" + }, + last_run_at: { + label: "Dernière exécution le" + }, + last_status: { + label: "Dernier statut", + options: { + success: "réussi", + failed: "échec", + timeout: "délai dépassé", + running: "en cours", + degraded: "dégradé" + } + }, + last_error: { + label: "Dernière erreur" + }, + run_count: { + label: "Nombre d’exécutions" + }, + failure_count: { + label: "Nombre d’échecs" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + } + }, + sys_job_run: { + label: "Exécution de tâche", + pluralLabel: "Exécutions de tâches", + description: "Historique d’exécution des tâches en arrière-plan", + fields: { + id: { + label: "ID de l’exécution" + }, + job_name: { + label: "Tâche" + }, + status: { + label: "Statut", + options: { + running: "en cours", + success: "réussi", + failed: "échec", + timeout: "délai dépassé", + degraded: "dégradé" + } + }, + started_at: { + label: "Commencé le" + }, + completed_at: { + label: "Terminé le" + }, + duration_ms: { + label: "Durée (ms)" + }, + attempt: { + label: "Tentative", + help: "1 pour la première exécution, >1 pour les nouvelles tentatives/rejeux" + }, + trigger: { + label: "Déclencheur", + options: { + schedule: "planification", + manual: "manuel", + replay: "rejeu" + } + }, + error: { + label: "Erreur" + }, + created_at: { + label: "Créé le" + } + } + }, + sys_job_queue: { + label: "Message de file de tâches", + pluralLabel: "Messages de file de tâches", + description: "File de tâches/messages durable, y compris les lettres mortes", + fields: { + id: { + label: "ID du message" + }, + queue: { + label: "File d’attente", + help: "Nom logique de la file (snake_case)" + }, + idempotency_key: { + label: "Clé d’idempotence", + help: "Clé de dédoublonnage au sein de (file, fenêtre)" + }, + payload_json: { + label: "Charge utile (JSON)", + help: "Corps du message sérialisé" + }, + metadata_json: { + label: "Métadonnées (JSON)", + help: "Ensemble de métadonnées sérialisé (tenant_id, source_record, ...)" + }, + status: { + label: "Statut", + help: "État du cycle de vie", + options: { + pending: "en attente", + running: "en cours", + completed: "terminé", + failed: "échec", + dlq: "dlq" + } + }, + priority: { + label: "Priorité", + help: "Plus la valeur est basse, plus la priorité est élevée" + }, + attempts: { + label: "Tentatives" + }, + max_attempts: { + label: "Nombre maximal de tentatives" + }, + backoff_type: { + label: "Temporisation", + options: { + fixed: "fixe", + exponential: "exponentielle" + } + }, + backoff_delay_ms: { + label: "Base de temporisation (ms)" + }, + backoff_max_delay_ms: { + label: "Plafond de temporisation (ms)" + }, + scheduled_for: { + label: "Planifié pour", + help: "Moment le plus tôt auquel un travailleur peut prendre ce message en bail" + }, + locked_by: { + label: "Verrouillé par", + help: "ID du travailleur qui détient le bail" + }, + locked_until: { + label: "Verrouillé jusqu’au", + help: "Expiration du bail ; une fois dépassée, un autre travailleur peut le réclamer" + }, + last_error: { + label: "Dernière erreur" + }, + completed_at: { + label: "Terminé le" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + } + }, + sys_import_job: { + label: "Tâche d’import", + pluralLabel: "Tâches d’import", + description: "État, progression et historique des tâches d’import en masse asynchrones", + fields: { + id: { + label: "ID de la tâche" + }, + object_name: { + label: "Objet", + help: "Nom d’API de l’objet dans lequel les données sont importées" + }, + status: { + label: "Statut", + options: { + pending: "en attente", + running: "en cours", + succeeded: "réussi", + failed: "échec", + cancelled: "annulé" + } + }, + total_rows: { + label: "Nombre total de lignes" + }, + processed_rows: { + label: "Lignes traitées" + }, + created_count: { + label: "Créés" + }, + updated_count: { + label: "Mis à jour" + }, + skipped_count: { + label: "Ignorés" + }, + error_count: { + label: "Erreurs" + }, + write_mode: { + label: "Mode d’écriture", + options: { + insert: "insertion", + update: "mise à jour", + upsert: "insertion ou mise à jour" + } + }, + dry_run: { + label: "Simulation" + }, + run_automations: { + label: "Exécuter les automatisations" + }, + treat_as_historical: { + label: "Traiter comme historique" + }, + error: { + label: "Motif de l’échec" + }, + results: { + label: "Résultats par ligne (échantillon)", + help: "Échantillon limité de résultats par ligne (échecs en premier) pour l’interface" + }, + undo_log: { + label: "Journal d’annulation", + help: "Instructions d’annulation ({created:[ids], updated:[{id,before}]}) capturées pour les petites tâches hors simulation afin que l’import puisse être annulé. Une annulation partielle ne conserve ici que les lignes non annulées, avec une liste des échecs donnant pour chacune son id, son action, son code et le motif du refus" + }, + reverted_at: { + label: "Annulé le", + help: "Renseigné lorsque l’import a été annulé (enregistrements créés supprimés, enregistrements mis à jour restaurés)" + }, + started_at: { + label: "Commencé le" + }, + completed_at: { + label: "Terminé le" + }, + created_by: { + label: "Créé par" + }, + created_at: { + label: "Créé le" + } + } + }, + sys_metadata: { + label: "Métadonnées système", + pluralLabel: "Métadonnées système", + description: "Stocke les enregistrements de métadonnées de la plateforme et des utilisateurs (objets, vues, flux, etc.)", + fields: { + id: { + label: "ID" + }, + name: { + label: "Nom" + }, + type: { + label: "Type de métadonnées" + }, + namespace: { + label: "Espace de noms" + }, + package_id: { + label: "ID du package", + help: "Chaîne d’ID de manifeste de package héritée. Utilisez package_version_id pour les nouveaux enregistrements." + }, + package_version_id: { + label: "Version du package", + help: "La version de package à partir de laquelle ces métadonnées ont été installées. Vide pour les métadonnées intégrées de la plateforme et pour les modifications effectuées dans cet environnement." + }, + managed_by: { + label: "Géré par", + options: { + package: "package", + platform: "plateforme", + user: "utilisateur" + } + }, + scope: { + label: "Portée", + options: { + system: "système", + platform: "plateforme", + user: "utilisateur" + } + }, + bundle_member: { + label: "Membre du lot", + help: "Membre du lot (par ex. la langue d’un modèle d’e-mail) que cette ligne de surcouche personnalise. Null = le membre canonique." + }, + metadata: { + label: "Métadonnées", + help: "Charge utile de métadonnées sérialisée en JSON" + }, + draft_changes: { + label: "Modifications en brouillon", + help: "Provenance par clé sérialisée en JSON d’un brouillon en attente : quelle surface (copilot, studio, api), quel acteur et à quel moment chaque chemin de clé a été modifié en dernier. Null sur les lignes actives." + }, + extends: { + label: "Étend" + }, + strategy: { + label: "Stratégie", + options: { + merge: "fusion", + replace: "remplacement" + } + }, + owner: { + label: "Propriétaire" + }, + state: { + label: "État", + options: { + draft: "brouillon", + active: "actif", + archived: "archivé", + deprecated: "obsolète" + } + }, + environment_id: { + label: "Environnement (obsolète)", + help: "OBSOLÈTE. Les surcouches sont des lignes d’environnement ; pas de colonne de locataire." + }, + version: { + label: "Version" + }, + checksum: { + label: "Somme de contrôle" + }, + source: { + label: "Source", + options: { + filesystem: "système de fichiers", + database: "base de données", + api: "api", + migration: "migration" + } + }, + tags: { + label: "Étiquettes", + help: "Tableau sérialisé en JSON d’étiquettes de classification" + }, + created_by: { + label: "Créé par" + }, + created_at: { + label: "Créé le" + }, + updated_by: { + label: "Mis à jour par" + }, + updated_at: { + label: "Mis à jour le" + } + }, + _views: { + only_objects: { + label: "Objets" + }, + only_fields: { + label: "Champs" + }, + all_metadata: { + label: "Toutes" + } + } + }, + sys_metadata_history: { + label: "Historique des métadonnées", + pluralLabel: "Historique des métadonnées", + description: "Journal d’événements durable des modifications des surcouches de métadonnées (à l’échelle de l’environnement, en ajout seul)", + fields: { + id: { + label: "ID" + }, + event_seq: { + label: "Séq. d’événement", + help: "Curseur monotone du journal d’événements par organisation." + }, + name: { + label: "Nom" + }, + type: { + label: "Type de métadonnées" + }, + version: { + label: "Version" + }, + operation_type: { + label: "Type d’opération", + options: { + create: "création", + update: "mise à jour", + publish: "publication", + revert: "annulation", + delete: "suppression" + } + }, + metadata: { + label: "Métadonnées", + help: "Instantané des métadonnées sérialisé en JSON à cette version (null pour les suppressions)." + }, + checksum: { + label: "Somme de contrôle" + }, + previous_checksum: { + label: "Somme de contrôle précédente" + }, + change_note: { + label: "Note de modification", + help: "Description de ce qui a changé dans cette version." + }, + source: { + label: "Source" + }, + recorded_by: { + label: "Enregistré par", + help: "Utilisateur qui a effectué cette modification. NULL = initiée par le système (synchronisation au démarrage, migration, tâche planifiée) — jamais une chaîne sentinelle." + }, + recorded_at: { + label: "Enregistré le" + } + } + }, + sys_metadata_audit: { + label: "Audit des métadonnées", + pluralLabel: "Audit des métadonnées", + description: "Piste d’audit en ajout seul des décisions d’écriture de métadonnées.", + fields: { + id: { + label: "ID" + }, + occurred_at: { + label: "Survenu le" + }, + actor: { + label: "Auteur de l’action", + help: "Principal agissant — ID utilisateur, ID système ou « system »." + }, + source: { + label: "Source" + }, + type: { + label: "Type de métadonnées" + }, + name: { + label: "Nom" + }, + operation: { + label: "Opération", + options: { + save: "enregistrement", + publish: "publication", + rollback: "restauration", + delete: "suppression", + reset: "réinitialisation" + } + }, + outcome: { + label: "Résultat", + options: { + allowed: "autorisé", + denied: "refusé", + forced: "forcé" + } + }, + code: { + label: "Code" + }, + lock_state: { + label: "État du verrou" + }, + lock_overridden: { + label: "Verrou outrepassé" + }, + request_id: { + label: "ID de la requête" + }, + note: { + label: "Note" + } + } + }, + sys_setting: { + label: "Paramètre", + pluralLabel: "Paramètres", + description: "Magasin clé-valeur générique qui sous-tend le contrat SettingsManifest.", + fields: { + id: { + label: "ID du paramètre" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + }, + namespace: { + label: "Espace de noms", + help: "Espace de noms du manifeste (par ex. mail, branding, storage)." + }, + key: { + label: "Clé", + help: "Clé de spécification au sein de l’espace de noms (snake_case)." + }, + scope: { + label: "Portée", + help: "Couche de la hiérarchie de résolution de configuration à laquelle appartient cette ligne.", + options: { + tenant: "Locataire", + user: "Utilisateur" + } + }, + user_id: { + label: "Utilisateur", + help: "Utilisateur propriétaire lorsque scope=user ; null sinon." + }, + value: { + label: "Valeur", + help: "Valeur encodée en JSON. Null lorsque encrypted=true (voir value_enc)." + }, + encrypted: { + label: "Chiffré", + help: "Si true, la valeur est stockée chiffrée au repos dans value_enc ; la colonne value est null." + }, + locked: { + label: "Verrouillé", + help: "Si true, les lignes de portée inférieure ne peuvent pas remplacer cette valeur ; les écritures sur les portées inférieures renvoient 409. Une ligne de locataire fige sa valeur pour les utilisateurs de ce locataire (cascade de la phase 2)." + }, + locked_reason: { + label: "Motif du verrouillage", + help: "Explication lisible affichée dans l’info-bulle de l’interface lorsque locked=true." + }, + value_enc: { + label: "Valeur chiffrée", + help: "Charge utile chiffrée (enveloppée par KMS). Renseignée uniquement lorsque encrypted=true." + }, + updated_by: { + label: "Mis à jour par", + help: "Dernier acteur ayant écrit cette ligne via SettingsService.set()." + } + }, + _views: { + by_namespace: { + label: "Par espace de noms" + }, + tenant_only: { + label: "Locataire" + }, + user_only: { + label: "Utilisateur" + }, + all_settings: { + label: "Tous" + } + } + }, + sys_platform_setting: { + label: "Paramètre de plateforme", + pluralLabel: "Paramètres de plateforme", + description: "Valeurs de paramètres à l’échelle du déploiement : l’échelon global de la cascade des paramètres.", + fields: { + id: { + label: "ID du paramètre" + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + }, + namespace: { + label: "Espace de noms", + help: "Espace de noms du manifeste (par ex. mail, storage, ai)." + }, + key: { + label: "Clé", + help: "Clé de spécification au sein de l’espace de noms (snake_case)." + }, + value: { + label: "Valeur", + help: "Valeur encodée en JSON. Null lorsque encrypted=true (voir value_enc)." + }, + encrypted: { + label: "Chiffré", + help: "Si true, la valeur est stockée chiffrée au repos dans value_enc ; la colonne value est null." + }, + locked: { + label: "Verrouillé", + help: "Si true, les lignes de locataire et d’utilisateur ne peuvent pas remplacer cette valeur ; les écritures sur ces portées renvoient 409." + }, + locked_reason: { + label: "Motif du verrouillage", + help: "Explication lisible affichée dans l’info-bulle de l’interface lorsque locked=true." + }, + value_enc: { + label: "Valeur chiffrée", + help: "Référence de la ligne sys_secret qui contient le texte chiffré. Renseignée uniquement lorsque encrypted=true." + }, + updated_by: { + label: "Mis à jour par", + help: "Dernier acteur ayant écrit cette ligne via le service de paramètres." + } + } + }, + sys_secret: { + label: "Secret", + pluralLabel: "Secrets", + description: "Magasin de textes chiffrés alimenté par trois producteurs privilégiés (voir managedBy) ; chacun conserve sa référence dans sa propre colonne. Ne contient jamais de texte en clair.", + fields: { + id: { + label: "ID", + help: "Référence opaque. La référence se trouve dans la colonne détentrice propre au producteur qui écrit — `sys_setting.value_enc`, une référence `secret:` sur une ligne métier, ou le `credentialsRef` d’une source de données — de sorte qu’une ligne non référencée par `sys_setting` n’est pas pour autant non référencée." + }, + created_at: { + label: "Créé le", + help: "Date de la première écriture du texte chiffré." + }, + rotated_at: { + label: "Renouvelé le", + help: "Date du dernier ré-enveloppement du texte chiffré sous une nouvelle clé KMS." + }, + namespace: { + label: "Espace de noms", + help: "Libellé propre au producteur, et non un espace de noms de paramètres en général : `SettingsService` y écrit l’espace de noms des paramètres, le chiffrement des champs `secret` du moteur y écrit le nom de l’objet, le liant de source de données y écrit la portée fournie par l’appelant (par défaut `datasource`). Voir managedBy." + }, + key: { + label: "Clé", + help: "Libellé propre au producteur, associé à `namespace` : la clé de spécification du paramètre, le nom du champ `secret` chiffré ou le nom de la source de données. La paire indique comment le producteur a adressé la valeur ; elle n’identifie pas le producteur qui a écrit la ligne." + }, + kms_key_id: { + label: "ID de clé KMS", + help: "Référence KMS externe (ARN, ID de ressource GCP ou `local`)." + }, + alg: { + label: "Algorithme", + help: "Étiquette de l’algorithme de chiffrement/AEAD." + }, + version: { + label: "Version", + help: "Incrémentée chaque fois que rotateKey() ré-enveloppe cette ligne." + }, + ciphertext: { + label: "Texte chiffré", + help: "Blob de texte chiffré encodé par le fournisseur (base64 / JSON). Défini par l’implémentation ; seul l’ICryptoProvider correspondant peut le lire." + } + }, + _views: { + all: { + label: "Tous les secrets" + } + } + }, + sys_setting_audit: { + label: "Entrée d’audit des paramètres", + pluralLabel: "Audit des paramètres", + description: "Piste d’audit en ajout seul des mutations de SettingsService.", + fields: { + id: { + label: "ID" + }, + created_at: { + label: "Créé le", + help: "Moment où la mutation a été enregistrée." + }, + namespace: { + label: "Espace de noms" + }, + key: { + label: "Clé" + }, + scope: { + label: "Portée", + help: "Couche de la cascade dans laquelle la ligne a été écrite.", + options: { + tenant: "Locataire", + user: "Utilisateur" + } + }, + action: { + label: "Action", + help: "Nature de la mutation.", + options: { + set: "Définir", + reset: "Réinitialiser", + lock: "Verrouiller", + unlock: "Déverrouiller", + rotate: "Renouveler" + } + }, + actor_id: { + label: "Auteur de l’action", + help: "Utilisateur qui a effectué la mutation ; null pour les tâches système." + }, + source: { + label: "Source", + help: "Point d’entrée de la mutation.", + options: { + ui: "Interface", + api: "API", + migration: "Migration", + import: "Import", + system: "Système" + } + }, + reason: { + label: "Motif", + help: "Justification libre fournie par l’acteur (facultative)." + }, + old_hash: { + label: "Ancienne empreinte", + help: "Empreinte de la valeur précédente, jamais le texte en clair : sha256: hex pour une valeur simple, l’empreinte à clé hmac-sha256: pour un secret. Null si la valeur n’était pas définie auparavant, ou pour un secret sans empreinte à clé." + }, + new_hash: { + label: "Nouvelle empreinte", + help: "Empreinte de la nouvelle valeur, jamais le texte en clair : sha256: hex pour une valeur simple, l’empreinte à clé hmac-sha256: pour un secret. Null lors d’une réinitialisation, ou pour un secret sans empreinte à clé." + }, + encrypted: { + label: "Chiffré", + help: "True lorsque le champ contient des éléments secrets (la rotation est alors pertinente)." + }, + request_id: { + label: "ID de la requête", + help: "Corrélé avec sys_audit_log / le traçage." + } + }, + _views: { + recent: { + label: "Récentes" + } + } + }, + sys_platform_setting_audit: { + label: "Entrée d’audit des paramètres de plateforme", + pluralLabel: "Audit des paramètres de plateforme", + description: "Piste d’audit en ajout seul des mutations de SettingsService sur les clés de portée globale.", + fields: { + id: { + label: "ID" + }, + created_at: { + label: "Créé le", + help: "Moment où la mutation a été enregistrée." + }, + namespace: { + label: "Espace de noms" + }, + key: { + label: "Clé" + }, + scope: { + label: "Portée", + help: "Couche de la cascade dans laquelle la ligne a été écrite — toujours l’échelon global ici.", + options: { + global: "Global" + } + }, + action: { + label: "Action", + help: "Nature de la mutation.", + options: { + set: "Définir", + reset: "Réinitialiser", + lock: "Verrouiller", + unlock: "Déverrouiller", + rotate: "Renouveler" + } + }, + actor_id: { + label: "Auteur de l’action", + help: "Utilisateur qui a effectué la mutation ; null pour les tâches système." + }, + source: { + label: "Source", + help: "Point d’entrée de la mutation.", + options: { + ui: "Interface", + api: "API", + migration: "Migration", + import: "Import", + system: "Système" + } + }, + reason: { + label: "Motif", + help: "Justification libre fournie par l’acteur (facultative)." + }, + old_hash: { + label: "Ancienne empreinte", + help: "Empreinte de la valeur précédente, jamais le texte en clair : sha256: hex pour une valeur simple, l’empreinte à clé hmac-sha256: pour un secret. Null si la valeur n’était pas définie auparavant, ou pour un secret sans empreinte à clé." + }, + new_hash: { + label: "Nouvelle empreinte", + help: "Empreinte de la nouvelle valeur, jamais le texte en clair : sha256: hex pour une valeur simple, l’empreinte à clé hmac-sha256: pour un secret. Null lors d’une réinitialisation, ou pour un secret sans empreinte à clé." + }, + encrypted: { + label: "Chiffré", + help: "True lorsque le champ contient des éléments secrets (la rotation est alors pertinente)." + }, + request_id: { + label: "ID de la requête", + help: "Corrélé avec sys_audit_log / le traçage." + } + }, + _views: { + recent: { + label: "Récentes" + } + } + }, + sys_migration: { + label: "Migration de données", + pluralLabel: "Migrations de données", + description: "Indicateurs de migration de données au niveau du déploiement : quelles migrations de données conditionnelles ont été exécutées ici et si leur autovérification a réussi.", + fields: { + id: { + label: "ID de la migration", + help: "ID de migration connu (par ex. adr-0104-file-references). Une ligne par migration." + }, + last_run_at: { + label: "Dernière exécution le", + help: "Moment où cette migration a terminé pour la dernière fois une exécution conditionnelle (en mode application) sur ce déploiement." + }, + verified_at: { + label: "Vérifié le", + help: "Moment où l’autovérification a RÉUSSI pour la dernière fois. Null tant que ce n’est pas le cas, et de nouveau effacé par une exécution ultérieure en échec — une régression referme le verrou. Les consommateurs exigent que ce champ soit renseigné ET que blocking = 0." + }, + applied_at: { + label: "Appliqué le", + help: "Moment où le remplissage rétroactif a été exécuté pour la dernière fois en mode application (écritures activées)." + }, + blocking: { + label: "Écarts bloquants", + help: "Écarts bloquants signalés par la dernière autovérification. Le verrou exige 0." + }, + advisory: { + label: "Constats consultatifs", + help: "Constats consultatifs de la dernière exécution (URL externes, propriétaires obsolètes, …) — ils coûtent du stockage ou demandent une décision de modélisation, mais ne bloquent jamais le verrou." + }, + details: { + label: "Détails (JSON)", + help: "Décomptes encodés en JSON de la dernière exécution, à des fins de diagnostic." + }, + deviation_observed_at: { + label: "Écart observé le", + help: "Moment où ce déploiement a ADMIS pour la dernière fois une valeur que son propre contrat vérifié rejette, via une échappatoire OS_ALLOW_LAX_*. N’efface délibérément PAS verified_at : cela retire uniquement la moitié irréversible de ce qu’autorise le certificat — la suppression des octets s’arrête, la validation et la mise en pierre tombale continuent. Effacé par la prochaine exécution en mode application." + }, + deviation_detail: { + label: "Détail de l’écart (JSON)", + help: "Premier contre-exemple encodé en JSON à l’origine de deviation_observed_at (objet, champ, type, problème d’analyse), afin qu’un opérateur puisse retrouver la valeur qui a fermé le verrou irréversible." + }, + columns_moved_at: { + label: "Colonnes déplacées le", + help: "Moment où ce déploiement a terminé pour la dernière fois le DÉPLACEMENT DE COLONNES de cette migration — l’étape qui change le type des colonnes migrées et réécrit les valeurs qu’elles contiennent dans le nouvel encodage. Preuve distincte de applied_at et verified_at, qui n’attestent que le remplissage rétroactif et son autovérification : un déploiement peut porter les deux et stocker encore l’ancien encodage. Null signifie exactement cela, et constitue un état stable attendu plutôt qu’une erreur — c’est ce que doit supposer un consommateur qui ne peut pas lire ce champ." + }, + created_at: { + label: "Créé le" + }, + updated_at: { + label: "Mis à jour le" + } + } + }, + sys_exchange_rate: { + label: "Taux de change", + pluralLabel: "Taux de change", + description: "Taux de change de la devise de référence de l’organisation vers une devise étrangère, par date d’effet — les taux avec lesquels les montants en devise étrangère sont convertis dans la devise de référence.", + fields: { + id: { + label: "ID" + }, + currency: { + label: "Devise", + help: "Code ISO 4217 de la devise étrangère, en majuscules (EUR, JPY, CHF)." + }, + rate: { + label: "Taux", + help: "1 unité de la devise de référence de l’organisation (Localisation → Devise par défaut) = Taux unités de cette devise. Exemple : devise de référence USD, devise EUR, taux 0,92 signifie 1 USD = 0,92 EUR." + }, + effective_date: { + label: "Date d’effet", + help: "Première date à laquelle ce taux s’applique. Il reste en vigueur jusqu’à ce qu’une ligne ultérieure pour la même devise prenne le relais." + }, + source: { + label: "Source", + help: "Origine de la ligne : saisie dans Configuration (manual), chargée depuis un fichier (import) ou écrite par un flux d’application (app).", + options: { + manual: "Manuelle", + import: "Import", + app: "Application" + } + } + }, + _views: { + all_rates: { + label: "Tous", + description: "1 unité de la devise de référence de l’organisation (Localisation → Devise par défaut) = Taux unités de cette devise. Exemple : devise de référence USD, devise EUR, taux 0,92 signifie 1 USD = 0,92 EUR.", + emptyState: { + title: "Aucun taux de change pour le moment", + message: "Ajoutez une ligne par devise pour chaque date à laquelle son taux change, par exemple une ligne par devise au début de chaque mois." + } + } + } + } +}; diff --git a/packages/platform-objects/src/apps/translations/fr.source-hashes.generated.ts b/packages/platform-objects/src/apps/translations/fr.source-hashes.generated.ts new file mode 100644 index 00000000000..91fa1adb515 --- /dev/null +++ b/packages/platform-objects/src/apps/translations/fr.source-hashes.generated.ts @@ -0,0 +1,204 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auto-generated by 'os i18n extract' for locale 'fr'. Do not hand-edit. + * + * Each entry is the digest of the SOURCE REVISION that this locale's leaf at + * that path is still a byte copy of — provenance for the generated half of the + * bundles (commit 09b4f4e4e): a leaf whose digest no longer matches its source is stale and serves the source text instead. + * + * An entry exists only while the leaf IS such a copy. Re-translate the leaf in + * `.objects.generated.ts` and the next extract drops its entry by + * itself — the table makes no claim about text a translator wrote. A path with + * no entry is LEGACY-TRUSTED and never reported stale. + * + * ⚠️ Do not "fix" a staleness report by editing this file. Refreshing a digest + * records that the current text was copied from the current source, which is + * the false claim the mechanism exists to detect. Fix the TRANSLATION. + */ + +export const frGeneratedSourceHashes: Readonly> = { + "metadataForms.action.fields.body.source.label": "8fe786e8e29c8cec", + "metadataForms.action.fields.description.label": "779a4342c8eb6707", + "metadataForms.action.fields.mode.label": "74dde5013435abdc", + "metadataForms.action.fields.type.label": "901bb552898b2cab", + "metadataForms.action.fields.visible.label": "7f733614bc9cea97", + "metadataForms.action.label": "a36bba313a7aa9ab", + "metadataForms.agent.fields.avatar.label": "0f06369995b69b8b", + "metadataForms.agent.fields.instructions.label": "ed1cc73e669573d0", + "metadataForms.agent.fields.surface.label": "5f41277948ff2598", + "metadataForms.app.fields.active.label": "eee107cf4e466779", + "metadataForms.app.fields.description.label": "779a4342c8eb6707", + "metadataForms.app.fields.name.helpText": "d600b805aeb8983a", + "metadataForms.app.fields.navigation.label": "51cd9fc784a9643f", + "metadataForms.app.label": "409ceb97d1ea6644", + "metadataForms.app.sections.navigation.label": "51cd9fc784a9643f", + "metadataForms.dashboard.fields.description.label": "779a4342c8eb6707", + "metadataForms.dashboard.fields.widgets.label": "bd4bf82396edd638", + "metadataForms.dashboard.sections.widgets.label": "bd4bf82396edd638", + "metadataForms.dataset.fields.description.label": "779a4342c8eb6707", + "metadataForms.dataset.fields.dimensions.label": "d75e6dd9d87e93b4", + "metadataForms.dataset.sections.dimensions.label": "d75e6dd9d87e93b4", + "metadataForms.dataset.sections.source.label": "8fe786e8e29c8cec", + "metadataForms.doc.label": "3f35cf5088b999ad", + "metadataForms.email_template.fields.description.label": "779a4342c8eb6707", + "metadataForms.email_template.fields.fromOverride.helpText": "c5e29fa12ee1525b", + "metadataForms.email_template.fields.variables.helpText": "27197774c2cda633", + "metadataForms.email_template.fields.variables.label": "bec6a3d74a9d2018", + "metadataForms.email_template.sections.variables.label": "bec6a3d74a9d2018", + "metadataForms.field.fields.description.label": "779a4342c8eb6707", + "metadataForms.field.fields.dimensions.label": "d75e6dd9d87e93b4", + "metadataForms.field.fields.expression.label": "cd1b52ad1588075e", + "metadataForms.field.fields.max.label": "8081d808ef64ff91", + "metadataForms.field.fields.min.label": "05b03a75153e31f3", + "metadataForms.field.fields.multiple.label": "252515392646e4cd", + "metadataForms.field.fields.options.label": "2cc9eb8d0c8ec657", + "metadataForms.field.fields.type.label": "901bb552898b2cab", + "metadataForms.field.fields.unique.label": "1d37471d3205fbc8", + "metadataForms.field.fields.widget.label": "0dd581c951a27892", + "metadataForms.field.sections.configuration.label": "5e77c0af10f4252c", + "metadataForms.flow.fields.description.label": "779a4342c8eb6707", + "metadataForms.flow.fields.type.label": "901bb552898b2cab", + "metadataForms.flow.fields.variables.label": "bec6a3d74a9d2018", + "metadataForms.flow.fields.version.label": "b74a37c0879ec50e", + "metadataForms.hook.fields.body.source.label": "8fe786e8e29c8cec", + "metadataForms.hook.fields.condition.label": "bfcca3fe6f93b90c", + "metadataForms.hook.fields.description.label": "779a4342c8eb6707", + "metadataForms.hook.fields.refusals.message.label": "2af15199795c25b9", + "metadataForms.hook.label": "248bb1cdc692252d", + "metadataForms.object.fields.description.label": "779a4342c8eb6707", + "metadataForms.object.fields.fields.description.label": "779a4342c8eb6707", + "metadataForms.object.fields.fields.expression.label": "cd1b52ad1588075e", + "metadataForms.object.fields.fields.max.label": "8081d808ef64ff91", + "metadataForms.object.fields.fields.min.label": "05b03a75153e31f3", + "metadataForms.object.fields.fields.multiple.label": "252515392646e4cd", + "metadataForms.object.fields.fields.options.label": "2cc9eb8d0c8ec657", + "metadataForms.object.fields.fields.type.label": "901bb552898b2cab", + "metadataForms.object.fields.fields.unique.label": "1d37471d3205fbc8", + "metadataForms.object.fields.lifecycle.archive.label": "dc721777496e2512", + "metadataForms.object.fields.userActions.import.label": "05c80d3a61ec3290", + "metadataForms.object.fields.validations.label": "06fc6dd288348779", + "metadataForms.page.fields.aria.label": "d98a61c7b79cbd75", + "metadataForms.page.fields.description.label": "779a4342c8eb6707", + "metadataForms.page.fields.interfaceConfig.addRecord.position.label": "9a24c02e441eb0b4", + "metadataForms.page.fields.interfaceConfig.source.label": "8fe786e8e29c8cec", + "metadataForms.page.fields.source.label": "8fe786e8e29c8cec", + "metadataForms.page.fields.type.label": "901bb552898b2cab", + "metadataForms.page.fields.variables.label": "bec6a3d74a9d2018", + "metadataForms.page.label": "a65b2d4829365115", + "metadataForms.permission.fields.description.label": "779a4342c8eb6707", + "metadataForms.permission.fields.fields.helpText": "887a0bae04a950e2", + "metadataForms.permission.fields.objects.helpText": "e2512cfb3b501aae", + "metadataForms.permission.fields.tabPermissions.helpText": "8942e2aebe59897c", + "metadataForms.position.fields.description.label": "779a4342c8eb6707", + "metadataForms.position.fields.name.helpText": "3594bba7cc2a3c8a", + "metadataForms.report.fields.description.label": "779a4342c8eb6707", + "metadataForms.report.fields.type.label": "901bb552898b2cab", + "metadataForms.skill.fields.active.label": "eee107cf4e466779", + "metadataForms.skill.fields.description.label": "779a4342c8eb6707", + "metadataForms.skill.fields.instructions.label": "ed1cc73e669573d0", + "metadataForms.skill.fields.surface.label": "5f41277948ff2598", + "metadataForms.tool.fields.description.label": "779a4342c8eb6707", + "metadataForms.view.fields.addRecord.position.label": "9a24c02e441eb0b4", + "metadataForms.view.fields.description.label": "779a4342c8eb6707", + "metadataForms.view.fields.gantt.label": "57ffc6ef520edfc5", + "metadataForms.view.fields.kanban.label": "dd95c452aa71459e", + "metadataForms.view.fields.navigation.label": "51cd9fc784a9643f", + "metadataForms.view.fields.navigation.mode.label": "74dde5013435abdc", + "metadataForms.view.fields.pagination.label": "309131fbaca258df", + "metadataForms.view.fields.selection.type.label": "901bb552898b2cab", + "metadataForms.view.fields.type.label": "901bb552898b2cab", + "metadataForms.view.sections.gantt.label": "57ffc6ef520edfc5", + "metadataForms.view.sections.kanban.label": "dd95c452aa71459e", + "metadataForms.view.sections.pagination.label": "309131fbaca258df", + "objects.sys_attachment.fields.description.label": "779a4342c8eb6707", + "objects.sys_business_unit.fields.active.label": "eee107cf4e466779", + "objects.sys_business_unit.fields.code.label": "083180bdf83435b3", + "objects.sys_business_unit.fields.kind.options.division": "fc8aef76b6a50eec", + "objects.sys_email.fields.cc_addresses.label": "02946d952cf15623", + "objects.sys_email.fields.message_id.label": "14cd089a4062f4b1", + "objects.sys_email.fields.reply_to.label": "90ad8f2ca791a95a", + "objects.sys_email_template.fields.category.options.marketing": "9d7f1456a6e64b03", + "objects.sys_email_template.fields.category.options.notification": "fcb1e456bfde071f", + "objects.sys_email_template.fields.category.options.workflow": "926ed40d693c428d", + "objects.sys_email_template.fields.description.label": "779a4342c8eb6707", + "objects.sys_email_template.fields.id.label": "00b0385c9c152888", + "objects.sys_email_template.fields.managed_by.options.package": "1c8c6168730324a3", + "objects.sys_email_template.fields.reply_to.label": "90ad8f2ca791a95a", + "objects.sys_email_template.fields.variables_json.label": "c2cfff50e8151e3d", + "objects.sys_exchange_rate.fields.id.label": "00b0385c9c152888", + "objects.sys_exchange_rate.fields.source.label": "8fe786e8e29c8cec", + "objects.sys_exchange_rate.fields.source.options.import": "05c80d3a61ec3290", + "objects.sys_invitation.label": "9bbd01ba5ed1a107", + "objects.sys_invitation.pluralLabel": "4ebb9fd13de7a546", + "objects.sys_job.fields.active.label": "eee107cf4e466779", + "objects.sys_job.fields.schedule_expression.label": "cd1b52ad1588075e", + "objects.sys_job.fields.schedule_type.options.cron": "fd48d0e2cd59cab2", + "objects.sys_job_queue.fields.status.options.dlq": "4aedfea31f7ea528", + "objects.sys_metadata.fields.id.label": "00b0385c9c152888", + "objects.sys_metadata.fields.managed_by.options.package": "1c8c6168730324a3", + "objects.sys_metadata.fields.source.label": "8fe786e8e29c8cec", + "objects.sys_metadata.fields.source.options.api": "cafc2fe86c3ec55e", + "objects.sys_metadata.fields.source.options.migration": "718854ca44d4caf2", + "objects.sys_metadata.fields.version.label": "b74a37c0879ec50e", + "objects.sys_metadata_audit.fields.code.label": "083180bdf83435b3", + "objects.sys_metadata_audit.fields.id.label": "00b0385c9c152888", + "objects.sys_metadata_audit.fields.note.label": "f78f61778f71bf68", + "objects.sys_metadata_audit.fields.source.label": "8fe786e8e29c8cec", + "objects.sys_metadata_history.fields.id.label": "00b0385c9c152888", + "objects.sys_metadata_history.fields.source.label": "8fe786e8e29c8cec", + "objects.sys_metadata_history.fields.version.label": "b74a37c0879ec50e", + "objects.sys_oauth_access_token.fields.confirmation.label": "64755493e6425b37", + "objects.sys_oauth_access_token.fields.id.label": "00b0385c9c152888", + "objects.sys_oauth_access_token.fields.session_id.label": "2650b5c24a1853d7", + "objects.sys_oauth_application._actions.create_oauth_application.params.type.options.native": "bbb5c124434d88f0", + "objects.sys_oauth_application._actions.create_oauth_application.params.type.options.web": "2a7ead3da8c035c5", + "objects.sys_oauth_application.fields.contacts.label": "05ad1dacce3af71b", + "objects.sys_oauth_application.fields.id.label": "00b0385c9c152888", + "objects.sys_oauth_application.fields.jwks.label": "b2cf79f9d66c0dbc", + "objects.sys_oauth_application.fields.jwks_uri.label": "2a4339df734ab269", + "objects.sys_oauth_application.fields.type.options.web": "053773d6478e022d", + "objects.sys_oauth_client_assertion.fields.id.label": "00b0385c9c152888", + "objects.sys_oauth_client_resource.fields.id.label": "00b0385c9c152888", + "objects.sys_oauth_consent.fields.id.label": "00b0385c9c152888", + "objects.sys_oauth_refresh_token.fields.confirmation.label": "64755493e6425b37", + "objects.sys_oauth_refresh_token.fields.id.label": "00b0385c9c152888", + "objects.sys_oauth_refresh_token.fields.session_id.label": "2650b5c24a1853d7", + "objects.sys_oauth_resource.fields.id.label": "00b0385c9c152888", + "objects.sys_organization._sections.configuration.label": "5e77c0af10f4252c", + "objects.sys_organization.fields.logo.label": "0ec59ac84fe9aecc", + "objects.sys_organization.fields.slug.label": "fe4132df05ec637b", + "objects.sys_platform_setting_audit.fields.action.label": "a36bba313a7aa9ab", + "objects.sys_platform_setting_audit.fields.id.label": "00b0385c9c152888", + "objects.sys_platform_setting_audit.fields.scope.options.global": "5e377106508d2ecd", + "objects.sys_platform_setting_audit.fields.source.label": "8fe786e8e29c8cec", + "objects.sys_platform_setting_audit.fields.source.options.api": "4baeeff968e192d1", + "objects.sys_platform_setting_audit.fields.source.options.import": "05c80d3a61ec3290", + "objects.sys_platform_setting_audit.fields.source.options.migration": "477a479630b97ad5", + "objects.sys_scim_connection_binding.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_connection_credential.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_group.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_group_member.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_identity_tombstone.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_projection_grant.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_subject.fields.id.label": "00b0385c9c152888", + "objects.sys_scim_user.fields.id.label": "00b0385c9c152888", + "objects.sys_secret.fields.id.label": "00b0385c9c152888", + "objects.sys_secret.fields.version.label": "b74a37c0879ec50e", + "objects.sys_secret.label": "03cb32e33fa5cc45", + "objects.sys_secret.pluralLabel": "66bcf0163f13a643", + "objects.sys_session.label": "2650b5c24a1853d7", + "objects.sys_session.pluralLabel": "673bde6e0c8a3ef9", + "objects.sys_setting_audit.fields.action.label": "a36bba313a7aa9ab", + "objects.sys_setting_audit.fields.id.label": "00b0385c9c152888", + "objects.sys_setting_audit.fields.source.label": "8fe786e8e29c8cec", + "objects.sys_setting_audit.fields.source.options.api": "4baeeff968e192d1", + "objects.sys_setting_audit.fields.source.options.import": "05c80d3a61ec3290", + "objects.sys_setting_audit.fields.source.options.migration": "477a479630b97ad5", + "objects.sys_sso_provider._actions.register_saml_provider.params.identifierFormat.placeholder": "5945ff9a6b644855", + "objects.sys_sso_provider._actions.register_sso_provider.params.mapEmail.placeholder": "66309c7adf5c9436", + "objects.sys_sso_provider._actions.register_sso_provider.params.mapName.placeholder": "a484c34aaf624bd6", + "objects.sys_sso_provider._actions.register_sso_provider.params.scopes.placeholder": "58ac90cde28c764d", + "objects.sys_sso_provider.fields.id.label": "00b0385c9c152888", + "objects.sys_two_factor.fields.secret.label": "03cb32e33fa5cc45", +}; diff --git a/packages/platform-objects/src/apps/translations/fr.source-hashes.ts b/packages/platform-objects/src/apps/translations/fr.source-hashes.ts new file mode 100644 index 00000000000..d0deefec35c --- /dev/null +++ b/packages/platform-objects/src/apps/translations/fr.source-hashes.ts @@ -0,0 +1,198 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Recorded source hashes for `fr` (#8765, Option B). + * + * Each entry is the digest of the `en.ts` SOURCE STRING that the `fr` + * translation at that path was made from. `setup.translation.ts` compares them + * against the current source when it assembles the served bundle: a mismatch + * marks that leaf stale and serves the source string in its place + * (`withSourceFallback`), which is the same degradation an untranslated key + * already produces. + * + * ## Maintaining this file + * + * **Re-translated a string?** Update the value in `fr.ts` AND the digest + * here — that pair is what "record the source hash at translation time" means, + * and it is what makes this locale recover on its own while the others keep + * falling back. The new digest is + * `collectSourceHashes(en)['']` (exported from `source-hash.ts`). + * + * **Added a brand-new translation?** No entry is needed. A path with no + * recorded hash is LEGACY-TRUSTED and served verbatim — the ruling's explicit + * rule, so that translations predating this mechanism did not all degrade the + * day it landed. An entry recorded now is strictly better (it will catch the + * NEXT source edit), but its absence never marks anything stale. + * + * ⚠️ Do not "fix" a mismatch by refreshing the digest alone. That records that + * the current translation was made from the current source, which is exactly + * the false claim this mechanism exists to detect — the served text would go + * back to being silently wrong. + * + * ## Which table wins for a path that appears in BOTH + * + * **This one.** A `--no-objects-only` extract records provenance for whatever + * sections it generated, so since #16872 a path under `apps` / `dashboards` / + * `pages` can carry an entry in `.source-hashes.generated.ts` as well + * as here. The two are read by two predicates over two tables and neither + * consults the other, so "wins" is a question about what a HUMAN maintains: + * maintain the digest HERE, exactly as this header already says, and ignore the + * generated one — it is regenerated from the tree on every extract and any + * edit to it is overwritten. + * + * The overlap cannot serve wrong text. The generated table only ever records a + * leaf whose bytes are still a copy of a source revision, so the only paths + * that can appear in both are the ones a translator deliberately left equal to + * the English source; for those, both predicates substitute the SAME source + * string and the served bytes are identical either way. Re-translate such a + * leaf and the next extract drops its generated entry by itself, leaving this + * table the only claim about it. + * + * Recorded when this locale was translated (#30512): every path the `fr` + * bundle authors carries the digest of the source string it was translated + * from, so the first source edit under any of them is caught here rather than + * served as a superseded translation. + */ +export const frSourceHashes: Readonly> = { + 'apps.account.label': 'ae88af048bfd5d64', + 'apps.account.description': 'd5fa9893662325fd', + 'apps.account.navigation.grp_account_inbox.label': '34dc7a0b58ada93b', + 'apps.account.navigation.grp_account_security.label': '0c463f40cccfdf86', + 'apps.account.navigation.grp_account_developer.label': '59668ec5af5afb55', + 'apps.account.navigation.grp_account_ai_assistant.label': 'cff764b13822a259', + 'apps.account.navigation.nav_account_profile.label': '2c92f019ece4d044', + 'apps.account.navigation.nav_account_notifications.label': 'e0fdb90b1d31078d', + 'apps.account.navigation.nav_account_approvals.label': '318a820df22a684b', + 'apps.account.navigation.nav_account_memberships.label': '86164fcd38e0378d', + 'apps.account.navigation.nav_account_linked.label': '58ca623b89e9ff5c', + 'apps.account.navigation.nav_account_sessions.label': '05a759e3a230615e', + 'apps.account.navigation.nav_account_api_keys.label': '8cda9851248b0490', + 'apps.account.navigation.nav_account_oauth_apps.label': '1b26cce61a4c10eb', + 'apps.account.navigation.nav_connect_agent.label': 'a4542422fb5e8a68', + 'apps.setup.label': 'e4bb0d8bcd47c273', + 'apps.setup.description': 'deacdfc679232a4a', + 'apps.setup.navigation.group_overview.label': 'eebdbe1cc7b59cdf', + 'apps.setup.navigation.group_apps.label': '699b367291abcbe6', + 'apps.setup.navigation.nav_packaged_actions.label': '0d86b11df972dff3', + 'apps.setup.navigation.nav_marketplace_browse.label': '2afbe021f5419fdd', + 'apps.setup.navigation.nav_marketplace_installed.label': '93bd5d458e05db60', + 'apps.setup.navigation.nav_cloud_connection.label': '3a7caad0cb98e71d', + 'apps.setup.navigation.group_people_org.label': 'd313f656b6f0ab65', + 'apps.setup.navigation.group_access_control.label': 'a1a4dd47bcbfaf67', + 'apps.setup.navigation.group_approvals.label': '318a820df22a684b', + 'apps.setup.navigation.group_configuration.label': '5e77c0af10f4252c', + 'apps.setup.navigation.group_integrations.label': '1c83392f57463173', + 'apps.setup.navigation.group_diagnostics.label': '62d648cd434b395c', + 'apps.setup.navigation.group_advanced.label': 'bc9e9a0da37ea229', + 'apps.setup.navigation.nav_system_overview.label': '574b2a4b7fb7f979', + 'apps.setup.navigation.nav_connect_agent.label': 'a4542422fb5e8a68', + 'apps.setup.navigation.nav_users.label': 'eee81f7307a66c40', + 'apps.setup.navigation.nav_organization.label': '3e55836156e1c1de', + 'apps.setup.navigation.nav_business_units.label': '46e0cd2344b40e77', + 'apps.setup.navigation.nav_teams.label': '3fe8519872f65108', + 'apps.setup.navigation.nav_organizations.label': '07488357631b973d', + 'apps.setup.navigation.nav_invitations.label': '4ebb9fd13de7a546', + 'apps.setup.navigation.nav_position_holders.label': '627cacb50fd1b0d8', + 'apps.setup.navigation.nav_permission_set_assignment.label': 'fcf7d1b69ee9c298', + 'apps.setup.navigation.nav_sharing_rules.label': 'ceaa66fa5f1df4ad', + 'apps.setup.navigation.nav_record_shares.label': 'e261d340ceb25b9a', + 'apps.setup.navigation.nav_queues.label': '226d23b8bd681e98', + 'apps.setup.navigation.nav_queue_members.label': '46715e86f5231603', + 'apps.setup.navigation.nav_assignment_rules.label': '1779fe384b23954f', + 'apps.setup.navigation.nav_sso_providers.label': '7b06b6a6abf53ba4', + 'apps.setup.navigation.nav_scim_credentials.label': '2d94a32c33ca2957', + 'apps.setup.navigation.nav_api_keys.label': '8cda9851248b0490', + 'apps.setup.navigation.nav_approvals_inbox.label': '0a9269b20d0266c9', + 'apps.setup.navigation.nav_approval_requests.label': 'aafd0f7c6a376f55', + 'apps.setup.navigation.nav_approval_actions.label': 'd2ebad7acfd50978', + 'apps.setup.navigation.nav_approval_delegations.label': 'fc0d41741659c462', + 'apps.setup.navigation.nav_settings_hub.label': 'a3ac65c7c7f461bf', + 'apps.setup.navigation.nav_settings_localization.label': 'beedfaf00452c829', + 'apps.setup.navigation.nav_exchange_rates.label': '6d043e1f08434720', + 'apps.setup.navigation.nav_settings_company.label': '9d7f685e11eaade9', + 'apps.setup.navigation.nav_settings_mail.label': '5d0599600587427d', + 'apps.setup.navigation.nav_settings_sms.label': '77a3056d33027ca0', + 'apps.setup.navigation.nav_settings_branding.label': 'ec6e391065514b64', + 'apps.setup.navigation.nav_settings_auth.label': '5c6d7dbd850ca37c', + 'apps.setup.navigation.nav_settings_access_policy.label': '39a7cfb9a7cba125', + 'apps.setup.navigation.nav_settings_storage.label': 'a2fb526b03c9a9e5', + 'apps.setup.navigation.nav_settings_ai.label': 'a6b41a554173f7ab', + 'apps.setup.navigation.nav_settings_knowledge.label': '140348539a50d9d5', + 'apps.setup.navigation.nav_settings_lifecycle.label': 'cd9fe9c930afed06', + 'apps.setup.navigation.nav_notification_preferences.label': '8869f324c1bd545a', + 'apps.setup.navigation.nav_notification_templates.label': '9e270c87ef2578df', + 'apps.setup.navigation.nav_sessions.label': '673bde6e0c8a3ef9', + 'apps.setup.navigation.nav_audit_logs.label': 'b2fad40733012b18', + 'apps.setup.navigation.nav_notifications.label': 'e0fdb90b1d31078d', + 'apps.setup.navigation.nav_webhooks.label': '8fdd81bc76459a6a', + 'apps.setup.navigation.nav_http_deliveries.label': '340f81dc6ea64987', + 'apps.setup.navigation.nav_datasources.label': '2f0648ce23158e6f', + 'apps.setup.navigation.nav_connector_credentials.label': '0504775224e05a81', + 'apps.setup.navigation.nav_connector_connections.label': '949e9a8fd25c3d57', + 'apps.setup.navigation.nav_oauth_apps.label': '1b26cce61a4c10eb', + 'apps.studio.label': '5aaaaea2294ee893', + 'apps.studio.description': 'c026a5795933995d', + 'apps.studio.navigation.group_overview.label': 'eebdbe1cc7b59cdf', + 'apps.studio.navigation.nav_app_builder.label': '128446e7650210e7', + 'apps.studio.navigation.nav_metadata_directory.label': '87f99e3de29ec25f', + 'apps.studio.navigation.nav_packages.label': '8cd8f29658025f73', + 'apps.studio.navigation.group_data_model.label': 'f7b1fa6f26b344e5', + 'apps.studio.navigation.nav_objects.label': '361008570f571b11', + 'apps.studio.navigation.group_ux.label': '32d21c81ff9312e2', + 'apps.studio.navigation.nav_apps.label': '699b367291abcbe6', + 'apps.studio.navigation.nav_views.label': 'e9c808e26b6a8d11', + 'apps.studio.navigation.nav_pages.label': 'cda91afda2acc4ab', + 'apps.studio.navigation.nav_dashboards.label': '6102d5e8e4bb4b7a', + 'apps.studio.navigation.nav_reports.label': 'f981f2f6d59fce84', + 'apps.studio.navigation.nav_datasets.label': '8491e1ea908b9b0d', + 'apps.studio.navigation.group_logic.label': '415c519f3ad330d7', + 'apps.studio.navigation.nav_actions.label': '7527654bedc9fdfc', + 'apps.studio.navigation.nav_hooks.label': '6fb7e3b7bc144f67', + 'apps.studio.navigation.group_automation.label': '1d5659130670f5f3', + 'apps.studio.navigation.nav_flows.label': '26f7b054faf636a2', + 'apps.studio.navigation.group_ai.label': '0227934c75e56686', + 'apps.studio.navigation.nav_agents.label': '5300d1df6736e399', + 'apps.studio.navigation.nav_tools.label': 'db86c97f27cd31ce', + 'apps.studio.navigation.nav_skills.label': 'eb2af90cd11bbd34', + 'apps.studio.navigation.group_developer.label': '59668ec5af5afb55', + 'apps.studio.navigation.nav_api_console.label': 'd90e508f1921e8d3', + 'apps.studio.navigation.nav_flow_runs.label': '0bdee1504c505862', + 'apps.studio.navigation.nav_public_forms.label': '4592e97959994e86', + 'apps.studio.navigation.nav_integrations.label': 'edbd97d7f401a5b8', + 'apps.studio.navigation.group_integration.label': 'a1f2ddfb5c00c83a', + 'apps.studio.navigation.nav_email_templates.label': '3463bce831c833d5', + 'dashboards.system_overview.label': '574b2a4b7fb7f979', + 'dashboards.system_overview.description': '56dbd944c4b834da', + 'dashboards.system_overview.widgets.widget_total_users.title': 'cee2b388d39f4b4d', + 'dashboards.system_overview.widgets.widget_total_users.description': '54777631b0239fa1', + 'dashboards.system_overview.widgets.widget_organizations.title': '07488357631b973d', + 'dashboards.system_overview.widgets.widget_organizations.description': '4c8ffa2d407f4410', + 'dashboards.system_overview.widgets.widget_active_sessions.title': '05a759e3a230615e', + 'dashboards.system_overview.widgets.widget_active_sessions.description': '72335b16a1f8624e', + 'dashboards.system_overview.widgets.widget_packages_installed.title': 'ca0ce2bb712d83b3', + 'dashboards.system_overview.widgets.widget_packages_installed.description': '67c5558bf10fcb4d', + 'dashboards.system_overview.widgets.widget_login_events.title': 'c6475b3ee5d1efc1', + 'dashboards.system_overview.widgets.widget_login_events.description': '3c41545dbbd34f3c', + 'dashboards.system_overview.widgets.widget_config_changes.title': '4ca2f591d9440211', + 'dashboards.system_overview.widgets.widget_config_changes.description': '6be1d6643282e176', + 'dashboards.system_overview.widgets.widget_events_by_type.title': 'accefd73f7c4187d', + 'dashboards.system_overview.widgets.widget_events_by_type.description': '013308726e1bd2d3', + 'dashboards.system_overview.widgets.widget_events_by_user.title': '04d5cab03abee166', + 'dashboards.system_overview.widgets.widget_events_by_user.description': 'ee9e0d412f66fb4d', + 'dashboards.system_overview.widgets.widget_recent_events.title': '5fb9db734193fe37', + 'dashboards.system_overview.widgets.widget_recent_events.description': '5b9ce7479f6c178c', + 'dashboards.system_overview.globalFilters.created_at.label': 'dcba3619736cb533', + 'datasets.sys_user_metrics.measures.user_count.label': 'eee81f7307a66c40', + 'datasets.sys_organization_metrics.measures.org_count.label': '07488357631b973d', + 'datasets.sys_session_metrics.measures.session_count.label': '673bde6e0c8a3ef9', + 'datasets.sys_package_installation_metrics.measures.package_count.label': 'bd7a67c9d5b3ac44', + 'datasets.sys_audit_log_metrics.dimensions.action.label': 'a36bba313a7aa9ab', + 'datasets.sys_audit_log_metrics.dimensions.user_id.label': '6c0d22a931e235bd', + 'datasets.sys_audit_log_metrics.measures.event_count.label': '41e7c95aaa871569', + 'pages.cloud_connection_settings.label': '3a7caad0cb98e71d', + 'pages.cloud_connection_settings.subtitle': 'f5cdd253e8ffef9f', + 'pages.connect_agent.label': 'a4542422fb5e8a68', + 'pages.connect_agent.subtitle': 'b0d7c6d174511bcb', + 'pages.sys_user_detail.label': '6c0d22a931e235bd', + 'pages.sys_organization_detail.label': '3e55836156e1c1de', +}; diff --git a/packages/platform-objects/src/apps/translations/fr.ts b/packages/platform-objects/src/apps/translations/fr.ts new file mode 100644 index 00000000000..2c04a0b5528 --- /dev/null +++ b/packages/platform-objects/src/apps/translations/fr.ts @@ -0,0 +1,215 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import type { TranslationData } from '@objectstack/spec/system'; +import { frObjects } from './fr.objects.generated.js'; + +/** + * Français (fr) — Setup App Translations + */ +export const fr: TranslationData = { + objects: frObjects, + apps: { + account: { + label: 'Compte', + description: 'Paramètres personnels de sécurité et d’identité', + navigation: { + grp_account_inbox: { label: 'Boîte de réception' }, + grp_account_security: { label: 'Sécurité' }, + grp_account_developer: { label: 'Développeur' }, + grp_account_ai_assistant: { label: 'Assistant IA' }, + nav_account_profile: { label: 'Profil' }, + nav_account_notifications: { label: 'Notifications' }, + nav_account_approvals: { label: 'Mes approbations en attente' }, + nav_account_memberships: { label: 'Mes organisations' }, + nav_account_linked: { label: 'Comptes liés' }, + nav_account_sessions: { label: 'Sessions actives' }, + nav_account_api_keys: { label: 'Clés API' }, + nav_account_oauth_apps: { label: 'Applications OAuth' }, + // Contributed at runtime by `@objectstack/mcp` into + // `grp_account_ai_assistant`; the Setup twin's key does not answer for the + // Account app (one `apps..navigation` namespace per app). + nav_connect_agent: { label: 'Connecter un assistant IA' }, + }, + }, + setup: { + label: 'Configuration', + description: 'Paramètres et administration de la plateforme', + navigation: { + group_overview: { label: 'Aperçu' }, + group_apps: { label: 'Applications' }, + nav_packaged_actions: { label: 'Actions fournies par les packages' }, + nav_marketplace_browse: { label: 'Parcourir la Marketplace' }, + nav_marketplace_installed: { label: 'Applications installées' }, + nav_cloud_connection: { label: 'Connexion au cloud' }, + group_people_org: { label: 'Personnes et organisation' }, + group_access_control: { label: 'Contrôle d’accès' }, + group_approvals: { label: 'Approbations' }, + group_configuration: { label: 'Configuration' }, + group_integrations: { label: 'Intégrations' }, + group_diagnostics: { label: 'Diagnostics' }, + group_advanced: { label: 'Avancé' }, + + nav_system_overview: { label: 'Aperçu du système' }, + nav_connect_agent: { label: 'Connecter un assistant IA' }, + + nav_users: { label: 'Utilisateurs' }, + nav_organization: { label: 'Organisation' }, + nav_business_units: { label: 'Unités commerciales' }, + nav_teams: { label: 'Équipes' }, + nav_organizations: { label: 'Organisations' }, + nav_invitations: { label: 'Invitations' }, + nav_position_holders: { label: 'Titulaires de postes' }, + nav_permission_set_assignment: { label: 'Attribution des ensembles d’autorisations' }, + + nav_sharing_rules: { label: 'Règles de partage' }, + nav_record_shares: { label: 'Partages d’enregistrements' }, + nav_queues: { label: 'Files d’attente' }, + nav_queue_members: { label: 'Membres de file d’attente' }, + nav_assignment_rules: { label: 'Règles d’attribution' }, + // Conditionally contributed by `@objectstack/plugin-auth` (only when an + // external IdP is wired), so `check:app-nav-i18n` cannot see it — see + // the rationale in `en.ts` (#6659). Wording matches this locale's + // `sys_sso_provider.pluralLabel`. + nav_sso_providers: { label: 'Fournisseurs SSO' }, + // Conditionally contributed by plugin-auth (only while SCIM is wired), + // the same case as the entry above; see `en.ts` (#31123). The short form + // of `sys_scim_connection_credential.pluralLabel`. + nav_scim_credentials: { label: 'Identifiants SCIM' }, + nav_api_keys: { label: 'Clés API' }, + + // Centre d’approbations = the Approvals Inbox component (#7234) — the + // surface with decision actions. The three below are the engine's raw + // tables; the first and last read as plugin-approvals' fr object plural + // labels. + nav_approvals_inbox: { label: 'Centre d’approbations' }, + nav_approval_requests: { label: 'Demandes d’approbation' }, + nav_approval_actions: { label: 'Historique des actions' }, + // `Délégations d’approbation` matches sys_approval_delegation's object + // plural label; `(absence)` renders the entry's own "(OOO)" qualifier. + nav_approval_delegations: { label: 'Délégations d’approbation (absence)' }, + + // Each nav_settings_* entry reads as its hub card's title in + // service-settings' fr bundle (`setup-nav-hub-parity.test.ts`), so the + // two delivery pages say Envoi here while the HTTP log below says + // Livraisons, as sys_http_delivery's fr object label does. + nav_settings_hub: { label: 'Tous les paramètres' }, + nav_settings_localization: { label: 'Localisation' }, + nav_exchange_rates: { label: 'Taux de change' }, + nav_settings_company: { label: 'Entreprise' }, + nav_settings_mail: { label: 'Envoi des e-mails' }, + nav_settings_sms: { label: 'Envoi des SMS' }, + nav_settings_branding: { label: 'Image de marque' }, + nav_settings_auth: { label: 'Authentification' }, + nav_settings_access_policy: { label: 'Politique d’accès' }, + nav_settings_storage: { label: 'Stockage des fichiers' }, + nav_settings_ai: { label: 'IA et Embedder' }, + nav_settings_knowledge: { label: 'Connaissances' }, + nav_settings_lifecycle: { label: 'Cycle de vie des données' }, + nav_notification_preferences: { label: 'Préférences de notification' }, + nav_notification_templates: { label: 'Modèles de notification' }, + + nav_sessions: { label: 'Sessions' }, + nav_audit_logs: { label: 'Journal d’audit' }, + nav_notifications: { label: 'Notifications' }, + + // Integrations — contributed at RUNTIME by the owning capability + // plugins (ADR-0029 K2), so no static walk of SETUP_APP can see them. + // `Webhooks` is sys_webhook's own fr object plural label, and this + // bundle is where that choice is RECORDED rather than reached by falling + // back (#5750). + nav_webhooks: { label: 'Webhooks' }, + nav_http_deliveries: { label: 'Livraisons HTTP' }, + nav_datasources: { label: 'Sources de données' }, + nav_connector_credentials: { label: 'Identifiants des connecteurs' }, + nav_connector_connections: { label: 'Connexions des connecteurs' }, + + nav_oauth_apps: { label: 'Applications OAuth' }, + }, + }, + studio: { + label: 'Studio', + description: 'Atelier de métadonnées pour les développeurs, les analystes et les intégrateurs', + navigation: { + group_overview: { label: 'Aperçu' }, + nav_app_builder: { label: 'Créateur d’applications' }, + nav_metadata_directory: { label: 'Tous les types de métadonnées' }, + nav_packages: { label: 'Packages' }, + group_data_model: { label: 'Modèle de données' }, + nav_objects: { label: 'Objets' }, + group_ux: { label: 'Expérience utilisateur' }, + nav_apps: { label: 'Applications' }, + nav_views: { label: 'Vues' }, + nav_pages: { label: 'Pages' }, + nav_dashboards: { label: 'Tableaux de bord' }, + nav_reports: { label: 'Rapports' }, + nav_datasets: { label: 'Jeux de données' }, + group_logic: { label: 'Logique' }, + nav_actions: { label: 'Actions' }, + nav_hooks: { label: 'Hooks' }, + group_automation: { label: 'Automatisation' }, + nav_flows: { label: 'Flux' }, + group_ai: { label: 'IA' }, + nav_agents: { label: 'Agents' }, + nav_tools: { label: 'Outils' }, + nav_skills: { label: 'Compétences' }, + group_developer: { label: 'Développeur' }, + nav_api_console: { label: 'Console API' }, + nav_flow_runs: { label: 'Exécutions de flux' }, + nav_public_forms: { label: 'Formulaires publics' }, + nav_integrations: { label: 'Intégrations et API' }, + group_integration: { label: 'Intégration' }, + nav_email_templates: { label: 'Modèles d’e-mail' }, + }, + }, + }, + + dashboards: { + system_overview: { + label: 'Aperçu du système', + description: 'État de la plateforme, activité de sécurité et événements d’audit récents', + widgets: { + widget_total_users: { title: 'Total des utilisateurs', description: 'Nombre total d’utilisateurs inscrits dans le système' }, + widget_organizations: { title: 'Organisations', description: 'Nombre total d’organisations sur la plateforme' }, + widget_active_sessions: { title: 'Sessions actives', description: 'Nombre de sessions utilisateur actuellement actives' }, + widget_packages_installed: { title: 'Packages installés', description: 'Installations de packages actives dans les projets' }, + widget_login_events: { title: 'Événements de connexion', description: 'Événements d’authentification enregistrés par le journal d’audit' }, + widget_config_changes: { title: 'Modifications de configuration', description: 'Modifications de la configuration du système' }, + widget_events_by_type: { title: 'Événements d’audit par action', description: 'Répartition des événements d’audit par type d’action' }, + widget_events_by_user: { title: 'Événements par utilisateur', description: 'Répartition de l’activité entre les utilisateurs' }, + widget_recent_events: { title: 'Volume d’événements par action', description: 'Volume d’événements regroupé par action (login, logout, config, …)' }, + }, + // The date-range bar above the widgets — addressable since + // `dashboards..globalFilters` (#16772), keyed by the filter's + // `field` because it authors no `name`. + globalFilters: { + created_at: { label: 'Période' }, + }, + }, + }, + + datasets: { + sys_user_metrics: { measures: { user_count: { label: 'Nombre d’utilisateurs' } } }, + sys_organization_metrics: { measures: { org_count: { label: 'Nombre d’organisations' } } }, + sys_session_metrics: { measures: { session_count: { label: 'Nombre de sessions' } } }, + sys_package_installation_metrics: { measures: { package_count: { label: 'Nombre d’installations' } } }, + sys_audit_log_metrics: { + dimensions: { action: { label: 'Type d’action' }, user_id: { label: 'Utilisateur' } }, + measures: { event_count: { label: 'Nombre d’événements' } }, + }, + }, + + pages: { + cloud_connection_settings: { + label: 'Connexion au cloud', + subtitle: 'Connectez ce runtime à un plan de contrôle ObjectStack pour parcourir les packages privés de votre organisation et les installer ici.', + }, + connect_agent: { + label: 'Connecter un assistant IA', + subtitle: 'Donnez à tout client IA compatible MCP un accès encadré à ce système : chaque appel s’exécute avec les autorisations et la sécurité au niveau des lignes de l’appelant.', + }, + + // Platform record pages (#15743) -- see `en.ts` for the key set. + sys_user_detail: { label: 'Utilisateur' }, + sys_organization_detail: { label: 'Organisation' }, + }, +}; diff --git a/packages/platform-objects/src/apps/translations/index.ts b/packages/platform-objects/src/apps/translations/index.ts index 38ad65732ea..86ea6347120 100644 --- a/packages/platform-objects/src/apps/translations/index.ts +++ b/packages/platform-objects/src/apps/translations/index.ts @@ -11,6 +11,7 @@ export { jaJP } from './ja-JP.js'; export { esES } from './es-ES.js'; export { zhHant } from './zh-Hant.js'; export { ar } from './ar.js'; +export { fr } from './fr.js'; /** * The staleness mechanism (#8765 Option B, extended to the generated bundles by diff --git a/packages/platform-objects/src/apps/translations/metadata-forms-vocabulary.test.ts b/packages/platform-objects/src/apps/translations/metadata-forms-vocabulary.test.ts index f01ff83ea3b..54218217222 100644 --- a/packages/platform-objects/src/apps/translations/metadata-forms-vocabulary.test.ts +++ b/packages/platform-objects/src/apps/translations/metadata-forms-vocabulary.test.ts @@ -16,6 +16,7 @@ import { jaJPMetadataForms } from './ja-JP.metadata-forms.generated.js'; import { esESMetadataForms } from './es-ES.metadata-forms.generated.js'; import { zhHantMetadataForms } from './zh-Hant.metadata-forms.generated.js'; import { arMetadataForms } from './ar.metadata-forms.generated.js'; +import { frMetadataForms } from './fr.metadata-forms.generated.js'; const LOCALES = [ { name: 'en', forms: enMetadataForms as Record }, @@ -24,6 +25,7 @@ const LOCALES = [ { name: 'es-ES', forms: esESMetadataForms as Record }, { name: 'zh-Hant', forms: zhHantMetadataForms as Record }, { name: 'ar', forms: arMetadataForms as Record }, + { name: 'fr', forms: frMetadataForms as Record }, ]; describe('ADR-0090 D2/D3 — metadata-form translations use the v2 vocabulary', () => { diff --git a/packages/platform-objects/src/apps/translations/repeater-row-properties.test.ts b/packages/platform-objects/src/apps/translations/repeater-row-properties.test.ts index d95ca849435..1443170945c 100644 --- a/packages/platform-objects/src/apps/translations/repeater-row-properties.test.ts +++ b/packages/platform-objects/src/apps/translations/repeater-row-properties.test.ts @@ -55,6 +55,7 @@ import { jaJPMetadataForms } from './ja-JP.metadata-forms.generated.js'; import { esESMetadataForms } from './es-ES.metadata-forms.generated.js'; import { zhHantMetadataForms } from './zh-Hant.metadata-forms.generated.js'; import { arMetadataForms } from './ar.metadata-forms.generated.js'; +import { frMetadataForms } from './fr.metadata-forms.generated.js'; const LOCALES = [ { name: 'en', forms: enMetadataForms as Record }, @@ -63,6 +64,7 @@ const LOCALES = [ { name: 'es-ES', forms: esESMetadataForms as Record }, { name: 'zh-Hant', forms: zhHantMetadataForms as Record }, { name: 'ar', forms: arMetadataForms as Record }, + { name: 'fr', forms: frMetadataForms as Record }, ]; /** Every `*.form.ts` the spec package exports, by its export name. */ diff --git a/packages/platform-objects/src/apps/translations/setup-nav-dead-key-tombstone.test.ts b/packages/platform-objects/src/apps/translations/setup-nav-dead-key-tombstone.test.ts index ed09c52265b..bfc4ecc7208 100644 --- a/packages/platform-objects/src/apps/translations/setup-nav-dead-key-tombstone.test.ts +++ b/packages/platform-objects/src/apps/translations/setup-nav-dead-key-tombstone.test.ts @@ -108,8 +108,9 @@ import { jaJP } from './ja-JP.js'; import { esES } from './es-ES.js'; import { zhHant } from './zh-Hant.js'; import { ar } from './ar.js'; +import { fr } from './fr.js'; -const LOCALES = { en, 'zh-CN': zhCN, 'ja-JP': jaJP, 'es-ES': esES, 'zh-Hant': zhHant, ar } as const; +const LOCALES = { en, 'zh-CN': zhCN, 'ja-JP': jaJP, 'es-ES': esES, 'zh-Hant': zhHant, ar, fr } as const; /** Removed Setup nav ids. Delete a line here only together with its nav item. */ const DEAD_SETUP_NAV_IDS = [ diff --git a/packages/platform-objects/src/apps/translations/setup.translation.ts b/packages/platform-objects/src/apps/translations/setup.translation.ts index c346aac1724..39459a555c5 100644 --- a/packages/platform-objects/src/apps/translations/setup.translation.ts +++ b/packages/platform-objects/src/apps/translations/setup.translation.ts @@ -7,30 +7,33 @@ import { jaJP } from './ja-JP.js'; import { esES } from './es-ES.js'; import { zhHant } from './zh-Hant.js'; import { ar } from './ar.js'; +import { fr } from './fr.js'; import { zhCNSourceHashes } from './zh-CN.source-hashes.js'; import { jaJPSourceHashes } from './ja-JP.source-hashes.js'; import { esESSourceHashes } from './es-ES.source-hashes.js'; import { zhHantSourceHashes } from './zh-Hant.source-hashes.js'; import { arSourceHashes } from './ar.source-hashes.js'; +import { frSourceHashes } from './fr.source-hashes.js'; import { zhCNGeneratedSourceHashes } from './zh-CN.source-hashes.generated.js'; import { jaJPGeneratedSourceHashes } from './ja-JP.source-hashes.generated.js'; import { esESGeneratedSourceHashes } from './es-ES.source-hashes.generated.js'; import { zhHantGeneratedSourceHashes } from './zh-Hant.source-hashes.generated.js'; import { arGeneratedSourceHashes } from './ar.source-hashes.generated.js'; +import { frGeneratedSourceHashes } from './fr.source-hashes.generated.js'; import { withSourceFallback } from './source-hash.js'; /** * Setup App — Internationalization (i18n) * * Mirrors the CRM example's `per_locale` convention: each language lives - * in its own file (`en.ts`, `zh-CN.ts`, `ja-JP.ts`, `es-ES.ts`, `zh-Hant.ts`, `ar.ts`) and is + * in its own file (`en.ts`, `zh-CN.ts`, `ja-JP.ts`, `es-ES.ts`, `zh-Hant.ts`, `ar.ts`, `fr.ts`) and is * assembled into a single `TranslationBundle` here. * * Loaded into the kernel's i18n service by `plugin-auth` during * `kernel:ready` (auth is the natural registration point for the Setup * App — see `auth-plugin.ts`). * - * Supported locales: en, zh-CN, ja-JP, es-ES, zh-Hant, ar. + * Supported locales: en, zh-CN, ja-JP, es-ES, zh-Hant, ar, fr. * * ## This assembly is where staleness is resolved (#8765) * @@ -77,4 +80,5 @@ export const SetupAppTranslations: TranslationBundle = { 'es-ES': withSourceFallback(esES, en, esESSourceHashes, esESGeneratedSourceHashes), 'zh-Hant': withSourceFallback(zhHant, en, zhHantSourceHashes, zhHantGeneratedSourceHashes), ar: withSourceFallback(ar, en, arSourceHashes, arGeneratedSourceHashes), + fr: withSourceFallback(fr, en, frSourceHashes, frGeneratedSourceHashes), }; diff --git a/packages/platform-objects/src/apps/translations/source-hash.test.ts b/packages/platform-objects/src/apps/translations/source-hash.test.ts index 04a29568afc..2a160463a6c 100644 --- a/packages/platform-objects/src/apps/translations/source-hash.test.ts +++ b/packages/platform-objects/src/apps/translations/source-hash.test.ts @@ -25,11 +25,13 @@ import { jaJP } from './ja-JP.js'; import { esES } from './es-ES.js'; import { zhHant } from './zh-Hant.js'; import { ar } from './ar.js'; +import { fr } from './fr.js'; import { zhCNSourceHashes } from './zh-CN.source-hashes.js'; import { jaJPSourceHashes } from './ja-JP.source-hashes.js'; import { esESSourceHashes } from './es-ES.source-hashes.js'; import { zhHantSourceHashes } from './zh-Hant.source-hashes.js'; import { arSourceHashes } from './ar.source-hashes.js'; +import { frSourceHashes } from './fr.source-hashes.js'; import { SetupAppTranslations } from './setup.translation.js'; import { collectFilledFromHashes, @@ -45,6 +47,7 @@ import { jaJPGeneratedSourceHashes } from './ja-JP.source-hashes.generated.js'; import { esESGeneratedSourceHashes } from './es-ES.source-hashes.generated.js'; import { zhHantGeneratedSourceHashes } from './zh-Hant.source-hashes.generated.js'; import { arGeneratedSourceHashes } from './ar.source-hashes.generated.js'; +import { frGeneratedSourceHashes } from './fr.source-hashes.generated.js'; // A source bundle and a translation of it, at one nav leaf per app so the // per-locale independence claim has something to be independent about. @@ -195,6 +198,7 @@ describe('the shipped bundles', () => { ['es-ES', esES, esESSourceHashes], ['zh-Hant', zhHant, zhHantSourceHashes], ['ar', ar, arSourceHashes], + ['fr', fr, frSourceHashes], ] as const; // ⭐ The acceptance criterion, applied to the real data: B introduces NO new @@ -394,6 +398,7 @@ describe('the committed provenance companions', () => { 'es-ES': esESGeneratedSourceHashes, 'zh-Hant': zhHantGeneratedSourceHashes, ar: arGeneratedSourceHashes, + fr: frGeneratedSourceHashes, } as const; // ⚠️ Deliberately NOT asserted here: "no shipped leaf is stale". That claim diff --git a/packages/platform-objects/src/identity/invitation-delivery-copy.test.ts b/packages/platform-objects/src/identity/invitation-delivery-copy.test.ts index 9391cf9fd71..0cc4c1937d5 100644 --- a/packages/platform-objects/src/identity/invitation-delivery-copy.test.ts +++ b/packages/platform-objects/src/identity/invitation-delivery-copy.test.ts @@ -31,6 +31,7 @@ import { jaJPObjects } from '../apps/translations/ja-JP.objects.generated.js'; import { esESObjects } from '../apps/translations/es-ES.objects.generated.js'; import { zhHantObjects } from '../apps/translations/zh-Hant.objects.generated.js'; import { arObjects } from '../apps/translations/ar.objects.generated.js'; +import { frObjects } from '../apps/translations/fr.objects.generated.js'; import { INVITE_USER_OUTCOME_MESSAGES, RESEND_INVITATION_OUTCOME_MESSAGES } from './invite-outcome-messages.js'; import { SysInvitation } from './sys-invitation.object.js'; import { SysMember } from './sys-member.object.js'; @@ -81,6 +82,7 @@ const TRANSLATED = [ ['es-ES', esESObjects], ['zh-Hant', zhHantObjects], ['ar', arObjects], + ['fr', frObjects], ] as const; /** Any spelling of "sent" a `link_only` copy must not carry. */ diff --git a/packages/platform-objects/src/identity/invitation-screens-copy.test.ts b/packages/platform-objects/src/identity/invitation-screens-copy.test.ts index a62a3b08f5b..beee8560099 100644 --- a/packages/platform-objects/src/identity/invitation-screens-copy.test.ts +++ b/packages/platform-objects/src/identity/invitation-screens-copy.test.ts @@ -21,6 +21,7 @@ import { jaJPObjects } from '../apps/translations/ja-JP.objects.generated.js'; import { esESObjects } from '../apps/translations/es-ES.objects.generated.js'; import { zhHantObjects } from '../apps/translations/zh-Hant.objects.generated.js'; import { arObjects } from '../apps/translations/ar.objects.generated.js'; +import { frObjects } from '../apps/translations/fr.objects.generated.js'; import { MEMBERSHIP_ROLE_PARAM_HELP } from './membership-role-help.js'; import { SysInvitation } from './sys-invitation.object.js'; import { SysMember } from './sys-member.object.js'; @@ -81,6 +82,7 @@ const TRANSLATED = [ ['es-ES', esESObjects], ['zh-Hant', zhHantObjects], ['ar', arObjects], + ['fr', frObjects], ] as const; const bundleObject = (bundle: unknown, object: string): Record | undefined => diff --git a/packages/platform-objects/src/identity/invite-user-action-copy.test.ts b/packages/platform-objects/src/identity/invite-user-action-copy.test.ts index 30db7026bd6..472f0e4cc1c 100644 --- a/packages/platform-objects/src/identity/invite-user-action-copy.test.ts +++ b/packages/platform-objects/src/identity/invite-user-action-copy.test.ts @@ -31,6 +31,7 @@ import { jaJPObjects } from '../apps/translations/ja-JP.objects.generated.js'; import { esESObjects } from '../apps/translations/es-ES.objects.generated.js'; import { zhHantObjects } from '../apps/translations/zh-Hant.objects.generated.js'; import { arObjects } from '../apps/translations/ar.objects.generated.js'; +import { frObjects } from '../apps/translations/fr.objects.generated.js'; import { SysInvitation } from './sys-invitation.object.js'; import { SysMember } from './sys-member.object.js'; import { SysOrganization } from './sys-organization.object.js'; @@ -80,6 +81,7 @@ const TRANSLATED = [ ['es-ES', esESObjects], ['zh-Hant', zhHantObjects], ['ar', arObjects], + ['fr', frObjects], ] as const; // [#24814] `successMessage` is a `template` slot: the parsed diff --git a/packages/platform-objects/src/identity/sys-api-key-action-copy.test.ts b/packages/platform-objects/src/identity/sys-api-key-action-copy.test.ts index 0e7dff3686d..b846ab31541 100644 --- a/packages/platform-objects/src/identity/sys-api-key-action-copy.test.ts +++ b/packages/platform-objects/src/identity/sys-api-key-action-copy.test.ts @@ -24,6 +24,7 @@ import { jaJPObjects } from '../apps/translations/ja-JP.objects.generated.js'; import { esESObjects } from '../apps/translations/es-ES.objects.generated.js'; import { zhHantObjects } from '../apps/translations/zh-Hant.objects.generated.js'; import { arObjects } from '../apps/translations/ar.objects.generated.js'; +import { frObjects } from '../apps/translations/fr.objects.generated.js'; import { SysApiKey } from './sys-api-key.object.js'; interface CopyAction { @@ -42,6 +43,7 @@ const TRANSLATED = [ ['es-ES', esESObjects], ['zh-Hant', zhHantObjects], ['ar', arObjects], + ['fr', frObjects], ] as const; // `successMessage` is a `template` slot: the parsed object carries the diff --git a/packages/platform-objects/src/identity/transfer-ownership-action-copy.test.ts b/packages/platform-objects/src/identity/transfer-ownership-action-copy.test.ts index 835901ea86d..a0af4156663 100644 --- a/packages/platform-objects/src/identity/transfer-ownership-action-copy.test.ts +++ b/packages/platform-objects/src/identity/transfer-ownership-action-copy.test.ts @@ -23,6 +23,7 @@ import { jaJPObjects } from '../apps/translations/ja-JP.objects.generated.js'; import { esESObjects } from '../apps/translations/es-ES.objects.generated.js'; import { zhHantObjects } from '../apps/translations/zh-Hant.objects.generated.js'; import { arObjects } from '../apps/translations/ar.objects.generated.js'; +import { frObjects } from '../apps/translations/fr.objects.generated.js'; import { SysMember } from './sys-member.object.js'; // [#27948] An action's `description`, each param's `helpText` and each @@ -83,6 +84,7 @@ const LOCALES = [ ['es-ES', esESObjects], ['zh-Hant', zhHantObjects], ['ar', arObjects], + ['fr', frObjects], ] as const; describe('sys_member.transfer_ownership — declared copy', () => { diff --git a/packages/platform-objects/src/metadata-translations/index.ts b/packages/platform-objects/src/metadata-translations/index.ts index f4aa38ce7b5..fdb6323ff75 100644 --- a/packages/platform-objects/src/metadata-translations/index.ts +++ b/packages/platform-objects/src/metadata-translations/index.ts @@ -7,11 +7,13 @@ import { jaJPMetadataForms } from '../apps/translations/ja-JP.metadata-forms.gen import { esESMetadataForms } from '../apps/translations/es-ES.metadata-forms.generated.js'; import { zhHantMetadataForms } from '../apps/translations/zh-Hant.metadata-forms.generated.js'; import { arMetadataForms } from '../apps/translations/ar.metadata-forms.generated.js'; +import { frMetadataForms } from '../apps/translations/fr.metadata-forms.generated.js'; import { zhCNGeneratedSourceHashes } from '../apps/translations/zh-CN.source-hashes.generated.js'; import { jaJPGeneratedSourceHashes } from '../apps/translations/ja-JP.source-hashes.generated.js'; import { esESGeneratedSourceHashes } from '../apps/translations/es-ES.source-hashes.generated.js'; import { zhHantGeneratedSourceHashes } from '../apps/translations/zh-Hant.source-hashes.generated.js'; import { arGeneratedSourceHashes } from '../apps/translations/ar.source-hashes.generated.js'; +import { frGeneratedSourceHashes } from '../apps/translations/fr.source-hashes.generated.js'; import { withSourceFallback } from '../apps/translations/source-hash.js'; /** The source bundle the translated locales are judged against — `en` is a copy of it. */ @@ -49,4 +51,5 @@ export const MetadataFormsTranslations: TranslationBundle = { 'es-ES': withSourceFallback({ metadataForms: esESMetadataForms }, enSource, undefined, esESGeneratedSourceHashes), 'zh-Hant': withSourceFallback({ metadataForms: zhHantMetadataForms }, enSource, undefined, zhHantGeneratedSourceHashes), ar: withSourceFallback({ metadataForms: arMetadataForms }, enSource, undefined, arGeneratedSourceHashes), + fr: withSourceFallback({ metadataForms: frMetadataForms }, enSource, undefined, frGeneratedSourceHashes), }; diff --git a/packages/platform-objects/src/pages/sys-organization.page.ts b/packages/platform-objects/src/pages/sys-organization.page.ts index df421c82e0f..b2645e10815 100644 --- a/packages/platform-objects/src/pages/sys-organization.page.ts +++ b/packages/platform-objects/src/pages/sys-organization.page.ts @@ -80,7 +80,7 @@ export const SysOrganizationDetailPage: Page = { position: 'top', items: [ { - label: { en: 'Members', 'zh-CN': '成员', 'ja-JP': 'メンバー', 'es-ES': 'Miembros', 'zh-Hant': '成員', ar: 'الأعضاء' }, + label: { en: 'Members', 'zh-CN': '成员', 'ja-JP': 'メンバー', 'es-ES': 'Miembros', 'zh-Hant': '成員', ar: 'الأعضاء', fr: 'Membres' }, icon: 'users', children: [ { @@ -92,13 +92,13 @@ export const SysOrganizationDetailPage: Page = { sort: [{ field: 'created_at', order: 'desc' }], limit: 25, showViewAll: true, - title: { en: 'Members', 'zh-CN': '成员', 'ja-JP': 'メンバー', 'es-ES': 'Miembros', 'zh-Hant': '成員', ar: 'الأعضاء' }, + title: { en: 'Members', 'zh-CN': '成员', 'ja-JP': 'メンバー', 'es-ES': 'Miembros', 'zh-Hant': '成員', ar: 'الأعضاء', fr: 'Membres' }, }, }, ], }, { - label: { en: 'Invitations', 'zh-CN': '邀请', 'ja-JP': '招待', 'es-ES': 'Invitaciones', 'zh-Hant': '邀請', ar: 'الدعوات' }, + label: { en: 'Invitations', 'zh-CN': '邀请', 'ja-JP': '招待', 'es-ES': 'Invitaciones', 'zh-Hant': '邀請', ar: 'الدعوات', fr: 'Invitations' }, icon: 'mail', children: [ { @@ -110,13 +110,13 @@ export const SysOrganizationDetailPage: Page = { sort: [{ field: 'created_at', order: 'desc' }], limit: 25, showViewAll: true, - title: { en: 'Invitations', 'zh-CN': '邀请', 'ja-JP': '招待', 'es-ES': 'Invitaciones', 'zh-Hant': '邀請', ar: 'الدعوات' }, + title: { en: 'Invitations', 'zh-CN': '邀请', 'ja-JP': '招待', 'es-ES': 'Invitaciones', 'zh-Hant': '邀請', ar: 'الدعوات', fr: 'Invitations' }, }, }, ], }, { - label: { en: 'Teams', 'zh-CN': '团队', 'ja-JP': 'チーム', 'es-ES': 'Equipos', 'zh-Hant': '團隊', ar: 'الفرق' }, + label: { en: 'Teams', 'zh-CN': '团队', 'ja-JP': 'チーム', 'es-ES': 'Equipos', 'zh-Hant': '團隊', ar: 'الفرق', fr: 'Équipes' }, icon: 'users-round', children: [ { @@ -128,7 +128,7 @@ export const SysOrganizationDetailPage: Page = { sort: [{ field: 'name', order: 'asc' }], limit: 25, showViewAll: true, - title: { en: 'Teams', 'zh-CN': '团队', 'ja-JP': 'チーム', 'es-ES': 'Equipos', 'zh-Hant': '團隊', ar: 'الفرق' }, + title: { en: 'Teams', 'zh-CN': '团队', 'ja-JP': 'チーム', 'es-ES': 'Equipos', 'zh-Hant': '團隊', ar: 'الفرق', fr: 'Équipes' }, }, }, ], diff --git a/packages/platform-objects/src/pages/sys-user-ai-assistants-tab.test.ts b/packages/platform-objects/src/pages/sys-user-ai-assistants-tab.test.ts index 2656464b215..08bd3abc61c 100644 --- a/packages/platform-objects/src/pages/sys-user-ai-assistants-tab.test.ts +++ b/packages/platform-objects/src/pages/sys-user-ai-assistants-tab.test.ts @@ -99,6 +99,7 @@ describe('sys_user_detail — the AI Assistants tab', () => { 'es-ES': 'Asistentes de IA', 'zh-Hant': 'AI 助理', ar: 'مساعدو الذكاء الاصطناعي', + fr: 'Assistants IA', }); for (const sibling of items) { expect(Object.keys(sibling.label as Rec).sort(), String((sibling.label as Rec).en)).toEqual( diff --git a/packages/platform-objects/src/pages/sys-user-details-tab.test.ts b/packages/platform-objects/src/pages/sys-user-details-tab.test.ts index a9274315b76..66d91ca1b7a 100644 --- a/packages/platform-objects/src/pages/sys-user-details-tab.test.ts +++ b/packages/platform-objects/src/pages/sys-user-details-tab.test.ts @@ -34,17 +34,17 @@ const HIDE_FIELDS = [ ]; const SECTIONS = [ { - label: { en: 'Identity', 'zh-CN': '身份', 'ja-JP': 'アイデンティティ', 'es-ES': 'Identidad', 'zh-Hant': '身分', ar: 'الهوية' }, + label: { en: 'Identity', 'zh-CN': '身份', 'ja-JP': 'アイデンティティ', 'es-ES': 'Identidad', 'zh-Hant': '身分', ar: 'الهوية', fr: 'Identité' }, // #27859: a saved Locale is shown; the renderer draws only authored sections. fields: ['name', 'image', 'locale'], }, { // #27859: a banned user reads as banned, with the reason, not as active. - label: { en: 'Account Status', 'zh-CN': '账号状态', 'ja-JP': 'アカウントの状態', 'es-ES': 'Estado de la cuenta', 'zh-Hant': '帳號狀態', ar: 'حالة الحساب' }, + label: { en: 'Account Status', 'zh-CN': '账号状态', 'ja-JP': 'アカウントの状態', 'es-ES': 'Estado de la cuenta', 'zh-Hant': '帳號狀態', ar: 'حالة الحساب', fr: 'État du compte' }, fields: ['banned', 'ban_reason', 'ban_expires'], }, { - label: { en: 'Audit', 'zh-CN': '审计', 'ja-JP': '監査', 'es-ES': 'Auditoría', 'zh-Hant': '稽核', ar: 'التدقيق' }, + label: { en: 'Audit', 'zh-CN': '审计', 'ja-JP': '監査', 'es-ES': 'Auditoría', 'zh-Hant': '稽核', ar: 'التدقيق', fr: 'Audit' }, fields: ['created_at', 'updated_at'], }, ]; diff --git a/packages/platform-objects/src/pages/sys-user.page.ts b/packages/platform-objects/src/pages/sys-user.page.ts index 6ea01d3d1e0..109bc3d71e2 100644 --- a/packages/platform-objects/src/pages/sys-user.page.ts +++ b/packages/platform-objects/src/pages/sys-user.page.ts @@ -143,6 +143,7 @@ export const SysUserDetailPage: Page = { 'es-ES': 'Correo no verificado', 'zh-Hant': '電子郵件尚未驗證', ar: 'البريد الإلكتروني غير موثَّق', + fr: 'E-mail non vérifié', }, body: { en: 'Verify your email to receive password resets and important system notifications.', @@ -151,6 +152,7 @@ export const SysUserDetailPage: Page = { 'es-ES': 'Verifica tu correo para recibir restablecimientos de contraseña y notificaciones importantes del sistema.', 'zh-Hant': '驗證你的電子郵件,以接收密碼重設和重要的系統通知。', ar: 'وثّق بريدك الإلكتروني لتتلقى رسائل إعادة تعيين كلمة المرور وإشعارات النظام المهمة.', + fr: 'Vérifiez votre e-mail pour recevoir les réinitialisations de mot de passe et les notifications système importantes.', }, dismissible: false, action: { @@ -162,6 +164,7 @@ export const SysUserDetailPage: Page = { 'es-ES': 'Reenviar correo de verificación', 'zh-Hant': '重新寄送驗證郵件', ar: 'إعادة إرسال رسالة التوثيق', + fr: 'Renvoyer l’e-mail de vérification', }, }, }, @@ -204,7 +207,7 @@ export const SysUserDetailPage: Page = { // authored sections, so a field none of them names is not on // the page: `locale` rides Identity, and the ban state has its // own section, so a banned user does not read as active. - label: { en: 'Details', 'zh-CN': '详情', 'ja-JP': '詳細', 'es-ES': 'Detalles', 'zh-Hant': '詳細資料', ar: 'التفاصيل' }, + label: { en: 'Details', 'zh-CN': '详情', 'ja-JP': '詳細', 'es-ES': 'Detalles', 'zh-Hant': '詳細資料', ar: 'التفاصيل', fr: 'Détails' }, icon: 'file-text', children: [ { @@ -221,15 +224,15 @@ export const SysUserDetailPage: Page = { ], sections: [ { - label: { en: 'Identity', 'zh-CN': '身份', 'ja-JP': 'アイデンティティ', 'es-ES': 'Identidad', 'zh-Hant': '身分', ar: 'الهوية' }, + label: { en: 'Identity', 'zh-CN': '身份', 'ja-JP': 'アイデンティティ', 'es-ES': 'Identidad', 'zh-Hant': '身分', ar: 'الهوية', fr: 'Identité' }, fields: ['name', 'image', 'locale'], }, { - label: { en: 'Account Status', 'zh-CN': '账号状态', 'ja-JP': 'アカウントの状態', 'es-ES': 'Estado de la cuenta', 'zh-Hant': '帳號狀態', ar: 'حالة الحساب' }, + label: { en: 'Account Status', 'zh-CN': '账号状态', 'ja-JP': 'アカウントの状態', 'es-ES': 'Estado de la cuenta', 'zh-Hant': '帳號狀態', ar: 'حالة الحساب', fr: 'État du compte' }, fields: ['banned', 'ban_reason', 'ban_expires'], }, { - label: { en: 'Audit', 'zh-CN': '审计', 'ja-JP': '監査', 'es-ES': 'Auditoría', 'zh-Hant': '稽核', ar: 'التدقيق' }, + label: { en: 'Audit', 'zh-CN': '审计', 'ja-JP': '監査', 'es-ES': 'Auditoría', 'zh-Hant': '稽核', ar: 'التدقيق', fr: 'Audit' }, fields: ['created_at', 'updated_at'], }, ], @@ -238,7 +241,7 @@ export const SysUserDetailPage: Page = { ], }, { - label: { en: 'Positions', 'zh-CN': '岗位', 'ja-JP': 'ポジション', 'es-ES': 'Puestos', 'zh-Hant': '職位', ar: 'المناصب' }, + label: { en: 'Positions', 'zh-CN': '岗位', 'ja-JP': 'ポジション', 'es-ES': 'Puestos', 'zh-Hant': '職位', ar: 'المناصب', fr: 'Postes' }, icon: 'shield-check', children: [ { @@ -258,7 +261,7 @@ export const SysUserDetailPage: Page = { sort: [{ field: 'created_at', order: 'desc' }], limit: 25, showViewAll: true, - title: { en: 'Positions', 'zh-CN': '岗位', 'ja-JP': 'ポジション', 'es-ES': 'Puestos', 'zh-Hant': '職位', ar: 'المناصب' }, + title: { en: 'Positions', 'zh-CN': '岗位', 'ja-JP': 'ポジション', 'es-ES': 'Puestos', 'zh-Hant': '職位', ar: 'المناصب', fr: 'Postes' }, }, }, ], @@ -281,7 +284,7 @@ export const SysUserDetailPage: Page = { // a row showing only the set would read as a live grant when it is // not. The same form sets the window, and the object refuses a // window whose end is not after its start. - label: { en: 'Permission Sets', 'zh-CN': '权限集', 'ja-JP': '権限セット', 'es-ES': 'Conjuntos de permisos', 'zh-Hant': '權限集', ar: 'مجموعات الأذونات' }, + label: { en: 'Permission Sets', 'zh-CN': '权限集', 'ja-JP': '権限セット', 'es-ES': 'Conjuntos de permisos', 'zh-Hant': '權限集', ar: 'مجموعات الأذونات', fr: 'Ensembles d’autorisations' }, icon: 'lock', children: [ { @@ -293,7 +296,7 @@ export const SysUserDetailPage: Page = { sort: [{ field: 'created_at', order: 'desc' }], limit: 25, showViewAll: true, - title: { en: 'Permission Sets', 'zh-CN': '权限集', 'ja-JP': '権限セット', 'es-ES': 'Conjuntos de permisos', 'zh-Hant': '權限集', ar: 'مجموعات الأذونات' }, + title: { en: 'Permission Sets', 'zh-CN': '权限集', 'ja-JP': '権限セット', 'es-ES': 'Conjuntos de permisos', 'zh-Hant': '權限集', ar: 'مجموعات الأذونات', fr: 'Ensembles d’autorisations' }, }, }, ], @@ -306,7 +309,7 @@ export const SysUserDetailPage: Page = { // The Add picker binds a BU by its id (linkField: 'business_unit_id'); // sys_business_unit has no `label` field so the picker labels rows by // its display field `name`. - label: { en: 'Business Units', 'zh-CN': '业务单元', 'ja-JP': 'ビジネスユニット', 'es-ES': 'Unidades de negocio', 'zh-Hant': '業務單位', ar: 'وحدات الأعمال' }, + label: { en: 'Business Units', 'zh-CN': '业务单元', 'ja-JP': 'ビジネスユニット', 'es-ES': 'Unidades de negocio', 'zh-Hant': '業務單位', ar: 'وحدات الأعمال', fr: 'Unités commerciales' }, icon: 'network', children: [ { @@ -318,7 +321,7 @@ export const SysUserDetailPage: Page = { sort: [{ field: 'created_at', order: 'desc' }], limit: 25, showViewAll: true, - title: { en: 'Business Units', 'zh-CN': '业务单元', 'ja-JP': 'ビジネスユニット', 'es-ES': 'Unidades de negocio', 'zh-Hant': '業務單位', ar: 'وحدات الأعمال' }, + title: { en: 'Business Units', 'zh-CN': '业务单元', 'ja-JP': 'ビジネスユニット', 'es-ES': 'Unidades de negocio', 'zh-Hant': '業務單位', ar: 'وحدات الأعمال', fr: 'Unités commerciales' }, add: { picker: { object: 'sys_business_unit', @@ -332,6 +335,7 @@ export const SysUserDetailPage: Page = { 'es-ES': 'Añadir a unidad de negocio', 'zh-Hant': '加入業務單位', ar: 'إضافة إلى وحدة أعمال', + fr: 'Ajouter à une unité commerciale', }, }, }, @@ -339,7 +343,7 @@ export const SysUserDetailPage: Page = { ], }, { - label: { en: 'Sessions', 'zh-CN': '会话', 'ja-JP': 'セッション', 'es-ES': 'Sesiones', 'zh-Hant': '工作階段', ar: 'الجلسات' }, + label: { en: 'Sessions', 'zh-CN': '会话', 'ja-JP': 'セッション', 'es-ES': 'Sesiones', 'zh-Hant': '工作階段', ar: 'الجلسات', fr: 'Sessions' }, icon: 'monitor', children: [ { @@ -351,13 +355,13 @@ export const SysUserDetailPage: Page = { sort: [{ field: 'created_at', order: 'desc' }], limit: 25, showViewAll: true, - title: { en: 'Sessions', 'zh-CN': '会话', 'ja-JP': 'セッション', 'es-ES': 'Sesiones', 'zh-Hant': '工作階段', ar: 'الجلسات' }, + title: { en: 'Sessions', 'zh-CN': '会话', 'ja-JP': 'セッション', 'es-ES': 'Sesiones', 'zh-Hant': '工作階段', ar: 'الجلسات', fr: 'Sessions' }, }, }, ], }, { - label: { en: 'Linked Accounts', 'zh-CN': '关联账号', 'ja-JP': '連携アカウント', 'es-ES': 'Cuentas vinculadas', 'zh-Hant': '已關聯帳戶', ar: 'الحسابات المرتبطة' }, + label: { en: 'Linked Accounts', 'zh-CN': '关联账号', 'ja-JP': '連携アカウント', 'es-ES': 'Cuentas vinculadas', 'zh-Hant': '已關聯帳戶', ar: 'الحسابات المرتبطة', fr: 'Comptes liés' }, icon: 'link', children: [ { @@ -369,13 +373,13 @@ export const SysUserDetailPage: Page = { sort: [{ field: 'created_at', order: 'desc' }], limit: 25, showViewAll: true, - title: { en: 'Linked Accounts', 'zh-CN': '关联账号', 'ja-JP': '連携アカウント', 'es-ES': 'Cuentas vinculadas', 'zh-Hant': '已關聯帳戶', ar: 'الحسابات المرتبطة' }, + title: { en: 'Linked Accounts', 'zh-CN': '关联账号', 'ja-JP': '連携アカウント', 'es-ES': 'Cuentas vinculadas', 'zh-Hant': '已關聯帳戶', ar: 'الحسابات المرتبطة', fr: 'Comptes liés' }, }, }, ], }, { - label: { en: 'Organizations', 'zh-CN': '组织', 'ja-JP': '組織', 'es-ES': 'Organizaciones', 'zh-Hant': '組織', ar: 'المؤسسات' }, + label: { en: 'Organizations', 'zh-CN': '组织', 'ja-JP': '組織', 'es-ES': 'Organizaciones', 'zh-Hant': '組織', ar: 'المؤسسات', fr: 'Organisations' }, icon: 'building-2', children: [ { @@ -387,13 +391,13 @@ export const SysUserDetailPage: Page = { sort: [{ field: 'created_at', order: 'desc' }], limit: 25, showViewAll: true, - title: { en: 'Organizations', 'zh-CN': '组织', 'ja-JP': '組織', 'es-ES': 'Organizaciones', 'zh-Hant': '組織', ar: 'المؤسسات' }, + title: { en: 'Organizations', 'zh-CN': '组织', 'ja-JP': '組織', 'es-ES': 'Organizaciones', 'zh-Hant': '組織', ar: 'المؤسسات', fr: 'Organisations' }, }, }, ], }, { - label: { en: 'OAuth Apps', 'zh-CN': 'OAuth 应用', 'ja-JP': 'OAuth アプリ', 'es-ES': 'Aplicaciones OAuth', 'zh-Hant': 'OAuth 應用程式', ar: 'تطبيقات OAuth' }, + label: { en: 'OAuth Apps', 'zh-CN': 'OAuth 应用', 'ja-JP': 'OAuth アプリ', 'es-ES': 'Aplicaciones OAuth', 'zh-Hant': 'OAuth 應用程式', ar: 'تطبيقات OAuth', fr: 'Applications OAuth' }, icon: 'key-square', children: [ { @@ -405,7 +409,7 @@ export const SysUserDetailPage: Page = { sort: [{ field: 'created_at', order: 'desc' }], limit: 25, showViewAll: true, - title: { en: 'OAuth Apps', 'zh-CN': 'OAuth 应用', 'ja-JP': 'OAuth アプリ', 'es-ES': 'Aplicaciones OAuth', 'zh-Hant': 'OAuth 應用程式', ar: 'تطبيقات OAuth' }, + title: { en: 'OAuth Apps', 'zh-CN': 'OAuth 应用', 'ja-JP': 'OAuth アプリ', 'es-ES': 'Aplicaciones OAuth', 'zh-Hant': 'OAuth 應用程式', ar: 'تطبيقات OAuth', fr: 'Applications OAuth' }, }, }, ], @@ -430,7 +434,7 @@ export const SysUserDetailPage: Page = { // reasons: this raw `Page` literal reaches the wire verbatim, and // only the `{ dialect: 'cel' }` envelope routes to CEL. { - label: { en: 'Security', 'zh-CN': '安全', 'ja-JP': 'セキュリティ', 'es-ES': 'Seguridad', 'zh-Hant': '安全', ar: 'الأمان' }, + label: { en: 'Security', 'zh-CN': '安全', 'ja-JP': 'セキュリティ', 'es-ES': 'Seguridad', 'zh-Hant': '安全', ar: 'الأمان', fr: 'Sécurité' }, icon: 'shield', visibleWhen: P`has(record.id) && record.id == ctx.user.id`, children: [ @@ -445,6 +449,7 @@ export const SysUserDetailPage: Page = { 'es-ES': 'Contraseña e inicio de sesión', 'zh-Hant': '密碼與登入', ar: 'كلمة المرور وتسجيل الدخول', + fr: 'Mot de passe et connexion', }, }, }, @@ -459,6 +464,7 @@ export const SysUserDetailPage: Page = { 'es-ES': 'Cambia tu contraseña o la dirección de correo asociada a esta cuenta.', 'zh-Hant': '變更密碼或此帳號繫結的電子郵件地址。', ar: 'غيّر كلمة المرور أو عنوان البريد الإلكتروني المرتبط بهذا الحساب.', + fr: 'Modifiez votre mot de passe ou l’adresse e-mail associée à ce compte.', }, }, }, @@ -488,6 +494,7 @@ export const SysUserDetailPage: Page = { 'es-ES': 'Autenticación de dos factores', 'zh-Hant': '兩步驟驗證', ar: 'المصادقة الثنائية', + fr: 'Authentification à deux facteurs', }, }, }, @@ -503,6 +510,7 @@ export const SysUserDetailPage: Page = { 'es-ES': 'Añade una segunda capa de seguridad con una app de autenticación TOTP. Los códigos de respaldo te permiten iniciar sesión si pierdes tu dispositivo.', 'zh-Hant': '使用 TOTP 驗證器應用程式新增第二層安全防護。裝置遺失時,可使用備用驗證碼登入。', ar: 'أضف طبقة أمان ثانية باستخدام تطبيق مصادقة TOTP. تتيح لك الرموز الاحتياطية تسجيل الدخول إذا فقدت جهازك.', + fr: 'Ajoutez une seconde couche de sécurité avec une application d’authentification TOTP. Les codes de secours vous permettent de vous connecter si vous perdez votre appareil.', }, }, }, @@ -536,6 +544,7 @@ export const SysUserDetailPage: Page = { 'es-ES': 'Verificación de correo', 'zh-Hant': '電子郵件驗證', ar: 'توثيق البريد الإلكتروني', + fr: 'Vérification de l’e-mail', }, }, }, @@ -550,6 +559,7 @@ export const SysUserDetailPage: Page = { 'es-ES': 'Verifica tu correo para recibir restablecimientos de contraseña y notificaciones. El botón solo aparece mientras la verificación está pendiente.', 'zh-Hant': '驗證你的電子郵件,以便接收密碼重設和系統通知。此按鈕僅在電子郵件待驗證時顯示。', ar: 'وثّق بريدك الإلكتروني لتصلك رسائل إعادة تعيين كلمة المرور والإشعارات. لا يظهر الزر إلا ما دام التوثيق معلّقًا.', + fr: 'Vérifiez votre e-mail pour recevoir les réinitialisations de mot de passe et les notifications. Le bouton n’apparaît que tant que la vérification est en attente.', }, }, }, @@ -573,6 +583,7 @@ export const SysUserDetailPage: Page = { 'es-ES': 'Zona de peligro', 'zh-Hant': '危險操作', ar: 'منطقة الخطر', + fr: 'Zone de danger', }, }, className: 'text-destructive', @@ -588,6 +599,7 @@ export const SysUserDetailPage: Page = { 'es-ES': 'Permanente. Una vez eliminada, tu cuenta no se puede recuperar.', 'zh-Hant': '此動作無法復原,帳號一經刪除即無法恢復。', ar: 'إجراء نهائي. بمجرد حذف حسابك، لا يمكن استعادته.', + fr: 'Action définitive. Une fois supprimé, votre compte ne peut pas être récupéré.', }, }, }, @@ -606,7 +618,7 @@ export const SysUserDetailPage: Page = { // user_id FK; the sys_api_key object's own list-item actions // (revoke / restore) handle row operations. { - label: { en: 'API Keys', 'zh-CN': 'API 密钥', 'ja-JP': 'API キー', 'es-ES': 'Claves de API', 'zh-Hant': 'API 金鑰', ar: 'مفاتيح API' }, + label: { en: 'API Keys', 'zh-CN': 'API 密钥', 'ja-JP': 'API キー', 'es-ES': 'Claves de API', 'zh-Hant': 'API 金鑰', ar: 'مفاتيح API', fr: 'Clés API' }, icon: 'key-round', children: [ { @@ -618,7 +630,7 @@ export const SysUserDetailPage: Page = { sort: [{ field: 'created_at', order: 'desc' }], limit: 25, showViewAll: true, - title: { en: 'API Keys', 'zh-CN': 'API 密钥', 'ja-JP': 'API キー', 'es-ES': 'Claves de API', 'zh-Hant': 'API 金鑰', ar: 'مفاتيح API' }, + title: { en: 'API Keys', 'zh-CN': 'API 密钥', 'ja-JP': 'API キー', 'es-ES': 'Claves de API', 'zh-Hant': 'API 金鑰', ar: 'مفاتيح API', fr: 'Clés API' }, }, }, ], @@ -647,7 +659,7 @@ export const SysUserDetailPage: Page = { // anyone else either way. Same `P` envelope as the Security tab: // this raw `Page` literal reaches the wire verbatim. { - label: { en: 'AI Assistants', 'zh-CN': 'AI 助手', 'ja-JP': 'AI アシスタント', 'es-ES': 'Asistentes de IA', 'zh-Hant': 'AI 助理', ar: 'مساعدو الذكاء الاصطناعي' }, + label: { en: 'AI Assistants', 'zh-CN': 'AI 助手', 'ja-JP': 'AI アシスタント', 'es-ES': 'Asistentes de IA', 'zh-Hant': 'AI 助理', ar: 'مساعدو الذكاء الاصطناعي', fr: 'Assistants IA' }, icon: 'bot', visibleWhen: P`current_user.isPlatformAdmin == true`, children: [ @@ -668,7 +680,7 @@ export const SysUserDetailPage: Page = { // object's `managedBy: 'system-data'` and the caller's grant, the // same verdict the grid's buttons read. { - label: { en: 'Preferences', 'zh-CN': '偏好设置', 'ja-JP': '設定', 'es-ES': 'Preferencias', 'zh-Hant': '偏好設定', ar: 'التفضيلات' }, + label: { en: 'Preferences', 'zh-CN': '偏好设置', 'ja-JP': '設定', 'es-ES': 'Preferencias', 'zh-Hant': '偏好設定', ar: 'التفضيلات', fr: 'Préférences' }, icon: 'sliders', children: [ { @@ -680,7 +692,7 @@ export const SysUserDetailPage: Page = { sort: [{ field: 'updated_at', order: 'desc' }], limit: 25, showViewAll: true, - title: { en: 'Preferences', 'zh-CN': '偏好设置', 'ja-JP': '設定', 'es-ES': 'Preferencias', 'zh-Hant': '偏好設定', ar: 'التفضيلات' }, + title: { en: 'Preferences', 'zh-CN': '偏好设置', 'ja-JP': '設定', 'es-ES': 'Preferencias', 'zh-Hant': '偏好設定', ar: 'التفضيلات', fr: 'Préférences' }, }, }, ], diff --git a/packages/platform-objects/src/plugin.translations.test.ts b/packages/platform-objects/src/plugin.translations.test.ts index 37ec2afb8f3..3e801fc4a98 100644 --- a/packages/platform-objects/src/plugin.translations.test.ts +++ b/packages/platform-objects/src/plugin.translations.test.ts @@ -191,8 +191,8 @@ describe('PlatformObjectsPlugin: a host translation bundle wins over the platfor await boot(ctx); - // Two bundles (Setup, MetadataForms) times the six locales each ships. - expect(loads.sort()).toEqual(['ar', 'ar', 'en', 'en', 'es-ES', 'es-ES', 'ja-JP', 'ja-JP', 'zh-CN', 'zh-CN', 'zh-Hant', 'zh-Hant']); + // Two bundles (Setup, MetadataForms) times the seven locales each ships. + expect(loads.sort()).toEqual(['ar', 'ar', 'en', 'en', 'es-ES', 'es-ES', 'fr', 'fr', 'ja-JP', 'ja-JP', 'zh-CN', 'zh-CN', 'zh-Hant', 'zh-Hant']); }); it('fail-safe: a provider with no getTranslations still gets the platform bundles, and says so', async () => { diff --git a/packages/plugins/plugin-approvals/src/translations/approval-status-vocabulary.test.ts b/packages/plugins/plugin-approvals/src/translations/approval-status-vocabulary.test.ts index c43e8cf99aa..bab0637cc29 100644 --- a/packages/plugins/plugin-approvals/src/translations/approval-status-vocabulary.test.ts +++ b/packages/plugins/plugin-approvals/src/translations/approval-status-vocabulary.test.ts @@ -41,7 +41,7 @@ import { describe, it, expect } from 'vitest'; import { APPROVAL_STATUSES, APPROVAL_STATUS_LABELS } from '@objectstack/spec/contracts'; import { translateObject, resolveViewLabel } from '@objectstack/spec/system'; import type { TranslationData } from '@objectstack/spec/system'; -import { zhCN, jaJP, esES, zhHant, ar } from '@objectstack/platform-objects/apps'; +import { zhCN, jaJP, esES, zhHant, ar, fr } from '@objectstack/platform-objects/apps'; import { ApprovalsTranslations } from './index.js'; import { SysApprovalRequest } from '../sys-approval-request.object.js'; @@ -142,6 +142,7 @@ describe('approval status vocabulary (#7232)', () => { ['es-ES', esES], ['zh-Hant', zhHant], ['ar', ar], + ['fr', fr], ] as const) { const navLabel = navApprovalsLabel(nav); expect(navLabel, `platform-objects lost the ${locale} nav_account_approvals label`).toBeTruthy(); diff --git a/packages/plugins/plugin-audit/src/activity-summary-fr-sys-user-labels.test.ts b/packages/plugins/plugin-audit/src/activity-summary-fr-sys-user-labels.test.ts new file mode 100644 index 00000000000..0d1f9270f7a --- /dev/null +++ b/packages/plugins/plugin-audit/src/activity-summary-fr-sys-user-labels.test.ts @@ -0,0 +1,85 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * A French reader's Home feed names `sys_user` fields in French (#28107). + * + * The Home feed lists `sys_activity` rows, and their summary is re-composed + * for each reader in the locale the door resolved for them: the console sends + * the signed-in user's `sys_user.locale` as `Accept-Language`, and + * {@link readerSummaryLocale} negotiates it against the locales the `i18n` + * service holds. The field names inside the sentence are looked up as + * `objects.sys_user.fields..label` in that locale, and fall back to the + * authored English label when the locale has none. + * + * Before `@objectstack/platform-objects` shipped `fr`, that lookup missed: this + * plugin's own `fr` catalog answered the frame, so a user whose locale was `fr` + * read `A modifié Utilisateur « Carl Edited » : Banned et Ban Reason` (French + * glue, English field labels). Here the i18n service holds what a booted kernel + * holds, this plugin's bundle and the platform's Setup bundle in every locale + * each ships, and the sentence is composed the way the feed's read composes it. + */ + +import { describe, expect, it } from 'vitest'; +import { createMemoryI18n } from '@objectstack/core'; +import { SetupAppTranslations } from '@objectstack/platform-objects/apps'; +import { SysUser } from '@objectstack/platform-objects/identity'; + +import { composeActivitySummary, readerSummaryLocale } from './activity-summary-message.js'; +import type { ActivitySummaryMessage } from './activity-summary-message.js'; +import { AuditTranslations } from './translations/index.js'; + +function bootedI18n() { + const i18n = createMemoryI18n(); + for (const bundle of [AuditTranslations, SetupAppTranslations]) { + for (const [locale, data] of Object.entries(bundle)) { + i18n.loadTranslations(locale, data as Record); + } + } + return i18n; +} + +const objectDef = (name: string) => (name === 'sys_user' ? SysUser : null); + +/** The #27869 (b) walkthrough row: an admin banned Carl and gave a reason. */ +const UPDATED_FIELDS: ActivitySummaryMessage = { + key: 'activityUpdatedFields', + object: 'sys_user', + label: 'Carl Edited', + fields: ['banned', 'ban_reason'], + more: 0, +}; + +describe('Home feed — a fr reader reads sys_user field labels in French (#28107)', () => { + it('the platform Setup bundle ships fr', () => { + expect(Object.keys(SetupAppTranslations)).toContain('fr'); + }); + + for (const requested of ['fr', 'fr-FR']) { + it(`a reader whose locale is ${requested} reads the fields in French, not in English`, () => { + const locale = readerSummaryLocale(bootedI18n(), requested, objectDef); + expect(locale?.locale).toBe('fr'); + + const summary = composeActivitySummary(UPDATED_FIELDS, locale!); + expect(summary).toBe('A modifié Utilisateur « Carl Edited » : Banni et Motif du bannissement'); + expect(summary).not.toMatch(/Banned|Ban Reason/); + }); + } + + it('a tracked change names the field in French too', () => { + const locale = readerSummaryLocale(bootedI18n(), 'fr', objectDef)!; + const summary = composeActivitySummary( + { + key: 'fieldChanges', + object: 'sys_user', + changes: [{ field: 'ban_reason', from: { text: 'spam' }, to: { text: 'abuse' } }], + }, + locale, + ); + expect(summary).toBe('Motif du bannissement: spam → abuse'); + }); + + it('control: the English reader still reads the authored labels', () => { + const locale = readerSummaryLocale(bootedI18n(), 'en', objectDef)!; + expect(composeActivitySummary(UPDATED_FIELDS, locale)).toBe('Updated User "Carl Edited": Banned and Ban Reason'); + }); +}); diff --git a/packages/plugins/plugin-security/src/people-org-assignment-nav-contribution.test.ts b/packages/plugins/plugin-security/src/people-org-assignment-nav-contribution.test.ts index 2ef1411a060..167a04d8dd4 100644 --- a/packages/plugins/plugin-security/src/people-org-assignment-nav-contribution.test.ts +++ b/packages/plugins/plugin-security/src/people-org-assignment-nav-contribution.test.ts @@ -180,7 +180,7 @@ describe('SecurityPlugin — People & Organization assignment entries are labell ((SetupAppTranslations as any)[locale]?.apps?.setup?.navigation ?? {}) as Record; const locales = Object.keys(SetupAppTranslations).filter((l) => navOf(l).group_people_org?.label); // Control: every Setup locale carries the group, so the loop below judges something. - expect(locales.sort()).toEqual(['ar', 'en', 'es-ES', 'ja-JP', 'zh-CN', 'zh-Hant']); + expect(locales.sort()).toEqual(['ar', 'en', 'es-ES', 'fr', 'ja-JP', 'zh-CN', 'zh-Hant']); const en = navOf('en'); expect(en[POSITION_HOLDERS.id]?.label).toBe(POSITION_HOLDERS.label); diff --git a/packages/services/service-settings/src/setup-nav-hub-parity.test.ts b/packages/services/service-settings/src/setup-nav-hub-parity.test.ts index a778159972d..e3b64e83c8e 100644 --- a/packages/services/service-settings/src/setup-nav-hub-parity.test.ts +++ b/packages/services/service-settings/src/setup-nav-hub-parity.test.ts @@ -46,7 +46,7 @@ import { settingsBuiltinTranslations } from './translations/index.js'; /** Where the hub opens a namespace; the sidebar entry for it points here. */ const SETTINGS_PAGE = /^\/apps\/setup\/system\/settings\/([a-z_]+)$/; -const TRANSLATED_LOCALES = ['zh-CN', 'ja-JP', 'es-ES', 'zh-Hant', 'ar'] as const; +const TRANSLATED_LOCALES = ['zh-CN', 'ja-JP', 'es-ES', 'zh-Hant', 'ar', 'fr'] as const; interface NavItem { id?: string; type?: string; label?: string; url?: string; children?: NavItem[] }