Compare commits

...
Author SHA1 Message Date
SiddharthandClaude Opus 4.7 f42c478725 fix homebrew cask audit warnings
- Use #{version} interpolation in URLs so brew detects them as versioned
  (silences "Use sha256 :no_check when URL is unversioned").
- Drop blank line between on_arm and on_intel (same stanza group).
- Alphabetize zap trash array.
- Add verified: stanza for the GitHub release URL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:22:58 -07:00
Sid 64c2a43006 Merge pull request #562 from siddharthvaddem/chore/homebrew-cask-workflow
chore: add homebrew cask bump workflow
2026-05-09 16:07:47 -07:00
Sid d8da26a41a Merge pull request #561 from auberginewly/fix/electron-screen-capture-permissions
fix(macOS): fix three screen capture permission issues in Electron layer
2026-05-09 15:10:47 -07:00
Sid f0699cc8b0 Merge pull request #507 from hthienloc/add-vietnamese-i18n-1022783609047552672
Add Vietnamese i18n support (vi locale)
2026-05-09 14:48:31 -07:00
Siddharth 3ad3e22a16 test(i18n): add vi to tutorialHelpTranslations locale map 2026-05-09 14:43:56 -07:00
Siddharth 2381e48a46 Merge main into add-vietnamese-i18n-1022783609047552672
Resolve conflict in electron/i18n.ts by keeping both `ar` (from main) and `vi` (from this branch). Also add `vi` to SUPPORTED_LOCALES in src/i18n/config.ts so Vietnamese is selectable in the language picker.
2026-05-09 14:35:03 -07:00
auberginewly 3dd7b85ebb fix(build): remove misplaced entitlement key from mac.extendInfo
com.apple.security.device.audio-input is an entitlement key and should
only appear in macos.entitlements. Placing it in extendInfo writes it
into Info.plist where it has no effect and is misleading.

The correct entry already exists in macos.entitlements; this removes
the redundant, incorrectly-placed duplicate.
2026-05-10 05:30:48 +08:00
auberginewly be4e2d0c94 fix(electron/macOS): proactively check screen recording permission on startup
Microphone permission is checked at startup via getMediaAccessStatus, and
camera has a dedicated request-camera-access IPC handler, but screen
recording relied entirely on desktopCapturer.getSources() to implicitly
trigger the TCC prompt — causing the permission dialog to reappear on
every launch (issue #558).

Note: askForMediaAccess() only accepts "microphone" | "camera"; screen
recording TCC is triggered via desktopCapturer.getSources() instead.

Fix:
- Import desktopCapturer in main.ts
- Call getMediaAccessStatus("screen") in app.whenReady(); trigger the
  TCC prompt via getSources when status is "not-determined"
- Add request-screen-access IPC handler symmetric to request-camera-access
2026-05-10 05:30:42 +08:00
auberginewly c9b6074626 fix(electron): add screen and display-capture to Electron permission allowlists
setPermissionCheckHandler and setPermissionRequestHandler only allowed
["media", "audioCapture", "microphone", "videoCapture", "camera"], causing
any renderer-side getUserMedia/desktopCapturer request using a screen source
to be silently denied by Electron before macOS TCC is ever consulted.

Fix: add "screen" and "display-capture" to both handler allowlists.
2026-05-10 05:24:19 +08:00
google-labs-jules[bot]andhthienloc 37c1ea5984 Add Vietnamese i18n support (vi locale)
Co-authored-by: hthienloc <148019203+hthienloc@users.noreply.github.com>
2026-04-30 16:27:57 +00:00
14 changed files with 533 additions and 13 deletions
+11 -5
View File
@@ -112,18 +112,24 @@ jobs:
mkdir -p tap/Casks
BASE="https://github.com/${REPO}/releases/download/${TAG}"
# #{version} is Ruby interpolation written literally to the cask
# file (bash heredoc leaves "#{...}" alone). \${VERSION}, \${ARM_SHA},
# etc. are bash variables expanded by the heredoc. The literal
# #{version} fixes Homebrew's "URL is unversioned" audit warning by
# making the version string statically detectable.
cat > "tap/Casks/${CASK_NAME}.rb" <<EOF
cask "${CASK_NAME}" do
version "${VERSION}"
on_arm do
sha256 "${ARM_SHA}"
url "${BASE}/${ARM_NAME}"
url "https://github.com/${REPO}/releases/download/v#{version}/${ARM_NAME}",
verified: "github.com/${REPO}/"
end
on_intel do
sha256 "${X64_SHA}"
url "${BASE}/${X64_NAME}"
url "https://github.com/${REPO}/releases/download/v#{version}/${X64_NAME}",
verified: "github.com/${REPO}/"
end
name "Openscreen"
@@ -137,9 +143,9 @@ jobs:
zap trash: [
"~/Library/Application Support/Openscreen",
"~/Library/Preferences/com.siddharthvaddem.openscreen.plist",
"~/Library/Logs/Openscreen",
"~/Library/Caches/com.siddharthvaddem.openscreen",
"~/Library/Logs/Openscreen",
"~/Library/Preferences/com.siddharthvaddem.openscreen.plist",
"~/Library/Saved Application State/com.siddharthvaddem.openscreen.savedState",
]
end
+1 -2
View File
@@ -52,8 +52,7 @@
"NSMicrophoneUsageDescription": "OpenScreen needs microphone access to record voice audio.",
"NSCameraUsageDescription": "OpenScreen needs camera access to record webcam video.",
"NSScreenCaptureUsageDescription": "OpenScreen needs screen recording permission to detect and capture windows.",
"NSCameraUseContinuityCameraDeviceType": true,
"com.apple.security.device.audio-input": true
"NSCameraUseContinuityCameraDeviceType": true
}
},
"linux": {
+6 -2
View File
@@ -15,12 +15,14 @@ import commonKo from "../src/i18n/locales/ko-KR/common.json";
import dialogsKo from "../src/i18n/locales/ko-KR/dialogs.json";
import commonTr from "../src/i18n/locales/tr/common.json";
import dialogsTr from "../src/i18n/locales/tr/dialogs.json";
import commonVi from "../src/i18n/locales/vi/common.json";
import dialogsVi from "../src/i18n/locales/vi/dialogs.json";
import commonZh from "../src/i18n/locales/zh-CN/common.json";
import dialogsZh from "../src/i18n/locales/zh-CN/dialogs.json";
import commonZhTw from "../src/i18n/locales/zh-TW/common.json";
import dialogsZhTw from "../src/i18n/locales/zh-TW/dialogs.json";
type Locale = "en" | "zh-CN" | "zh-TW" | "es" | "fr" | "ja-JP" | "ko-KR" | "tr" | "ar";
type Locale = "en" | "zh-CN" | "zh-TW" | "es" | "fr" | "ja-JP" | "ko-KR" | "tr" | "ar" | "vi";
type Namespace = "common" | "dialogs";
type MessageMap = Record<string, unknown>;
@@ -34,6 +36,7 @@ const messages: Record<Locale, Record<Namespace, MessageMap>> = {
"ko-KR": { common: commonKo, dialogs: dialogsKo },
tr: { common: commonTr, dialogs: dialogsTr },
ar: { common: commonAr, dialogs: dialogsAr },
vi: { common: commonVi, dialogs: dialogsVi },
};
let currentLocale: Locale = "en";
@@ -48,7 +51,8 @@ export function setMainLocale(locale: string) {
locale === "ja-JP" ||
locale === "ko-KR" ||
locale === "tr" ||
locale === "ar"
locale === "ar" ||
locale === "vi"
) {
currentLocale = locale;
}
+26
View File
@@ -718,6 +718,32 @@ export function registerIpcHandlers(
}
});
ipcMain.handle("request-screen-access", async () => {
if (process.platform !== "darwin") {
return { success: true, granted: true, status: "granted" };
}
try {
const status = systemPreferences.getMediaAccessStatus("screen");
if (status === "granted") {
return { success: true, granted: true, status };
}
// Screen recording has no askForMediaAccess equivalent — the TCC prompt
// is triggered by desktopCapturer.getSources(). Fire it and return so
// the renderer can re-check status after the user responds.
if (status === "not-determined") {
desktopCapturer.getSources({ types: ["screen"] }).catch(() => {});
return { success: true, granted: false, status: "not-determined" };
}
return { success: true, granted: false, status };
} catch (error) {
console.error("Failed to request screen access:", error);
return { success: false, granted: false, status: "unknown", error: String(error) };
}
});
// macOS Accessibility prompt for global click capture. First call shows the
// system dialog; the user has to toggle the app in System Settings (no
// programmatic grant exists for Accessibility).
+30 -4
View File
@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
import {
app,
BrowserWindow,
desktopCapturer,
ipcMain,
Menu,
nativeImage,
@@ -449,23 +450,48 @@ app.whenReady().then(async () => {
app.dock?.show();
}
// Allow microphone/media permission checks
// Allow microphone/media/screen permission checks
session.defaultSession.setPermissionCheckHandler((_webContents, permission) => {
const allowed = ["media", "audioCapture", "microphone", "videoCapture", "camera"];
const allowed = [
"media",
"audioCapture",
"microphone",
"videoCapture",
"camera",
"screen",
"display-capture",
];
return allowed.includes(permission);
});
session.defaultSession.setPermissionRequestHandler((_webContents, permission, callback) => {
const allowed = ["media", "audioCapture", "microphone", "videoCapture", "camera"];
const allowed = [
"media",
"audioCapture",
"microphone",
"videoCapture",
"camera",
"screen",
"display-capture",
];
callback(allowed.includes(permission));
});
// Request microphone permission from macOS
// Request microphone and screen recording permissions from macOS
if (process.platform === "darwin") {
const micStatus = systemPreferences.getMediaAccessStatus("microphone");
if (micStatus !== "granted") {
await systemPreferences.askForMediaAccess("microphone");
}
// Screen recording has no askForMediaAccess equivalent — the TCC prompt is
// triggered by the first desktopCapturer.getSources() call. Firing it here
// at startup settles the permission state early and prevents repeated prompts
// driven by later getSources() calls (fixes repeated permission dialog).
const screenStatus = systemPreferences.getMediaAccessStatus("screen");
if (screenStatus === "not-determined") {
desktopCapturer.getSources({ types: ["screen"] }).catch(() => {});
}
}
// Listen for HUD overlay quit event (macOS only)
@@ -8,6 +8,7 @@ import jaJPDialogs from "@/i18n/locales/ja-JP/dialogs.json";
import koKRDialogs from "@/i18n/locales/ko-KR/dialogs.json";
import ruDialogs from "@/i18n/locales/ru/dialogs.json";
import trDialogs from "@/i18n/locales/tr/dialogs.json";
import viDialogs from "@/i18n/locales/vi/dialogs.json";
import zhCNDialogs from "@/i18n/locales/zh-CN/dialogs.json";
import zhTWDialogs from "@/i18n/locales/zh-TW/dialogs.json";
@@ -47,6 +48,7 @@ const dialogsByLocale = {
ru: ruDialogs,
"ja-JP": jaJPDialogs,
ar: arDialogs,
vi: viDialogs,
} satisfies Record<Locale, { tutorial: Record<string, unknown> }>;
describe("TutorialHelp translations", () => {
+1
View File
@@ -10,6 +10,7 @@ export const SUPPORTED_LOCALES = [
"ja-JP",
"ar",
"ru",
"vi",
] as const;
export const I18N_NAMESPACES = [
"common",
+30
View File
@@ -0,0 +1,30 @@
{
"actions": {
"cancel": "Hủy",
"save": "Lưu",
"delete": "Xóa",
"close": "Đóng",
"share": "Chia sẻ",
"done": "Hoàn tất",
"open": "Mở",
"upload": "Tải lên",
"export": "Xuất",
"showInFolder": "Hiển thị trong thư mục",
"file": "Tệp",
"edit": "Chỉnh sửa",
"view": "Xem",
"window": "Cửa sổ",
"quit": "Thoát",
"stopRecording": "Dừng ghi hình"
},
"playback": {
"play": "Phát",
"pause": "Tạm dừng",
"fullscreen": "Toàn màn hình",
"exitFullscreen": "Thoát toàn màn hình"
},
"locale": {
"name": "Tiếng Việt",
"short": "VI"
}
}
+70
View File
@@ -0,0 +1,70 @@
{
"export": {
"complete": "Xuất hoàn tất",
"yourFormatReady": "{{format}} của bạn đã sẵn sàng",
"showInFolder": "Hiển thị trong thư mục",
"finalizingVideo": "Đang hoàn tất xuất video...",
"compilingGifProgress": "Đang biên dịch GIF... {{progress}}%",
"compilingGifWait": "Đang biên dịch GIF... Có thể mất một lúc",
"takeMoment": "Có thể mất một chút thời gian...",
"failed": "Xuất thất bại",
"tryAgain": "Vui lòng thử lại",
"finalizingVideoTitle": "Hoàn tất video",
"compilingGif": "Biên dịch GIF",
"exportingFormat": "Đang xuất {{format}}",
"compiling": "Đang biên dịch",
"renderingFrames": "Kết xuất khung hình",
"processing": "Đang xử lý...",
"finalizing": "Đang hoàn tất...",
"compilingStatus": "Đang biên dịch...",
"status": "Trạng thái",
"format": "Định dạng",
"frames": "Khung hình",
"cancelExport": "Hủy xuất",
"savedSuccessfully": "Đã lưu {{format}} thành công!"
},
"tutorial": {
"triggerLabel": "Cách hoạt động của công cụ cắt",
"title": "Cách cắt video",
"description": "Hiểu cách cắt bỏ các phần không mong muốn trong video của bạn.",
"explanationBefore": "Công cụ Cắt hoạt động bằng cách xác định các đoạn bạn muốn",
"remove": "xóa",
"explanationMiddle": " — bất cứ thứ gì",
"covered": "được bao phủ",
"explanationAfter": "bởi một đoạn cắt màu đỏ sẽ bị loại bỏ khi bạn xuất.",
"visualExample": "Ví dụ trực quan",
"removed": "ĐÃ XÓA",
"kept": "Giữ lại",
"part1": "Phần 1",
"part2": "Phần 2",
"part3": "Phần 3",
"finalVideo": "Video cuối cùng",
"step1Title": "1. Thêm đoạn cắt",
"step1DescriptionBefore": "Nhấn ",
"step1DescriptionAfter": " hoặc nhấp vào biểu tượng cái kéo để đánh dấu một phần cần xóa.",
"step2Title": "2. Điều chỉnh",
"step2Description": "Kéo các cạnh của vùng màu đỏ để bao phủ chính xác những gì bạn muốn cắt bỏ."
},
"unsavedChanges": {
"title": "Thay đổi chưa được lưu",
"message": "Bạn có các thay đổi chưa được lưu.",
"detail": "Bạn có muốn lưu dự án của mình trước khi đóng không?",
"saveAndClose": "Lưu & Đóng",
"discardAndClose": "Bỏ qua & Đóng",
"loadProject": "Tải dự án…",
"saveProject": "Lưu dự án…",
"saveProjectAs": "Lưu dự án thành…"
},
"fileDialogs": {
"saveGif": "Lưu GIF đã xuất",
"saveVideo": "Lưu Video đã xuất",
"selectVideo": "Chọn tệp video",
"saveProject": "Lưu dự án OpenScreen",
"openProject": "Mở dự án OpenScreen",
"gifImage": "Hình ảnh GIF",
"mp4Video": "Video MP4",
"videoFiles": "Tệp Video",
"openscreenProject": "Dự án OpenScreen",
"allFiles": "Tất cả các tệp"
}
}
+45
View File
@@ -0,0 +1,45 @@
{
"newRecording": {
"title": "Quay lại Trình ghi",
"description": "Phiên hiện tại của bạn đã được lưu.",
"cancel": "Hủy",
"confirm": "Xác nhận"
},
"loadingVideo": "Đang tải video...",
"errors": {
"noVideoLoaded": "Chưa tải video nào",
"videoNotReady": "Video chưa sẵn sàng",
"unableToDetermineSourcePath": "Không thể xác định đường dẫn video gốc",
"failedToSaveGif": "Không thể lưu GIF",
"gifExportFailed": "Xuất GIF thất bại",
"failedToSaveVideo": "Không thể lưu video",
"exportFailed": "Xuất thất bại",
"exportFailedWithError": "Xuất thất bại: {{error}}",
"exportBackgroundLoadFailed": "Xuất thất bại: không thể tải hình nền ({{url}})",
"failedToSaveExport": "Không thể lưu bản xuất",
"failedToSaveExportedVideo": "Không thể lưu video đã xuất",
"failedToRevealInFolder": "Lỗi khi hiển thị trong thư mục: {{error}}"
},
"export": {
"canceled": "Đã hủy xuất",
"exportedSuccessfully": "Đã xuất {{format}} thành công"
},
"project": {
"saveCanceled": "Đã hủy lưu dự án",
"failedToSave": "Lưu dự án thất bại",
"savedTo": "Đã lưu dự án vào {{path}}",
"failedToLoad": "Tải dự án thất bại",
"invalidFormat": "Định dạng tệp dự án không hợp lệ",
"loadedFrom": "Đã tải dự án từ {{path}}"
},
"recording": {
"failedCameraAccess": "Yêu cầu quyền truy cập máy ảnh thất bại.",
"cameraBlocked": "Quyền truy cập máy ảnh bị chặn. Hãy bật nó trong cài đặt hệ thống để sử dụng webcam.",
"systemAudioUnavailable": "Âm thanh hệ thống không khả dụng. Ghi hình không có âm thanh hệ thống.",
"microphoneDenied": "Quyền truy cập micro bị từ chối. Sẽ tiếp tục ghi hình không có âm thanh.",
"cameraDenied": "Quyền truy cập máy ảnh bị từ chối. Sẽ tiếp tục ghi hình không có webcam.",
"cameraDisconnected": "Webcam bị ngắt kết nối.",
"cameraNotFound": "Không tìm thấy máy ảnh.",
"permissionDenied": "Quyền ghi hình bị từ chối. Vui lòng cho phép ghi màn hình."
}
}
+43
View File
@@ -0,0 +1,43 @@
{
"tooltips": {
"hideHUD": "Ẩn HUD",
"closeApp": "Đóng ứng dụng",
"restartRecording": "Khởi động lại ghi hình",
"cancelRecording": "Hủy ghi hình",
"pauseRecording": "Tạm dừng ghi hình",
"resumeRecording": "Tiếp tục ghi hình",
"openVideoFile": "Mở tệp video",
"openProject": "Mở dự án"
},
"audio": {
"enableSystemAudio": "Bật âm thanh hệ thống",
"disableSystemAudio": "Tắt âm thanh hệ thống",
"enableMicrophone": "Bật micro",
"disableMicrophone": "Tắt micro",
"defaultMicrophone": "Micro Mặc định"
},
"webcam": {
"enableWebcam": "Bật webcam",
"disableWebcam": "Tắt webcam",
"defaultCamera": "Máy ảnh Mặc định",
"searching": "Đang tìm kiếm...",
"noneFound": "Không tìm thấy máy ảnh",
"unavailable": "Máy ảnh không khả dụng"
},
"sourceSelector": {
"loading": "Đang tải nguồn...",
"screens": "Màn hình ({{count}})",
"windows": "Cửa sổ ({{count}})",
"defaultSourceName": "Màn hình"
},
"recording": {
"selectSource": "Vui lòng chọn một nguồn để ghi"
},
"language": "Ngôn ngữ",
"systemLanguagePrompt": {
"title": "Sử dụng ngôn ngữ hệ thống của bạn?",
"description": "Chúng tôi phát hiện {{language}} là ngôn ngữ hệ thống của bạn. Bạn có muốn chuyển OpenScreen sang {{language}} không?",
"switch": "Chuyển sang {{language}}",
"keepDefault": "Giữ ngôn ngữ hiện tại"
}
}
+176
View File
@@ -0,0 +1,176 @@
{
"zoom": {
"level": "Mức độ thu phóng",
"selectRegion": "Chọn vùng thu phóng để điều chỉnh",
"deleteZoom": "Xóa thu phóng",
"focusMode": {
"title": "Chế độ lấy nét",
"manual": "Thủ công",
"auto": "Tự động",
"autoDescription": "Máy ảnh đi theo vị trí con trỏ đã ghi"
}
},
"speed": {
"playbackSpeed": "Tốc độ phát",
"selectRegion": "Chọn vùng tốc độ để điều chỉnh",
"deleteRegion": "Xóa vùng tốc độ",
"customPlaybackSpeed": "Tốc độ phát tùy chỉnh",
"maxSpeedError": "Tốc độ không thể cao hơn 16×"
},
"trim": {
"deleteRegion": "Xóa vùng cắt"
},
"layout": {
"title": "Bố cục",
"preset": "Cài đặt sẵn",
"selectPreset": "Chọn cài đặt sẵn",
"pictureInPicture": "Hình trong hình",
"verticalStack": "Xếp chồng dọc",
"dualFrame": "Khung kép",
"webcamShape": "Hình dạng máy ảnh",
"webcamSize": "Kích thước Webcam"
},
"effects": {
"title": "Hiệu ứng video",
"blurBg": "Làm mờ nền",
"motionBlur": "Làm mờ chuyển động",
"off": "tắt",
"shadow": "Bóng đổ",
"roundness": "Độ bo tròn",
"padding": "Phần đệm"
},
"background": {
"title": "Nền",
"image": "Hình ảnh",
"color": "Màu sắc",
"gradient": "Dải màu",
"uploadCustom": "Tải lên tùy chỉnh",
"gradientLabel": "Dải màu {{index}}"
},
"crop": {
"title": "Cắt xén",
"cropVideo": "Cắt xén video",
"dragInstruction": "Kéo ở mỗi cạnh để điều chỉnh vùng cắt xén",
"ratio": "Tỷ lệ",
"free": "Tự do",
"done": "Hoàn tất",
"lockAspectRatio": "Khóa tỷ lệ khung hình",
"unlockAspectRatio": "Mở khóa tỷ lệ khung hình"
},
"exportFormat": {
"mp4": "MP4",
"gif": "GIF",
"mp4Video": "Video MP4",
"mp4Description": "Tệp video chất lượng cao",
"gifAnimation": "Ảnh động GIF",
"gifDescription": "Hình ảnh động để chia sẻ"
},
"exportQuality": {
"title": "Chất lượng xuất",
"low": "Thấp",
"medium": "Trung bình",
"high": "Cao"
},
"gifSettings": {
"frameRate": "Tốc độ khung hình GIF",
"size": "Kích thước GIF",
"loop": "Lặp lại GIF"
},
"project": {
"save": "Lưu dự án",
"load": "Tải dự án"
},
"export": {
"videoButton": "Xuất Video",
"gifButton": "Xuất GIF",
"chooseSaveLocation": "Chọn vị trí lưu"
},
"links": {
"reportBug": "Báo cáo lỗi",
"starOnGithub": "Đánh giá sao trên GitHub"
},
"imageUpload": {
"invalidFileType": "Loại tệp không hợp lệ",
"jpgOnly": "Vui lòng tải lên tệp hình ảnh JPG hoặc JPEG.",
"uploadSuccess": "Tải lên hình ảnh tùy chỉnh thành công!",
"failedToUpload": "Tải lên hình ảnh thất bại",
"errorReading": "Đã xảy ra lỗi khi đọc tệp."
},
"annotation": {
"title": "Cài đặt chú thích",
"active": "Hoạt động",
"typeText": "Văn bản",
"typeImage": "Hình ảnh",
"typeArrow": "Mũi tên",
"typeBlur": "Làm mờ",
"textContent": "Nội dung văn bản",
"textPlaceholder": "Nhập văn bản của bạn...",
"fontStyle": "Kiểu phông chữ",
"selectStyle": "Chọn kiểu",
"size": "Kích thước",
"customFonts": "Phông chữ tùy chỉnh",
"textColor": "Màu văn bản",
"background": "Nền",
"none": "Không có",
"color": "Màu sắc",
"clearBackground": "Xóa nền",
"uploadImage": "Tải lên hình ảnh",
"supportedFormats": "Định dạng hỗ trợ: JPG, PNG, GIF, WebP",
"arrowDirection": "Hướng mũi tên",
"strokeWidth": "Độ dày nét: {{width}}px",
"arrowColor": "Màu mũi tên",
"blurType": "Loại làm mờ",
"blurTypeBlur": "Làm mờ",
"blurTypeMosaic": "Khảm",
"blurColor": "Màu làm mờ",
"blurColorWhite": "Trắng",
"blurColorBlack": "Đen",
"blurShape": "Hình dạng làm mờ",
"blurIntensity": "Cường độ làm mờ",
"mosaicBlockSize": "Kích thước khối khảm",
"blurShapeRectangle": "Chữ nhật",
"blurShapeOval": "Bầu dục",
"blurShapeFreehand": "Vẽ tự do",
"deleteAnnotation": "Xóa chú thích",
"shortcutsAndTips": "Phím tắt & Mẹo",
"tipMovePlayhead": "Di chuyển đầu phát đến phần chú thích chồng chéo và chọn một mục.",
"tipTabCycle": "Sử dụng Tab để chuyển qua các mục chồng chéo.",
"tipShiftTabCycle": "Sử dụng Shift+Tab để chuyển ngược lại.",
"invalidImageType": "Loại tệp không hợp lệ",
"imageFormatsOnly": "Vui lòng tải lên tệp hình ảnh JPG, PNG, GIF hoặc WebP.",
"imageUploadSuccess": "Tải lên hình ảnh thành công!",
"failedImageUpload": "Tải lên hình ảnh thất bại"
},
"fontStyles": {
"classic": "Cổ điển",
"editor": "Trình chỉnh sửa",
"strong": "Đậm",
"typewriter": "Máy đánh chữ",
"deco": "Trang trí",
"simple": "Đơn giản",
"modern": "Hiện đại",
"clean": "Sạch sẽ"
},
"customFont": {
"dialogTitle": "Thêm Google Font",
"urlLabel": "URL nhập Google Fonts",
"urlPlaceholder": "https://fonts.googleapis.com/css2?family=Roboto&display=swap",
"urlHelp": "Lấy từ Google Fonts: Chọn một phông chữ → Nhấp \"Get font\" → Sao chép URL @import",
"nameLabel": "Tên hiển thị",
"namePlaceholder": "Phông chữ tùy chỉnh của tôi",
"nameHelp": "Đây là cách phông chữ sẽ xuất hiện trong bộ chọn phông chữ",
"addButton": "Thêm phông chữ",
"addingButton": "Đang thêm...",
"errorEmptyUrl": "Vui lòng nhập URL nhập Google Fonts",
"errorInvalidUrl": "Vui lòng nhập URL Google Fonts hợp lệ",
"errorEmptyName": "Vui lòng nhập tên phông chữ",
"errorExtractFailed": "Không thể trích xuất họ phông chữ từ URL",
"successMessage": "Thêm phông chữ \"{{fontName}}\" thành công",
"failedToAdd": "Thêm phông chữ thất bại",
"errorTimeout": "Tải phông chữ mất quá nhiều thời gian. Vui lòng kiểm tra URL và thử lại.",
"errorLoadFailed": "Không thể tải phông chữ. Vui lòng xác minh URL Google Fonts là chính xác."
},
"language": {
"title": "Ngôn ngữ"
}
}
+37
View File
@@ -0,0 +1,37 @@
{
"title": "Phím tắt",
"customize": "Tùy chỉnh",
"configurable": "Có thể định cấu hình",
"fixed": "Cố định",
"pressKey": "Nhấn một phím…",
"clickToChange": "Nhấp để thay đổi",
"pressEscToCancel": "Nhấn Esc để hủy",
"helpText": "Nhấp vào một phím tắt rồi nhấn tổ hợp phím mới. Nhấn Esc để hủy.",
"resetToDefaults": "Khôi phục mặc định",
"alreadyUsedBy": "Đã được sử dụng bởi {{action}}",
"swap": "Hoán đổi",
"reservedShortcut": "Phím tắt này được dành riêng cho \"{{label}}\" và không thể gán lại.",
"savedToast": "Đã lưu phím tắt",
"resetToast": "Đã đặt lại về phím tắt mặc định — nhấp Lưu để áp dụng",
"actions": {
"addZoom": "Thêm Thu phóng",
"addTrim": "Thêm Cắt",
"addSpeed": "Thêm Tốc độ",
"addAnnotation": "Thêm Chú thích",
"addBlur": "Thêm Làm mờ",
"addKeyframe": "Thêm Khung hình chính",
"deleteSelected": "Xóa mục đã chọn",
"playPause": "Phát / Tạm dừng"
},
"fixedActions": {
"undo": "Hoàn tác",
"redo": "Làm lại",
"cycleAnnotationsForward": "Chuyển tiếp qua các chú thích",
"cycleAnnotationsBackward": "Chuyển lùi qua các chú thích",
"deleteSelectedAlt": "Xóa mục đã chọn (alt)",
"panTimeline": "Xoay Trục thời gian",
"zoomTimeline": "Thu phóng Trục thời gian",
"frameBack": "Lùi một khung hình",
"frameForward": "Tiến một khung hình"
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"buttons": {
"addZoom": "Thêm Thu phóng (Z)",
"suggestZooms": "Đề xuất Thu phóng từ Con trỏ",
"addTrim": "Thêm Cắt (T)",
"addAnnotation": "Thêm Chú thích (A)",
"addBlur": "Thêm Làm mờ (B)",
"addSpeed": "Thêm Tốc độ (S)"
},
"hints": {
"pressZoom": "Nhấn Z để thêm thu phóng",
"pressTrim": "Nhấn T để thêm cắt",
"pressAnnotation": "Nhấn A để thêm chú thích",
"pressBlur": "Nhấn B để thêm vùng làm mờ",
"pressSpeed": "Nhấn S để thêm tốc độ"
},
"labels": {
"pan": "Xoay",
"zoom": "Thu phóng",
"trim": "Cắt",
"speed": "Tốc độ",
"zoomItem": "Thu phóng {{index}}",
"trimItem": "Cắt {{index}}",
"speedItem": "Tốc độ {{index}}",
"annotationItem": "Chú thích",
"blurItem": "Làm mờ {{index}}",
"imageItem": "Hình ảnh",
"emptyText": "Văn bản trống"
},
"emptyState": {
"noVideo": "Chưa tải video",
"dragAndDrop": "Kéo và thả video để bắt đầu chỉnh sửa"
},
"errors": {
"cannotPlaceZoom": "Không thể đặt thu phóng ở đây",
"zoomExistsAtLocation": "Thu phóng đã tồn tại ở vị trí này hoặc không có đủ không gian.",
"zoomSuggestionUnavailable": "Trình xử lý đề xuất thu phóng không khả dụng",
"noCursorTelemetry": "Không có dữ liệu từ xa của con trỏ",
"noCursorTelemetryDescription": "Ghi hình màn hình trước để tạo các đề xuất dựa trên con trỏ.",
"noUsableTelemetry": "Không có dữ liệu từ xa của con trỏ có thể sử dụng",
"noUsableTelemetryDescription": "Bản ghi không chứa đủ dữ liệu chuyển động của con trỏ.",
"noDwellMoments": "Không tìm thấy khoảnh khắc dừng con trỏ rõ ràng",
"noDwellMomentsDescription": "Thử ghi hình với các lần tạm dừng con trỏ chậm hơn ở các thao tác quan trọng.",
"noAutoZoomSlots": "Không có khe thu phóng tự động nào",
"noAutoZoomSlotsDescription": "Các điểm dừng được phát hiện chồng chéo với các vùng thu phóng hiện có.",
"cannotPlaceTrim": "Không thể đặt cắt ở đây",
"trimExistsAtLocation": "Cắt đã tồn tại ở vị trí này hoặc không có đủ không gian.",
"cannotPlaceSpeed": "Không thể đặt tốc độ ở đây",
"speedExistsAtLocation": "Vùng tốc độ đã tồn tại ở vị trí này hoặc không có đủ không gian."
},
"success": {
"addedZoomSuggestions": "Đã thêm {{count}} đề xuất thu phóng dựa trên con trỏ",
"addedZoomSuggestionsPlural": "Đã thêm {{count}} đề xuất thu phóng dựa trên con trỏ"
}
}