Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions MiniApp/Skills/miniapp-dev/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,57 @@ body {
- iframe 加载后 bridge 会向宿主发送 `bitfun/request-theme`,宿主回推当前主题变量,iframe 内 `_applyThemeVars` 写入 `:root`。
- 主应用切换主题时,宿主会向 iframe 发送 `themeChange` 事件,bridge 更新变量并触发 `onThemeChange` 回调。

## 国际化(i18n)

MiniApp 框架在 V2 之后内置 i18n 支持,开发者**必须**为多语言用户考虑两类文案:

1. **Gallery 元数据**(`name` / `description` / `tags`)—— 在 `meta.json` 顶层加 `i18n.locales` 块,宿主 Gallery / Card / Scene 标题自动按当前语言挑选。
2. **应用内文案**(HTML / JS 中的所有可见字符串)—— 通过 `window.app.locale`、`window.app.onLocaleChange(fn)` 与 `window.app.t(table, fallback)` 实现。

### `meta.json` 多语言示例

```json
{
"id": "your-app",
"name": "默认名(兜底)",
"description": "默认描述",
"tags": ["默认标签"],
"i18n": {
"locales": {
"zh-CN": { "name": "中文名", "description": "中文描述", "tags": ["中文"] },
"en-US": { "name": "English Name", "description": "English desc", "tags": ["en"] }
}
}
}
```

回退顺序:`current` → `en-US` → `zh-CN` → 顶层默认值。

### `window.app` i18n 运行时 API

| 成员 | 说明 |
|------|------|
| `app.locale` | 当前语言 ID(如 `'zh-CN'` / `'en-US'`),随宿主切换更新 |
| `app.onLocaleChange(fn)` | 注册语言切换回调,参数为新 locale 字符串 |
| `app.t(table, fallback)` | 从 `{ 'zh-CN': '...', 'en-US': '...' }` 表挑选字符串;解析顺序:current → en-US → zh-CN → 表的第一项 → fallback |

### HTML 静态文案:`data-i18n` 约定

宿主不强制要求该写法,但推荐 MiniApp 内部统一约定:

- `<span data-i18n="key">默认</span>` —— 切换语言时 `applyStaticI18n()` 读取 `data-i18n` 并替换 `textContent`
- `<div data-i18n="ariaKey" data-i18n-attr="aria-label">...</div>` —— 设置某个属性而非文本

参考 `builtin/assets/gomoku/ui.js` 等内置应用的 `I18N` 表 + `applyStaticI18n()` + `app.onLocaleChange` 三件套即可复用。

### 编写自检清单

- [ ] `meta.json` 已加 `i18n.locales`(至少 `zh-CN` / `en-US`)
- [ ] HTML 中静态文案均带 `data-i18n` 属性
- [ ] JS 内动态拼接的字符串使用 `app.t()` 或自有 `I18N` 表
- [ ] 注册了 `app.onLocaleChange`,切换语言时重新渲染(包括动态列表、aria-label、title)
- [ ] 持久化数据(`app.storage`)保存语言无关的索引/键,而非已翻译的字符串

## 开发约定

### 新增 Agent 工具
Expand Down
23 changes: 23 additions & 0 deletions MiniApp/Skills/miniapp-dev/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ app.appId // string — 当前 MiniApp 的 ID
app.appDataDir // string — 应用数据目录绝对路径
app.workspaceDir // string — 当前工作区路径
app.theme // 'dark' | 'light' — 当前主题
app.locale // string — 当前语言 ID(如 'zh-CN' / 'en-US'),随宿主切换更新
app.platform // 'win32' | 'darwin' | 'linux'
app.mode // 'hosted'
```
Expand Down Expand Up @@ -268,8 +269,30 @@ app.onDeactivate(() => { /* Tab 切走 */ });
app.onThemeChange((payload) => {
// payload: { type: 'dark'|'light', vars: { '--bitfun-bg': '...', ... } }
});
app.onLocaleChange((locale) => {
// locale: 新的语言 ID 字符串(如 'zh-CN' / 'en-US')
});
```

## 国际化 i18n

### `app.t(table, fallback)` — 多语言字符串挑选

```javascript
const label = app.t({ 'zh-CN': '保存', 'en-US': 'Save' }, 'Save');
```

挑选顺序:`app.locale` → `'en-US'` → `'zh-CN'` → 表的第一个值 → `fallback`。适合在 JS 里就地写少量翻译。

更完整的做法(推荐):

1. 在 `meta.json` 顶层加 `i18n.locales` 块翻译 `name` / `description` / `tags`,宿主 Gallery 自动按当前语言显示。
2. 在 HTML 静态文案上加 `data-i18n="key"`(可选 `data-i18n-attr="aria-label"` 翻译属性)。
3. 在 `ui.js` 中维护 `I18N` 字典,封装 `t(key)` 与 `applyStaticI18n()`,并 `app.onLocaleChange(...)` 时重新渲染动态内容。
4. `app.storage` 持久化的字段保存语言无关的索引/键,避免存了翻译后字符串导致切换语言失效。

参考实现:`builtin/assets/gomoku/ui.js`、`builtin/assets/regex-playground/ui.js`。

## 自定义事件

```javascript
Expand Down
33 changes: 30 additions & 3 deletions src/crates/core/src/miniapp/bridge_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,13 @@ pub fn build_bridge_script(
}}

let _theme = {theme_esc};
// Default to en-US until the host pushes the real locale via 'bitfun:event'.
// The script below proactively requests it on startup.
let _locale = 'en-US';

const app = {{
get theme() {{ return _theme; }},
get locale() {{ return _locale; }},
appId: {app_id_esc},
appDataDir: {app_data_esc},
workspaceDir: {workspace_esc},
Expand Down Expand Up @@ -115,10 +119,25 @@ pub fn build_bridge_script(
readText: () => _rpc('clipboard.readText', {{}}),
}},

_lifecycleHandlers: {{ activate: [], deactivate: [], themeChange: [] }},
onActivate: (fn) => app._lifecycleHandlers.activate.push(fn),
onDeactivate: (fn) => app._lifecycleHandlers.deactivate.push(fn),
_lifecycleHandlers: {{ activate: [], deactivate: [], themeChange: [], localeChange: [] }},
onActivate: (fn) => app._lifecycleHandlers.activate.push(fn),
onDeactivate: (fn) => app._lifecycleHandlers.deactivate.push(fn),
onThemeChange: (fn) => app._lifecycleHandlers.themeChange.push(fn),
/// Subscribe to host locale changes. Callback receives the locale id (e.g. "zh-CN").
onLocaleChange: (fn) => app._lifecycleHandlers.localeChange.push(fn),

/// Pick the best-matching string from an i18n table for the current locale.
/// Resolution: current → en-US → zh-CN → first value → fallback.
/// Usage: app.t({{'en-US':'Hello','zh-CN':'你好'}}, 'Hello')
t: (table, fallback) => {{
if (!table || typeof table !== 'object') return fallback != null ? fallback : '';
if (table[_locale]) return table[_locale];
if (table['en-US']) return table['en-US'];
if (table['zh-CN']) return table['zh-CN'];
const keys = Object.keys(table);
if (keys.length) return table[keys[0]];
return fallback != null ? fallback : '';
}},

_eventHandlers: {{}},
on: (event, fn) => {{ (app._eventHandlers[event] = app._eventHandlers[event] || []).push(fn); }},
Expand All @@ -140,6 +159,13 @@ pub fn build_bridge_script(
}}
app._lifecycleHandlers.themeChange.forEach(f => f(payload));
(app._eventHandlers[event] || []).forEach(f => f(payload));
}} else if (event === 'localeChange') {{
if (payload && typeof payload === 'object' && typeof payload.locale === 'string') {{
_locale = payload.locale;
document.documentElement.setAttribute('lang', _locale);
}}
app._lifecycleHandlers.localeChange.forEach(f => f(_locale));
(app._eventHandlers[event] || []).forEach(f => f(_locale));
}} else if (event === 'ai:stream') {{
// Route AI stream chunks to the registered callbacks
if (payload && payload.streamId) {{
Expand Down Expand Up @@ -172,6 +198,7 @@ pub fn build_bridge_script(
window.app = app;
document.documentElement.setAttribute('data-theme-type', _theme);
window.parent.postMessage({{ method: 'bitfun/request-theme' }}, '*');
window.parent.postMessage({{ method: 'bitfun/request-locale' }}, '*');
}})();
"#,
app_id_esc = app_id_esc,
Expand Down
153 changes: 153 additions & 0 deletions src/crates/core/src/miniapp/builtin/assets/coding-selfie/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<div class="cs-shell" id="root">
<section class="cs-empty is-loading" id="empty" role="status" aria-live="polite">
<div class="cs-empty-art" id="empty-icon" aria-hidden="true"></div>
<div class="cs-empty-text">
<p class="cs-empty-title" id="empty-title">正在扫描你的代码足迹</p>
<p class="cs-empty-desc" id="empty-desc">…</p>
</div>
</section>

<main class="cs-bento" id="content" hidden>
<!-- HERO TILE -->
<section class="cs-tile cs-tile-hero" aria-labelledby="hero-greeting">
<div class="cs-hero-glow" aria-hidden="true"></div>
<h1 class="cs-hero-greeting" id="hero-greeting" data-i18n="title">每日编码快照</h1>
<p class="cs-hero-subtitle" id="hero-subtitle" data-i18n="subtitle">扫描你的本地 Git 仓库,凝结成一张今天的编码画像。</p>
<div class="cs-hero-author" id="hero-author"></div>
</section>

<!-- STREAK TILE -->
<section class="cs-tile cs-tile-streak" aria-labelledby="streak-label">
<div class="cs-tile-head">
<svg class="cs-tile-ic" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/></svg>
<span id="streak-label" data-i18n="streakLabel">连续编码</span>
</div>
<div class="cs-streak-num" id="streak-num">0</div>
<div class="cs-streak-unit" data-i18n="streakUnit">DAYS</div>
<div class="cs-streak-foot" id="streak-foot"></div>
</section>

<!-- STAT TILES -->
<section class="cs-tile cs-tile-stat">
<div class="cs-tile-head">
<svg class="cs-tile-ic" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><line x1="3" y1="12" x2="9" y2="12"/><line x1="15" y1="12" x2="21" y2="12"/></svg>
<span data-i18n="todayCommits">今日提交</span>
</div>
<div class="cs-stat-num" id="stat-commits">0</div>
<div class="cs-stat-foot" id="stat-commits-delta"></div>
</section>

<section class="cs-tile cs-tile-stat">
<div class="cs-tile-head">
<svg class="cs-tile-ic" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
<span data-i18n="codeChanges">代码变动</span>
</div>
<div class="cs-stat-num cs-stat-diff">
<span class="cs-add" id="stat-add">+0</span>
<span class="cs-sep">/</span>
<span class="cs-del" id="stat-del">-0</span>
</div>
<div class="cs-stat-foot" id="stat-net"></div>
</section>

<section class="cs-tile cs-tile-stat">
<div class="cs-tile-head">
<svg class="cs-tile-ic" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><path d="m9 18 3-3-3-3"/><path d="m13 18 3-3-3-3"/></svg>
<span data-i18n="filesTouched">涉及文件</span>
</div>
<div class="cs-stat-num" id="stat-files">0</div>
<div class="cs-stat-foot" data-i18n="touchedToday">touched today</div>
</section>

<section class="cs-tile cs-tile-stat cs-tile-optional cs-tile-langs-stat">
<div class="cs-tile-head">
<svg class="cs-tile-ic" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
<span data-i18n="languagesUsed">使用语言</span>
</div>
<div class="cs-stat-num" id="stat-langs">0</div>
<div class="cs-stat-foot" id="stat-langs-list">--</div>
</section>

<!-- DONUT TILE -->
<section class="cs-tile cs-tile-donut" aria-labelledby="donut-title">
<div class="cs-tile-head">
<svg class="cs-tile-ic" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 2a10 10 0 0 1 10 10"/></svg>
<span id="donut-title" data-i18n="donutTitle">本周语言分布</span>
</div>
<div class="cs-donut-row">
<div class="cs-donut-wrap">
<svg class="cs-donut" viewBox="0 0 120 120" id="donut" aria-hidden="true"></svg>
<div class="cs-donut-center">
<div class="cs-donut-value cs-mono" id="donut-total">0</div>
<div class="cs-donut-label" data-i18n="commits">commits</div>
</div>
</div>
<ul class="cs-legend" id="lang-legend"></ul>
</div>
</section>

<!-- HOURS TILE -->
<section class="cs-tile cs-tile-hours cs-tile-optional" aria-labelledby="hours-title">
<div class="cs-tile-head cs-tile-head-row">
<span class="cs-tile-head-left">
<svg class="cs-tile-ic" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
<span id="hours-title" data-i18n="hoursTitle">24 小时活跃节律</span>
</span>
<span class="cs-tile-hint" data-i18n="thisWeek">本周</span>
</div>
<div class="cs-hours" id="hours" role="img" data-i18n-attr="aria-label" data-i18n="hoursAria"></div>
<div class="cs-hours-axis">
<span>00</span><span>06</span><span>12</span><span>18</span><span>23</span>
</div>
<div class="cs-style-badge" id="style-badge"></div>
</section>

<!-- HEATMAP TILE -->
<section class="cs-tile cs-tile-heatmap" aria-labelledby="heatmap-title">
<div class="cs-tile-head cs-tile-head-row">
<span class="cs-tile-head-left">
<svg class="cs-tile-ic" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
<span id="heatmap-title" data-i18n="heatmapTitle">编码热力</span>
</span>
<span class="cs-tile-hint" id="heatmap-hint"></span>
</div>
<div class="cs-heatmap-body" id="heatmap-body" role="img" data-i18n-attr="aria-label" data-i18n="heatmapAria"></div>
<div class="cs-heatmap-legend">
<span data-i18n="less">少</span>
<span class="cs-heat-cell cs-heat-0"></span>
<span class="cs-heat-cell cs-heat-1"></span>
<span class="cs-heat-cell cs-heat-2"></span>
<span class="cs-heat-cell cs-heat-3"></span>
<span class="cs-heat-cell cs-heat-4"></span>
<span data-i18n="more">多</span>
</div>
</section>

<!-- COMMITS TILE -->
<section class="cs-tile cs-tile-commits" aria-labelledby="commits-title">
<div class="cs-tile-head cs-tile-head-row">
<span class="cs-tile-head-left">
<svg class="cs-tile-ic" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><line x1="1.05" y1="12" x2="7" y2="12"/><line x1="17.01" y1="12" x2="22.96" y2="12"/></svg>
<span id="commits-title" data-i18n="commitsTitle">今日提交</span>
</span>
<span class="cs-tile-hint" id="commits-hint"></span>
</div>
<ul class="cs-commits" id="commits-list"></ul>
</section>
</main>

<footer class="cs-meta-footer" id="meta-footer" aria-label="snapshot context">
<span class="cs-meta-chip cs-meta-repo">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>
<span id="meta-repo">--</span>
</span>
<span class="cs-meta-sep" aria-hidden="true">·</span>
<span class="cs-meta-chip" id="meta-date">--</span>
<span class="cs-meta-sep" aria-hidden="true">·</span>
<span class="cs-meta-chip cs-meta-time">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><polyline points="12 7 12 12 15 14"/></svg>
<span id="meta-time">--:--</span>
</span>
<span class="cs-meta-brand" aria-hidden="true" data-i18n="brand">每日编码快照</span>
</footer>
</div>
32 changes: 32 additions & 0 deletions src/crates/core/src/miniapp/builtin/assets/coding-selfie/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"id": "builtin-coding-selfie",
"name": "每日编码快照",
"description": "扫描当前工作区 git 仓库,一屏呈现今日提交、增删行、语言分布、活跃节律与全年编码热力。",
"icon": "Aperture",
"category": "developer",
"tags": ["git", "统计", "报告", "内置"],
"version": 1,
"created_at": 0,
"updated_at": 0,
"permissions": {
"fs": { "read": ["{appdata}", "{workspace}"], "write": ["{appdata}"] },
"shell": { "allow": ["git"] },
"net": { "allow": [] },
"node": { "enabled": true, "max_memory_mb": 256, "timeout_ms": 20000 }
},
"ai_context": null,
"i18n": {
"locales": {
"zh-CN": {
"name": "每日编码快照",
"description": "扫描当前工作区 git 仓库,一屏呈现今日提交、增删行、语言分布、活跃节律与全年编码热力。",
"tags": ["git", "统计", "报告", "内置"]
},
"en-US": {
"name": "Daily Coding Snapshot",
"description": "Scans the current workspace git repo and renders today's commits, ±lines, language mix, active rhythm and yearly heatmap on a single screen.",
"tags": ["git", "stats", "report", "built-in"]
}
}
}
}
Loading
Loading