create-ryos-app
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCreating ryOS Applications
创建ryOS应用
Quick Start Checklist
快速入门检查清单
- [ ] 1. Create app directory: src/apps/[app-name]/
- [ ] 2. Create main component: components/[AppName]AppComponent.tsx
- [ ] 3. Create menu bar: components/[AppName]MenuBar.tsx
- [ ] 4. Create logic hook: hooks/use[AppName]Logic.ts
- [ ] 5. Create app definition: index.tsx (include 6 help items)
- [ ] 6. Add icon: public/icons/default/[app-name].png
- [ ] 7. Register in src/config/appRegistry.tsx
- [ ] 8. Add translation keys to src/lib/locales/en/translation.json
- [ ] 9. Localize (last): add en strings, sync locales; use the localize skill to finish- [ ] 1. Create app directory: src/apps/[app-name]/
- [ ] 2. Create main component: components/[AppName]AppComponent.tsx
- [ ] 3. Create menu bar: components/[AppName]MenuBar.tsx
- [ ] 4. Create logic hook: hooks/use[AppName]Logic.ts
- [ ] 5. Create app definition: index.tsx (include 6 help items)
- [ ] 6. Add icon: public/icons/default/[app-name].png
- [ ] 7. Register in src/config/appRegistry.tsx
- [ ] 8. Add translation keys to src/lib/locales/en/translation.json
- [ ] 9. Localize (last): add en strings, sync locales; use the localize skill to finishDirectory Structure
目录结构
src/apps/[app-name]/
├── components/
│ ├── [AppName]AppComponent.tsx # Main component (required)
│ └── [AppName]MenuBar.tsx # Menu bar (required)
├── hooks/
│ └── use[AppName]Logic.ts # Logic hook (recommended)
└── index.tsx # App definition (required)src/apps/[app-name]/
├── components/
│ ├── [AppName]AppComponent.tsx # Main component (required)
│ └── [AppName]MenuBar.tsx # Menu bar (required)
├── hooks/
│ └── use[AppName]Logic.ts # Logic hook (recommended)
└── index.tsx # App definition (required)1. App Definition (index.tsx
)
index.tsx1. 应用定义(index.tsx
)
index.tsxtsx
export const appMetadata = {
name: "[App Name]",
version: "1.0.0",
creator: { name: "Ryo Lu", url: "https://ryo.lu" },
github: "https://github.com/ryokun6/ryos",
icon: "/icons/default/[app-name].png",
};
// Always include exactly 6 help items (icon, title, description each).
export const helpItems = [
{ icon: "🚀", title: "Getting Started", description: "How to use this app" },
{ icon: "📂", title: "Open & Save", description: "Open and save files from the File menu" },
{ icon: "✏️", title: "Editing", description: "Use the Edit menu for cut, copy, paste" },
{ icon: "👁️", title: "View Options", description: "Adjust view and layout from the View menu" },
{ icon: "⌨️", title: "Shortcuts", description: "Use keyboard shortcuts for faster workflows" },
{ icon: "❓", title: "Help & About", description: "Open Help from the Help menu for more info" },
];tsx
export const appMetadata = {
name: "[App Name]",
version: "1.0.0",
creator: { name: "Ryo Lu", url: "https://ryo.lu" },
github: "https://github.com/ryokun6/ryos",
icon: "/icons/default/[app-name].png",
};
// Always include exactly 6 help items (icon, title, description each).
export const helpItems = [
{ icon: "🚀", title: "Getting Started", description: "How to use this app" },
{ icon: "📂", title: "Open & Save", description: "Open and save files from the File menu" },
{ icon: "✏️", title: "Editing", description: "Use the Edit menu for cut, copy, paste" },
{ icon: "👁️", title: "View Options", description: "Adjust view and layout from the View menu" },
{ icon: "⌨️", title: "Shortcuts", description: "Use keyboard shortcuts for faster workflows" },
{ icon: "❓", title: "Help & About", description: "Open Help from the Help menu for more info" },
];2. Main Component ([AppName]AppComponent.tsx
)
[AppName]AppComponent.tsx2. 主组件([AppName]AppComponent.tsx
)
[AppName]AppComponent.tsxtsx
import { WindowFrame } from "@/components/layout/WindowFrame";
import { [AppName]MenuBar } from "./[AppName]MenuBar";
import { AppProps } from "@/apps/base/types";
import { use[AppName]Logic } from "../hooks/use[AppName]Logic";
import { HelpDialog } from "@/components/dialogs/HelpDialog";
import { AboutDialog } from "@/components/dialogs/AboutDialog";
import { appMetadata } from "..";
export function [AppName]AppComponent({
isWindowOpen,
onClose,
isForeground,
skipInitialSound,
instanceId,
}: AppProps) {
const {
t,
translatedHelpItems,
isHelpDialogOpen,
setIsHelpDialogOpen,
isAboutDialogOpen,
setIsAboutDialogOpen,
isXpTheme,
} = use[AppName]Logic({ isWindowOpen, isForeground, instanceId });
const menuBar = (
<[AppName]MenuBar
onClose={onClose}
onShowHelp={() => setIsHelpDialogOpen(true)}
onShowAbout={() => setIsAboutDialogOpen(true)}
/>
);
if (!isWindowOpen) return null;
return (
<>
{!isXpTheme && isForeground && menuBar}
<WindowFrame
title={t("apps.[app-name].title")}
onClose={onClose}
isForeground={isForeground}
appId="[app-name]"
skipInitialSound={skipInitialSound}
instanceId={instanceId}
menuBar={isXpTheme ? menuBar : undefined}
>
<div className="flex flex-col h-full bg-os-window-bg font-os-ui">
{/* App content */}
</div>
</WindowFrame>
<HelpDialog
isOpen={isHelpDialogOpen}
onOpenChange={setIsHelpDialogOpen}
appId="[app-name]"
helpItems={translatedHelpItems}
/>
<AboutDialog
isOpen={isAboutDialogOpen}
onOpenChange={setIsAboutDialogOpen}
metadata={appMetadata}
appId="[app-name]"
/>
</>
);
}tsx
import { WindowFrame } from "@/components/layout/WindowFrame";
import { [AppName]MenuBar } from "./[AppName]MenuBar";
import { AppProps } from "@/apps/base/types";
import { use[AppName]Logic } from "../hooks/use[AppName]Logic";
import { HelpDialog } from "@/components/dialogs/HelpDialog";
import { AboutDialog } from "@/components/dialogs/AboutDialog";
import { appMetadata } from "..";
export function [AppName]AppComponent({
isWindowOpen,
onClose,
isForeground,
skipInitialSound,
instanceId,
}: AppProps) {
const {
t,
translatedHelpItems,
isHelpDialogOpen,
setIsHelpDialogOpen,
isAboutDialogOpen,
setIsAboutDialogOpen,
isXpTheme,
} = use[AppName]Logic({ isWindowOpen, isForeground, instanceId });
const menuBar = (
<[AppName]MenuBar
onClose={onClose}
onShowHelp={() => setIsHelpDialogOpen(true)}
onShowAbout={() => setIsAboutDialogOpen(true)}
/>
);
if (!isWindowOpen) return null;
return (
<>
{!isXpTheme && isForeground && menuBar}
<WindowFrame
title={t("apps.[app-name].title")}
onClose={onClose}
isForeground={isForeground}
appId="[app-name]"
skipInitialSound={skipInitialSound}
instanceId={instanceId}
menuBar={isXpTheme ? menuBar : undefined}
>
<div className="flex flex-col h-full bg-os-window-bg font-os-ui">
{/* App content */}
</div>
</WindowFrame>
<HelpDialog
isOpen={isHelpDialogOpen}
onOpenChange={setIsHelpDialogOpen}
appId="[app-name]"
helpItems={translatedHelpItems}
/>
<AboutDialog
isOpen={isAboutDialogOpen}
onOpenChange={setIsAboutDialogOpen}
metadata={appMetadata}
appId="[app-name]"
/>
</>
);
}3. Logic Hook (use[AppName]Logic.ts
)
use[AppName]Logic.ts3. 逻辑Hook(use[AppName]Logic.ts
)
use[AppName]Logic.tstsx
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useTranslatedHelpItems } from "@/hooks/useTranslatedHelpItems";
import { useThemeStore } from "@/stores/useThemeStore";
import { helpItems } from "..";
export function use[AppName]Logic({ instanceId }: { instanceId: string }) {
const { t } = useTranslation();
const translatedHelpItems = useTranslatedHelpItems("[app-name]", helpItems);
const currentTheme = useThemeStore((state) => state.current);
const isXpTheme = currentTheme === "xp" || currentTheme === "win98";
const [isHelpDialogOpen, setIsHelpDialogOpen] = useState(false);
const [isAboutDialogOpen, setIsAboutDialogOpen] = useState(false);
return {
t,
translatedHelpItems,
isXpTheme,
isHelpDialogOpen,
setIsHelpDialogOpen,
isAboutDialogOpen,
setIsAboutDialogOpen,
};
}tsx
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useTranslatedHelpItems } from "@/hooks/useTranslatedHelpItems";
import { useThemeStore } from "@/stores/useThemeStore";
import { helpItems } from "..";
export function use[AppName]Logic({ instanceId }: { instanceId: string }) {
const { t } = useTranslation();
const translatedHelpItems = useTranslatedHelpItems("[app-name]", helpItems);
const currentTheme = useThemeStore((state) => state.current);
const isXpTheme = currentTheme === "xp" || currentTheme === "win98";
const [isHelpDialogOpen, setIsHelpDialogOpen] = useState(false);
const [isAboutDialogOpen, setIsAboutDialogOpen] = useState(false);
return {
t,
translatedHelpItems,
isXpTheme,
isHelpDialogOpen,
setIsHelpDialogOpen,
isAboutDialogOpen,
setIsAboutDialogOpen,
};
}4. Menu Bar ([AppName]MenuBar.tsx
)
[AppName]MenuBar.tsx4. 菜单栏([AppName]MenuBar.tsx
)
[AppName]MenuBar.tsxMatch existing app menubars: structure, classes, and spacing.
- Wrapper: — no extra gap between menus (layout uses
<MenuBar inWindowFrame={isXpTheme}>).space-x-0 - Trigger: .
MenubarTrigger className="text-md px-2 py-1 border-none focus-visible:ring-0" - Content: .
MenubarContent align="start" sideOffset={1} className="px-0" - Items: .
MenubarItem className="text-md h-6 px-3" - Separators: .
MenubarSeparator className="h-[2px] bg-black my-1"
tsx
import { MenuBar } from "@/components/layout/MenuBar";
import {
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
} from "@/components/ui/menubar";
import { useThemeStore } from "@/stores/useThemeStore";
import { useTranslation } from "react-i18next";
interface [AppName]MenuBarProps {
onClose: () => void;
onShowHelp: () => void;
onShowAbout: () => void;
}
export function [AppName]MenuBar({ onClose, onShowHelp, onShowAbout }: [AppName]MenuBarProps) {
const { t } = useTranslation();
const currentTheme = useThemeStore((state) => state.current);
const isXpTheme = currentTheme === "xp" || currentTheme === "win98";
const isMacOsxTheme = currentTheme === "macosx";
return (
<MenuBar inWindowFrame={isXpTheme}>
<MenubarMenu>
<MenubarTrigger className="text-md px-2 py-1 border-none focus-visible:ring-0">
{t("common.menu.file")}
</MenubarTrigger>
<MenubarContent align="start" sideOffset={1} className="px-0">
<MenubarSeparator className="h-[2px] bg-black my-1" />
<MenubarItem onClick={onClose} className="text-md h-6 px-3">
{t("common.menu.close")}
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger className="text-md px-2 py-1 border-none focus-visible:ring-0">
{t("common.menu.help")}
</MenubarTrigger>
<MenubarContent align="start" sideOffset={1} className="px-0">
<MenubarItem onClick={onShowHelp} className="text-md h-6 px-3">
{t("apps.[app-name].menu.help")}
</MenubarItem>
{!isMacOsxTheme && (
<>
<MenubarSeparator className="h-[2px] bg-black my-1" />
<MenubarItem onClick={onShowAbout} className="text-md h-6 px-3">
{t("apps.[app-name].menu.about")}
</MenubarItem>
</>
)}
</MenubarContent>
</MenubarMenu>
</MenuBar>
);
}与现有应用菜单栏保持一致:结构、类名和间距都要对齐。
- 外层容器: — 菜单之间无额外间距(布局使用
<MenuBar inWindowFrame={isXpTheme}>)。space-x-0 - 触发器: 。
MenubarTrigger className="text-md px-2 py-1 border-none focus-visible:ring-0" - 内容容器: 。
MenubarContent align="start" sideOffset={1} className="px-0" - 菜单项: 。
MenubarItem className="text-md h-6 px-3" - 分隔符: 。
MenubarSeparator className="h-[2px] bg-black my-1"
tsx
import { MenuBar } from "@/components/layout/MenuBar";
import {
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
} from "@/components/ui/menubar";
import { useThemeStore } from "@/stores/useThemeStore";
import { useTranslation } from "react-i18next";
interface [AppName]MenuBarProps {
onClose: () => void;
onShowHelp: () => void;
onShowAbout: () => void;
}
export function [AppName]MenuBar({ onClose, onShowHelp, onShowAbout }: [AppName]MenuBarProps) {
const { t } = useTranslation();
const currentTheme = useThemeStore((state) => state.current);
const isXpTheme = currentTheme === "xp" || currentTheme === "win98";
const isMacOsxTheme = currentTheme === "macosx";
return (
<MenuBar inWindowFrame={isXpTheme}>
<MenubarMenu>
<MenubarTrigger className="text-md px-2 py-1 border-none focus-visible:ring-0">
{t("common.menu.file")}
</MenubarTrigger>
<MenubarContent align="start" sideOffset={1} className="px-0">
<MenubarSeparator className="h-[2px] bg-black my-1" />
<MenubarItem onClick={onClose} className="text-md h-6 px-3">
{t("common.menu.close")}
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger className="text-md px-2 py-1 border-none focus-visible:ring-0">
{t("common.menu.help")}
</MenubarTrigger>
<MenubarContent align="start" sideOffset={1} className="px-0">
<MenubarItem onClick={onShowHelp} className="text-md h-6 px-3">
{t("apps.[app-name].menu.help")}
</MenubarItem>
{!isMacOsxTheme && (
<>
<MenubarSeparator className="h-[2px] bg-black my-1" />
<MenubarItem onClick={onShowAbout} className="text-md h-6 px-3">
{t("apps.[app-name].menu.about")}
</MenubarItem>
</>
)}
</MenubarContent>
</MenubarMenu>
</MenuBar>
);
}5. Register in appRegistry.tsx
appRegistry.tsx5. 在appRegistry.tsx
中注册
appRegistry.tsxtsx
// Import
import { appMetadata as [appName]Metadata, helpItems as [appName]HelpItems } from "@/apps/[app-name]";
// Lazy component
const Lazy[AppName]App = createLazyComponent<unknown>(
() => import("@/apps/[app-name]/components/[AppName]AppComponent")
.then(m => ({ default: m.[AppName]AppComponent })),
"[app-name]"
);
// Add to registry
["[app-name]"]: {
id: "[app-name]",
name: "[App Name]",
icon: { type: "image", src: [appName]Metadata.icon },
description: "App description",
component: Lazy[AppName]App,
helpItems: [appName]HelpItems,
metadata: [appName]Metadata,
windowConfig: {
defaultSize: { width: 650, height: 475 },
minSize: { width: 400, height: 300 },
} as WindowConstraints,
},tsx
// Import
import { appMetadata as [appName]Metadata, helpItems as [appName]HelpItems } from "@/apps/[app-name]";
// Lazy component
const Lazy[AppName]App = createLazyComponent<unknown>(
() => import("@/apps/[app-name]/components/[AppName]AppComponent")
.then(m => ({ default: m.[AppName]AppComponent })),
"[app-name]"
);
// Add to registry
["[app-name]"]: {
id: "[app-name]",
name: "[App Name]",
icon: { type: "image", src: [appName]Metadata.icon },
description: "App description",
component: Lazy[AppName]App,
helpItems: [appName]HelpItems,
metadata: [appName]Metadata,
windowConfig: {
defaultSize: { width: 650, height: 475 },
minSize: { width: 400, height: 300 },
} as WindowConstraints,
},AppProps Interface
AppProps接口
| Prop | Type | Description |
|---|---|---|
| | Window visibility |
| | Close handler |
| | Window is active |
| | Unique instance ID |
| | Skip open sound |
| | Optional startup data |
| 属性 | 类型 | 描述 |
|---|---|---|
| | 窗口可见性 |
| | 关闭事件处理函数 |
| | 窗口是否处于激活状态 |
| | 唯一实例ID |
| | 跳过启动音效 |
| | 可选启动数据 |
Menu Bar Placement
菜单栏位置
- macOS/System7: Render outside WindowFrame when
isForeground - XP/Win98: Pass via prop to WindowFrame
menuBar
tsx
const isXpTheme = currentTheme === "xp" || currentTheme === "win98";
return (
<>
{!isXpTheme && isForeground && menuBar}
<WindowFrame menuBar={isXpTheme ? menuBar : undefined}>- macOS/System7: 当为true时渲染在WindowFrame外部
isForeground - XP/Win98: 通过属性传递给WindowFrame
menuBar
tsx
const isXpTheme = currentTheme === "xp" || currentTheme === "win98";
return (
<>
{!isXpTheme && isForeground && menuBar}
<WindowFrame menuBar={isXpTheme ? menuBar : undefined}>WindowFrame Options
WindowFrame配置项
| Prop | Values | Use |
|---|---|---|
| | Window style |
| | Show save dialog before close |
| | Preserve state when minimized |
| 属性 | 可选值 | 用途 |
|---|---|---|
| | 窗口样式 |
| | 关闭前显示保存确认弹窗 |
| | 最小化时保留组件状态 |
Common Patterns
通用模式
Initial Data
初始数据
tsx
interface ViewerInitialData { filePath: string; }
export function ViewerAppComponent({ initialData }: AppProps<ViewerInitialData>) {
const filePath = initialData?.filePath ?? "";
}tsx
interface ViewerInitialData { filePath: string; }
export function ViewerAppComponent({ initialData }: AppProps<ViewerInitialData>) {
const filePath = initialData?.filePath ?? "";
}Launch Other Apps
启动其他应用
tsx
import { useLaunchApp } from "@/hooks/useLaunchApp";
const launchApp = useLaunchApp();
launchApp("photos", { path: "/image.png" });tsx
import { useLaunchApp } from "@/hooks/useLaunchApp";
const launchApp = useLaunchApp();
launchApp("photos", { path: "/image.png" });Global Store (Zustand)
全局状态存储(Zustand)
tsx
// src/stores/use[AppName]Store.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";
export const use[AppName]Store = create<State>()(
persist((set) => ({ /* state and actions */ }), { name: "[app-name]-storage" })
);tsx
// src/stores/use[AppName]Store.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";
export const use[AppName]Store = create<State>()(
persist((set) => ({ /* state and actions */ }), { name: "[app-name]-storage" })
);9. Localize (Do Last)
9. 本地化(最后执行)
After the app is built and wired up, finish by localizing:
- Add translation keys for all user-facing strings (menu labels, dialogs, status, help).
- Add English entries under in
apps.[app-name].*.src/lib/locales/en/translation.json - Sync other locales (e.g. ).
bun run scripts/sync-translations.ts --mark-untranslated
Use the localize skill for the full workflow: extract strings → calls → en keys → sync. Do this step last so all UI copy is stable before extracting and syncing.
t()应用构建并完成逻辑关联后,最后执行本地化操作:
- 为所有用户可见文本添加翻译key(菜单标签、弹窗、状态、帮助内容)。
- 在的
src/lib/locales/en/translation.json节点下添加英文翻译条目。apps.[app-name].* - 同步其他语言包(例如执行)。
bun run scripts/sync-translations.ts --mark-untranslated
使用localize技能完成完整流程:提取文本→调用→英文key配置→同步语言包。请最后执行此步骤,确保所有UI文本稳定后再进行提取和同步。
t()