Skip to content

feat: auto-translate blog and memo content via DeepSeek API#87

Merged
metrue merged 10 commits into
mainfrom
feature/auto-translate
Jul 2, 2026
Merged

feat: auto-translate blog and memo content via DeepSeek API#87
metrue merged 10 commits into
mainfrom
feature/auto-translate

Conversation

@metrue

@metrue metrue commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Summary

Adds automatic translation of blog posts and memos to the user's browser language using DeepSeek Chat API, with file-based caching.

Changes (10 files, +856/-131)

File Purpose
lib/translate.ts Server-side engine — DeepSeek Chat API + file-based caching in data/translations/
lib/translate.shared.ts Client-safe locale helpers (no Node.js deps)
app/api/translate/route.ts POST /api/translate — accepts {text, targetLocale, isMarkdown}
hooks/useTranslation.ts React hook — locale detection, API call, sessionStorage cache, show-original toggle
components/BlogPostContent.tsx Blog title + content auto-translated; indicator + toggle
components/MemoCard.tsx Memo content auto-translated; indicator + toggle
components/BlogCard.tsx Card listing titles auto-translated
.gitignore Ignore data/translations/ cache
.env.example Documents DEEPSEEK_APIKEY / DEEPSEEK_API_KEY

How it works

  1. next-intl detects the user's browser locale via Accept-Language
  2. Chinese readers (zh/zh-TW/zh-HK) → content shown as-is (no-op)
  3. Other localesuseTranslation hook calls /api/translate, which sends content to DeepSeek Chat with a markdown-preserving system prompt
  4. Translations cached server-side in data/translations/ (file) and client-side in sessionStorage
  5. Users see "Auto-translated to English" indicator with a "Show original" toggle

Clean-code applied

  • Functions extracted for single responsibility (buildTranslationPrompt, callDeepSeek, shouldFetch)
  • Magic numbers named (MAX_SINGLE_REQUEST_CHARS = 8_000)
  • Dead code removed (setTranslateFn, _translateFn)
  • Section-marker comments eliminated — code structure speaks for itself

Fixed

  • sessionStorage.getItem() in the translate-trigger effect was not wrapped in try-catch, causing the entire effect to silently crash during SSR — translation never started
  • Indicator now uses actuallyTranslated flag instead of needsTranslation — won't falsely claim "Auto-translated" when API key is missing or API returns unchanged text
  • Accepts both DEEPSEEK_APIKEY and DEEPSEEK_API_KEY env var names

Setup

Set either DEEPSEEK_APIKEY or DEEPSEEK_API_KEY in the Cofe Vercel project's Preview environment variables. Falls back gracefully (no-op) if unset.

Verification

  • ✅ Build compiles successfully
  • ✅ Lint clean (no warnings/errors)
  • ✅ All 142 tests pass
  • ✅ Manual testing: zh→en, zh→ja, zh→ko, zh→fr, zh→de — all working
  • ✅ Markdown syntax preserved (headings, code blocks, images, URLs)
  • ✅ Cache hits return in ~5ms

Adds automatic translation of blog posts and memos to the user's browser
language using DeepSeek Chat API, with file-based caching.

- lib/translate.ts — server-side engine using DeepSeek Chat API, with
  file-based caching in data/translations/
- lib/translate.shared.ts — client-safe locale helpers (shouldTranslate,
  localeToLabel) importable by browser components
- app/api/translate/route.ts — POST /api/translate endpoint, validates
  inputs (max 50K chars), returns translated text
- hooks/useTranslation.ts — React hook that auto-detects locale, calls
  the translate API, and caches in sessionStorage
- components/TranslateContent.tsx — alternative wrapper component
- components/BlogPostContent.tsx — title + content auto-translated,
  with translation indicator and show-original toggle
- components/MemoCard.tsx — memo content auto-translated, with toggle
- components/BlogCard.tsx — card listing titles auto-translated
- .gitignore — ignore data/translations/ cache
- .env.example — documents DEEPSEEK_APIKEY requirement

Chinese readers (zh/zh-TW/zh-HK) see the original content (no-op).
Translation gracefully falls back to identity when API key is unset.
@vercel

vercel Bot commented Jun 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cofe Ready Ready Preview, Comment Jul 2, 2026 8:19pm

metrue added 2 commits June 21, 2026 18:46
- Extract DeepSeek system prompt into buildTranslationPrompt()
- Extract fetch call into callDeepSeek() — one function, one concern
- Remove dead code: _translateFn variable, setTranslateFn export
- Replace section-marker comments with structural code ordering
- Extract magic 8000 into named MAX_SINGLE_REQUEST_CHARS constant
- Rename _cacheDir → cacheDir, getCacheDir → ensureCacheDir
- Inline redundant variable declarations for clarity
- Wrap sessionStorage.getItem() in try-catch in the second useEffect
  to prevent the effect from silently crashing during SSR
- Add actuallyTranslated flag to useTranslation hook so the UI
  only shows the indicator when a real translation occurred
- Accept DEEPSEEK_API_KEY as an alternative env var name for
  compatibility with Vercel conventions
- Update .env.example to document both accepted names
metrue added 6 commits June 22, 2026 07:38
TranslateContent duplicated the entire useTranslation hook (fetch,
sessionStorage caching, hashText) and was imported nowhere. The hook
is the single source of truth used by BlogPostContent, MemoCard, and
BlogCard.
- Correct the normalizeLocale doc comment (engine is DeepSeek, not
  Google Translate) and describe what it actually does.
- Merge the duplicate zh-HK/zh-TW branches in normalizeLocale.
- Drop the redundant second Chinese-locale check in shouldTranslate;
  normalizeLocale already collapses every variant into CHINESE_LOCALES.
  Verified behaviourally identical across 15 locale inputs.
Collapse the separate reset effect, translate-trigger effect, and
shouldFetch helper into a single resolution effect plus a stable
translate callback. Behaviour is unchanged: Chinese/empty content
shows the original, a cached translation is served immediately, and a
cache miss fetches while showing the original meanwhile.

Extract readSessionCache/writeSessionCache so every sessionStorage
access stays wrapped in try-catch (preserving the SSR-safety fix)
instead of being re-implemented inline in three places.
BlogPostContent and MemoCard had near-identical copies of the
translation status row (toggle button + auto-translated badge +
spinner). Extract a single TranslationIndicator that reproduces both
the verbose article ('full') and compact card ('compact') variants,
and owns the 'show only when in-flight or translated' guard.
- translate.shared: normalizeLocale, shouldTranslate, localeToLabel,
  getTranslatableLocales across Chinese variants and unknown codes.
- translate engine: blank no-op, cache miss calls DeepSeek and caches,
  cache hit skips the API, and missing API key falls back to the
  original text. Uses an isolated temp cache dir and a mocked fetch.
@metrue metrue merged commit a3ec98a into main Jul 2, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant