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