story-theme-mcp 1.7.2 → 1.7.4

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Votre assistant IA connaît le [Story Thème](https://story-theme.com) par cœur — chaque section, chaque bloc, chaque réglage — et construit des boutiques Shopify propres et professionnelles, sans jamais produire un fichier que Shopify refuserait.
4
4
 
5
- Réservé aux titulaires d'une licence Story Thème. Le jeton se génère dans l'espace membre : **Story Thème > MCP & Appli**.
5
+ Réservé aux titulaires d'une licence Story Thème. Le jeton se génère dans l'espace membre : **Story Thème > MCP**.
6
6
 
7
7
  ```
8
8
  Vous : « Crée la boutique Maison Lumen : bougies parfumées coulées à la main à Lyon. »
@@ -15,7 +15,7 @@ IA : une seule salve de questions (photos, collection, conditions… ou « vas
15
15
 
16
16
  ## Installation
17
17
 
18
- **Le plus simple — l'extension Claude.** Téléchargez `story-theme-mcp.mcpb` depuis votre espace membre (page MCP & Appli) et ouvrez le fichier : Claude installe le serveur et vous demande votre jeton dans un formulaire. Rien d'autre à installer, Node compris.
18
+ **Le plus simple — l'extension Claude.** Téléchargez `story-theme-mcp.mcpb` depuis votre espace membre (page MCP) et ouvrez le fichier : Claude installe le serveur et vous demande votre jeton dans un formulaire. Rien d'autre à installer, Node compris.
19
19
 
20
20
  Les deux méthodes ci-dessous restent valables si vous préférez configurer à la main. Elles demandent Node 20 ou plus ([nodejs.org](https://nodejs.org)).
21
21
 
@@ -82,7 +82,7 @@ Prompts prêts à l'emploi : `creer-boutique`, `refaire-une-page`, `auditer-un-t
82
82
 
83
83
  | Variable | Défaut | Rôle |
84
84
  |---|---|---|
85
- | `STORY_MCP_TOKEN` | — | Jeton personnel (espace membre > MCP & Appli). Vérifie la licence et donne accès au thème publié. |
85
+ | `STORY_MCP_TOKEN` | — | Jeton personnel (espace membre > MCP). Vérifie la licence et donne accès au thème publié. |
86
86
  | `STORY_MCP_HOME` | `~/Documents/Story Theme MCP` | Projets, historique, exports, cache du thème. |
87
87
  | `STORY_MCP_LANGUAGE` | `fr` | Langue par défaut des projets et des textes. |
88
88
  | `STORY_MCP_CANAL` | `stable` | `test` pour la version en test du thème (comptes testeurs). |
@@ -117,7 +117,7 @@ npm run fonts # recharge la bibliothèque de polices Shopify (shopify.dev)
117
117
  npm run bundle # fabrique build/story-theme-mcp-<version>.mcpb (extension Claude, un clic)
118
118
  ```
119
119
 
120
- Après `npm run bundle`, déposer le `.mcpb` dans le dossier `mcp/` de l'espace membre : la page MCP & Appli sert automatiquement la version la plus récente qu'elle y trouve.
120
+ Après `npm run bundle`, déposer le `.mcpb` dans le dossier `mcp/` de l'espace membre : la page MCP sert automatiquement la version la plus récente qu'elle y trouve.
121
121
 
122
122
  Avec `STORY_THEME_DIR` sur le dépôt du thème, le MCP lit le thème en cours d'écriture et n'appelle pas l'espace membre. Les connaissances rédigées à la main vivent dans `src/knowledge/` (descriptions de chaque section et bloc, compositions par métier, directions artistiques, guides) ; tout le reste vient du thème. `npm run check` signale une section ou un bloc public sans description.
123
123
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "story-theme-mcp",
3
- "version": "1.7.2",
3
+ "version": "1.7.4",
4
4
  "description": "Serveur MCP du Story Thème : votre assistant IA connaît le thème Shopify Story par cœur et construit des boutiques propres et professionnelles.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/config.js CHANGED
@@ -11,7 +11,7 @@ export function loadConfig(env = process.env) {
11
11
  const themeDir = env.STORY_THEME_DIR || '';
12
12
  return {
13
13
  version: VERSION,
14
- // Jeton personnel du client (espace membre > MCP & Appli). Sans lui, pas de thème à distance :
14
+ // Jeton personnel du client (espace membre > MCP). Sans lui, pas de thème à distance :
15
15
  // le MCP retombe sur son cache, puis sur un dossier local.
16
16
  token: (env.STORY_MCP_TOKEN || '').trim(),
17
17
  apiBase: (env.STORY_MCP_API || DEFAULT_API).trim(),
package/src/index.js CHANGED
@@ -25,7 +25,7 @@ if (process.argv.includes('--help') || process.argv.includes('-h')) {
25
25
  'construit des boutiques Shopify prêtes à importer. Il ne s’utilise pas à la main.',
26
26
  '',
27
27
  'Variables d’environnement :',
28
- ' STORY_MCP_TOKEN jeton personnel (espace membre Story Thème > MCP & Appli) — indispensable',
28
+ ' STORY_MCP_TOKEN jeton personnel (espace membre Story Thème > MCP) — indispensable',
29
29
  ' STORY_MCP_HOME dossier des projets et exports (défaut : ~/Documents/Story Theme MCP)',
30
30
  ' STORY_MCP_LANGUAGE langue des textes (défaut : fr)',
31
31
  ' STORY_MCP_CANAL « test » pour la version en test du thème (comptes testeurs)',
@@ -57,6 +57,7 @@ try {
57
57
  // l'interface de Claude.
58
58
  }
59
59
 
60
- const server = createServer({ ...config, source });
60
+ // L'appli Story Theme Apps n'est proposée que si l'espace membre la dit disponible pour ce compte.
61
+ const server = createServer({ ...config, source, appli: source.appli });
61
62
  await server.connect(new StdioServerTransport());
62
63
  journal(`prêt (projets : ${config.homeDir})`);
@@ -18,7 +18,7 @@ const ETAPES = [
18
18
  'Dans l’administration Shopify, installez l’application gratuite Theme Access (éditée par Shopify) : https://apps.shopify.com/theme-access',
19
19
  'Dans Theme Access : « Créer un mot de passe », avec votre propre adresse e-mail.',
20
20
  'Ouvrez l’e-mail reçu et affichez le mot de passe : il commence par shptka_ et ne s’affiche qu’une fois.',
21
- `Collez-le dans votre espace membre Story Thème, page MCP & Appli > « Brancher une boutique » (${PAGE_BRANCHEMENT}). Jamais dans la conversation.`,
21
+ `Collez-le dans votre espace membre Story Thème, page MCP > « Brancher une boutique » (${PAGE_BRANCHEMENT}). Jamais dans la conversation.`,
22
22
  ];
23
23
 
24
24
  /** La marche à suivre complète, prête à recopier au marchand. */
@@ -33,3 +33,31 @@ export const TUTO_BRANCHEMENT = [
33
33
 
34
34
  /** Une ligne pour les réponses où la boutique n'est pas (ou peut-être pas) branchée. */
35
35
  export const INVITATION_BRANCHEMENT = `Boutique pas encore branchée ? Proposez Theme Access au marchand (deux minutes, une seule fois) : list_shops donne la marche à suivre${VIDEO_BRANCHEMENT ? ` et la vidéo (${VIDEO_BRANCHEMENT})` : ''}. Sans lui, le zip reste la solution de repli, à importer à chaque version.`;
36
+
37
+ /* --------------------------------------------- le connecteur Shopify officiel */
38
+
39
+ // Theme Access n'ouvre que les thèmes. Le connecteur Shopify officiel, édité par Shopify pour
40
+ // ChatGPT et Claude (mai 2026), ouvre le reste : produits, prix, stocks, collections, réductions,
41
+ // commandes. Avec lui, l'assistant remplit les pages des vrais produits et crée les réductions des
42
+ // offres au lieu de laisser une liste au marchand : le MCP le propose donc, surtout au début.
43
+
44
+ /** La fiche du connecteur dans l'annuaire de Claude (vérifiée le 20/10/2026). */
45
+ export const LIEN_CONNECTEUR_SHOPIFY_CLAUDE = 'https://claude.com/connectors/shopify';
46
+
47
+ /** Le conseil, tel que l'assistant le lit dans un résultat d'outil. */
48
+ // Le MCP ne voit pas les autres connecteurs de l'assistant ; l'assistant, lui, voit ses outils :
49
+ // c'est à lui de vérifier, et de se taire si le connecteur est déjà là.
50
+ export const CONSEIL_CONNECTEUR_SHOPIFY = `Connecteur Shopify officiel — seulement s'il n'est pas déjà en place. Vérifiez d'abord vos outils : si vous avez déjà ceux du connecteur Shopify (produits, commandes, réductions…), n'en parlez pas. S'il est installé mais pas actif dans cette conversation, dites simplement de l'activer (ChatGPT : @Shopify). Sinon, proposez-le au marchand en une phrase : édité par Shopify, il vous ouvre, en plus du thème, ses produits, prix, stocks, collections, réductions et commandes (pages remplies de ses vrais produits, réductions des offres créées pour lui, photos déposées dans Fichiers). Où l'ajouter : Claude, ${LIEN_CONNECTEUR_SHOPIFY_CLAUDE} ; ChatGPT, Applis › « Shopify » ; ailleurs, l'annuaire de connecteurs de son IA. S'il l'a refusé ou dit l'avoir déjà, n'y revenez plus.`;
51
+
52
+ /**
53
+ * Surtout au début, pas tout le temps : un compteur par conversation (une session MCP) qui
54
+ * rend le conseil aux deux premiers résultats, puis à un sur cinq. Chaque appel compte.
55
+ * @returns {() => string} le conseil, ou une chaîne vide
56
+ */
57
+ export function rappelConnecteurShopify() {
58
+ let appels = 0;
59
+ return () => {
60
+ appels += 1;
61
+ return appels <= 2 || appels % 5 === 0 ? CONSEIL_CONNECTEUR_SHOPIFY : '';
62
+ };
63
+ }
@@ -22,7 +22,7 @@ export function creerDemos(config) {
22
22
  const appel = async (chemin) => {
23
23
  if (!config.token) {
24
24
  throw new Error(
25
- 'Les boutiques démo demandent un jeton : ajoutez STORY_MCP_TOKEN (espace membre Story Thème > MCP & Appli).',
25
+ 'Les boutiques démo demandent un jeton : ajoutez STORY_MCP_TOKEN (espace membre Story Thème > MCP).',
26
26
  );
27
27
  }
28
28
  const reponse = await fetch(`${config.apiBase.replace(/\/+$/, '')}${chemin}`, {
@@ -217,8 +217,8 @@ Formats conseillés :
217
217
  limites: {
218
218
  title: 'Ce que le MCP (V1) ne fait pas',
219
219
  body: `- Il ne publie jamais un thème : il écrit dans un thème NON publié (push_to_shop) ou fabrique un zip à importer (export_theme) ; le marchand vérifie et publie lui-même.
220
- - Sans l'appli Story Theme Apps sur la boutique, il n'a accès à rien d'elle : ni ses produits, ni ses thèmes. Appelez list_shops pour le savoir.
221
- - Il ne crée pas de lui-même produits, collections, menus, pages, réductions ni images : avec une boutique connectée, shop_graphql le permet ; sinon la check-list les liste.
220
+ - Il n'ouvre que les THÈMES d'une boutique branchée par Theme Access (list_shops le dit) : ni ses produits, ni ses commandes, ni ses clients.
221
+ - Il ne crée pas de lui-même produits, collections, menus, pages, réductions ni images : le connecteur Shopify officiel du marchand le permet ; sinon la check-list les liste.
222
222
  - Il ne modifie jamais le code du thème (Liquid, CSS, JS) : seulement les modèles, les groupes de sections et les réglages, comme l'éditeur de thème.
223
223
  - Il ne garantit pas le rendu visuel : prévisualisez toujours dans l'éditeur Shopify avant de publier.`,
224
224
  },
@@ -53,7 +53,9 @@ const ROLES = [
53
53
  ['title', /^(heading|title|image_heading|review_title|tab_label|headline|heading_text|question)$/],
54
54
  ['eyebrow', /^(subheading|image_subheading|subtitle|label|tagline|eyebrow|caption|announcment)$/],
55
55
  ['text', /^(description|text|row_content|content|text_footer|caption_text|form_text|text_above|message|body|paragraph|info|tooltip|answer|text_before|text_after|helper_text)$/],
56
- ['image', /^(image|image_custom|custom_icon|icon_custom|icon_image|avatar|cover_image|background_image|image_before|image_after|logo|photo)$/],
56
+ // L'image qui remplace un pictogramme (3.x : image_custom, icon_N_custom_icon) reste un pictogramme.
57
+ ['icon_image', /^(image_custom|custom_icon|icon_custom|icon_image)$/],
58
+ ['image', /^(image|avatar|cover_image|background_image|image_before|image_after|logo|photo)$/],
57
59
  ['image_mobile', /^(image_mobile|background_image_mobile)$/],
58
60
  ['video', /^(video|video_url|video_link|video_mobile|video_link_mobile)$/],
59
61
  ];
@@ -92,9 +94,14 @@ const texteVide = (valeur) => !plainText(isHtml(valeur) ? valeur.html : valeur).
92
94
  // (« Écrire un avis », « Voir plus », jours/heures du compte à rebours…), que la 4.0 traduit elle-même.
93
95
  const INTERFACE = /^(section_id|custom_class|anchor|write_review_\w+|success_message|plus|less|button_show_all_text|placeholder|error_message|default_value|days|hours|mins|secs|free_label|reviews_based_on_text|reviews_suffix_text|reviews_count|cart_\w+|sale_badge|share_label|size_guide_trigger|product_trigger|trigger|final_message|track)$/;
94
96
 
97
+ // Le pictogramme choisi dans la bibliothèque du thème (« truck », « leaf ») : la 4.0 a la même.
98
+ const PICTO = /(^|_)icon(_type)?$/;
99
+
95
100
  /** Un réglage lu : sa clé, son rôle (titre, texte, lien…), son genre et sa valeur. */
96
101
  function lireReglage(cle, valeur, type) {
97
102
  if (INTERFACE.test(cle)) return null;
103
+ // « none » compte aussi : la colonne n'avait pas de pictogramme, la 4.0 n'en devine pas un.
104
+ if (PICTO.test(cle) && typeof valeur === 'string' && /^[a-z_]+$/.test(valeur)) return { cle, genre: 'icon', valeur };
98
105
  const genre = type ? SCHEMA_GENRE[type] ?? null : genreDeviné(cle, valeur);
99
106
  if (!genre || valeur === '' || valeur == null) return null;
100
107
  if (genre === 'html' || genre === 'text') {
@@ -119,11 +126,13 @@ function ranger(contenu, reglages) {
119
126
  const { genre, valeur } = reglage;
120
127
 
121
128
  if (genre === 'palette') contenu.palette ??= valeur;
129
+ else if (genre === 'icon') contenu.icon ??= valeur;
122
130
  else if (genre === 'code') contenu.code.push(valeur);
123
131
  else if (['product', 'collection', 'blog', 'page', 'menu', 'article'].includes(genre)) contenu[genre] ??= valeur;
124
132
  else if (genre === 'products' || genre === 'collections') contenu[genre] = [...(contenu[genre] || []), ...(Array.isArray(valeur) ? valeur : [valeur])];
125
133
  else if (genre === 'image') {
126
- if (role === 'image_mobile') contenu.image_mobile ??= valeur;
134
+ if (role === 'icon_image') contenu.icon_image ??= valeur;
135
+ else if (role === 'image_mobile') contenu.image_mobile ??= valeur;
127
136
  else if (/after/.test(base)) contenu.image_after ??= valeur;
128
137
  else if (/before/.test(base)) contenu.image_before ??= valeur;
129
138
  else if (!contenu.image) contenu.image = valeur;
@@ -191,7 +200,7 @@ function element(reglages, genre = null) {
191
200
  }
192
201
 
193
202
  const utile = (item) =>
194
- item.title !== undefined || item.text !== undefined || item.name !== undefined || item.image || item.video || item.product || item.collection || item.buttons.length || item.eyebrow !== undefined;
203
+ item.title !== undefined || item.text !== undefined || item.name !== undefined || item.image || item.icon_image || item.video || item.product || item.collection || item.buttons.length || item.eyebrow !== undefined;
195
204
 
196
205
  /**
197
206
  * Le contenu d'une section 3.x.
@@ -244,13 +253,16 @@ export function extraireSection(section, schema, { elements: typesElements = nul
244
253
  }
245
254
  if (lu.elements.length && !lu.simples.some((r) => ['text', 'html'].includes(r.genre) && /^(heading|title|name|description|text)$/.test(r.cle))) {
246
255
  // Un bloc qui porte plusieurs éléments numérotés (icônes, questions, avis).
247
- contenu.items.push(...lu.elements.map((el) => ({ ...el, kind: bloc.type })));
256
+ contenu.items.push(...lu.elements.map((el) => ({ ...el, kind: bloc.type, bloc: id })));
248
257
  continue;
249
258
  }
250
259
  const item = element(lu.simples, bloc.type);
260
+ item.bloc = id;
251
261
  if (lu.elements.length) item.sous = lu.elements;
252
262
  if (bloc.blocks) item.enfants = bloc;
253
263
  if (utile(item) || item.enfants) contenu.items.push(item);
264
+ // Un bloc qui ne porte que du code (Liquid, HTML) : le code de la section, dans l'ordre.
265
+ else if (item.code.length) contenu.code.push(...item.code);
254
266
  }
255
267
  return contenu;
256
268
  }
@@ -15,26 +15,80 @@
15
15
  import fs from 'node:fs';
16
16
  import path from 'node:path';
17
17
  import { unzipSync, strFromU8 } from 'fflate';
18
- import { parseThemeJson } from '../theme/loader.js';
18
+ import { stripJsonComment } from '../theme/loader.js';
19
19
  import { fileKind } from '../validate/template.js';
20
20
  import { devinerLangue, textesDesModeles } from '../validate/design.js';
21
21
 
22
22
  const SCHEMA_RE = /\{%-?\s*schema\s*-?%\}([\s\S]*?)\{%-?\s*endschema\s*-?%\}/;
23
23
 
24
+ /**
25
+ * Le JSON tel que Shopify l'accepte : commentaires et virgule finale permis. Le thème 3.x d'origine
26
+ * en a (schéma de main-product, cart-drawer…), et un settings_schema.json retouché à la main aussi :
27
+ * un JSON.parse strict perdait la version du thème et le type de chaque réglage.
28
+ */
29
+ export function parseJsonSouple(texte) {
30
+ const source = stripJsonComment(String(texte));
31
+ try {
32
+ return JSON.parse(source);
33
+ } catch (erreur) {
34
+ try {
35
+ return JSON.parse(sansVirguleFinale(sansCommentaires(source)));
36
+ } catch {
37
+ throw erreur;
38
+ }
39
+ }
40
+ }
41
+
42
+ // Parcourt le texte hors des chaînes : `quoi(c, i)` rend ce qu'il faut écrire et où reprendre.
43
+ function horsChaines(source, quoi) {
44
+ let sortie = '';
45
+ for (let i = 0; i < source.length; i++) {
46
+ if (source[i] === '"') {
47
+ let j = i + 1;
48
+ while (j < source.length && source[j] !== '"') j += source[j] === '\\' ? 2 : 1;
49
+ sortie += source.slice(i, j + 1);
50
+ i = j;
51
+ continue;
52
+ }
53
+ const [texte, suite] = quoi(source, i);
54
+ sortie += texte;
55
+ i = suite;
56
+ }
57
+ return sortie;
58
+ }
59
+
60
+ const sansCommentaires = (source) =>
61
+ horsChaines(source, (t, i) => {
62
+ if (t[i] === '/' && t[i + 1] === '/') {
63
+ const fin = t.indexOf('\n', i);
64
+ return ['\n', fin === -1 ? t.length : fin];
65
+ }
66
+ if (t[i] === '/' && t[i + 1] === '*') {
67
+ const fin = t.indexOf('*/', i + 2);
68
+ return [' ', fin === -1 ? t.length : fin + 1];
69
+ }
70
+ return [t[i], i];
71
+ });
72
+
73
+ const sansVirguleFinale = (source) => horsChaines(source, (t, i) => (t[i] === ',' && /^\s*[}\]]/.test(t.slice(i + 1, i + 200)) ? ['', i] : [t[i], i]));
74
+
24
75
  function lireSchema(liquid) {
25
76
  const match = SCHEMA_RE.exec(liquid);
26
77
  if (!match) return null;
27
78
  try {
28
- return JSON.parse(match[1]);
79
+ return parseJsonSouple(match[1]);
29
80
  } catch {
30
81
  return null;
31
82
  }
32
83
  }
33
84
 
85
+ // La langue par défaut du thème 3.x : locales/da.default.json → « da ». Seul le nom compte.
86
+ const LOCALE_DEFAUT = /^locales\/([a-z]{2}(?:-[A-Za-z]{2,4})?)\.default\.json$/;
87
+
34
88
  /** Les fichiers utiles d'un thème : chemin relatif → texte. */
35
89
  function lireFichiers(source) {
36
90
  const fichiers = new Map();
37
- const garder = (rel) => fileKind(rel) || rel === 'config/settings_schema.json' || /^(sections|blocks)\/[^/]+\.liquid$/.test(rel);
91
+ const garder = (rel) => fileKind(rel) || rel === 'config/settings_schema.json' || /^(sections|blocks)\/[^/]+\.liquid$/.test(rel) || LOCALE_DEFAUT.test(rel);
38
92
 
39
93
  if (fs.statSync(source).isDirectory()) {
40
94
  const parcourir = (dossier) => {
@@ -46,7 +100,7 @@ function lireFichiers(source) {
46
100
  else if (garder(rel)) fichiers.set(rel, fs.readFileSync(path.join(source, rel), 'utf8'));
47
101
  }
48
102
  };
49
- for (const dossier of ['templates', 'sections', 'blocks', 'config']) parcourir(dossier);
103
+ for (const dossier of ['templates', 'sections', 'blocks', 'config', 'locales']) parcourir(dossier);
50
104
  return fichiers;
51
105
  }
52
106
 
@@ -81,7 +135,14 @@ export function lireThemeV3DepuisFichiers(fichiers, origine = 'boutique') {
81
135
  const schemas = { sections: new Map(), blocks: new Map() };
82
136
  let version = null;
83
137
  let nom = null;
138
+ let langueParDefaut = null;
139
+ let reglagesSchema = [];
84
140
  for (const [rel, texte] of fichiers) {
141
+ const locale = LOCALE_DEFAUT.exec(rel);
142
+ if (locale) {
143
+ langueParDefaut = locale[1];
144
+ continue;
145
+ }
85
146
  const liquid = /^(sections|blocks)\/([^/]+)\.liquid$/.exec(rel);
86
147
  if (liquid) {
87
148
  const schema = lireSchema(texte);
@@ -90,28 +151,33 @@ export function lireThemeV3DepuisFichiers(fichiers, origine = 'boutique') {
90
151
  }
91
152
  if (rel === 'config/settings_schema.json') {
92
153
  try {
93
- const info = parseThemeJson(texte).find((groupe) => groupe.name === 'theme_info');
154
+ const groupes = parseJsonSouple(texte);
155
+ const info = groupes.find((groupe) => groupe.name === 'theme_info');
94
156
  version = info?.theme_version || null;
95
157
  nom = info?.theme_name || null;
158
+ reglagesSchema = groupes.flatMap((groupe) => groupe.settings || []).filter((reglage) => reglage?.id);
96
159
  } catch {
97
160
  /* version inconnue */
98
161
  }
99
162
  continue;
100
163
  }
101
164
  try {
102
- fichiersJson.set(rel, parseThemeJson(texte));
165
+ fichiersJson.set(rel, parseJsonSouple(texte));
103
166
  } catch (error) {
104
167
  erreurs.push(`${rel} : JSON illisible (${error.message})`);
105
168
  }
106
169
  }
107
170
 
108
- // La langue de la boutique se lit dans SES textes : le fichier de traductions par défaut du
109
- // thème (en.default.json) est l'anglais pour toutes les boutiques Story Thème.
110
- const langue = devinerLangue(textesDesModeles([...fichiersJson].filter(([rel]) => rel.startsWith('templates/') || rel.startsWith('sections/')).map(([, data]) => data)));
111
-
112
171
  const reglages = fichiersJson.get('config/settings_data.json');
113
172
  const courant = typeof reglages?.current === 'string' ? reglages.presets?.[reglages.current] : reglages?.current;
114
- return { origine, version, nom, langue, fichiers: fichiersJson, schemas, reglages: courant || {}, erreurs };
173
+
174
+ // La langue de la boutique se lit dans SES textes (modèles, groupes, et sections fixes comme le
175
+ // panier). Le fichier de traductions par défaut du thème (en.default.json) est l'anglais pour
176
+ // toutes les boutiques Story Thème : il ne départage que s'il a été changé (da.default.json…).
177
+ const modeles = [...fichiersJson].filter(([rel]) => rel.startsWith('templates/') || rel.startsWith('sections/')).map(([, data]) => data);
178
+ const textes = textesDesModeles([...modeles, { sections: courant?.sections || {} }]);
179
+ const langue = devinerLangue(textes) || (langueParDefaut && langueParDefaut !== 'en' ? langueParDefaut : null);
180
+ return { origine, version, nom, langue, langueParDefaut, fichiers: fichiersJson, schemas, reglages: courant || {}, reglagesSchema, erreurs };
115
181
  }
116
182
 
117
183
  /** La version majeure d'un numéro « 3.7.0 », ou null. */