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
15 changes: 15 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,21 @@ This PR adds a proper Spanish branch in both blocks:
French CBT scaffolding stays as the explicit default fallback. No behavior change for any other language.

**Why:** Spanish was target-language #2 added to the app, but the original French edge function was never split. The bug only surfaced when the language-* consolidation reorganized the file. Catching it now keeps the granularity goal honest — coaching prompts should be in the target language, full stop.
---
### Bilingual rendering — dedupe identical sides, CTA gets Japanese + Chinese

**Fixes the "Write today / Write today" rendering when the active pair has no translation for either side.**

Two changes:

1. **`bilingual()` dedupes identical sides.** When `targetLang` and `primaryLang` resolve to the same string (typically because both fell back to English via the Translations fallback chain), the helper now returns the string once instead of `"X / X"`. Silent improvement for every call site across the app — no API change, no semantic change for pairs that legitimately differ (e.g. `fr / en`).

2. **HomeScreen CTA gets explicit Japanese, Simplified Chinese, and Traditional Chinese keys.** "Write today" / "Write another" now render correctly when the target is `ja` or the primary is `zh-Hans` / `zh-Hant`:
- ja: `今日書きましょう` / `もう一度書きましょう` (polite, inviting register)
- zh-Hans: `今天写日记` / `再写一篇`
- zh-Hant: `今天寫日記` / `再寫一篇`

**Why partial:** the CTA is the most visually prominent bilingual call on Home, so it gets translations now. The rest of the chrome (`Brain Dump`, `Thought Garden`, `French journaling practice`, `More tools`, etc.) is silently improved by the dedupe alone — they no longer say `"Brain Dump / Brain Dump"` when both sides fall back. A future translation-pass PR will add `ja:` and `zh-*:` keys to the remaining anchors.

### Japanese language support — target-only

Expand Down
18 changes: 16 additions & 2 deletions src/components/journal/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,22 @@ export function HomeScreen({ hasJournaledToday, streak, totalDays, totalWords, e
>
<Feather className="w-5 h-5 mr-2" />
{hasJournaledToday
? bilingual({ fr: 'Écrire encore', en: 'Write another', es: 'Escribir más' })
: bilingual({ fr: "Écrire aujourd'hui", en: 'Write today', es: 'Escribir hoy' })
? bilingual({
fr: 'Écrire encore',
en: 'Write another',
es: 'Escribir más',
ja: 'もう一度書きましょう',
'zh-Hans': '再写一篇',
'zh-Hant': '再寫一篇',
})
: bilingual({
fr: "Écrire aujourd'hui",
en: 'Write today',
es: 'Escribir hoy',
ja: '今日書きましょう',
'zh-Hans': '今天写日记',
'zh-Hant': '今天寫日記',
})
}
</Button>

Expand Down
31 changes: 31 additions & 0 deletions src/contexts/LanguageContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,37 @@ describe('bilingual() — formatted pair', () => {
expect(result.current.bilingual({ fr: 'Vide-tête', en: 'Brain Dump', es: 'Volcado mental' }))
.toBe('Vide-tête / Brain Dump');
});

it('dedupes when target and primary resolve to the same string', () => {
// target=ja primary=zh-Hant; neither key present → both fall back to en.
// bilingual() should render once, not "Write today / Write today".
localStorage.setItem(STORAGE_KEY, JSON.stringify({ primary: 'zh-Hant', target: 'ja' }));
const { result } = renderHook(() => useLanguage(), { wrapper });
expect(result.current.bilingual({ fr: "Écrire aujourd'hui", en: 'Write today', es: 'Escribir hoy' }))
.toBe('Write today');
});

it('does not dedupe when target and primary differ', () => {
// target=fr primary=en — both resolve and differ → keep the pair.
const { result } = renderHook(() => useLanguage(), { wrapper });
expect(result.current.bilingual({ fr: "Écrire aujourd'hui", en: 'Write today', es: 'Escribir hoy' }))
.toBe("Écrire aujourd'hui / Write today");
});

it('renders both when ja and zh-Hant keys are provided', () => {
// Same pair as the dedupe case, but now both keys are present → no fallback,
// no dedupe, both strings render.
localStorage.setItem(STORAGE_KEY, JSON.stringify({ primary: 'zh-Hant', target: 'ja' }));
const { result } = renderHook(() => useLanguage(), { wrapper });
expect(result.current.bilingual({
fr: "Écrire aujourd'hui",
en: 'Write today',
es: 'Escribir hoy',
ja: '今日書きましょう',
'zh-Hans': '今天写日记',
'zh-Hant': '今天寫日記',
})).toBe('今日書きましょう / 今天寫日記');
});
});

describe('stringFor() — dev-mode fallback warning', () => {
Expand Down
4 changes: 3 additions & 1 deletion src/contexts/LanguageContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,9 @@ export function LanguageProvider({ children }: { children: ReactNode }) {
const bilingual = (translations: Translations) => {
const tgt = stringFor(pair.target, translations);
const prm = stringFor(pair.primary, translations);
return `${tgt} / ${prm}`;
// Dedupe: when target and primary resolve to the same string (typically
// because both fell back to en), render once instead of "X / X".
return tgt === prm ? tgt : `${tgt} / ${prm}`;
};

return (
Expand Down
Loading