Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions __tests__/unit/cat/nudge-language-ignores-currency.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Nudge language used to be inferred from currency: no `profile.language` plus
* a CHF preference meant German. CHF is this platform's DEFAULT fiat, so that
* describes every account that never chose anything — and the founder, whose
* interface is English, opened his dashboard to a column of German nudge cards
* ("Lass „bitcoin" für dich arbeiten", "Biete „Handgemachte Keramiktasse" als
* Produkt an").
*
* A default is not a preference, and a currency is not a language — Switzerland
* has four national languages. This is the same category error `@/utils/locale`
* was written to end one layer down.
*/

import { resolveNudgeLanguage, NUDGE_COPY } from '@/services/cat/nudge-copy';

describe('nudge language', () => {
it('does not read a language out of the currency', () => {
for (const currency of ['CHF', 'EUR', 'USD', 'chf', null, undefined]) {
expect(resolveNudgeLanguage({ language: null, currency })).toBe('en');
}
});

it('defaults to the language the interface actually speaks', () => {
expect(resolveNudgeLanguage(null)).toBe('en');
expect(resolveNudgeLanguage(undefined)).toBe('en');
expect(resolveNudgeLanguage({})).toBe('en');
expect(resolveNudgeLanguage({ language: '', currency: 'CHF' })).toBe('en');
});

it('still honours an explicit choice, whatever the currency says', () => {
expect(resolveNudgeLanguage({ language: 'de', currency: 'USD' })).toBe('de');
expect(resolveNudgeLanguage({ language: 'de-CH', currency: 'USD' })).toBe('de');
expect(resolveNudgeLanguage({ language: 'en', currency: 'CHF' })).toBe('en');
});

it('keeps a copy table for every language it can return', () => {
expect(Object.keys(NUDGE_COPY).sort()).toEqual(['de', 'en']);
expect(NUDGE_COPY.en.languageName).toBe('English');
});
});
42 changes: 27 additions & 15 deletions src/services/cat/nudge-copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
* Nudge copy SSOT — every user-facing nudge string, per language.
*
* One language per user: the language is resolved ONCE from the user's profile
* (profile.language, falling back to a preferred-currency heuristic) and every
* nudge — deterministic templates AND LLM-written reasons — uses it. Mixed
* English/German cards on one dashboard were a founder-verified trust bug.
* (profile.language, and nothing else) and every nudge — deterministic
* templates AND LLM-written reasons — uses it. Mixed English/German cards on
* one dashboard were a founder-verified trust bug.
*/

import { ENTITY_REGISTRY, type EntityType } from '@/config/entity-registry';
Expand All @@ -13,23 +13,35 @@ export type NudgeLanguage = 'en' | 'de';

/**
* Resolve the ONE language all nudges for this user are written in.
* profile.language is authoritative; with no explicit language, an explicit
* CHF currency preference (Swiss-focused platform) implies German. Default English.
*
* `profile.language` — an explicit choice — is the only input. Currency is NOT
* a language signal, and reading it as one is why a founder with an English
* interface got a dashboard of German nudge cards: CHF is this platform's
* DEFAULT fiat (see config/currencies), so "prefers CHF" is what every account
* that never chose anything looks like. A default is not a preference. And
* Switzerland has four national languages, so even a deliberate CHF would not
* imply German.
*
* It is the same category error `@/utils/locale` was written to end one layer
* down — there it was "the language of the interface is not the language of
* your operating system"; here it is "…is not the currency you get paid in".
*
* Note the practical consequence: no surface in this app writes
* `profile.language` (the sole write is a mapper default of 'en'), and the
* interface ships `<html lang="en">` with no translations. So 'de' is
* currently unreachable, and NUDGE_COPY.de waits for real i18n rather than
* being selected by a heuristic nobody opted into. That is deliberate — German
* nudge cards inside an English interface are the mixed-language dashboard
* this file's header calls a trust bug, just at a larger grain.
*/
export function resolveNudgeLanguage(
// `currency` stays in the accepted shape because callers pass a whole
// profile — and because a regression test has to be able to hand this
// function the exact CHF-shaped profile that used to come back German.
profile: { language?: string | null; currency?: string | null } | null | undefined
): NudgeLanguage {
const lang = (profile?.language ?? '').toLowerCase();
if (lang.startsWith('de')) {
return 'de';
}
if (lang.startsWith('en')) {
return 'en';
}
if ((profile?.currency ?? '').toUpperCase() === 'CHF') {
return 'de';
}
return 'en';
return lang.startsWith('de') ? 'de' : 'en';
}

interface CopyBlock {
Expand Down
Loading