Recipe: Storefront multi-locale
Esta guía cubre tres escenarios habituales:
- Un solo shop, múltiples idiomas (storefront i18n con un único catálogo).
- Múltiples shops, uno por mercado (catálogo / pricing / stock distintos por país).
- 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):
- Atributo
data-locale="es"en el<script>. - URL parameter
?lang=es. <html lang="es">del documento.navigator.languagedel navegador.- Default del backend (resuelto por
LocaleHeaderFilteren el API).
Cita: cascada de detección descrita en
Widget · i18ny respaldada porLocaleHeaderFilteren el backend (ver memoriaproject_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
nameen 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:
| Mercado | shopId | Locale UI | Catálogo |
|---|---|---|---|
| ES | shop_es_xxx | es | precios EUR, descripciones ES |
| EN | shop_en_xxx | en | precios EUR/GBP, descripciones EN |
| DE | shop_de_xxx | de | precios 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.comowww.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
externalIddistintos (SKU-001-ES,SKU-001-EN). En la práctica, suele ser preferible un shop por mercado.
Errores frecuentes
| Síntoma | Causa | Solución |
|---|---|---|
El widget arranca en en aunque el sitio está en es | <html lang> faltante o sobrescrito por un script anterior | Define data-locale="es" explícito en el script. |
| Resultados con descripciones mezcladas (ES + EN) | Catálogo único con productos sincronizados en idiomas distintos | Normaliza a un único idioma por producto o usa multi-shop. |
| Conversiones imputadas al shop equivocado | Backend usa shopId global en lugar de resolver por order | Pasa shopId desde el contexto del pedido. |
Próximos pasos
- Widget · i18n — locales soportados y customización de strings.
plugins/custom/server-to-server— multi-shop server-to-server.- Recipe · Server-to-server token.