@salifbiaye/create-boutique 0.7.0 → 0.7.2

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.
Files changed (32) hide show
  1. package/cli/dist/commands.js +85 -4
  2. package/cli/dist/generate.js +2 -0
  3. package/cli/dist/index.js +11 -5
  4. package/docs/guide-complet.fr.html +29 -1
  5. package/docs/guide-images/personnaliser-style.jpg +0 -0
  6. package/docs/guide-images/personnaliser-textes.jpg +0 -0
  7. package/package.json +1 -1
  8. package/templates/backend/medusa-config.ts +4 -0
  9. package/templates/backend/src/admin/i18n/json/en.json +64 -0
  10. package/templates/backend/src/admin/i18n/json/fr.json +64 -0
  11. package/templates/backend/src/admin/routes/personnaliser/page.tsx +407 -0
  12. package/templates/backend/src/api/admin/customization/css/restore/route.ts +21 -0
  13. package/templates/backend/src/api/admin/customization/css/route.ts +37 -0
  14. package/templates/backend/src/api/admin/customization/texts/route.ts +38 -0
  15. package/templates/backend/src/api/middlewares.ts +4 -0
  16. package/templates/backend/src/api/store/customization/route.ts +21 -0
  17. package/templates/backend/src/lib/customization.ts +43 -0
  18. package/templates/backend/src/modules/customization/index.ts +9 -0
  19. package/templates/backend/src/modules/customization/migrations/Migration20261001080000.ts +21 -0
  20. package/templates/backend/src/modules/customization/models/css-version.ts +16 -0
  21. package/templates/backend/src/modules/customization/models/site-text.ts +17 -0
  22. package/templates/backend/src/modules/customization/service.ts +7 -0
  23. package/templates/backend/src/modules/customization/validators.ts +49 -0
  24. package/templates/backend/src/workflows/customization.ts +15 -0
  25. package/templates/backend/src/workflows/steps/save-css-version.ts +20 -0
  26. package/templates/backend/src/workflows/steps/save-site-texts.ts +56 -0
  27. package/templates/storefront/Dockerfile +2 -0
  28. package/templates/storefront/PERSONNALISER.md +15 -4
  29. package/templates/storefront/src/app/layout.tsx +17 -2
  30. package/templates/storefront/src/lib/boutique/customization.ts +76 -0
  31. package/templates/storefront/src/lib/boutique/i18n.ts +4 -2
  32. package/templates/storefront/src/styles/custom.css +2 -0
@@ -7,7 +7,7 @@ import { readEnv, refreshEnv, writeCampaignFiles, writeCompose, writeReadme } fr
7
7
  import { copyTemplate, templateVersion } from "./paths.js";
8
8
  import { KEEP, allCodeFiles, copyFiles, syncCustomCss, localChanges, restoreKept, stashKept, writeFingerprint } from "./code.js";
9
9
  import * as prompts from "@clack/prompts";
10
- import { nextFreePort, reservedByOthers, reservePorts } from "./ports.js";
10
+ import { nextFreePort, releasePorts, reservedByOthers, reservePorts } from "./ports.js";
11
11
  import { boutiqueUrl, currencies, effective, normalizeUrl, readProject, validateProject, writeJsonSchema, writeProject, ProjectError, SLUG, storePorts } from "./project.js";
12
12
  import { listThemes } from "./themes.js";
13
13
  import { writeGuides } from "./guide.js";
@@ -114,8 +114,21 @@ export async function urls(dir, o, opts) {
114
114
  throw new ProjectError("--site-url concerne le mode single ; en mode multi, mettez url sur chaque boutique dans config/project.json");
115
115
  p.site = { ...p.site, url: normalizeUrl(o.site, "--site-url") };
116
116
  }
117
- if (!o.api && !o.files && !o.site && !o.from)
118
- throw new ProjectError("rien à changer : --api-url, --files-url, --site-url ou --from");
117
+ // Adresse d'une boutique (mode multi ; gardée aussi en mode single pour un retour au multi) : --boutique-url slug=domaine
118
+ for (const spec of o.boutiques ?? []) {
119
+ const i = spec.indexOf("=");
120
+ const slug = spec.slice(0, i);
121
+ const b = p.boutiques.find((x) => x.slug === slug);
122
+ if (i < 1 || !b)
123
+ throw new ProjectError(`--boutique-url : « ${spec} » (format slug=domaine, slug d'une boutique du projet)`);
124
+ const value = spec.slice(i + 1).trim();
125
+ if (value)
126
+ b.url = normalizeUrl(value, `--boutique-url ${slug}`);
127
+ else
128
+ delete b.url;
129
+ }
130
+ if (!o.api && !o.files && !o.site && !o.from && !o.boutiques?.length)
131
+ throw new ProjectError("rien à changer : --api-url, --files-url, --site-url, --boutique-url ou --from");
119
132
  writeProject(target, validateProject(p));
120
133
  await apply(target, opts);
121
134
  const after = readEnv(target).S3_FILE_URL;
@@ -466,7 +479,8 @@ export async function check(dir) {
466
479
  const url = boutiqueUrl(p, b);
467
480
  const html = await fetch(url).then((r) => (r.ok ? r.text() : Promise.reject(new Error(`boutique HTTP ${r.status}`))));
468
481
  // Mode single : le site n'affiche que la boutique active
469
- if (p.mode !== "single" && !html.includes(`data-theme="${b.theme}"`))
482
+ // Variante (theme.json → layout) : data-theme = mise en page empruntée, data-variant = le thème lui-même
483
+ if (p.mode !== "single" && !html.includes(`data-theme="${b.theme}"`) && !html.includes(`data-variant="${b.theme}"`))
470
484
  throw new Error(`thème ${b.theme} absent de la page`);
471
485
  steps.push("page");
472
486
  const key = composeOutput(target, ["exec", "-T", "backend", "cat", `/app/keys/${b.slug}`]).stdout.trim();
@@ -542,6 +556,73 @@ export function admin(dir) {
542
556
  console.log(`Mot de passe ${env.ADMIN_PASSWORD ?? "?"}`);
543
557
  console.log(pc.dim("À transmettre au client séparément de la fiche (jamais par le même canal). Il peut le changer ensuite : « Mot de passe oublié ? » sur la page de connexion."));
544
558
  }
559
+ /**
560
+ * Supprime un projet entièrement : containers, volumes (base, images produits, clés), images Docker construites pour
561
+ * lui, ports réservés et dossier (sauvegardes de backups/ comprises). Irréversible : confirmation en tapant le nom.
562
+ */
563
+ export async function destroy(dir, opts) {
564
+ const target = path.resolve(dir);
565
+ readProject(target);
566
+ const name = path.basename(target);
567
+ if (!opts.yes) {
568
+ if (!process.stdin.isTTY)
569
+ throw new ProjectError("suppression : relancez avec --yes pour confirmer");
570
+ const typed = await prompts.text({ message: `Suppression définitive (données, images, sauvegardes). Tapez « ${name} » pour confirmer` });
571
+ if (prompts.isCancel(typed) || typed !== name)
572
+ throw new ProjectError("suppression annulée");
573
+ }
574
+ if (fs.existsSync(path.join(target, "docker-compose.yml"))) {
575
+ console.log("Containers, volumes et images du projet…");
576
+ if (compose(target, ["down", "--volumes", "--rmi", "local", "--remove-orphans"]) !== 0)
577
+ throw new Error("docker compose down a échoué : rien n'a été supprimé du dossier");
578
+ }
579
+ releasePorts(target);
580
+ fs.rmSync(target, { recursive: true, force: true });
581
+ console.log(pc.green(`✔ projet ${name} supprimé (containers, volumes, images, dossier, ports libérés)`));
582
+ }
583
+ /** Script exécuté dans le container backend (medusa exec) : e-mail + mot de passe lus sur l'entrée standard. */
584
+ const PASSWORD_SCRIPT = `const fs = require("fs")
585
+ exports.default = async function ({ container }) {
586
+ const { email, password } = JSON.parse(fs.readFileSync(0, "utf8"))
587
+ await container.resolve("auth").updateProvider("emailpass", { entity_id: email, password })
588
+ console.log("UB_PASSWORD_OK")
589
+ }
590
+ `;
591
+ /**
592
+ * Change le mot de passe de l'admin Medusa (module Auth, provider emailpass) puis met à jour ADMIN_PASSWORD dans .env.
593
+ * Mot de passe : NEW_ADMIN_PASSWORD (environnement) ou généré (32 caractères). Jamais en argument (visible dans la liste
594
+ * des processus) : il passe par l'entrée standard du container. Le backend doit tourner.
595
+ */
596
+ export function adminPassword(dir) {
597
+ const target = path.resolve(dir);
598
+ readProject(target);
599
+ const env = readEnv(target);
600
+ const email = env.ADMIN_EMAIL;
601
+ if (!email)
602
+ throw new ProjectError("ADMIN_EMAIL absent du .env");
603
+ const password = process.env.NEW_ADMIN_PASSWORD || crypto.randomBytes(24).toString("base64url");
604
+ if (password.length < 12)
605
+ throw new ProjectError("mot de passe trop court : 12 caractères minimum");
606
+ if (!isRunning(target, "backend"))
607
+ throw new ProjectError("le backend ne tourne pas : docker compose up -d, puis relancez");
608
+ const name = ".ub-admin-password.js";
609
+ const local = path.join(target, name);
610
+ fs.writeFileSync(local, PASSWORD_SCRIPT);
611
+ try {
612
+ if (compose(target, ["cp", name, `backend:/app/${name}`]) !== 0)
613
+ throw new Error("copie du script dans le backend impossible");
614
+ const r = composeOutput(target, ["exec", "-T", "backend", "npx", "medusa", "exec", `./${name}`], JSON.stringify({ email, password }));
615
+ composeOutput(target, ["exec", "-T", "backend", "rm", "-f", `/app/${name}`]);
616
+ if (!r.stdout.includes("UB_PASSWORD_OK"))
617
+ throw new Error(`changement refusé par Medusa :\n${(r.stderr || r.stdout).trim().split("\n").slice(-8).join("\n")}`);
618
+ }
619
+ finally {
620
+ fs.rmSync(local, { force: true });
621
+ }
622
+ setEnvValue(target, "ADMIN_PASSWORD", password);
623
+ console.log(pc.green(`✔ mot de passe de l'admin ${email} changé (ADMIN_PASSWORD dans .env)`));
624
+ console.log(pc.dim("Pour l'afficher : create-boutique admin <dossier>. Les sessions déjà ouvertes dans l'admin restent valides jusqu'à leur expiration."));
625
+ }
545
626
  /** Régénère la fiche client (1 page) et le guide complet du projet. */
546
627
  export function guide(dir) {
547
628
  const target = path.resolve(dir);
@@ -53,6 +53,8 @@ ${services}
53
53
  - ./config:/app/config:ro # boutiques, devises, pays → régions, sales channels, clés
54
54
  - ./seed:/app/seed:ro # products.csv + images à importer
55
55
  - ./storefront/themes:/app/themes:ro # textes par défaut des thèmes (pré-remplis dans admin → Campagnes)
56
+ - ./storefront/locales:/app/locales:ro # textes d'origine de l'interface (admin → Personnaliser → Textes du site)
57
+ - ./storefront/src/styles/custom.css:/app/custom/custom.css:ro # affiché dans admin → Personnaliser → Style
56
58
  - keys:/app/keys # y écrit la publishable key de chaque boutique
57
59
  depends_on:
58
60
  postgres: { condition: service_healthy }
package/cli/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import fs from "node:fs";
3
3
  import { parseArgs } from "node:util";
4
4
  import pc from "picocolors";
5
5
  import { z } from "zod";
6
- import { add, admin, apply, urls, backup, restore, campaignExport, campaignImport, check, guide, remove, set, stripeWebhook, themes, update } from "./commands.js";
6
+ import { add, admin, adminPassword, apply, destroy, urls, backup, restore, campaignExport, campaignImport, check, guide, remove, set, stripeWebhook, themes, update } from "./commands.js";
7
7
  import { answersSchema, create } from "./create.js";
8
8
  import { ProjectError } from "./project.js";
9
9
  const HELP = `create-boutique — Usine à Boutiques
@@ -27,7 +27,7 @@ const HELP = `create-boutique — Usine à Boutiques
27
27
  --yes aucune question, valeurs par défaut
28
28
 
29
29
  create-boutique apply <dossier> [--no-up] applique config/project.json
30
- create-boutique urls <dossier> [--api-url …] [--files-url …] [--site-url …] [--from <ancienne adresse des images>]
30
+ create-boutique urls <dossier> [--api-url …] [--files-url …] [--site-url …] [--boutique-url slug=domaine …] [--from <ancienne adresse des images>]
31
31
  change les domaines publics ; réécrit l'adresse des images déjà en base
32
32
  create-boutique add <dossier> --boutique <slug>:<thème>
33
33
  create-boutique remove <dossier> --boutique <slug>
@@ -40,12 +40,14 @@ const HELP = `create-boutique — Usine à Boutiques
40
40
  create-boutique backup <dossier> [--keep 7] base + images dans backups/<date>/ (--keep : garde les N plus récentes)
41
41
  create-boutique restore <dossier> [--from <nom>] [--yes] remet une sauvegarde (la plus récente par défaut)
42
42
  create-boutique admin <dossier> adresse, e-mail et mot de passe de l'admin
43
+ create-boutique delete <dossier> [--yes] supprime tout : containers, volumes, images, dossier (irréversible)
44
+ create-boutique admin <dossier> --new-password change le mot de passe de l'admin (généré, ou NEW_ADMIN_PASSWORD)
43
45
  create-boutique guide <dossier> régénère GUIDE-CLIENT.pdf et GUIDE-COMPLET.pdf
44
46
  create-boutique stripe-webhook <dossier> --url https://api.marque.com crée le webhook Stripe, écrit STRIPE_WEBHOOK_SECRET
45
47
  create-boutique campaign export <dossier> campagnes en base → config/campaigns/*.json
46
48
  create-boutique campaign import <dossier> [--boutique <slug>] [--yes] JSON → base (écrase)
47
49
  `;
48
- const COMMANDS = new Set(["restore", "urls", "apply", "add", "remove", "set", "themes", "update", "check", "backup", "campaign", "admin", "guide", "stripe-webhook", "help"]);
50
+ const COMMANDS = new Set(["restore", "urls", "apply", "add", "remove", "set", "themes", "update", "check", "backup", "campaign", "admin", "guide", "stripe-webhook", "delete", "help"]);
49
51
  async function main() {
50
52
  const { values, positionals } = parseArgs({
51
53
  allowPositionals: true,
@@ -62,8 +64,10 @@ async function main() {
62
64
  "api-url": { type: "string" },
63
65
  "files-url": { type: "string" },
64
66
  "site-url": { type: "string" },
67
+ "boutique-url": { type: "string", multiple: true },
65
68
  from: { type: "string" },
66
69
  keep: { type: "string" },
70
+ "new-password": { type: "boolean" },
67
71
  "stripe-pk": { type: "string" },
68
72
  "stripe-sk": { type: "string" },
69
73
  "stripe-whsec": { type: "string" },
@@ -131,7 +135,7 @@ async function main() {
131
135
  case "apply":
132
136
  return apply(dirArg(), up);
133
137
  case "urls":
134
- return urls(dirArg(), { api: values["api-url"], files: values["files-url"], site: values["site-url"], from: values.from }, up);
138
+ return urls(dirArg(), { api: values["api-url"], files: values["files-url"], site: values["site-url"], from: values.from, boutiques: values["boutique-url"] }, up);
135
139
  case "add":
136
140
  return add(dirArg(), values.boutique ?? [], up);
137
141
  case "remove":
@@ -158,8 +162,10 @@ async function main() {
158
162
  return void backup(dirArg(), { keep: values.keep ? Number(values.keep) : undefined });
159
163
  case "restore":
160
164
  return restore(dirArg(), { from: values.from, yes: !!values.yes });
165
+ case "delete":
166
+ return destroy(dirArg(), { yes: !!values.yes });
161
167
  case "admin":
162
- return admin(dirArg());
168
+ return values["new-password"] ? adminPassword(dirArg()) : admin(dirArg());
163
169
  case "guide":
164
170
  return guide(dirArg());
165
171
  case "stripe-webhook":
@@ -147,7 +147,7 @@
147
147
  <li><span>8</span>Le stock</li>
148
148
  <li><span>9</span>Catégories et collections</li></ol></div>
149
149
  <div class="part"><b>Partie IV · Boutiques et promotions</b><ol>
150
- <li><span>10</span>Mes boutiques : sites, textes et visuels</li>
150
+ <li><span>10</span>Mes boutiques : sites, textes, visuels et personnalisation</li>
151
151
  <li><span>11</span>Prix barrés (listes de prix)</li>
152
152
  <li><span>12</span>Codes promo et promotions</li></ol></div>
153
153
  <div class="part"><b>Partie V · Réglages</b><ol>
@@ -208,6 +208,7 @@
208
208
  <tr><td><b>Promotions</b></td><td>codes promo, remises automatiques</td><td>12</td></tr>
209
209
  <tr><td><b>Listes de prix</b></td><td>prix barrés, prix réservés à un groupe</td><td>11</td></tr>
210
210
  <tr><td><b>Avis</b></td><td>valider ou refuser les avis des acheteurs, par boutique</td><td>6.7</td></tr>
211
+ <tr><td><b>Personnaliser</b></td><td>changer les textes de l'interface (boutons, panier…) et le style des sites</td><td>10.7, 10.8</td></tr>
211
212
  <tr><td><b>Mes boutiques</b></td><td>ouvrir / fermer un site, textes, dates et visuels de campagne</td><td>10</td></tr>
212
213
  <tr><td><b>Paramètres</b></td><td>équipe, régions, livraison, taxes</td><td>13</td></tr>
213
214
  </table>
@@ -544,6 +545,33 @@
544
545
 
545
546
  <h2><span class="n">10.6</span>Revenir aux textes d'origine</h2>
546
547
  <p><span class="ui">Réinitialiser aux textes du thème</span> (en bas du panneau) efface vos textes ; les images, le logo et les dates sont gardés.</p>
548
+
549
+ <h2><span class="n">10.7</span>Changer les textes du site (boutons, panier, paiement…)</h2>
550
+ <div class="path">Personnaliser › Textes du site</div>
551
+ <p>Tous les textes de l'interface des boutiques (menu, boutons, panier, paiement, compte client, messages) sont listés ici, rangés par rubrique, avec leur texte d'origine en gris.</p>
552
+ <ol class="steps">
553
+ <li>Dans <span class="ui">Appliquer à</span>, choisissez <span class="ui">Toutes les boutiques</span>, ou une seule boutique.</li>
554
+ <li>Cherchez le texte (par exemple « panier » ou « Shop now ») dans la barre de recherche.</li>
555
+ <li>Tapez le nouveau texte dans le champ à droite. Laissez vide pour garder le texte d'origine.</li>
556
+ <li>Cliquez sur <span class="ui">Enregistrer</span> (en bas). Le site affiche le nouveau texte en une minute environ.</li>
557
+ </ol>
558
+ <figure><img src="guide-images/personnaliser-textes.jpg" alt=""><figcaption><b>Figure 10.7</b> — Les textes du site : texte d'origine à gauche, votre texte à droite.</figcaption></figure>
559
+ <div class="note tip"><div><b class="t">Une boutique différente des autres</b>Un texte saisi pour une boutique passe avant celui de « Toutes les boutiques ». Le badge <span class="ui">Texte du projet</span> indique qu'une boutique reprend le texte commun.</div></div>
560
+ <div class="note info"><div><b class="t">Bon à savoir</b>La flèche à droite d'un texte modifié le remet à l'origine. L'interrupteur <span class="ui">Seulement les textes modifiés</span> montre tout ce que vous avez changé. Textes simples uniquement : la mise en forme (HTML) est refusée.</div></div>
561
+
562
+ <h2><span class="n">10.8</span>Changer le style (couleurs, tailles, éléments masqués)</h2>
563
+ <div class="path">Personnaliser › Style (CSS)</div>
564
+ <p>Pour les personnes à l'aise avec le CSS (ou avec l'aide de votre prestataire) : le code saisi ici s'ajoute après le thème, sur toutes les boutiques, sans rien reconstruire.</p>
565
+ <ol class="steps">
566
+ <li>Écrivez ou collez le CSS dans l'éditeur (par exemple, pour une seule boutique : <code>[data-boutique="nom-de-la-boutique"] .ub-countdown { display: none; }</code>).</li>
567
+ <li>Ajoutez une note facultative (ce qui change), puis <span class="ui">Enregistrer</span>. Le site change en une minute environ.</li>
568
+ <li>Le résultat ne convient pas ? <span class="ui">Annuler les modifications</span> remet la dernière version enregistrée dans l'éditeur.</li>
569
+ </ol>
570
+ <figure><img src="guide-images/personnaliser-style.jpg" alt=""><figcaption><b>Figure 10.8</b> — L'éditeur de style, l'historique des versions et le fichier custom.css du projet.</figcaption></figure>
571
+ <p><b>Historique</b> (à droite) : chaque enregistrement est gardé, avec sa date, son auteur et sa note. <span class="ui">Charger dans l'éditeur</span> permet de relire une ancienne version ; <span class="ui">Restaurer</span> la remet en ligne (la version actuelle reste dans l'historique).</p>
572
+ <p><b>Fichier custom.css du projet</b> : les retouches faites dans le code par votre prestataire, visibles ici en lecture seule. L'interrupteur <span class="ui">Appliquer aussi le fichier custom.css du projet</span> les active ou non.</p>
573
+ <div class="note warn"><div><b class="t">Revenir au CSS du thème</b>Ce bouton retire tout style personnalisé (éditeur et fichier custom.css) : les boutiques retrouvent l'apparence d'origine du thème. Rien n'est perdu : les versions restent dans l'historique et peuvent être restaurées.</div></div>
574
+ <div class="note info"><div><b class="t">Bon à savoir</b>Pour la sécurité, sont refusés : le chargement de fichiers d'autres sites (<code>@import</code>, <code>url(https://…)</code>) et tout code HTML. Les images doivent être dans le dossier <code>/custom/</code> du site (demandez à votre prestataire).</div></div>
547
575
  </section>
548
576
 
549
577
  <section class="chapter">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salifbiaye/create-boutique",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Usine à Boutiques : génère un projet complet (backend Medusa + une boutique Next.js par campagne) prêt pour docker compose",
5
5
  "type": "module",
6
6
  "repository": {
@@ -52,6 +52,10 @@ const modules: Record<string, unknown>[] = [
52
52
  // Avis clients (acheteurs vérifiés, validés dans l'admin → Avis)
53
53
  resolve: "./src/modules/product-review",
54
54
  },
55
+ {
56
+ // Admin → Personnaliser : textes d'interface remplacés et style (CSS) versionné, appliqués sans reconstruire
57
+ resolve: "./src/modules/customization",
58
+ },
55
59
  ]
56
60
 
57
61
  // E-mails aux clients et au marchand (src/lib/email-config.ts) : Resend (une clé pour tous les projets, prioritaire)
@@ -125,5 +125,69 @@
125
125
  "pending": "Review back to pending",
126
126
  "deleted": "Review deleted"
127
127
  }
128
+ },
129
+ "customize": {
130
+ "nav": "Customize",
131
+ "title": "Customize",
132
+ "description": "Interface texts and store styling, without rebuilding anything: changes show up on the sites in about a minute.",
133
+ "tabs": {
134
+ "texts": "Site texts",
135
+ "css": "Style (CSS)"
136
+ },
137
+ "loading": "Loading…",
138
+ "save": "Save",
139
+ "cancel": "Cancel",
140
+ "texts": {
141
+ "scope": "Apply to",
142
+ "allStores": "All stores",
143
+ "search": "Search a text or key (e.g. cart, checkout.pay)",
144
+ "onlyChanged": "Only changed texts ({{count}})",
145
+ "helpAll": "Your text replaces the original on every store. Leave empty to keep the original text (shown in grey). Plain text only.",
146
+ "helpOne": "Your text applies to this store only and overrides “All stores”. Leave empty to use the project text.",
147
+ "noDefaults": "Original texts are not available: update the project (create-boutique update) to see them here.",
148
+ "none": "No text matches.",
149
+ "custom": "Changed",
150
+ "fromProject": "Project text",
151
+ "reset": "Back to the original text",
152
+ "pending_one": "{{count}} unsaved change",
153
+ "pending_other": "{{count}} unsaved changes",
154
+ "saved_one": "{{count}} text saved",
155
+ "saved_other": "{{count}} texts saved"
156
+ },
157
+ "css": {
158
+ "help": "This CSS is added after the theme on every store. Target a store [data-boutique=\"slug\"], a theme [data-theme=\"halloween\"], a page [data-page=\"product\"]. Images: /custom/… or data: only. Full guide: the project's PERSONNALISER.md.",
159
+ "placeholder": "/* Example: hide the countdown on the Christmas store */\n[data-boutique=\"yout-noel\"] .ub-countdown { display: none; }",
160
+ "unsaved": "unsaved changes",
161
+ "appliedSince": "version applied since {{date}}",
162
+ "none": "no custom style",
163
+ "useFile": "Also apply the project's custom.css file",
164
+ "notePlaceholder": "Note (optional): what changes",
165
+ "discard": "Discard changes",
166
+ "theme": "Back to theme CSS",
167
+ "themeTitle": "Back to the theme's original style?",
168
+ "themeText": "The custom style and the project's custom.css file will no longer be applied. Versions stay in the history: you can restore them.",
169
+ "themeConfirm": "Back to theme",
170
+ "themeNote": "Back to the theme's original style",
171
+ "saved": "Style saved: visible on the sites in about a minute",
172
+ "restored": "Version restored: visible on the sites in about a minute",
173
+ "file": "Project custom.css file",
174
+ "fileOn": "applied",
175
+ "fileOff": "not applied",
176
+ "fileMissing": "not available (update the project)",
177
+ "show": "show",
178
+ "hide": "hide",
179
+ "fileHelp": "Read only: this file is part of the project code (storefront/src/styles/custom.css). The editor CSS comes after it.",
180
+ "history": "History",
181
+ "noHistory": "No saved version yet.",
182
+ "applied": "applied",
183
+ "noNote": "No note",
184
+ "empty": "Empty style",
185
+ "withFile": "with custom.css",
186
+ "withoutFile": "without custom.css",
187
+ "load": "Load in editor",
188
+ "restore": "Restore",
189
+ "restoreTitle": "Restore this version?",
190
+ "restoreText": "The style from {{date}} becomes the stores' style again. The current version stays in the history."
191
+ }
128
192
  }
129
193
  }
@@ -125,5 +125,69 @@
125
125
  "pending": "Avis remis en attente",
126
126
  "deleted": "Avis supprimé"
127
127
  }
128
+ },
129
+ "customize": {
130
+ "nav": "Personnaliser",
131
+ "title": "Personnaliser",
132
+ "description": "Textes de l'interface et style des boutiques, sans rien reconstruire : les modifications apparaissent sur les sites en une minute environ.",
133
+ "tabs": {
134
+ "texts": "Textes du site",
135
+ "css": "Style (CSS)"
136
+ },
137
+ "loading": "Chargement…",
138
+ "save": "Enregistrer",
139
+ "cancel": "Annuler",
140
+ "texts": {
141
+ "scope": "Appliquer à",
142
+ "allStores": "Toutes les boutiques",
143
+ "search": "Rechercher un texte ou une clé (ex. panier, checkout.pay)",
144
+ "onlyChanged": "Seulement les textes modifiés ({{count}})",
145
+ "helpAll": "Le texte saisi remplace celui d'origine sur toutes les boutiques. Laissez vide pour garder le texte d'origine (affiché en gris). Texte simple uniquement.",
146
+ "helpOne": "Le texte saisi ne s'applique qu'à cette boutique et passe avant celui de « Toutes les boutiques ». Laissez vide pour reprendre le texte du projet.",
147
+ "noDefaults": "Les textes d'origine ne sont pas disponibles : mettez le projet à jour (create-boutique update) pour les voir ici.",
148
+ "none": "Aucun texte ne correspond.",
149
+ "custom": "Modifié",
150
+ "fromProject": "Texte du projet",
151
+ "reset": "Revenir au texte d'origine",
152
+ "pending_one": "{{count}} modification non enregistrée",
153
+ "pending_other": "{{count}} modifications non enregistrées",
154
+ "saved_one": "{{count}} texte enregistré",
155
+ "saved_other": "{{count}} textes enregistrés"
156
+ },
157
+ "css": {
158
+ "help": "Ce CSS s'ajoute après le thème sur toutes les boutiques. Cibler : une boutique [data-boutique=\"slug\"], un thème [data-theme=\"halloween\"], une page [data-page=\"product\"]. Images : seulement /custom/… ou data:. Guide complet : PERSONNALISER.md du projet.",
159
+ "placeholder": "/* Exemple : masquer le compte à rebours sur la boutique Noël */\n[data-boutique=\"yout-noel\"] .ub-countdown { display: none; }",
160
+ "unsaved": "modifications non enregistrées",
161
+ "appliedSince": "version appliquée du {{date}}",
162
+ "none": "aucun style personnalisé",
163
+ "useFile": "Appliquer aussi le fichier custom.css du projet",
164
+ "notePlaceholder": "Note (facultatif) : ce qui change",
165
+ "discard": "Annuler les modifications",
166
+ "theme": "Revenir au CSS du thème",
167
+ "themeTitle": "Revenir au style d'origine du thème ?",
168
+ "themeText": "Le style personnalisé et le fichier custom.css du projet ne seront plus appliqués. Les versions restent dans l'historique : vous pourrez les restaurer.",
169
+ "themeConfirm": "Revenir au thème",
170
+ "themeNote": "Retour au style d'origine du thème",
171
+ "saved": "Style enregistré : visible sur les sites dans une minute environ",
172
+ "restored": "Version restaurée : visible sur les sites dans une minute environ",
173
+ "file": "Fichier custom.css du projet",
174
+ "fileOn": "appliqué",
175
+ "fileOff": "non appliqué",
176
+ "fileMissing": "non disponible (mettre le projet à jour)",
177
+ "show": "afficher",
178
+ "hide": "masquer",
179
+ "fileHelp": "Lecture seule : ce fichier fait partie du code du projet (storefront/src/styles/custom.css). Le CSS de l'éditeur passe après lui.",
180
+ "history": "Historique",
181
+ "noHistory": "Aucune version enregistrée pour l'instant.",
182
+ "applied": "appliquée",
183
+ "noNote": "Sans note",
184
+ "empty": "Style vide",
185
+ "withFile": "avec custom.css",
186
+ "withoutFile": "sans custom.css",
187
+ "load": "Charger dans l'éditeur",
188
+ "restore": "Restaurer",
189
+ "restoreTitle": "Restaurer cette version ?",
190
+ "restoreText": "Le style du {{date}} redevient celui des boutiques. La version actuelle reste dans l'historique."
191
+ }
128
192
  }
129
193
  }