diff --git a/clients/adeo/config.json b/clients/adeo/config.json index 8be9f17..a49f51d 100644 --- a/clients/adeo/config.json +++ b/clients/adeo/config.json @@ -43,5 +43,169 @@ "3": "Systematic, consistent, and well-evidenced capability", "4": "Best-in-class — automated, measured, and continuously improved" } + }, + "question_topics": [ + "Across all on/off content, how the unique brand promise is identifiable", + "As part of the same campaign, how the brand identity and message is preserved across all media and content", + "How messages and assets are adapted to the specificities of each channel and format", + "How assets comply with platform-specific guidelines and requirements", + "How assets are designed for efficiency and platform specifications (responsive, vertical)", + "How similar/complementary assets are used across channels for continuous narration", + "How content consistency is maintained across channels", + "The ability to produce high-quality content across various formats (print, digital, video, audio)", + "The strategic approach to paid and organic content, resource allocation, and optimization", + "How the brand promise has been designed to answer what the customer need", + "How the audiences and persona have been chosen in the strategy according to their availability and how precise they are", + "How detailed the needs are, for each persona", + "How the chosen media channels effectively match the actual online habits and platform preferences of the target persona", + "How the specific content formats used are appropriate and engaging for the target personae on each particular channel", + "How well content is tailored to individual users based on their specific data (e.g., name, past purchases, browsing history) to create a more unique and relevant experience beyond broad persona targeting", + "If the brand's official definition of its target customer (i.e., the persona) is treated as a living document, regularly updated with new performance data, customer feedback, and market trends", + "This examines how often (frequency) and how thoroughly (depth) the brand measures its health and market perception. Depth refers to the specific metrics tracked, such as awareness, sentiment, purchase intent, and competitive positioning.", + "Whether there is a consistent and formalized process for testing creative concepts before a full launch (pre-test) and for measuring their impact and effectiveness after a campaign has run (post-test).", + "The ability to go beyond standard metrics (like clicks) to understand why certain creative assets succeed. It involves analysing specific elements like visuals, copy, and calls-to-action to derive actionable insights for future content", + "The proficiency in using brand lift studies to measure the direct impact of advertising campaigns on key brand metrics, such as ad recall, brand awareness, and purchase intent, and the ability to apply these insights effectively", + "The tracking of metrics that measure how deeply an audience interacts with content. It focuses on indicators of genuine interest, such as video view-through rates (VTR), article scroll depth, or time-on-page, rather than just surface-level actions.", + "The process for tracking and managing the resources invested in content creation. It covers both the efficiency of time spent (e.g., man-hours per asset) and the direct financial expenses (e.g., shooting, design, licensing costs)", + "The tracking of production efficiency, including time-to-market, cost per asset, and resource utilisation", + "The use of automated tools (like Creative X) that systematically check creative assets to ensure they comply with brand guidelines, legal disclaimers, and platform-specific ad policies before publication.", + "The company's Digital Asset Management (DAM) system, which is the central library for all creative assets. It evaluates both its availability (how easily and widely it's accessed by teams and partners) and its functional depth (its range of features beyond simple storage, like advanced search, rights management, and software integrations)", + "The tool used to manage and distribute product data to e-commerce channels (like Google Shopping or Facebook Ads). It evaluates its availability to all relevant teams and its functional depth—the ability to customise, optimise, and analyse product feeds for various platforms.", + "This assesses the company's Dynamic Creative Optimization (DCO) technology, which automates the creation of personalised ads. It evaluates its availability (who can access and operate the platform) and its functional depth—the sophistication of its features for creating, testing, and personalising ads in real-time based on data.", + "The platform used for Creative Intelligence, which analyses creative elements to provide data-driven insights on performance. It evaluates the tool's availability (who can access its dashboards and reports) and its functional depth—its ability to move beyond reporting to offer predictive analytics and actionable recommendations for future creative", + "The platforms used for team collaboration on marketing projects (e.g., project management, creative review). It evaluates their availability (are they standardised and accessible to all internal teams and external partners?) and their functional depth—the advanced features that streamline workflows, such as integrated proofing and approval cycles.", + "The company's centralised platform or preferred marketplace for sourcing and licensing creative assets (e.g., stock photography, illustrations, video). It evaluates how this system streamlines the purchasing process, manages budgets, and tracks usage rights.", + "The comprehensive set of technologies and tools used for content production across all formats and channels", + "The availability and proficiency with creative software and tools for content creation and editing", + "The use of automation tools to streamline production workflows and reduce manual processes", + "The use of standardised templates for creating off-line marketing assets (e.g., print ads, in-store posters, brochures). It evaluates the extent to which a system of templates is used to maintain brand consistency and streamline the creation of localised or campaign-specific materials.", + "The system of standardised templates for creating online assets (e.g., social media posts, display ads, email newsletters). It evaluates how this system ensures brand consistency and supports the rapid, scalable production of content for digital channels.", + "The technical capability to automatically generate multiple variations of a creative asset by dynamically inserting personalised elements. This includes tailoring content based on user data, location, browsing behaviour, or a product feed.", + "The use of technology to automatically publish or send final creative assets to various marketing channels. This includes pushing content directly from a central library to ad platforms, social media schedulers, or a Content Management System (CMS) without manual uploads.", + "How seamlessly the validation (approval), production (creation/personalisation), and distribution (publishing) stages are connected through automation. It measures the degree to which assets flow through this entire lifecycle with minimal manual intervention", + "The level of automation in production workflows from concept to final delivery", + "The use of templates and standardisation to ensure consistency and efficiency in production", + "The ability to process multiple assets or content pieces simultaneously for efficiency", + "The use of automated systems to ensure quality control and compliance in production", + "How generative AI tools are integrated into the content creation process. It examines its use across different stages, from brainstorming and copywriting to image generation, as well as the existence of formal guidelines governing its ethical and on-brand application", + "The company's commitment to experimenting with new content types (emerging formats) like interactive AR, virtual world experiences, or new social video styles. It evaluates the volume (frequency and scale of tests) and depth (the rigor of analysis) of this experimentation to find future opportunities.", + "How Creative Test & Learn is an ongoing, systematic process embedded in the marketing workflow. It evaluates if there is a regular cadence for testing creative hypotheses, gathering insights, and applying those learnings to consistently optimise future assets", + "The capability to identify, evaluate, and adopt emerging technologies for production and creative processes", + "The systematic approach to experimenting with new creative concepts and production methods", + "The management of innovation projects from ideation to implementation", + "The strategic planning for future production and creative capabilities", + "The existence, quality, and accessibility of documentation designed to guide marketing and creative execution. It covers foundational brand standards, tactical checklists for processes, and practical guides (playbooks) for specific tasks.", + "How effectively the organisation leverages its network of external partners, including agencies, tech vendors, and freelancers. It evaluates whether these relationships are purely transactional or are treated as a strategic ecosystem to drive innovation, share insights, and create greater value.", + "The effectiveness and consistency of the recurring practices (rituals) that facilitate collaboration between central (corporate) teams and individual Business Units (BUs). It evaluates whether these processes are well-defined and purposeful, ensuring alignment between global strategy and local market execution", + "Whether these teams work in integrated workflows with shared goals or operate in isolated departmental silos.", + "How formally the RACI (Responsible, Accountable, Consulted, Informed) framework is used to define roles and responsibilities for key operational tasks. It evaluates whether these role assignments are clearly documented, up-to-date, and consistently followed to avoid confusion and ensure clear ownership.", + "The team's collective capabilities, evaluating the presence and strength of both strategic skills (e.g., long-term planning, data analysis, brand strategy) and operational skills (e.g., platform expertise, content creation, campaign management). It aims to identify any critical talent or skill gaps.", + "The organisational structure and role definitions for production and creative teams", + "The programs and processes for developing production and creative skills within the organisation", + "The processes and tools for collaboration within and across production and creative teams", + "The capability to manage and adapt to changes in processes, technologies, and organizational structure" + ], + "translations": { + "fr": { + "description": "Évaluation de la maturité du contenu", + "entity_label": "Marchés", + "entity_label_singular": "Marché", + "scoring": { + "labels": { + "1": "Débutant", + "2": "Intermédiaire", + "3": "Maîtrisé", + "4": "Expert" + } + }, + "pillars": [ + "OMNICANAL", + "CENTRICITÉ CLIENT", + "MESURE", + "CAPACITÉS TECHNOLOGIQUES", + "AUTOMATISATION & INDUSTRIALISATION", + "INNOVATION", + "ORGANISATION" + ], + "about": { + "summary": "Un audit de 59 questions réparties sur 7 piliers qui évalue le niveau de sophistication de chaque Business Unit dans la production et la gestion du contenu marketing — indiquant à ADEO exactement où se situe chaque marque et où concentrer les investissements.", + "pillar_descriptions": { + "OMNICANAL": "Le contenu fonctionne-t-il de manière cohérente sur TV, réseaux sociaux, print, digital, etc. ?", + "CENTRICITÉ CLIENT": "Le contenu est-il adapté à des types de clients et des comportements spécifiques ?", + "MESURE": "Les performances du contenu sont-elles correctement suivies ?", + "CAPACITÉS TECHNOLOGIQUES": "Quels outils et plateformes sont utilisés (ex. DAM, DCO, Figma) ?", + "AUTOMATISATION & INDUSTRIALISATION": "Dans quelle mesure le workflow de production de contenu est-il automatisé ?", + "INNOVATION": "L'IA est-elle utilisée, de nouveaux formats testés et des expériences menées ?", + "ORGANISATION": "L'équipe, les processus et le partage des connaissances sont-ils bien structurés ?" + }, + "scoring_descriptions": { + "1": "Rien de systématique — les choses se produisent de manière ad hoc ou pas du tout", + "2": "Des capacités existent, mais elles sont incohérentes ou non documentées", + "3": "Capacité systématique, cohérente et bien documentée", + "4": "Meilleure pratique — automatisée, mesurée et continuellement améliorée" + } + }, + "question_topics": [ + "Comment la promesse de marque unique est identifiable dans tout le contenu on/off", + "Dans le cadre d'une même campagne, comment l'identité et le message de marque sont préservés sur tous les médias et contenus", + "Comment les messages et les assets sont adaptés aux spécificités de chaque canal et format", + "Comment les assets respectent les directives et exigences spécifiques à chaque plateforme", + "Comment les assets sont conçus pour l'efficacité et les spécifications des plateformes (responsive, vertical)", + "Comment des assets similaires/complémentaires sont utilisés sur différents canaux pour une narration continue", + "Comment la cohérence du contenu est maintenue d'un canal à l'autre", + "La capacité à produire du contenu de haute qualité dans différents formats (print, digital, vidéo, audio)", + "L'approche stratégique du contenu payant et organique, l'allocation des ressources et l'optimisation", + "Comment la promesse de marque a été conçue pour répondre aux besoins des clients", + "Comment les audiences et les personas ont été choisis dans la stratégie selon leur disponibilité et leur précision", + "Le niveau de détail des besoins pour chaque persona", + "Comment les canaux médias choisis correspondent aux habitudes en ligne et aux préférences de plateforme du persona cible", + "Comment les formats de contenu utilisés sont appropriés et engageants pour les personas cibles sur chaque canal", + "Dans quelle mesure le contenu est personnalisé pour chaque utilisateur selon ses données spécifiques (ex. nom, achats passés, historique de navigation) au-delà du ciblage par persona", + "Si la définition officielle du client cible (le persona) est traitée comme un document vivant, régulièrement mis à jour avec de nouvelles données de performance, retours clients et tendances du marché", + "Fréquence et profondeur avec lesquelles la marque mesure sa santé et sa perception sur le marché, incluant les indicateurs tels que la notoriété, le sentiment, l'intention d'achat et le positionnement concurrentiel.", + "L'existence d'un processus cohérent et formalisé pour tester les concepts créatifs avant un lancement complet (pré-test) et mesurer leur impact et leur efficacité après une campagne (post-test).", + "La capacité à aller au-delà des indicateurs standards (comme les clics) pour comprendre pourquoi certains assets créatifs réussissent, en analysant des éléments spécifiques comme les visuels, les textes et les appels à l'action.", + "La maîtrise des études de brand lift pour mesurer l'impact direct des campagnes publicitaires sur des indicateurs clés de marque, tels que le rappel publicitaire, la notoriété et l'intention d'achat.", + "Le suivi des indicateurs mesurant la profondeur d'interaction du public avec le contenu, en se concentrant sur des indicateurs d'intérêt réel tels que les taux de visionnage vidéo (VTR), la profondeur de défilement ou le temps passé sur la page.", + "Le processus de suivi et de gestion des ressources investies dans la création de contenu, couvrant l'efficacité du temps passé (ex. heures/asset) et les dépenses financières directes (ex. shooting, design, licences).", + "Le suivi de l'efficacité de la production, incluant le délai de mise sur le marché, le coût par asset et l'utilisation des ressources.", + "L'utilisation d'outils automatisés (comme Creative X) qui vérifient systématiquement les assets créatifs pour s'assurer de leur conformité aux chartes de marque, mentions légales et politiques publicitaires des plateformes avant publication.", + "Le système de gestion des assets digitaux (DAM) de l'entreprise, bibliothèque centrale de tous les assets créatifs. Évalue sa disponibilité (accessibilité pour les équipes et partenaires) et sa profondeur fonctionnelle (recherche avancée, gestion des droits, intégrations logicielles).", + "L'outil de gestion et de distribution des données produits vers les canaux e-commerce (ex. Google Shopping, Facebook Ads). Évalue sa disponibilité pour toutes les équipes concernées et sa profondeur fonctionnelle — capacité à personnaliser, optimiser et analyser les flux produits.", + "Évalue la technologie de Dynamic Creative Optimization (DCO) de l'entreprise, qui automatise la création de publicités personnalisées. Mesure sa disponibilité (qui peut accéder à la plateforme) et sa profondeur fonctionnelle — sophistication des fonctionnalités de création, test et personnalisation en temps réel.", + "La plateforme de Creative Intelligence qui analyse les éléments créatifs pour fournir des informations basées sur les données. Évalue sa disponibilité (accès aux tableaux de bord) et sa profondeur fonctionnelle — capacité à aller au-delà du reporting vers l'analyse prédictive et les recommandations actionnables.", + "Les plateformes de collaboration d'équipe sur les projets marketing (ex. gestion de projet, révision créative). Évalue leur disponibilité (standardisation et accessibilité pour toutes les équipes et partenaires) et leur profondeur fonctionnelle — fonctionnalités avancées comme les cycles de validation et d'approbation intégrés.", + "La plateforme centralisée ou la marketplace préférée de l'entreprise pour sourcer et licencier des assets créatifs (ex. photothèques, illustrations, vidéos). Évalue comment ce système optimise le processus d'achat, gère les budgets et suit les droits d'utilisation.", + "L'ensemble des technologies et outils utilisés pour la production de contenu dans tous les formats et canaux", + "La disponibilité et la maîtrise des logiciels et outils créatifs pour la création et l'édition de contenu", + "L'utilisation d'outils d'automatisation pour optimiser les workflows de production et réduire les processus manuels", + "L'utilisation de modèles standardisés pour créer des assets marketing hors ligne (ex. publicités print, affiches en magasin, brochures). Évalue dans quelle mesure un système de modèles est utilisé pour maintenir la cohérence de marque et simplifier la création de supports localisés.", + "Le système de modèles standardisés pour créer des assets en ligne (ex. posts réseaux sociaux, display ads, newsletters). Évalue comment ce système garantit la cohérence de marque et soutient la production rapide et évolutive de contenu pour les canaux digitaux.", + "La capacité technique à générer automatiquement plusieurs variations d'un asset créatif en insérant dynamiquement des éléments personnalisés, selon les données utilisateur, la localisation, le comportement de navigation ou un flux produit.", + "L'utilisation de la technologie pour publier ou envoyer automatiquement les assets créatifs finaux vers divers canaux marketing, depuis une bibliothèque centrale vers les plateformes publicitaires, les planificateurs de réseaux sociaux ou un CMS, sans upload manuel.", + "La fluidité avec laquelle les étapes de validation (approbation), production (création/personnalisation) et distribution (publication) sont connectées par l'automatisation. Mesure le degré auquel les assets traversent ce cycle de vie avec un minimum d'intervention manuelle.", + "Le niveau d'automatisation des workflows de production, du concept à la livraison finale", + "L'utilisation de modèles et de standardisation pour garantir la cohérence et l'efficacité de la production", + "La capacité à traiter simultanément plusieurs assets ou contenus pour gagner en efficacité", + "L'utilisation de systèmes automatisés pour assurer le contrôle qualité et la conformité en production", + "Comment les outils d'IA générative sont intégrés dans le processus de création de contenu, depuis le brainstorming et la rédaction jusqu'à la génération d'images, ainsi que l'existence de directives formelles régissant leur application éthique et conforme à la marque.", + "L'engagement de l'entreprise à expérimenter de nouveaux types de contenu (formats émergents) comme la RA interactive, les expériences en monde virtuel ou les nouveaux formats vidéo sociaux. Évalue le volume (fréquence et échelle des tests) et la profondeur (rigueur de l'analyse) de cette expérimentation.", + "Dans quelle mesure le Test & Learn créatif est un processus systématique intégré au workflow marketing, avec une cadence régulière pour tester des hypothèses créatives, collecter des insights et les appliquer pour optimiser les futurs assets.", + "La capacité à identifier, évaluer et adopter les technologies émergentes pour les processus de production et de création", + "L'approche systématique pour expérimenter de nouveaux concepts créatifs et méthodes de production", + "La gestion des projets d'innovation, de l'idéation à la mise en œuvre", + "La planification stratégique des capacités futures de production et de création", + "L'existence, la qualité et l'accessibilité de la documentation destinée à guider l'exécution marketing et créative, couvrant les standards de marque fondamentaux, les checklists tactiques et les guides pratiques (playbooks) pour des tâches spécifiques.", + "L'efficacité avec laquelle l'organisation exploite son réseau de partenaires externes (agences, fournisseurs technologiques, freelances). Évalue si ces relations sont purement transactionnelles ou traitées comme un écosystème stratégique pour stimuler l'innovation et créer de la valeur.", + "L'efficacité et la régularité des pratiques récurrentes (rituels) qui facilitent la collaboration entre les équipes centrales et les Business Units. Évalue si ces processus sont bien définis et purposeful, assurant l'alignement entre la stratégie globale et l'exécution locale.", + "Si ces équipes travaillent dans des workflows intégrés avec des objectifs partagés ou fonctionnent en silos départementaux isolés.", + "Dans quelle mesure le cadre RACI (Responsable, Approbateur, Consulté, Informé) est formellement utilisé pour définir les rôles et responsabilités des tâches opérationnelles clés. Évalue si ces assignations sont clairement documentées, à jour et systématiquement respectées.", + "Les capacités collectives de l'équipe, évaluant la présence et la force des compétences stratégiques (ex. planification long terme, analyse de données, stratégie de marque) et opérationnelles (ex. expertise plateforme, création de contenu, gestion de campagnes). Vise à identifier les lacunes de compétences critiques.", + "La structure organisationnelle et la définition des rôles pour les équipes de production et création", + "Les programmes et processus de développement des compétences de production et création au sein de l'organisation", + "Les processus et outils de collaboration au sein et entre les équipes de production et de création", + "La capacité à gérer et à s'adapter aux changements de processus, de technologies et de structure organisationnelle" + ] + } } } diff --git a/i18n/ui.js b/i18n/ui.js new file mode 100644 index 0000000..d79cbc8 --- /dev/null +++ b/i18n/ui.js @@ -0,0 +1,262 @@ +'use strict'; +// UI string catalogue — EN + FR +// Loaded as a plain diff --git a/script.js b/script.js index 8cf9261..cce28af 100644 --- a/script.js +++ b/script.js @@ -32,7 +32,134 @@ const PILLAR_SHORT = { }; function pillarShort(name) { - return PILLAR_SHORT[name] || name.slice(0, 6).toUpperCase(); + return window.UI_STRINGS?.[currentLang]?.pillar_short?.[name] + || PILLAR_SHORT[name] + || name.slice(0, 6).toUpperCase(); +} + +// ── Language state ───────────────────────────────────────────────────────────── +let currentLang = 'en'; + +function t(key, ...args) { + const strings = window.UI_STRINGS?.[currentLang] ?? window.UI_STRINGS?.en ?? {}; + const val = (key in strings) ? strings[key] : window.UI_STRINGS?.en?.[key]; + if (val === undefined) return key; + return typeof val === 'function' ? val(...args) : val; +} + +function cfg(...path) { + const base = activeClient?.config; + if (!base) return undefined; + if (currentLang === 'fr' && base.translations?.fr) { + let v = base.translations.fr; + for (const k of path) { if (v && typeof v === 'object') v = v[k]; else { v = undefined; break; } } + if (v !== undefined) return v; + } + let v = base; + for (const k of path) { if (v && typeof v === 'object') v = v[k]; else { v = undefined; break; } } + return v; +} + +function pillarDisplayName(i) { + const frPillars = currentLang === 'fr' ? activeClient?.config?.translations?.fr?.pillars : null; + return frPillars?.[i] ?? activeClient?.config?.pillars?.[i] ?? ''; +} + +function pillarDisplayByName(englishName) { + const pillars = activeClient?.config?.pillars ?? []; + const i = pillars.indexOf(englishName); + return i >= 0 ? pillarDisplayName(i) : englishName; +} + +function scoreLabelDisplay(level) { + return cfg('scoring', 'labels')?.[level] ?? activeClient?.config?.scoring?.labels?.[level] ?? ''; +} + +function getQuestionTopic(q) { + const topics = cfg('question_topics'); + const idx = parseInt(q.q_num, 10) - 1; + if (topics && idx >= 0 && topics[idx]) return topics[idx]; + return q.topic; +} + +function applyStoredLang() { + if (!activeClient) { currentLang = 'en'; return; } + const key = `maturityLang2_${activeClient.config.id}`; + const stored = localStorage.getItem(key); + if (stored === 'en' || stored === 'fr') { currentLang = stored; return; } + currentLang = 'en'; + localStorage.setItem(key, 'en'); +} + +function toggleLang() { + currentLang = currentLang === 'en' ? 'fr' : 'en'; + const langKey = activeClient ? `maturityLang2_${activeClient.config.id}` : 'maturityLang2_home'; + localStorage.setItem(langKey, currentLang); + document.documentElement.lang = currentLang; + updateLangToggleButton(); + refreshCurrentView(); +} + +function updateLangToggleButton() { + const btn = document.getElementById('langToggle'); + const lbl = document.getElementById('langToggleLabel'); + if (!btn || !lbl) return; + const hasFr = activeClient?.config?.translations?.fr + || allClients?.some(c => c.has_translations); + btn.style.display = hasFr ? '' : 'none'; + lbl.textContent = currentLang === 'en' ? 'FR' : 'EN'; +} + +function refreshCurrentView() { + if (!activeClient) { + const aboutEl = document.getElementById('homeTab-about'); + if (aboutEl && aboutEl.style.display !== 'none') renderAbout(); + else renderHome(); + return; + } + const entityLabel = cfg('entity_label') || activeClient.config.entity_label || 'Entities'; + document.getElementById('tab-entities').textContent = entityLabel; + document.getElementById('tab-compare').textContent = t('tab_compare'); + document.getElementById('tab-heatmap').textContent = t('tab_heatmap'); + document.getElementById('tab-update').textContent = t('tab_update'); + document.getElementById('headerSub').textContent = cfg('description') || activeClient.config.description; + renderSummaryBar(); + if (activeTab === 'entities') { + if (detailEntity) openEntityDetail(detailEntity); + else renderEntityCards(); + } else if (activeTab === 'compare') { + renderCompareSelector(); + if (selectedForCompare.size >= 2) renderCompareTable(); + } else if (activeTab === 'heatmap') { + renderHeatmap(); + } else if (activeTab === 'update') { + updateUpdateTabText(); + } +} + +function updateUpdateTabText() { + const map = { + syncTitle: t('sync_title'), + syncDesc: t('sync_desc'), + syncScopeLabel: t('sync_scope'), + uploadTitle: t('upload_title'), + uploadEntityLabel: t('upload_entity'), + uploadFileLabel: t('upload_file'), + uploadNote: t('upload_note'), + exportAllLabel: t('export_all'), + }; + for (const [id, text] of Object.entries(map)) { + const el = document.getElementById(id); + if (el) el.textContent = text; + } + const syncSel = document.getElementById('syncEntitySel'); + const importSel = document.getElementById('importEntitySel'); + if (syncSel?.options[0]) syncSel.options[0].textContent = t('sync_all'); + if (importSel?.options[0]) importSel.options[0].textContent = t('upload_placeholder'); + const dropText = document.getElementById('dropZoneText'); + if (dropText && !document.getElementById('dropZone').classList.contains('has-file')) { + dropText.textContent = t('upload_drop'); + } } // ── Data quality flags ──────────────────────────────────────────────────────── @@ -42,20 +169,20 @@ function getEntityFlags(entity) { const zeroScore = qs.filter(q => !q.score || q.score === 0); if (zeroScore.length > 0) - flags.push({ icon: '✕', label: `${zeroScore.length} question${zeroScore.length > 1 ? 's' : ''} missing score` }); + flags.push({ icon: '✕', label: t('flag_missing_score', zeroScore.length) }); const noRationale = qs.filter(q => !q.rationale || q.rationale.trim().length < 5); if (noRationale.length > 0) - flags.push({ icon: '◌', label: `${noRationale.length} question${noRationale.length > 1 ? 's' : ''} missing rationale` }); + flags.push({ icon: '◌', label: t('flag_missing_rationale', noRationale.length) }); const noGaps = qs.filter(q => !q.gaps || q.gaps.trim().length < 5); if (noGaps.length > Math.floor(qs.length * 0.2)) - flags.push({ icon: '◌', label: `${noGaps.length} questions missing gap analysis` }); + flags.push({ icon: '◌', label: t('flag_missing_gaps', noGaps.length) }); if (entity.synced) { const days = Math.floor((Date.now() - new Date(entity.synced)) / 86400000); if (days > 30) - flags.push({ icon: '⏱', label: `Data last synced ${days} days ago` }); + flags.push({ icon: '⏱', label: t('flag_stale', days) }); } return flags; @@ -82,10 +209,15 @@ async function init() { try { const res = await fetch(API_BASE + '/api/clients'); allClients = await res.json(); + // Restore home-screen language preference + const stored = localStorage.getItem('maturityLang2_home'); + currentLang = (stored === 'en' || stored === 'fr') ? stored : 'en'; + document.documentElement.lang = currentLang; + updateLangToggleButton(); renderHome(); } catch (e) { - document.getElementById('headerSub').textContent = 'Could not connect to server'; - showToast('Failed to load data', 'error'); + document.getElementById('headerSub').textContent = t('toast_connect_failed'); + showToast(t('toast_load_failed'), 'error'); } } @@ -176,13 +308,13 @@ function renderAbout() { const scoreColors = { 1:'#C62828', 2:'#E65100', 3:'#2E7D32', 4:'#1B5E20' }; - const levelRows = scoring ? Object.entries(scoring.labels).map(([lvl, name]) => { + const levelRows = scoring ? Object.entries(scoring.labels).map(([lvl, lname]) => { const desc = about && about.scoring_descriptions ? about.scoring_descriptions[lvl] : ''; return `
- ${escHtml(name)} + ${escHtml(lname)}
-
Level ${escHtml(lvl)}
+
${escHtml(t('about_level', lvl))}
${desc ? `
${escHtml(desc)}
` : ''}
`; @@ -206,7 +338,7 @@ function renderAbout() {
-

What is this tool?

+

${escHtml(t('about_what_is'))}

${about ? escHtml(about.summary) : 'A content maturity assessment dashboard for evaluating and comparing business units across key marketing capability pillars.'}

@@ -222,21 +354,14 @@ function renderAbout() {
-

Maturity Levels

+

${escHtml(t('about_maturity_levels'))}

${levelRows}
-

How to use this tool

- ${[ - ['Markets tab', 'Browse all markets sorted by overall score. Click any card to drill into pillar scores and individual question rationale.'], - ['Heatmap tab', 'See all markets and pillars in one grid — instantly spot which pillars are strong or weak across the board.'], - ['Compare tab', 'Select 2 or more markets to put them side by side. Green = highest score in that pillar, red = lowest.'], - ['Radar chart', 'Each market detail view shows a spider chart of its 7 pillar scores — useful for understanding the shape of a market\'s maturity.'], - ['⚠ Flag badge', 'An amber warning on a card means that market has incomplete data (missing scores, rationale, or gap analysis). Click the card to see detail.'], - ['Update Data tab', 'Re-sync data from Box source files or upload a new CSV/XLSX directly.'], - ].map(([title, desc]) => ` +

${escHtml(t('about_how_to'))}

+ ${t('how_to_items').map(([title, desc]) => `
${escHtml(title)}
${escHtml(desc)}
@@ -245,7 +370,7 @@ function renderAbout() {
-

The ${(client && client.pillars || []).length} Pillars

+

${escHtml(t('about_the_pillars', (client && client.pillars || []).length))}

${pillarRows}
@@ -259,7 +384,7 @@ function renderHome() { document.getElementById('tabBar').style.display = 'none'; document.getElementById('homeScreen').style.display = ''; document.getElementById('clientView').style.display = 'none'; - document.getElementById('headerTitle').textContent = 'Maturity Tool'; + document.getElementById('headerTitle').textContent = t('header_title'); document.getElementById('headerSub').textContent = `${allClients.length} client${allClients.length !== 1 ? 's' : ''}`; if (allClients.length === 1) { @@ -277,20 +402,20 @@ function renderHome() {

- Click below to explore the maturity scores, pillar breakdowns, and deliverables for all ${c.entity_count} ${escHtml(c.entity_label || 'markets')}. + ${escHtml(t('home_explore', c.entity_count, c.entity_label || 'markets'))}

-

Last updated ${escHtml(c.generated)}

+

${escHtml(t('home_last_updated', c.generated))}

-

New Client

-

Set up a new maturity assessment client

+

${escHtml(t('home_new_client'))}

+

${escHtml(t('home_new_client_sub'))}

`; } else { document.getElementById('clientCards').innerHTML = allClients.map((c, i) => ` @@ -307,8 +432,8 @@ function renderHome() {

${escHtml(c.name)}

${escHtml(c.description)}

- Generated ${escHtml(c.generated)} - Open → + ${escHtml(t('home_generated', c.generated))} + ${escHtml(t('home_open_arrow'))}
`).join('') + ` @@ -316,8 +441,8 @@ function renderHome() {
-

New Client

-

Set up a new maturity assessment client

+

${escHtml(t('home_new_client'))}

+

${escHtml(t('home_new_client_sub'))}

`; } } @@ -361,7 +486,7 @@ async function renderSingleClientHome(id) { ${e.overall_score.toFixed(2)} - ${escHtml(e.overall_label)} + ${escHtml(scoreLabelDisplay(e.overall_level) || e.overall_label)} `; }).join(''); @@ -389,15 +514,15 @@ async function renderSingleClientHome(id) {
${avg}
-
Avg Score
+
${escHtml(t('summary_avg'))}
-
Highest
+
${escHtml(t('summary_highest'))}
${escHtml(best.short)}
${best.overall_score.toFixed(2)}
-
Lowest
+
${escHtml(t('summary_lowest'))}
${escHtml(worst.short)}
${worst.overall_score.toFixed(2)}
@@ -407,21 +532,21 @@ async function renderSingleClientHome(id) {
-

${escHtml(config.entity_label || 'Entities')} Overview

+

${escHtml(cfg('entity_label') || config.entity_label || 'Entities')} Overview

${marketRows}
-

Data generated ${escHtml(data.generated)} · Click any row to view details

+

${escHtml(t('overview_generated', data.generated))}

@@ -437,54 +562,72 @@ async function renderSingleClientHome(id) { }); } catch (e) { - showToast('Failed to load overview', 'error'); + showToast(t('toast_overview_failed'), 'error'); } } function renderAboutSection(config) { - const about = config.about; - const scoring = config.scoring; + const about = config.about; + const scoring = config.scoring; + const frAbout = cfg('about') || about; + const frScoring = cfg('scoring') || scoring; + const frPillars = cfg('pillars') || config.pillars; + const frSummary = frAbout?.summary || about?.summary; - const pillarGrid = config.pillars.map(pname => { - const desc = (about.pillar_descriptions || {})[pname] || ''; + const pillarGrid = config.pillars.map((pname, i) => { + const displayName = pillarDisplayName(i); + const desc = (frAbout?.pillar_descriptions || {})[displayName] + || (about?.pillar_descriptions || {})[pname] + || ''; return `
-

${escHtml(pname)}

+

${escHtml(displayName)}

${escHtml(desc)}

`; }).join(''); const scoringRows = Object.keys(scoring.labels).sort((a,b) => a-b).map(n => { - const lvl = scoreClass(parseInt(n)); - const desc = (about.scoring_descriptions || {})[n] || ''; + const lvl = scoreClass(parseInt(n)); + const label = frScoring?.labels?.[n] || scoring.labels[n]; + const desc = (frAbout?.scoring_descriptions || {})[n] + || (about?.scoring_descriptions || {})[n] + || ''; return `
- ${n} — ${escHtml(scoring.labels[n])} + ${n} — ${escHtml(label)} ${escHtml(desc)}
`; }).join(''); + const pillarsCountText = about?.question_count + ? `${about.question_count} ${currentLang === 'fr' ? 'questions sur' : 'Questions across'} ${config.pillars.length} ${currentLang === 'fr' ? 'piliers' : 'Pillars'}` + : `${config.pillars.length} ${currentLang === 'fr' ? 'Piliers' : 'Pillars'}`; + return `
-

About the Assessment

- ${about.summary ? `

${escHtml(about.summary)}

` : ''} +

${escHtml(t('about_assessment'))}

+ ${frSummary ? `

${escHtml(frSummary)}

` : ''}

- ${about.question_count ? `${about.question_count} Questions across ` : ''}${config.pillars.length} Pillars + ${escHtml(pillarsCountText)}

${pillarGrid}
-

Scoring Scale

+

${escHtml(t('about_scoring_scale'))}

${scoringRows}
`; } function goHome() { activeClient = null; + const stored = localStorage.getItem('maturityLang2_home'); + currentLang = (stored === 'en' || stored === 'fr') ? stored : 'en'; + document.documentElement.lang = currentLang; detailEntity = null; selectedForCompare.clear(); + updateLangToggleButton(); renderHome(); } @@ -707,10 +850,10 @@ function wizardSaveStep() { } function wizardValidate() { - if (wizStep === 0 && !wizData.name) { showToast('Client name is required', 'error'); return false; } - if (wizStep === 0 && !wizData.id) { showToast('Could not derive a client ID from that name', 'error'); return false; } + if (wizStep === 0 && !wizData.name) { showToast(t('wiz_client_required'), 'error'); return false; } + if (wizStep === 0 && !wizData.id) { showToast(t('wiz_id_failed'), 'error'); return false; } if (wizStep === 3 && !wizData.pillars.filter(p => p.trim()).length) { - showToast('Add at least one pillar', 'error'); return false; + showToast(t('wiz_pillar_required'), 'error'); return false; } return true; } @@ -741,13 +884,13 @@ async function createClient() { const data = await res.json(); if (!res.ok) throw new Error(data.error || res.statusText); closeWizard(); - showToast(`"${cfg.name}" created! Set up data in the Update tab.`, 'success'); + showToast(t('wiz_created', cfg.name), 'success'); const r2 = await fetch(API_BASE + '/api/clients'); allClients = await r2.json(); await loadClient(data.id); showTab('update'); } catch (e) { - showToast('Failed to create: ' + e.message, 'error'); + showToast(t('wiz_create_failed', e.message), 'error'); btn.disabled = false; btn.textContent = '✓ Create Client'; } } @@ -764,6 +907,9 @@ async function loadClient(id) { const data = await datRes.json(); const deliverables = delRes.ok ? await delRes.json() : {}; activeClient = { config, data, deliverables }; + applyStoredLang(); + document.documentElement.lang = currentLang; + updateLangToggleButton(); applyAccent(config.accent_color); enterClientView(config); @@ -772,7 +918,7 @@ async function loadClient(id) { renderCompareSelector(); document.getElementById('exportRow').style.display = 'flex'; } catch (e) { - showToast('Failed to load client data', 'error'); + showToast(t('toast_client_failed'), 'error'); } } @@ -793,8 +939,11 @@ function enterClientView(config) { document.getElementById('clientView').style.display = ''; // Update tab label to match entity type - const entityLabel = config.entity_label || 'Entities'; + const entityLabel = cfg('entity_label') || config.entity_label || 'Entities'; document.getElementById('tab-entities').textContent = entityLabel; + document.getElementById('tab-compare').textContent = t('tab_compare'); + document.getElementById('tab-heatmap').textContent = t('tab_heatmap'); + document.getElementById('tab-update').textContent = t('tab_update'); showTab('entities'); } @@ -806,7 +955,7 @@ function showTab(name) { document.getElementById(`tab-${t}`).classList.toggle('active', t === name); }); activeTab = name; - if (name === 'update') populateImportEntitySelect(); + if (name === 'update') { populateImportEntitySelect(); updateUpdateTabText(); } if (name === 'heatmap') renderHeatmap(); } @@ -817,7 +966,7 @@ function renderSummaryBar() { const avg = (scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(2); const best = entities[0]; const worst = entities[entities.length - 1]; - const entityLabel = activeClient.config.entity_label || 'Entities'; + const entityLabel = cfg('entity_label') || activeClient.config.entity_label || 'Entities'; document.getElementById('summaryBar').innerHTML = `
@@ -827,15 +976,15 @@ function renderSummaryBar() {
${avg}
-
Avg Score
+
${escHtml(t('summary_avg'))}
-
Highest
+
${escHtml(t('summary_highest'))}
${escHtml(best.short)}
${best.overall_score.toFixed(2)}
-
Lowest
+
${escHtml(t('summary_lowest'))}
${escHtml(worst.short)}
${worst.overall_score.toFixed(2)}
@@ -853,18 +1002,18 @@ function renderCardControls() { if (!controls) return; const pillars = activeClient.config.pillars; - const pillarOpts = pillars.map(p => - `` + const pillarOpts = pillars.map((p, i) => + `` ).join(''); controls.innerHTML = ` - Sort by: + ${escHtml(t('sort_by'))} `; @@ -968,11 +1117,12 @@ function buildEntityCard(entity, i) { const flags = getEntityFlags(entity); const flagTip = flags.map(f => f.label).join(' · '); - const pillarBars = pillars.map(pname => { + const pillarBars = pillars.map((pname, pi) => { const p = entity.pillars.find(x => x.name === pname); if (!p) return ''; - const pct = pillarBarPct(p.avg).toFixed(1); - const short = pillarShort(pname); + const pct = pillarBarPct(p.avg).toFixed(1); + const displayName = pillarDisplayName(pi); + const short = pillarShort(displayName); return `
${escHtml(short)} @@ -991,14 +1141,14 @@ function buildEntityCard(entity, i) {

${escHtml(entity.label)}

${entity.overall_score.toFixed(2)} - ${escHtml(entity.overall_label)} + ${escHtml(scoreLabelDisplay(entity.overall_level) || entity.overall_label)}
${pillarBars}
- ${entity.pillars.reduce((s, p) => s + p.questions.length, 0)} questions - ${flags.length ? `⚠ ${flags.length} issue${flags.length > 1 ? 's' : ''}` : ''} - View detail → + ${escHtml(t('card_questions', entity.pillars.reduce((s, p) => s + p.questions.length, 0)))} + ${flags.length ? `${escHtml(t('card_issues', flags.length))}` : ''} + ${escHtml(t('card_view_detail'))}
`; } @@ -1064,14 +1214,15 @@ function radarSvg(entity, config) { // Labels const labels = pillars.map((pname, i) => { - const angle = (Math.PI * 2 * i / n) - Math.PI / 2; - const labelR = r + 22; - const lx = cx + labelR * Math.cos(angle); - const ly = cy + labelR * Math.sin(angle); - let anchor = 'middle'; + const angle = (Math.PI * 2 * i / n) - Math.PI / 2; + const labelR = r + 22; + const lx = cx + labelR * Math.cos(angle); + const ly = cy + labelR * Math.sin(angle); + let anchor = 'middle'; if (lx < cx - 8) anchor = 'end'; else if (lx > cx + 8) anchor = 'start'; - const short = pillarShort(pname); + const displayName = pillarDisplayName(i); + const short = pillarShort(displayName); return `${escHtml(short)}`; }).join(''); @@ -1095,11 +1246,13 @@ function openEntityDetail(entity) { const lvlCls = scoreClass(entity.overall_level); const bgCls = scoreBgClass(entity.overall_level); - const pillarAccordions = pillars.map(pname => { + const pillarAccordions = pillars.map((pname, pi) => { const p = entity.pillars.find(x => x.name === pname); if (!p) return ''; - const plvl = scoreClass(p.level); - const pbg = scoreBgClass(p.level); + const plvl = scoreClass(p.level); + const pbg = scoreBgClass(p.level); + const displayName = pillarDisplayName(pi); + const plabel = scoreLabelDisplay(p.level) || p.label; const pct = pillarBarPct(p.avg).toFixed(1); const qRows = p.questions.map((q, qi) => { @@ -1107,7 +1260,7 @@ function openEntityDetail(entity) { return `
${q.score} - ${escHtml(q.topic)} + ${escHtml(getQuestionTopic(q))}
`; }).join(''); @@ -1116,7 +1269,7 @@ function openEntityDetail(entity) {
- ${escHtml(pname)} + ${escHtml(displayName)} @@ -1124,7 +1277,7 @@ function openEntityDetail(entity) {
${p.avg.toFixed(2)} - ${escHtml(p.label)} + ${escHtml(plabel)}
@@ -1136,16 +1289,16 @@ function openEntityDetail(entity) { // Deliverable buttons (if configured for this entity) const del = (activeClient.deliverables || {})[entity.id] || {}; const deliverablesBtns = (del.pdf || del.xlsx) ? ` - Deliverables + ${escHtml(t('deliverables_label'))} ${del.pdf ? ` ` : ''} ${del.xlsx ? ` ` : ''} ` : ''; @@ -1154,11 +1307,11 @@ function openEntityDetail(entity) {
${deliverablesBtns} - Export + ${escHtml(t('export_label'))}
@@ -1188,12 +1341,12 @@ function openEntityDetail(entity) { return `
- Data quality + ${escHtml(t('data_quality'))} ${flags.map(f => `${escHtml(f.label)}`).join(' · ')}
`; })()} -

Pillar Breakdown

+

${escHtml(t('pillar_breakdown'))}

${pillarAccordions}
`; @@ -1225,20 +1378,21 @@ function openQuestionModal(entity, pillar, question) { ${escHtml(entity.short)} ${escHtml(pillar.name)} `; - document.getElementById('qModalTitle').textContent = question.topic; + document.getElementById('qModalTitle').textContent = getQuestionTopic(question); document.getElementById('qModalMeta').textContent = `Q${question.q_num} · ${pillar.name} · ${entity.label}`; const fields = [ - { label: 'Rationale', value: question.rationale }, - { label: 'Gaps Identified', value: question.gaps || '—' }, - { label: 'References', value: question.refs || '—' }, + { label: t('field_rationale'), value: question.rationale }, + { label: t('field_gaps'), value: question.gaps || '—' }, + { label: t('field_refs'), value: question.refs || '—' }, ]; const scoring = activeClient.config.scoring; const scoreNums = Object.keys(scoring.labels).map(Number).sort((a, b) => a - b); const scoreBtns = scoreNums.map(n => { - const lvl = scoreToLevel(n, scoring); - const isCurrent = n === question.score; + const lvl = scoreToLevel(n, scoring); + const isCurrent = n === question.score; + const scoreLabel = scoreLabelDisplay(lvl) || scoring.labels[n]; return ` `; }).join(''); document.getElementById('qModalBody').innerHTML = `
- Score + ${escHtml(t('field_score'))}
${scoreBtns}
-

Click a score to update it

+

${escHtml(t('score_hint'))}

${fields.map(f => { @@ -1318,7 +1472,7 @@ async function updateScore(entityId, pillarName, qNum, newScore) { btn.style.background = isCurrent ? 'rgba(120,190,32,0.08)' : 'var(--bg-inset)'; }); const hint = document.getElementById('scoreEditHint'); - if (hint) hint.innerHTML = `✓ Score updated ${oldScore} → ${newScore}`; + if (hint) hint.innerHTML = `${escHtml(t('score_updated', oldScore, newScore))}`; // Re-render cards/detail if visible if (detailEntity && detailEntity.id === entityId) { @@ -1337,9 +1491,9 @@ async function updateScore(entityId, pillarName, qNum, newScore) { body: JSON.stringify(activeClient.data), }); if (!res.ok) throw new Error(await res.text()); - showToast(`Q${qNum} score saved: ${newScore} (${question.label})`, 'success'); + showToast(t('toast_score_saved', qNum, newScore, scoreLabelDisplay(question.level) || question.label), 'success'); } catch (e) { - showToast('Save failed — changes kept in memory only', 'error'); + showToast(t('toast_save_failed'), 'error'); } } @@ -1357,7 +1511,7 @@ function renderCompareSelector() { const entityLabel = activeClient.config.entity_label || 'Entities'; document.getElementById('compareSelector').innerHTML = ` -

Select ${escHtml(entityLabel)} to Compare

+

${escHtml(t('compare_select', entityLabel))}

${entities.map(e => { const lvl = scoreClass(e.overall_level); @@ -1369,15 +1523,15 @@ function renderCompareSelector() {

${escHtml(e.short)}

${e.overall_score.toFixed(2)} - ${escHtml(e.overall_label)} + ${escHtml(scoreLabelDisplay(e.overall_level) || e.overall_label)}

`; }).join('')}
- - + +
`; } @@ -1394,7 +1548,7 @@ function clearCompare() { function renderCompareTable() { if (selectedForCompare.size < 2) { - showToast('Select at least 2 to compare', 'error'); + showToast(t('toast_select_two'), 'error'); return; } @@ -1447,7 +1601,7 @@ function renderCompareTable() { const maxO = Math.max(...overallDef), minO = Math.min(...overallDef); const allSameO = overallDef.every(v => v === overallDef[0]); const overallRow = ` - OVERALL + ${escHtml(t('compare_overall'))} ${markets.map(m => { const cls = !allSameO && m.overall_score === maxO ? 'diff-hi' : !allSameO && m.overall_score === minO ? 'diff-lo' : ''; return ` @@ -1456,12 +1610,12 @@ function renderCompareTable() { }).join('')} `; - const pillarRows = pillars.map(pname => { + const pillarRows = pillars.map((pname, pi) => { const vals = markets.map(m => { const p = m.pillars.find(x => x.name === pname); return p ? p.avg : null; }); - return buildRow(pname, vals); + return buildRow(pillarDisplayName(pi), vals); }).join(''); wrap.innerHTML = ` @@ -1473,8 +1627,8 @@ function renderCompareTable() {
- Highest in row - Lowest in row + ${escHtml(t('compare_legend_high'))} + ${escHtml(t('compare_legend_low'))}
`; } @@ -1500,6 +1654,7 @@ function renderHeatmap() { // Pillar rows const pillarRows = pillars.map((pname, ri) => { + const displayName = pillarDisplayName(ri); const cells = entities.map(e => { const p = e.pillars.find(x => x.name === pname); if (!p) return `—`; @@ -1519,7 +1674,7 @@ function renderHeatmap() { const rowBg = ri % 2 === 0 ? '' : 'background:var(--bg-inset);'; return ` - ${escHtml(pname)} + ${escHtml(displayName)} ${cells} ${rowAvgCell} `; @@ -1539,20 +1694,20 @@ function renderHeatmap() { const grandAvgCell = `${grandAvg.toFixed(2)}`; panel.innerHTML = ` -

Pillar Heatmap

+

${escHtml(t('heatmap_title'))}

- + ${headerCells} - + ${pillarRows} - + ${overallCells} ${grandAvgCell} @@ -1562,7 +1717,8 @@ function renderHeatmap() {
${Object.keys(scoring.labels).sort((a,b)=>a-b).map(n => { const bgCls = scoreBgClass(parseInt(n)); - return `${n} ${escHtml(scoring.labels[n])}`; + const lbl = scoreLabelDisplay(parseInt(n)) || scoring.labels[n]; + return `${n} ${escHtml(lbl)}`; }).join('')}
`; @@ -1589,7 +1745,7 @@ async function downloadDeliverable(entityId, type) { const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); - showToast(`${label} downloaded`); + showToast(type === 'pdf' ? t('toast_pdf_downloaded') : t('toast_xlsx_downloaded')); } catch (e) { showToast(`${label} unavailable: ${e.message}`, 'error'); } finally { @@ -1633,7 +1789,7 @@ function exportCsv() { r.map(v => `"${String(v ?? '').replace(/"/g, '""')}"`).join(',') ).join('\r\n'); triggerDownload(`${config.id}_maturity_all_entities.csv`, 'text/csv;charset=utf-8;', csv); - showToast('CSV downloaded'); + showToast(t('toast_csv_downloaded')); } // XLSX — multi-sheet workbook @@ -1726,7 +1882,7 @@ function exportXlsx(entityId) { : `${config.id}_Maturity_All_Entities.xlsx`; XLSX.writeFile(wb, filename); - showToast('XLSX downloaded'); + showToast(t('toast_xlsx_downloaded')); } // PDF — server-generated via Python/ReportLab @@ -1746,9 +1902,9 @@ async function downloadPdf(entityId) { a.download = `${entityId}_Maturity_Report.pdf`; a.click(); URL.revokeObjectURL(url); - showToast('PDF downloaded'); + showToast(t('toast_pdf_downloaded')); } catch (e) { - showToast('PDF failed: ' + e.message, 'error'); + showToast(t('toast_pdf_failed', e.message), 'error'); } finally { if (btn) { btn.innerHTML = ' PDF'; btn.disabled = false; } } @@ -1772,9 +1928,9 @@ function populateImportEntitySelect() { `` ).join(''); document.getElementById('importEntitySel').innerHTML = - '' + options; + `` + options; document.getElementById('syncEntitySel').innerHTML = - '' + options; + `` + options; } function onFileSelected(file) { @@ -1815,10 +1971,10 @@ async function runBoxSync() { const data = await res.json(); if (!res.ok) { log.textContent = `Error:\n${data.detail || data.error}`; - showToast('Sync failed', 'error'); + showToast(t('toast_sync_failed'), 'error'); } else { log.textContent = (data.log || 'Done.').trim(); - showToast('Sync complete — reloading…', 'success'); + showToast(t('toast_sync_complete'), 'success'); setTimeout(async () => { await loadClient(activeClient.config.id); showTab('entities'); @@ -1826,7 +1982,7 @@ async function runBoxSync() { } } catch (e) { log.textContent = 'Network error: ' + e.message; - showToast('Sync failed: ' + e.message, 'error'); + showToast(t('toast_sync_failed') + ': ' + e.message, 'error'); } finally { btn.disabled = false; btn.innerHTML = ` @@ -1839,8 +1995,8 @@ async function runBoxSync() { async function runFileImport() { const entityId = document.getElementById('importEntitySel').value; - if (!entityId) { showToast('Select an entity first', 'error'); return; } - if (!importFile) { showToast('Select a file to upload', 'error'); return; } + if (!entityId) { showToast(t('toast_select_entity'), 'error'); return; } + if (!importFile) { showToast(t('toast_select_file'), 'error'); return; } const btn = document.getElementById('importRunBtn'); btn.disabled = true; @@ -1857,22 +2013,22 @@ async function runFileImport() { }); const data = await res.json(); if (!res.ok) { - showToast('Import failed: ' + (data.error || res.statusText), 'error'); + showToast(t('toast_import_failed', data.error || res.statusText), 'error'); } else { const e = data.entity; - showToast(`Imported ${escHtml(e.label)}: ${e.overall_score.toFixed(2)} (${e.overall_label})`, 'success'); + showToast(t('toast_import_done', e.label, e.overall_score.toFixed(2), e.overall_label), 'success'); await loadClient(activeClient.config.id); showTab('entities'); } } catch (e) { - showToast('Import failed: ' + e.message, 'error'); + showToast(t('toast_import_failed', e.message), 'error'); } finally { btn.disabled = false; btn.innerHTML = ` - Import`; + ${escHtml(t('upload_btn'))}`; } } diff --git a/server/index.js b/server/index.js index 38ff212..d9b07a5 100644 --- a/server/index.js +++ b/server/index.js @@ -77,7 +77,8 @@ app.get('/api/clients', authenticate, (_req, res) => { generated: dat.generated, pillars: cfg.pillars || [], scoring: cfg.scoring || {}, - about: cfg.about || null, + about: cfg.about || null, + has_translations: !!(cfg.translations && Object.keys(cfg.translations).length > 0), }; }); res.json(clients);
Pillar${escHtml(t('heatmap_pillar'))}Avg${escHtml(t('heatmap_avg'))}
Overall${escHtml(t('heatmap_overall'))}