103 lines
4.5 KiB
JavaScript
103 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)}`;
|