From 9064436230e0c1f6975850b343a17381ffe996c9 Mon Sep 17 00:00:00 2001 From: Xule Lin <43122877+linxule@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:22:06 +0200 Subject: [PATCH] fix(i18n): draw CJK dashes and ellipses from a local CJK font (5.4.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In zh/ja text the Latin web fonts supplied U+2014/U+2015/U+2026, so 我同意——开始访谈 showed two separated short dashes and …… Latin dots. On :lang(zh) and :lang(ja), a local()-only face with unicode-range U+2014-2015, U+2026 now comes first in the sans, serif and mono stacks; nothing is downloaded and English is unchanged. Researcher transcript turns carry the interview language as lang, so the fix (and screen-reader pronunciation) applies there too. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 4 ++-- docs/releases/v5.4.1.md | 23 ++++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- src/app/globals.css | 34 ++++++++++++++++++++++++++++++ src/components/InterviewDetail.tsx | 3 ++- tests/unit/selfHostedFonts.test.ts | 13 ++++++++++++ 7 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 docs/releases/v5.4.1.md diff --git a/README.md b/README.md index 3f17af0..e9bca64 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ OpenInterviewer is an open-source platform for adaptive, AI-assisted qualitative interviews. Researchers configure a study, share an opaque participant link, and review transcripts and synthesis in a dashboard. An AI interviewer conducts each interview, and the transcript is saved before any analysis runs. -The latest release is 5.4.0, which adds on-device voice input and analysis files in the study export. Read the [5.4.0 release notes](docs/releases/v5.4.0.md) or [all release notes](docs/releases/). +The latest release is 5.4.1, which fixes how Chinese and Japanese dashes display. Version 5.4.0 added on-device voice input and analysis files in the study export. Read the [5.4.1](docs/releases/v5.4.1.md) and [5.4.0](docs/releases/v5.4.0.md) release notes, or [all release notes](docs/releases/). The project's own instance is [open-interview.linxule.com](https://open-interview.linxule.com). It is a standalone installation on Cloudflare, with an EU-jurisdiction workspace and provider calls through its own Cloudflare AI Gateway. The former address, `openinterviewer.vercel.app`, redirects there. @@ -133,7 +133,7 @@ For contributors: - [Development and verification](docs/development.md): checks, test lanes, live-provider smoke test, releases, provider API and model contract, and project structure - [Contributing guide](CONTRIBUTING.md) and [agent and architecture guide](AGENTS.md) -- [Release notes](docs/releases/), latest [5.4.0](docs/releases/v5.4.0.md) +- [Release notes](docs/releases/), latest [5.4.1](docs/releases/v5.4.1.md) - [Translation review, October 2026](docs/translations/REVIEW-2026-10.md) ## Acknowledgments diff --git a/docs/releases/v5.4.1.md b/docs/releases/v5.4.1.md new file mode 100644 index 0000000..befab4b --- /dev/null +++ b/docs/releases/v5.4.1.md @@ -0,0 +1,23 @@ +# OpenInterviewer 5.4.1 + +Chinese and Japanese dashes and ellipses now display correctly. Before this fix, +"——" in a Chinese sentence or button, such as 我同意——开始访谈, showed as two +short, separated dashes, and "……" showed as Latin dots. + +## What changed + +- On Chinese and Japanese participant pages, the dash (U+2014, U+2015) and the + ellipsis (U+2026) are drawn from a CJK font already installed on the device, + such as PingFang, Hiragino, Microsoft YaHei or Noto Sans CJK. All other + characters keep the site's fonts, and nothing new is downloaded. +- The fix covers fixed screens, the researcher's consent text and the AI + interviewer's replies. +- In the researcher's transcript view, each turn is marked with the interview + language. The same dashes display correctly there, and screen readers read the + turn in the right language. +- English and the other interface languages are unchanged. + +## Upgrade and rollback + +A code-only release, with no schema, configuration or binding changes. Rolling +back to 5.4.0 is safe. diff --git a/package-lock.json b/package-lock.json index 96746b7..d62aff8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openinterviewer", - "version": "5.4.0", + "version": "5.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openinterviewer", - "version": "5.4.0", + "version": "5.4.1", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.131.0", diff --git a/package.json b/package.json index 4400b0d..b11bdf6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openinterviewer", - "version": "5.4.0", + "version": "5.4.1", "description": "Open-source platform for adaptive, AI-assisted qualitative interviews.", "private": true, "license": "MIT", diff --git a/src/app/globals.css b/src/app/globals.css index c42121a..4b0a76d 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -25,6 +25,40 @@ --shadow-note: var(--note-shadow); } +/* + CJK dashes and ellipses. In Chinese and Japanese text the Latin web fonts + supply U+2014/U+2015 and U+2026, so "——" draws as two short, separated + dashes and "……" as Latin dots. On zh/ja content, take only those code points + from the system CJK font (unicode-range), so they join as the reader expects. + Applies to participant pages (html lang is set from the interview language) + and to any element marked with lang="zh-…" or lang="ja". +*/ +@font-face { + font-family: 'OI CJK Punctuation SC'; + src: local('PingFang SC'), local('PingFangSC-Regular'), local('Hiragino Sans GB'), local('Microsoft YaHei'), + local('Noto Sans CJK SC'), local('NotoSansCJKsc-Regular'), local('Source Han Sans SC'), local('Noto Sans SC'); + unicode-range: U+2014-2015, U+2026; +} + +@font-face { + font-family: 'OI CJK Punctuation JP'; + src: local('Hiragino Sans'), local('HiraginoSans-W3'), local('Hiragino Kaku Gothic ProN'), local('Yu Gothic'), + local('Meiryo'), local('Noto Sans CJK JP'), local('NotoSansCJKjp-Regular'), local('Source Han Sans JP'); + unicode-range: U+2014-2015, U+2026; +} + +:lang(zh) { + --font-sans: 'OI CJK Punctuation SC', var(--typeface-sans), system-ui, sans-serif; + --font-serif: 'OI CJK Punctuation SC', var(--typeface-serif), Georgia, serif; + --font-mono: 'OI CJK Punctuation SC', var(--typeface-mono), ui-monospace, monospace; +} + +:lang(ja) { + --font-sans: 'OI CJK Punctuation JP', var(--typeface-sans), system-ui, sans-serif; + --font-serif: 'OI CJK Punctuation JP', var(--typeface-serif), Georgia, serif; + --font-mono: 'OI CJK Punctuation JP', var(--typeface-mono), ui-monospace, monospace; +} + /* The default border color has changed to `currentcolor` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still diff --git a/src/components/InterviewDetail.tsx b/src/components/InterviewDetail.tsx index 4040ed5..fb8a122 100644 --- a/src/components/InterviewDetail.tsx +++ b/src/components/InterviewDetail.tsx @@ -10,6 +10,7 @@ import { InterviewAnalysisPanel } from '@/components/analysis/InterviewAnalysisP import { useInterviewAnalysis } from '@/components/analysis/useInterviewAnalysis'; import { useSetTrailingCrumb } from '@/components/shell/breadcrumb'; import { cn } from '@/lib/cn'; +import { LANGUAGE_TAGS } from '@/lib/i18n/languages'; interface InterviewDetailProps { interviewId: string; @@ -297,7 +298,7 @@ const InterviewDetail: React.FC = ({ interviewId, studyId, showCoordinate className="mt-1" > -
+
{msg.content}
diff --git a/tests/unit/selfHostedFonts.test.ts b/tests/unit/selfHostedFonts.test.ts index 194d0d8..86738ab 100644 --- a/tests/unit/selfHostedFonts.test.ts +++ b/tests/unit/selfHostedFonts.test.ts @@ -58,4 +58,17 @@ describe('self-hosted fonts', () => { expect(latin).toContain(`{ prop: 'font-family', value: "'${family}'" }`); } }); + it('draws CJK dashes and ellipses from a local CJK font on zh and ja content only', () => { + const theme = readFileSync(path.join(process.cwd(), 'src/app/globals.css'), 'utf8'); + for (const [selector, face] of [[':lang(zh)', 'OI CJK Punctuation SC'], [':lang(ja)', 'OI CJK Punctuation JP']]) { + const block = theme.slice(theme.indexOf(`${selector} {`), theme.indexOf('}', theme.indexOf(`${selector} {`))); + for (const variable of ['sans', 'serif', 'mono']) { + expect(block).toContain(`--font-${variable}: '${face}', var(--typeface-${variable}),`); + } + const faceRule = theme.slice(theme.indexOf(`font-family: '${face}';`)); + // Only the dash and ellipsis code points, and only fonts already on the device (no download). + expect(faceRule.slice(0, faceRule.indexOf('}'))).toContain('unicode-range: U+2014-2015, U+2026;'); + expect(faceRule.slice(0, faceRule.indexOf('}'))).not.toContain('url('); + } + }); });