-
Notifications
You must be signed in to change notification settings - Fork 0
feat(wiki-compose): add Japanese UI and localized LLM output #985
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
54 changes: 54 additions & 0 deletions
54
server/api/src/__tests__/agents/core/composeLocale.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { | ||
| composeConflictRationale, | ||
| composeContentLocaleInstruction, | ||
| normalizeComposeContentLocale, | ||
| readContentLocaleFromSessionMetadata, | ||
| resolveComposeContentLocale, | ||
| resolveSessionContentLocale, | ||
| stripContentLocaleFromGraphInput, | ||
| structureDialogueFallbackOutline, | ||
| } from "../../../agents/core/composeLocale.js"; | ||
|
|
||
| describe("composeLocale", () => { | ||
| it("normalizes supported locales only", () => { | ||
| expect(normalizeComposeContentLocale("ja")).toBe("ja"); | ||
| expect(normalizeComposeContentLocale("en")).toBe("en"); | ||
| expect(normalizeComposeContentLocale("fr")).toBeNull(); | ||
| }); | ||
|
|
||
| it("resolves from input then Accept-Language", () => { | ||
| expect(resolveComposeContentLocale({ contentLocale: "en" }, "ja-JP")).toBe("en"); | ||
| expect(resolveComposeContentLocale({}, "ja-JP,en;q=0.8")).toBe("ja"); | ||
| expect(resolveComposeContentLocale({}, "en-US,en;q=0.9")).toBe("en"); | ||
| expect(resolveComposeContentLocale({}, "zh-CN,en-US;q=0.9")).toBe("en"); | ||
| expect(resolveComposeContentLocale({}, null, "ja")).toBe("ja"); | ||
| }); | ||
|
|
||
| it("strips contentLocale from graph input", () => { | ||
| expect(stripContentLocaleFromGraphInput({ contentLocale: "ja", chatSeed: { x: 1 } })).toEqual({ | ||
| chatSeed: { x: 1 }, | ||
| }); | ||
| expect(stripContentLocaleFromGraphInput(null)).toBeNull(); | ||
| expect(stripContentLocaleFromGraphInput(undefined)).toBeUndefined(); | ||
| }); | ||
|
|
||
| it("persists locale via session metadata resolution", () => { | ||
| expect(readContentLocaleFromSessionMetadata({ contentLocale: "en" })).toBe("en"); | ||
| expect( | ||
| resolveSessionContentLocale({ contentLocale: "en" }, { contentLocale: "ja" }, "ja-JP"), | ||
| ).toBe("en"); | ||
| expect(resolveSessionContentLocale(null, { contentLocale: "ja" }, "en-US")).toBe("ja"); | ||
| }); | ||
|
|
||
| it("includes Japanese in locale instruction when ja", () => { | ||
| expect(composeContentLocaleInstruction("ja")).toMatch(/Japanese/); | ||
| expect(composeContentLocaleInstruction("en")).toMatch(/English/); | ||
| }); | ||
|
|
||
| it("provides localized conflict rationale and fallback outline", () => { | ||
| expect(composeConflictRationale("ja")).toMatch(/却下/); | ||
| expect(structureDialogueFallbackOutline("ja")[0]?.heading).toBe("概要"); | ||
| expect(structureDialogueFallbackOutline("en")[0]?.heading).toBe("Overview"); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| /** | ||
| * Content locale for Wiki Compose LLM outputs (#950). | ||
| * Wiki Compose の生成言語(ユーザー向けテキスト)を表す。 | ||
| */ | ||
| import { resolveLocaleFromAcceptLanguage } from "../../services/invitationService.js"; | ||
|
|
||
| export type ComposeContentLocale = "ja" | "en"; | ||
|
|
||
| /** | ||
| * Normalize an arbitrary value to a supported compose content locale. | ||
| * 任意の入力をサポートされる compose 用ロケールに正規化する。 | ||
| */ | ||
| export function normalizeComposeContentLocale(raw: unknown): ComposeContentLocale | null { | ||
| if (raw === "ja" || raw === "en") return raw; | ||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Resolve locale from graph run input (`contentLocale`) with a server default. | ||
| * graph の run input(`contentLocale`)からロケールを解決する。 | ||
| */ | ||
| export function resolveComposeContentLocale( | ||
| input: unknown, | ||
| acceptLanguage: string | undefined | null, | ||
| fallback: ComposeContentLocale = "ja", | ||
| ): ComposeContentLocale { | ||
| if (input && typeof input === "object" && "contentLocale" in input) { | ||
| const fromInput = normalizeComposeContentLocale( | ||
| (input as { contentLocale?: unknown }).contentLocale, | ||
| ); | ||
| if (fromInput) return fromInput; | ||
| } | ||
| const fromHeader = resolveLocaleFromAcceptLanguage(acceptLanguage); | ||
| if (fromHeader) return fromHeader; | ||
| return fallback; | ||
| } | ||
|
|
||
| /** | ||
| * Read persisted `contentLocale` from a compose session metadata blob. | ||
| * compose セッション metadata に保存された `contentLocale` を読む。 | ||
| */ | ||
| export function readContentLocaleFromSessionMetadata( | ||
| metadata: unknown, | ||
| ): ComposeContentLocale | null { | ||
| if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) return null; | ||
| return normalizeComposeContentLocale((metadata as Record<string, unknown>).contentLocale); | ||
| } | ||
|
|
||
| /** | ||
| * Resolve locale for a session: metadata (first run) → input → Accept-Language → fallback. | ||
| * セッション用ロケール解決: metadata(初回 run で固定)→ input → Accept-Language。 | ||
| */ | ||
| export function resolveSessionContentLocale( | ||
| metadata: unknown, | ||
| input: unknown, | ||
| acceptLanguage: string | undefined | null, | ||
| fallback: ComposeContentLocale = "ja", | ||
| ): ComposeContentLocale { | ||
| const persisted = readContentLocaleFromSessionMetadata(metadata); | ||
| if (persisted) return persisted; | ||
| return resolveComposeContentLocale(input, acceptLanguage, fallback); | ||
| } | ||
|
|
||
| /** | ||
| * Strip `contentLocale` before passing input to LangGraph (not a state channel). | ||
| * LangGraph に渡す前に `contentLocale` を除去する(state チャネルではない)。 | ||
| */ | ||
| export function stripContentLocaleFromGraphInput(raw: unknown): unknown { | ||
| if (raw === null || raw === undefined) return raw; | ||
| if (typeof raw !== "object" || Array.isArray(raw)) return raw; | ||
| const { contentLocale: _removed, ...rest } = raw as Record<string, unknown>; | ||
| return rest; | ||
| } | ||
|
|
||
| /** | ||
| * Instruction appended to system prompts so questions, outlines, and drafts | ||
| * match the user's UI language. | ||
| * 質問・アウトライン・本文が UI 言語と一致するよう system prompt に付与する。 | ||
| */ | ||
| export function composeContentLocaleInstruction(locale: ComposeContentLocale): string { | ||
| if (locale === "ja") { | ||
| return ( | ||
| "\n\nLanguage: Write all user-facing text (questions, option labels, rationales, " + | ||
| "outline headings and intents, evaluation rationales, missing aspects, and article " + | ||
| "body) in Japanese. Use clear, natural Japanese suitable for a wiki article." | ||
| ); | ||
| } | ||
| return ( | ||
| "\n\nLanguage: Write all user-facing text (questions, option labels, rationales, " + | ||
| "outline headings and intents, evaluation rationales, missing aspects, and article " + | ||
| "body) in English." | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Localized conflict-resolution rationale shown in the interrupt UI. | ||
| * interrupt UI に表示する矛盾解消用の説明文(ロケール別)。 | ||
| */ | ||
| export function composeConflictRationale(locale: ComposeContentLocale): string { | ||
| if (locale === "ja") { | ||
| return ( | ||
| "却下したソースと採用したソースが混在しています。採用したソースのセットで" + | ||
| "アウトライン生成に進んでよいか確認してください。" | ||
| ); | ||
| } | ||
| return ( | ||
| "Multiple sources were rejected while others were kept. Confirm you want to proceed " + | ||
| "with the approved set before generating the outline." | ||
| ); | ||
| } | ||
|
|
||
| /** Default outline rows when structure LLM fails (locale-specific headings). */ | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| export function structureDialogueFallbackOutline( | ||
| locale: ComposeContentLocale, | ||
| ): Array<{ heading: string; depth: 1; intent: string }> { | ||
| if (locale === "ja") { | ||
| return [ | ||
| { heading: "概要", depth: 1, intent: "トピックの簡潔な導入。" }, | ||
| { heading: "要点", depth: 1, intent: "主要な事実と背景。" }, | ||
| { heading: "参考", depth: 1, intent: "出典と関連情報。" }, | ||
| ]; | ||
| } | ||
| return [ | ||
| { heading: "Overview", depth: 1, intent: "Brief introduction to the topic." }, | ||
| { heading: "Key points", depth: 1, intent: "Main facts and context." }, | ||
| { heading: "References", depth: 1, intent: "Sources and further reading." }, | ||
| ]; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current implementation of
stripContentLocaleFromGraphInputuses!rawandraw ?? {}which silently coerces falsy values likenullorundefinedinto empty objects{}.\n\nTo make this utility function more robust and prevent unexpected type coercions, we should return the input as-is if it is not a non-null object or array.