Skip to content
Open
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ npm run check:browser-first
## Internationalization

English is the source dictionary. Every key must exist in English, Simplified
Chinese, and Japanese with matching placeholders.
Chinese, Traditional Chinese, and Japanese with matching placeholders.

1. Add the English key to `src/i18n/locales/en.ts`.
2. Add translations to `zh-CN.ts` and `ja.ts`.
2. Add translations to `zh-CN.ts`, `zh-TW.ts`, and `ja.ts`.
3. Use `t("key")` in components; do not add user-facing hard-coded strings.
4. Run `npm run check:i18n`.

Expand Down Expand Up @@ -107,7 +107,7 @@ npm run check:mcp:live
For UI changes, verify:

- desktop, tablet, and phone layouts;
- English, Simplified Chinese, and Japanese;
- English, Simplified Chinese, Traditional Chinese, and Japanese;
- keyboard focus and Escape behavior for dialogs;
- reduced-motion behavior;
- the actual live app, not only static markup.
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -576,8 +576,8 @@ Codex should:
For an explicitly unattended or headless job, say so. Only then should Codex use the one-call
`create_deck` / `create` fast path.

A localized visual tutorial with copy-ready prompts in English, Simplified Chinese, and
Japanese is linked from the home page beneath the composer, and is also reachable directly
A localized visual tutorial with copy-ready prompts in English, Simplified Chinese,
Traditional Chinese, and Japanese is linked from the home page beneath the composer, and is also reachable directly
once the local app is running:

```text
Expand Down Expand Up @@ -676,7 +676,7 @@ Play mode, then export PPTX and PDF.
- **Play mode** — full-screen, dual-window presentation with keyboard navigation and speaker
notes.
- **PDF and PPTX export** — export after visual review.
- **English, Simplified Chinese, and Japanese UI** with strict key parity.
- **English, Simplified Chinese, Traditional Chinese, and Japanese UI** with strict key parity.
- **Portable Skill and typed MCP server** — the complete workflow remains available when one
transport is unavailable.

Expand Down
6 changes: 3 additions & 3 deletions mcp/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ register(
inputSchema: {
topic: z.string().min(1),
scenarioId: z.string().optional().describe("Scenario id from list_scenarios."),
uiLocale: z.enum(["zh-CN", "en", "ja"]).optional(),
uiLocale: z.enum(["zh-CN", "zh-TW", "en", "ja"]).optional(),
},
annotations: { readOnlyHint: true, openWorldHint: true },
},
Expand Down Expand Up @@ -702,7 +702,7 @@ register(
kind: z.enum(["chat", "outline", "render"]),
instruction: z.string().min(1).optional().describe("Required for chat and outline runs."),
conversationId: z.string().optional(),
locale: z.enum(["zh-CN", "en", "ja"]).optional(),
locale: z.enum(["zh-CN", "zh-TW", "en", "ja"]).optional(),
slideIndex: z.number().int().min(1).optional(),
materialIds: z.array(z.string()).max(8).optional(),
designFilePaths: z.array(z.string()).max(12).optional(),
Expand Down Expand Up @@ -1196,7 +1196,7 @@ register(
group: z.enum(["create", "transform", "data", "research", "optimize", "delivery"]).optional(),
query: z.string().optional(),
featured: z.boolean().optional(),
locale: z.enum(["zh-CN", "en", "ja"]).optional(),
locale: z.enum(["zh-CN", "zh-TW", "en", "ja"]).optional(),
scenarioId: z.string().optional().describe("Preselect this scenario in the returned Browser link."),
},
annotations: { readOnlyHint: true, openWorldHint: false },
Expand Down
2 changes: 1 addition & 1 deletion src/components/GlobalSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export default function GlobalSettings() {
onClick={() => setLocale(value)}
>
<span>
<b>{value === "zh-CN" ? "" : value === "ja" ? "日" : value.slice(0, 2).toUpperCase()}</b>
<b>{value === "zh-CN" ? "简" : value === "zh-TW" ? "繁" : value === "ja" ? "日" : value.slice(0, 2).toUpperCase()}</b>
<span>{LOCALE_LABEL[value]}</span>
</span>
{locale === value && <Check size={16} weight="bold" aria-hidden="true" />}
Expand Down
982 changes: 982 additions & 0 deletions src/i18n/locales/zh-TW.ts

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/i18n/messages.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { en } from "./locales/en";
import { ja } from "./locales/ja";
import { zhCN } from "./locales/zh-CN";
import { zhTW } from "./locales/zh-TW";
import { LOCALES, type Dict, type Locale, type MessageKey, type MessageValues, type UiLocale } from "./types";

export { LOCALES, LOCALE_LABEL, RTL_LOCALES } from "./types";
Expand All @@ -13,6 +14,7 @@ export const UI_LOCALE_STORAGE_KEY = "codex-slides-ui-locale";
export const DICTS: Record<Locale, Dict> = {
en,
"zh-CN": zhCN,
"zh-TW": zhTW,
ja,
};

Expand Down
5 changes: 3 additions & 2 deletions src/i18n/types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { Dict, MessageKey } from "./locales/en";

/** BCP-47 UI locales. Add a locale here, then register its dictionary in messages.ts. */
export type Locale = "en" | "zh-CN" | "ja";
export type Locale = "en" | "zh-CN" | "zh-TW" | "ja";
export type UiLocale = Locale;

export const LOCALES: Locale[] = ["en", "zh-CN", "ja"];
export const LOCALES: Locale[] = ["en", "zh-CN", "zh-TW", "ja"];
export const LOCALE_LABEL: Record<Locale, string> = {
en: "English",
"zh-CN": "简体中文",
"zh-TW": "繁體中文",
ja: "日本語",
};

Expand Down
8 changes: 5 additions & 3 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ export function buildOnboardPrompt(
const cats = CATEGORIES.map((c) => `${c.id} (${c.labelEn})`).join(", ");
const responseLanguage = uiLocale === "zh-CN"
? "Simplified Chinese"
: uiLocale === "ja"
? "Japanese"
: "English";
: uiLocale === "zh-TW"
? "Traditional Chinese (Taiwan)"
: uiLocale === "ja"
? "Japanese"
: "English";
return [
"You are an onboarding assistant for an AI slide-deck generator.",
"Given the user's topic, produce 6-7 SHORT clarifying questions that will make the deck much better.",
Expand Down
8 changes: 7 additions & 1 deletion src/lib/questionSemantics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export function semanticOptionLabel(
return descriptor ? `${token} · ${descriptor}` : token;
}

export type QuestionCopyLocale = "zh-CN" | "en" | "ja";
export type QuestionCopyLocale = "zh-CN" | "zh-TW" | "en" | "ja";

/** Deterministic fallback when a model omits the required output-quality question. */
export function resolutionQuestionCopy(locale: QuestionCopyLocale) {
Expand All @@ -86,6 +86,12 @@ export function resolutionQuestionCopy(locale: QuestionCopyLocale) {
labels: ["標準", "高画質", "超高画質"],
};
}
if (locale === "zh-TW") {
return {
question: "輸出清晰度要選哪一檔?",
labels: ["標準品質", "高畫質", "超高畫質"],
};
}
return {
question: "输出清晰度选择哪档?",
labels: ["标准质量", "高清质量", "超高清质量"],
Expand Down
2 changes: 2 additions & 0 deletions src/lib/scenarios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,8 @@ export const PRESENTATION_SCENARIOS: PresentationScenario[] = [
export const FEATURED_SCENARIOS = PRESENTATION_SCENARIOS.filter((scenario) => scenario.featured);

export function scenarioText(value: LocalizedScenarioText, locale: string): string {
// Traditional Chinese reuses the Simplified copy until scenario text is localized.
if (locale === "zh-TW") return value["zh-CN"];
return value[locale as keyof LocalizedScenarioText] ?? value.en;
}

Expand Down
6 changes: 5 additions & 1 deletion src/lib/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@ export const CATEGORIES: DeckCategory[] = [
];

export function categoryLabel(category: DeckCategory, locale: string): string {
return locale === "zh-CN" ? category.labelZh : locale === "ja" ? category.labelJa : category.labelEn;
return locale === "zh-CN" || locale === "zh-TW"
? category.labelZh
: locale === "ja"
? category.labelJa
: category.labelEn;
}

// ---- seeded templates (5 paid categories x 3) --------------------------
Expand Down