From 69ff295184e906e47b0d175bdb74db0a80fcaa62 Mon Sep 17 00:00:00 2001 From: Allan Braun Date: Sun, 25 Oct 2026 17:14:16 -0300 Subject: [PATCH] fix(assistant): answer Brazilian Portuguese users in Portuguese, not Breton TREK's code for Brazilian Portuguese is 'br', which Intl reads as Breton. The assistant passed it straight to Intl.DisplayNames for the system prompt's answer language and to the card formatters, so the model was told to answer in Breton. Convert TREK codes to their BCP-47 tag with getIntlLanguage before handing them to Intl. Closes #2800 --- server/src/nest/assistant/assistant-context.ts | 3 ++- server/src/nest/assistant/assistant-words.ts | 9 +++++++++ server/src/nest/assistant/cards/preview-format.ts | 3 ++- .../tests/unit/nest/assistant/assistant-context.test.ts | 6 ++++++ .../nest/assistant/tool-call-preview.helpers.test.ts | 1 + 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/server/src/nest/assistant/assistant-context.ts b/server/src/nest/assistant/assistant-context.ts index d572edb72..e3a5d9914 100644 --- a/server/src/nest/assistant/assistant-context.ts +++ b/server/src/nest/assistant/assistant-context.ts @@ -1,3 +1,4 @@ +import { intlLocale } from './assistant-words'; import { fileKeyOf } from './cards/card-values'; import type { AssistantFocus, AssistantMention } from '@trek/shared'; @@ -412,7 +413,7 @@ export function todayIn(timeZone: string, now: Date): string { /** The English name of a locale's language, for the instruction to answer in it. */ export function languageName(locale: string): string { try { - return new Intl.DisplayNames(['en'], { type: 'language' }).of(locale) ?? locale; + return new Intl.DisplayNames(['en'], { type: 'language' }).of(intlLocale(locale)) ?? locale; } catch { return locale; } diff --git a/server/src/nest/assistant/assistant-words.ts b/server/src/nest/assistant/assistant-words.ts index cb6b44af4..a27890af6 100644 --- a/server/src/nest/assistant/assistant-words.ts +++ b/server/src/nest/assistant/assistant-words.ts @@ -17,6 +17,15 @@ export function supportedLocale(locale: string): string { return SUPPORTED_LANGUAGE_CODES.includes(language) ? language : 'en'; } +/** + * The BCP 47 tag of a request's locale, for Intl: a TREK code reads as its + * language ('br' is Brazilian Portuguese here, Breton to Intl), anything else + * is passed through. + */ +export function intlLocale(locale: string): string { + return SUPPORTED_LANGUAGE_CODES.includes(locale) ? getIntlLanguage(locale) : locale; +} + /** The words of one locale's table, or none when it cannot be read. */ export async function localeTable(code: string): Promise { try { diff --git a/server/src/nest/assistant/cards/preview-format.ts b/server/src/nest/assistant/cards/preview-format.ts index 960458ff1..afc814114 100644 --- a/server/src/nest/assistant/cards/preview-format.ts +++ b/server/src/nest/assistant/cards/preview-format.ts @@ -1,3 +1,4 @@ +import { intlLocale } from '../assistant-words'; import type { PreviewContext } from './card-types'; import type { AssistantChangeLabelKey, AssistantChangeRow } from '@trek/shared'; @@ -57,7 +58,7 @@ export class Rows { /** A locale Intl accepts, the user's when it can. */ export function localeOf(ctx: Pick): string { try { - return Intl.getCanonicalLocales(ctx.locale)[0] ?? 'en'; + return Intl.getCanonicalLocales(intlLocale(ctx.locale))[0] ?? 'en'; } catch { return 'en'; } diff --git a/server/tests/unit/nest/assistant/assistant-context.test.ts b/server/tests/unit/nest/assistant/assistant-context.test.ts index 2f0e6113b..ecb84dc07 100644 --- a/server/tests/unit/nest/assistant/assistant-context.test.ts +++ b/server/tests/unit/nest/assistant/assistant-context.test.ts @@ -231,6 +231,12 @@ describe('the turn context', () => { expect(languageName('!!')).toBe('!!'); }); + it('ASTCTX-033: reads TREK language codes as their language, not as the BCP 47 tag they spell', () => { + expect(languageName('br')).toBe('Brazilian Portuguese'); + expect(languageName('gr')).toBe('Greek'); + expect(languageName('pt-BR')).toBe('Brazilian Portuguese'); + }); + it('ASTCTX-022: carries the date, the units the user prefers and the card', () => { const card = buildTripCard(1, 7, SUMMARY, PLACES, null); const context = turnContext(card, 'Asia/Tokyo', new Date('2026-06-11T23:30:00Z'), { diff --git a/server/tests/unit/nest/assistant/tool-call-preview.helpers.test.ts b/server/tests/unit/nest/assistant/tool-call-preview.helpers.test.ts index b0099d89b..e2bf76a42 100644 --- a/server/tests/unit/nest/assistant/tool-call-preview.helpers.test.ts +++ b/server/tests/unit/nest/assistant/tool-call-preview.helpers.test.ts @@ -69,6 +69,7 @@ describe('dayLabel', () => { expect(dayLabel(ctx, 99)).toBe('#99'); expect(dayLabel({ ...ctx, locale: 'de' }, 32)).toBe('Fr., 12. Juni'); expect(dayLabel({ ...ctx, locale: 'not a locale!' }, 32)).toBe('Fri, Jun 12'); + expect(dayLabel({ ...ctx, locale: 'br' }, 32)).toBe(dayLabel({ ...ctx, locale: 'pt-BR' }, 32)); }); });