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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ jobs:
- name: Typecheck (electron)
run: npm run typecheck:electron

- name: UI localization tests
run: npm run test:i18n

- name: Build Next.js
run: npm run build

Expand Down
22 changes: 16 additions & 6 deletions app/global-error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

import { useEffect } from "react";
import { reportError } from "@/lib/sentry";
import {
loadUiLocalePreference,
resolveUiLocale,
systemLanguages,
translate,
} from "@/lib/i18n";

/**
* Last-resort boundary for a crash that escaped the editor's own error handling.
Expand All @@ -18,12 +24,17 @@ export default function GlobalError({
error: Error & { digest?: string };
unstable_retry: () => void;
}) {
const locale = resolveUiLocale(
loadUiLocalePreference(),
systemLanguages()
);
const t = (key: Parameters<typeof translate>[1]) => translate(locale, key);
useEffect(() => {
reportError(error, "render");
}, [error]);

return (
<html lang="en">
<html lang={locale}>
<body
style={{
margin: 0,
Expand All @@ -35,7 +46,7 @@ export default function GlobalError({
color: "var(--fg)",
}}
>
<title>Rescript — something went wrong</title>
<title>{t("globalError.title")}</title>
<style>{`
:root { --bg: #fafafa; --fg: #18181b; --muted: #71717a; --btn: #18181b; --btnfg: #fff; }
@media (prefers-color-scheme: dark) {
Expand All @@ -44,11 +55,10 @@ export default function GlobalError({
`}</style>
<main style={{ padding: "2rem", textAlign: "center", maxWidth: "34rem" }}>
<h1 style={{ fontSize: "17px", fontWeight: 600, margin: 0 }}>
Something went wrong
{t("globalError.heading")}
</h1>
<p style={{ color: "var(--muted)", fontSize: "13px", marginTop: "0.75rem" }}>
The editor hit an unexpected error. Your saved projects are still on
this device — reloading should bring them back.
{t("globalError.body")}
</p>
<button
type="button"
Expand All @@ -65,7 +75,7 @@ export default function GlobalError({
color: "var(--btnfg)",
}}
>
Try again
{t("common.retry")}
</button>
</main>
</body>
Expand Down
6 changes: 6 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const metadata: Metadata = {

/** Apply stored appearance before paint to avoid a light→dark flash. */
const appearanceBootScript = `(function(){try{if(localStorage.getItem("rescript.appearance")==="dark")document.documentElement.classList.add("dark")}catch(e){}})();`;
const localeBootScript = `(function(){try{var p=localStorage.getItem("rescript.ui-locale")||"system";var l=p;if(p==="system"){var a=navigator.languages&&navigator.languages.length?navigator.languages:[navigator.language];l="en";for(var i=0;i<a.length;i++){var v=(a[i]||"").toLowerCase();if(v==="zh"||v.indexOf("zh-")===0){l="zh-CN";break}if(v==="en"||v.indexOf("en-")===0){l="en";break}}}document.documentElement.lang=l==="zh-CN"?"zh-CN":"en"}catch(e){}})();`;

export default function RootLayout({
children,
Expand All @@ -82,6 +83,11 @@ export default function RootLayout({
strategy="beforeInteractive"
dangerouslySetInnerHTML={{ __html: appearanceBootScript }}
/>
<Script
id="locale-boot"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{ __html: localeBootScript }}
/>
</head>
<body className="min-h-full">
{children}
Expand Down
7 changes: 6 additions & 1 deletion app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import dynamic from "next/dynamic";
import I18nProvider from "@/components/I18nProvider";

// The editor relies on browser-only APIs (Workers, WebAssembly, WebGPU),
// so it is rendered exclusively on the client.
Expand All @@ -14,5 +15,9 @@ const Editor = dynamic(() => import("@/components/Editor"), {
});

export default function Home() {
return <Editor />;
return (
<I18nProvider>
<Editor />
</I18nProvider>
);
}
12 changes: 7 additions & 5 deletions components/DesktopAppBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type Platform,
} from "@/lib/platform";
import { usePlatform } from "@/hooks/usePlatform";
import { useI18n } from "./I18nProvider";

const DISMISSED_STORAGE_KEY = "rescript:desktop-banner-dismissed";

Expand Down Expand Up @@ -87,6 +88,7 @@ function PlatformIcon({ platform }: { platform: Platform }) {
* the user dismisses it.
*/
export default function DesktopAppBanner() {
const { t } = useI18n();
const platform = usePlatform();
const [dismissed, setDismissed] = useState(
() => isElectron || readDismissed(),
Expand All @@ -100,9 +102,9 @@ export default function DesktopAppBanner() {
<div className="relative flex h-11 shrink-0 items-center justify-center gap-3 border-b border-blue-200/60 bg-gradient-to-b from-blue-50/70 to-blue-100/50 pr-11 pl-3 text-[13px] text-blue-900 dark:border-blue-400/15 dark:from-blue-950/40 dark:to-blue-900/25 dark:text-blue-100">
<p className="truncate">
<span className="hidden sm:inline">
Want faster transcription and exports?{" "}
{t("banner.faster")}{" "}
</span>
Get the Rescript desktop app
{t("banner.getDesktop")}
</p>
<a
href={downloadUrlFor(platform)}
Expand All @@ -111,15 +113,15 @@ export default function DesktopAppBanner() {
className="flex h-7 shrink-0 items-center gap-1.5 rounded-full bg-white/70 pr-3.5 pl-3 text-[12px] font-medium text-blue-700 ring-1 ring-blue-300/60 transition hover:bg-white hover:ring-blue-400/70 dark:bg-white/10 dark:text-blue-100 dark:ring-white/15 dark:hover:bg-white/15 dark:hover:ring-white/25"
>
<PlatformIcon platform={platform} />
{label ? `Download for ${label}` : "Download"}
{label ? t("banner.downloadFor", { platform: label }) : t("common.download")}
</a>
<button
onClick={() => {
setDismissed(true);
writeDismissed();
}}
title="Dismiss"
aria-label="Dismiss"
title={t("banner.dismiss")}
aria-label={t("banner.dismiss")}
className="absolute right-2.5 flex h-6 w-6 cursor-pointer items-center justify-center rounded-md text-blue-500 transition hover:bg-blue-500/10 hover:text-blue-800 dark:text-blue-300 dark:hover:bg-white/10 dark:hover:text-blue-100"
>
<X size={14} />
Expand Down
18 changes: 10 additions & 8 deletions components/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import ModelSelector, {
import ImportTranscriptOption from "./ImportTranscriptOption";
import { MODEL_ORDER } from "@/lib/models";
import { isTypingTarget } from "@/lib/keyboard";
import { useI18n } from "./I18nProvider";

/** How long the desktop mode-change overlay stays up. Matches the macOS
* `setBounds(..., animate)` duration plus a small buffer so the layout
Expand Down Expand Up @@ -128,6 +129,7 @@ function EditorWorkspace() {
}

export default function Editor() {
const { t } = useI18n();
const status = useEditorStore((s) => s.status);
const videoFile = useEditorStore((s) => s.videoFile);
const skipTranscription = useEditorStore((s) => s.skipTranscription);
Expand Down Expand Up @@ -156,7 +158,7 @@ export default function Editor() {
const { source, pendingTranscript } = useEditorStore.getState();
if (source === "import") {
if (!pendingTranscript) {
alert("Choose a transcript file from the source menu first.");
alert(t("editor.chooseTranscript"));
return;
}
loadVideo(file, {
Expand All @@ -167,7 +169,7 @@ export default function Editor() {
}
loadVideo(file);
},
[loadVideo]
[loadVideo, t]
);

// The pipeline needs SharedArrayBuffer, so a file picked before the page is
Expand All @@ -188,7 +190,7 @@ export default function Editor() {
(file: File | undefined) => {
if (!file) return;
if (!detectMediaKind(file)) {
alert("Please choose a video or audio file.");
alert(t("editor.chooseMedia"));
return;
}
if (!isolated) {
Expand All @@ -197,7 +199,7 @@ export default function Editor() {
}
startMenuFile(file);
},
[isolated, startMenuFile]
[isolated, startMenuFile, t]
);

// Daily-active signal: reports the launch, then again on each day rollover so
Expand Down Expand Up @@ -349,7 +351,7 @@ export default function Editor() {
{status === "idle" ? (
<>
{isElectron && <TopBar>
<ModelSelector groupLabel="Transcript source">
<ModelSelector groupLabel={t("model.transcriptSource")}>
{MODEL_ORDER.map((id) => (
<ModelOption key={id} id={id} />
))}
Expand All @@ -369,15 +371,15 @@ export default function Editor() {
<button
onClick={undo}
disabled={!canUndo}
title="Undo (⌘Z)"
title={t("editor.undo")}
className="flex h-8 w-8 items-center justify-center rounded-lg text-zinc-500 transition hover:bg-zinc-100 hover:text-zinc-800 cursor-pointer disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
>
<Undo2 size={16} />
</button>
<button
onClick={redo}
disabled={!canRedo}
title="Redo (⇧⌘Z)"
title={t("editor.redo")}
className="flex h-8 w-8 items-center justify-center rounded-lg text-zinc-500 transition hover:bg-zinc-100 hover:text-zinc-800 cursor-pointer disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
>
<Redo2 size={16} />
Expand All @@ -389,7 +391,7 @@ export default function Editor() {
className="flex ml-1 h-8 items-center gap-1.5 rounded-full bg-zinc-900 px-4 text-[13px] font-medium text-white transition hover:bg-zinc-700 cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200"
>
<Download size={14} />
Export
{t("editor.export")}
</button>
<div className="mx-1 h-5 w-px bg-zinc-200 dark:bg-zinc-700" />
<SettingsMenu />
Expand Down
Loading