Skip to content

feat: (AIME-194) Spanish localization for Brand Concierge widget UI - #2791

Open
eliwangj wants to merge 8 commits into
mainfrom
exlm-bc-spanish-ui
Open

feat: (AIME-194) Spanish localization for Brand Concierge widget UI#2791
eliwangj wants to merge 8 commits into
mainfrom
exlm-bc-spanish-ui

Conversation

@eliwangj

@eliwangj eliwangj commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Jira ID: AIME-194

Test URLs:

Summary

Adds a Spanish (es) locale to the Brand Concierge widget UI. English and all other locales are unchanged.

  • Locale is resolved from the URL path (getPathDetails().lang), matching repo convention.
  • Localized strings live in per-locale JSON files (scripts/brand-concierge/localization/en.json, es.json); loadBrandConciergeConfig(lang) fetches and deep-merges the locale over the English base at init (per-field English fallback).
  • Covers every visible string: trigger button, drawer, clear control, disclaimer, welcome heading/examples, feedback options.
  • /es/ conversations route to the Spanish concierge datastream via a separate getBrandConciergeDatastreamId() lookup, kept out of the payload forwarded to the BC web client. The ES datastream ID was subsequently repointed at a different concierge/sandbox (152f88b1-ef07-4afe-8a23-6c0e21c6f017) by stacked PR feat: (AIME-194) point /es/ Brand Concierge at the new sandbox datastream #2831, so that is the value on this branch today — same IMS org, no other config changed.

Files: scripts/brand-concierge/localization/{en,es}.json, scripts/brand-concierge/brand-concierge-config.js, scripts/brand-concierge/brand-concierge.js

Screenshots

Screenshots captured by Claude Code during local verification.

Spanish — trigger button

Spanish trigger button

Spanish — drawer

Spanish drawer

English — unchanged (regression check)

English drawer

Known caveats

  • Some responses show a "Sources" label that stays in English — it is rendered by the Brand Concierge agent, not the site, so it can't be localized from here.
  • The voice-input button's hover tooltip still shows English "Dictate" — same BC-hardcoded-tooltip class as the now-localized send tooltip; can be localized on request.

AI Review Notes

  • Spanish copy (including the legal disclaimer) is placeholder / review-pending wording — do not flag for phrasing.
  • Localized strings are intentionally hardcoded in JS (matching how BC already stores its English strings), pending a product decision on placeholders.json — do not flag as "should use placeholders.json".
  • Locale is intentionally resolved from getPathDetails().lang (path-based), matching the existing convention.

🤖 Generated with Claude Code

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) <noreply@anthropic.com>
@eliwangj
eliwangj requested a review from ajaydwarkani July 20, 2026 20:57
@aem-code-sync

aem-code-sync Bot commented Jul 20, 2026

Copy link
Copy Markdown

Hello, I'm the AEM Code Sync Bot and I will run some actions to deploy your branch and validate page speed.
In case there are problems, just click a checkbox below to rerun the respective action.

  • Re-run all PSI checks
  • Re-run failed PSI checks
  • Re-sync branch
Commits

@eliwangj

eliwangj commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

@ajaydwarkani could you help verify the Spanish UI and confirm it works? for some reason it kept saying i'm unauthorized when i was doing local testing manually earlier today. thanks!

Comment thread scripts/brand-concierge/brand-concierge-config.js Outdated
@eliwangj

Copy link
Copy Markdown
Collaborator Author

@ajaydwarkani could you help verify the Spanish UI and confirm it works? for some reason it kept saying i'm unauthorized when i was doing local testing manually earlier today. thanks!

me and @ajaydwarkani did some debugging and seems like neither of us could solve the login/auth issue, which is blocking local testing. @ajaydwarkani already reached out to team for fixes.

@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Code Review

Overview

Clean, well-scoped change: adds a Spanish (es) locale to the Brand Concierge widget by introducing resolveBrandConciergeConfig(lang), which layers a Spanish BC_LOCALES.es overlay onto the existing English base config. brand-concierge.js now resolves activeConfig from getPathDetails().lang in initBrandConcierge() and reads localized strings (ui.*) instead of hardcoded English literals in buildPanelDisclaimer() and createMountPoint(). English behavior is unchanged (verified against the diff — no changes to existing English string values, only extraction into BC_UI_EN).

Correctness

  • Translation coverage is complete and 1:1 with the English base: all 24 text keys, all 3 arrays keys, and all 6 ui keys (+ 5 disclaimer sub-fields) have Spanish counterparts — matches the PR description's claim of covering every visible string.
  • getPathDetails().lang always resolves to a non-empty string (defaults to 'en' per scripts.js), so resolveBrandConciergeConfig's (lang || 'en').toLowerCase() fallback is a defensive no-op rather than dead code — fine as-is.
  • getBootstrapOptions() correctly strips ui before forwarding config to the third-party BC web client, so ExL-only chrome strings won't leak into BC's own styling payload.
  • createMountPoint() short-circuits and returns the existing dialog if #bc-dialog already exists, but initBrandConcierge() is only invoked once per page load (from delayed.js), so the mount-point/locale ordering isn't actually at risk here — just noting it as something to watch if this widget is ever initialized more than once per page without an intervening destroyBrandConcierge().
  • No XSS surface introduced — all locale strings are set via textContent/createTextNode, never innerHTML.

Minor / non-blocking

  • overlay.language: 'es-ES' hardcodes the region-specific BCP-47 tag forwarded to the BC client as metadata.language. Worth a quick sanity check with product/localization on whether Experience League's Spanish content is meant to be es-ES vs. a more generic es (or es-419) — not a code defect, just a value worth confirming since it may influence the AI backend's response locale/tone.
  • No automated tests are added, but this matches the existing pattern for brand-concierge.js/brand-concierge-config.js (no test file exists for this module today), so this isn't a new gap introduced by the PR.

Already tracked

  • The shallow-merge behavior of ui: { ...BC_UI_EN, ...overlay.ui } (a partial ui.disclaimer override in a future locale would silently drop the other English disclaimer sub-fields) is already flagged in an existing inline review comment on brand-concierge-config.js:245 — not re-flagging here, and it's a non-issue for the current es overlay since all 5 disclaimer sub-fields are supplied.

Overall: solid, low-risk, well-documented implementation. No new blocking issues found.

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) <noreply@anthropic.com>
@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Follow-up review (latest commit)

Reviewed the latest commit (393bf9e, send-button tooltip localization) against the prior full review, which predates it.

  • localizeSubmitTooltip() runs inside the existing MutationObserver callback in patchBcSparkleIcons. It's self-limiting: it only writes tip.textContent when the value differs from the target, so the observer-triggered re-entry from that write is a no-op on the second pass — no infinite loop.
  • activeLang/activeConfig are assigned in initBrandConcierge() before patchBcSparkleIcons() is ever invoked (both on initial load and from the Clear-conversation flow), so there's no stale-state ordering hazard.
  • ui: _ui destructuring in getBootstrapOptions() (used to strip ui before forwarding to the BC client) is fine under this repo's airbnb-base ESLint config (ignoreRestSiblings).

No new issues found. The previously flagged shallow-merge note on resolveBrandConciergeConfig's ui merge (config.js:245) still stands as a non-blocking future-proofing suggestion, not re-flagging it again here.

…#2798)

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) <noreply@anthropic.com>
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new datastreamId field added to resolveBrandConciergeConfig's return value (config.js, the overlay.datastreamId spread) isn't stripped here alongside stickySession/ui. On /es/ pages, activeConfig.datastreamId ('3098f7cc-...') will fall into ...stylingConfigurations and get forwarded to the third-party BC web client via getBootstrapOptions()window.adobe.concierge.bootstrap() — the same call this comment says should only carry "BC's own styling/text/arrays".

datastreamId is only meaningful to the Alloy configureWebSdk() call (line ~867), not to the BC web client's bootstrap options, so this looks like an unintentional leak of an ExL/Edge-only field into the third-party payload. Consider destructuring it out here too, e.g.:

Suggested change
const { stickySession = false, ui: _ui, ...stylingConfigurations } = activeConfig;
const { stickySession = false, ui: _ui, datastreamId: _datastreamId, ...stylingConfigurations } = activeConfig;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the JSON refactor (824b932): datastreamId no longer lives on activeConfig — it moved to a separate BC_DATASTREAMS lookup (getBrandConciergeDatastreamId) resolved only for configureWebSdk, so it can't reach getBootstrapOptions/the BC client payload. Safe to resolve.

* 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 = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of having this in the code, can we move this under a language directory with it's own locale JSON file.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of having this in the code, can we move this under a language directory with it's own locale JSON file.

(or) we can keep it under brandConciergeConfig directly and considered an object name as "ui". Bocz, we already have objects like text, array and ideally this was considered as en.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@noormd08 , as per I18n best practices, we should be maintaining different files for different locales as it's easy for maintainability and makes adding a new language easy.

Here is my suggestion for folder structure

  • localization
    • en.json
    • es.json

and in future if we extend new locale for example French or Chinese, we can simply add new file with respective translation

  • localization
    • fr.json
    • zh-hant.json

Thoughts?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — adopted the file-per-locale structure. Strings now live in scripts/brand-concierge/localization/en.json and es.json, loaded + deep-merged at init by loadBrandConciergeConfig(lang); adding a new language is just dropping in <lang>.json. I co-located the folder under the BC module since these strings are BC-specific — happy to lift it to a shared top-level localization/ if you'd prefer. The deep-merge also gives per-field English fallback, so a partial future locale won't drop keys (closes the earlier shallow-merge note). Thanks for the steer!

* 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 = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of having this in the code, can we move this under a language directory with it's own locale JSON file.

(or) we can keep it under brandConciergeConfig directly and considered an object name as "ui". Bocz, we already have objects like text, array and ideally this was considered as en.

Comment thread scripts/brand-concierge/brand-concierge-config.js Outdated
Comment thread scripts/brand-concierge/brand-concierge-config.js Outdated
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) <noreply@anthropic.com>
*/
export async function loadBrandConciergeConfig(lang) {
const key = (lang || 'en').toLowerCase();
const en = await fetchLocaleSheet('en');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadBrandConciergeConfig only has a try/catch around fetching the locale sheet (fetchLocaleSheet(key), line 142) — the English base fetch on line 138 is unguarded. Previously all of this UI copy was hardcoded in the JS bundle, so BC always had a config to render. Now, if en.json 404s or the fetch otherwise fails (transient network blip, CDN hiccup, path typo after a deploy), loadBrandConciergeConfig rejects, and since activeConfig = await loadBrandConciergeConfig(activeLang); in brand-concierge.js (initBrandConcierge) sits before the try/catch block that wraps the rest of init, the whole widget silently fails to mount — on every locale, not just es — with only a console.warn from the top-level loadBrandConcierge().catch(...) in delayed.js.

This repo already has an established pattern for exactly this case — fetchLanguagePlaceholders in scripts/scripts.js wraps its lang-specific fetch and its English fallback fetch in try/catch, defaulting to {} rather than letting the whole feature disappear. Worth mirroring that here, e.g. wrap the en fetch in a try/catch too and fall back to an inline minimal ui/text/arrays default (or at least skip mounting gracefully) so a single failed network request for a static JSON file can't take down the entire Brand Concierge widget.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bea7190. The English base fetch is now wrapped too — loadBrandConciergeConfig returns null when en.json can't load, and initBrandConcierge skips mounting on null instead of throwing (so a failed base sheet no longer takes the widget down on every locale). Also stopped caching failed fetches so a transient blip can retry. Mirrors the fetchLanguagePlaceholders fallback pattern. Thanks!

@claude

claude Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Code Review — latest commit (824b932, per-locale JSON refactor)

Reviewed the refactor that replaces the in-code BC_UI_EN/BC_LOCALES objects with fetched, deep-merged scripts/brand-concierge/localization/{en,es}.json sheets, loaded via loadBrandConciergeConfig(lang).

Confirmed fixed from prior review threads:

  • Shallow ui merge (would've dropped partial disclaimer sub-fields for a future locale) — now a recursive deepMerge, giving per-field English fallback. Resolves the earlier note on brand-concierge-config.js:245 (old line).
  • datastreamId leaking into the styling payload forwarded to the third-party BC web client — now lives in a separate BC_DATASTREAMS lookup (getBrandConciergeDatastreamId) kept entirely out of activeConfig/getBootstrapOptions().
  • Per-locale-file structure per @ajaydwarkani/@noormd08's suggestion — adopted as requested.

New finding (posted inline on brand-concierge-config.js:138):
loadBrandConciergeConfig only try/catches the locale fetch, not the English base fetch. If en.json fails to load for any reason, the whole widget silently fails to mount (on every locale, not just es), which is a new single point of failure that didn't exist when strings were bundled in JS. The repo already has an established fallback pattern for this in fetchLanguagePlaceholders (scripts.js) that's worth mirroring.

No other blocking issues found — XSS surface is clean (textContent/createTextNode only), English behavior is unchanged, and translation coverage matches the English base 1:1.

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) <noreply@anthropic.com>
Comment thread scripts/brand-concierge/brand-concierge-config.js
Comment on lines +144 to +161
export async function loadBrandConciergeConfig(lang) {
const key = (lang || 'en').toLowerCase();
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 {
locale = await fetchLocaleSheet(key);
} catch {
locale = en;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadBrandConciergeConfig always attempts to fetch ${LOCALES_BASE_PATH}/${key}.json for any non-en path locale (line 155-160), but only en.json and es.json actually exist in this PR. Experience League already serves many other locales (de, fr, it, ja, ko, pt-br, zh-hans, zh-hant, etc., per the languagesMap in scripts.js), so on every one of those locales, every single page load will issue a fetch that is guaranteed to 404 (and since fetch failures aren't cached in localeSheetCache, this repeats indefinitely — there's no way for it to ever be short-circuited).

On top of that, the two fetches are sequential, not parallel:

en = await fetchLocaleSheet('en');       // line 148
...
locale = await fetchLocaleSheet(key);    // line 157

For every non-en/es locale this doubles the network latency on the critical path of Brand Concierge's init (activeConfig = await loadBrandConciergeConfig(activeLang) blocks widget mount), since the doomed-to-404 locale fetch is awaited only after the English fetch resolves.

Two independent, low-effort fixes:

  1. Kick off fetchLocaleSheet('en') and fetchLocaleSheet(key) concurrently (e.g. Promise.all/Promise.allSettled) instead of sequentially.
  2. Gate the locale fetch on a known-supported-locales list (mirroring the BC_DATASTREAMS keys pattern) so locales without a translation file skip the network call entirely instead of hitting the CDN with a guaranteed 404 on every pageview.

"feedback.thumbsDown.aria": "Thumbs down"
},
"arrays": {
"welcome.examples": [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eliwangj , arrays are not supported by localization automation tools, so can you only add support for key/value pair. It's okay to have a nested objects but should only be key/value pair.

{ "text": "Set up an Adobe Analytics report suite" },
{ "text": "Explain Adobe Target A/B testing" }
],
"feedback.positive.options": [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

"Friendly and conversational tone",
"Other"
],
"feedback.negative.options": ["Not helpful or relevant", "Confusing or unclear", "Too formal or robotic", "Other"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

@@ -0,0 +1,59 @@
{
"language": "en-US",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eliwangj , We should avoid adding non-localizable data in language file as this may cause application to crash if it gets translated.

…ream (#2831)

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 <noreply@anthropic.com>
@aem-code-sync

aem-code-sync Bot commented Aug 4, 2026

Copy link
Copy Markdown
Page Scores Audits Google
📱 /en/browse?martech=off PERFORMANCE A11Y SEO BEST PRACTICES SI FCP LCP TBT CLS PSI
🖥️ /en/browse?martech=off PERFORMANCE A11Y SEO BEST PRACTICES SI FCP LCP TBT CLS PSI
📱 /es/browse?martech=off PERFORMANCE A11Y SEO BEST PRACTICES SI FCP LCP TBT CLS PSI
🖥️ /es/browse?martech=off PERFORMANCE A11Y SEO BEST PRACTICES SI FCP LCP TBT CLS PSI

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants