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
9 changes: 9 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,17 @@ Thanks for your interest in contributing!

- One feature or fix per PR
- `cargo clippy -- -D warnings` and `npm run build` should pass
- `npm run i18n:check` should pass for locale changes
- Use [conventional commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `docs:`, `chore:`)

## Translations

- Source locale is `src/i18n/locales/en.json`
- Add/update translated keys in `src/i18n/locales/<locale>.json`
- New locale files are auto-discovered in-app (including Settings language dropdown); no code changes needed
- Keep placeholders aligned with English (`{count}`, `{name}`, plural vars)
- Validate with `npm run i18n:check`

## Issues

Found a bug? [Open an issue](https://github.com/w0nk1/StepCast/issues) with your macOS version and steps to reproduce.
63 changes: 63 additions & 0 deletions docs/breadcrumbs.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,3 +310,66 @@
2026-02-15
- Confirmed publish run `22037041331` succeeded and produced both arch assets for draft `v0.3.0`, but release metadata remained tied to prior dispatch context (`targetCommitish` mismatch).
- Chose clean provenance path: prepare fresh version/tag `0.3.1` from current `main`.

2026-02-16
- Loaded superpowers skill docs: using-superpowers, brainstorming, writing-plans, executing-plans, test-driven-development, verification-before-completion, requesting-code-review.
- Chosen execution default: implement multilingual MVP now (frontend + settings + contributor file schema), defer backend export/tray and Swift AI localization to planned next phases.
- Started skill-driven flow with explicit plan tracking and docs-first artifacts.
- Implemented frontend i18n MVP: added `src/i18n` runtime, locale catalogs (`en`,`de`), and app/editor providers in `src/main.tsx` + `src/editor.tsx`.
- Added Settings language selector with Tauri sync event (`language-changed`) and editor listener.
- Migrated key UI components to i18n keys: Settings, RecorderPanel, ExportSheet, WelcomeBanner, EditorWindow, EditorStepCard, StepItem, CropEditorModal, StepScreenshot, ImageLightbox, UndoToast.
- Added contributor translation validator `scripts/check-i18n.js`, npm script `i18n:check`, and contributing docs update.
- Verified: `npm run i18n:check`, `npm test` (254/254), `npm run build`.
- Ran requesting-code-review flow with explorer subagent; finding: test isolation risk in `SettingsSheet.test.tsx` due persisted `appLanguage`.
- Applied fix: clear `localStorage` and reset `data-theme` in test `beforeEach`; re-verified full frontend gate.
2026-02-16
- Began Phase 2 backend i18n implementation for multilingual support (export + tray) based on prior Phase 1 frontend rollout.
- Added new Rust locale module `src-tauri/src/i18n.rs` with app-language parsing, locale resolution, translated export/tray strings, and unit tests.
- Extended `export_guide` command in `src-tauri/src/lib.rs` to accept optional `app_language` and pass resolved locale into export pipeline.
- Localized export pipeline:
- `src-tauri/src/export/mod.rs` now routes locale through all formats.
- `src-tauri/src/export/helpers.rs` adds localized action/effective descriptions and auth-placeholder localization guard.
- `src-tauri/src/export/html.rs` and `src-tauri/src/export/markdown.rs` render localized step count/headings/alt text.
- `src-tauri/src/export/pdf.rs` now renders PDF HTML with locale-aware strings.
- Localized tray labels/tooltips in `src-tauri/src/tray.rs` using system locale helpers.
- Updated frontend export invoke in `src/components/RecorderPanel.tsx` to include language and pass resolved locale (`en|de`) when setting is `system`.
- Updated export payload expectation in `src/components/RecorderPanel.test.tsx`.
- Ran independent review subagent; key finding was macOS `system` locale fallback risk.
- Applied review-driven fix:
- Frontend now sends resolved locale for export instead of raw `system`.
- Backend locale detection hardened to read macOS `AppleLocale` + cache result.
- Verification completed:
- `cd src-tauri && cargo test --quiet`
- `npm run i18n:check`
- `npm test`
- `npm run build`
2026-02-16
- Started Phase 3 Apple Intelligence i18n work with spec `docs/specs/2026-02-16-apple-intelligence-i18n-phase3.md`.
- Extended Rust AI bridge:
- `src-tauri/src/lib.rs`: `generate_step_descriptions` now accepts optional `app_language` and resolves locale.
- `src-tauri/src/apple_intelligence/mod.rs`: `GenerateRequest` now includes `app_language`; generation forwards locale (`en|de`) to Swift helper.
- Extended frontend AI invoke payloads:
- `src/components/RecorderPanel.tsx`: stop-triggered `generate_step_descriptions` now sends resolved language.
- `src/components/EditorWindow.tsx`: single-step, bulk, and enhance-all generation now send resolved language.
- Localized Swift helper outputs:
- `src-tauri/swift/stepcast_ai_helper.swift`: locale plumbing (`appLanguage`), localized instructions/fallback errors, optional `availability --lang` support.
- `src-tauri/swift/stepcast_ai_helper_descriptions.swift`: localized baseline sentence templates, prompt text, verb detection, and quality-gate locale-aware checks.
- Review pass (requesting-code-review style) completed; implemented follow-up fixes:
- localized availability details path (`--lang`) for helper availability command,
- hardened `language-changed` listener cleanup in `EditorWindow`.
- Added regression coverage in `src/components/EditorWindow.test.tsx` verifying `language-changed -> appLanguage: "de"` in AI generation invoke payload.
- Verification completed:
- `swiftc -typecheck src-tauri/swift/stepcast_ai_helper.swift src-tauri/swift/stepcast_ai_helper_descriptions.swift src-tauri/swift/stepcast_ai_helper_vision.swift`
- `cd src-tauri && cargo test --quiet`
- `npm test` (255 tests)
- `npm run i18n:check`
- `npm run build`
2026-02-16
- Fixed stopped-state action bar overflow in localized UI: `src/App.css` now uses a strict 3-column grid (`minmax(0,1fr)`) and `src/components/RecorderPanel.tsx` wraps action button labels in `.action-label` for safe truncation.
- Verification: `npm test` (255/255) and `npm run build` pass.
2026-02-16
- Implemented dynamic-language UX scaling: `src/components/SettingsSheet.tsx` now renders a language dropdown backed by i18n `availableLocales` + `getLanguageLabel` instead of hardcoded `en/de` pills.
- Hardened language event sync for future locales in `src/components/EditorWindow.tsx` via `isSupportedAppLanguage`.
- Added select styling in `src/App.css` and adjusted settings tests to combobox interaction in `src/components/SettingsSheet.test.tsx`.
- Updated contributor docs in `CONTRIBUTING.md` to document auto-discovery of new locale JSON files.
- Verification: `npm test` (255/255), `npm run i18n:check`, `npm run build`.
26 changes: 26 additions & 0 deletions docs/choices.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,29 @@

2026-02-15 (release-provenance-clean-cut)
- Default: cut `v0.3.1` from latest `main` instead of publishing `v0.3.0` draft produced via workflow_dispatch commit mismatch; reason: keep tag, source, and artifacts strictly aligned.

2026-02-16 (multilanguage-mvp-kickoff)
- Default: execute multilingual support in phased rollout; Phase 1 = frontend i18n infrastructure + language selector + key UI migration, Phase 2 = export/tray/backend strings, Phase 3 = Swift Apple-Intelligence prompt/baseline localization; reason: deliver contributor-ready value quickly without risky cross-stack big-bang.
- Default: keep English as canonical source locale (`en`) and require all additional locales to mirror key structure; reason: deterministic fallback + safe contributor workflow.
- Default: do not block on branch/worktree setup in this run; reason: user explicitly requested immediate brainstorming -> plan -> execution in current session.
- Default: keep i18n runtime lightweight and in-repo (`src/i18n`) instead of adding external i18n dependency in MVP; reason: lower migration risk and simpler contributor onboarding while preserving key-based catalogs.
- Default: use `appLanguage` (`system|en|de`) persisted in localStorage and synchronized across webviews via Tauri `language-changed` event; reason: mirrors existing `ai-toggle-changed` reliability pattern in Tauri multi-webview setups.
- Default: migrate high-frequency frontend surfaces first (panel/settings/editor/export) and keep Rust export/tray + Swift AI localization as planned follow-up phases; reason: fast user-visible impact with controlled scope.

2026-02-16 (multilanguage-phase2-backend)
- Default: export command receives resolved locale (`en|de`) from frontend even when UI setting is `system`; reason: avoid macOS GUI env-var locale gaps and keep export language consistent with visible UI language.
- Default: backend system locale detection now checks macOS `AppleLocale` (`defaults read -g AppleLocale`) with env fallback and one-time caching; reason: tray localization should remain correct in packaged app launches where `LANG` is often unset.
- Default: keep tray language derived from system locale (not app setting) for now; reason: Phase 2 scope targets startup/system consistency and avoids runtime tray relabel complexity.
- Default: auth placeholder export text is localized only when step description is empty or matches known default placeholder texts; reason: preserve user-authored/custom descriptions.

2026-02-16 (multilanguage-phase3-apple-intelligence)
- Default: `generate_step_descriptions` receives `appLanguage` from frontend and forwards it to the Swift helper request as `app_language`; reason: AI output language must follow in-app language selection, not only system locale.
- Default: keep AI heuristics/classification on canonical internal verbs/kinds and localize only rendered baseline/prompt/verb checks; reason: reduce regression risk in existing quality-gate logic while adding localized output.
- Default: localize helper `availability` details via optional `--lang` argument (system locale from Rust caller); reason: keep fallback/error text language-consistent when model/framework is unavailable.
- Default: add listener cleanup hardening (`cancelled` pattern) for editor `language-changed` event; reason: avoid stale unlisten handles in async listener registration races.
- Default: stopped-state action bar uses fixed 3-column grid with `minmax(0, 1fr)` and label ellipsis; reason: prevent localized button labels from forcing horizontal overflow.

2026-02-16 (multilanguage-phase4-dynamic-locale-selector)
- Default: language selection UI uses a dropdown (`<select>`) instead of segmented pills; reason: scales for many locales and avoids crowded/overflowing controls as translators add languages.
- Default: language options are sourced from dynamically discovered locale catalogs (`availableLocales`) plus `system`; reason: contributor flow should be "add locale file only" with no UI code edits.
- Default: cross-webview `language-changed` handling validates locale via `isSupportedAppLanguage` instead of hardcoded `en/de`; reason: runtime should automatically accept newly added locale files.
23 changes: 23 additions & 0 deletions docs/specs/2026-02-16-apple-intelligence-i18n-phase3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Phase 3: Apple Intelligence i18n (Swift)

## Scope
- Localize Apple Intelligence generated step descriptions for `en`/`de`.
- Pipe frontend app language into `generate_step_descriptions` backend command.
- Keep deterministic baseline behavior and quality-gate safety.

## Design
- Extend Rust command `generate_step_descriptions` with optional `app_language`.
- Forward locale into `apple_intelligence::generate_descriptions` and helper JSON request.
- Extend Swift `GenerateRequest` with `appLanguage`.
- Add lightweight locale helpers in Swift (`en|de`) and localize:
- action verbs
- baseline descriptions
- prompt rules/examples
- quality-gate verb/context checks
- Keep fallback behavior deterministic; unknown locale -> English.

## Verification
- `swiftc -typecheck` helper files
- `cd src-tauri && cargo test --quiet`
- `npm test`
- `npm run build`
23 changes: 23 additions & 0 deletions docs/specs/2026-02-16-backend-i18n-phase2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Backend i18n Phase 2 (Export + Tray)

## Scope
- Localize Rust export text output (HTML/Markdown/PDF text templates and fallback action descriptions).
- Localize tray menu labels/tooltips in Rust.
- Use frontend-selected `appLanguage` for exports (`system|en|de`).
- Use system locale for tray labels/tooltips.

## Non-Goals
- No Swift Apple Intelligence prompt localization yet (Phase 3).
- No runtime tray relabeling on language change (applies at app start).

## Design
- Add `src-tauri/src/i18n.rs` with `Locale` and locale-resolution helpers.
- Add pure translation helpers for export/tray text pieces.
- Extend export pipeline to receive locale and render localized strings.
- Extend `export_guide` command to accept optional `app_language` from frontend.
- Keep English defaults for compatibility and fallback.

## Verification
- Rust unit tests for locale resolution + export/tray strings.
- Existing export tests remain green (English default).
- Frontend tests + build remain green.
17 changes: 17 additions & 0 deletions docs/specs/2026-02-16-dynamic-locale-dropdown.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Dynamic Locale Selector (Contributor-Scale)

## Scope
- Replace hardcoded language pills with a scalable dropdown.
- Ensure language sync accepts any supported locale, not only `en`/`de`.
- Keep contributor workflow simple: add locale JSON, run checks, open PR.

## Design
- Use i18n runtime `availableLocales` + `getLanguageLabel` in `SettingsSheet`.
- Render language selector as `<select>` with options: `system` + discovered locales.
- Validate event payloads with `isSupportedAppLanguage` in `EditorWindow` listener.
- Keep English fallback behavior unchanged in i18n core.

## Verification
- `npm test`
- `npm run i18n:check`
- `npm run build`
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"preview": "vite preview",
"tauri": "tauri",
"session:ai-report": "node scripts/session-ai-report.js",
"i18n:check": "node scripts/check-i18n.js",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
Expand Down
80 changes: 80 additions & 0 deletions scripts/check-i18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env node

import { readdirSync, readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const localesDir = path.resolve(__dirname, "../src/i18n/locales");
const sourceFile = path.join(localesDir, "en.json");

function readJson(filePath) {
return JSON.parse(readFileSync(filePath, "utf8"));
}

function extractPlaceholders(input) {
const text = String(input ?? "");
const names = new Set();
const pluralPattern =
/\{(\w+),\s*plural,\s*one\s*\{[^{}]*\}\s*other\s*\{[^{}]*\}\s*\}/g;

for (const match of text.matchAll(pluralPattern)) {
names.add(match[1]);
}
const withoutPlurals = text.replace(pluralPattern, "");
for (const match of withoutPlurals.matchAll(/\{(\w+)\}/g)) {
names.add(match[1]);
}

return [...names].sort();
}

const source = readJson(sourceFile);
const sourceKeys = Object.keys(source).sort();
const localeFiles = readdirSync(localesDir)
.filter((name) => name.endsWith(".json") && name !== "en.json")
.sort();

let hasIssues = false;

for (const localeFile of localeFiles) {
const localePath = path.join(localesDir, localeFile);
const localeCode = localeFile.replace(/\.json$/, "");
const locale = readJson(localePath);
const localeKeys = Object.keys(locale).sort();

const missing = sourceKeys.filter((k) => !(k in locale));
const extra = localeKeys.filter((k) => !(k in source));

if (missing.length > 0) {
hasIssues = true;
console.error(`[i18n] ${localeCode}: missing keys (${missing.length})`);
for (const key of missing) console.error(` - ${key}`);
}
if (extra.length > 0) {
hasIssues = true;
console.error(`[i18n] ${localeCode}: extra keys (${extra.length})`);
for (const key of extra) console.error(` - ${key}`);
}

for (const key of sourceKeys) {
if (!(key in locale)) continue;
const sourceVars = extractPlaceholders(source[key]);
const localeVars = extractPlaceholders(locale[key]);
if (sourceVars.join("|") !== localeVars.join("|")) {
hasIssues = true;
console.error(`[i18n] ${localeCode}: placeholder mismatch for key "${key}"`);
console.error(` source: [${sourceVars.join(", ")}]`);
console.error(` locale: [${localeVars.join(", ")}]`);
}
}
}

if (hasIssues) {
process.exit(1);
}

console.log(
`[i18n] OK: ${sourceKeys.length} keys checked across ${localeFiles.length} locale(s).`,
);
4 changes: 3 additions & 1 deletion src-tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,9 @@ fn build_swift_ai_helper() {
.arg("runtime")
.arg(&resource_dst)
.status()
.unwrap_or_else(|e| panic!("failed to run codesign for {}: {e}", resource_dst.display()));
.unwrap_or_else(|e| {
panic!("failed to run codesign for {}: {e}", resource_dst.display())
});

if !status.success() {
panic!(
Expand Down
20 changes: 18 additions & 2 deletions src-tauri/src/apple_intelligence/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::i18n::Locale;
use crate::recorder::types::Step;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
Expand All @@ -17,6 +18,8 @@ pub struct GenerateRequest {
pub steps: Vec<Step>,
#[serde(default)]
pub max_chars: Option<usize>,
#[serde(default)]
pub app_language: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -127,19 +130,32 @@ fn run_helper(_args: &[&str], _stdin: Option<&[u8]>) -> Result<Vec<u8>, String>
Err("not supported on this platform".into())
}

pub fn availability() -> Result<AvailabilityResponse, String> {
let out = run_helper(&["availability"], None)?;
pub fn availability(locale: Option<Locale>) -> Result<AvailabilityResponse, String> {
let mut args = vec!["availability"];
if let Some(locale) = locale {
args.push("--lang");
args.push(match locale {
Locale::En => "en",
Locale::De => "de",
});
}
let out = run_helper(&args, None)?;
serde_json::from_slice(&out).map_err(|e| format!("parse availability json: {e}"))
}

pub fn generate_descriptions(
steps: Vec<Step>,
max_chars: usize,
locale: Locale,
) -> Result<GenerateResponse, String> {
// Keep the Swift helper API stable: snake_case JSON.
let req = GenerateRequest {
steps,
max_chars: Some(max_chars),
app_language: Some(match locale {
Locale::En => "en".to_string(),
Locale::De => "de".to_string(),
}),
};
let input = serde_json::to_vec(&req).map_err(|e| format!("encode generate json: {e}"))?;
let out = run_helper(&["generate"], Some(&input))?;
Expand Down
Loading