The screens need one clock, one translation mechanism and one way to format and name, or each component grows its own. horloge.js is the only code that reads time and arms timers, injected so tests drive it. traduction.js keys texts by domain, in two families of tables, raises on a missing key and never shows a raw key; libelles/ splits the label table by domain. format.js formats numbers and dates in French, and personnes.js composes the displayed name the frozen labels use. Nothing persists between sessions. Checked: 1773 node and 43 browser tests. --- FR --- [ADD] application : horloge, traduction, mise en forme, nom affiché Les écrans exigent une horloge, un mécanisme de traduction et une façon de mettre en forme et de nommer, sans quoi chaque composant ferait les siens. horloge.js est le seul code qui lit l'heure et arme des minuteries, injecté pour que les épreuves le pilotent. traduction.js range les textes par domaine, en deux familles de tables, lève sur une clé absente et ne montre jamais une clé brute ; libelles/ découpe la table des libellés par domaine. format.js met en forme nombres et dates en français, et personnes.js compose le nom affiché qu'emploient les libellés figés. Rien ne persiste d'une séance à l'autre. Vérifié : 1773 node et 43 navigateur. Assisted-by: Claude Opus 5.5
102 lines
4.5 KiB
JavaScript
102 lines
4.5 KiB
JavaScript
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
|
|
|
// Les aides de composition que partagent les tables de l'application : citer,
|
|
// compter, énumérer, dater, nommer un état. Ce module n'est pas une table et
|
|
// n'exporte aucune TABLE. Une aide qui lit le texte d'une autre clé reçoit
|
|
// texte, la fonction de composition que la traduction passe à chaque entrée
|
|
// (traduction.js) : elle ne connaît pas l'assemblage des tables.
|
|
//
|
|
// Les aides ne lisent ni l'horloge ni la langue du système : un horodatage se
|
|
// met en mots à partir de sa seule forme, AAAA-MM-JJTHH:MM:SS±HH:MM.
|
|
|
|
const NOMS_DES_ETATS = new Map([
|
|
['brouillon', 'Brouillon'],
|
|
['propose', 'Proposé'],
|
|
['retenu', 'Retenu'],
|
|
['bloque', 'Bloqué'],
|
|
]);
|
|
|
|
/** Valeur d'une table de noms, ou RangeError qui la nomme. */
|
|
export function nomDans(table, valeur, quoi) {
|
|
if (!table.has(valeur)) throw new RangeError(`libelle : ${quoi} inconnu ${JSON.stringify(valeur)}`);
|
|
return table.get(valeur);
|
|
}
|
|
|
|
/** Le nom d'un état du plan (§ 9) ; un état inconnu lève RangeError. */
|
|
export const nomEtat = (etat) => nomDans(NOMS_DES_ETATS, etat, 'état');
|
|
|
|
/** Un texte cité entre guillemets français. */
|
|
export const cite = (texte) => `« ${texte} »`;
|
|
|
|
/** Un compte et son nom, au singulier jusqu'à un, comme le veut le français. */
|
|
export const compte = (n, singulier, pluriel) => `${n} ${n <= 1 ? singulier : pluriel}`;
|
|
|
|
/** Une énumération : « a », « a et b », « a, b et c ». */
|
|
export function enumeration(termes) {
|
|
if (termes.length <= 1) return termes.join('');
|
|
return `${termes.slice(0, -1).join(', ')} et ${termes.at(-1)}`;
|
|
}
|
|
|
|
/** La première lettre en capitale. */
|
|
export const capitale = (texte) => texte.charAt(0).toUpperCase() + texte.slice(1);
|
|
|
|
/** Le chemin de l'élément fautif, rien quand il manque ou désigne la racine. */
|
|
export const element = (chemin) => (typeof chemin === 'string' && chemin !== '' ? ` (${chemin})` : '');
|
|
|
|
/** Le chemin affichable d'une racine, reçue entière ou par son seul chemin. */
|
|
export const cheminDe = (racine) => (typeof racine === 'string' ? racine : racine?.chemin);
|
|
|
|
/**
|
|
* Une cause entre parenthèses, rien quand elle manque : son texte quand les
|
|
* tables en portent un, sinon la cause telle que le système la nomme.
|
|
*/
|
|
export function causeEntreParentheses(cause, texte) {
|
|
if (cause === null || cause === undefined || cause === '') return '';
|
|
return ` (${texte.existe(cause) ? texte(cause) : cause})`;
|
|
}
|
|
|
|
/** Ce qu'un refus d'écriture nomme : le fichier, son dossier, la cause. */
|
|
export const ouEcrire = ({ chemin, dossier, cause }, texte) =>
|
|
`${cite(chemin)} dans le dossier ${cite(dossier)}${causeEntreParentheses(cause, texte)}`;
|
|
|
|
/**
|
|
* Une raison codée en une phrase : son texte quand les tables en portent un
|
|
* et que les détails reçus lui suffisent ; sinon une phrase générique qui la
|
|
* cite telle quelle. Un texte qui attend ses propres détails lève ou rend
|
|
* « undefined » sans eux : la phrase générique le remplace alors.
|
|
*/
|
|
export function phraseDeRaison(raison, details, texte) {
|
|
const generique = `La raison donnée est ${cite(String(raison))}.`;
|
|
if (!texte.existe(raison)) return generique;
|
|
try {
|
|
const rendu = texte(raison, details);
|
|
return typeof rendu === 'string' && !rendu.includes('undefined') ? rendu : generique;
|
|
} catch {
|
|
return generique;
|
|
}
|
|
}
|
|
|
|
/** Des numéros de proposition, un illisible nommé comme tel. */
|
|
export const numeros = (ids) => enumeration(ids.map((id) => (id === null ? 'sans numéro lisible' : `n° ${id}`)));
|
|
|
|
// La forme d'un horodatage : date, heure et minute se lisent, les secondes et
|
|
// le décalage se taisent.
|
|
const HORODATAGE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):\d{2}[+-]\d{2}:\d{2}$/;
|
|
|
|
/**
|
|
* Un horodatage AAAA-MM-JJTHH:MM:SS±HH:MM en mots, « le 17 mai 2031 à
|
|
* 8 h 05 », à l'heure qu'il porte, par les textes de la mise en forme
|
|
* (format.date, format.heure) ; une autre forme se cite telle quelle.
|
|
*/
|
|
export function dater(horodatage, texte) {
|
|
const morceaux = HORODATAGE.exec(String(horodatage));
|
|
if (morceaux === null) return String(horodatage);
|
|
const [annee, mois, jour, heure, minute] = morceaux.slice(1).map(Number);
|
|
if (mois < 1 || mois > 12) return String(horodatage);
|
|
return `le ${texte('format.date', { annee, mois, jour })} à ${texte('format.heure', { heure, minute })}`;
|
|
}
|
|
|
|
/** « depuis » et l'horodatage en mots, rien quand il manque. */
|
|
export const depuisQuand = (depuis, texte) =>
|
|
depuis === null || depuis === undefined ? '' : ` depuis ${dater(depuis, texte)}`;
|