mirror of
https://github.com/siddharthvaddem/openscreen.git
synced 2026-11-02 21:55:16 +08:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7feb05cca7 | ||
|
|
9ac4e778f8 | ||
|
|
ed825d8b37 | ||
|
|
b48370e3d0 | ||
|
|
8c0c555640 | ||
|
|
24be97bae7 | ||
|
|
c820b416f9 | ||
|
|
f42c478725 | ||
|
|
64c2a43006 | ||
|
|
d8da26a41a | ||
|
|
f0699cc8b0 | ||
|
|
3ad3e22a16 | ||
|
|
2381e48a46 | ||
|
|
3dd7b85ebb | ||
|
|
be4e2d0c94 | ||
|
|
c9b6074626 | ||
|
|
37c1ea5984 |
@@ -0,0 +1,118 @@
|
||||
name: Bump Nix package on release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Release tag to bump (e.g. v1.5.0)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_dispatch' || !github.event.release.prerelease
|
||||
steps:
|
||||
- name: Resolve tag and version
|
||||
id: meta
|
||||
env:
|
||||
GH_EVENT_TAG: ${{ github.event.release.tag_name }}
|
||||
INPUT_TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${GH_EVENT_TAG:-$INPUT_TAG}"
|
||||
if [[ -z "$TAG" ]]; then
|
||||
echo "::error::No tag resolved from release event or workflow input"
|
||||
exit 1
|
||||
fi
|
||||
VERSION="${TAG#v}"
|
||||
BRANCH="chore/bump-nix-${VERSION}"
|
||||
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v27
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
extra_nix_config: |
|
||||
experimental-features = nix-command flakes
|
||||
|
||||
- name: Compute npmDepsHash
|
||||
id: hash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
HASH=$(nix run nixpkgs#prefetch-npm-deps -- package-lock.json)
|
||||
if [[ -z "$HASH" ]]; then
|
||||
echo "::error::prefetch-npm-deps returned an empty hash"
|
||||
exit 1
|
||||
fi
|
||||
echo "hash=$HASH" >> "$GITHUB_OUTPUT"
|
||||
echo "Computed npmDepsHash: $HASH"
|
||||
|
||||
- name: Update nix/package.nix
|
||||
env:
|
||||
VERSION: ${{ steps.meta.outputs.version }}
|
||||
HASH: ${{ steps.hash.outputs.hash }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Update version line: ` version = "<anything>";`
|
||||
sed -i -E "s|^([[:space:]]*version[[:space:]]*=[[:space:]]*)\"[^\"]*\";|\1\"${VERSION}\";|" nix/package.nix
|
||||
# Update npmDepsHash line: ` npmDepsHash = "<anything>";`
|
||||
sed -i -E "s|^([[:space:]]*npmDepsHash[[:space:]]*=[[:space:]]*)\"[^\"]*\";|\1\"${HASH}\";|" nix/package.nix
|
||||
|
||||
echo "=== diff ==="
|
||||
git --no-pager diff nix/package.nix || true
|
||||
|
||||
- name: Create PR
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
VERSION: ${{ steps.meta.outputs.version }}
|
||||
HASH: ${{ steps.hash.outputs.hash }}
|
||||
BRANCH: ${{ steps.meta.outputs.branch }}
|
||||
TAG: ${{ steps.meta.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if git diff --quiet -- nix/package.nix; then
|
||||
echo "nix/package.nix already at v${VERSION} with this hash — nothing to do."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Replace any prior bump branch to keep the workflow idempotent.
|
||||
git push origin --delete "$BRANCH" 2>/dev/null || true
|
||||
git checkout -b "$BRANCH"
|
||||
git add nix/package.nix
|
||||
git commit -m "chore: bump nix package to v${VERSION}"
|
||||
git push -u origin "$BRANCH"
|
||||
|
||||
gh pr create \
|
||||
--title "chore: bump nix package to v${VERSION}" \
|
||||
--base main \
|
||||
--head "$BRANCH" \
|
||||
--body "$(cat <<EOF
|
||||
Automated bump triggered by release \`${TAG}\`.
|
||||
|
||||
- \`version\` → \`${VERSION}\`
|
||||
- \`npmDepsHash\` → \`${HASH}\` (computed via \`prefetch-npm-deps package-lock.json\`)
|
||||
|
||||
Merge this so Nix users (NixOS, Home Manager, \`nix run github:siddharthvaddem/openscreen\`) pick up the new release.
|
||||
|
||||
> Note: PRs opened by \`GITHUB_TOKEN\` don't auto-trigger CI. The diff is two lines — review the change here, then merge. If you want CI to run, push an empty commit to this branch or close-and-reopen the PR.
|
||||
EOF
|
||||
)"
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Publish release to WinGet
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Release tag to publish to winget (e.g. v1.4.0)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: windows-latest
|
||||
if: github.event_name == 'workflow_dispatch' || !github.event.release.prerelease
|
||||
steps:
|
||||
- uses: vedantmgoyal9/winget-releaser@v2
|
||||
with:
|
||||
identifier: SiddharthVaddem.OpenScreen
|
||||
# Match the Windows installer asset attached to each release.
|
||||
# Today: "Openscreen.Setup.latest.exe". Adjust this regex if you
|
||||
# ever rename the installer to include a version (e.g. "Setup\.\d+\.\d+\.\d+\.exe").
|
||||
installers-regex: 'Setup\..*\.exe$'
|
||||
release-tag: ${{ inputs.tag || github.event.release.tag_name }}
|
||||
token: ${{ secrets.WINGET_ACC_TOKEN }}
|
||||
@@ -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}"
|
||||
end
|
||||
|
||||
url "https://github.com/${REPO}/releases/download/v#{version}/${ARM_NAME}"
|
||||
end
|
||||
on_intel do
|
||||
sha256 "${X64_SHA}"
|
||||
url "${BASE}/${X64_NAME}"
|
||||
|
||||
url "https://github.com/${REPO}/releases/download/v#{version}/${X64_NAME}"
|
||||
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
|
||||
|
||||
@@ -50,7 +50,20 @@ Download the latest installer for your platform from the [GitHub Releases](https
|
||||
|
||||
### macOS
|
||||
|
||||
If you encounter issues with macOS Gatekeeper blocking the app (since it does not come with a developer certificate), you can bypass this by running the following command in your terminal after installation:
|
||||
The easiest way to install on macOS is via [Homebrew](https://brew.sh):
|
||||
|
||||
```bash
|
||||
brew install --cask siddharthvaddem/openscreen/openscreen
|
||||
```
|
||||
|
||||
Brew automatically picks the right build for Apple Silicon or Intel, and verifies the download against a notarized signature so Gatekeeper won't block it.
|
||||
|
||||
To update later: `brew upgrade --cask openscreen`
|
||||
To uninstall: `brew uninstall --cask openscreen` (add `--zap` to also remove app data)
|
||||
|
||||
#### Manual install (if you prefer)
|
||||
|
||||
If you'd rather grab the `.dmg` directly from the [Releases page](https://github.com/siddharthvaddem/openscreen/releases) and encounter Gatekeeper blocking the app, you can bypass it by running the following command in your terminal after installation:
|
||||
|
||||
```bash
|
||||
xattr -rd com.apple.quarantine /Applications/Openscreen.app
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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", () => {
|
||||
|
||||
@@ -10,6 +10,7 @@ export const SUPPORTED_LOCALES = [
|
||||
"ja-JP",
|
||||
"ar",
|
||||
"ru",
|
||||
"vi",
|
||||
] as const;
|
||||
export const I18N_NAMESPACES = [
|
||||
"common",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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ữ"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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ỏ"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user