Skip to content
Open
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
6 changes: 2 additions & 4 deletions apps/chrome-extension/e2e/overlay-ui.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,9 +346,7 @@ test("clicking X in the panel closes the panel and the camera preview", async ()

const panelFrame = frameWithUrl(targetPage, "popup.html");
if (!panelFrame) throw new Error("panel frame missing");
await panelFrame
.locator('button[aria-label="Close Cap and hide all recorder UI"]')
.click();
await panelFrame.getByTestId("close-recorder-ui").click();

// The panel and the camera preview should both tear down.
await expect
Expand Down Expand Up @@ -419,7 +417,7 @@ test("a failed recording start reopens the panel with the error", async () => {
.toBe(true);
const panelFrame = frameWithUrl(targetPage, "popup.html");
if (!panelFrame) throw new Error("panel frame missing");
await expect(panelFrame.getByText("Recording failed.")).toBeVisible({
await expect(panelFrame.getByTestId("recording-start-error")).toBeVisible({
timeout: 10_000,
});
await targetPage.screenshot({
Expand Down
17 changes: 17 additions & 0 deletions apps/chrome-extension/public/_locales/en/messages.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"actionDefaultTitle": {
"message": "Record your screen with Cap"
},
"extensionDescription": {
"message": "Free, open source screen recorder. Capture your screen, tab, camera & mic in Chrome and share a video link the moment you stop."
},
"extensionName": {
"message": "Cap - Screen Recorder & Screen Capture"
},
"extensionShortName": {
"message": "Cap"
},
"offscreenJustification": {
"message": "Record and upload Cap videos from an extension page."
}
}
17 changes: 17 additions & 0 deletions apps/chrome-extension/public/_locales/zh_CN/messages.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"actionDefaultTitle": {
"message": "使用 Cap 录制屏幕"
},
"extensionDescription": {
"message": "免费开源的屏幕录制工具。在 Chrome 中录制屏幕、标签页、摄像头和麦克风,停止录制后即可分享视频链接。"
},
"extensionName": {
"message": "Cap - 屏幕录制与截图"
},
"extensionShortName": {
"message": "Cap"
},
"offscreenJustification": {
"message": "从扩展页面录制并上传 Cap 视频。"
}
}
9 changes: 5 additions & 4 deletions apps/chrome-extension/public/manifest.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
{
"manifest_version": 3,
"name": "Cap - Screen Recorder & Screen Capture",
"short_name": "Cap",
"description": "Free, open source screen recorder. Capture your screen, tab, camera & mic in Chrome and share a video link the moment you stop.",
"name": "__MSG_extensionName__",
"short_name": "__MSG_extensionShortName__",
"description": "__MSG_extensionDescription__",
"default_locale": "en",
"version": "1.0.2",
"homepage_url": "https://cap.so",
"minimum_chrome_version": "116",
Expand All @@ -13,7 +14,7 @@
"128": "icons/icon-128.png"
},
"action": {
"default_title": "Record your screen with Cap",
"default_title": "__MSG_actionDefaultTitle__",
"default_icon": {
"16": "icons/icon-16.png",
"32": "icons/icon-32.png",
Expand Down
3 changes: 2 additions & 1 deletion apps/chrome-extension/src/background/service-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
parseAuthResponse,
revokeAuth,
} from "../shared/api";
import { msg } from "../shared/i18n";
import {
isRecordingStatusBroadcast,
isServiceWorkerRequest,
Expand Down Expand Up @@ -219,7 +220,7 @@ const createOffscreenDocument = () =>
{
url: OFFSCREEN_URL,
reasons: ["USER_MEDIA", "DISPLAY_MEDIA", "BLOBS", "AUDIO_PLAYBACK"],
justification: "Record and upload Cap videos from an extension page.",
justification: msg("offscreenJustification"),
},
() => {
const error = chrome.runtime.lastError;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const RecorderHeader = ({
: "cursor-pointer transition-transform hover:scale-110",
)}
aria-label="Close Cap and hide all recorder UI"
data-testid="close-recorder-ui"
>
<X
size={10}
Expand Down
5 changes: 4 additions & 1 deletion apps/chrome-extension/src/popup/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,10 @@ function App() {
</div>
)}
{status.phase === "error" && (
<div className="cap-fade-up rounded-md border border-red-6 bg-red-3/70 px-3 py-2 text-xs leading-snug text-red-12">
<div
className="cap-fade-up rounded-md border border-red-6 bg-red-3/70 px-3 py-2 text-xs leading-snug text-red-12"
data-testid="recording-start-error"
>
<span className="font-medium">Recording failed.</span>{" "}
{status.message}
</div>
Expand Down
71 changes: 71 additions & 0 deletions apps/chrome-extension/src/shared/i18n.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import englishMessages from "../../public/_locales/en/messages.json";
import chineseMessages from "../../public/_locales/zh_CN/messages.json";
import { type MessageKey, msg } from "./i18n";

afterEach(() => {
vi.unstubAllGlobals();
});

describe("extension messages", () => {
it("keeps locale catalogs aligned", () => {
expect(Object.keys(chineseMessages).sort()).toEqual(
Object.keys(englishMessages).sort(),
);
});

it("uses the browser locale when a translation is available", () => {
const getMessage = vi.fn(() => "使用 Cap 录制屏幕");
vi.stubGlobal("chrome", { i18n: { getMessage } });

expect(msg("actionDefaultTitle")).toBe("使用 Cap 录制屏幕");
expect(getMessage).toHaveBeenCalledWith("actionDefaultTitle", undefined);
});

it("falls back to English outside an extension context", () => {
vi.stubGlobal("chrome", undefined);

expect(msg("actionDefaultTitle")).toBe("Record your screen with Cap");
});

it("falls back to English when Chrome i18n is unavailable", () => {
vi.stubGlobal("chrome", {});

expect(msg("actionDefaultTitle")).toBe("Record your screen with Cap");
});

it("falls back to English when Chrome cannot resolve a message", () => {
vi.stubGlobal("chrome", {
i18n: { getMessage: vi.fn(() => "") },
});

expect(msg("offscreenJustification")).toBe(
"Record and upload Cap videos from an extension page.",
);
});

it("applies substitutions to the English fallback", () => {
const originalMessage = englishMessages.actionDefaultTitle.message;
vi.stubGlobal("chrome", undefined);

try {
englishMessages.actionDefaultTitle.message = "Record $1";
expect(msg("actionDefaultTitle", "your screen")).toBe(
"Record your screen",
);

englishMessages.actionDefaultTitle.message = "Record $1 with $2";
expect(msg("actionDefaultTitle", ["your screen", "Cap"])).toBe(
"Record your screen with Cap",
);
} finally {
englishMessages.actionDefaultTitle.message = originalMessage;
}
});

it("returns the key when the English fallback is missing", () => {
vi.stubGlobal("chrome", undefined);

expect(msg("missingMessage" as MessageKey)).toBe("missingMessage");
});
});
22 changes: 22 additions & 0 deletions apps/chrome-extension/src/shared/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import englishMessages from "../../public/_locales/en/messages.json";

export type MessageKey = keyof typeof englishMessages & string;

export const msg = (key: MessageKey, substitutions?: string | string[]) => {
const localizedMessage =
typeof chrome === "undefined" ||
typeof chrome.i18n?.getMessage !== "function"
? ""
: chrome.i18n.getMessage(key, substitutions);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

window.chrome can exist outside an extension context (but without i18n), so typeof chrome === "undefined" alone can still blow up here. I’d guard i18n.getMessage as well, and maybe add a test for that case.

Suggested change
: chrome.i18n.getMessage(key, substitutions);
typeof chrome === "undefined" || typeof (chrome as any).i18n?.getMessage !== "function"
? ""
: chrome.i18n.getMessage(key, substitutions);


if (localizedMessage) return localizedMessage;

const fallback = englishMessages[key]?.message || key;
if (!substitutions) return fallback;

const values = Array.isArray(substitutions) ? substitutions : [substitutions];
return fallback.replace(
/\$(\d+)/g,
(_match, index: string) => values[Number(index) - 1] ?? "",
);
};