Files
m3e-canvas/web/vendor/lib/i18n.ts
T
pastilhas 80529dd9e7 six defects, and a typecheck the repo could not pass
Found by reviewing the plugin rather than running it. Nothing was live — the
table does not exist and the plugin is not installed — so all of these were
waiting for the first person to use it.

── The second unnamed design could not be saved ──

`uniqueIndex(userId, title)`, and every design starts life called "Untitled":
`titleOf` returns it for a document with no title and the API defaults to it. So
the first saved, the second collided, and the insert threw under a panel that
could only say "Changes are not being saved." Renaming one design to match
another did the same.

Two designs may share a name, as files and notes and dashboards do. Nothing looks
a design up by title, so uniqueness bought nothing. The index is now ordinary and
on the columns the list query actually uses.

The lesson from `dashboards.id` that the schema comment cites is that a key must
be scoped to its owner. `userId` on the row does that; making the pair unique as
well imported the collision it was citing, one account at a time.

── New did not start a new design ──

It cleared `?doc=`, and no parameter already means "open what I had last",
answered with `/documents/latest`. So New handed back the design on screen and
pointed the next save at its id. There is a `new` sentinel now, in its own module
because both panels read it and neither owns it.

── Two ways the load effect raced the debounced save ──

Changing `?doc=` does not unmount the panel, so the unmount flush never ran for
it: an edit made within the debounce and navigated away from was written after
`latest.current` had been replaced, which is to say never.

And the first save of a new design puts its id in the URL, which lands in the
same effect — re-reading the row overwrote in-memory state with the copy the
server had just been given, discarding whatever was drawn during the round trip.

The effect now flushes what is pending before it reads, and does not re-read a
document it already holds. The editor is keyed on a load generation rather than
on `row?.id`, which could not express "blank again" and left the previous design
on screen with `rowId` cleared underneath it.

── The typecheck ──

354 of the 355 errors were upstream's tree measured against this platform's
settings, which it was never written for. `bunx tsgo` is how this repo is kept
clean and it could not be clean while this plugin existed. `exclude` does not
help: adapter.ts imports the tree back in. So vendor.sh marks each vendored file
unchecked in the same deterministic pass that rewrites its imports. Types still
resolve; only the errors inside those files are silenced, which is what the
adapter already says the seam is for. Whole repo: 0.

── PROVENANCE.md understated what is done to the copy ──

It said "the one edit vendor.sh makes" and described the import rewrite. It also
runs apply-host.py, which places our own lib/host.ts and wires three export
paths. Four files are not upstream and the document whose job is making the copy
trustworthy did not say so. It lists all four now.

Also: web/host.ts imported a path only correct at its destination, so the file
could never typecheck where it is edited — apply-host.py rewrites that line when
placing it, and re-marks the placed copy, which it overwrites after vendor.sh has
marked it. PUT is no longer full-replace: omitting `title` renamed a design to
"Untitled" and omitting `ui` nulled it, a trap set for the second caller. And a
document is bounded at 4 MB, measured in bytes, where the title was capped at 200
and the document at nothing.
2026-09-05 19:52:27 +01:00

686 lines
47 KiB
TypeScript

// @ts-nocheck — vendored, see PROVENANCE.md
"use client";
import { createContext, useContext } from "react";
export type Lang = "ja" | "en" | "zh" | "ko";
export const LANGS: { key: Lang; label: string }[] = [
{ key: "ja", label: "日本語" },
{ key: "en", label: "English" },
{ key: "zh", label: "中文" },
{ key: "ko", label: "한국어" },
];
export const isLang = (v: unknown): v is Lang => v === "ja" || v === "en" || v === "zh" || v === "ko";
/* A module-level copy lets non-React helpers (item defaults, prompt text)
* follow the language without threading it through every call. */
let current: Lang = "ja";
export const getLang = () => current;
export const setGlobalLang = (l: Lang) => {
current = l;
};
export const LangContext = createContext<Lang>("ja");
export const useLang = () => useContext(LangContext);
export const SEED_TEXT: Record<Lang, { favorite: string; share: string; inbox: string; starred: string; archive: string; supporting: string; start: string }> = {
ja: { favorite: "お気に入り", share: "共有", inbox: "受信トレイ", starred: "スター付き", archive: "アーカイブ", supporting: "サブテキスト", start: "はじめる" },
en: { favorite: "Favorite", share: "Share", inbox: "Inbox", starred: "Starred", archive: "Archive", supporting: "Supporting text", start: "Get started" },
zh: { favorite: "收藏", share: "分享", inbox: "收件箱", starred: "已加星标", archive: "归档", supporting: "辅助文本", start: "开始" },
ko: { favorite: "즐겨찾기", share: "공유", inbox: "받은편지함", starred: "별표 표시", archive: "보관함", supporting: "보조 텍스트", start: "시작하기" },
};
/** ponytail: matches defaults by text; add provenance if authored copies must be distinguished. */
export function translateDefaultText(value: string, kind: string, field: "label" | "supporting" | "tab", lang: Lang): string {
for (const { key: from } of LANGS) {
if (field === "tab") {
const labels = (l: Lang) => kind === "tabs" ? TAB_LABELS[l] : (kind === "fabMenu" ? FAB_MENU_TABS[l] : NAV_TABS[l]).map((tab) => tab.label);
const index = labels(from).indexOf(value);
if (index >= 0) return labels(lang)[index] ?? value;
} else {
if (value && value === KIND_TEXT[from][kind]?.[field]) return KIND_TEXT[lang][kind]?.[field] ?? value;
const keys: (keyof typeof SEED_TEXT.en)[] = field === "supporting" ? ["supporting"] : kind === "button" ? ["favorite", "share", "start"] : kind === "listItem" ? ["inbox", "starred", "archive"] : [];
for (const key of keys) {
if (value === SEED_TEXT[from][key]) return SEED_TEXT[lang][key];
}
}
}
return value;
}
type Str = { ja: string; en: string; zh: string };
export function translateDefaultFrameName(name: string, lang: Lang): string {
for (const { key } of LANGS) {
if (name === t("home", key)) return t("home", lang);
const prefix = `${t("screenN", key)} `;
if (name.startsWith(prefix) && /^\d+$/.test(name.slice(prefix.length))) {
return `${t("screenN", lang)} ${name.slice(prefix.length)}`;
}
}
return name;
}
export const COLOR_TOKEN_TEXT = {
ja: { surface: "サーフェス", surfaceContainerLow: "コンテナ(低)", surfaceContainer: "コンテナ", surfaceContainerHigh: "コンテナ(高)", surfaceContainerHighest: "コンテナ(最高)", primaryContainer: "プライマリコンテナ", secondaryContainer: "セカンダリコンテナ", tertiaryContainer: "ターシャリコンテナ", primary: "プライマリ", inverseSurface: "反転サーフェス" },
zh: { surface: "表面", surfaceContainerLow: "低层容器", surfaceContainer: "容器", surfaceContainerHigh: "高层容器", surfaceContainerHighest: "最高层容器", primaryContainer: "主色容器", secondaryContainer: "次色容器", tertiaryContainer: "第三色容器", primary: "主色", inverseSurface: "反色表面" },
ko: { surface: "표면", surfaceContainerLow: "낮은 컨테이너", surfaceContainer: "컨테이너", surfaceContainerHigh: "높은 컨테이너", surfaceContainerHighest: "가장 높은 컨테이너", primaryContainer: "주 색상 컨테이너", secondaryContainer: "보조 색상 컨테이너", tertiaryContainer: "세 번째 색상 컨테이너", primary: "주 색상", inverseSurface: "반전 표면" },
};
const UI = {
// panels
parts: { ja: "部品", en: "Parts", zh: "组件" },
layers: { ja: "レイヤー", en: "Layers", zh: "图层" },
edit: { ja: "編集", en: "Edit", zh: "编辑" },
prompt: { ja: "プロンプト", en: "Prompt", zh: "提示词" },
closePanel: { ja: "パネルを閉じる", en: "Close panel", zh: "关闭面板" },
search: { ja: "検索", en: "Search", zh: "搜索" },
favorites: { ja: "お気に入り", en: "Favorites", zh: "收藏" },
addFavorite: { ja: "お気に入りに追加", en: "Add to favorites", zh: "添加到收藏" },
removeFavorite: { ja: "お気に入りから外す", en: "Remove from favorites", zh: "取消收藏" },
clear: { ja: "クリア", en: "Clear", zh: "清除" },
language: { ja: "言語", en: "Language", zh: "语言" },
// toolbar
select: { ja: "選択 (V)", en: "Select (V)", zh: "选择 (V)" },
hand: { ja: "手のひら (H / Space)", en: "Hand (H / Space)", zh: "抓手 (H / Space)" },
blank: { ja: "白紙", en: "Blank canvas", zh: "空白画布" },
phone: { ja: "スマホ画面", en: "Phone screens", zh: "手机屏幕" },
addFrame: { ja: "画面を追加", en: "Add screen", zh: "添加屏幕" },
preview: { ja: "プレビュー (P)", en: "Preview (P)", zh: "预览 (P)" },
zoomIn: { ja: "拡大 (+)", en: "Zoom in (+)", zh: "放大 (+)" },
zoomOut: { ja: "縮小 (-)", en: "Zoom out (-)", zh: "缩小 (-)" },
fit: { ja: "全体を表示 (0)", en: "Fit (0)", zh: "适应窗口 (0)" },
undo: { ja: "元に戻す (Ctrl+Z)", en: "Undo (Ctrl+Z)", zh: "撤销 (Ctrl+Z)" },
redo: { ja: "やり直す (Ctrl+Shift+Z)", en: "Redo (Ctrl+Shift+Z)", zh: "重做 (Ctrl+Shift+Z)" },
clearAll: { ja: "すべて消す", en: "Clear canvas", zh: "全部清除" },
clearAllTitle: { ja: "すべて消しますか?", en: "Clear the canvas?", zh: "要全部清除吗?" },
clearAllBody: {
ja: "すべての画面と部品を削除します。元に戻す (Ctrl+Z) で復元できます。",
en: "Every screen and part will be removed. Undo (Ctrl+Z) can bring them back.",
zh: "将删除所有屏幕和组件。可以用撤销 (Ctrl+Z) 恢复。",
},
// inspector
screen: { ja: "画面", en: "Screen", zh: "屏幕" },
screenName: { ja: "画面の名前", en: "Screen name", zh: "屏幕名称" },
name: { ja: "名前", en: "Name", zh: "名称" },
background: { ja: "背景", en: "Background", zh: "背景" },
export: { ja: "書き出し", en: "Export", zh: "导出" },
project: { ja: "プロジェクト", en: "Project", zh: "项目" },
saveProject: { ja: "プロジェクトを保存", en: "Save project", zh: "保存项目" },
openProject: { ja: "プロジェクトを開く", en: "Open project", zh: "打开项目" },
replaceProjectTitle: { ja: "プロジェクトを開きますか?", en: "Open this project?", zh: "要打开这个项目吗?" },
askAi: { ja: "AI に頼む", en: "Ask an AI", zh: "让 AI 来画" },
askAiHint: {
ja: "作りたいものを書いて「AI で作る」を押すと、AI タブのキーで設計がキャンバスに描かれます。代わりに指示をコピーして Claude Code などの AI エージェントに渡すこともできます。返ってきたリンクを開くか、JSON をファイルに保存して「プロジェクトを開く」から読み込みます。",
en: "Describe what you want and press Draft with AI: the model from the AI tab draws the design on the canvas. Or copy the instruction for an AI agent such as Claude Code; open the link it replies with, or save its JSON and load it with Open project.",
zh: "写下想做的东西并按“让 AI 生成”,AI 标签页里的模型会把设计画到画布上。也可以复制指令交给 Claude Code 等 AI 代理:打开它回复的链接,或把 JSON 保存为文件后用“打开项目”读取。",
},
askAiIdea: { ja: "作りたいもの(例: レシピを保存して検索できるアプリ)", en: "What to build (e.g. an app to save and search recipes)", zh: "想做的东西(例如:能保存和搜索菜谱的应用)" },
askAiIdeaFallback: { ja: "(ここに作りたいものを書く)", en: "(describe what to build here)", zh: "(在这里写下想做的东西)" },
askAiCopy: { ja: "指示をコピー", en: "Copy the instruction", zh: "复制指令" },
askAiText: {
ja: "M3E Canvas のスケッチを作ってください。まず {url} を読み、その手順どおりに設計を JSON で組み立てて、共有リンクの形で返してください。コードを実行できない場合は、設計の JSON をコードブロックで返してください(ファイルに保存して読み込みます)。検証は不要です。\n\n作りたいもの: {idea}",
en: "Make an M3E Canvas sketch. First read {url} and follow it: build the design as JSON and reply with a share link. If you cannot run code, reply with the JSON in a code block (it will be saved to a file and opened). No verification is needed.\n\nWhat to build: {idea}",
zh: "请制作一个 M3E Canvas 草图。先阅读 {url},按其中的步骤用 JSON 组织设计,并以分享链接的形式回复。如果无法运行代码,就在代码块中回复设计的 JSON(会保存为文件后打开)。不需要验证。\n\n想做的东西:{idea}",
},
askAiGenerate: { ja: "AI で作る", en: "Draft with AI", zh: "让 AI 生成" },
askAiPasted: { ja: "AI エージェントに貼り付けてください", en: "Paste it into your AI agent", zh: "请粘贴到 AI 代理中" },
askAiTitle: { ja: "AI に設計を頼む", en: "Ask an AI for a design", zh: "让 AI 设计" },
askAiCopyTitle: { ja: "AI エージェント用の指示", en: "Instruction for an AI agent", zh: "AI 代理用指令" },
askAiGenerateTitle: { ja: "AI タブのキーで作る", en: "Uses the key in the AI tab", zh: "使用 AI 标签页的密钥" },
aiSetup: { ja: "AI の設定", en: "AI settings", zh: "AI 设置" },
aiSetupHint: { ja: "キーを入れると、ここで直接作れます", en: "Add a key to draft it right here", zh: "填写密钥后可在此直接生成" },
aiSetupTitle: { ja: "AI タブを開く", en: "Open the AI tab", zh: "打开 AI 标签页" },
draftKeep: { ja: "この設計を採用", en: "Keep this design", zh: "采用这个设计" },
draftUndo: { ja: "前の設計に戻す", en: "Back to the previous design", zh: "恢复之前的设计" },
askAiGenerating: { ja: "作成中…", en: "Drafting…", zh: "生成中…" },
selectedTab: { ja: "選択中にする", en: "Make this the selected one", zh: "设为选中项" },
shareLinkCopy: { ja: "リンクをコピー", en: "Copy link", zh: "复制链接" },
shareLinkHint: { ja: "この設計を開くリンク(画像は含まない)", en: "A link that opens this design (no images)", zh: "打开此设计的链接(不含图片)" },
replaceProject: {
ja: "現在のキャンバスは置き換わります。元に戻す (Ctrl+Z) で戻せます。",
en: "The current canvas will be replaced. Undo (Ctrl+Z) brings it back.",
zh: "当前画布将被替换。可以用撤销 (Ctrl+Z) 恢复。",
},
invalidProject: {
ja: "プロジェクトファイルを開けませんでした。",
en: "Could not open the project file.",
zh: "无法打开项目文件。",
},
readOnlyTitle: { ja: "別のタブで編集中です", en: "Editing in another tab", zh: "正在其他标签页中编辑" },
readOnlyBody: {
ja: "このキャンバスは別のタブで編集中です。そのタブを閉じてから、このページを再読み込みしてください。",
en: "This canvas is being edited in another tab. Close that tab, then reload this page to edit.",
zh: "此画布正在其他标签页中编辑。关闭该标签页后,重新加载此页面即可编辑。",
},
reload: { ja: "再読み込み", en: "Reload", zh: "重新加载" },
copied: { ja: "コピーしました", en: "Copied", zh: "已复制" },
saveImage: { ja: "画像で保存", en: "Save as image", zh: "保存为图片" },
saving: { ja: "保存中…", en: "Saving…", zh: "保存中…" },
previewFrom: { ja: "この画面からプレビュー", en: "Preview from this screen", zh: "从此屏幕预览" },
duplicate: { ja: "複製", en: "Duplicate", zh: "复制" },
duplicateKey: { ja: "複製 (Ctrl+D)", en: "Duplicate (Ctrl+D)", zh: "复制 (Ctrl+D)" },
delete: { ja: "削除 (Delete)", en: "Delete (Delete)", zh: "删除 (Delete)" },
deleteSelection: { ja: "選択を削除", en: "Delete selection", zh: "删除所选" },
text: { ja: "テキスト", en: "Text", zh: "文本" },
label: { ja: "ラベル", en: "Label", zh: "标签" },
bold: { ja: "太字", en: "Bold", zh: "粗体" },
action: { ja: "アクション", en: "Action", zh: "操作" },
supporting: { ja: "サブテキスト", en: "Supporting text", zh: "辅助文本" },
tabs: { ja: "項目", en: "Items", zh: "项目" },
changeIcon: { ja: "アイコンを変更", en: "Change icon", zh: "更改图标" },
image: { ja: "画像", en: "Image", zh: "图片" },
pickImage: { ja: "画像を選ぶ", en: "Choose image", zh: "选择图片" },
removeImage: { ja: "画像を外す", en: "Remove image", zh: "移除图片" },
imageUrl: { ja: "画像の URL", en: "Image URL", zh: "图片网址" },
imageArea: { ja: "画像エリア", en: "Image area", zh: "图片区域" },
autoWidth: { ja: "文字の幅", en: "Text width", zh: "随文字" },
icon: { ja: "アイコン", en: "Icon", zh: "图标" },
noIcon: { ja: "アイコンなし", en: "No icon", zh: "无图标" },
searchIcons: { ja: "アイコンを検索", en: "Search icons", zh: "搜索图标" },
style: { ja: "スタイル", en: "Style", zh: "样式" },
filled: { ja: "塗りつぶし", en: "Filled", zh: "填充" },
tonal: { ja: "トーナル", en: "Tonal", zh: "色调" },
elevated: { ja: "浮き上がり", en: "Elevated", zh: "凸起" },
outlined: { ja: "枠線", en: "Outlined", zh: "描边" },
standard: { ja: "標準", en: "Standard", zh: "标准" },
vibrant: { ja: "鮮やか", en: "Vibrant", zh: "鲜艳" },
state: { ja: "状態", en: "State", zh: "状态" },
selected: { ja: "選択", en: "Selected", zh: "已选中" },
handle: { ja: "ハンドル(ボトムシート)", en: "Handle (bottom sheet)", zh: "拖动条(底部面板)" },
listSwitch: { ja: "末尾にスイッチ", en: "Trailing switch", zh: "末尾开关" },
on: { ja: "オン", en: "On", zh: "开" },
container: { ja: "コンテナ", en: "Container", zh: "容器" },
wavy: { ja: "波形", en: "Wavy", zh: "波浪形" },
determinate: { ja: "確定", en: "Determinate", zh: "确定进度" },
size: { ja: "サイズ", en: "Size", zh: "尺寸" },
width: { ja: "幅", en: "Width", zh: "宽度" },
height: { ja: "高さ", en: "Height", zh: "高度" },
fontSize: { ja: "文字サイズ", en: "Font size", zh: "字号" },
cornerRadius: { ja: "角丸", en: "Corner radius", zh: "圆角" },
cornerTop: { ja: "上の角丸", en: "Top corners", zh: "上方圆角" },
cornerBottom: { ja: "下の角丸", en: "Bottom corners", zh: "下方圆角" },
cornerLeft: { ja: "左の角丸", en: "Left corners", zh: "左侧圆角" },
cornerRight: { ja: "右の角丸", en: "Right corners", zh: "右侧圆角" },
cornersEach: { ja: "角ごとに指定", en: "Each corner", zh: "分别设置各角" },
cornerTl: { ja: "左上", en: "Top left", zh: "左上" },
cornerTr: { ja: "右上", en: "Top right", zh: "右上" },
cornerBl: { ja: "左下", en: "Bottom left", zh: "左下" },
cornerBr: { ja: "右下", en: "Bottom right", zh: "右下" },
screenWidth: { ja: "画面いっぱい", en: "Screen width", zh: "全屏宽" },
contentWidth: { ja: "左右 16dp の余白", en: "16dp side margins", zh: "左右 16dp 边距" },
halfWidth: { ja: "2 列に並べる幅", en: "Half a row (two columns)", zh: "两列宽" },
columnWidth: { ja: "スマホ 1 画面分の幅", en: "One phone column", zh: "一个手机屏幕的宽度" },
screenHeight: { ja: "画面の高さ", en: "Screen height", zh: "全屏高" },
halfHeight: { ja: "画面の半分", en: "Half the screen", zh: "半屏高" },
tapTo: { ja: "タップで移動", en: "Tap to open", zh: "点击跳转" },
none: { ja: "なし", en: "None", zh: "无" },
goBack: { ja: "戻る", en: "Back", zh: "返回" },
swipeTo: { ja: "スワイプで移動", en: "Swipe to open", zh: "滑动跳转" },
toggle: { ja: "切り替えボタン", en: "Toggle button", zh: "切换按钮" },
toggleHint: { ja: "タップでオン/オフ", en: "Tap toggles on / off", zh: "点击切换开/关" },
thumbCheck: { ja: "オンのときチェックアイコン", en: "Check icon when on", zh: "开启时显示勾选图标" },
behavior: { ja: "振る舞い", en: "Behavior", zh: "行为" },
whenPressed: { ja: "押したとき…", en: "When pressed…", zh: "按下时…" },
whatItDoes: { ja: "この部品の動き…", en: "What this part does…", zh: "这个组件的作用…" },
removeLink: { ja: "リンクを外す", en: "Remove link", zh: "移除链接" },
group: { ja: "グループ", en: "Group", zh: "组" },
makeGroup: { ja: "グループ化", en: "Group", zh: "编组" },
ungroup: { ja: "グループ解除", en: "Ungroup", zh: "取消编组" },
selectedParts: { ja: "個を選択中", en: "selected", zh: "个已选中" },
groupHint: {
ja: "重なりを保ったまま、ひとつのレイヤーとして一緒に動かせます",
en: "Keeps the overlap and moves as one layer",
zh: "保持叠放关系,作为一个图层一起移动",
},
iconBackground: { ja: "アイコンの背景", en: "Icon background", zh: "图标背景" },
noBackground: { ja: "背景なし", en: "No background", zh: "无背景" },
normalState: { ja: "通常", en: "Normal", zh: "常态" },
onState: { ja: "オン", en: "On", zh: "开启" },
onStateHint: { ja: "オンのときの文字・アイコン・スタイル", en: "Text, icon and style when on", zh: "开启时的文字、图标和样式" },
groupEditNote: { ja: "中の部品を編集するにはグループを解除してください", en: "Ungroup to edit the parts inside", zh: "要编辑其中的组件,请先取消编组" },
openPanel: { ja: "パネルを開く", en: "Open panel", zh: "打开面板" },
colors: { ja: "カラー", en: "Colors", zh: "配色" },
templates: { ja: "パレット", en: "Palettes", zh: "调色板" },
customColor: { ja: "カスタム", en: "Custom", zh: "自定义" },
seedColor: { ja: "ベースの色", en: "Seed color", zh: "基准色" },
seedHint: {
ja: "1 色選ぶと Material 3 のスキーム全体を作ります。細かく調整で個別の色も変えられます。",
en: "One color builds the whole Material 3 scheme. Fine-tune changes single roles.",
zh: "选一个颜色即可生成整套 Material 3 配色。可在微调中单独修改。",
},
useThis: { ja: "この色にする", en: "Use it", zh: "使用" },
fineTune: { ja: "細かく調整", en: "Fine-tune", zh: "微调" },
dynamicColor: { ja: "ダイナミックカラー", en: "Dynamic color", zh: "动态配色" },
dynamicOnHint: {
ja: "ここの色はエディタ上だけ。実機では壁紙の色になります。",
en: "These colors are editor-only; the phone uses its wallpaper colors.",
zh: "这些颜色仅用于编辑器,手机上会使用壁纸颜色。",
},
dynamicOffHint: {
ja: "オンにすると実機は壁紙の色を使い、ここの色は予備になります。",
en: "When on, the phone uses wallpaper colors and these are the fallback.",
zh: "开启后手机使用壁纸颜色,这里的颜色作为备用。",
},
closeBtn: { ja: "閉じる", en: "Close", zh: "关闭" },
screens: { ja: "画面を選ぶ", en: "Choose screen", zh: "选择屏幕" },
// layers
noLayers: { ja: "この画面には部品がありません", en: "Nothing on this screen yet", zh: "此屏幕还没有组件" },
showParts: { ja: "中の部品を表示", en: "Show the parts inside", zh: "显示组内组件" },
hideParts: { ja: "中の部品を隠す", en: "Hide the parts inside", zh: "隐藏组内组件" },
// prompt panel
brief: { ja: "このアプリの説明…", en: "What this app is…", zh: "这个应用的说明…" },
appName: { ja: "アプリの名前", en: "App name", zh: "应用名称" },
targetPlatform: { ja: "実装先", en: "Target", zh: "目标平台" },
targetAndroid: { ja: "Android のネイティブアプリとして作る", en: "Build as a native Android app", zh: "作为 Android 原生应用构建" },
targetWeb: { ja: "ブラウザで動く Web アプリとして作る", en: "Build as a web app that runs in the browser", zh: "作为在浏览器中运行的 Web 应用构建" },
copyPrompt: { ja: "プロンプトをコピー", en: "Copy prompt", zh: "复制提示词" },
// preview
back: { ja: "戻る", en: "Back", zh: "返回" },
close: { ja: "閉じる (Esc)", en: "Close (Esc)", zh: "关闭 (Esc)" },
// parts content
cancel: { ja: "キャンセル", en: "Cancel", zh: "取消" },
ok: { ja: "OK", en: "OK", zh: "确定" },
leading: { ja: "先頭", en: "Leading", zh: "前置" },
trailing: { ja: "末尾", en: "Trailing", zh: "后置" },
// frames
home: { ja: "ホーム", en: "Home", zh: "首页" },
screenN: { ja: "画面", en: "Screen", zh: "屏幕" },
copySuffix: { ja: " コピー", en: " copy", zh: " 副本" },
frameSize: { ja: "画面サイズ", en: "Screen size", zh: "屏幕尺寸" },
phoneFrame: { ja: "スマホ", en: "Phone", zh: "手机" },
desktopFrame: { ja: "デスクトップ", en: "Desktop", zh: "桌面" },
// mobile
mobileNote: { ja: "フル機能は PC のブラウザで使えます", en: "Full features on a desktop browser", zh: "完整功能请在电脑浏览器中使用" },
addButton: { ja: "ボタンを追加", en: "Add button", zh: "添加按钮" },
done: { ja: "完了", en: "Done", zh: "完成" },
theme: { ja: "テーマ", en: "Theme", zh: "主题" },
settings: { ja: "テーマと設定", en: "Theme and settings", zh: "主题与设置" },
// theme panels
shape: { ja: "シェイプ", en: "Shape", zh: "形状" },
typography: { ja: "タイポグラフィ", en: "Type", zh: "字体" },
motion: { ja: "モーション", en: "Motion", zh: "动效" },
brightness: { ja: "明るさ", en: "Brightness", zh: "明暗" },
light: { ja: "ライト", en: "Light", zh: "浅色" },
dark: { ja: "ダーク", en: "Dark", zh: "深色" },
contrast: { ja: "コントラスト", en: "Contrast", zh: "对比度" },
bothModes: { ja: "両対応", en: "Both", zh: "两者" },
contrastStandard: { ja: "標準", en: "Standard", zh: "标准" },
contrastMedium: { ja: "中", en: "Medium", zh: "中" },
contrastHigh: { ja: "高", en: "High", zh: "高" },
shapeScale: { ja: "角丸の度合い", en: "Corner roundness", zh: "圆角程度" },
shapeSquare: { ja: "スクエア", en: "Square", zh: "方形" },
shapeRounded: { ja: "標準", en: "Rounded", zh: "圆角" },
shapeFull: { ja: "フル", en: "Full", zh: "全圆" },
shapeHint: {
ja: "すべての部品の初期の角丸をまとめて変えます。部品ごとに入力した角丸はそのまま残ります。",
en: "Changes the default corners of every part at once. A radius you typed on a part stays as it is.",
zh: "统一改变所有组件的默认圆角。已为单个组件输入的圆角保持不变。",
},
fontFamily: { ja: "書体", en: "Typeface", zh: "字体" },
emphasized: { ja: "強調スタイル", en: "Emphasized", zh: "强调样式" },
emphasizedHint: {
ja: "見出しやラベルを M3 Expressive の太めのスタイルにします。",
en: "Headlines and labels use the heavier M3 Expressive styles.",
zh: "标题和标签使用 M3 Expressive 的加粗样式。",
},
motionScheme: { ja: "動きの種類", en: "Motion scheme", zh: "动效方案" },
motionStandard: { ja: "スタンダード", en: "Standard", zh: "标准" },
motionExpressive: { ja: "エクスプレッシブ", en: "Expressive", zh: "富有表现力" },
motionHint: {
ja: "エクスプレッシブは弾むスプリング。プレビューの画面遷移とプロンプトに反映されます。",
en: "Expressive is a bouncy spring. It drives the preview transitions and the prompt.",
zh: "富有表现力为弹性弹簧动效,作用于预览的屏幕过渡和提示词。",
},
tryIt: { ja: "タップして確認", en: "Tap to try", zh: "点击试试" },
// tidy
tidy: { ja: "整える", en: "Tidy", zh: "整理" },
tidyUndo: { ja: "整える前に戻す", en: "Undo tidy", zh: "撤销整理" },
tidyDone: { ja: "すでに整っています", en: "Already tidy", zh: "已经整齐" },
// screen description
description: { ja: "説明", en: "Description", zh: "说明" },
screenDescription: { ja: "この画面の目的", en: "What this screen is for", zh: "这个屏幕的用途" },
// ai
ai: { ja: "AI", en: "AI", zh: "AI" },
promptReset: { ja: "生成されたプロンプトに戻す", en: "Back to the generated prompt", zh: "恢复为生成的提示词" },
aiWriteShort: { ja: "AI で書く", en: "Write with AI", zh: "AI 撰写" },
aiWrite: { ja: "AI に書いてもらう", en: "Let the AI write it", zh: "让 AI 来写" },
aiSettings: { ja: "AI の設定", en: "AI settings", zh: "AI 设置" },
aiProvider: { ja: "プロバイダ", en: "Provider", zh: "服务商" },
aiBaseUrl: { ja: "ベース URL", en: "Base URL", zh: "基础 URL" },
aiModel: { ja: "モデル ID", en: "Model ID", zh: "模型 ID" },
aiKey: { ja: "API キー", en: "API key", zh: "API 密钥" },
aiGetKey: { ja: "キーを取得", en: "Get a key", zh: "获取密钥" },
aiKeyHint: {
ja: "キーはこのブラウザにだけ保存され、プロバイダへ直接送られます。",
en: "Stored only in this browser and sent straight to the provider.",
zh: "密钥只保存在此浏览器中,并直接发送给服务商。",
},
aiRestore: { ja: "AI の前と切り替える", en: "Switch between the AI rewrite and the original", zh: "在 AI 改写与原文之间切换" },
aiApplied: { ja: "適用しました", en: "Applied", zh: "已应用" },
aiSelectScreen: { ja: "先に画面を選んでください", en: "Select a screen first", zh: "请先选择一个屏幕" },
aiNoKey: { ja: "AI タブでキーを入れると使えます", en: "Add a key in the AI tab to use this", zh: "在 AI 标签页中填写密钥后即可使用" },
aiError: { ja: "AI の呼び出しに失敗しました", en: "The AI request failed", zh: "AI 请求失败" },
aiErrorRefusal: { ja: "モデルが回答を拒否しました", en: "The model declined to answer", zh: "模型拒绝回答" },
aiErrorJson: { ja: "モデルの返答を読み取れませんでした", en: "The model's reply could not be read", zh: "无法解析模型的回复" },
aiErrorLong: { ja: "返答が長すぎて途中で切れました。画面を減らして試してください", en: "The reply was cut short. Try fewer screens", zh: "回复过长被截断了。请减少屏幕数量再试" },
aiErrorModel: { ja: "モデル ID を入力してください", en: "Enter a model ID", zh: "请输入模型 ID" },
aiErrorInsecure: { ja: "ベース URL は https か localhost にしてください", en: "The base URL must use https or point at localhost", zh: "基础 URL 必须使用 https 或指向 localhost" },
aiErrorNetwork: {
ja: "接続できませんでした。URL、ネットワーク、CORS の設定を確認してください",
en: "Could not connect. Check the URL, the network and the server's CORS settings",
zh: "无法连接。请检查 URL、网络和服务器的 CORS 设置",
},
} as const satisfies Record<string, Str>;
export type UIKey = keyof typeof UI;
const KO: Record<UIKey, string> = {
frameSize: "화면 크기", phoneFrame: "휴대전화", desktopFrame: "데스크톱", columnWidth: "휴대전화 한 화면 너비", cornerLeft: "왼쪽 모서리", cornerRight: "오른쪽 모서리", cornersEach: "모서리별로 지정", cornerTl: "왼쪽 위", cornerTr: "오른쪽 위", cornerBl: "왼쪽 아래", cornerBr: "오른쪽 아래",
filled: "채움", tonal: "색조", elevated: "그림자", outlined: "윤곽선", standard: "표준", vibrant: "선명함",
parts: "부품", layers: "레이어", edit: "편집", prompt: "프롬프트", closePanel: "패널 닫기",
search: "검색", favorites: "즐겨찾기", addFavorite: "즐겨찾기에 추가", removeFavorite: "즐겨찾기에서 제거", clear: "지우기", language: "언어",
select: "선택 (V)", hand: "손 도구 (H / Space)", blank: "빈 캔버스", phone: "휴대전화 화면", addFrame: "화면 추가", preview: "미리보기 (P)",
zoomIn: "확대 (+)", zoomOut: "축소 (-)", fit: "전체 맞춤 (0)", undo: "실행 취소 (Ctrl+Z)", redo: "다시 실행 (Ctrl+Shift+Z)",
clearAll: "모두 지우기", clearAllTitle: "캔버스를 비울까요?", clearAllBody: "모든 화면과 부품을 삭제합니다. 실행 취소(Ctrl+Z)로 복원할 수 있습니다.",
screen: "화면", screenName: "화면 이름", name: "이름", background: "배경", export: "내보내기", project: "프로젝트",
saveProject: "프로젝트 저장", openProject: "프로젝트 열기", replaceProjectTitle: "이 프로젝트를 열까요?",
replaceProject: "현재 캔버스가 교체됩니다. 실행 취소(Ctrl+Z)로 되돌릴 수 있습니다.",
askAi: "AI에게 맡기기",
askAiHint: "만들고 싶은 것을 적고 'AI로 만들기'를 누르면 AI 탭의 키로 설계가 캔버스에 그려집니다. 대신 지시를 복사해 Claude Code 같은 AI 에이전트에 넘길 수도 있습니다. 답한 링크를 열거나 JSON을 파일로 저장해 '프로젝트 열기'로 불러오세요.",
askAiGenerate: "AI로 만들기", askAiGenerating: "만드는 중…",
askAiPasted: "AI 에이전트에 붙여 넣으세요", askAiTitle: "AI에게 설계 맡기기", askAiCopyTitle: "AI 에이전트용 지시", askAiGenerateTitle: "AI 탭의 키로 만들기", aiSetup: "AI 설정", aiSetupHint: "키를 입력하면 여기서 바로 만들 수 있습니다", aiSetupTitle: "AI 탭 열기",
draftKeep: "이 설계 채택", draftUndo: "이전 설계로 되돌리기",
askAiIdea: "만들고 싶은 것(예: 레시피를 저장하고 검색하는 앱)", askAiIdeaFallback: "(여기에 만들고 싶은 것을 적으세요)", askAiCopy: "지시 복사",
askAiText: "M3E Canvas 스케치를 만들어 주세요. 먼저 {url}을 읽고 그 절차대로 설계를 JSON으로 구성한 뒤, 공유 링크 형태로 답해 주세요. 코드를 실행할 수 없다면 설계 JSON을 코드 블록으로 답해 주세요(파일로 저장해 불러옵니다). 검증은 필요 없습니다.\n\n만들고 싶은 것: {idea}",
selectedTab: "선택 항목으로 지정",
shareLinkCopy: "링크 복사", shareLinkHint: "이 설계를 여는 링크(이미지 제외)",
invalidProject: "프로젝트 파일을 열 수 없습니다.", readOnlyTitle: "다른 탭에서 편집 중입니다",
readOnlyBody: "이 캔버스는 다른 탭에서 편집 중입니다. 해당 탭을 닫은 다음 이 페이지를 새로고침하세요.",
reload: "새로고침",
copied: "복사됨", saveImage: "이미지로 저장", saving: "저장 중…", previewFrom: "이 화면부터 미리보기",
duplicate: "복제", duplicateKey: "복제 (Ctrl+D)", delete: "삭제 (Delete)", deleteSelection: "선택 항목 삭제",
text: "텍스트", label: "레이블", bold: "굵게", action: "동작", supporting: "보조 텍스트", tabs: "항목", changeIcon: "아이콘 변경",
image: "이미지", pickImage: "이미지 선택", removeImage: "이미지 제거", imageUrl: "이미지 URL", imageArea: "이미지 영역", autoWidth: "글자 너비", icon: "아이콘", noIcon: "아이콘 없음", searchIcons: "아이콘 검색",
style: "스타일", state: "상태", selected: "선택됨", handle: "핸들(하단 시트)", listSwitch: "끝에 스위치", on: "켜짐", container: "컨테이너", wavy: "물결 모양", determinate: "확정형",
size: "크기", width: "너비", height: "높이", fontSize: "글자 크기", cornerRadius: "모서리 둥글기", cornerTop: "위쪽 모서리", cornerBottom: "아래쪽 모서리",
screenWidth: "화면 너비", contentWidth: "좌우 16dp 여백", halfWidth: "한 행의 절반(2열)", screenHeight: "화면 높이", halfHeight: "화면의 절반",
tapTo: "탭하여 이동", none: "없음", goBack: "뒤로", swipeTo: "스와이프하여 이동", toggle: "토글 버튼", toggleHint: "탭할 때 켜짐/꺼짐 전환",
thumbCheck: "켜졌을 때 체크 아이콘 표시", behavior: "동작", whenPressed: "눌렀을 때…", whatItDoes: "이 부품의 동작…", removeLink: "링크 제거",
group: "그룹", makeGroup: "그룹화", ungroup: "그룹 해제", selectedParts: "개 선택됨", groupHint: "겹침을 유지한 채 하나의 레이어처럼 함께 이동합니다",
iconBackground: "아이콘 배경", noBackground: "배경 없음", normalState: "기본", onState: "켜짐", onStateHint: "켜졌을 때의 텍스트, 아이콘, 스타일",
groupEditNote: "안쪽 부품을 편집하려면 그룹을 해제하세요", openPanel: "패널 열기", colors: "색상", templates: "팔레트", customColor: "사용자 지정",
seedColor: "기준 색상", seedHint: "색상 하나로 전체 Material 3 색상 구성을 만듭니다. 세부 조정에서 개별 색상도 바꿀 수 있습니다.",
useThis: "이 색상 사용", fineTune: "세부 조정", dynamicColor: "동적 색상",
dynamicOnHint: "여기 표시된 색상은 편집기 전용입니다. 실제 기기에서는 배경화면 색상을 사용합니다.",
dynamicOffHint: "켜면 실제 기기는 배경화면 색상을 사용하고 여기의 색상은 대체 색상이 됩니다.", closeBtn: "닫기", screens: "화면 선택",
noLayers: "이 화면에는 아직 부품이 없습니다", showParts: "안의 부품 표시", hideParts: "안의 부품 숨기기",
brief: "이 앱에 대한 설명…", appName: "앱 이름", targetPlatform: "구현 대상", targetAndroid: "Android 네이티브 앱으로 만들기",
targetWeb: "브라우저에서 실행되는 웹 앱으로 만들기", copyPrompt: "프롬프트 복사", back: "뒤로", close: "닫기 (Esc)", cancel: "취소", ok: "확인",
leading: "앞쪽", trailing: "뒤쪽", home: "홈", screenN: "화면", copySuffix: " 복사본", mobileNote: "전체 기능은 데스크톱 브라우저에서 사용할 수 있습니다",
addButton: "버튼 추가", done: "완료", theme: "테마", settings: "테마 및 설정", shape: "모양", typography: "글꼴", motion: "모션",
brightness: "밝기", light: "라이트", dark: "다크", contrast: "대비", bothModes: "둘 다", contrastStandard: "표준", contrastMedium: "중간", contrastHigh: "높음",
shapeScale: "모서리 둥글기", shapeSquare: "사각형", shapeRounded: "둥근형", shapeFull: "완전 둥근형",
shapeHint: "모든 부품의 기본 모서리를 한 번에 바꿉니다. 부품에 직접 입력한 반경은 유지됩니다.", fontFamily: "글꼴", emphasized: "강조 스타일",
emphasizedHint: "제목과 레이블에 더 굵은 M3 Expressive 스타일을 사용합니다.", motionScheme: "모션 방식", motionStandard: "표준", motionExpressive: "익스프레시브",
motionHint: "익스프레시브는 통통 튀는 스프링 효과입니다. 미리보기 화면 전환과 프롬프트에 반영됩니다.", tryIt: "탭하여 확인",
tidy: "정리", tidyUndo: "정리 실행 취소", tidyDone: "이미 정돈되어 있습니다", description: "설명", screenDescription: "이 화면의 용도",
ai: "AI", promptReset: "생성된 프롬프트로 되돌리기", aiWriteShort: "AI로 작성", aiWrite: "AI에게 작성 맡기기", aiSettings: "AI 설정",
aiProvider: "제공업체", aiBaseUrl: "기본 URL", aiModel: "모델 ID", aiKey: "API 키", aiGetKey: "키 받기",
aiKeyHint: "키는 이 브라우저에만 저장되며 제공업체로 직접 전송됩니다.", aiRestore: "AI 수정본과 원본 전환", aiApplied: "적용됨",
aiSelectScreen: "먼저 화면을 선택하세요", aiNoKey: "AI 탭에 키를 입력하면 사용할 수 있습니다", aiError: "AI 요청에 실패했습니다",
aiErrorRefusal: "모델이 답변을 거부했습니다", aiErrorJson: "모델의 응답을 읽을 수 없습니다", aiErrorLong: "답변이 너무 길어 중간에 잘렸습니다. 화면 수를 줄여 다시 시도하세요", aiErrorModel: "모델 ID를 입력하세요",
aiErrorInsecure: "기본 URL은 https를 사용하거나 localhost를 가리켜야 합니다", aiErrorNetwork: "연결할 수 없습니다. URL, 네트워크 및 서버의 CORS 설정을 확인하세요",
};
export const t = (key: UIKey, lang: Lang = current): string => (lang === "ko" ? KO[key] : UI[key][lang]);
/* ---- part defaults and nouns ---- */
export const KIND_TEXT: Record<
Lang,
Record<string, { noun: string; label?: string; supporting?: string }>
> = {
ja: {
box: { noun: "ボックス" },
button: { noun: "ボタン", label: "ボタン" },
iconButton: { noun: "アイコンボタン" },
fab: { noun: "FAB(フローティングボタン)" },
extendedFab: { noun: "拡張 FAB", label: "作成" },
chip: { noun: "チップ", label: "チップ" },
topAppBar: { noun: "トップアプリバー", label: "タイトル" },
bottomNav: { noun: "ナビゲーションバー" },
navRail: { noun: "ナビゲーションレール" },
searchBar: { noun: "検索バー", label: "検索" },
card: { noun: "カード", label: "カードの見出し", supporting: "補足テキストがここに入ります。" },
listItem: { noun: "リスト項目", label: "リスト項目", supporting: "サブテキスト" },
dialog: { noun: "ダイアログ", label: "確認", supporting: "この操作を実行しますか?" },
snackbar: { noun: "スナックバー", label: "保存しました", supporting: "元に戻す" },
textField: { noun: "テキスト入力", label: "ラベル" },
switch: { noun: "スイッチ", label: "通知" },
checkbox: { noun: "チェックボックス", label: "同意する" },
slider: { noun: "スライダー" },
text: { noun: "テキスト", label: "見出し" },
image: { noun: "画像" },
divider: { noun: "区切り線" },
loadingIndicator: { noun: "ローディングインジケータ" },
linearProgress: { noun: "リニアプログレス" },
circularProgress: { noun: "サーキュラープログレス" },
splitButton: { noun: "スプリットボタン", label: "送信" },
fabMenu: { noun: "FAB メニュー" },
toolbar: { noun: "ツールバー" },
tabs: { noun: "タブ" },
radio: { noun: "ラジオボタン", label: "選択肢" },
badge: { noun: "バッジ", label: "3" },
},
en: {
box: { noun: "box" },
button: { noun: "button", label: "Button" },
iconButton: { noun: "icon button" },
fab: { noun: "FAB" },
extendedFab: { noun: "extended FAB", label: "Create" },
chip: { noun: "chip", label: "Chip" },
topAppBar: { noun: "top app bar", label: "Title" },
bottomNav: { noun: "navigation bar" },
navRail: { noun: "navigation rail" },
searchBar: { noun: "search bar", label: "Search" },
card: { noun: "card", label: "Card headline", supporting: "Supporting text goes here." },
listItem: { noun: "list item", label: "List item", supporting: "Supporting text" },
dialog: { noun: "dialog", label: "Confirm", supporting: "Do you want to continue?" },
snackbar: { noun: "snackbar", label: "Saved", supporting: "Undo" },
textField: { noun: "text field", label: "Label" },
switch: { noun: "switch", label: "Notifications" },
checkbox: { noun: "checkbox", label: "I agree" },
slider: { noun: "slider" },
text: { noun: "text", label: "Headline" },
image: { noun: "image" },
divider: { noun: "divider" },
loadingIndicator: { noun: "loading indicator" },
linearProgress: { noun: "linear progress indicator" },
circularProgress: { noun: "circular progress indicator" },
splitButton: { noun: "split button", label: "Send" },
fabMenu: { noun: "FAB menu" },
toolbar: { noun: "toolbar" },
tabs: { noun: "tabs" },
radio: { noun: "radio button", label: "Option" },
badge: { noun: "badge", label: "3" },
},
zh: {
box: { noun: "容器框" },
button: { noun: "按钮", label: "按钮" },
iconButton: { noun: "图标按钮" },
fab: { noun: "FAB(悬浮按钮)" },
extendedFab: { noun: "扩展 FAB", label: "新建" },
chip: { noun: "标签片", label: "标签" },
topAppBar: { noun: "顶部应用栏", label: "标题" },
bottomNav: { noun: "导航栏" },
navRail: { noun: "侧边导航栏" },
searchBar: { noun: "搜索栏", label: "搜索" },
card: { noun: "卡片", label: "卡片标题", supporting: "这里是辅助说明文字。" },
listItem: { noun: "列表项", label: "列表项", supporting: "辅助文本" },
dialog: { noun: "对话框", label: "确认", supporting: "要执行此操作吗?" },
snackbar: { noun: "消息条", label: "已保存", supporting: "撤销" },
textField: { noun: "文本输入框", label: "标签" },
switch: { noun: "开关", label: "通知" },
checkbox: { noun: "复选框", label: "我同意" },
slider: { noun: "滑块" },
text: { noun: "文本", label: "标题" },
image: { noun: "图片" },
divider: { noun: "分割线" },
loadingIndicator: { noun: "加载指示器" },
linearProgress: { noun: "线性进度条" },
circularProgress: { noun: "圆形进度条" },
splitButton: { noun: "拆分按钮", label: "发送" },
fabMenu: { noun: "FAB 菜单" },
toolbar: { noun: "工具栏" },
tabs: { noun: "标签页" },
radio: { noun: "单选按钮", label: "选项" },
badge: { noun: "徽标", label: "3" },
},
ko: {
box: { noun: "상자" },
button: { noun: "버튼", label: "버튼" },
iconButton: { noun: "아이콘 버튼" },
fab: { noun: "FAB" },
extendedFab: { noun: "확장 FAB", label: "만들기" },
chip: { noun: "칩", label: "칩" },
topAppBar: { noun: "상단 앱 바", label: "제목" },
bottomNav: { noun: "내비게이션 바" },
navRail: { noun: "내비게이션 레일" },
searchBar: { noun: "검색창", label: "검색" },
card: { noun: "카드", label: "카드 제목", supporting: "보조 텍스트가 여기에 표시됩니다." },
listItem: { noun: "목록 항목", label: "목록 항목", supporting: "보조 텍스트" },
dialog: { noun: "대화상자", label: "확인", supporting: "계속하시겠습니까?" },
snackbar: { noun: "스낵바", label: "저장됨", supporting: "실행 취소" },
textField: { noun: "텍스트 입력란", label: "레이블" },
switch: { noun: "스위치", label: "알림" },
checkbox: { noun: "체크박스", label: "동의합니다" },
slider: { noun: "슬라이더" },
text: { noun: "텍스트", label: "제목" },
image: { noun: "이미지" },
divider: { noun: "구분선" },
loadingIndicator: { noun: "로딩 표시기" },
linearProgress: { noun: "선형 진행 표시기" },
circularProgress: { noun: "원형 진행 표시기" },
splitButton: { noun: "분할 버튼", label: "보내기" },
fabMenu: { noun: "FAB 메뉴" },
toolbar: { noun: "도구 모음" },
tabs: { noun: "탭" },
radio: { noun: "라디오 버튼", label: "옵션" },
badge: { noun: "배지", label: "3" },
},
};
/** default labels of a tab row */
export const TAB_LABELS: Record<Lang, string[]> = {
ja: ["おすすめ", "フォロー中", "人気", "新着", "保存済み"],
en: ["For you", "Following", "Trending", "New", "Saved"],
zh: ["推荐", "关注", "热门", "最新", "已保存"],
ko: ["추천", "팔로잉", "인기", "새 항목", "저장됨"],
};
/** default entries of a FAB menu */
export const FAB_MENU_TABS: Record<Lang, { icon: string; label: string }[]> = {
ja: [
{ icon: "edit", label: "メモ" },
{ icon: "photo_camera", label: "写真" },
{ icon: "mic", label: "音声" },
{ icon: "attach_file", label: "ファイル" },
{ icon: "event", label: "予定" },
],
en: [
{ icon: "edit", label: "Note" },
{ icon: "photo_camera", label: "Photo" },
{ icon: "mic", label: "Audio" },
{ icon: "attach_file", label: "File" },
{ icon: "event", label: "Event" },
],
zh: [
{ icon: "edit", label: "笔记" },
{ icon: "photo_camera", label: "照片" },
{ icon: "mic", label: "语音" },
{ icon: "attach_file", label: "文件" },
{ icon: "event", label: "日程" },
],
ko: [
{ icon: "edit", label: "메모" },
{ icon: "photo_camera", label: "사진" },
{ icon: "mic", label: "오디오" },
{ icon: "attach_file", label: "파일" },
{ icon: "event", label: "일정" },
],
};
export const NAV_TABS: Record<Lang, { icon: string; label: string }[]> = {
ja: [
{ icon: "home", label: "ホーム" },
{ icon: "search", label: "検索" },
{ icon: "favorite", label: "保存" },
{ icon: "settings", label: "設定" },
],
en: [
{ icon: "home", label: "Home" },
{ icon: "search", label: "Search" },
{ icon: "favorite", label: "Saved" },
{ icon: "settings", label: "Settings" },
],
zh: [
{ icon: "home", label: "首页" },
{ icon: "search", label: "搜索" },
{ icon: "favorite", label: "收藏" },
{ icon: "settings", label: "设置" },
],
ko: [
{ icon: "home", label: "홈" },
{ icon: "search", label: "검색" },
{ icon: "favorite", label: "저장됨" },
{ icon: "settings", label: "설정" },
],
};
export const TRANSITION_TEXT: Record<Lang, Record<string, string>> = {
ja: {
slide: "右からのスライド",
slideLeft: "左からのスライド",
slideUp: "下からのスライド",
slideDown: "上からのスライド",
fade: "フェード",
expand: "拡大",
none: "アニメーションなし",
},
en: {
slide: "a slide in from the right",
slideLeft: "a slide in from the left",
slideUp: "a slide up from the bottom",
slideDown: "a slide down from the top",
fade: "a fade",
expand: "an expand",
none: "no animation",
},
zh: {
slide: "从右侧滑入",
slideLeft: "从左侧滑入",
slideUp: "从底部滑入",
slideDown: "从顶部滑入",
fade: "淡入",
expand: "放大",
none: "无动画",
},
ko: {
slide: "오른쪽에서 슬라이드",
slideLeft: "왼쪽에서 슬라이드",
slideUp: "아래에서 슬라이드",
slideDown: "위에서 슬라이드",
fade: "페이드",
expand: "확대",
none: "애니메이션 없음",
},
};
export const SWIPE_TEXT: Record<Lang, Record<string, string>> = {
ja: { left: "左へスワイプ", right: "右へスワイプ", up: "上へスワイプ", down: "下へスワイプ" },
en: { left: "swiping left", right: "swiping right", up: "swiping up", down: "swiping down" },
zh: { left: "向左滑动", right: "向右滑动", up: "向上滑动", down: "向下滑动" },
ko: { left: "왼쪽으로 스와이프", right: "오른쪽으로 스와이프", up: "위로 스와이프", down: "아래로 스와이프" },
};