From da752099f2c502a13fa94ee23cfb47826ac8af8a Mon Sep 17 00:00:00 2001 From: Eli Wang Date: Mon, 20 Jul 2026 13:56:06 -0700 Subject: [PATCH 1/6] feat: (AIME-194) Spanish localization for Brand Concierge widget UI Add a Spanish (es) locale to the Brand Concierge widget, resolved from the URL path via getPathDetails().lang. English remains the untouched default. - brand-concierge-config.js: add BC_UI_EN (English chrome strings), BC_LOCALES.es overlay (ui/text/arrays), and a pure resolveBrandConciergeConfig(lang). - brand-concierge.js: resolve locale into activeConfig before mount; read chrome strings from activeConfig.ui; forward localized text/arrays and strip ui from the bootstrap options. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../brand-concierge/brand-concierge-config.js | 122 ++++++++++++++++++ scripts/brand-concierge/brand-concierge.js | 45 ++++--- 2 files changed, 151 insertions(+), 16 deletions(-) diff --git a/scripts/brand-concierge/brand-concierge-config.js b/scripts/brand-concierge/brand-concierge-config.js index 1c837a7461..32a9ab01d3 100644 --- a/scripts/brand-concierge/brand-concierge-config.js +++ b/scripts/brand-concierge/brand-concierge-config.js @@ -130,4 +130,126 @@ const brandConciergeConfig = { }, }; +/** + * English chrome strings (trigger button, drawer, clear control, legal disclaimer). + * These render from ExL's own code in brand-concierge.js (not forwarded to the BC web + * client), so they live here alongside the client-facing `text`/`arrays` above and are + * threaded through as `config.ui`. 'BETA' is intentionally left untranslated (brand term). + */ +const BC_UI_EN = { + triggerAriaLabel: 'Open AI assistant', + triggerAsk: 'Ask a question', + drawerAriaLabel: 'AI assistant', + drawerTitle: 'Ask', + clearLabel: 'Clear', + clearAriaLabel: 'Clear conversation', + disclaimer: { + prefix: "Use of this beta AI chatbot is subject to Adobe's ", + privacyLabel: 'Privacy Policy', + middle: + ". Don't share sensitive data. AI responses are not your Content, may be inaccurate, and any offers provided are non-binding. ", + termsLabel: 'Generative AI Terms', + suffix: '.', + }, +}; + +/** + * Per-locale overlays merged onto the English base by resolveBrandConciergeConfig(). + * Keyed by getPathDetails().lang (e.g. 'es' for /es/... paths). Translations are + * review-pending — legal disclaimer copy in particular needs sign-off before launch. + */ +const BC_LOCALES = { + es: { + language: 'es-ES', + ui: { + triggerAriaLabel: 'Abrir el asistente de IA', + triggerAsk: 'Hacer una pregunta', + drawerAriaLabel: 'Asistente de IA', + drawerTitle: 'Preguntar', + clearLabel: 'Borrar', + clearAriaLabel: 'Borrar conversación', + disclaimer: { + prefix: 'El uso de este chatbot de IA en versión beta está sujeto a la ', + privacyLabel: 'Política de privacidad', + middle: + ' de Adobe. No comparta datos confidenciales. Las respuestas de la IA no son su Contenido, pueden ser inexactas y cualquier oferta proporcionada no es vinculante. ', + termsLabel: 'Términos de IA generativa', + suffix: '.', + }, + }, + text: { + 'welcome.heading': '¿No sabe por dónde empezar?
Pregúnteme lo que quiera sobre los productos de Adobe.', + 'welcome.subheading': 'Escriba su pregunta o elija una sugerencia a continuación.', + 'input.placeholder': 'Haga una pregunta…', + 'input.messageInput.aria': 'Campo de mensaje', + 'input.send.aria': 'Enviar mensaje', + 'input.mic.aria': 'Entrada de voz', + 'card.aria.select': 'Seleccionar mensaje de ejemplo', + 'carousel.prev.aria': 'Tarjetas anteriores', + 'carousel.next.aria': 'Tarjetas siguientes', + 'scroll.bottom.aria': 'Desplazarse hasta abajo', + 'error.network': + 'Lo sentimos, en este momento tenemos problemas de conexión. Vuelva a intentarlo en unos instantes.', + 'error.general': 'Lo sentimos, se ha producido un error. Vuelva a intentarlo en unos instantes.', + 'loading.message': 'Generando a partir de los recursos fiables de Adobe', + 'feedback.dialog.title.positive': 'Agradecemos sus comentarios', + 'feedback.dialog.title.negative': 'Agradecemos sus comentarios', + 'feedback.dialog.question.positive': '¿Qué salió bien? Seleccione todas las opciones aplicables.', + 'feedback.dialog.question.negative': '¿Qué salió mal? Seleccione todas las opciones aplicables.', + 'feedback.dialog.notes': 'Notas', + 'feedback.dialog.submit': 'Enviar', + 'feedback.dialog.cancel': 'Cancelar', + 'feedback.dialog.notes.placeholder': 'Notas adicionales (opcional)', + 'feedback.toast.success': 'Gracias por sus comentarios.', + 'feedback.thumbsUp.aria': 'Me gusta', + 'feedback.thumbsDown.aria': 'No me gusta', + }, + arrays: { + 'welcome.examples': [ + { text: '¿Dónde puedo aprender sobre la IA en Experience League?' }, + { text: 'Primeros pasos con Experience Manager' }, + { text: 'Configurar un conjunto de informes de Adobe Analytics' }, + { text: 'Explicar las pruebas A/B de Adobe Target' }, + ], + 'feedback.positive.options': [ + 'Útil y relevante', + 'Claro y fácil de entender', + 'Tono cercano y conversacional', + 'Otro', + ], + 'feedback.negative.options': [ + 'Poco útil o irrelevante', + 'Confuso o poco claro', + 'Demasiado formal o robótico', + 'Otro', + ], + }, + }, +}; + +/** + * Resolves the BC config for a given path language, layering any locale overlay onto the + * English base. English (and any unknown lang) returns the base config plus English `ui`. + * Pure function — the caller supplies `lang` (from getPathDetails().lang) so this module + * stays free of a scripts.js import and its cyclic dependency. + * @param {string} [lang] - Path language, e.g. 'en', 'es'. + * @returns {typeof brandConciergeConfig & { ui: typeof BC_UI_EN }} + */ +export function resolveBrandConciergeConfig(lang) { + const overlay = BC_LOCALES[(lang || 'en').toLowerCase()]; + if (!overlay) { + return { ...brandConciergeConfig, ui: BC_UI_EN }; + } + return { + ...brandConciergeConfig, + ui: { ...BC_UI_EN, ...overlay.ui }, + text: { ...brandConciergeConfig.text, ...overlay.text }, + arrays: { ...brandConciergeConfig.arrays, ...overlay.arrays }, + metadata: { + ...brandConciergeConfig.metadata, + ...(overlay.language ? { language: overlay.language } : {}), + }, + }; +} + export default brandConciergeConfig; diff --git a/scripts/brand-concierge/brand-concierge.js b/scripts/brand-concierge/brand-concierge.js index 06c95b9e1c..31eb983bb9 100644 --- a/scripts/brand-concierge/brand-concierge.js +++ b/scripts/brand-concierge/brand-concierge.js @@ -1,8 +1,8 @@ // eslint-disable-next-line import/no-cycle -import { getConfig } from '../scripts.js'; +import { getConfig, getPathDetails } from '../scripts.js'; import { loadScript, decorateIcon } from '../lib-franklin.js'; import { openDrawer } from '../dialog/dialog.js'; -import brandConciergeConfig from './brand-concierge-config.js'; +import { resolveBrandConciergeConfig } from './brand-concierge-config.js'; // Separate alloy instance avoids conflicting with the Launch-owned window.alloy. const ALLOY_INSTANCE_NAME = 'alloyBC'; @@ -51,6 +51,13 @@ const warn = (...args) => console.warn('[BC]', ...args); // eslint-disable-next-line no-console const error = (...args) => console.error('[BC]', ...args); +/** + * Locale-resolved config (English base + any locale overlay). Re-assigned in + * initBrandConcierge() from the page's path language; defaults to the English resolution + * so `activeConfig.ui` is always populated even before init runs. + */ +let activeConfig = resolveBrandConciergeConfig('en'); + let cssLinkEl = null; let drawerHandle = null; let inputLabelIconObserver = null; @@ -130,6 +137,8 @@ function patchBcSparkleIcons(mount) { } function buildPanelDisclaimer() { + const copy = activeConfig.ui.disclaimer; + const disclaimer = document.createElement('p'); disclaimer.id = PANEL_DISCLAIMER_ID; disclaimer.className = 'bc-panel-disclaimer'; @@ -138,22 +147,20 @@ function buildPanelDisclaimer() { privacyLink.href = PRIVACY_POLICY_URL; privacyLink.target = '_blank'; privacyLink.rel = 'noopener noreferrer'; - privacyLink.textContent = 'Privacy Policy'; + privacyLink.textContent = copy.privacyLabel; const termsLink = document.createElement('a'); termsLink.href = GENERATIVE_AI_TERMS_URL; termsLink.target = '_blank'; termsLink.rel = 'noopener noreferrer'; - termsLink.textContent = 'Generative AI Terms'; + termsLink.textContent = copy.termsLabel; disclaimer.append( - document.createTextNode("Use of this beta AI chatbot is subject to Adobe's "), + document.createTextNode(copy.prefix), privacyLink, - document.createTextNode( - ". Don't share sensitive data. AI responses are not your Content, may be inaccurate, and any offers provided are non-binding. ", - ), + document.createTextNode(copy.middle), termsLink, - document.createTextNode('.'), + document.createTextNode(copy.suffix), ); return disclaimer; @@ -414,7 +421,9 @@ function handleBrandConciergeClientEvent(event) { } function getBootstrapOptions() { - const { stickySession = false, ...stylingConfigurations } = brandConciergeConfig; + // `ui` holds ExL chrome strings (rendered by our own code) — strip it so only BC's + // own styling/text/arrays are forwarded to the third-party web client. + const { stickySession = false, ui: _ui, ...stylingConfigurations } = activeConfig; return { instanceName: ALLOY_INSTANCE_NAME, stylingConfigurations, @@ -699,9 +708,11 @@ function installKeyboardScrollHandler(dialog, mount) { function createMountPoint() { if (document.getElementById(DIALOG_ID)) return document.getElementById(DIALOG_ID); + const { ui } = activeConfig; + const trigger = document.createElement('button'); trigger.id = TRIGGER_ID; - trigger.setAttribute('aria-label', 'Open AI assistant'); + trigger.setAttribute('aria-label', ui.triggerAriaLabel); trigger.setAttribute('aria-expanded', 'false'); trigger.setAttribute('aria-controls', DIALOG_ID); @@ -709,7 +720,7 @@ function createMountPoint() { triggerIcon.className = 'icon icon-bc-ask-sparkles'; const triggerAsk = document.createElement('span'); triggerAsk.className = 'bc-trigger-ask'; - triggerAsk.textContent = 'Ask a question'; + triggerAsk.textContent = ui.triggerAsk; trigger.append(triggerIcon, triggerAsk); const betaBadge = document.createElement('span'); betaBadge.className = 'bc-trigger-beta'; @@ -729,13 +740,13 @@ function createMountPoint() { clearBtn.id = HEADER_CLEAR_ID; clearBtn.type = 'button'; clearBtn.className = 'exl-dialog-header-clear'; - clearBtn.textContent = 'Clear'; - clearBtn.setAttribute('aria-label', 'Clear conversation'); + clearBtn.textContent = ui.clearLabel; + clearBtn.setAttribute('aria-label', ui.clearAriaLabel); drawerHandle = openDrawer({ id: DIALOG_ID, - ariaLabel: 'AI assistant', - title: 'Ask', + ariaLabel: ui.drawerAriaLabel, + title: ui.drawerTitle, titleBadge: 'BETA', titleIcon: 'bc-ask-sparkles', content: mount, @@ -829,6 +840,8 @@ export function destroyBrandConcierge() { export async function initBrandConcierge() { const { bcAlloySdkUrl, bcDatastreamId, bcOrgId, bcWebClientUrl, bcEdgeDomain } = getConfig(); + activeConfig = resolveBrandConciergeConfig(getPathDetails().lang); + createMountPoint(); injectAlloyStub(); From 393bf9edd603a1efe44f04542a89de4a0d70768b Mon Sep 17 00:00:00 2001 From: Eli Wang Date: Tue, 21 Jul 2026 14:18:26 -0700 Subject: [PATCH 2/6] fix: (AIME-194) localize Brand Concierge send-button tooltip for es BC hardcodes English "Send" in the send button's hover tooltip (a bc-tooltip element referenced via aria-describedby, not covered by the config text map). Patch it to the localized send label for non-default locales via the existing mount observer; English is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/brand-concierge/brand-concierge.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/scripts/brand-concierge/brand-concierge.js b/scripts/brand-concierge/brand-concierge.js index 31eb983bb9..23a5197f9c 100644 --- a/scripts/brand-concierge/brand-concierge.js +++ b/scripts/brand-concierge/brand-concierge.js @@ -57,6 +57,8 @@ const error = (...args) => console.error('[BC]', ...args); * so `activeConfig.ui` is always populated even before init runs. */ let activeConfig = resolveBrandConciergeConfig('en'); +/** Path language backing `activeConfig`; 'en' means no locale overlay is active. */ +let activeLang = 'en'; let cssLinkEl = null; let drawerHandle = null; @@ -94,6 +96,21 @@ function clearBrandConciergeTranscriptStorage(options = {}) { toRemove.forEach((k) => localStorage.removeItem(k)); } +/** + * BC hardcodes English copy in its hover tooltips (e.g. the send button's "Send"), which its + * config `text` map does not cover. For non-default locales, replace the send tooltip's text + * with the localized send label. No-op for English so BC's own copy is preserved. + */ +function localizeSubmitTooltip(mount) { + if (activeLang === 'en') return; + const label = activeConfig.text['input.send.aria']; + if (!label) return; + mount.querySelectorAll('.submit-button[aria-describedby]').forEach((btn) => { + const tip = document.getElementById(btn.getAttribute('aria-describedby')); + if (tip && tip.textContent !== label) tip.textContent = label; + }); +} + /** * BC renders inline SVG sparkles in several places; swap them for icons/bc-ask-sparkles.svg. */ @@ -128,6 +145,8 @@ function patchBcSparkleIcons(mount) { img.className = 'bc-sparkle-img'; svg.replaceWith(img); }); + + localizeSubmitTooltip(mount); }; run(); @@ -840,7 +859,8 @@ export function destroyBrandConcierge() { export async function initBrandConcierge() { const { bcAlloySdkUrl, bcDatastreamId, bcOrgId, bcWebClientUrl, bcEdgeDomain } = getConfig(); - activeConfig = resolveBrandConciergeConfig(getPathDetails().lang); + activeLang = getPathDetails().lang; + activeConfig = resolveBrandConciergeConfig(activeLang); createMountPoint(); injectAlloyStub(); From 01a35a970a3547ec7405aed5c17772ddc0167eb3 Mon Sep 17 00:00:00 2001 From: Eli Wang <37430922+eliwangj@users.noreply.github.com> Date: Tue, 21 Jul 2026 21:24:30 -0700 Subject: [PATCH 3/6] feat: (AIME-194) route /es/ Brand Concierge to the Spanish datastream (#2798) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On /es/ pages, configure the alloyBC instance with the Spanish concierge datastream (3098f7cc-…, same IMS org) instead of the default, so conversations reach the Spanish concierge/manifest. The locale datastream is resolved from BC_LOCALES via resolveBrandConciergeConfig(); other locales keep the default. Co-authored-by: Claude Opus 4.8 (1M context) --- scripts/brand-concierge/brand-concierge-config.js | 6 ++++++ scripts/brand-concierge/brand-concierge.js | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/scripts/brand-concierge/brand-concierge-config.js b/scripts/brand-concierge/brand-concierge-config.js index 32a9ab01d3..e311add1fe 100644 --- a/scripts/brand-concierge/brand-concierge-config.js +++ b/scripts/brand-concierge/brand-concierge-config.js @@ -161,6 +161,9 @@ const BC_UI_EN = { const BC_LOCALES = { es: { language: 'es-ES', + // Spanish Brand Concierge datastream (same IMS org as the default). Routes /es/ conversations + // to the Spanish concierge/manifest instead of the default English datastream. + datastreamId: '3098f7cc-36bb-4965-bea3-6e80fc59571e', ui: { triggerAriaLabel: 'Abrir el asistente de IA', triggerAsk: 'Hacer una pregunta', @@ -249,6 +252,9 @@ export function resolveBrandConciergeConfig(lang) { ...brandConciergeConfig.metadata, ...(overlay.language ? { language: overlay.language } : {}), }, + // Locale-specific Edge datastream, consumed in brand-concierge.js; falls back to the + // default bcDatastreamId when a locale defines none. + ...(overlay.datastreamId ? { datastreamId: overlay.datastreamId } : {}), }; } diff --git a/scripts/brand-concierge/brand-concierge.js b/scripts/brand-concierge/brand-concierge.js index 23a5197f9c..1f34f8105f 100644 --- a/scripts/brand-concierge/brand-concierge.js +++ b/scripts/brand-concierge/brand-concierge.js @@ -862,13 +862,17 @@ export async function initBrandConcierge() { activeLang = getPathDetails().lang; activeConfig = resolveBrandConciergeConfig(activeLang); + // Route to the locale's Brand Concierge datastream (e.g. the Spanish concierge on /es/), + // falling back to the default datastream for locales without an override. + const datastreamId = activeConfig.datastreamId ?? bcDatastreamId; + createMountPoint(); injectAlloyStub(); try { - log('[BC] loading Web SDK (alloyBC instance)', { bcEdgeDomain, bcDatastreamId }); + log('[BC] loading Web SDK (alloyBC instance)', { bcEdgeDomain, datastreamId, locale: activeLang }); await loadScript(bcAlloySdkUrl); - await configureWebSdk(bcDatastreamId, bcOrgId, bcEdgeDomain); + await configureWebSdk(datastreamId, bcOrgId, bcEdgeDomain); log('[BC] Web SDK configured'); log('[BC] loading Web Client', bcWebClientUrl); From 824b9325e7af7b4d510481cc8554da30106614b7 Mon Sep 17 00:00:00 2001 From: Eli Wang Date: Wed, 22 Jul 2026 11:38:05 -0700 Subject: [PATCH 4/6] refactor: (AIME-194) move Brand Concierge i18n to per-locale JSON files Per reviewer feedback on #2791, extract localized strings into scripts/brand-concierge/localization/{en,es}.json (one file per locale) and load + deep-merge them at init via loadBrandConciergeConfig(lang), replacing the in-code BC_UI_EN / BC_LOCALES overlays. Deep-merge gives per-field English fallback (closes the earlier shallow-merge note on ui.disclaimer). Move the per-locale Edge datastream to a separate BC_DATASTREAMS lookup (getBrandConciergeDatastreamId), keeping routing config out of activeConfig so it can't leak into the styling payload forwarded to the BC web client (addresses the bot's getBootstrapOptions leak finding). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../brand-concierge/brand-concierge-config.js | 232 ++++++------------ scripts/brand-concierge/brand-concierge.js | 25 +- scripts/brand-concierge/localization/en.json | 59 +++++ scripts/brand-concierge/localization/es.json | 64 +++++ 4 files changed, 211 insertions(+), 169 deletions(-) create mode 100644 scripts/brand-concierge/localization/en.json create mode 100644 scripts/brand-concierge/localization/es.json diff --git a/scripts/brand-concierge/brand-concierge-config.js b/scripts/brand-concierge/brand-concierge-config.js index e311add1fe..a083b57b9c 100644 --- a/scripts/brand-concierge/brand-concierge-config.js +++ b/scripts/brand-concierge/brand-concierge-config.js @@ -22,6 +22,12 @@ function getProductNamespace() { return key ? `exl-bc-${key}` : 'exl-bc'; } +/** + * Non-localizable Brand Concierge config. Localized strings (`ui`/`text`/`arrays`) and + * `metadata.language` come per-locale from ./localization/.json and are merged in by + * loadBrandConciergeConfig(); this base holds only values that can't live in a JSON sheet + * (runtime-resolved CSS theme, session/behavior flags, namespace). + */ const brandConciergeConfig = { // destructured out in brand-concierge.js before forwarding to bootstrap(). stickySession: true, @@ -42,49 +48,6 @@ const brandConciergeConfig = { namespace: getProductNamespace(), }, - text: { - 'welcome.heading': 'Not sure where to start?
Ask me anything about Adobe products.', - 'welcome.subheading': 'Type your question or pick a suggestion below.', - 'input.placeholder': 'Ask a question…', - 'input.messageInput.aria': 'Message input', - 'input.send.aria': 'Send message', - 'input.mic.aria': 'Voice input', - 'card.aria.select': 'Select example message', - 'carousel.prev.aria': 'Previous cards', - 'carousel.next.aria': 'Next cards', - 'scroll.bottom.aria': 'Scroll to bottom', - 'error.network': "I'm sorry, I'm having trouble connecting right now. Please try again in a moment.", - 'error.general': "I'm sorry, something went wrong. Please try again in a moment.", - 'loading.message': "Generating from Adobe's trusted resources", - 'feedback.dialog.title.positive': 'Your feedback is appreciated', - 'feedback.dialog.title.negative': 'Your feedback is appreciated', - 'feedback.dialog.question.positive': 'What went well? Select all that apply.', - 'feedback.dialog.question.negative': 'What went wrong? Select all that apply.', - 'feedback.dialog.notes': 'Notes', - 'feedback.dialog.submit': 'Submit', - 'feedback.dialog.cancel': 'Cancel', - 'feedback.dialog.notes.placeholder': 'Additional notes (optional)', - 'feedback.toast.success': 'Thank you for the feedback.', - 'feedback.thumbsUp.aria': 'Thumbs up', - 'feedback.thumbsDown.aria': 'Thumbs down', - }, - - arrays: { - 'welcome.examples': [ - { text: 'Where can I go to learn about AI on Experience League?' }, - { text: 'Getting started with Experience Manager' }, - { text: 'Set up an Adobe Analytics report suite' }, - { text: 'Explain Adobe Target A/B testing' }, - ], - 'feedback.positive.options': [ - 'Helpful and relevant', - 'Clear and easy to understand', - 'Friendly and conversational tone', - 'Other', - ], - 'feedback.negative.options': ['Not helpful or relevant', 'Confusing or unclear', 'Too formal or robotic', 'Other'], - }, - // CSS variable overrides forwarded to BC. Only set values that diverge from // BC defaults or need tuning for the compact dialog context. // spacing and sizing for pill/suggestion buttons are intentionally set only @@ -130,132 +93,85 @@ const brandConciergeConfig = { }, }; -/** - * English chrome strings (trigger button, drawer, clear control, legal disclaimer). - * These render from ExL's own code in brand-concierge.js (not forwarded to the BC web - * client), so they live here alongside the client-facing `text`/`arrays` above and are - * threaded through as `config.ui`. 'BETA' is intentionally left untranslated (brand term). - */ -const BC_UI_EN = { - triggerAriaLabel: 'Open AI assistant', - triggerAsk: 'Ask a question', - drawerAriaLabel: 'AI assistant', - drawerTitle: 'Ask', - clearLabel: 'Clear', - clearAriaLabel: 'Clear conversation', - disclaimer: { - prefix: "Use of this beta AI chatbot is subject to Adobe's ", - privacyLabel: 'Privacy Policy', - middle: - ". Don't share sensitive data. AI responses are not your Content, may be inaccurate, and any offers provided are non-binding. ", - termsLabel: 'Generative AI Terms', - suffix: '.', - }, -}; +const LOCALES_BASE_PATH = `${window.hlx.codeBasePath}/scripts/brand-concierge/localization`; -/** - * Per-locale overlays merged onto the English base by resolveBrandConciergeConfig(). - * Keyed by getPathDetails().lang (e.g. 'es' for /es/... paths). Translations are - * review-pending — legal disclaimer copy in particular needs sign-off before launch. - */ -const BC_LOCALES = { - es: { - language: 'es-ES', - // Spanish Brand Concierge datastream (same IMS org as the default). Routes /es/ conversations - // to the Spanish concierge/manifest instead of the default English datastream. - datastreamId: '3098f7cc-36bb-4965-bea3-6e80fc59571e', - ui: { - triggerAriaLabel: 'Abrir el asistente de IA', - triggerAsk: 'Hacer una pregunta', - drawerAriaLabel: 'Asistente de IA', - drawerTitle: 'Preguntar', - clearLabel: 'Borrar', - clearAriaLabel: 'Borrar conversación', - disclaimer: { - prefix: 'El uso de este chatbot de IA en versión beta está sujeto a la ', - privacyLabel: 'Política de privacidad', - middle: - ' de Adobe. No comparta datos confidenciales. Las respuestas de la IA no son su Contenido, pueden ser inexactas y cualquier oferta proporcionada no es vinculante. ', - termsLabel: 'Términos de IA generativa', - suffix: '.', - }, - }, - text: { - 'welcome.heading': '¿No sabe por dónde empezar?
Pregúnteme lo que quiera sobre los productos de Adobe.', - 'welcome.subheading': 'Escriba su pregunta o elija una sugerencia a continuación.', - 'input.placeholder': 'Haga una pregunta…', - 'input.messageInput.aria': 'Campo de mensaje', - 'input.send.aria': 'Enviar mensaje', - 'input.mic.aria': 'Entrada de voz', - 'card.aria.select': 'Seleccionar mensaje de ejemplo', - 'carousel.prev.aria': 'Tarjetas anteriores', - 'carousel.next.aria': 'Tarjetas siguientes', - 'scroll.bottom.aria': 'Desplazarse hasta abajo', - 'error.network': - 'Lo sentimos, en este momento tenemos problemas de conexión. Vuelva a intentarlo en unos instantes.', - 'error.general': 'Lo sentimos, se ha producido un error. Vuelva a intentarlo en unos instantes.', - 'loading.message': 'Generando a partir de los recursos fiables de Adobe', - 'feedback.dialog.title.positive': 'Agradecemos sus comentarios', - 'feedback.dialog.title.negative': 'Agradecemos sus comentarios', - 'feedback.dialog.question.positive': '¿Qué salió bien? Seleccione todas las opciones aplicables.', - 'feedback.dialog.question.negative': '¿Qué salió mal? Seleccione todas las opciones aplicables.', - 'feedback.dialog.notes': 'Notas', - 'feedback.dialog.submit': 'Enviar', - 'feedback.dialog.cancel': 'Cancelar', - 'feedback.dialog.notes.placeholder': 'Notas adicionales (opcional)', - 'feedback.toast.success': 'Gracias por sus comentarios.', - 'feedback.thumbsUp.aria': 'Me gusta', - 'feedback.thumbsDown.aria': 'No me gusta', - }, - arrays: { - 'welcome.examples': [ - { text: '¿Dónde puedo aprender sobre la IA en Experience League?' }, - { text: 'Primeros pasos con Experience Manager' }, - { text: 'Configurar un conjunto de informes de Adobe Analytics' }, - { text: 'Explicar las pruebas A/B de Adobe Target' }, - ], - 'feedback.positive.options': [ - 'Útil y relevante', - 'Claro y fácil de entender', - 'Tono cercano y conversacional', - 'Otro', - ], - 'feedback.negative.options': [ - 'Poco útil o irrelevante', - 'Confuso o poco claro', - 'Demasiado formal o robótico', - 'Otro', - ], - }, - }, -}; +/** lang -> Promise resolving that locale's { language, ui, text, arrays } sheet (deduped). */ +const localeSheetCache = {}; + +function fetchLocaleSheet(lang) { + if (!localeSheetCache[lang]) { + localeSheetCache[lang] = fetch(`${LOCALES_BASE_PATH}/${lang}.json`).then((res) => { + if (!res.ok) throw new Error(`Brand Concierge locale '${lang}' -> ${res.status}`); + return res.json(); + }); + } + return localeSheetCache[lang]; +} + +/** Recursive merge: `over` wins; nested plain objects merge per-field, arrays/scalars replace. */ +function deepMerge(base, over) { + if (!over) return base; + const out = { ...base }; + Object.entries(over).forEach(([key, value]) => { + const baseVal = base?.[key]; + const bothPlainObjects = + value && + typeof value === 'object' && + !Array.isArray(value) && + baseVal && + typeof baseVal === 'object' && + !Array.isArray(baseVal); + out[key] = bothPlainObjects ? deepMerge(baseVal, value) : value; + }); + return out; +} /** - * Resolves the BC config for a given path language, layering any locale overlay onto the - * English base. English (and any unknown lang) returns the base config plus English `ui`. - * Pure function — the caller supplies `lang` (from getPathDetails().lang) so this module - * stays free of a scripts.js import and its cyclic dependency. - * @param {string} [lang] - Path language, e.g. 'en', 'es'. - * @returns {typeof brandConciergeConfig & { ui: typeof BC_UI_EN }} + * Loads the BC config for a path language, layering the locale's localization sheet over the + * English base. English is always fetched as the fallback base, so a partial locale sheet + * degrades per-field rather than dropping keys. Falls back to English for unknown/failed locales. + * @param {string} [lang] - Path language from getPathDetails().lang (e.g. 'en', 'es'). + * @returns {Promise} */ -export function resolveBrandConciergeConfig(lang) { - const overlay = BC_LOCALES[(lang || 'en').toLowerCase()]; - if (!overlay) { - return { ...brandConciergeConfig, ui: BC_UI_EN }; +export async function loadBrandConciergeConfig(lang) { + const key = (lang || 'en').toLowerCase(); + const en = await fetchLocaleSheet('en'); + let locale = en; + if (key !== 'en') { + try { + locale = await fetchLocaleSheet(key); + } catch { + locale = en; + } } return { ...brandConciergeConfig, - ui: { ...BC_UI_EN, ...overlay.ui }, - text: { ...brandConciergeConfig.text, ...overlay.text }, - arrays: { ...brandConciergeConfig.arrays, ...overlay.arrays }, + ui: deepMerge(en.ui, locale.ui), + text: { ...en.text, ...locale.text }, + arrays: { ...en.arrays, ...locale.arrays }, metadata: { ...brandConciergeConfig.metadata, - ...(overlay.language ? { language: overlay.language } : {}), + language: locale.language || brandConciergeConfig.metadata.language, }, - // Locale-specific Edge datastream, consumed in brand-concierge.js; falls back to the - // default bcDatastreamId when a locale defines none. - ...(overlay.datastreamId ? { datastreamId: overlay.datastreamId } : {}), }; } +/** + * Per-locale Brand Concierge Edge datastream overrides. This is routing config, not translation, + * so it is kept out of the localization sheets and out of the config forwarded to the BC web + * client. Same IMS org as the default datastream. + */ +const BC_DATASTREAMS = { + es: '3098f7cc-36bb-4965-bea3-6e80fc59571e', +}; + +/** + * @param {string} [lang] - Path language, e.g. 'en', 'es'. + * @param {string} fallback - Default datastream id used when the locale has no override. + * @returns {string} + */ +export function getBrandConciergeDatastreamId(lang, fallback) { + return BC_DATASTREAMS[(lang || 'en').toLowerCase()] ?? fallback; +} + export default brandConciergeConfig; diff --git a/scripts/brand-concierge/brand-concierge.js b/scripts/brand-concierge/brand-concierge.js index 1f34f8105f..b766849679 100644 --- a/scripts/brand-concierge/brand-concierge.js +++ b/scripts/brand-concierge/brand-concierge.js @@ -2,7 +2,7 @@ import { getConfig, getPathDetails } from '../scripts.js'; import { loadScript, decorateIcon } from '../lib-franklin.js'; import { openDrawer } from '../dialog/dialog.js'; -import { resolveBrandConciergeConfig } from './brand-concierge-config.js'; +import { loadBrandConciergeConfig, getBrandConciergeDatastreamId } from './brand-concierge-config.js'; // Separate alloy instance avoids conflicting with the Launch-owned window.alloy. const ALLOY_INSTANCE_NAME = 'alloyBC'; @@ -52,11 +52,11 @@ const warn = (...args) => console.warn('[BC]', ...args); const error = (...args) => console.error('[BC]', ...args); /** - * Locale-resolved config (English base + any locale overlay). Re-assigned in - * initBrandConcierge() from the page's path language; defaults to the English resolution - * so `activeConfig.ui` is always populated even before init runs. + * Locale-resolved config (English base + any locale overlay), loaded async from per-locale + * JSON in initBrandConcierge(). Null until loaded; consumers run only after the load resolves, + * and the observer callbacks guard against a null value defensively. */ -let activeConfig = resolveBrandConciergeConfig('en'); +let activeConfig = null; /** Path language backing `activeConfig`; 'en' means no locale overlay is active. */ let activeLang = 'en'; @@ -103,7 +103,7 @@ function clearBrandConciergeTranscriptStorage(options = {}) { */ function localizeSubmitTooltip(mount) { if (activeLang === 'en') return; - const label = activeConfig.text['input.send.aria']; + const label = activeConfig?.text?.['input.send.aria']; if (!label) return; mount.querySelectorAll('.submit-button[aria-describedby]').forEach((btn) => { const tip = document.getElementById(btn.getAttribute('aria-describedby')); @@ -156,7 +156,8 @@ function patchBcSparkleIcons(mount) { } function buildPanelDisclaimer() { - const copy = activeConfig.ui.disclaimer; + const copy = activeConfig?.ui?.disclaimer; + if (!copy) return null; const disclaimer = document.createElement('p'); disclaimer.id = PANEL_DISCLAIMER_ID; @@ -192,7 +193,8 @@ function installPanelDisclaimer(mount) { if (!mount) return; const inputSection = mount.querySelector('.input-section'); if (!inputSection || inputSection.querySelector(`#${PANEL_DISCLAIMER_ID}`)) return; - inputSection.append(buildPanelDisclaimer()); + const disclaimer = buildPanelDisclaimer(); + if (disclaimer) inputSection.append(disclaimer); } function watchPanelDisclaimer(mount) { @@ -860,11 +862,12 @@ export async function initBrandConcierge() { const { bcAlloySdkUrl, bcDatastreamId, bcOrgId, bcWebClientUrl, bcEdgeDomain } = getConfig(); activeLang = getPathDetails().lang; - activeConfig = resolveBrandConciergeConfig(activeLang); + activeConfig = await loadBrandConciergeConfig(activeLang); // Route to the locale's Brand Concierge datastream (e.g. the Spanish concierge on /es/), - // falling back to the default datastream for locales without an override. - const datastreamId = activeConfig.datastreamId ?? bcDatastreamId; + // falling back to the default datastream for locales without an override. Kept separate from + // activeConfig so this routing id never leaks into the styling payload sent to the BC client. + const datastreamId = getBrandConciergeDatastreamId(activeLang, bcDatastreamId); createMountPoint(); injectAlloyStub(); diff --git a/scripts/brand-concierge/localization/en.json b/scripts/brand-concierge/localization/en.json new file mode 100644 index 0000000000..2b271c379c --- /dev/null +++ b/scripts/brand-concierge/localization/en.json @@ -0,0 +1,59 @@ +{ + "language": "en-US", + "ui": { + "triggerAriaLabel": "Open AI assistant", + "triggerAsk": "Ask a question", + "drawerAriaLabel": "AI assistant", + "drawerTitle": "Ask", + "clearLabel": "Clear", + "clearAriaLabel": "Clear conversation", + "disclaimer": { + "prefix": "Use of this beta AI chatbot is subject to Adobe's ", + "privacyLabel": "Privacy Policy", + "middle": ". Don't share sensitive data. AI responses are not your Content, may be inaccurate, and any offers provided are non-binding. ", + "termsLabel": "Generative AI Terms", + "suffix": "." + } + }, + "text": { + "welcome.heading": "Not sure where to start?
Ask me anything about Adobe products.", + "welcome.subheading": "Type your question or pick a suggestion below.", + "input.placeholder": "Ask a question…", + "input.messageInput.aria": "Message input", + "input.send.aria": "Send message", + "input.mic.aria": "Voice input", + "card.aria.select": "Select example message", + "carousel.prev.aria": "Previous cards", + "carousel.next.aria": "Next cards", + "scroll.bottom.aria": "Scroll to bottom", + "error.network": "I'm sorry, I'm having trouble connecting right now. Please try again in a moment.", + "error.general": "I'm sorry, something went wrong. Please try again in a moment.", + "loading.message": "Generating from Adobe's trusted resources", + "feedback.dialog.title.positive": "Your feedback is appreciated", + "feedback.dialog.title.negative": "Your feedback is appreciated", + "feedback.dialog.question.positive": "What went well? Select all that apply.", + "feedback.dialog.question.negative": "What went wrong? Select all that apply.", + "feedback.dialog.notes": "Notes", + "feedback.dialog.submit": "Submit", + "feedback.dialog.cancel": "Cancel", + "feedback.dialog.notes.placeholder": "Additional notes (optional)", + "feedback.toast.success": "Thank you for the feedback.", + "feedback.thumbsUp.aria": "Thumbs up", + "feedback.thumbsDown.aria": "Thumbs down" + }, + "arrays": { + "welcome.examples": [ + { "text": "Where can I go to learn about AI on Experience League?" }, + { "text": "Getting started with Experience Manager" }, + { "text": "Set up an Adobe Analytics report suite" }, + { "text": "Explain Adobe Target A/B testing" } + ], + "feedback.positive.options": [ + "Helpful and relevant", + "Clear and easy to understand", + "Friendly and conversational tone", + "Other" + ], + "feedback.negative.options": ["Not helpful or relevant", "Confusing or unclear", "Too formal or robotic", "Other"] + } +} diff --git a/scripts/brand-concierge/localization/es.json b/scripts/brand-concierge/localization/es.json new file mode 100644 index 0000000000..d614864638 --- /dev/null +++ b/scripts/brand-concierge/localization/es.json @@ -0,0 +1,64 @@ +{ + "language": "es-ES", + "ui": { + "triggerAriaLabel": "Abrir el asistente de IA", + "triggerAsk": "Hacer una pregunta", + "drawerAriaLabel": "Asistente de IA", + "drawerTitle": "Preguntar", + "clearLabel": "Borrar", + "clearAriaLabel": "Borrar conversación", + "disclaimer": { + "prefix": "El uso de este chatbot de IA en versión beta está sujeto a la ", + "privacyLabel": "Política de privacidad", + "middle": " de Adobe. No comparta datos confidenciales. Las respuestas de la IA no son su Contenido, pueden ser inexactas y cualquier oferta proporcionada no es vinculante. ", + "termsLabel": "Términos de IA generativa", + "suffix": "." + } + }, + "text": { + "welcome.heading": "¿No sabe por dónde empezar?
Pregúnteme lo que quiera sobre los productos de Adobe.", + "welcome.subheading": "Escriba su pregunta o elija una sugerencia a continuación.", + "input.placeholder": "Haga una pregunta…", + "input.messageInput.aria": "Campo de mensaje", + "input.send.aria": "Enviar mensaje", + "input.mic.aria": "Entrada de voz", + "card.aria.select": "Seleccionar mensaje de ejemplo", + "carousel.prev.aria": "Tarjetas anteriores", + "carousel.next.aria": "Tarjetas siguientes", + "scroll.bottom.aria": "Desplazarse hasta abajo", + "error.network": "Lo sentimos, en este momento tenemos problemas de conexión. Vuelva a intentarlo en unos instantes.", + "error.general": "Lo sentimos, se ha producido un error. Vuelva a intentarlo en unos instantes.", + "loading.message": "Generando a partir de los recursos fiables de Adobe", + "feedback.dialog.title.positive": "Agradecemos sus comentarios", + "feedback.dialog.title.negative": "Agradecemos sus comentarios", + "feedback.dialog.question.positive": "¿Qué salió bien? Seleccione todas las opciones aplicables.", + "feedback.dialog.question.negative": "¿Qué salió mal? Seleccione todas las opciones aplicables.", + "feedback.dialog.notes": "Notas", + "feedback.dialog.submit": "Enviar", + "feedback.dialog.cancel": "Cancelar", + "feedback.dialog.notes.placeholder": "Notas adicionales (opcional)", + "feedback.toast.success": "Gracias por sus comentarios.", + "feedback.thumbsUp.aria": "Me gusta", + "feedback.thumbsDown.aria": "No me gusta" + }, + "arrays": { + "welcome.examples": [ + { "text": "¿Dónde puedo aprender sobre la IA en Experience League?" }, + { "text": "Primeros pasos con Experience Manager" }, + { "text": "Configurar un conjunto de informes de Adobe Analytics" }, + { "text": "Explicar las pruebas A/B de Adobe Target" } + ], + "feedback.positive.options": [ + "Útil y relevante", + "Claro y fácil de entender", + "Tono cercano y conversacional", + "Otro" + ], + "feedback.negative.options": [ + "Poco útil o irrelevante", + "Confuso o poco claro", + "Demasiado formal o robótico", + "Otro" + ] + } +} From bea719083e9834c2c5721332d8424d23467c8690 Mon Sep 17 00:00:00 2001 From: Eli Wang Date: Wed, 22 Jul 2026 11:54:51 -0700 Subject: [PATCH 5/6] fix: (AIME-194) guard Brand Concierge English localization fetch Wrap the English base sheet fetch (not just the locale fetch) so a failed en.json returns null and BC skips mounting gracefully instead of throwing an unhandled rejection that breaks the widget on every locale. Also stop caching failed locale fetches so a transient blip can retry. Mirrors the fetchLanguagePlaceholders fallback pattern in scripts.js. Addresses the review comment on brand-concierge-config.js. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../brand-concierge/brand-concierge-config.js | 27 ++++++++++++++----- scripts/brand-concierge/brand-concierge.js | 2 ++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/scripts/brand-concierge/brand-concierge-config.js b/scripts/brand-concierge/brand-concierge-config.js index a083b57b9c..9eb176529e 100644 --- a/scripts/brand-concierge/brand-concierge-config.js +++ b/scripts/brand-concierge/brand-concierge-config.js @@ -100,10 +100,16 @@ const localeSheetCache = {}; function fetchLocaleSheet(lang) { if (!localeSheetCache[lang]) { - localeSheetCache[lang] = fetch(`${LOCALES_BASE_PATH}/${lang}.json`).then((res) => { - if (!res.ok) throw new Error(`Brand Concierge locale '${lang}' -> ${res.status}`); - return res.json(); - }); + localeSheetCache[lang] = fetch(`${LOCALES_BASE_PATH}/${lang}.json`) + .then((res) => { + if (!res.ok) throw new Error(`Brand Concierge locale '${lang}' -> ${res.status}`); + return res.json(); + }) + .catch((err) => { + // Don't cache failures — a transient network blip shouldn't permanently break this locale. + delete localeSheetCache[lang]; + throw err; + }); } return localeSheetCache[lang]; } @@ -130,12 +136,21 @@ function deepMerge(base, over) { * Loads the BC config for a path language, layering the locale's localization sheet over the * English base. English is always fetched as the fallback base, so a partial locale sheet * degrades per-field rather than dropping keys. Falls back to English for unknown/failed locales. + * Returns `null` if the English base sheet itself can't be loaded, so the caller can skip mounting + * gracefully rather than render a widget with no copy (single-point-of-failure guard). * @param {string} [lang] - Path language from getPathDetails().lang (e.g. 'en', 'es'). - * @returns {Promise} + * @returns {Promise<(typeof brandConciergeConfig & { ui: object }) | null>} */ export async function loadBrandConciergeConfig(lang) { const key = (lang || 'en').toLowerCase(); - const en = await fetchLocaleSheet('en'); + let en; + try { + en = await fetchLocaleSheet('en'); + } catch (err) { + // eslint-disable-next-line no-console + console.warn('[BC] English localization sheet failed to load; skipping mount', err?.message || err); + return null; + } let locale = en; if (key !== 'en') { try { diff --git a/scripts/brand-concierge/brand-concierge.js b/scripts/brand-concierge/brand-concierge.js index b766849679..e6252fe513 100644 --- a/scripts/brand-concierge/brand-concierge.js +++ b/scripts/brand-concierge/brand-concierge.js @@ -863,6 +863,8 @@ export async function initBrandConcierge() { activeLang = getPathDetails().lang; activeConfig = await loadBrandConciergeConfig(activeLang); + // If even the English base sheet can't load, skip mounting rather than render an empty widget. + if (!activeConfig) return; // Route to the locale's Brand Concierge datastream (e.g. the Spanish concierge on /es/), // falling back to the default datastream for locales without an override. Kept separate from From c2c0bf3f23b8ca6961b3ddb10515f6c5df7041f5 Mon Sep 17 00:00:00 2001 From: Eli Wang <37430922+eliwangj@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:35:42 -0700 Subject: [PATCH 6/6] feat: (AIME-194) point /es/ Brand Concierge at the new sandbox datastream (#2831) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swaps the Spanish datastream override to 152f88b1-… so /es/ routes to the new concierge/sandbox. Same IMS org, so only the datastream id changes — orgId, edge domain and web client url are unchanged. Co-authored-by: Claude Opus 5 --- scripts/brand-concierge/brand-concierge-config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/brand-concierge/brand-concierge-config.js b/scripts/brand-concierge/brand-concierge-config.js index 9eb176529e..2a7b2764b1 100644 --- a/scripts/brand-concierge/brand-concierge-config.js +++ b/scripts/brand-concierge/brand-concierge-config.js @@ -177,7 +177,7 @@ export async function loadBrandConciergeConfig(lang) { * client. Same IMS org as the default datastream. */ const BC_DATASTREAMS = { - es: '3098f7cc-36bb-4965-bea3-6e80fc59571e', + es: '152f88b1-ef07-4afe-8a23-6c0e21c6f017', }; /**