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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
19 changes: 19 additions & 0 deletions lingui.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { defineConfig } from "@lingui/cli";
import { formatter } from "@lingui/format-po";

// i18n is renderer-only: macros are expanded by Babel in the Vite pipeline, and
// the main process (tsdown) carries no catalogs. Adding a language is two steps:
// add its code to `locales` here (and to SUPPORTED_LOCALES in
// src/shared/locale.ts), then run `pnpm i18n:extract`.
export default defineConfig({
sourceLocale: "en",
locales: ["en", "es", "ru", "uk", "zh-CN", "ja", "pt-BR", "de", "fr", "ko", "pl", "vi", "tr"],
catalogs: [
{
path: "src/renderer/locales/{locale}/messages",
include: ["src/renderer"],
exclude: ["**/*.test.*", "**/node_modules/**"],
},
],
format: formatter({ lineNumbers: false }),
});
14 changes: 11 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@
"lint:fix": "oxlint --type-aware --fix .",
"fmt": "oxfmt --write .",
"fmt:check": "oxfmt --check .",
"test": "vitest run",
"test:perf:cli-hook": "vitest run src/supervisor/runtime/cliHookEventChain.perf.test.ts",
"test:integration:providers": "vitest run --config vitest.integration.config.ts",
"test": "vitest run --configLoader runner",
"test:perf:cli-hook": "vitest run --configLoader runner src/supervisor/runtime/cliHookEventChain.perf.test.ts",
"test:integration:providers": "vitest run --configLoader runner --config vitest.integration.config.ts",
"update-server": "node scripts/update-server.mjs",
"db:clone": "node scripts/clone-db.mjs",
"i18n:extract": "lingui extract",
"i18n:extract:clean": "lingui extract --clean",
"prepare": "husky"
},
"dependencies": {
Expand All @@ -68,6 +70,8 @@
"@heroui/styles": "^3.0.3",
"@huggingface/transformers": "^4.2.0",
"@lightcode/agents-usage": "workspace:*",
"@lingui/core": "6.2.0",
"@lingui/react": "6.2.0",
"@monaco-editor/react": "^4.7.0",
"@opencode-ai/sdk": "^1.14.48",
"@sentry/electron": "^7.13.0",
Expand Down Expand Up @@ -109,6 +113,10 @@
"devDependencies": {
"@babel/core": "^7.29.0",
"@electron/rebuild": "^4.0.4",
"@lingui/babel-plugin-lingui-macro": "6.2.0",
"@lingui/cli": "6.2.0",
"@lingui/format-po": "6.2.0",
"@lingui/vite-plugin": "6.2.0",
"@rolldown/plugin-babel": "^0.2.3",
"@sentry/cli": "^3.4.2",
"@tailwindcss/postcss": "^4.2.4",
Expand Down
429 changes: 429 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/main/sharedSettingsFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ describe("sharedSettingsFile", () => {
writeSharedSettingsFile(settingsPath, {
themeMode: "dark",
themePreset: "default",
locale: "system",
gitTextLanguage: "en",
terminalPosition: "right",
commitGenProvider: "auto",
commitGenModel: "",
Expand Down Expand Up @@ -124,6 +126,8 @@ describe("sharedSettingsFile", () => {
expect(readSharedSettingsFile(settingsPath)).toEqual({
themeMode: "dark",
themePreset: "default",
locale: "system",
gitTextLanguage: "en",
terminalPosition: "right",
commitGenProvider: "auto",
commitGenModel: "",
Expand Down
30 changes: 17 additions & 13 deletions src/renderer/RendererCrashScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { Component, useState, type ErrorInfo, type ReactNode } from "react";
import { Copy, RefreshCw } from "lucide-react";
import { msg } from "@lingui/core/macro";
import type { MessageDescriptor } from "@lingui/core";
import { Button } from "./components/common";
import { captureRendererException } from "./diagnostics/sentry";
import { i18n } from "./i18n/i18n";

export type RendererCrashKind = "bootstrap" | "react" | "uncaught" | "unhandled-rejection";

Expand Down Expand Up @@ -117,16 +120,16 @@ export function formatRendererCrashReport(report: RendererCrashReport): string {
return lines.filter((line): line is string => line !== null).join("\n");
}

function crashTitle(kind: RendererCrashKind): string {
function crashTitle(kind: RendererCrashKind): MessageDescriptor {
switch (kind) {
case "bootstrap":
return "Renderer failed during startup";
return msg`Renderer failed during startup`;
case "react":
return "Renderer hit a React error";
return msg`Renderer hit a React error`;
case "unhandled-rejection":
return "Renderer hit an unhandled promise rejection";
return msg`Renderer hit an unhandled promise rejection`;
case "uncaught":
return "Renderer hit an uncaught error";
return msg`Renderer hit an uncaught error`;
}
}

Expand All @@ -153,25 +156,26 @@ export function RendererCrashScreen(props: RendererCrashScreenProps) {
<div className="flex min-h-0 w-full flex-col gap-4 px-8 pt-14 pb-8">
<header className="flex shrink-0 items-start justify-between gap-4">
<div className="min-w-0">
<p className="text-xs font-medium text-danger">Renderer crashed</p>
<h1 className="mt-2 text-xl font-semibold">{crashTitle(report.kind)}</h1>
<p className="text-xs font-medium text-danger">{i18n._(msg`Renderer crashed`)}</p>
<h1 className="mt-2 text-xl font-semibold">{i18n._(crashTitle(report.kind))}</h1>
<p className="mt-1 max-w-3xl text-sm text-muted">
The normal app shell could not render. The diagnostics below are shown before reload
so the failure can be investigated.
{i18n._(
msg`The normal app shell could not render. The diagnostics below are shown before reload so the failure can be investigated.`,
)}
</p>
</div>
<div className="flex shrink-0 items-center gap-2">
<Button size="sm" variant="secondary" onPress={() => window.location.reload()}>
<RefreshCw className="size-3.5" />
Reload
{i18n._(msg`Reload`)}
</Button>
<Button size="sm" variant="secondary" onPress={() => void copyDiagnostics()}>
<Copy className="size-3.5" />
{copyState === "copied"
? "Copied"
? i18n._(msg`Copied`)
: copyState === "failed"
? "Copy failed"
: "Copy diagnostics"}
? i18n._(msg`Copy failed`)
: i18n._(msg`Copy diagnostics`)}
</Button>
</div>
</header>
Expand Down
14 changes: 10 additions & 4 deletions src/renderer/actions/agentLoginActions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { toast } from "@heroui/react";
import { msg } from "@lingui/core/macro";
import type { Project, ProjectLocation } from "@/shared/contracts";
import { stripAnsi } from "@/shared/ansi";
import { readBridge } from "@/renderer/bridge";
import { i18n } from "@/renderer/i18n/i18n";
import { useAppStore } from "@/renderer/state/appStore";
import { useDevTerminalStore } from "@/renderer/state/devTerminalStore";
import { useLoginTerminalStore } from "@/renderer/state/loginTerminalStore";
Expand Down Expand Up @@ -45,7 +47,7 @@ export function runAgentLoginCommand(input: {
}): boolean {
const project = input.project ?? resolveLoginProject();
if (!project) {
toast.warning("Add a project before signing in.");
toast.warning(i18n._(msg`Add a project before signing in.`));
return false;
}

Expand Down Expand Up @@ -120,7 +122,9 @@ export function runAgentLoginCommand(input: {
// (and leave callers' pending UI stuck). Tear it down and report failure.
stopWatching();
fireOnce(-1);
toast.danger(error instanceof Error ? error.message : `Unable to open ${input.label} login.`);
toast.danger(
error instanceof Error ? error.message : i18n._(msg`Unable to open ${input.label} login.`),
);
useLoginTerminalStore.getState().close();
});
writeScriptToShell(shellId, script);
Expand All @@ -144,7 +148,7 @@ export function runAgentInstallCommand(input: {
}): boolean {
const project = input.project ?? resolveLoginProject();
if (!project) {
toast.warning("Add a project before installing an agent.");
toast.warning(i18n._(msg`Add a project before installing an agent.`));
return false;
}

Expand Down Expand Up @@ -201,7 +205,9 @@ export function runAgentInstallCommand(input: {
.catch((error) => {
stopWatching();
fireOnce(-1);
toast.danger(error instanceof Error ? error.message : `Unable to install ${input.label}.`);
toast.danger(
error instanceof Error ? error.message : i18n._(msg`Unable to install ${input.label}.`),
);
useLoginTerminalStore.getState().close();
});
writeScriptToShell(shellId, script);
Expand Down
4 changes: 3 additions & 1 deletion src/renderer/actions/worktreeActions.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { toast } from "@heroui/react";
import { msg } from "@lingui/core/macro";
import { buildWorktreeLocation } from "@/shared/worktree";
import { errorDetail } from "@/shared/messages";
import type { Project } from "@/shared/contracts";
import { readBridge } from "@/renderer/bridge";
import { i18n } from "@/renderer/i18n/i18n";
import { useAppStore } from "@/renderer/state/appStore";
import { useDevTerminalStore } from "@/renderer/state/devTerminalStore";
import { useFileEditorStore } from "@/renderer/state/fileEditorStore";
Expand Down Expand Up @@ -59,7 +61,7 @@ export async function performWorktreeRemoval(
} catch (err: unknown) {
const detail = errorDetail(err);
console.warn(`[renderer] failed to remove worktree ${worktreePath}:`, detail);
toast.danger(detail || "Unable to remove worktree.");
toast.danger(detail || i18n._(msg`Unable to remove worktree.`));
return;
}

Expand Down
3 changes: 2 additions & 1 deletion src/renderer/app.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Fragment, type ReactNode } from "react";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
import { renderWithI18n as render } from "@/renderer/testUtils/i18n";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { useAppStore } from "./state/appStore";
import { useGitStore } from "./state/gitStore";
Expand Down
5 changes: 4 additions & 1 deletion src/renderer/app.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { toast } from "@heroui/react";
import { Trans } from "@lingui/react/macro";
import { useEffect } from "react";
import { PixelLoader } from "./components/common";
import { msg } from "@/shared/messages";
Expand Down Expand Up @@ -272,7 +273,9 @@ export function App() {
<div className="flex h-screen w-screen items-center justify-center bg-background text-foreground">
<div className="flex flex-col items-center gap-4">
<PixelLoader size="lg" />
<p className="text-sm text-muted">Loading&hellip;</p>
<p className="text-sm text-muted">
<Trans>Loading…</Trans>
</p>
</div>
</div>
</AppProvider>
Expand Down
36 changes: 25 additions & 11 deletions src/renderer/commands/CommandPalette.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useEffect, useRef, useState } from "react";
import { Input, Label, Modal } from "@heroui/react";
import { Trans, useLingui } from "@lingui/react/macro";
import type { MessageDescriptor } from "@lingui/core";
import { Command, Search } from "lucide-react";
import { readBridge } from "@/renderer/bridge";
import { useAppStore } from "@/renderer/state/appStore";
Expand All @@ -18,6 +20,7 @@ import {
const MAX_VISIBLE_COMMANDS = 80;

export function CommandPalette() {
const { t } = useLingui();
const isOpen = useCommandPaletteStore((state) => state.isOpen);
const close = useCommandPaletteStore((state) => state.close);
const keybindings = useKeybindingStore((state) => state.keybindings);
Expand All @@ -37,7 +40,9 @@ export function CommandPalette() {
const commands = buildCommandRegistry().filter((command) =>
isCommandAvailable(command, whenContext),
);
const filteredCommands = filterCommands(commands, query).slice(0, MAX_VISIBLE_COMMANDS);
const resolve = (value: string | MessageDescriptor): string =>
typeof value === "string" ? value : t(value);
const filteredCommands = filterCommands(commands, query, resolve).slice(0, MAX_VISIBLE_COMMANDS);
const activeCommand = filteredCommands[activeIndex];

useEffect(() => {
Expand Down Expand Up @@ -76,9 +81,12 @@ export function CommandPalette() {
<Search className="size-4 shrink-0 text-muted" />
<Input
ref={inputRef}
aria-label="Command"
aria-label={t({
message: "Command",
comment: "Accessible label for the command palette search input",
})}
variant="secondary"
placeholder="Type a command"
placeholder={t`Type a command`}
value={query}
onChange={(event) => {
setQuery(event.target.value);
Expand All @@ -101,7 +109,7 @@ export function CommandPalette() {
</div>
<div className="max-h-[min(520px,70vh)] overflow-y-auto p-2">
{filteredCommands.length > 0 ? (
<div role="listbox" aria-label="Commands" className="space-y-1">
<div role="listbox" aria-label={t`Commands`} className="space-y-1">
{filteredCommands.map((command, index) => {
const shortcut = shortcutForCommand(command.id, keybindings);
return (
Expand All @@ -118,9 +126,9 @@ export function CommandPalette() {
>
<Command className="size-4 shrink-0 text-muted" />
<span className="min-w-0 flex-1">
<Label className="block truncate text-sm">{command.title}</Label>
<Label className="block truncate text-sm">{resolve(command.title)}</Label>
<span className="block truncate text-xs text-muted">
{command.subtitle ?? command.group}
{resolve(command.subtitle ?? command.group)}
</span>
</span>
{shortcut ? (
Expand All @@ -133,7 +141,9 @@ export function CommandPalette() {
})}
</div>
) : (
<div className="px-3 py-8 text-center text-sm text-muted">No commands found</div>
<div className="px-3 py-8 text-center text-sm text-muted">
<Trans>No commands found</Trans>
</div>
)}
</div>
</Modal.Dialog>
Expand All @@ -142,16 +152,20 @@ export function CommandPalette() {
);
}

function filterCommands(commands: AppCommand[], query: string): AppCommand[] {
function filterCommands(
commands: AppCommand[],
query: string,
resolve: (value: string | MessageDescriptor) => string,
): AppCommand[] {
const normalized = query.trim().toLowerCase();
if (!normalized) return commands;
const terms = normalized.split(/\s+/);
return commands.filter((command) => {
const haystack = [
command.id,
command.title,
command.group,
command.subtitle ?? "",
resolve(command.title),
resolve(command.group),
command.subtitle ? resolve(command.subtitle) : "",
...(command.keywords ?? []),
]
.join(" ")
Expand Down
Loading