@mostajs/wagon-ui 0.3.1 → 0.5.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 +27 -0
- package/llms.txt +3 -1
- package/package.json +4 -1
- package/src/i18n/ar.js +19 -4
- package/src/i18n/en.js +7 -4
- package/src/i18n/fr.js +8 -5
- package/src/index.js +53 -14
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,33 @@
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
## 0.5.0 — 2026-10-08
|
|
9
|
+
|
|
10
|
+
L'en-tête de la maquette Bolt de travel, devenu disponible pour tous les sites du wagon. Trois options, toutes
|
|
11
|
+
désactivées par défaut : un site qui ne les demande pas reste identique.
|
|
12
|
+
|
|
13
|
+
**Ajouté**
|
|
14
|
+
- `collant: true` : l'en-tête reste en haut de l'écran au défilement (`position: sticky`).
|
|
15
|
+
- `appel: { href, libelle: { fr, en, ar } }` : un bouton d'action dans l'en-tête (« Demander un devis »).
|
|
16
|
+
- `selecteurLangue: 'menu'` : la langue se choisit dans un menu déroulant (`<details>`, donc sans script),
|
|
17
|
+
langue courante marquée par `aria-current` ; sur mobile, le menu se déplie dans le hamburger.
|
|
18
|
+
|
|
19
|
+
**Essais** : T-WUI-19 à 21.
|
|
20
|
+
|
|
21
|
+
## 0.4.0 — 2026-10-08
|
|
22
|
+
|
|
23
|
+
- **Les textes passent par `@mostajs/i18n`** (≥ 0.2.0) au lieu d'un traducteur maison : même ordre de résolution
|
|
24
|
+
(surcharges `textes` de l'application, catalogue de la langue, français), même API `ui.t(lang)(cle, vars)`.
|
|
25
|
+
- **Pluriels CLDR** : `{n}` sert de compte. Fini « clé(s) active(s) », « restant(e)s » et la paire
|
|
26
|
+
`cles`/`clesPluriel` ; l'arabe a ses six formes — « بمفتاحين » pour deux, « 11 مفتاحًا » de 11 à 99, au lieu de
|
|
27
|
+
« مفتاح/مفاتيح ». Clés touchées : `tarifs.cles` (+ `tarifs.clesIllimitees`), `espace.appels`,
|
|
28
|
+
`espace.plafondAtteint`, `espace.restant` (« Reste : {n} »). ⚠️ Une application qui surchargeait `tarifs.clesPluriel`
|
|
29
|
+
dans `textes` doit passer à `tarifs.cles_other`.
|
|
30
|
+
- **Prix formatés dans la langue** : « 9,99 € », « €9.99 », et en arabe `ar-DZ` aux chiffres latins — au lieu de
|
|
31
|
+
« 9.99 € » avec un point partout. `ui.euros(centimes, lang)` exposé (pour les suites de conformité).
|
|
32
|
+
- Essais T-WUI-16 (catalogues vérifiés par `checkCatalogues`, clés construites `legal.*` comprises), T-WUI-17
|
|
33
|
+
(pluriels, duel arabe), T-WUI-18 (prix) ; T-WUI-7 compare désormais les clés LOGIQUES. Trois sabotages, détectés.
|
|
34
|
+
|
|
8
35
|
## 0.3.1 — 2026-10-02
|
|
9
36
|
|
|
10
37
|
- **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,14 @@ 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.5.0 : `creerWagonUi({ …, collant: true, appel: { href, libelle }, selecteurLangue: 'menu' })` — en-tête collant, bouton d'appel, langue en menu déroulant sans script ; défauts inchangés.
|
|
30
|
+
- 0.4.0 : `tarifs.clesPluriel` n'existe plus → `tarifs.cles_one` / `tarifs.cles_other` (+ formes arabes) ; prix formatés par langue (« 9,99 € »).
|
|
29
31
|
- `legal({ html })` n'échappe pas `html` : il doit venir des textes de l'éditeur (mostajs/templates), jamais d'un formulaire.
|
|
30
32
|
- Les prix sont en CENTIMES. `-1` = sans limite ; `0` = non compris.
|
|
31
33
|
- 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
|
+
"version": "0.5.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
|
-
"
|
|
30
|
-
"
|
|
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
|
-
"
|
|
80
|
-
"
|
|
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
|
-
"
|
|
30
|
-
"
|
|
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
|
-
"
|
|
80
|
-
"
|
|
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
|
-
"
|
|
30
|
-
"
|
|
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}
|
|
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
|
-
"
|
|
80
|
-
"
|
|
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
|
-
|
|
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.
|
|
@@ -111,7 +116,18 @@ header.bande{max-width:none;background:var(--brand);border-bottom:0;padding:.8re
|
|
|
111
116
|
header.bande .brand,header.bande nav a,header.bande .moi a,header.bande .langues a,header.bande .langues b,header.bande .langues{color:var(--text-on-brand)}
|
|
112
117
|
header.bande nav a{opacity:.9} header.bande nav a.on,header.bande nav a:hover{background:rgba(255,255,255,.16);opacity:1}
|
|
113
118
|
header.bande .burger{color:var(--text-on-brand);border-color:rgba(255,255,255,.4)}
|
|
114
|
-
header.bande + main{padding-top:1.2rem}
|
|
119
|
+
header.bande + main{padding-top:1.2rem}
|
|
120
|
+
header.collant{position:sticky;top:0;z-index:50;box-shadow:0 6px 20px -14px rgba(15,23,42,.45)}
|
|
121
|
+
header .appel{display:inline-block;padding:.45em 1.1em;border-radius:99px;background:var(--surface);color:var(--brand);font-weight:700;text-decoration:none;white-space:nowrap;border:1px solid var(--line)}
|
|
122
|
+
header.bande .appel{background:var(--surface);color:var(--brand);border-color:transparent}
|
|
123
|
+
.choix-langue{position:relative;font-size:.9em}
|
|
124
|
+
.choix-langue summary{list-style:none;cursor:pointer;padding:.35em .8em;border-radius:99px;border:1px solid var(--line);color:var(--link);user-select:none;white-space:nowrap}
|
|
125
|
+
.choix-langue summary::-webkit-details-marker{display:none;}
|
|
126
|
+
header.bande .choix-langue summary{color:var(--text-on-brand);border-color:rgba(255,255,255,.35);background:rgba(255,255,255,.1)}
|
|
127
|
+
.choix-langue ul{position:absolute;inset-inline-end:0;top:calc(100% + .4em);margin:0;padding:.3em 0;list-style:none;min-width:10em;background:var(--surface);border:1px solid var(--line);border-radius:12px;box-shadow:0 18px 35px -18px rgba(15,23,42,.45);z-index:60}
|
|
128
|
+
.choix-langue li a{display:block;padding:.45em 1em;color:var(--ink,var(--text));text-decoration:none}
|
|
129
|
+
.choix-langue li a:hover{background:var(--fond-doux)} .choix-langue li a[aria-current]{font-weight:700;color:var(--brand)}
|
|
130
|
+
@media (max-width: 760px){ .choix-langue ul{position:static;box-shadow:none;margin-top:.3em} header .appel{text-align:center} }`;
|
|
115
131
|
}
|
|
116
132
|
|
|
117
133
|
/** Un thème de Palette Studio n'est accepté que s'il ne fait que redéfinir des jetons dans :root{…}. */
|
|
@@ -156,7 +172,7 @@ const DOCUMENTS_LEGAUX = Object.freeze([['mentions-legales', 'mentions'], ['cgu'
|
|
|
156
172
|
* parPeriode?: string|object, sansLimite?: string|object, aucun?: string|object }>} [o.ressources]
|
|
157
173
|
* @param {object} [o.textes] surcharges des libellés, par langue : { fr: { tarifs: { note: '…' } } }
|
|
158
174
|
*/
|
|
159
|
-
export function creerWagonUi({ marque, couleurs = {}, langues = ['fr', 'en', 'ar'], liens = [], ressources = [], textes = {}, theme = null, entete = 'simple' } = {}) {
|
|
175
|
+
export function creerWagonUi({ marque, couleurs = {}, langues = ['fr', 'en', 'ar'], liens = [], ressources = [], textes = {}, theme = null, entete = 'simple', collant = false, appel = null, selecteurLangue = 'liens' } = {}) {
|
|
160
176
|
if (!String(marque?.nom ?? '').trim()) throw new Error('@mostajs/wagon-ui : `marque.nom` requis — le nom du service');
|
|
161
177
|
if (!String(marque?.auteur ?? '').trim()) throw new Error('@mostajs/wagon-ui : `marque.auteur` requis — qui édite le service');
|
|
162
178
|
const LANGUES = langues.filter((l) => CATALOGUES[l]);
|
|
@@ -166,11 +182,26 @@ export function creerWagonUi({ marque, couleurs = {}, langues = ['fr', 'en', 'ar
|
|
|
166
182
|
/** Le thème courant (Palette Studio), relu à chaque page ; rejeté s'il sort de :root{…}. */
|
|
167
183
|
const themeCss = () => { try { const t = typeof theme === 'function' ? String(theme() ?? '') : ''; return THEME_SAIN.test(t) ? t : ''; } catch { return ''; } };
|
|
168
184
|
|
|
169
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* Le traducteur d'une langue, par @mostajs/i18n (0.4.0) : surcharges de l'application, puis le catalogue, puis
|
|
187
|
+
* le français. Pluriels CLDR — `{n}` sert de compte : « 1 clé active », « 3 clés actives », et en arabe les six
|
|
188
|
+
* formes (« بمفتاحين » pour deux). Une clé absente se rend telle quelle, comme avant.
|
|
189
|
+
*/
|
|
190
|
+
const i18n = Object.fromEntries(LANGUES.map((l) => [l, createI18n({
|
|
191
|
+
locale: l, fallback: 'fr',
|
|
192
|
+
bundles: [
|
|
193
|
+
{ locale: 'fr', namespace: 'wagon', messages: CATALOGUES.fr },
|
|
194
|
+
...(l === 'fr' ? [] : [{ locale: l, namespace: 'wagon', messages: CATALOGUES[l] }]),
|
|
195
|
+
...['fr', l].filter((x, k, a) => a.indexOf(x) === k && textes[x]).map((x) => ({ locale: x, namespace: 'wagon', messages: textes[x] })),
|
|
196
|
+
],
|
|
197
|
+
})]));
|
|
170
198
|
function t(lang) {
|
|
171
199
|
const L = LANGUES.includes(lang) ? lang : LANGUES[0];
|
|
172
|
-
|
|
173
|
-
|
|
200
|
+
return (cle, vars) => {
|
|
201
|
+
const v = vars && typeof vars.n === 'number' && vars.count === undefined ? { ...vars, count: vars.n } : vars;
|
|
202
|
+
const r = i18n[L].t(`wagon:${cle}`, v);
|
|
203
|
+
return r === `wagon:${cle}` ? cle : r;
|
|
204
|
+
};
|
|
174
205
|
}
|
|
175
206
|
const langueDe = (lang) => (LANGUES.includes(lang) ? lang : LANGUES[0]);
|
|
176
207
|
|
|
@@ -184,6 +215,8 @@ export function creerWagonUi({ marque, couleurs = {}, langues = ['fr', 'en', 'ar
|
|
|
184
215
|
|
|
185
216
|
const ui = {
|
|
186
217
|
t, dir: dirDe, langues: LANGUES, style: STYLE,
|
|
218
|
+
/** Un prix en euros dans la langue : `ui.euros(999, 'fr')` → « 9,99 € ». */
|
|
219
|
+
euros: (centimes, lang) => euros(centimes, langueDe(lang)),
|
|
187
220
|
|
|
188
221
|
/**
|
|
189
222
|
* La page entière. `contenu` est du HTML déjà rendu (par une vue de ce module, ou par la charge
|
|
@@ -200,13 +233,19 @@ export function creerWagonUi({ marque, couleurs = {}, langues = ['fr', 'en', 'ar
|
|
|
200
233
|
<meta property="og:type" content="website"><meta property="og:site_name" content="${nom}"><meta property="og:title" content="${esc(titre)} — ${nom}">
|
|
201
234
|
<meta property="og:description" content="${esc(description || slogan)}"><meta property="og:url" content="${esc(canonique)}"><meta property="og:locale" content="${L}">` : '';
|
|
202
235
|
const nav = [...liens.map((x) => [x.href, dansLangue(x.libelle, L)]), ['/tarifs', tr('nav.tarifs')], ['/aide', tr('nav.aide')], ['/apropos', tr('nav.apropos')]];
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
236
|
+
// 0.5 — le choix de langue en menu déroulant (`selecteurLangue: 'menu'`) : <details>, donc SANS script.
|
|
237
|
+
const choixLangue = LANGUES.length < 2 ? '' : selecteurLangue === 'menu'
|
|
238
|
+
? `<details class="choix-langue"><summary aria-label="${esc(tr('layout.langue'))}">🌐 ${L.toUpperCase()} ▾</summary><ul>${LANGUES.map((l) => `<li><a href="${chemin}?lang=${l}" hreflang="${l}" lang="${l}" rel="alternate"${l === L ? ' aria-current="true"' : ''}>${esc(LIBELLES_LANGUES[l])}</a></li>`).join('')}</ul></details>`
|
|
239
|
+
: `<span class="langues">${LANGUES.map((l) => (l === L
|
|
240
|
+
? `<b lang="${l}">${esc(LIBELLES_LANGUES[l])}</b>`
|
|
241
|
+
: `<a href="${chemin}?lang=${l}" hreflang="${l}" lang="${l}" rel="alternate">${esc(LIBELLES_LANGUES[l])}</a>`)).join(' · ')}</span>`;
|
|
242
|
+
// 0.5 — l'en-tête collant (`collant`) et le bouton d'appel (`appel: { href, libelle }`).
|
|
243
|
+
const classesTete = [entete === 'barre' ? 'bande' : '', collant ? 'collant' : ''].filter(Boolean).join(' ');
|
|
244
|
+
const boutonAppel = appel?.href ? `<a class="appel" href="${lien(appel.href)}">${esc(dansLangue(appel.libelle, L))}</a>` : '';
|
|
206
245
|
return `<!doctype html><html lang="${L}" dir="${dirDe(L)}"><head><meta charset="utf-8">
|
|
207
246
|
<meta name="viewport" content="width=device-width, initial-scale=1"><title>${esc(titre)} — ${nom}</title>
|
|
208
247
|
${tete}<style>${STYLE}</style>${themeCss() ? `<style id="theme-palette">${themeCss()}</style>` : ''}</head><body>
|
|
209
|
-
<header${
|
|
248
|
+
<header${classesTete ? ` class="${classesTete}"` : ''}><a class="brand" href="/">${marque.symbole ? `${esc(marque.symbole)} ` : ''}${nom}</a><input type="checkbox" id="wagon-menu" class="bascule"><label for="wagon-menu" class="burger" aria-label="${esc(tr('layout.menu'))}">☰</label><div class="menu"><nav>${nav.map(([h, l]) => `<a href="${lien(h)}"${actif === h ? ' class="on"' : ''}>${esc(l)}</a>`).join('')}</nav><span class="moi"><a href="${connecte ? '/compte' : '/connexion'}">${esc(tr(connecte ? 'espace.compte' : 'espace.seConnecter'))}</a>${connecte ? ` · <a href="/deconnexion">${esc(tr('espace.seDeconnecter'))}</a>` : ''}</span>${boutonAppel}${choixLangue}</div></header>
|
|
210
249
|
<main>${contenu}</main>
|
|
211
250
|
<footer><b>${nom}</b>${slogan ? ` — ${esc(slogan)}` : ''} · ${esc(tr('layout.auteur'))} : <b>${esc(marque.auteur)}</b>${marque.courriel ? ` <${esc(marque.courriel)}>` : ''} · ${esc(marque.licence ?? 'AGPL-3.0')} · ${esc(tr('layout.propulse'))} <code>@mostajs/*</code><br><span class="legal">${DOCUMENTS_LEGAUX.map(([h, k]) => `<a href="/${h}">${esc(tr(`legal.${k}`))}</a>`).join(' · ')}</span></footer>
|
|
212
251
|
</body></html>`;
|
|
@@ -236,10 +275,10 @@ ${paliers.filter((p) => p.active !== false).map((p) => {
|
|
|
236
275
|
const cles = p.limits?.maxCles;
|
|
237
276
|
return `<section class="plan">
|
|
238
277
|
<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>
|
|
278
|
+
<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
279
|
<ul>
|
|
241
280
|
${ressources.map((r) => `<li>${esc(ligneRessource(r, p.limits?.[r.limite], L))}</li>`).join('\n ')}
|
|
242
|
-
${cles !== undefined ? `<li>${esc(Number(cles) === -1 ?
|
|
281
|
+
${cles !== undefined ? `<li>${esc(Number(cles) === -1 ? tr('tarifs.clesIllimitees') : tr('tarifs.cles', { n: Number(cles) }))}</li>` : ''}
|
|
243
282
|
</ul>
|
|
244
283
|
<p><a class="bouton" href="${lien(`${inscriptionHref}?plan=${encodeURIComponent(p.slug)}`)}">${esc(tr(gratuit ? 'tarifs.commencer' : 'tarifs.choisir'))}</a></p>
|
|
245
284
|
</section>`;
|
|
@@ -258,7 +297,7 @@ ${erreur ? `<p class="msg">${esc(erreur)}</p>` : ''}
|
|
|
258
297
|
<form method="post" action="${lien(action)}">
|
|
259
298
|
<input type="hidden" name="plan" value="${esc(palier?.slug ?? '')}">
|
|
260
299
|
<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>
|
|
300
|
+
<p><b>${esc(palier?.name ?? '')}</b> — ${gratuit ? esc(tr('tarifs.gratuit')) : `${esc(euros(palier.price, L))}${esc(tr('tarifs.parMois'))}`}</p>
|
|
262
301
|
</fieldset>
|
|
263
302
|
<div class="champs">
|
|
264
303
|
<label>${esc(tr('inscription.nom'))}<input type="text" name="nom" required maxlength="80"></label>
|
|
@@ -335,7 +374,7 @@ ${erreur ? `<p class="msg">${esc(erreur)}</p>` : ''}
|
|
|
335
374
|
${erreur ? `<p class="msg">${esc(erreur)}</p>` : ''}
|
|
336
375
|
${cleNeuve ? `<p class="ok">${esc(tr('espace.cleCreee'))}</p><pre><code>${esc(cleNeuve)}</code></pre>` : ''}
|
|
337
376
|
<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>
|
|
377
|
+
<p><b>${esc(v.plan?.name ?? '—')}</b>${Number(v.plan?.price) ? ` — ${esc(euros(v.plan.price, L))}${esc(tr('tarifs.parMois'))}` : ''}</p>
|
|
339
378
|
<ul class="jauges">
|
|
340
379
|
${ressources.map(jauge).join('\n ')}
|
|
341
380
|
</ul>
|