Saltar al contenido principal

Recipe: Storefront multi-locale

Esta guía cubre tres escenarios habituales:

  1. Un solo shop, múltiples idiomas (storefront i18n con un único catálogo).
  2. Múltiples shops, uno por mercado (catálogo / pricing / stock distintos por país).
  3. Subdominios o sub-paths (/es, /en, de.example.com, etc.).

Cómo el widget detecta el locale

El widget aplica esta cascada (mayor prioridad arriba):

  1. Atributo data-locale="es" en el <script>.
  2. URL parameter ?lang=es.
  3. <html lang="es"> del documento.
  4. navigator.language del navegador.
  5. Default del backend (resuelto por LocaleHeaderFilter en el API).

Cita: cascada de detección descrita en Widget · i18n y respaldada por LocaleHeaderFilter en el backend (ver memoria project_i18n_auto_detect).

Idiomas soportados

El widget renderiza UI en al menos: es, en, de, fr, it, pt. Para idiomas no soportados, el widget cae a en y el backend genera respuestas en el idioma del usuario igualmente.

Escenario 1. Un shop, múltiples idiomas

Si tu catálogo es uno y solo cambia la UI, define data-locale por render:

<script
src="https://cdn.neuroon.ai/widget@0.9.10/widget.js"
data-token="<%= widgetToken %>"
data-container="#neuroon-search"
data-locale="<%= currentLocale %>" async></script>

Aclaraciones:

  • El catálogo en Neuroon admite descripciones / nombres en un único idioma por producto. Si tu producto tiene name en español, el widget puede traducir on-the-fly el copy de UI pero no el contenido del producto. - Para storefronts con catálogos traducidos producto a producto, usa el escenario 2 con un shop por idioma.

Escenario 2. Multi-shop por mercado

Cuando precio, stock o catálogo varían por país, lo natural es un shop por mercado, cada uno con su Shop API Key y su Widget Token:

MercadoshopIdLocale UICatálogo
ESshop_es_xxxesprecios EUR, descripciones ES
ENshop_en_xxxenprecios EUR/GBP, descripciones EN
DEshop_de_xxxdeprecios EUR, descripciones DE

Tu backend resuelve qué shopId (y por tanto qué token) usar en cada render:

const shopByLocale = {
es: process.env.NEUROON_SHOP_ID_ES,
en: process.env.NEUROON_SHOP_ID_EN,
de: process.env.NEUROON_SHOP_ID_DE,
};

async function getWidgetTokenForLocale(locale: string) {
const shopId = shopByLocale[locale] ?? shopByLocale.en;
const apiKey = process.env[`NEUROON_API_KEY_${locale.toUpperCase()}`];

const res = await fetch(
`${process.env.NEUROON_API_URL}/api/shops/${shopId}/widget-token`,
{ method: 'POST', headers: { 'X-Shop-API-Key': apiKey } },
);
if (!res.ok) throw new Error('Token issuance failed');
return (await res.json()).token;
}

Cada shop se sincroniza por separado con su propio cron (/products/sync).

Escenario 3. URL routing por locale

Path-based (/es/, /en/)

// Next.js i18n
export default async function Layout({
params: { locale },
children,
}: { params: { locale: string }; children: React.ReactNode }) {
const token = await getWidgetTokenForLocale(locale);
return (
<html lang={locale}>
<body>
{children}
<Script
src="https://cdn.neuroon.ai/widget@0.9.10/widget.js"
strategy="afterInteractive"
data-token={token}
data-container="#neuroon-search"
data-locale={locale}
/>
</body>
</html>
);
}

Sub-domain (es.example.com, de.example.com)

function detectLocale(host: string): string {
const sub = host.split('.')[0];
return ['es', 'en', 'de', 'fr', 'it', 'pt'].includes(sub) ? sub : 'en';
}

Y luego usa detectLocale(req.headers.host) para resolver el locale en cada request.

Override por URL parameter

Para QA y A/B testing es útil forzar el locale vía ?lang=:

// El widget ya detecta ?lang=es del URL, no necesitas código extra.
// Si quieres persistir la preferencia:
const url = new URL(window.location);
const lang = url.searchParams.get('lang');
if (lang) localStorage.setItem('neuroon-locale', lang);

Cart bridge multi-locale

El cart bridge no cambia entre escenarios. El payload solo contiene identificadores de producto y agregados; el currency lo define el shop, no el locale UI.

Tracking de conversiones

En multi-shop, el track:conversion debe ir al shopId correcto:

const shopId = resolveShopFromOrder(order); // mismo criterio que en frontend
const apiKey = resolveApiKey(shopId);
await fetch(`${API_URL}/api/plugin/shops/${shopId}/track/conversion`, {
method: 'POST',
headers: { 'X-Shop-API-Key': apiKey, 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});

Buenas prácticas

  • Un Widget Token por shop, no por locale. Para escenario 1 (un solo shop, varios idiomas), usa el mismo token y solo cambia data-locale.
  • Cachea por shop, no por usuario. Los tokens son por tienda y soportan miles de impresiones simultáneas.
  • Aliases de URL canónica. Verifica el dominio canónico (example.com o www.example.com) y usa el mismo en todas las llamadas.
  • Pricing: si tu producto tiene precios distintos por mercado y usas un solo shop, la única forma limpia es duplicar el producto con externalId distintos (SKU-001-ES, SKU-001-EN). En la práctica, suele ser preferible un shop por mercado.

Errores frecuentes

SíntomaCausaSolución
El widget arranca en en aunque el sitio está en es<html lang> faltante o sobrescrito por un script anteriorDefine data-locale="es" explícito en el script.
Resultados con descripciones mezcladas (ES + EN)Catálogo único con productos sincronizados en idiomas distintosNormaliza a un único idioma por producto o usa multi-shop.
Conversiones imputadas al shop equivocadoBackend usa shopId global en lugar de resolver por orderPasa shopId desde el contexto del pedido.

Próximos pasos