From b2fc18b5bc54c4b9b5a37ecbee4cb58d83bc3540 Mon Sep 17 00:00:00 2001 From: Oriol Raventos Date: Mon, 13 Apr 2026 11:05:48 +0200 Subject: [PATCH 1/8] feat: create a class to wrapp logic to resolve flow locale using user locale auto detected by backend --- .../botonic-plugin-flow-builder/src/api.ts | 51 +-- .../src/utils/flow-locale.ts | 83 +++++ .../tests/flow-locale.test.ts | 332 ++++++++++++++++++ 3 files changed, 424 insertions(+), 42 deletions(-) create mode 100644 packages/botonic-plugin-flow-builder/src/utils/flow-locale.ts create mode 100644 packages/botonic-plugin-flow-builder/tests/flow-locale.test.ts diff --git a/packages/botonic-plugin-flow-builder/src/api.ts b/packages/botonic-plugin-flow-builder/src/api.ts index d21b98a882..c8be4cf141 100644 --- a/packages/botonic-plugin-flow-builder/src/api.ts +++ b/packages/botonic-plugin-flow-builder/src/api.ts @@ -25,6 +25,7 @@ import { type HtSmartIntentNode, } from './content-fields/hubtype-fields' import { type FlowBuilderApiOptions, ProcessEnvNodeEnvs } from './types' +import { FlowLocale } from './utils/flow-locale' export class FlowBuilderApi { url: string @@ -310,47 +311,13 @@ export class FlowBuilderApi { } getResolvedLocale(): string { - const systemLocale = this.request.getSystemLocale() - - const locale = this.resolveAsLocale(systemLocale) - if (locale) { - return locale - } - - const language = this.resolveAsLanguage(systemLocale) - if (language) { - this.request.setSystemLocale(language) - return language - } - - const defaultLocale = this.resolveAsDefaultLocale() - this.request.setSystemLocale(defaultLocale) - return defaultLocale - } - - private resolveAsLocale(locale: string): string | undefined { - if (this.flow.locales.find(flowLocale => flowLocale === locale)) { - return locale - } - return undefined - } - - private resolveAsLanguage(locale?: string): string | undefined { - const language = locale?.split('-')[0] - if ( - language && - this.flow.locales.find(flowLocale => flowLocale === language) - ) { - console.log(`locale: ${locale} has been resolved as ${language}`) - return language - } - return undefined - } - - private resolveAsDefaultLocale(): string { - console.log( - `Resolve locale with default locale: ${this.flow.default_locale_code}` - ) - return this.flow.default_locale_code || 'en' + const flowLocales = this.flow.locales + const defaultLocaleCode = this.flow.default_locale_code + + return new FlowLocale( + this.request, + flowLocales, + defaultLocaleCode + ).resolve() } } diff --git a/packages/botonic-plugin-flow-builder/src/utils/flow-locale.ts b/packages/botonic-plugin-flow-builder/src/utils/flow-locale.ts new file mode 100644 index 0000000000..6262b8c91a --- /dev/null +++ b/packages/botonic-plugin-flow-builder/src/utils/flow-locale.ts @@ -0,0 +1,83 @@ +import type { BotContext } from '@botonic/core' + +export class FlowLocale { + constructor( + private botContext: BotContext, + private flowLocales: string[], + private defaultLocaleCode: string + ) {} + + resolve(): string { + const resolvedUserLocale = this.resolveAsUserLocale() + if (resolvedUserLocale) { + return resolvedUserLocale + } + + const resolvedSystemLocale = this.resolveAsSystemLocale() + if (resolvedSystemLocale) { + return resolvedSystemLocale + } + + const defaultLocale = this.resolveAsDefaultLocale() + this.botContext.setSystemLocale(defaultLocale) + return defaultLocale + } + + private resolveAsUserLocale(): string | undefined { + const userLocale = this.botContext.session.user.locale + + const resolvedUserLocale = this.resolveAsLocale(userLocale) + if (resolvedUserLocale) { + this.botContext.setSystemLocale(resolvedUserLocale) + return resolvedUserLocale + } + + const resolvedUserLanguage = this.resolveAsLanguage(userLocale) + if (resolvedUserLanguage) { + this.botContext.setSystemLocale(resolvedUserLanguage) + return resolvedUserLanguage + } + + return undefined + } + + private resolveAsSystemLocale(): string | undefined { + const systemLocale = this.botContext.getSystemLocale() + + const locale = this.resolveAsLocale(systemLocale) + if (locale) { + return locale + } + + const language = this.resolveAsLanguage(systemLocale) + if (language) { + this.botContext.setSystemLocale(language) + return language + } + return undefined + } + + private resolveAsLocale(locale: string): string | undefined { + if (this.flowLocales.find(flowLocale => flowLocale === locale)) { + return locale + } + return undefined + } + + private resolveAsLanguage(locale?: string): string | undefined { + const language = locale?.split('-')[0] + if ( + language && + this.flowLocales.find(flowLocale => flowLocale === language) + ) { + // console.log(`locale: ${locale} has been resolved as ${language}`) + return language + } + return undefined + } + + private resolveAsDefaultLocale(): string { + // console.log(`Resolve locale with default locale: ${this.defaultLocaleCode}`) + return this.defaultLocaleCode || 'en' + } +} diff --git a/packages/botonic-plugin-flow-builder/tests/flow-locale.test.ts b/packages/botonic-plugin-flow-builder/tests/flow-locale.test.ts new file mode 100644 index 0000000000..905f03a2aa --- /dev/null +++ b/packages/botonic-plugin-flow-builder/tests/flow-locale.test.ts @@ -0,0 +1,332 @@ +import type { BotContext } from '@botonic/core' +import { describe, expect, test } from '@jest/globals' + +import { FlowLocale } from '../src/utils/flow-locale' + +function createMockBotContext( + userLocale: string, + systemLocale: string +): BotContext { + let currentSystemLocale = systemLocale + + return { + session: { + user: { + locale: userLocale, + system_locale: systemLocale, + }, + }, + getSystemLocale: () => currentSystemLocale, + setSystemLocale: (locale: string) => { + currentSystemLocale = locale + }, + } as unknown as BotContext +} + +describe('FlowLocale.resolve()', () => { + describe('when user locale matches exactly', () => { + test('should return the user locale when it matches a flow locale', () => { + const botContext = createMockBotContext('es', 'en') + const flowLocales = ['en', 'es', 'fr'] + const defaultLocaleCode = 'en' + + const flowLocale = new FlowLocale( + botContext, + flowLocales, + defaultLocaleCode + ) + + const result = flowLocale.resolve() + + expect(result).toBe('es') + }) + + test('should set system locale when user locale is resolved', () => { + const botContext = createMockBotContext('fr', 'en') + const flowLocales = ['en', 'es', 'fr'] + const defaultLocaleCode = 'en' + + const flowLocale = new FlowLocale( + botContext, + flowLocales, + defaultLocaleCode + ) + + flowLocale.resolve() + + expect(botContext.getSystemLocale()).toBe('fr') + }) + }) + + describe('when user locale has language-region format', () => { + test('should resolve to language code when exact locale not found but language is available', () => { + const botContext = createMockBotContext('es-ES', 'en') + const flowLocales = ['en', 'es', 'fr'] + const defaultLocaleCode = 'en' + + const flowLocale = new FlowLocale( + botContext, + flowLocales, + defaultLocaleCode + ) + + const result = flowLocale.resolve() + + expect(result).toBe('es') + }) + + test('should set system locale to resolved language', () => { + const botContext = createMockBotContext('fr-CA', 'en') + const flowLocales = ['en', 'es', 'fr'] + const defaultLocaleCode = 'en' + + const flowLocale = new FlowLocale( + botContext, + flowLocales, + defaultLocaleCode + ) + + flowLocale.resolve() + + expect(botContext.getSystemLocale()).toBe('fr') + }) + + test('should prefer exact match over language extraction', () => { + const botContext = createMockBotContext('pt-BR', 'en') + const flowLocales = ['en', 'pt-BR', 'pt'] + const defaultLocaleCode = 'en' + + const flowLocale = new FlowLocale( + botContext, + flowLocales, + defaultLocaleCode + ) + + const result = flowLocale.resolve() + + expect(result).toBe('pt-BR') + }) + }) + + describe('when system locale matches', () => { + test('should return system locale when user locale does not match', () => { + const botContext = createMockBotContext('de', 'es') + const flowLocales = ['en', 'es', 'fr'] + const defaultLocaleCode = 'en' + + const flowLocale = new FlowLocale( + botContext, + flowLocales, + defaultLocaleCode + ) + + const result = flowLocale.resolve() + + expect(result).toBe('es') + }) + + test('should resolve system locale as language when exact match not found', () => { + const botContext = createMockBotContext('de', 'fr-FR') + const flowLocales = ['en', 'es', 'fr'] + const defaultLocaleCode = 'en' + + const flowLocale = new FlowLocale( + botContext, + flowLocales, + defaultLocaleCode + ) + + const result = flowLocale.resolve() + + expect(result).toBe('fr') + }) + + test('should update system locale when resolved as language', () => { + const botContext = createMockBotContext('de', 'es-MX') + const flowLocales = ['en', 'es', 'fr'] + const defaultLocaleCode = 'en' + + const flowLocale = new FlowLocale( + botContext, + flowLocales, + defaultLocaleCode + ) + + flowLocale.resolve() + + expect(botContext.getSystemLocale()).toBe('es') + }) + }) + + describe('when falling back to default locale', () => { + test('should return default locale when no match found', () => { + const botContext = createMockBotContext('de', 'it') + const flowLocales = ['en', 'es', 'fr'] + const defaultLocaleCode = 'en' + + const flowLocale = new FlowLocale( + botContext, + flowLocales, + defaultLocaleCode + ) + + const result = flowLocale.resolve() + + expect(result).toBe('en') + }) + + test('should set system locale to default when falling back', () => { + const botContext = createMockBotContext('de', 'it') + const flowLocales = ['en', 'es', 'fr'] + const defaultLocaleCode = 'fr' + + const flowLocale = new FlowLocale( + botContext, + flowLocales, + defaultLocaleCode + ) + + flowLocale.resolve() + + expect(botContext.getSystemLocale()).toBe('fr') + }) + + test('should return "en" when default locale code is empty', () => { + const botContext = createMockBotContext('de', 'it') + const flowLocales = ['en', 'es', 'fr'] + const defaultLocaleCode = '' + + const flowLocale = new FlowLocale( + botContext, + flowLocales, + defaultLocaleCode + ) + + const result = flowLocale.resolve() + + expect(result).toBe('en') + }) + }) + + describe('edge cases', () => { + test('should handle empty flow locales array', () => { + const botContext = createMockBotContext('es', 'en') + const flowLocales: string[] = [] + const defaultLocaleCode = 'en' + + const flowLocale = new FlowLocale( + botContext, + flowLocales, + defaultLocaleCode + ) + + const result = flowLocale.resolve() + + expect(result).toBe('en') + }) + + test('should handle undefined user locale', () => { + const botContext = createMockBotContext( + undefined as unknown as string, + 'es' + ) + const flowLocales = ['en', 'es', 'fr'] + const defaultLocaleCode = 'en' + + const flowLocale = new FlowLocale( + botContext, + flowLocales, + defaultLocaleCode + ) + + const result = flowLocale.resolve() + + expect(result).toBe('es') + }) + + test('should handle undefined system locale', () => { + const botContext = createMockBotContext( + 'de', + undefined as unknown as string + ) + const flowLocales = ['en', 'es', 'fr'] + const defaultLocaleCode = 'fr' + + const flowLocale = new FlowLocale( + botContext, + flowLocales, + defaultLocaleCode + ) + + const result = flowLocale.resolve() + + expect(result).toBe('fr') + }) + + test('should handle locale with multiple dashes', () => { + const botContext = createMockBotContext('zh-Hans-CN', 'en') + const flowLocales = ['en', 'zh'] + const defaultLocaleCode = 'en' + + const flowLocale = new FlowLocale( + botContext, + flowLocales, + defaultLocaleCode + ) + + const result = flowLocale.resolve() + + expect(result).toBe('zh') + }) + + test('should be case sensitive when matching locales', () => { + const botContext = createMockBotContext('ES', 'EN') + const flowLocales = ['en', 'es', 'fr'] + const defaultLocaleCode = 'en' + + const flowLocale = new FlowLocale( + botContext, + flowLocales, + defaultLocaleCode + ) + + const result = flowLocale.resolve() + + expect(result).toBe('en') + }) + }) + + describe('priority order', () => { + test('should prioritize user locale over system locale', () => { + const botContext = createMockBotContext('fr', 'es') + const flowLocales = ['en', 'es', 'fr'] + const defaultLocaleCode = 'en' + + const flowLocale = new FlowLocale( + botContext, + flowLocales, + defaultLocaleCode + ) + + const result = flowLocale.resolve() + + expect(result).toBe('fr') + }) + + test('should prioritize system locale over default', () => { + const botContext = createMockBotContext('de', 'fr') + const flowLocales = ['en', 'es', 'fr'] + const defaultLocaleCode = 'en' + + const flowLocale = new FlowLocale( + botContext, + flowLocales, + defaultLocaleCode + ) + + const result = flowLocale.resolve() + + expect(result).toBe('fr') + }) + }) +}) From c5348a5146dafc83fdb18c60b1c29e937ce5768a Mon Sep 17 00:00:00 2001 From: Oriol Raventos Date: Wed, 15 Apr 2026 12:45:20 +0200 Subject: [PATCH 2/8] feat: add system_locale_updated flag and send webchatSetting with updated system_locale --- .../botonic-core/src/models/legacy-types.ts | 1 + .../src/action/index.tsx | 36 +++++++++++++++---- .../botonic-plugin-flow-builder/src/index.ts | 1 + .../src/utils/flow-locale.ts | 13 ++++--- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/packages/botonic-core/src/models/legacy-types.ts b/packages/botonic-core/src/models/legacy-types.ts index 01c7b7f82d..4eb8137106 100644 --- a/packages/botonic-core/src/models/legacy-types.ts +++ b/packages/botonic-core/src/models/legacy-types.ts @@ -241,6 +241,7 @@ export interface SessionUser { locale: string country: string system_locale: string + system_locale_updated?: boolean } export interface HubtypeCaseContactReason { diff --git a/packages/botonic-plugin-flow-builder/src/action/index.tsx b/packages/botonic-plugin-flow-builder/src/action/index.tsx index e80ab2416e..9b8a9efce9 100644 --- a/packages/botonic-plugin-flow-builder/src/action/index.tsx +++ b/packages/botonic-plugin-flow-builder/src/action/index.tsx @@ -73,13 +73,36 @@ export class FlowBuilderAction extends React.Component { return filteredContents } - render(): JSX.Element | JSX.Element[] { - const { contents, webchatSettingsParams } = this.props - const botContext = this.context as BotContext + protected getWebchatSettingsParams(botContext: BotContext): { + shouldSendWebchatSettings: boolean + webchatSettingsParams?: WebchatSettingsProps + } { + let { webchatSettingsParams } = this.props + if (botContext.session.user.system_locale_updated) { + webchatSettingsParams = { + ...webchatSettingsParams, + user: { + ...webchatSettingsParams?.user, + system_locale: botContext.session.user.system_locale, + }, + } + } const shouldSendWebchatSettings = (isWebchat(botContext.session) || isDev(botContext.session)) && !!webchatSettingsParams + return { + shouldSendWebchatSettings, + webchatSettingsParams, + } + } + + render(): JSX.Element | JSX.Element[] { + const { contents } = this.props + const botContext = this.context as BotContext + const { shouldSendWebchatSettings, webchatSettingsParams } = + this.getWebchatSettingsParams(botContext) + return ( <> {shouldSendWebchatSettings && ( @@ -93,11 +116,10 @@ export class FlowBuilderAction extends React.Component { export class FlowBuilderMultichannelAction extends FlowBuilderAction { render(): JSX.Element | JSX.Element[] { - const { contents, webchatSettingsParams } = this.props + const { contents } = this.props const botContext = this.context as BotContext - const shouldSendWebchatSettings = - (isWebchat(botContext.session) || isDev(botContext.session)) && - !!webchatSettingsParams + const { shouldSendWebchatSettings, webchatSettingsParams } = + this.getWebchatSettingsParams(botContext) return ( diff --git a/packages/botonic-plugin-flow-builder/src/index.ts b/packages/botonic-plugin-flow-builder/src/index.ts index cf59200267..8135c48912 100644 --- a/packages/botonic-plugin-flow-builder/src/index.ts +++ b/packages/botonic-plugin-flow-builder/src/index.ts @@ -167,6 +167,7 @@ export default class BotonicPluginFlowBuilder implements Plugin { post(request: PluginPreRequest): void { request.input.nluResolution = undefined + delete request.session.user.system_locale_updated } async getContentsByContentID( diff --git a/packages/botonic-plugin-flow-builder/src/utils/flow-locale.ts b/packages/botonic-plugin-flow-builder/src/utils/flow-locale.ts index 6262b8c91a..8f782b4cbd 100644 --- a/packages/botonic-plugin-flow-builder/src/utils/flow-locale.ts +++ b/packages/botonic-plugin-flow-builder/src/utils/flow-locale.ts @@ -19,7 +19,7 @@ export class FlowLocale { } const defaultLocale = this.resolveAsDefaultLocale() - this.botContext.setSystemLocale(defaultLocale) + this.setSystemLocale(defaultLocale) return defaultLocale } @@ -28,13 +28,13 @@ export class FlowLocale { const resolvedUserLocale = this.resolveAsLocale(userLocale) if (resolvedUserLocale) { - this.botContext.setSystemLocale(resolvedUserLocale) + this.setSystemLocale(resolvedUserLocale) return resolvedUserLocale } const resolvedUserLanguage = this.resolveAsLanguage(userLocale) if (resolvedUserLanguage) { - this.botContext.setSystemLocale(resolvedUserLanguage) + this.setSystemLocale(resolvedUserLanguage) return resolvedUserLanguage } @@ -51,7 +51,7 @@ export class FlowLocale { const language = this.resolveAsLanguage(systemLocale) if (language) { - this.botContext.setSystemLocale(language) + this.setSystemLocale(language) return language } return undefined @@ -80,4 +80,9 @@ export class FlowLocale { // console.log(`Resolve locale with default locale: ${this.defaultLocaleCode}`) return this.defaultLocaleCode || 'en' } + + private setSystemLocale(locale: string): void { + this.botContext.setSystemLocale(locale) + this.botContext.session.user.system_locale_updated = true + } } From e896d50d02bc389f26064cc507f6c3e8d0a89f50 Mon Sep 17 00:00:00 2001 From: Oriol Raventos Date: Tue, 21 Apr 2026 16:43:28 +0200 Subject: [PATCH 3/8] feat: update locale rules --- .../src/utils/flow-locale.ts | 70 +++++----- .../tests/flow-locale.test.ts | 123 +++++++++++------- 2 files changed, 115 insertions(+), 78 deletions(-) diff --git a/packages/botonic-plugin-flow-builder/src/utils/flow-locale.ts b/packages/botonic-plugin-flow-builder/src/utils/flow-locale.ts index 8f782b4cbd..349b599ffe 100644 --- a/packages/botonic-plugin-flow-builder/src/utils/flow-locale.ts +++ b/packages/botonic-plugin-flow-builder/src/utils/flow-locale.ts @@ -8,14 +8,27 @@ export class FlowLocale { ) {} resolve(): string { - const resolvedUserLocale = this.resolveAsUserLocale() - if (resolvedUserLocale) { - return resolvedUserLocale - } + const userLocale = this.botContext.getUserLocale() + const systemLocale = this.botContext.getSystemLocale() + + const moreSpecificLocale = this.selectMoreSpecificLocale( + userLocale, + systemLocale + ) + + if (moreSpecificLocale) { + const resolvedSpecific = this.resolveAsLocale(moreSpecificLocale) + if (resolvedSpecific) { + this.setSystemLocale(resolvedSpecific) + return resolvedSpecific + } - const resolvedSystemLocale = this.resolveAsSystemLocale() - if (resolvedSystemLocale) { - return resolvedSystemLocale + const resolvedSpecificLanguage = + this.resolveAsLanguage(moreSpecificLocale) + if (resolvedSpecificLanguage) { + this.setSystemLocale(resolvedSpecificLanguage) + return resolvedSpecificLanguage + } } const defaultLocale = this.resolveAsDefaultLocale() @@ -23,38 +36,29 @@ export class FlowLocale { return defaultLocale } - private resolveAsUserLocale(): string | undefined { - const userLocale = this.botContext.session.user.locale - - const resolvedUserLocale = this.resolveAsLocale(userLocale) - if (resolvedUserLocale) { - this.setSystemLocale(resolvedUserLocale) - return resolvedUserLocale + private selectMoreSpecificLocale( + userLocale: string, + systemLocale: string + ): string | undefined { + if (!userLocale || !systemLocale) { + return undefined } - const resolvedUserLanguage = this.resolveAsLanguage(userLocale) - if (resolvedUserLanguage) { - this.setSystemLocale(resolvedUserLanguage) - return resolvedUserLanguage - } + const languageUser = userLocale.split('-')[0] + const languageSystem = systemLocale.split('-')[0] - return undefined - } + if (languageUser !== languageSystem) { + return userLocale + } - private resolveAsSystemLocale(): string | undefined { - const systemLocale = this.botContext.getSystemLocale() + const hasRegionUserLocale = userLocale.includes('-') + const hasRegionSystemLocale = systemLocale.includes('-') - const locale = this.resolveAsLocale(systemLocale) - if (locale) { - return locale + if (hasRegionUserLocale === hasRegionSystemLocale) { + return userLocale } - const language = this.resolveAsLanguage(systemLocale) - if (language) { - this.setSystemLocale(language) - return language - } - return undefined + return hasRegionUserLocale ? userLocale : systemLocale } private resolveAsLocale(locale: string): string | undefined { @@ -70,14 +74,12 @@ export class FlowLocale { language && this.flowLocales.find(flowLocale => flowLocale === language) ) { - // console.log(`locale: ${locale} has been resolved as ${language}`) return language } return undefined } private resolveAsDefaultLocale(): string { - // console.log(`Resolve locale with default locale: ${this.defaultLocaleCode}`) return this.defaultLocaleCode || 'en' } diff --git a/packages/botonic-plugin-flow-builder/tests/flow-locale.test.ts b/packages/botonic-plugin-flow-builder/tests/flow-locale.test.ts index 905f03a2aa..7841334d69 100644 --- a/packages/botonic-plugin-flow-builder/tests/flow-locale.test.ts +++ b/packages/botonic-plugin-flow-builder/tests/flow-locale.test.ts @@ -16,6 +16,7 @@ function createMockBotContext( system_locale: systemLocale, }, }, + getUserLocale: () => userLocale, getSystemLocale: () => currentSystemLocale, setSystemLocale: (locale: string) => { currentSystemLocale = locale @@ -109,7 +110,7 @@ describe('FlowLocale.resolve()', () => { }) describe('when system locale matches', () => { - test('should return system locale when user locale does not match', () => { + test('should return default locale when user locale does not match and is different from system locale', () => { const botContext = createMockBotContext('de', 'es') const flowLocales = ['en', 'es', 'fr'] const defaultLocaleCode = 'en' @@ -122,39 +123,7 @@ describe('FlowLocale.resolve()', () => { const result = flowLocale.resolve() - expect(result).toBe('es') - }) - - test('should resolve system locale as language when exact match not found', () => { - const botContext = createMockBotContext('de', 'fr-FR') - const flowLocales = ['en', 'es', 'fr'] - const defaultLocaleCode = 'en' - - const flowLocale = new FlowLocale( - botContext, - flowLocales, - defaultLocaleCode - ) - - const result = flowLocale.resolve() - - expect(result).toBe('fr') - }) - - test('should update system locale when resolved as language', () => { - const botContext = createMockBotContext('de', 'es-MX') - const flowLocales = ['en', 'es', 'fr'] - const defaultLocaleCode = 'en' - - const flowLocale = new FlowLocale( - botContext, - flowLocales, - defaultLocaleCode - ) - - flowLocale.resolve() - - expect(botContext.getSystemLocale()).toBe('es') + expect(result).toBe('en') }) }) @@ -241,7 +210,7 @@ describe('FlowLocale.resolve()', () => { const result = flowLocale.resolve() - expect(result).toBe('es') + expect(result).toBe('en') }) test('should handle undefined system locale', () => { @@ -296,25 +265,91 @@ describe('FlowLocale.resolve()', () => { }) }) - describe('priority order', () => { - test('should prioritize user locale over system locale', () => { - const botContext = createMockBotContext('fr', 'es') - const flowLocales = ['en', 'es', 'fr'] + describe('when locales share language but differ in specificity', () => { + test('should prefer system locale with region over generic user locale', () => { + const botContext = createMockBotContext('es', 'es-MX') + const flowLocales = ['es', 'es-MX'] const defaultLocaleCode = 'en' - const flowLocale = new FlowLocale( + const result = new FlowLocale( botContext, flowLocales, defaultLocaleCode - ) + ).resolve() - const result = flowLocale.resolve() + expect(result).toBe('es-MX') + }) + + test('should fall back to generic locale when specific system locale not in flow', () => { + const botContext = createMockBotContext('es', 'es-MX') + const flowLocales = ['es'] + const defaultLocaleCode = 'en' + + const result = new FlowLocale( + botContext, + flowLocales, + defaultLocaleCode + ).resolve() + + expect(result).toBe('es') + }) + + test('should prefer user locale with region over generic system locale', () => { + const botContext = createMockBotContext('es-ES', 'es') + const flowLocales = ['es-ES', 'es'] + const defaultLocaleCode = 'en' + + const result = new FlowLocale( + botContext, + flowLocales, + defaultLocaleCode + ).resolve() + + expect(result).toBe('es-ES') + }) + + test('should use user locale when both have regions for the same language', () => { + const botContext = createMockBotContext('es-MX', 'es-CO') + const flowLocales = ['es-MX', 'es-CO'] + const defaultLocaleCode = 'en' + + const result = new FlowLocale( + botContext, + flowLocales, + defaultLocaleCode + ).resolve() + + expect(result).toBe('es-MX') + }) + + test('should not apply specificity logic when languages differ', () => { + const botContext = createMockBotContext('fr', 'es-MX') + const flowLocales = ['fr', 'es-MX'] + const defaultLocaleCode = 'en' + + const result = new FlowLocale( + botContext, + flowLocales, + defaultLocaleCode + ).resolve() expect(result).toBe('fr') }) - test('should prioritize system locale over default', () => { - const botContext = createMockBotContext('de', 'fr') + test('should set system locale to the more specific resolved locale', () => { + const botContext = createMockBotContext('es', 'es-MX') + const flowLocales = ['es-MX'] + const defaultLocaleCode = 'en' + + new FlowLocale(botContext, flowLocales, defaultLocaleCode).resolve() + + expect(botContext.getSystemLocale()).toBe('es-MX') + }) + }) + + describe('priority order', () => { + test('should prioritize user locale over system locale', () => { + const botContext = createMockBotContext('fr', 'es') const flowLocales = ['en', 'es', 'fr'] const defaultLocaleCode = 'en' From c57d56d94a0e8cce990dfa0a5983261369f67d24 Mon Sep 17 00:00:00 2001 From: Oriol Raventos Date: Wed, 22 Apr 2026 09:18:56 +0200 Subject: [PATCH 4/8] refactor: simplify code with early returns --- .../src/utils/flow-locale.ts | 99 +++++++++---------- 1 file changed, 48 insertions(+), 51 deletions(-) diff --git a/packages/botonic-plugin-flow-builder/src/utils/flow-locale.ts b/packages/botonic-plugin-flow-builder/src/utils/flow-locale.ts index 349b599ffe..907ed467e3 100644 --- a/packages/botonic-plugin-flow-builder/src/utils/flow-locale.ts +++ b/packages/botonic-plugin-flow-builder/src/utils/flow-locale.ts @@ -2,89 +2,86 @@ import type { BotContext } from '@botonic/core' export class FlowLocale { constructor( - private botContext: BotContext, - private flowLocales: string[], - private defaultLocaleCode: string + private readonly botContext: BotContext, + private readonly flowLocales: string[], + private readonly defaultLocaleCode: string ) {} resolve(): string { - const userLocale = this.botContext.getUserLocale() - const systemLocale = this.botContext.getSystemLocale() + const priorityLocale = this.getPriorityLocale() - const moreSpecificLocale = this.selectMoreSpecificLocale( - userLocale, - systemLocale - ) - - if (moreSpecificLocale) { - const resolvedSpecific = this.resolveAsLocale(moreSpecificLocale) - if (resolvedSpecific) { - this.setSystemLocale(resolvedSpecific) - return resolvedSpecific + if (priorityLocale) { + const exactMatch = this.matchExactLocale(priorityLocale) + if (exactMatch) { + return this.applyLocale(exactMatch) } - const resolvedSpecificLanguage = - this.resolveAsLanguage(moreSpecificLocale) - if (resolvedSpecificLanguage) { - this.setSystemLocale(resolvedSpecificLanguage) - return resolvedSpecificLanguage + const languageMatch = this.matchLanguage(priorityLocale) + if (languageMatch) { + return this.applyLocale(languageMatch) } } - const defaultLocale = this.resolveAsDefaultLocale() - this.setSystemLocale(defaultLocale) - return defaultLocale + return this.applyLocale(this.getDefaultLocale()) } - private selectMoreSpecificLocale( - userLocale: string, - systemLocale: string - ): string | undefined { + /** + * Rules: + * - If user and system languages differ, user locale takes priority. + * - If both share the same language, the more specific locale wins. + * - If both have the same specificity, user locale wins. + */ + private getPriorityLocale(): string | undefined { + const userLocale = this.botContext.getUserLocale() + const systemLocale = this.botContext.getSystemLocale() + if (!userLocale || !systemLocale) { return undefined } - const languageUser = userLocale.split('-')[0] - const languageSystem = systemLocale.split('-')[0] + const userLanguage = this.getLanguage(userLocale) + const systemLanguage = this.getLanguage(systemLocale) - if (languageUser !== languageSystem) { + if (userLanguage !== systemLanguage) { return userLocale } + const userIsSpecific = this.isSpecificLocale(userLocale) + const systemIsSpecific = this.isSpecificLocale(systemLocale) - const hasRegionUserLocale = userLocale.includes('-') - const hasRegionSystemLocale = systemLocale.includes('-') - - if (hasRegionUserLocale === hasRegionSystemLocale) { + if (userIsSpecific && !systemIsSpecific) { return userLocale } + if (!userIsSpecific && systemIsSpecific) { + return systemLocale + } - return hasRegionUserLocale ? userLocale : systemLocale + return userLocale } - private resolveAsLocale(locale: string): string | undefined { - if (this.flowLocales.find(flowLocale => flowLocale === locale)) { - return locale - } - return undefined + private matchExactLocale(locale: string): string | undefined { + return this.flowLocales.includes(locale) ? locale : undefined } - private resolveAsLanguage(locale?: string): string | undefined { - const language = locale?.split('-')[0] - if ( - language && - this.flowLocales.find(flowLocale => flowLocale === language) - ) { - return language - } - return undefined + private matchLanguage(locale: string): string | undefined { + const language = this.getLanguage(locale) + return this.flowLocales.includes(language) ? language : undefined } - private resolveAsDefaultLocale(): string { + private getDefaultLocale(): string { return this.defaultLocaleCode || 'en' } - private setSystemLocale(locale: string): void { + private applyLocale(locale: string): string { this.botContext.setSystemLocale(locale) this.botContext.session.user.system_locale_updated = true + return locale + } + + private getLanguage(locale: string): string { + return locale.split('-')[0] + } + + private isSpecificLocale(locale: string): boolean { + return locale.includes('-') } } From 19a5d4027b5a43db7298164e49db6232bc4e16f3 Mon Sep 17 00:00:00 2001 From: Oriol Raventos Date: Mon, 27 Apr 2026 13:52:29 +0200 Subject: [PATCH 5/8] feat: when language detection is not enabled use system locale as a priority locale --- .../botonic-plugin-flow-builder/src/utils/flow-locale.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/botonic-plugin-flow-builder/src/utils/flow-locale.ts b/packages/botonic-plugin-flow-builder/src/utils/flow-locale.ts index 907ed467e3..0079a55296 100644 --- a/packages/botonic-plugin-flow-builder/src/utils/flow-locale.ts +++ b/packages/botonic-plugin-flow-builder/src/utils/flow-locale.ts @@ -8,7 +8,9 @@ export class FlowLocale { ) {} resolve(): string { - const priorityLocale = this.getPriorityLocale() + const priorityLocale = this.isLanguageDetectionEnabled() + ? this.getPriorityLocale() + : this.botContext.getSystemLocale() if (priorityLocale) { const exactMatch = this.matchExactLocale(priorityLocale) @@ -25,6 +27,10 @@ export class FlowLocale { return this.applyLocale(this.getDefaultLocale()) } + private isLanguageDetectionEnabled(): boolean { + return !!this.botContext.settings.LANGUAGE_DETECTION_ENABLED + } + /** * Rules: * - If user and system languages differ, user locale takes priority. From 93c4019577c7cf63d937790b588ba73e66b3bfca Mon Sep 17 00:00:00 2001 From: Oriol Raventos Date: Mon, 27 Apr 2026 15:56:41 +0200 Subject: [PATCH 6/8] test: refactor test with new botonic/core/testing functions and add a test to check when LANGUAGE_DETECTION_ENABLED is falsy --- .../tests/flow-locale.test.ts | 139 +++++++++++++----- 1 file changed, 100 insertions(+), 39 deletions(-) diff --git a/packages/botonic-plugin-flow-builder/tests/flow-locale.test.ts b/packages/botonic-plugin-flow-builder/tests/flow-locale.test.ts index 7841334d69..dbf38c4d8b 100644 --- a/packages/botonic-plugin-flow-builder/tests/flow-locale.test.ts +++ b/packages/botonic-plugin-flow-builder/tests/flow-locale.test.ts @@ -1,33 +1,70 @@ import type { BotContext } from '@botonic/core' +import { + createTestBotContext, + createTestSession, + createTestSettings, + TEST_DEFAULTS, +} from '@botonic/core/testing' import { describe, expect, test } from '@jest/globals' import { FlowLocale } from '../src/utils/flow-locale' -function createMockBotContext( - userLocale: string, - systemLocale: string +type FlowLocaleBotContextOptions = { + /** When false, `LANGUAGE_DETECTION_ENABLED` is cleared so `!!value` is falsy in FlowLocale. */ + languageDetectionEnabled?: boolean +} + +/** + * Builds a BotContext using @botonic/core/testing factories. Overrides locale getters/setters so + * `setSystemLocale` updates state (the stock `createTestBotContext` uses no-op setters). + */ +function createFlowLocaleBotContext( + userLocale: string | undefined, + systemLocale: string | undefined, + options?: FlowLocaleBotContextOptions ): BotContext { - let currentSystemLocale = systemLocale + const session = createTestSession({ + user: { + locale: + userLocale !== undefined ? userLocale : TEST_DEFAULTS.LOCALE, + systemLocale: + systemLocale !== undefined ? systemLocale : TEST_DEFAULTS.LOCALE, + }, + }) + + const user = session.user + + if (userLocale === undefined) { + Object.assign(user, { locale: undefined }) + } + if (systemLocale === undefined) { + Object.assign(user, { system_locale: undefined }) + } + + let currentSystemLocale = user.system_locale as string | undefined + + const base = createTestBotContext({ + session, + ...(options?.languageDetectionEnabled === false + ? { settings: createTestSettings({ LANGUAGE_DETECTION_ENABLED: '' }) } + : {}), + }) return { - session: { - user: { - locale: userLocale, - system_locale: systemLocale, - }, - }, - getUserLocale: () => userLocale, + ...base, + getUserLocale: () => user.locale as string | undefined, getSystemLocale: () => currentSystemLocale, setSystemLocale: (locale: string) => { currentSystemLocale = locale + user.system_locale = locale }, - } as unknown as BotContext + } as BotContext } describe('FlowLocale.resolve()', () => { describe('when user locale matches exactly', () => { test('should return the user locale when it matches a flow locale', () => { - const botContext = createMockBotContext('es', 'en') + const botContext = createFlowLocaleBotContext('es', 'en') const flowLocales = ['en', 'es', 'fr'] const defaultLocaleCode = 'en' @@ -43,7 +80,7 @@ describe('FlowLocale.resolve()', () => { }) test('should set system locale when user locale is resolved', () => { - const botContext = createMockBotContext('fr', 'en') + const botContext = createFlowLocaleBotContext('fr', 'en') const flowLocales = ['en', 'es', 'fr'] const defaultLocaleCode = 'en' @@ -61,7 +98,7 @@ describe('FlowLocale.resolve()', () => { describe('when user locale has language-region format', () => { test('should resolve to language code when exact locale not found but language is available', () => { - const botContext = createMockBotContext('es-ES', 'en') + const botContext = createFlowLocaleBotContext('es-ES', 'en') const flowLocales = ['en', 'es', 'fr'] const defaultLocaleCode = 'en' @@ -77,7 +114,7 @@ describe('FlowLocale.resolve()', () => { }) test('should set system locale to resolved language', () => { - const botContext = createMockBotContext('fr-CA', 'en') + const botContext = createFlowLocaleBotContext('fr-CA', 'en') const flowLocales = ['en', 'es', 'fr'] const defaultLocaleCode = 'en' @@ -93,7 +130,7 @@ describe('FlowLocale.resolve()', () => { }) test('should prefer exact match over language extraction', () => { - const botContext = createMockBotContext('pt-BR', 'en') + const botContext = createFlowLocaleBotContext('pt-BR', 'en') const flowLocales = ['en', 'pt-BR', 'pt'] const defaultLocaleCode = 'en' @@ -111,7 +148,7 @@ describe('FlowLocale.resolve()', () => { describe('when system locale matches', () => { test('should return default locale when user locale does not match and is different from system locale', () => { - const botContext = createMockBotContext('de', 'es') + const botContext = createFlowLocaleBotContext('de', 'es') const flowLocales = ['en', 'es', 'fr'] const defaultLocaleCode = 'en' @@ -129,7 +166,7 @@ describe('FlowLocale.resolve()', () => { describe('when falling back to default locale', () => { test('should return default locale when no match found', () => { - const botContext = createMockBotContext('de', 'it') + const botContext = createFlowLocaleBotContext('de', 'it') const flowLocales = ['en', 'es', 'fr'] const defaultLocaleCode = 'en' @@ -145,7 +182,7 @@ describe('FlowLocale.resolve()', () => { }) test('should set system locale to default when falling back', () => { - const botContext = createMockBotContext('de', 'it') + const botContext = createFlowLocaleBotContext('de', 'it') const flowLocales = ['en', 'es', 'fr'] const defaultLocaleCode = 'fr' @@ -161,7 +198,7 @@ describe('FlowLocale.resolve()', () => { }) test('should return "en" when default locale code is empty', () => { - const botContext = createMockBotContext('de', 'it') + const botContext = createFlowLocaleBotContext('de', 'it') const flowLocales = ['en', 'es', 'fr'] const defaultLocaleCode = '' @@ -179,7 +216,7 @@ describe('FlowLocale.resolve()', () => { describe('edge cases', () => { test('should handle empty flow locales array', () => { - const botContext = createMockBotContext('es', 'en') + const botContext = createFlowLocaleBotContext('es', 'en') const flowLocales: string[] = [] const defaultLocaleCode = 'en' @@ -195,10 +232,7 @@ describe('FlowLocale.resolve()', () => { }) test('should handle undefined user locale', () => { - const botContext = createMockBotContext( - undefined as unknown as string, - 'es' - ) + const botContext = createFlowLocaleBotContext(undefined, 'es') const flowLocales = ['en', 'es', 'fr'] const defaultLocaleCode = 'en' @@ -214,10 +248,7 @@ describe('FlowLocale.resolve()', () => { }) test('should handle undefined system locale', () => { - const botContext = createMockBotContext( - 'de', - undefined as unknown as string - ) + const botContext = createFlowLocaleBotContext('de', undefined) const flowLocales = ['en', 'es', 'fr'] const defaultLocaleCode = 'fr' @@ -233,7 +264,7 @@ describe('FlowLocale.resolve()', () => { }) test('should handle locale with multiple dashes', () => { - const botContext = createMockBotContext('zh-Hans-CN', 'en') + const botContext = createFlowLocaleBotContext('zh-Hans-CN', 'en') const flowLocales = ['en', 'zh'] const defaultLocaleCode = 'en' @@ -249,7 +280,7 @@ describe('FlowLocale.resolve()', () => { }) test('should be case sensitive when matching locales', () => { - const botContext = createMockBotContext('ES', 'EN') + const botContext = createFlowLocaleBotContext('ES', 'EN') const flowLocales = ['en', 'es', 'fr'] const defaultLocaleCode = 'en' @@ -267,7 +298,7 @@ describe('FlowLocale.resolve()', () => { describe('when locales share language but differ in specificity', () => { test('should prefer system locale with region over generic user locale', () => { - const botContext = createMockBotContext('es', 'es-MX') + const botContext = createFlowLocaleBotContext('es', 'es-MX') const flowLocales = ['es', 'es-MX'] const defaultLocaleCode = 'en' @@ -281,7 +312,7 @@ describe('FlowLocale.resolve()', () => { }) test('should fall back to generic locale when specific system locale not in flow', () => { - const botContext = createMockBotContext('es', 'es-MX') + const botContext = createFlowLocaleBotContext('es', 'es-MX') const flowLocales = ['es'] const defaultLocaleCode = 'en' @@ -295,7 +326,7 @@ describe('FlowLocale.resolve()', () => { }) test('should prefer user locale with region over generic system locale', () => { - const botContext = createMockBotContext('es-ES', 'es') + const botContext = createFlowLocaleBotContext('es-ES', 'es') const flowLocales = ['es-ES', 'es'] const defaultLocaleCode = 'en' @@ -309,7 +340,7 @@ describe('FlowLocale.resolve()', () => { }) test('should use user locale when both have regions for the same language', () => { - const botContext = createMockBotContext('es-MX', 'es-CO') + const botContext = createFlowLocaleBotContext('es-MX', 'es-CO') const flowLocales = ['es-MX', 'es-CO'] const defaultLocaleCode = 'en' @@ -323,7 +354,7 @@ describe('FlowLocale.resolve()', () => { }) test('should not apply specificity logic when languages differ', () => { - const botContext = createMockBotContext('fr', 'es-MX') + const botContext = createFlowLocaleBotContext('fr', 'es-MX') const flowLocales = ['fr', 'es-MX'] const defaultLocaleCode = 'en' @@ -337,7 +368,7 @@ describe('FlowLocale.resolve()', () => { }) test('should set system locale to the more specific resolved locale', () => { - const botContext = createMockBotContext('es', 'es-MX') + const botContext = createFlowLocaleBotContext('es', 'es-MX') const flowLocales = ['es-MX'] const defaultLocaleCode = 'en' @@ -349,7 +380,7 @@ describe('FlowLocale.resolve()', () => { describe('priority order', () => { test('should prioritize user locale over system locale', () => { - const botContext = createMockBotContext('fr', 'es') + const botContext = createFlowLocaleBotContext('fr', 'es') const flowLocales = ['en', 'es', 'fr'] const defaultLocaleCode = 'en' @@ -364,4 +395,34 @@ describe('FlowLocale.resolve()', () => { expect(result).toBe('fr') }) }) + + describe('when LANGUAGE_DETECTION_ENABLED is falsy', () => { + test('should resolve using system locale only, ignoring user vs system priority', () => { + const botContext = createFlowLocaleBotContext('fr', 'es', { + languageDetectionEnabled: false, + }) + const flowLocales = ['en', 'es', 'fr'] + const defaultLocaleCode = 'en' + + const result = new FlowLocale( + botContext, + flowLocales, + defaultLocaleCode + ).resolve() + + expect(result).toBe('es') + }) + + test('should set system locale from flow match derived from current system locale', () => { + const botContext = createFlowLocaleBotContext('es', 'fr', { + languageDetectionEnabled: false, + }) + const flowLocales = ['en', 'fr', 'de'] + const defaultLocaleCode = 'en' + + new FlowLocale(botContext, flowLocales, defaultLocaleCode).resolve() + + expect(botContext.getSystemLocale()).toBe('fr') + }) + }) }) From 0aa7f5fc2eee443b051fbbed4811378e612ae98f Mon Sep 17 00:00:00 2001 From: Oriol Raventos Date: Mon, 27 Apr 2026 18:18:24 +0200 Subject: [PATCH 7/8] refactor: LANGUAGE_DETECTION_ENABLED is a boolean --- packages/botonic-core/src/core-bot.ts | 6 +++--- packages/botonic-core/src/models/legacy-types.ts | 2 +- packages/botonic-core/src/testing/index.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/botonic-core/src/core-bot.ts b/packages/botonic-core/src/core-bot.ts index da655ebe6a..b7040a05f1 100644 --- a/packages/botonic-core/src/core-bot.ts +++ b/packages/botonic-core/src/core-bot.ts @@ -97,7 +97,7 @@ export class CoreBot { } private getBotContext(request: BotRequest): BotContext { - const { input, session, lastRoutePath } = request + const { input, session, lastRoutePath, settings, secrets } = request return { input, session, @@ -114,8 +114,8 @@ export class CoreBot { setUserLocale: (locale: string) => this.setUserLocale(locale, session), setSystemLocale: (locale: string) => this.setSystemLocale(locale, session), - settings: request.settings, - secrets: request.secrets, + settings, + secrets, } } diff --git a/packages/botonic-core/src/models/legacy-types.ts b/packages/botonic-core/src/models/legacy-types.ts index 145fb50a6b..3a332af9a7 100644 --- a/packages/botonic-core/src/models/legacy-types.ts +++ b/packages/botonic-core/src/models/legacy-types.ts @@ -345,7 +345,7 @@ export interface BotSettings { LITELLM_API_URL: string AZURE_OPENAI_API_BASE: string AZURE_OPENAI_API_VERSION: string - LANGUAGE_DETECTION_ENABLED: string + LANGUAGE_DETECTION_ENABLED: boolean CUSTOM_SHORT_URL_HOST: string | null custom: Record } diff --git a/packages/botonic-core/src/testing/index.ts b/packages/botonic-core/src/testing/index.ts index 1abfd26faa..6e5c130b8d 100644 --- a/packages/botonic-core/src/testing/index.ts +++ b/packages/botonic-core/src/testing/index.ts @@ -23,7 +23,7 @@ export const TEST_DEFAULTS = { LITELLM_API_URL: 'https://api.litellm.com', AZURE_OPENAI_API_BASE: 'https://api.openai.com', AZURE_OPENAI_API_VERSION: '2026-02-01', - LANGUAGE_DETECTION_ENABLED: 'true', + LANGUAGE_DETECTION_ENABLED: true, HUBTYPE_ACCESS_TOKEN: 'testAccessToken', // pragma: allowlist secret LITELLM_API_KEY: 'testLiteLLMAPIKey', // pragma: allowlist secret AZURE_OPENAI_API_KEY: 'testAzureOpenAIAPIKey', // pragma: allowlist secret From 0aca2db041656af43ab5513705d270808c52cb3f Mon Sep 17 00:00:00 2001 From: Oriol Raventos Date: Mon, 27 Apr 2026 18:20:29 +0200 Subject: [PATCH 8/8] refactor: LANGUAGE_DETECTION_ENABLED maybe undefined --- packages/botonic-core/src/models/legacy-types.ts | 2 +- .../botonic-plugin-flow-builder/tests/flow-locale.test.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/botonic-core/src/models/legacy-types.ts b/packages/botonic-core/src/models/legacy-types.ts index 3a332af9a7..1f0a713a63 100644 --- a/packages/botonic-core/src/models/legacy-types.ts +++ b/packages/botonic-core/src/models/legacy-types.ts @@ -345,7 +345,7 @@ export interface BotSettings { LITELLM_API_URL: string AZURE_OPENAI_API_BASE: string AZURE_OPENAI_API_VERSION: string - LANGUAGE_DETECTION_ENABLED: boolean + LANGUAGE_DETECTION_ENABLED?: boolean CUSTOM_SHORT_URL_HOST: string | null custom: Record } diff --git a/packages/botonic-plugin-flow-builder/tests/flow-locale.test.ts b/packages/botonic-plugin-flow-builder/tests/flow-locale.test.ts index dbf38c4d8b..e708bba990 100644 --- a/packages/botonic-plugin-flow-builder/tests/flow-locale.test.ts +++ b/packages/botonic-plugin-flow-builder/tests/flow-locale.test.ts @@ -25,8 +25,7 @@ function createFlowLocaleBotContext( ): BotContext { const session = createTestSession({ user: { - locale: - userLocale !== undefined ? userLocale : TEST_DEFAULTS.LOCALE, + locale: userLocale !== undefined ? userLocale : TEST_DEFAULTS.LOCALE, systemLocale: systemLocale !== undefined ? systemLocale : TEST_DEFAULTS.LOCALE, }, @@ -46,7 +45,7 @@ function createFlowLocaleBotContext( const base = createTestBotContext({ session, ...(options?.languageDetectionEnabled === false - ? { settings: createTestSettings({ LANGUAGE_DETECTION_ENABLED: '' }) } + ? { settings: createTestSettings({ LANGUAGE_DETECTION_ENABLED: false }) } : {}), })