@mostajs/wagon-ui 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@
5
5
 
6
6
 
7
7
 
8
+ ## 0.4.0 — 2026-10-08
9
+
10
+ - **Les textes passent par `@mostajs/i18n`** (≥ 0.2.0) au lieu d'un traducteur maison : même ordre de résolution
11
+ (surcharges `textes` de l'application, catalogue de la langue, français), même API `ui.t(lang)(cle, vars)`.
12
+ - **Pluriels CLDR** : `{n}` sert de compte. Fini « clé(s) active(s) », « restant(e)s » et la paire
13
+ `cles`/`clesPluriel` ; l'arabe a ses six formes — « بمفتاحين » pour deux, « 11 مفتاحًا » de 11 à 99, au lieu de
14
+ « مفتاح/مفاتيح ». Clés touchées : `tarifs.cles` (+ `tarifs.clesIllimitees`), `espace.appels`,
15
+ `espace.plafondAtteint`, `espace.restant` (« Reste : {n} »). ⚠️ Une application qui surchargeait `tarifs.clesPluriel`
16
+ dans `textes` doit passer à `tarifs.cles_other`.
17
+ - **Prix formatés dans la langue** : « 9,99 € », « €9.99 », et en arabe `ar-DZ` aux chiffres latins — au lieu de
18
+ « 9.99 € » avec un point partout. `ui.euros(centimes, lang)` exposé (pour les suites de conformité).
19
+ - Essais T-WUI-16 (catalogues vérifiés par `checkCatalogues`, clés construites `legal.*` comprises), T-WUI-17
20
+ (pluriels, duel arabe), T-WUI-18 (prix) ; T-WUI-7 compare désormais les clés LOGIQUES. Trois sabotages, détectés.
21
+
8
22
  ## 0.3.1 — 2026-10-02
9
23
 
10
24
  - **Correctif** : l'en-tête `entete: 'barre'` portait la classe `barre`, déjà prise par la jauge de consommation
package/llms.txt CHANGED
@@ -20,12 +20,13 @@ Rendre les écrans communs à tout SaaS, identiques d'un produit à l'autre, san
20
20
  - `ui.adminPaliers({ lang, offres, message? })` — champs `<slug>.<limite>` et `<slug>.maxCles` (POST /admin/plans).
21
21
  - `ui.adminUsage({ lang, usage: { jours, totaux: { comptes, actifs, payants, erreurs, parRessource }, lignes: [{ nom, plan, actif, dernierJour, erreursFenetre, parRessource: { <cle>: { courant, limite, part, fenetre } } }] }, anonyme? })`
22
22
  - `ui.legal({ lang, doc: 'mentions-legales'|'cgu'|'cgv'|'confidentialite', html })` · `ui.introuvable({ lang })`
23
- - `ui.t(lang)(cle, vars)` · `CATALOGUES` · `dirDe(lang)` · `esc`
23
+ - `ui.t(lang)(cle, vars)` (par @mostajs/i18n : `{n}` = compte, pluriels CLDR, six formes en arabe) · `ui.euros(centimes, lang)` · `CATALOGUES` · `dirDe(lang)` · `esc`
24
24
 
25
25
  ## ROUTES ATTENDUES (liens et formulaires des vues)
26
26
  `/`, `/tarifs`, `/aide`, `/apropos`, `/inscription` (POST : plan, nom, courriel, motdepasse), `/connexion` (POST : courriel, motdepasse), `/deconnexion`, `/compte`, `/compte/cles` (POST : libelle), `/compte/cles/revoquer` (POST : prefix), `/compte/portail` (POST), `/paiement` (POST : compte, plan), `/admin/plans` (POST), `/admin/usage`, `/mentions-legales`, `/cgu`, `/cgv`, `/confidentialite`.
27
27
 
28
28
  ## PIÈGES
29
+ - 0.4.0 : `tarifs.clesPluriel` n'existe plus → `tarifs.cles_one` / `tarifs.cles_other` (+ formes arabes) ; prix formatés par langue (« 9,99 € »).
29
30
  - `legal({ html })` n'échappe pas `html` : il doit venir des textes de l'éditeur (mostajs/templates), jamais d'un formulaire.
30
31
  - Les prix sont en CENTIMES. `-1` = sans limite ; `0` = non compris.
31
32
  - Une ressource sans `parPeriode` s'affiche « libellé : n ».
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/wagon-ui",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Les pages du wagon d'un SaaS — mise en page (langues, pied légal, en-têtes pour les moteurs), accueil, tarifs, inscription, connexion (mot de passe ou code), espace client (palier, jauges, clés, factures, gestion de l'abonnement), administration (paliers, usage), pages légales — en fr, en, ar. Extraites de mdseal, sans ce qu'il vend : l'identité et les ressources mesurées viennent de la configuration. Vues pures, zéro dépendance.",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "author": "Dr Hamid MADANI <drmdh@msn.com>",
@@ -68,5 +68,8 @@
68
68
  },
69
69
  "devDependencies": {
70
70
  "@mostajs/mjs-unit": "^0.7.0"
71
+ },
72
+ "dependencies": {
73
+ "@mostajs/i18n": "^0.2.0"
71
74
  }
72
75
  }
package/src/i18n/ar.js CHANGED
@@ -26,8 +26,13 @@ export const ar = {
26
26
  "titre": "الأسعار",
27
27
  "parMois": "/شهريًّا",
28
28
  "gratuit": "مجّاني",
29
- "cles": "مفتاح واجهة واحد",
30
- "clesPluriel": "{n} مفاتيح واجهة",
29
+ "cles_zero": "بلا مفتاح واجهة",
30
+ "cles_one": "مفتاح واجهة واحد",
31
+ "cles_two": "مفتاحا واجهة",
32
+ "cles_few": "{n} مفاتيح واجهة",
33
+ "cles_many": "{n} مفتاحًا للواجهة",
34
+ "cles_other": "{n} مفتاح واجهة",
35
+ "clesIllimitees": "مفاتيح واجهة بلا حدّ",
31
36
  "choisir": "اختر هذه الخطّة",
32
37
  "commencer": "ابدأ مجّانًا",
33
38
  "encours": "الدفع غير متاح حاليًّا",
@@ -76,8 +81,18 @@ export const ar = {
76
81
  "revoquer": "إلغاء",
77
82
  "revoquee": "مُلغًى",
78
83
  "jamaisUtilisee": "لم يُستعمَل قطّ",
79
- "appels": "{n} نداء",
80
- "plafondAtteint": "تسمح خطّتك بـ {n} مفتاح/مفاتيح نشِطة.",
84
+ "appels_zero": "لا نداءات",
85
+ "appels_one": "نداء واحد",
86
+ "appels_two": "نداءان",
87
+ "appels_few": "{n} نداءات",
88
+ "appels_many": "{n} نداءً",
89
+ "appels_other": "{n} نداء",
90
+ "plafondAtteint_zero": "لا تسمح خطّتك بأيّ مفتاح نشِط.",
91
+ "plafondAtteint_one": "تسمح خطّتك بمفتاح نشِط واحد.",
92
+ "plafondAtteint_two": "تسمح خطّتك بمفتاحين نشِطين.",
93
+ "plafondAtteint_few": "تسمح خطّتك بـ {n} مفاتيح نشِطة.",
94
+ "plafondAtteint_many": "تسمح خطّتك بـ {n} مفتاحًا نشِطًا.",
95
+ "plafondAtteint_other": "تسمح خطّتك بـ {n} مفتاح نشِط.",
81
96
  "cleCreee": "أُنشئ المفتاح. انسخه: لن يُعرَض مرّة أخرى.",
82
97
  "gererAbonnement": "إدارة اشتراكي",
83
98
  "gererAide": "وسيلة الدفع والفواتير والإلغاء — لدى Stripe.",
package/src/i18n/en.js CHANGED
@@ -26,8 +26,9 @@ export const en = {
26
26
  "titre": "Pricing",
27
27
  "parMois": "/month",
28
28
  "gratuit": "Free",
29
- "cles": "{n} API key",
30
- "clesPluriel": "{n} API keys",
29
+ "cles_one": "{n} API key",
30
+ "cles_other": "{n} API keys",
31
+ "clesIllimitees": "Unlimited API keys",
31
32
  "choisir": "Choose this plan",
32
33
  "commencer": "Start for free",
33
34
  "encours": "Payment is not available yet",
@@ -76,8 +77,10 @@ export const en = {
76
77
  "revoquer": "Revoke",
77
78
  "revoquee": "Revoked",
78
79
  "jamaisUtilisee": "never used",
79
- "appels": "{n} calls",
80
- "plafondAtteint": "Your plan allows {n} active key(s).",
80
+ "appels_one": "{n} call",
81
+ "appels_other": "{n} calls",
82
+ "plafondAtteint_one": "Your plan allows {n} active key.",
83
+ "plafondAtteint_other": "Your plan allows {n} active keys.",
81
84
  "cleCreee": "Key created. Copy it: it will not be shown again.",
82
85
  "gererAbonnement": "Manage my subscription",
83
86
  "gererAide": "Payment method, invoices, cancellation — at Stripe.",
package/src/i18n/fr.js CHANGED
@@ -26,8 +26,9 @@ export const fr = {
26
26
  "titre": "Tarifs",
27
27
  "parMois": "/mois",
28
28
  "gratuit": "Gratuit",
29
- "cles": "{n} clé d’API",
30
- "clesPluriel": "{n} clés d’API",
29
+ "cles_one": "{n} clé d’API",
30
+ "cles_other": "{n} clés d’API",
31
+ "clesIllimitees": "Clés d’API sans limite",
31
32
  "choisir": "Choisir ce plan",
32
33
  "commencer": "Commencer gratuitement",
33
34
  "encours": "Paiement indisponible pour l’instant",
@@ -68,7 +69,7 @@ export const fr = {
68
69
  "votrePlan": "Votre plan",
69
70
  "consommationDuJour": "Aujourd’hui",
70
71
  "consommationDuMois": "Ce mois-ci",
71
- "restant": "{n} restant(e)s",
72
+ "restant": "Reste : {n}",
72
73
  "illimite": "sans limite",
73
74
  "vosCles": "Vos clés d’API",
74
75
  "creerUneCle": "Créer une clé",
@@ -76,8 +77,10 @@ export const fr = {
76
77
  "revoquer": "Révoquer",
77
78
  "revoquee": "Révoquée",
78
79
  "jamaisUtilisee": "jamais utilisée",
79
- "appels": "{n} appels",
80
- "plafondAtteint": "Votre plan permet {n} clé(s) active(s).",
80
+ "appels_one": "{n} appel",
81
+ "appels_other": "{n} appels",
82
+ "plafondAtteint_one": "Votre plan permet {n} clé active.",
83
+ "plafondAtteint_other": "Votre plan permet {n} clés actives.",
81
84
  "cleCreee": "Clé créée. Copiez-la : elle ne sera plus affichée.",
82
85
  "gererAbonnement": "Gérer mon abonnement",
83
86
  "gererAide": "Moyen de paiement, factures, résiliation — chez Stripe.",
package/src/index.js CHANGED
@@ -21,6 +21,7 @@
21
21
  * impression) ; le code reste de gauche à droite dans une page arabe ; une clé n'est montrée
22
22
  * qu'une fois ; la gestion de l'abonnement n'apparaît que si elle est possible.
23
23
  */
24
+ import { createI18n, formatNumber, resolveFormatLocale } from '@mostajs/i18n';
24
25
  import { fr } from './i18n/fr.js';
25
26
  import { en } from './i18n/en.js';
26
27
  import { ar } from './i18n/ar.js';
@@ -37,7 +38,11 @@ const lien = (href) => {
37
38
  return /^(\/(?!\/)|https?:\/\/|mailto:|tel:|#|\?)/i.test(h) ? esc(h) : '#';
38
39
  };
39
40
  const NBSP = ' ';
40
- const euros = (centimes) => `${(Number(centimes) || 0) / 100}${NBSP}€`;
41
+ /**
42
+ * Un prix en euros, formaté dans la langue (0.4.0) : « 9,99 € », « €9.99 », et en arabe la locale ar-DZ aux
43
+ * chiffres latins (@mostajs/i18n) — plus « 9.99 € » avec un point, en français comme ailleurs.
44
+ */
45
+ const euros = (centimes, lang = 'fr') => formatNumber(resolveFormatLocale(lang), (Number(centimes) || 0) / 100, { style: 'currency', currency: 'EUR' });
41
46
 
42
47
  // 0.3 : les jetons portent les NOMS de Palette Studio (palette.amia.fr) — un thème qui les redéfinit habille le site
43
48
  // entier ; les anciens noms en dérivent par var(), donc le suivent. Contrastes des replis vérifiés sur blanc.
@@ -166,11 +171,26 @@ export function creerWagonUi({ marque, couleurs = {}, langues = ['fr', 'en', 'ar
166
171
  /** Le thème courant (Palette Studio), relu à chaque page ; rejeté s'il sort de :root{…}. */
167
172
  const themeCss = () => { try { const t = typeof theme === 'function' ? String(theme() ?? '') : ''; return THEME_SAIN.test(t) ? t : ''; } catch { return ''; } };
168
173
 
169
- /** Le traducteur d'une langue : surcharges de l'application, puis le catalogue, puis le français. */
174
+ /**
175
+ * Le traducteur d'une langue, par @mostajs/i18n (0.4.0) : surcharges de l'application, puis le catalogue, puis
176
+ * le français. Pluriels CLDR — `{n}` sert de compte : « 1 clé active », « 3 clés actives », et en arabe les six
177
+ * formes (« بمفتاحين » pour deux). Une clé absente se rend telle quelle, comme avant.
178
+ */
179
+ const i18n = Object.fromEntries(LANGUES.map((l) => [l, createI18n({
180
+ locale: l, fallback: 'fr',
181
+ bundles: [
182
+ { locale: 'fr', namespace: 'wagon', messages: CATALOGUES.fr },
183
+ ...(l === 'fr' ? [] : [{ locale: l, namespace: 'wagon', messages: CATALOGUES[l] }]),
184
+ ...['fr', l].filter((x, k, a) => a.indexOf(x) === k && textes[x]).map((x) => ({ locale: x, namespace: 'wagon', messages: textes[x] })),
185
+ ],
186
+ })]));
170
187
  function t(lang) {
171
188
  const L = LANGUES.includes(lang) ? lang : LANGUES[0];
172
- const chercher = (cat, cle) => cle.split('.').reduce((o, k) => (o && typeof o === 'object' ? o[k] : undefined), cat);
173
- return (cle, vars) => remplir(chercher(textes[L] ?? {}, cle) ?? chercher(CATALOGUES[L], cle) ?? chercher(CATALOGUES.fr, cle) ?? cle, vars);
189
+ return (cle, vars) => {
190
+ const v = vars && typeof vars.n === 'number' && vars.count === undefined ? { ...vars, count: vars.n } : vars;
191
+ const r = i18n[L].t(`wagon:${cle}`, v);
192
+ return r === `wagon:${cle}` ? cle : r;
193
+ };
174
194
  }
175
195
  const langueDe = (lang) => (LANGUES.includes(lang) ? lang : LANGUES[0]);
176
196
 
@@ -184,6 +204,8 @@ export function creerWagonUi({ marque, couleurs = {}, langues = ['fr', 'en', 'ar
184
204
 
185
205
  const ui = {
186
206
  t, dir: dirDe, langues: LANGUES, style: STYLE,
207
+ /** Un prix en euros dans la langue : `ui.euros(999, 'fr')` → « 9,99 € ». */
208
+ euros: (centimes, lang) => euros(centimes, langueDe(lang)),
187
209
 
188
210
  /**
189
211
  * La page entière. `contenu` est du HTML déjà rendu (par une vue de ce module, ou par la charge
@@ -236,10 +258,10 @@ ${paliers.filter((p) => p.active !== false).map((p) => {
236
258
  const cles = p.limits?.maxCles;
237
259
  return `<section class="plan">
238
260
  <h2>${esc(p.name)}</h2>
239
- <p class="prix">${gratuit ? esc(tr('tarifs.gratuit')) : `<b>${esc(euros(p.price))}</b><span class="hint">${esc(tr('tarifs.parMois'))}</span>`}</p>
261
+ <p class="prix">${gratuit ? esc(tr('tarifs.gratuit')) : `<b>${esc(euros(p.price, L))}</b><span class="hint">${esc(tr('tarifs.parMois'))}</span>`}</p>
240
262
  <ul>
241
263
  ${ressources.map((r) => `<li>${esc(ligneRessource(r, p.limits?.[r.limite], L))}</li>`).join('\n ')}
242
- ${cles !== undefined ? `<li>${esc(Number(cles) === -1 ? `${tr('tarifs.clesPluriel', { n: '∞' })}` : tr(Number(cles) > 1 ? 'tarifs.clesPluriel' : 'tarifs.cles', { n: cles }))}</li>` : ''}
264
+ ${cles !== undefined ? `<li>${esc(Number(cles) === -1 ? tr('tarifs.clesIllimitees') : tr('tarifs.cles', { n: Number(cles) }))}</li>` : ''}
243
265
  </ul>
244
266
  <p><a class="bouton" href="${lien(`${inscriptionHref}?plan=${encodeURIComponent(p.slug)}`)}">${esc(tr(gratuit ? 'tarifs.commencer' : 'tarifs.choisir'))}</a></p>
245
267
  </section>`;
@@ -258,7 +280,7 @@ ${erreur ? `<p class="msg">${esc(erreur)}</p>` : ''}
258
280
  <form method="post" action="${lien(action)}">
259
281
  <input type="hidden" name="plan" value="${esc(palier?.slug ?? '')}">
260
282
  <fieldset><legend>${esc(tr('inscription.plan'))}</legend>
261
- <p><b>${esc(palier?.name ?? '')}</b> — ${gratuit ? esc(tr('tarifs.gratuit')) : `${esc(euros(palier.price))}${esc(tr('tarifs.parMois'))}`}</p>
283
+ <p><b>${esc(palier?.name ?? '')}</b> — ${gratuit ? esc(tr('tarifs.gratuit')) : `${esc(euros(palier.price, L))}${esc(tr('tarifs.parMois'))}`}</p>
262
284
  </fieldset>
263
285
  <div class="champs">
264
286
  <label>${esc(tr('inscription.nom'))}<input type="text" name="nom" required maxlength="80"></label>
@@ -335,7 +357,7 @@ ${erreur ? `<p class="msg">${esc(erreur)}</p>` : ''}
335
357
  ${erreur ? `<p class="msg">${esc(erreur)}</p>` : ''}
336
358
  ${cleNeuve ? `<p class="ok">${esc(tr('espace.cleCreee'))}</p><pre><code>${esc(cleNeuve)}</code></pre>` : ''}
337
359
  <h2>${esc(tr('espace.votrePlan'))}</h2>
338
- <p><b>${esc(v.plan?.name ?? '—')}</b>${Number(v.plan?.price) ? ` — ${esc(euros(v.plan.price))}${esc(tr('tarifs.parMois'))}` : ''}</p>
360
+ <p><b>${esc(v.plan?.name ?? '—')}</b>${Number(v.plan?.price) ? ` — ${esc(euros(v.plan.price, L))}${esc(tr('tarifs.parMois'))}` : ''}</p>
339
361
  <ul class="jauges">
340
362
  ${ressources.map(jauge).join('\n ')}
341
363
  </ul>