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)); }); });