49 Commits

Author SHA1 Message Date
david_bai 87ff5aab44 feat: add visual feedback for copy buttons in ShareCard
Add check icon feedback when copying RoomID and URL to clipboard:
- RoomID copy button now shows check icon for 2 seconds after copy
- URL copy button now shows check icon for 2 seconds after copy
- Consistent with existing QR code copy button behavior
2026-03-28 10:36:34 +08:00
david_bai e4ca70d758 fix: resolve React hydration mismatch in ThemeToggle
The ThemeToggle component was causing hydration errors because the server
and client rendered different icons (Sun/Moon) based on the theme state.

Changes:
- Render a placeholder button until component is mounted
- Only render the actual theme icon after client-side hydration
- This ensures server and client HTML match during initial render

Fixes console error: 'Expected server HTML to contain a matching <circle> in <svg>'
2026-03-28 10:23:43 +08:00
david_bai c791c0820e chore: update backend package-lock.json
Update backend/package-lock.json to ensure dependency consistency.
This file should be committed to the repository as per npm best practices
for reproducible builds across all environments.
2026-03-28 08:02:10 +08:00
david_bai b81e39ac65 fix: restore QR code rendering and improve paste button label
- Restored QRCodeSVG component rendering in ShareCard for QR code display
- Fixed copy QR code and download QR code functionality
- Changed paste button label in SendTabPanel from 'Paste RoomID' to generic 'Paste'
- Keep 'Paste RoomID' label in RetrieveTabPanel where it's contextually appropriate
2026-03-28 08:02:04 +08:00
david_bai 7e781631bb chore(ui): clear remaining frontend warnings
Resolve the remaining lint warnings without changing behavior by fixing hook dependency lists, removing the icon naming false positive, and switching the YouTube thumbnail to next/image for compliant rendering.
2026-03-27 17:20:49 +08:00
david_bai 7a1ab18657 refactor(i18n): stabilize schema and restore locale translations
Align the next-intl message schema across components, hooks, and locale files so the frontend uses one canonical structure instead of compile-first workarounds. Restore Spanish, French, German, Japanese, and Korean translations to the new schema while narrowing clipboard hook dependencies to translation contracts.
2026-03-27 17:13:31 +08:00
david_bai 29897bea87 refactor(i18n): convert remaining useMessages to useTranslations
- FAQSection: useTranslations with dynamic keys via type assertion
- ClipboardApp: useTranslations for JSX, keep useMessages for hooks
- SendTabPanel: useTranslations for html and roomStatus namespaces
- RetrieveTabPanel: useTranslations for html, roomStatus, and ClipboardApp
- FileListDisplay: useTranslations for FileListDisplay namespace
- FileUploadHandler: useTranslations for fileUploadHandler namespace

Only ClipboardApp.tsx retains useMessages for hooks requiring full messages object.
2026-03-27 15:09:15 +08:00
david_bai 131d1e12f5 refactor(i18n): use native messages in clipboard panels
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-27 14:21:53 +08:00
david_bai 362a805c9b refactor(i18n): use native messages in faq section
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-27 14:21:53 +08:00
david_bai 2012412bc1 refactor(i18n): remove translation provider facade
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-27 14:21:53 +08:00
david_bai b1b663e1ce refactor(i18n): migrate clipboard action hook translations
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-27 14:16:17 +08:00
david_bai d72f3d3860 refactor(i18n): migrate clipboard widgets to useTranslations
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-27 14:16:08 +08:00
david_bai 529024eed7 refactor(i18n): migrate legal content translations
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-27 14:16:08 +08:00
david_bai c0826c7d34 refactor(i18n): migrate about and help content translations
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-27 14:16:08 +08:00
david_bai b364ef3c16 refactor(i18n): migrate blog list item translations
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-27 14:15:46 +08:00
david_bai c845399856 refactor(i18n): migrate home sections to native next-intl hooks
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-27 14:15:46 +08:00
david_bai 0ccefbd0c1 refactor(i18n): migrate shared chrome to useTranslations
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-27 14:15:45 +08:00
david_bai b6193b662f refactor(i18n): migrate server routes to next-intl messages
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-27 14:04:28 +08:00
david_bai 6c93b1d995 build(i18n): add next-intl routing infrastructure
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-27 14:04:28 +08:00
david_bai cf529eed64 refactor(i18n): replace prop drilling with translation context
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-27 14:04:28 +08:00
david_bai 57004b3a1f refactor(i18n): normalize translation keys
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-27 14:04:28 +08:00
david_bai 8f6f0a9266 docs: add low-memory deployment guidance
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-26 07:24:03 +08:00
david_bai 83b835f19c docs: sync docker deployment guide with full-mode runtime behavior 2026-03-26 07:17:34 +08:00
david_bai 0e2cf068c8 fix(deploy): use same-origin API and include www CORS origin
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-26 07:09:03 +08:00
david_bai ae06c45324 fix(deploy): refresh full-mode edge config and frontend healthcheck
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-26 00:14:28 +08:00
david_bai fa8ca13283 chore: remove temporary docker-only cleanup notes 2026-02-28 13:36:43 +08:00
david_bai 201b36ed19 docs: require explicit --mode in Docker deployment guide 2026-02-28 13:36:26 +08:00
david_bai 927e227c29 docs: record docker-only cleanup execution 2026-02-28 12:39:04 +08:00
david_bai fd70fa35ca chore: remove PM2-based deploy scripts 2026-02-28 12:38:14 +08:00
david_bai de6199bbf2 chore: remove bare-metal ops assets under backend/docker 2026-02-28 12:37:57 +08:00
david_bai 3ce1ca58ea chore: remove bare-metal deployment guides 2026-02-28 12:37:30 +08:00
david_bai 0dfe627e25 docs: remove PM2/bare-metal references 2026-02-28 12:37:11 +08:00
david_bai 2b24dbef0e docs(playbook): remove bare-metal deployment entries 2026-02-28 12:35:59 +08:00
david_bai e0c31957cc docs: make Docker the only supported deployment path 2026-02-28 12:35:06 +08:00
david_bai 5c8df1867c docs: add docker-only cleanup plan 2026-02-28 12:35:02 +08:00
david_bai 1a0467b439 chore(doc):Fix the issue of missing content in some language blog posts 2025-12-30 12:06:34 +08:00
david_bai bb90d0c0fd chore(doc):Apply a unified bold underline style to the links 2025-12-27 10:50:04 +08:00
david_bai 4328cd0a1c chore(doc):Add blog cover image 2025-12-27 10:11:16 +08:00
david_bai 0b82fc2d47 chore(doc):Polish the blog and translate it 2025-12-26 20:51:06 +08:00
david_bai 49e20edd80 chore(merge):merge sub-branch into main 2025-12-24 12:14:52 +08:00
david_bai 30d7a6c27b chore(doc):rename AGENTS.md to AGENTS.en.md;Add a blog draft 2025-12-22 12:38:58 +08:00
david_bai 52bb56501e chore(doc):Add an English version of documents such as AGENTS.md and ai-playbook 2025-12-18 12:28:29 +08:00
david_bai cb9797d0e8 chore(doc):Split docs/ai-playbook/flows.zh-CN.md into multiple documents
- Simplify flows.zh-CN.md to an entry + quick reference, add "Quick Navigation" and in-depth reading links, original sections 6/7/9/10 are retained in the entry page with titles but changed to links.

  - Add in-depth reading sub-document:
      - docs/ai-playbook/flows/frontend.zh-CN.md
      - docs/ai-playbook/flows/backpressure-chunking.zh-CN.md
      - docs/ai-playbook/flows/resume.zh-CN.md
      - docs/ai-playbook/flows/reconnect-consistency.zh-CN.md
  - docs/ai-playbook/index.zh-CN.md add "Process (In-depth reading split)" index
2025-12-18 12:28:29 +08:00
david_bai 7a027a27b8 chore(doc):Added multi-receiver support bullets in both READMEs and indexed the Chinese README in the AI playbook. 2025-12-18 12:28:25 +08:00
david_bai dceaae8efa fix(ssr): guard DOM/window access and client-only listeners
- Prevent server-side exceptions (Application error) on mobile after redirects
  - useOneShotSlowHint: guard document; use global setTimeout; conditionally attach visibilitychange
  - useConnectionFeedback: guard document.visibilityState; register/remove listeners only on client
  - usePageSetup: guard window before tracking referrer and parsing roomId
  - tracking: early return when window is undefined
  - docs(flows): add “SSR & DOM access guard (must-read)” checklist; renumber next section
2025-12-06 12:00:03 +08:00
david_bai 0d830114cd feat(ui): extract reusable one-shot slow-hint hook; refactor join(3s) and RTC negotiate(8s) hints; share rtcPhase mapper; update docs 2025-12-06 11:05:39 +08:00
david_bai 761921684c feat(ui): add 8s P2P slow-connect hint + i18n; document full connection feedback flows
- Hook (useConnectionFeedback):
      - Add SLOW_RTC_MS=8000 timer when entering negotiating
      - Foreground-only; pending while hidden; show once per negotiation attempt
      - Clear timers on connect/disconnect; reset attempt flags when leaving negotiating
      - Cleanup timers on unmount
  - i18n:
      - Add required key ClipboardApp.rtc_slow to types
      - Provide translations for zh, en, ja, es, de, fr, ko
  - Docs:
      - flows.zh-CN: add UI connection feedback state machine covering
        join_inProgress (immediate), join_slow (3s), join_timeout (15s),
        rtc_negotiating, rtc_slow (8s), rtc_connected, rtc_reconnecting, rtc_restored;
        document equivalent success signals and visibility gating
      - code-map.zh-CN: outline responsibilities/locations for useRoomManager (join slow/timeout)
        and useConnectionFeedback (negotiation slow, reconnect/restored)
2025-12-05 19:10:00 +08:00
david_bai 621d65bdfd doc(webrtc): sync flows/code-map 2025-11-26 12:17:53 +08:00
david_bai a0befd06f4 fix(webrtc): avoid false "Join room timeout" with 15s timeout + early success
- Increase joinRoom timeout to 15s for slow networks/polling.
  - Early-resolve join on handshake signals:
      - Initiator: ready / recipient-ready
      - Recipient: offer
  - Single-settle guard; cleanup listeners/timer to prevent leaks.

  File: frontend/lib/webrtc_base.ts
2025-11-25 23:31:06 +08:00
131 changed files with 11773 additions and 8243 deletions
+2
View File
@@ -71,3 +71,5 @@ logs/
# Temporary files
.temp/
.tmp/
AGENTS.md
+19
View File
@@ -0,0 +1,19 @@
# AGENTS — PrivyDrop Repository Rules (Short)
## First Principles
- Communicate in Chinese: Always use Simplified Chinese when communicating with the project owner/maintainers. Use English for code comments, naming, commit messages, and PR titles/descriptions.
- Best practices, aligned with the existing stack: Prefer proven approaches consistent with what the repo already uses; iterate in small steps and keep changes easy to roll back.
- Plan first: Before implementing anything, propose a change plan and get approval (goals, scope/files, approach, risks, acceptance, rollback, docs updates, validation). Template: `docs/ai-playbook/collab-rules.md` (or `docs/ai-playbook/collab-rules.zh-CN.md`).
- One change, one purpose: Each change should solve one clear goal; avoid “while Im here” fixes; keep it minimal and reversible.
- Privacy & architecture red line: The backend is for signaling and room coordination only. Do not relay, store, or upload any user file data to the server or third parties in any form.
- Transport guardrails: Keep established chunking/backpressure/retry parameters and mechanisms; any breaking change or parameter-level change must be approved first.
- Dependencies & infrastructure: Do not add new dependencies/component libraries/infrastructure or do large refactors without approval.
- Docs must stay in sync: If a change affects flows, interfaces, or entry file paths, update `docs/ai-playbook/flows.zh-CN.md` and `docs/ai-playbook/code-map.zh-CN.md` in the same PR.
- Verification required: Frontend must build (`next build`); list key manual test cases and regression points.
## Priority & Conflicts
- Explicit user instructions override this file; if theres a conflict, call it out in the plan and get approval.
- For detailed rules, examples, and checklists, follow `docs/ai-playbook/collab-rules.md` (this file only keeps the highest-level principles).
View File
+4 -3
View File
@@ -20,8 +20,9 @@ We believe everyone should have control over their own data. PrivyDrop was creat
## ✨ Key Features
- 🔄 **Unlimited File Transfer** - Support files of any size through Chrome's direct-to-disk streaming (Need to set the save directory)
- 🔒 **End-to-End Encryption**: Leverages P2P direct connections via WebRTC. All files and text are transferred directly between browsers without passing through any central server.
- 🔄 **Unlimited File Transfer** - Support files of any size through Chrome's direct-to-disk streaming (Need to set the save directory)
- 👥 **Multi-receiver Support** - A single room can have multiple receivers simultaneously downloading files/text, and newcomers can join without disrupting ongoing transfers.
- 📂 **File & Folder Transfer**: Supports transferring multiple files and entire folders.
- ⏸️ **Resume Transfer**: Resume file transfer from the point of interruption. Simply set the save directory to enable this feature, ensuring your large files are delivered safely even with unstable networks. If interrupted, you currently need to refresh both the sender and receiver web pages to restart the transfer.
-**Real-time & Efficient**: Displays real-time transfer progress and automatically calculates transfer speed.
@@ -37,7 +38,7 @@ We believe everyone should have control over their own data. PrivyDrop was creat
- **Backend**: Node.js, Express.js, TypeScript
- **Real-time Communication**: WebRTC, Socket.IO
- **Data Storage**: Redis
- **Deployment**: PM2, Nginx, Docker
- **Deployment**: Docker (Docker Compose)
## 🐳 Docker One-Click Deployment (Recommended)
@@ -124,7 +125,7 @@ We provide detailed documentation to help you dive deeper into the project's des
- [**Overall Project Architecture**](./docs/ARCHITECTURE.md): Understand how all components of the PrivyDrop system work together.
- [**Frontend Architecture Deep Dive**](./docs/FRONTEND_ARCHITECTURE.md): Explore the frontend's modern, layered architecture, state management with Zustand, and the decoupled service-based approach to WebRTC.
- [**Backend Architecture Deep Dive**](./docs/BACKEND_ARCHITECTURE.md): Dive into the backend's code structure, signaling flow, and Redis design.
- [**Deployment Guide**](./docs/DEPLOYMENT.md): Learn how to deploy the complete PrivyDrop application in a production environment.
- [**Docker Deployment Guide**](./docs/DEPLOYMENT_docker.md): One-click deployment (LAN/Public/Full), HTTPS automation, TURN, and troubleshooting.
- [AI Playbook (zh-CN)](./docs/ai-playbook/index.zh-CN.md) · [Collaboration Rules (zh-CN)](./docs/ai-playbook/collab-rules.zh-CN.md)
## 🤝 Contributing
+4 -3
View File
@@ -20,8 +20,9 @@ PrivyDrop (原 SecureShare) 是一个基于 WebRTC 的开源点对点(P2P)
## ✨ 主要特性
- 🔄 **无限制文件传输** - 支持任意大小文件传输,通过 Chrome 的流式保存到磁盘功能实现(需设置保存目录)
- 🔒 **端到端加密**: 基于 WebRTC 的 P2P 直连技术,所有文件和文本在浏览器间直接传输,不经过任何中央服务器。
- 🔄 **无限制文件传输** - 支持任意大小文件传输,通过 Chrome 的流式保存到磁盘功能实现(需设置保存目录)
- 👥 **多接收端支持** - 单个房间可同时让多个接收端并行获取文件/文本,加入房间不会中断正在进行的传输。
- 📂 **文件与文件夹传输**: 支持多文件和整个文件夹的传输。
- ⏸️ **断点续传**: 自动从中断处恢复文件传输。只需设置保存目录即可启用此功能,确保即使在网络不稳定的情况下,您的大文件也能安全送达。如果中断,目前需要同时刷新发送端和接收端网页,重新开始传输即可。
-**实时高效**: 实时显示传输进度、自动计算传输速度。
@@ -37,7 +38,7 @@ PrivyDrop (原 SecureShare) 是一个基于 WebRTC 的开源点对点(P2P)
- **后端**: Node.js, Express.js, TypeScript
- **实时通信**: WebRTC, Socket.IO
- **数据存储**: Redis
- **部署**: PM2, Nginx, Docker
- **部署**: DockerDocker Compose
## 🚀 快速上手
@@ -135,7 +136,7 @@ bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn -
- [**项目整体架构**](./docs/ARCHITECTURE.zh-CN.md): 了解 PrivyDrop 系统各个组件如何协同工作。
- [**前端架构详解**](./docs/FRONTEND_ARCHITECTURE.zh-CN.md): 深入探索前端的现代化分层架构、基于 Zustand 的状态管理,以及解耦的服务化 WebRTC 实现。
- [**后端架构详解**](./docs/BACKEND_ARCHITECTURE.zh-CN.md): 深入探索后端的代码结构、信令流程和 Redis 设计。
- [**部署指南**](./docs/DEPLOYMENT.zh-CN.md): 学习如何在生产环境部署完整的 PrivyDrop 应用
- [**Docker 部署指南**](./docs/DEPLOYMENT_docker.zh-CN.md): 一键部署(内网/公网/full)、HTTPS 自动化、TURN 与排错
- [AI Playbook 索引](./docs/ai-playbook/index.zh-CN.md) · [协作规则](./docs/ai-playbook/collab-rules.zh-CN.md)
## 🤝 参与贡献
+1 -2
View File
@@ -16,7 +16,6 @@ This is the backend server for PrivyDrop. It is built with Node.js, Express, and
- **Language**: TypeScript
- **Real-time Communication**: Socket.IO
- **Database**: Redis (using the ioredis client)
- **Process Management**: PM2
## 🚀 Getting Started (Local Development)
@@ -56,4 +55,4 @@ This service provides a set of API endpoints and Socket.IO events to support the
- To understand the backend's code structure, module design, and Redis data model in depth, please read the [**Backend Architecture Deep Dive**](../docs/BACKEND_ARCHITECTURE.md).
- To learn about how the frontend and backend collaborate, refer to the [**Overall Project Architecture**](../docs/ARCHITECTURE.md).
- For instructions on deploying in a production environment, please see the [**Deployment Guide**](../docs/DEPLOYMENT.md).
- For production deployment, see the [**Docker Deployment Guide**](../docs/DEPLOYMENT_docker.md).
+1 -2
View File
@@ -16,7 +16,6 @@
- **语言**: TypeScript
- **实时通信**: Socket.IO
- **数据库**: Redis (使用 ioredis 客户端)
- **进程管理**: PM2
## 🚀 入门 (本地开发)
@@ -56,4 +55,4 @@
- 要深入理解后端的代码结构、模块设计和 Redis 数据模型,请阅读 [**后端架构详解**](../docs/BACKEND_ARCHITECTURE.zh-CN.md)。
- 要了解项目前后端的整体协作方式,请参阅 [**项目整体架构**](../docs/ARCHITECTURE.zh-CN.md)。
- 有关生产环境部署方法,请参考 [**部署指南**](../docs/DEPLOYMENT.zh-CN.md)。
- 有关生产环境部署,请参考 [**Docker 部署指南**](../docs/DEPLOYMENT_docker.zh-CN.md)。
-53
View File
@@ -1,53 +0,0 @@
# Use Ubuntu 20.04 image as base
FROM ubuntu:20.04
# Set environment variables to avoid interactive installation
ENV DEBIAN_FRONTEND=noninteractive
# Set Tsinghua University software source
RUN sed -i 's/archive.ubuntu.com/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list
RUN apt-get update && apt-get install -y tzdata
# Set Shanghai time zone
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
# Install certbot nginx
RUN apt install -y certbot python3-certbot-nginx ssl-cert
# TURN server
RUN apt-get install -y vim coturn
# redis service
RUN apt-get install -y redis-server
# Install nodejs 20
RUN apt-get install -y curl lsb-release
# node.js
## Import repository GPG key
RUN apt install -y ca-certificates gnupg && mkdir -p /etc/apt/keyrings
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
## Add Node.JS 20 LTS APT repository.
ENV NODE_MAJOR=20
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
## Update package index.
RUN apt-get update
## Install Node.js, npm, pnpm
RUN apt install -y nodejs
RUN npm install -g pnpm pm2
## node -v -> v20.18.1;npm -v -> 10.8.2;pnpm -v -> 9.14.4
## install Yarn package manager
#curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /usr/share/keyrings/yarnkey.gpg >/dev/null
#echo "deb [signed-by=/usr/share/keyrings/yarnkey.gpg] https://dl.yarnpkg.com/debian stable main" | tee /etc/apt/sources.list.d/yarn.list
#apt update && apt-get install yarn -y
## Install Nginx
RUN curl -fsSL https://nginx.org/keys/nginx_signing.key | apt-key add - && \
echo "deb https://nginx.org/packages/ubuntu/ $(lsb_release -cs) nginx" | tee /etc/apt/sources.list.d/nginx.list && \
apt update && apt install -y nginx
#clean up
RUN apt-get clean autoclean
RUN apt-get autoremove --yes
RUN rm -rf /var/lib/{apt,cache,log}/ && rm -rf /tmp/*
-111
View File
@@ -1,111 +0,0 @@
#!/bin/bash
# Define required environment variables
declare -A required_vars=(
["NGINX_SERVER_NAME"]="Nginx server domain"
["NGINX_FRONTEND_ROOT"]="Frontend build file path"
["BACKEND_PORT"]="Backend service port"
["TURN_REALM"]="TURN server domain name"
)
# Validate environment variables
validate_env_vars() {
local missing_vars=()
local env_file=$1
echo "Verifying Nginx environment variable configuration..."
# Load environment variables
source "$env_file"
# Check required variables
for var in "${!required_vars[@]}"; do
if [ -z "${!var}" ]; then
missing_vars+=("$var (${required_vars[$var]})")
fi
done
# If there are missing variables, display an error message and exit
if [ ${#missing_vars[@]} -ne 0 ]; then
echo "Error: The following required Nginx variables are not set:"
printf '%s\n' "${missing_vars[@]}" | sed 's/^/ - /'
echo "Please set these variables in $env_file and try again."
exit 1
fi
echo "Nginx production environment variables verified successfully!"
}
# Check parameters
if [ -z "$1" ]; then
echo "Usage: $0 <env_file_path>"
exit 1
fi
ENV_FILE=$1
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "Nginx path: $SCRIPT_DIR"
# Check if the environment variable file exists
if [ ! -f "$ENV_FILE" ]; then
echo "Error: Environment file $ENV_FILE not found"
exit 1
fi
# Validate environment variables
validate_env_vars "$ENV_FILE"
# Read environment variables
source "$ENV_FILE"
# Configure Nginx
configure_nginx() {
echo "Configuring Nginx..."
NGINX_TEMPLATE="$SCRIPT_DIR/default"
echo "reading $NGINX_TEMPLATE ..."
TEMP_NGINX=$(mktemp)
# Use sed for more robust replacement
sed -e "s/www\.YourDomain/www.$NGINX_SERVER_NAME/g" \
-e "s/YourDomain/$NGINX_SERVER_NAME/g" \
-e "s|path/to/PrivyDrop/frontend|$NGINX_FRONTEND_ROOT|g" \
-e "s/localhost:3001/localhost:$BACKEND_PORT/g" \
-e "s/TurnServerName/$TURN_REALM/g" \
"$NGINX_TEMPLATE" > "$TEMP_NGINX"
# Copy the configuration file to the target location
mkdir -p /etc/nginx/sites-enabled
cp "$TEMP_NGINX" /etc/nginx/sites-enabled/default
# cp "$TEMP_NGINX" default_temp
rm "$TEMP_NGINX"
}
# Configure nginx.conf with variable substitution
configure_nginx_conf() {
echo "Configuring nginx.conf..."
NGINX_CONF_TEMPLATE="$SCRIPT_DIR/nginx.conf"
echo "reading $NGINX_CONF_TEMPLATE ..."
TEMP_NGINX_CONF=$(mktemp)
# Use sed to replace variables in nginx.conf
sed -e "s/TurnServerName/$TURN_REALM/g" \
"$NGINX_CONF_TEMPLATE" > "$TEMP_NGINX_CONF"
# Copy the configuration file to the target location
cp "$TEMP_NGINX_CONF" /etc/nginx/nginx.conf
rm "$TEMP_NGINX_CONF"
}
# Execute configuration
configure_nginx
configure_nginx_conf
echo "Nginx configuration files generated successfully:"
echo " - /etc/nginx/sites-enabled/default (site configuration)"
echo " - /etc/nginx/nginx.conf (main configuration with TURN routing)"
echo "The script no longer restarts Nginx automatically."
echo ""
echo "NEXT STEP: Run Certbot to install the SSL certificate and automatically configure Nginx:"
echo "sudo certbot --nginx -d your_domain.com -d www.your_domain.com -d turn.your_domain.com"
-138
View File
@@ -1,138 +0,0 @@
server { # Redirect HTTP to HTTPS
listen 80;
server_name YourDomain www.YourDomain;
return 301 https://$server_name$request_uri;
}
server {
# No longer listening on public 443/TCP, change to listening on internal port
listen 127.0.0.1:4443 ssl;
http2 on;
ssl_protocols TLSv1.2 TLSv1.3;
server_name YourDomain www.YourDomain;
# Redirect bare domain to www
if ($host = 'YourDomain') {
return 301 https://www.YourDomain$request_uri;
}
# SSL Configuration (using placeholder certs for Certbot)
# Certbot will find this block and replace these with the real certificates.
ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
# SSL Optimization
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
# Modern Configuration
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# HSTS (Enable with caution)
# add_header Strict-Transport-Security "max-age=63072000" always;
# Define the root path of the frontend build artifacts inside the container
# !!! Important: Please modify this path to the actual path of your frontend project build inside the Nginx container !!!
set $frontend_build_root path/to/PrivyDrop/frontend;
# 1. Prioritize handling of Next.js core static resources (_next/static)
location /_next/static/ {
alias $frontend_build_root/.next/static/;
expires 365d; # Long-term cache
access_log off; # Disable access log for this path
add_header Cache-Control "public"; # Explicitly inform the browser that it can be cached publicly
}
# WebSocket signaling server configuration
location /socket.io/ {
proxy_pass http://localhost:3001/socket.io/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# CORS Configuration
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
# WebSocket related optimizations
proxy_read_timeout 86400; # 24h
proxy_send_timeout 86400; # 24h
proxy_connect_timeout 7d;
proxy_buffering off;
}
# Backend API address -- forward
location /api/ {
proxy_pass http://localhost:3001/api/; # Backend API address -- forward
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Modify CORS configuration, only set one Origin
add_header Access-Control-Allow-Origin "https://www.privydrop.app" always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range" always;
add_header Access-Control-Allow-Credentials "true" always;
}
# Next.js Image Optimization Service (usually handled by the Next.js application)
location /_next/image {
proxy_pass http://localhost:3002; # Point to the Next.js application
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
# Add cache optimization for image optimization
proxy_cache_valid 200 1d;
add_header Cache-Control "public, max-age=31536000, immutable";
proxy_read_timeout 60s;
proxy_connect_timeout 5s;
}
# 2. Handle static files under the public directory and Next.js dynamic requests
# This location should be after specific proxies (like /api/, /socket.io/),
# but it can be before or after /_next/static/ because they match different paths.
# For clarity, we put it here.
location / {
# root points to the parent directory of the public directory, which is the root directory of the frontend build artifacts
root $frontend_build_root/public;
# Try to find files in order:
# 1. $uri: as a file in the public directory (e.g., /image.png -> $frontend_build_root/public/image.png)
# 2. @nextjs: If none of the above are found, pass the request to the Next.js application for processing
try_files $uri @nextjs_app;
}
# Named location, used to proxy requests to the Next.js application
location @nextjs_app {
proxy_pass http://localhost:3002; # Point to the Next.js application
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
server { # Add a server block for Certbot to install certificates for TURN server
listen 80;
server_name TurnServerName;
# Only process Let's Encrypt validation requests
location /.well-known/acme-challenge/ {
root /var/www/html;
}
}
-144
View File
@@ -1,144 +0,0 @@
#!/bin/bash
# --- Configuration ---
NGINX_CONF_FILE="/etc/nginx/sites-enabled/default"
# Define the new configuration block to be added
read -r -d '' NEW_BLOCK <<'EOF'
# Configuration for turn.privydrop.app - used only for Certbot renewal
server {
listen 80;
listen [::]:80;
server_name turn.privydrop.app;
# Handle only Let's Encrypt ACME challenge requests
location /.well-known/acme-challenge/ {
root /var/www/html;
}
# Return 404 for all other requests
location / {
return 404;
}
}
EOF
# --- Main function ---
main() {
echo "▶️ Starting Nginx configuration check..."
# Check for root privileges
if [[ $EUID -ne 0 ]]; then
echo "❌ Error: This script must be run as root"
exit 1
fi
# Check if config file exists
if [ ! -f "$NGINX_CONF_FILE" ]; then
echo "❌ Error: Configuration file not found: $NGINX_CONF_FILE"
exit 1
fi
# Create a temporary backup
TEMP_FILE=$(mktemp)
cp "$NGINX_CONF_FILE" "$TEMP_FILE"
echo "🔐 Backup created at: $TEMP_FILE"
# Use Python to count and optionally remove the last two server blocks
ACTION=$(python3 -c "
import re
# Read the file
try:
with open('$NGINX_CONF_FILE', 'r') as f:
lines = f.readlines()
except Exception as e:
print('ERROR: Unable to read config file')
exit(1)
# Find all server block start and end positions
server_blocks = []
i = 0
while i < len(lines):
if re.match(r'^\s*server\s*\{', lines[i]):
start = i
brace_count = 1
j = i + 1
while j < len(lines) and brace_count > 0:
brace_count += lines[j].count('{') - lines[j].count('}')
j += 1
server_blocks.append((start, j-1))
i = j
else:
i += 1
num_blocks = len(server_blocks)
print(f'🔍 Found {num_blocks} server blocks')
if num_blocks >= 4:
print('✅ Condition met (≥4 blocks), preparing to remove last two and add new config')
print('ACTION: MODIFY')
# Keep up to the third-to-last block end, or before last two if only 4
if num_blocks > 2:
keep_until = server_blocks[-3][1] + 1
else:
keep_until = server_blocks[-2][0]
result_lines = lines[:keep_until]
# Remove trailing empty lines
while result_lines and result_lines[-1].strip() == '':
result_lines.pop()
# Ensure ends with newline
if result_lines and not result_lines[-1].endswith('\n'):
result_lines[-1] += '\n'
# Write modified content back
with open('$NGINX_CONF_FILE', 'w') as f:
f.writelines(result_lines)
else:
print('️ Less than 4 server blocks found. No changes will be made.')
print('ACTION: SKIP')
")
# Extract action decision from Python script output
ACTION=$(echo "$ACTION" | grep '^ACTION:' | cut -d' ' -f2 | tr -d '\r')
# Show number of blocks
echo "$ACTION" | grep -o 'Found [0-9]* server blocks' | head -1
if [[ "$ACTION" == "SKIP" ]]; then
echo "⏭️ Skipping modification and new configuration addition."
rm "$TEMP_FILE"
exit 0
fi
# Append the new configuration block
echo "✍️ Adding new configuration block for turn.privydrop.app..."
echo "$NEW_BLOCK" >> "$NGINX_CONF_FILE"
# Test the Nginx configuration
echo "🔍 Testing Nginx configuration..."
if nginx -t 2>/dev/null; then
echo "✅ Configuration test successful!"
echo "🚀 Apply changes with:"
echo " sudo systemctl reload nginx"
echo ""
rm "$TEMP_FILE"
else
echo "❌ Configuration test failed. Showing details:"
nginx -t
echo ""
echo "🔄 Restoring from backup..."
cp "$TEMP_FILE" "$NGINX_CONF_FILE"
echo "✅ Original configuration restored"
rm "$TEMP_FILE"
exit 1
fi
}
# Run main function with all arguments
main "$@"
-120
View File
@@ -1,120 +0,0 @@
# The user that nginx runs as, needs file directory access permissions
user root;
# The number of worker processes, usually set to be equal to the number of CPUs
# worker_processes 1;
worker_processes auto;
pid /run/nginx.pid;
#include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
}
stream {
# Define backend services
upstream turns_backend {
# Coturn's TURNS service, listening on local port 5349
server 127.0.0.1:5349;
}
upstream website_backend {
# Your website is now listening on the internal HTTPS port
server 127.0.0.1:4443;
}
# Use SNI hostname to determine traffic destination
map $ssl_preread_server_name $backend {
TurnServerName turns_backend; # If accessing the turn subdomain, hand it over to Coturn
default website_backend; # All other domains are handed over to the website
}
# Listening for all TCP traffic on port 443
server {
listen 443;
listen [::]:443;
# Enable SSL pre-read feature to obtain SNI hostname
ssl_preread on;
# Proxy traffic to the corresponding backend based on map results
proxy_pass $backend;
proxy_timeout 1d; # Suggest setting a longer timeout for TURN
proxy_connect_timeout 5s;
}
}
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# server_tokens off;
# server_names_hash_bucket_size 64;
# server_name_in_redirect off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# SSL Settings
##
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
##
# Logging Settings
##
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
##
# Gzip Settings
##
gzip on;
# gzip_vary on;
# gzip_proxied any;
# gzip_comp_level 6;
# gzip_buffers 16 8k;
# gzip_http_version 1.1;
# gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
##
# Virtual Host Configs
##
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
#mail {
# # See sample authentication script at:
# # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript
#
# # auth_http localhost/auth.php;
# # pop3_capabilities "TOP" "USER";
# # imap_capabilities "IMAP4rev1" "UIDPLUS";
#
# server {
# listen localhost:110;
# protocol pop3;
# proxy on;
# }
#
# server {
# listen localhost:143;
# protocol imap;
# proxy on;
# }
#}
-2
View File
@@ -1,2 +0,0 @@
/etc/init.d/nginx stop
rm /var/log/nginx/*
-119
View File
@@ -1,119 +0,0 @@
#!/bin/bash
# Define required environment variables
declare -A required_vars=(
["TURN_EXTERNAL_IP"]="TURN server external IP address"
["TURN_REALM"]="TURN server realm"
["TURN_USERNAME"]="TURN server username"
["TURN_PASSWORD"]="TURN server password"
)
# Additional required variables for production environment
production_vars=(
"TURN_CERT_PATH"
"TURN_KEY_PATH"
)
# Validate environment variables
validate_env_vars() {
local missing_vars=()
local env_file=$1
echo "Verifying TURN server environment variable configuration..."
# Load environment variables
source "$env_file"
# Check basic required variables
for var in "${!required_vars[@]}"; do
if [ -z "${!var}" ]; then
missing_vars+=("$var (${required_vars[$var]})")
fi
done
# If it is a production environment, check additional required variables
if [[ "$NODE_ENV" == "production" ]]; then
for var in "${production_vars[@]}"; do
if [ -z "${!var}" ]; then
missing_vars+=("$var (Required for production)")
fi
done
fi
# If there are missing variables, display an error message and exit
if [ ${#missing_vars[@]} -ne 0 ]; then
echo "Error: The following required TURN server variables are not set:"
printf '%s\n' "${missing_vars[@]}" | sed 's/^/ - /'
echo "Please set these variables in $env_file and try again."
exit 1
fi
echo "TURN server environment variables verified successfully!"
}
# Check parameters
if [ -z "$1" ]; then
echo "Usage: $0 <env_file_path>"
exit 1
fi
ENV_FILE=$1
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Check if the environment variable file exists
if [ ! -f "$ENV_FILE" ]; then
echo "Error: Environment file $ENV_FILE not found"
exit 1
fi
# Validate environment variables
validate_env_vars "$ENV_FILE"
# Read environment variables
source "$ENV_FILE"
echo "Configuring TURN server..."
# Determine which configuration template to use
if [[ "$NODE_ENV" == "development" ]]; then
TEMPLATE_FILE="$SCRIPT_DIR/turnserver_development.conf"
else
TEMPLATE_FILE="$SCRIPT_DIR/turnserver_production.conf"
fi
# Create a temporary configuration file
TEMP_CONF=$(mktemp)
# Read the template and replace variables
while IFS= read -r line || [ -n "$line" ]; do
# Replace external-ip
if [[ $line =~ ^external-ip= ]]; then
echo "external-ip=$TURN_EXTERNAL_IP"
# Replace realm
elif [[ $line =~ ^realm= ]]; then
echo "realm=$TURN_REALM"
# Replace user credentials
elif [[ $line =~ ^user= ]]; then
echo "user=$TURN_USERNAME:$TURN_PASSWORD"
# Replace certificate path
elif [[ $line =~ ^cert= ]]; then
echo "cert=$TURN_CERT_PATH"
# Replace key path
elif [[ $line =~ ^pkey= ]]; then
echo "pkey=$TURN_KEY_PATH"
else
echo "$line"
fi
done < "$TEMPLATE_FILE" > "$TEMP_CONF"
# cp "$TEMP_CONF" turnserver.conf
# Use sudo to copy the configuration file to the target location
cp "$TEMP_CONF" /etc/turnserver.conf
# Delete temporary file
rm "$TEMP_CONF"
# Restart the TURN server
service coturn restart
echo "TURN server configuration has been updated and service restarted."
@@ -1,45 +0,0 @@
# /etc/turnserver.conf
# Listen on all interfaces
listening-ip=0.0.0.0
# Use your server's public IP
external-ip=YourServerPublicIP
# TURN server port
listening-port=3478
# Enable TLS -- TURNS (encrypted TURN)
#tls-listening-port=5349
# Relay port range
min-port=49152
max-port=65535
# Long-term certificate mechanism
lt-cred-mech
# TURN server domain (if any) IP or YourTURNDomain
# realm=YourTURNDomain
realm=YourServerPublicIP
# TURN server certificate and key (for TLS) certificates are not required in the development environment
# cert=/etc/letsencrypt/live/turn.privydrop.app/fullchain.pem
# pkey=/etc/letsencrypt/live/turn.privydrop.app/privkey.pem
# Username and password (a more secure method should be used in a production environment)
user=UserName:PassWord
# Enable verbose logging
verbose
# Allow loopback addresses
# allow-loopback-peers
# Set maximum bandwidth (bytes/second)
# max-bandwidth=0
# Disable TLS
# no-tls
# Disable DTLS
# no-dtls
@@ -1,45 +0,0 @@
# /etc/turnserver.conf
# Listen on all interfaces
listening-ip=0.0.0.0
# Use your server's public IP
external-ip=YourServerPublicIP
# TURN server port
listening-port=3478
# Enable TLS -- TURNS (encrypted TURN)
tls-listening-port=5349
# Relay port range
min-port=49152
max-port=65535
# Long-term certificate mechanism
lt-cred-mech
# TURN server domain (if any) IP or YourTURNDomain
# realm=YourServerPublicIP
realm=YourTURNDomain
# TURN server certificate and key (for TLS)
cert=path/to/your/certFile
pkey=path/to/your/privkeyFile
# Username and password (a more secure method should be used in a production environment)
user=UserName:PassWord
# Enable verbose logging
verbose
# Allow loopback addresses
# allow-loopback-peers
# Set maximum bandwidth (bytes/second)
# max-bandwidth=0
# Disable TLS
# no-tls
# Disable DTLS
# no-dtls
-28
View File
@@ -1,28 +0,0 @@
sudo apt install -y certbot python3-certbot-nginx ssl-cert
sudo apt-get install -y vim coturn
sudo apt-get install -y redis-server
sudo apt-get install -y curl lsb-release
sudo apt install -y ca-certificates gnupg && sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
export NODE_MAJOR=20
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
sudo apt-get update
sudo apt install -y nodejs
sudo npm install -g pnpm pm2
# Install Nginx from official repository
curl -fsSL https://nginx.org/keys/nginx_signing.key | sudo apt-key add -
echo "deb https://nginx.org/packages/ubuntu/ $(lsb_release -cs) nginx" | sudo tee /etc/apt/sources.list.d/nginx.list
sudo apt update && sudo apt install -y nginx
# Verify stream module
nginx -V 2>&1 | grep -o with-stream || echo "Stream module not available"
sudo apt-get clean autoclean
sudo apt-get autoremove --yes
sudo rm -rf /var/lib/{apt,cache,log}/ && sudo rm -rf /tmp/*
-227
View File
@@ -1,227 +0,0 @@
#!/bin/bash
set -euo pipefail
# Check if a build package already exists
if [ -f "out.zip" ]; then
echo "📦 Detected existing build package: out.zip"
echo "📦 Package size: $(du -sh out.zip | cut -f1)"
echo "📝 Build info:"
if [ -f "out/deploy-info.txt" ]; then
cat out/deploy-info.txt
fi
echo ""
echo "⚠️ Choose an option:"
echo " 1. Deploy existing package"
echo " 2. Rebuild and deploy"
echo " 3. Exit"
echo ""
read -p "Select (1/2/3): " -n 1 -r
echo ""
case $REPLY in
1)
echo "🚀 Deploying existing package..."
DEPLOY_EXISTING=true
;;
2)
echo "🔄 Rebuilding..."
rm -rf out out.zip
;;
3)
echo "👋 Exit"
exit 0
;;
*)
echo "❌ Invalid option, aborting"
exit 1
;;
esac
fi
if [ "${DEPLOY_EXISTING:-}" != "true" ]; then
echo "🚀 Start local build..."
# Clean previous build outputs
echo "🧹 Cleaning previous build outputs..."
rm -rf frontend/.next
rm -rf backend/dist
rm -rf out
# Create output directory for packaging
mkdir -p out
# Build frontend
echo "📦 Building frontend..."
cd frontend
pnpm install
pnpm build
cd ..
# Build backend
echo "📦 Building backend..."
cd backend
pnpm install
pnpm build
cd ..
# Prepare deploy bundle
echo "📋 Preparing deploy bundle..."
mkdir -p out/frontend
mkdir -p out/backend
# Copy frontend artifacts
cp -r frontend/.next out/frontend/
cp frontend/package.json out/frontend/
cp -r frontend/public out/frontend/ 2>/dev/null || true
cp -r frontend/app out/frontend/ 2>/dev/null || true
cp -r frontend/components out/frontend/ 2>/dev/null || true
cp -r frontend/lib out/frontend/ 2>/dev/null || true
cp -r frontend/styles out/frontend/ 2>/dev/null || true
cp frontend/next.config.js out/frontend/ 2>/dev/null || true
cp frontend/tailwind.config.ts out/frontend/ 2>/dev/null || true
cp frontend/postcss.config.js out/frontend/ 2>/dev/null || true
cp -r frontend/content out/frontend/ 2>/dev/null || true
# Copy backend artifacts
cp -r backend/dist out/backend/
cp backend/package.json out/backend/
# Write deployment info
echo "📝 Writing deployment info..."
cat > out/deploy-info.txt << EOF
Build time: $(date)
Git commit: $(git rev-parse --short HEAD)
Git branch: $(git branch --show-current)
Frontend BUILD_ID: $(cat frontend/.next/BUILD_ID 2>/dev/null || echo "N/A")
EOF
# Archive deploy bundle
echo "📦 Archiving deploy bundle..."
cd out
zip -r ../out.zip .
cd ..
echo "✅ Local build and packaging completed!"
echo "📦 Package: out.zip"
echo "📦 Size: $(du -sh out.zip | cut -f1)"
fi
# Deploy logic
if [ -f "out.zip" ]; then
echo ""
echo "🚀 Detected out.zip, ready to deploy to server"
echo "⚠️ Deployment will:"
echo " 1. Upload out.zip to server"
echo " 2. Backup current version"
echo " 3. Unzip and replace files"
echo " 4. Restart PM2 apps"
echo ""
read -p "Proceed with deployment? (y/N): " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "🚀 Starting deployment..."
# Load deploy config file
if [ -f "deploy.config" ]; then
source deploy.config
fi
# Validate required environment variables
if [ -z "$DEPLOY_SERVER" ] || [ -z "$DEPLOY_USER" ] || [ -z "$DEPLOY_PATH" ]; then
echo "❌ Missing server configuration. Please configure one of the following:"
echo " 1. Copy deploy.config.example to deploy.config and edit values"
echo " 2. Or set environment variables:"
echo " export DEPLOY_SERVER=your-server-ip"
echo " export DEPLOY_USER=root"
echo " export DEPLOY_PATH=/root/PrivyDrop"
exit 1
fi
# Build SSH options (port/key)
SSH_OPTS=""
SCP_OPTS=""
if [ -n "${SSH_PORT:-}" ]; then
SSH_OPTS+=" -p $SSH_PORT"
SCP_OPTS+=" -P $SSH_PORT"
fi
if [ -n "${SSH_KEY_PATH:-}" ]; then
SSH_OPTS+=" -i $SSH_KEY_PATH"
SCP_OPTS+=" -i $SSH_KEY_PATH"
fi
# Upload build package to server
echo "📤 Uploading package to server..."
# shellcheck disable=SC2086
scp $SCP_OPTS out.zip $DEPLOY_USER@$DEPLOY_SERVER:/tmp/
# Run remote deployment (fix: ensure heredoc script actually executes)
echo "🔧 Executing remote deployment..."
# Inject DEPLOY_PATH and execute heredoc via 'bash -s' on remote host
# shellcheck disable=SC2086
ssh $SSH_OPTS $DEPLOY_USER@$DEPLOY_SERVER "DEPLOY_PATH='$DEPLOY_PATH' bash -s" << 'EOF'
set -euo pipefail
# Create structured backup directory
BACKUP_ROOT="/tmp/privydrop_backup"
BACKUP_DIR="$BACKUP_ROOT/$(date +%Y%m%d_%H%M%S)"
mkdir -p "$BACKUP_DIR/frontend" "$BACKUP_DIR/backend"
# Backup current artifacts if present
if [ -d "$DEPLOY_PATH/frontend/.next" ]; then
echo "📋 Backing up current frontend build..."
mv "$DEPLOY_PATH/frontend/.next" "$BACKUP_DIR/frontend/.next"
fi
if [ -d "$DEPLOY_PATH/backend/dist" ]; then
echo "📋 Backing up current backend build..."
mv "$DEPLOY_PATH/backend/dist" "$BACKUP_DIR/backend/dist"
fi
# Stop PM2 processes
echo "⏹️ Stopping PM2 apps..."
sudo pm2 stop all || true
sudo pm2 delete all || true
# Extract new version
echo "📂 Extracting new version..."
cd "$DEPLOY_PATH"
unzip -o /tmp/out.zip
rm -f /tmp/out.zip
# Fix ownership
sudo chown -R "$(id -un)":"$(id -gn)" "$DEPLOY_PATH/frontend/.next" 2>/dev/null || true
sudo chown -R "$(id -un)":"$(id -gn)" "$DEPLOY_PATH/backend/dist" 2>/dev/null || true
# Start PM2 apps
echo "▶️ Starting PM2 apps..."
sudo pm2 start ecosystem.config.js
# Wait for services to start
sleep 5
# Check PM2 status
echo "🔍 Checking PM2 status..."
sudo pm2 status
# Print version identifiers for verification
if [ -f "$DEPLOY_PATH/frontend/.next/BUILD_ID" ]; then
echo "📦 Frontend BUILD_ID: $(cat "$DEPLOY_PATH/frontend/.next/BUILD_ID")"
fi
if [ -f "$DEPLOY_PATH/deploy-info.txt" ]; then
echo "📝 Deploy info:"
cat "$DEPLOY_PATH/deploy-info.txt" || true
fi
echo "✅ Deployment completed!"
echo "📋 Backup saved at: $BACKUP_DIR"
EOF
echo "🎉 Deployment finished. Check PM2 status on server:"
echo " ssh $DEPLOY_USER@$DEPLOY_SERVER 'sudo pm2 status'"
else
echo "❌ Deployment canceled"
fi
else
echo "❌ out.zip not found"
exit 1
fi
-19
View File
@@ -1,19 +0,0 @@
# Deployment configuration
# Copy this file to 'deploy.config' and fill in your server details
# Server IP or domain
DEPLOY_SERVER="your-server-ip"
# Server username (default: root)
# Note: Using 'ssh root' is recommended here for simplicity. Ensure you understand the
# security implications and restrict access appropriately (keys, firewall, etc.).
DEPLOY_USER="root"
# Deploy path on the server (project root)
DEPLOY_PATH="/root/PrivyDrop"
# SSH port (optional, default 22)
# SSH_PORT="22"
# SSH private key path (optional)
# SSH_KEY_PATH="~/.ssh/id_rsa"
+7
View File
@@ -741,6 +741,13 @@ main() {
# If full + nginx, automatically issue certs and enable 443
provision_letsencrypt_cert || true
if [[ "$DEPLOYMENT_MODE" == "full" && "$WITH_NGINX" == "true" ]]; then
log_info "Refreshing edge containers to apply generated HTTPS/SNI config..."
docker compose up -d --force-recreate nginx >/dev/null
if [[ "$WITH_TURN" == "true" ]]; then
docker compose up -d --force-recreate coturn >/dev/null
fi
fi
# Ensure TURN is running (when requested with --with-turn)
ensure_turn_running || true
+14 -1
View File
@@ -232,8 +232,21 @@ generate_env_file() {
turn_enabled="true"
;;
full)
cors_origin="https://${DOMAIN_NAME:-$LOCAL_IP}"
if [[ -n "$DOMAIN_NAME" ]]; then
if [[ "$DOMAIN_NAME" == www.* ]]; then
local bare_domain="${DOMAIN_NAME#www.}"
cors_origin="https://${DOMAIN_NAME},https://${bare_domain}"
else
cors_origin="https://${DOMAIN_NAME},https://www.${DOMAIN_NAME}"
fi
else
cors_origin="https://${LOCAL_IP}"
fi
if [[ "$WITH_NGINX" == "true" ]]; then
api_url=""
else
api_url="https://${DOMAIN_NAME:-$LOCAL_IP}"
fi
ssl_mode="letsencrypt"
turn_enabled="true"
;;
-1
View File
@@ -42,7 +42,6 @@ backend/
│ │ ├── room.ts
│ │ └── socket.ts
│ └── server.ts # Main application entry point: Express and Socket.IO setup
├── ecosystem.config.js # PM2 configuration file
├── package.json
└── tsconfig.json
```
-1
View File
@@ -42,7 +42,6 @@ backend/
│ │ ├── room.ts
│ │ └── socket.ts
│ └── server.ts # 主应用程序入口点: Express 和 Socket.IO 设置
├── ecosystem.config.js # PM2 配置文件
├── package.json
└── tsconfig.json
```
-400
View File
@@ -1,400 +0,0 @@
# PrivyDrop Deployment Guide (Bare-Metal)
> Audience & Scope: This guide targets developers/operators who prefer a non-container (bare-metal) setup.
>
> Recommended: Prefer the one-click Docker deployment for simplicity and robustness, including auto HTTPS and TURN. See [Docker Deployment Guide](./DEPLOYMENT_docker.md).
This guide provides comprehensive instructions for deploying the full-stack PrivyDrop application, including setting up Redis, a TURN server, the backend service, the frontend application, and configuring Nginx as a reverse proxy.
## 1. Introduction
This document will guide you through preparing your server environment, configuring dependencies, and deploying both the frontend and backend of PrivyDrop. Whether you are setting up a development/testing environment or a full production instance, this guide aims to cover all essential aspects.
## 2. Prerequisites
Before you begin, please ensure your server environment meets the following requirements:
- **Operating System:** A Linux distribution (e.g., Ubuntu 20.04 LTS or newer is recommended).
- **Node.js:** v18.x or higher.
- **npm/pnpm:** The package manager for Node.js.
- **Root or Sudo Privileges:** Required for installing packages and configuring services.
- **Domain Name:** Required for a production deployment.
- **Optional: Base Environment & Docker Image Reference:** If you are starting from a very clean system environment or wish to see the base dependencies for a Docker build, you can refer to the `backend/docker/Dockerfile` (for Docker image creation) and `backend/docker/env_install.log` (dependency installation log) files.
## 3. Environment Installation
**Important Note:** The `backend/docker/env_install.sh` script in the project root contains all necessary dependency installation commands, including Node.js, Redis, Coturn, Nginx, and more. You can run this script directly to install all dependencies:
```bash
# Make the script executable
chmod +x backend/docker/env_install.sh
# Run the installation script
sudo bash backend/docker/env_install.sh
```
This script will automatically install:
- **Node.js v20** - Runtime environment
- **Redis Server** - Used for room management and caching
- **Coturn** - TURN/STUN server (optional, for NAT traversal)
- **Nginx** - Web server and reverse proxy (from official repository)
- **PM2** - Node.js process manager
- **Certbot** - SSL certificate management
After installation, you can verify the services:
```bash
# Verify Node.js version
node -v
# Verify Redis status
sudo systemctl status redis-server
# Verify Nginx installation
nginx -V
# Verify Coturn installation
sudo systemctl status coturn
```
**Configuration Notes:**
- **Redis Configuration:** Default listening on `127.0.0.1:6379`, ensure your backend `.env` file includes correct `REDIS_HOST` and `REDIS_PORT`
- **TURN Service:** Optional configuration, PrivyDrop uses public STUN servers by default, only needed for extremely high NAT traversal requirements
- **Nginx:** Script installs official version and verifies stream module support
**TURN Server Firewall Configuration (if configuring TURN service):**
```bash
# Enable the Coturn service
sudo sed -i 's/#TURNSERVER_ENABLED=1/TURNSERVER_ENABLED=1/' /etc/default/coturn
# Firewall Configuration: Open Turnserver default ports
sudo ufw allow Turnserver
sudo ufw reload
```
The ports seen via `sudo ufw app info Turnserver` are as follows:
- `3478,3479,5349,5350,49152:65535/tcp`
- `3478,3479,5349,5350,49152:65535/udp`
## 4. Application Deployment (Production)
This section describes how to deploy PrivyDrop in a production environment using Nginx and PM2.
### 4.1. Get the Code and Install Dependencies
```bash
git clone https://github.com/david-bai00/PrivyDrop.git
cd PrivyDrop
# Install backend dependencies
cd backend && npm install && cd ..
# Install frontend dependencies
cd frontend && pnpm install && cd ..
```
### 4.2. Build the Application
```bash
cd frontend && pnpm build && cd ..
cd backend && npm run build && cd ..
```
This will generate an optimized production build in the `frontend/.next` and `backend/dist` directories.
### 4.3. Configure Nginx as a Reverse Proxy
In production, Nginx will act as the entry point for all traffic, handling SSL termination and routing requests to the correct frontend or backend service.
1. **Prepare Production Environment Variables for Backend and Frontend**
Before deployment, ensure the production environment files for both backend and frontend are ready. You will need to copy them from the example files and modify them with your server's information.
- **Backend Configuration:**
```bash
# From the project root
cp backend/.env_production_example backend/.env.production
```
Then, edit `backend/.env.production`, configuring at least `CORS_ORIGIN` to your main domain (e.g., `https://privydrop.app`) and your `REDIS` details.
- **Frontend Configuration:**
```bash
# From the project root
cp frontend/.env_production_example frontend/.env.production
```
Then, edit `frontend/.env.production` to set `NEXT_PUBLIC_API_URL` to your backend service domain (e.g., `https://privydrop.app`).
2. **Firewall:**
Open 'Nginx Full' default ports and 443/udp:
```bash
sudo ufw allow 'Nginx Full'
sudo ufw reload # or ufw enable
```
The ports seen via `sudo ufw app info 'Nginx Full'` are as follows:
80,443/tcp
3. **Generate Base Nginx Configuration:**
The `backend/docker/Nginx/` directory provides a configuration script and template. This template uses a temporary "placeholder" certificate to ensure the Nginx configuration is valid before obtaining a real certificate.
- Now, edit the `backend/.env.production` file and add the `NGINX_*` related variables. **Do not include SSL certificate paths yet**. Example:
```
NGINX_SERVER_NAME=privydrop.app # Your main domain
NGINX_FRONTEND_ROOT=/path/to/your/PrivyDrop/frontend # Path to the frontend project root
TURN_REALM=turn.privydrop.app # TURN server domain name (if configuring TURN service)
```
- Execute the script to generate the Nginx configuration file:
```bash
# This script uses variables from your .env file to generate the Nginx config
sudo bash backend/docker/Nginx/configure.sh backend/.env.production
```
### 4.4. Use Certbot to Install a Unified SSL Certificate
With the base Nginx configuration in place, we can now use Certbot to obtain and install a real SSL certificate. We will request a single, unified certificate for all our services (main domain, www, and TURN) and let Certbot automatically update your Nginx configuration.
1. **Install Certbot's Nginx Plugin:**
```bash
sudo apt install python3-certbot-nginx
```
2. **Run Certbot to Request the Certificate:**
- This command automatically detects your Nginx configuration.
- The `-d` flag specifies all domains to be included in the certificate. Ensure your domains' DNS records correctly point to your server's IP.
- The `--deploy-hook` is a crucial parameter: it will automatically restart the Coturn service after a successful certificate renewal, applying the new certificate. This enables fully automated certificate maintenance.
```bash
# Replace privydrop.app with your main domain
sudo certbot --nginx \
-d privydrop.app \
-d www.privydrop.app \
-d turn.privydrop.app \
--deploy-hook "sudo systemctl restart coturn"
```
Follow the on-screen prompts from Certbot (e.g., enter your email, agree to the ToS). Once complete, Certbot will automatically modify your Nginx configuration to enable HTTPS and reload the Nginx service.
Run the following command to check if the certificate path has been replaced:
```bash
sudo grep ssl_certificate /etc/nginx/sites-enabled/default
```
You should see a path pointing to `/etc/letsencrypt/live/privydrop.app/`
3. **Remove the redundant configuration generated by Certbot:**
```bash
sudo bash backend/docker/Nginx/del_redundant_cfg.sh
```
4. **start nginx:**
```bash
sudo systemctl start[reload] nginx
```
If you see an error "Address already in use" (check via `systemctl status nginx.service`), run `pkill nginx`.
### 4.5. Configure and Start the TURN Service (Production)
With the unified SSL certificate obtained, we can now complete the production configuration for the Coturn service.
1. **Configure Environment Variables**:
Open your `backend/.env.production` file and configure all `TURN_*` related variables.
```ini
# .env.production
# ... other variables ...
# TURN/STUN Server (Coturn) Configuration
TURN_REALM=turn.privydrop.app # Your TURN domain
TURN_USERNAME=YourTurnUsername # Set a secure username
TURN_PASSWORD=YourTurnPassword # Set a strong password
# Critical: Point to the unified certificate generated by Certbot for your main domain
TURN_CERT_PATH=/etc/letsencrypt/live/privydrop.app/fullchain.pem
TURN_KEY_PATH=/etc/letsencrypt/live/privydrop.app/privkey.pem
```
2. **Verify SSL Certificate Permissions**:
The Coturn process typically runs as a low-privilege user (e.g., `turnserver` or `coturn`), while certificates generated by Certbot are owned by `root`. We need to adjust permissions to allow Coturn to read the certificate.
```bash
# (Optional) Find the user the coturn service runs as
# ps aux | grep turnserver
# Create a shared group and add the turnserver user to it
sudo groupadd -f ssl-cert
sudo usermod -a -G ssl-cert turnserver # Replace 'turnserver' if the user is different
# Change ownership and permissions of the certificate directories
sudo chown -R root:ssl-cert /etc/letsencrypt/
sudo chmod -R 750 /etc/letsencrypt/
```
3. **Generate Configuration File and Start the Service**:
Run the provided script, which will generate `/etc/turnserver.conf` from your `.env.production` file and restart Coturn.
```bash
# Located in the backend/ directory
# Use the path to your .env file
sudo bash ./docker/TURN/configure.sh backend/.env.production
```
4. **Check Service Status and Test Online**:
- Check the service status:
```bash
sudo systemctl status coturn
# Also, check the logs to ensure there are no permission errors
# sudo journalctl -u coturn -f
```
- **Online Test (Recommended)**:
Once the service is running, use an online tool like the [Metered TURN Server Tester](https://www.metered.ca/turn-server-testing) to verify that your TURNS service is working correctly:
- **TURNS URL**: `turn:turn.privydrop.app:3478` (replace with your domain)
- **Username**: `The username you set in your .env file`
- **Password**: `The password you set in your .env file`
If all checkpoints show a green "Success" or "Reachable", your TURN server is configured successfully.
### 4.6. Run the Application with PM2
PM2 is a powerful process manager for Node.js. We will use it to run both backend and frontend services.
1. **Start Services Using Unified Configuration:**
The project root directory provides a unified `ecosystem.config.js` configuration file that can start all services at once:
```bash
# If services were previously running, stop and delete them first
sudo pm2 stop all && sudo pm2 delete all
# Start all services using the unified configuration file
sudo pm2 start ecosystem.config.js
```
2. **Manage Applications:**
- View status: `pm2 list`
- View logs: `pm2 logs <app_name>` (e.g., `pm2 logs signaling-server` or `pm2 logs privydrop-frontend`)
- Set up startup script: `pm2 startup` followed by `pm2 save`
- Restart services: `pm2 restart all` or specific service `pm2 restart signaling-server`
- Stop services: `pm2 stop all` or specific service `pm2 stop privydrop-frontend`
### 4.7. Daily Incremental Update (Local Build + Remote Replace)
This section describes how to build locally and deploy only the built artifacts to the server. It is optimized for day-to-day releases: fast, low resource usage on the server, and easy to verify.
- Assumes you have completed the first-time deployment and can access the app in production.
- The frontend runs in Next.js Standalone mode (configured in `ecosystem.config.js`), so the server does not need Next CLI or frontend dependencies installed.
0. Sync frontend production environment variables (important)
The local build reads variables from `frontend/.env.production` (e.g., `NEXT_PUBLIC_API_URL`, TURN settings, and build-time flags like `NEXT_IMAGE_UNOPTIMIZED`). To ensure the build matches production behavior, copy the production environment file from the server to your local machine before running `bash build-and-deploy.sh`.
- Example (sync from server to local):
```bash
# Assuming the server project root is /root/PrivyDrop
scp root@<server>:/root/PrivyDrop/frontend/.env.production ./frontend/.env.production
```
- If the file does not exist on the server yet, create it from the example and keep it consistent with production:
```bash
cp frontend/.env_production_example frontend/.env.production
# Fill in NEXT_PUBLIC_API_URL, TURN_*, NEXT_IMAGE_UNOPTIMIZED, etc.
```
- Note: `build-and-deploy.sh` does not auto-create or overwrite `frontend/.env.production`. Make sure it exists locally and matches production; otherwise, the built behavior may differ from the server (e.g., image optimization toggles).
1. Prepare deployment configuration
- From the project root:
```bash
cp deploy.config.example deploy.config
```
- Edit `deploy.config` with at least:
```bash
DEPLOY_SERVER="<your-server-ip-or-domain>"
DEPLOY_USER="root" # Recommended: use ssh root for simplicity
DEPLOY_PATH="/root/PrivyDrop" # Project root on the server
# Optional: SSH_PORT, SSH_KEY_PATH
```
- Security notes: Use SSH key authentication, restrict source IPs, and enforce firewall rules in production.
2. Build locally and deploy
- From the project root:
```bash
bash build-and-deploy.sh
```
- When an existing package (out.zip) is detected, the script lets you choose:
- 1. Deploy existing package
- 2. Rebuild and deploy
- Script flow (summary):
- Build frontend and backend locally
- Package artifacts into `out.zip`
- Upload to server path `/tmp/out.zip`
- Server-side backup to `/tmp/privydrop_backup/YYYYmmdd_HHMMSS/`
- Unzip and replace:
- Frontend: `frontend/.next` (includes `.next/standalone` and `.next/static`)
- Frontend static assets: `frontend/public`
- Frontend content: `frontend/content` (for blog file reads)
- Backend: `backend/dist`
- Restart using `pm2 start ecosystem.config.js`
3. Post-deployment verification
- Check process status on the server:
```bash
ssh root@<server> 'sudo pm2 status'
```
- Compare frontend BUILD_ID (optional):
```bash
ssh root@<server> 'cat /root/PrivyDrop/frontend/.next/BUILD_ID'
```
- Force refresh the browser or use an incognito window to confirm the new version.
4. Backups and manual rollback
- Each deployment creates a structured backup on the server at `/tmp/privydrop_backup/YYYYmmdd_HHMMSS/`:
- Frontend: `frontend/.next`
- Backend: `backend/dist`
- To rollback manually (example):
```bash
# Stop PM2
sudo pm2 stop all && sudo pm2 delete all
# Choose a backup directory, e.g. /tmp/privydrop_backup/20241024_235959
export DEPLOY_PATH=/root/PrivyDrop
export BACKUP=/tmp/privydrop_backup/20241024_235959
# Restore frontend and backend build artifacts
rm -rf "$DEPLOY_PATH/frontend/.next" "$DEPLOY_PATH/backend/dist"
cp -a "$BACKUP/frontend/.next" "$DEPLOY_PATH/frontend/.next"
cp -a "$BACKUP/backend/dist" "$DEPLOY_PATH/backend/dist"
# Restart PM2
sudo pm2 start ecosystem.config.js
```
5. Common issues
- Page still shows the old version: clear browser cache/force refresh; compare BUILD_ID; check Nginx/CDN caching.
- Blog posts not loading: ensure `frontend/content/blog` exists on the server and the PM2 frontend process `cwd` is `./frontend`.
- `out.zip not found`: choose “Rebuild and deploy” to create a new package.
## 5. Troubleshooting
- **Connection Issues:** Check firewall settings, Nginx proxy configurations, `CORS_ORIGIN` settings, and ensure all PM2 processes are running.
- **Nginx Errors:** Use `sudo nginx -t` to check syntax and review `/var/log/nginx/error.log`.
- **PM2 Issues:** Use `pm2 logs <app_name>` to view application logs.
- **Certificate Permissions (Production):** If Coturn or Nginx cannot read SSL certificates, carefully review the file permissions and user/group settings in `Section 4.5`.
## 6. Security & Maintenance
- **SSL Certificate Renewal:** When you successfully configure your certificate using `certbot --nginx` with the `--deploy-hook`, Certbot automatically creates a renewal task for both Nginx and Coturn. No manual intervention is required; the certificate will be renewed and applied automatically before it expires.
- **Firewall:** Maintain strict firewall rules, only allowing necessary ports.
-400
View File
@@ -1,400 +0,0 @@
# Privydrop 部署指南(裸机部署)
> 说明与定位:本指南面向具备 Linux 运维能力的开发者,介绍“裸机(非容器)”部署方式。
>
> 推荐方案:优先使用“一键 Docker 部署”,更简单、更稳健,支持自动签发/续期证书与 TURN。详见 [Docker 部署指南](./DEPLOYMENT_docker.zh-CN.md)。
本指南提供部署 Privydrop 全栈应用的全面说明,包括设置 Redis、TURN 服务器、后端服务、前端应用以及配置 Nginx 作为反向代理。
## 1. 引言
本文档将引导您完成准备服务器环境、配置依赖项和部署 Privydrop 的前后端。无论您是设置开发/测试环境还是完整的生产实例,本指南都旨在涵盖所有基本方面。
## 2. 先决条件
在开始之前,请确保您的服务器环境满足以下要求:
- **操作系统:** Linux 发行版(例如,推荐 Ubuntu 20.04 LTS 或更高版本)。
- **Node.js** v18.x 或更高版本。
- **npm/pnpm** Node.js 的包管理器。
- **Root 或 Sudo 权限:** 安装软件包和配置服务所需。
- **域名:** 生产环境部署需要一个域名。
- **可选:基础环境与 Docker 镜像参考:** 如果您需要从一个非常纯净的系统环境开始搭建,或者希望了解用于 Docker 构建的基础依赖,可以参考 `backend/docker/Dockerfile` 文件(用于 Docker 基础镜像构建)和 `backend/docker/env_install.log` 文件(依赖安装记录)。
## 3. 环境安装
**重要提示:** 项目根目录的 `backend/docker/env_install.sh` 脚本包含了所有必要的依赖安装命令,包括 Node.js、Redis、Coturn、Nginx 等。您可以直接运行此脚本来安装所有依赖:
```bash
# 确保脚本有执行权限
chmod +x backend/docker/env_install.sh
# 运行安装脚本
sudo bash backend/docker/env_install.sh
```
该脚本将自动安装:
- **Node.js v20** - 运行环境
- **Redis Server** - 用于房间管理和缓存
- **Coturn** - TURN/STUN 服务器(可选,用于 NAT 穿透)
- **Nginx** - Web 服务器和反向代理(使用官方仓库)
- **PM2** - Node.js 进程管理器
- **Certbot** - SSL 证书管理
安装完成后,可以验证各服务状态:
```bash
# 验证 Node.js 版本
node -v
# 验证 Redis 状态
sudo systemctl status redis-server
# 验证 Nginx 安装
nginx -V
# 验证 Coturn 安装
sudo systemctl status coturn
```
**注意事项:**
- **Redis 配置:** 默认监听 `127.0.0.1:6379`,请确保后端 `.env` 文件中包含正确的 `REDIS_HOST``REDIS_PORT`
- **TURN 服务:** 为可选配置,Privydrop 默认使用公共 STUN 服务器,只有对 NAT 穿透有极高要求时才需要配置
- **Nginx** 脚本安装官方版本并验证 stream 模块支持
**TURN 服务器防火墙配置(如果需要配置 TURN 服务):**
```bash
# 启用 Coturn 服务
sudo sed -i 's/#TURNSERVER_ENABLED=1/TURNSERVER_ENABLED=1/' /etc/default/coturn
# 防火墙配置:打开 Turnserver 默认端口
sudo ufw allow Turnserver
sudo ufw reload
```
通过 `sudo ufw app info Turnserver` 看到的端口如下:
- `3478,3479,5349,5350,49152:65535/tcp`
- `3478,3479,5349,5350,49152:65535/udp`
## 4. 应用部署 (生产环境)
本节介绍如何使用 Nginx 和 PM2 在生产环境部署 PrivyDrop。
### 4.1. 获取代码并安装依赖
```bash
git clone https://github.com/david-bai00/PrivyDrop.git
cd PrivyDrop
# 安装后端依赖
cd backend && npm install && cd ..
# 安装前端依赖
cd frontend && pnpm install && cd ..
```
### 4.2. 构建应用
```bash
cd frontend && pnpm build && cd ..
cd backend && npm run build && cd ..
```
这将分别在 `frontend/.next``backend/dist` 目录生成优化后的生产版本。
### 4.3. 配置 Nginx 作为反向代理
在生产中,Nginx 将作为所有流量的入口,负责 SSL 终止,并将请求路由到正确的前端或后端服务。
1. **为后端和前端准备生产环境变量**
在部署之前,请确保后端和前端的生产环境变量文件已准备就绪。您需要从示例文件复制并根据您的服务器信息进行修改。
- **后端配置:**
```bash
# 位于项目根目录
cp backend/.env_production_example backend/.env.production
```
然后编辑 `backend/.env.production`,至少配置 `CORS_ORIGIN` 为您的主域名 (例如 `https://privydrop.app`) 以及 `REDIS` 相关信息。
- **前端配置:**
```bash
# 位于项目根目录
cp frontend/.env_production_example frontend/.env.production
```
然后编辑 `frontend/.env.production`,配置 `NEXT_PUBLIC_API_URL` 为您的后端服务域名 (例如 `https://privydrop.app`)。
2. **防火墙:**
打开'Nginx Full'默认端口以及 443/udp
```bash
sudo ufw allow 'Nginx Full'
sudo ufw reload # 或 ufw enable
```
通过 sudo ufw app info 'Nginx Full'看到的端口如下:
80,443/tcp
3. **生成 Nginx 基础配置:**
后端项目 `backend/docker/Nginx/` 目录中提供了配置脚本和模板。此模板使用一个临时的"占位符"证书,以确保 Nginx 配置在申请真实证书前是有效的。
- 现在,编辑 `backend/.env.production` 文件,添加 `NGINX_*` 相关变量。**无需 SSL 证书路径**。示例为:
```
NGINX_SERVER_NAME=privydrop.app # 你的主域名
NGINX_FRONTEND_ROOT=/path/to/your/PrivyDrop/frontend # 前端项目根目录
TURN_REALM=turn.privydrop.app # TURN 服务器域名(如需配置 TURN 服务)
```
- 执行脚本生成 Nginx 配置文件:
```bash
# 此脚本会使用 .env 文件中的变量来生成 Nginx 配置文件
sudo bash backend/docker/Nginx/configure.sh backend/.env.production
```
### 4.4. 使用 Certbot 安装统一 SSL 证书
现在 Nginx 有了基础配置,我们可以使用 Certbot 来获取并安装真实的 SSL 证书。我们将为所有服务(主域名、www 和 TURN)申请一张统一的证书,并让 Certbot 自动更新 Nginx 配置。
1. **安装 Certbot 的 Nginx 插件:**
```bash
sudo apt install python3-certbot-nginx
```
2. **运行 Certbot 申请证书:**
- 此命令会自动检测您的 Nginx 配置并为其安装证书。
- `-d` 参数指定所有需要包含在此证书中的域名。请确保您的域名 DNS 已正确解析到服务器 IP。
- `--deploy-hook` 是一个关键参数:它会在证书成功续期后,自动重启 Coturn 服务,以加载新证书。这实现了完全自动化的证书维护。
```bash
# 将 privydrop.app 替换为你的主域名
sudo certbot --nginx \
-d privydrop.app \
-d www.privydrop.app \
-d turn.privydrop.app \
--deploy-hook "sudo systemctl restart coturn"
```
按照 Certbot 的提示操作(例如输入邮箱、同意服务条款等)。
运行如下命令,查看证书路径是否已替换:
```bash
sudo grep ssl_certificate /etc/nginx/sites-enabled/default
```
应该能看到指向 `/etc/letsencrypt/live/privydrop.app/` 的路径
3. **删除由 Certbot 产生的多余配置:**
```bash
sudo bash backend/docker/Nginx/del_redundant_cfg.sh
```
4. **启动 nginx 服务:**
```bash
sudo systemctl start[reload] nginx
```
如果报错显示 Address already in use(通过 systemctl status nginx.service 查看),则运行 pkill nginx。
### 4.5. 配置并启动 TURN 服务 (生产环境)
获取到统一的 SSL 证书后,我们现在来完成 Coturn 服务的生产环境配置。
1. **配置环境变量**:
打开后端的 `.env.production` 文件,配置所有 `TURN_*` 相关变量。
```ini
# .env.production
# ... 其他变量 ...
# TURN/STUN Server (Coturn) Configuration
TURN_REALM=turn.privydrop.app # 你的 TURN 域名
TURN_USERNAME=YourTurnUsername # 设置一个安全的用户名
TURN_PASSWORD=YourTurnPassword # 设置一个强密码
# 关键:将证书路径指向由 Certbot 为主域名生成的统一证书
TURN_CERT_PATH=/etc/letsencrypt/live/privydrop.app/fullchain.pem
TURN_KEY_PATH=/etc/letsencrypt/live/privydrop.app/privkey.pem
```
2. **验证 SSL 证书权限**:
Coturn 进程通常以一个低权限用户(如 `turnserver` 或 `coturn`)运行,而 Certbot 生成的证书文件默认属于 `root` 用户。因此,我们需要调整权限,确保 Coturn 有权限读取证书。
```bash
# (可选) 查找 coturn 服务的运行用户
# ps aux | grep turnserver
# 创建一个共享组,并将 turnserver 用户添加进去
sudo groupadd -f ssl-cert
sudo usermod -a -G ssl-cert turnserver # 如果运行用户不是 turnserver,请替换
# 更改证书目录的所有权和权限
sudo chown -R root:ssl-cert /etc/letsencrypt/
sudo chmod -R 750 /etc/letsencrypt/
```
3. **生成配置文件并启动服务**:
运行项目提供的脚本,它会根据 `.env.production` 文件生成 `/etc/turnserver.conf` 并重启 Coturn。
```bash
# 使用你的 .env 文件路径
sudo bash backend/docker/TURN/configure.sh backend/.env.production
```
4. **检查服务状态与在线测试**:
- 检查服务状态:
```bash
sudo systemctl status coturn
# 同时检查日志确保没有权限错误
# sudo journalctl -u coturn -f
```
- **在线测试 (推荐)**:
服务启动后,使用在线工具,如 [Metered TURN Server Tester](https://www.metered.ca/turn-server-testing),验证 TURNS 服务是否正常工作:
- **TURNS URL**: `turn:turn.privydrop.app:3478` (将域名替换为你的)
- **Username**: `你在 .env 中设置的用户名`
- **Password**: `你在 .env 中设置的密码`
如果所有检查点都显示绿色 "Success" 或 "Reachable",则表示您的 TURN 服务器已成功配置。
### 4.6. 使用 PM2 运行应用
PM2 是一个强大的 Node.js 进程管理器,我们将用它来运行后端和前端服务。
1. **使用统一配置文件启动服务:**
项目根目录提供了一个统一的 `ecosystem.config.js` 配置文件,可以一次性启动所有服务:
```bash
# 如果之前运行过服务,先停止并删除
sudo pm2 stop all && sudo pm2 delete all
# 使用统一配置文件启动所有服务
sudo pm2 start ecosystem.config.js
```
2. **管理应用:**
- 查看状态: `pm2 list`
- 查看日志: `pm2 logs <app_name>` (例如:`pm2 logs signaling-server` 或 `pm2 logs privydrop-frontend`)
- 设置开机自启: `pm2 startup` 然后 `pm2 save`
- 重启服务: `pm2 restart all` 或指定服务 `pm2 restart signaling-server`
- 停止服务: `pm2 stop all` 或指定服务 `pm2 stop privydrop-frontend`
### 4.7. 日常增量更新(本地构建 + 远程替换)
本小节介绍如何在本地构建后,将前后端的生产产物一并打包上传到服务器,完成“增量更新”。该流程适合日常发布,速度快、资源占用低。
- 默认假设你已按“首次部署”完成环境配置(包括 PM2、Nginx/证书等),并能正常访问应用。
- 默认使用前端 Next.js Standalone 运行方式(ecosystem.config.js 已配置),服务器无需安装前端依赖和 next CLI。
0. 同步前端生产环境变量(重要)
本地构建会读取 `frontend/.env.production` 中的变量(例如 `NEXT_PUBLIC_API_URL`、TURN 配置、以及构建期开关 `NEXT_IMAGE_UNOPTIMIZED` 等)。
为确保构建产物与线上一致,请在执行 `bash build-and-deploy.sh` 之前,将“线上部署环境的 `frontend/.env.production`”拷贝到本地对应路径。
- 示例(从服务器同步到本地):
```bash
# 假设服务器项目根目录为 /root/PrivyDrop
scp root@<server>:/root/PrivyDrop/frontend/.env.production ./frontend/.env.production
```
- 如线上暂未建立该文件,可基于示例创建并与线上保持一致:
```bash
cp frontend/.env_production_example frontend/.env.production
# 按需填写 NEXT_PUBLIC_API_URL、TURN_*、NEXT_IMAGE_UNOPTIMIZED 等
```
- 说明:`build-and-deploy.sh` 不会自动生成/覆盖你的 `frontend/.env.production`,请确保本地文件存在且与线上一致,否则可能出现与线上不一致的行为(例如图片优化开关不同导致的差异)。
1. 准备部署配置
- 在项目根目录复制示例配置:
```bash
cp deploy.config.example deploy.config
```
- 编辑 `deploy.config`,至少设置:
```bash
DEPLOY_SERVER="<你的服务器IP或域名>"
DEPLOY_USER="root" # 推荐使用 ssh root 登录(简单直接)
DEPLOY_PATH="/root/PrivyDrop" # 你的服务器项目根目录
# 可选:SSH_PORT、SSH_KEY_PATH
```
- 安全建议:生产环境请启用密钥登录、限制来源 IP、开启防火墙(仅放行必要端口)。
2. 本地构建并部署
- 在项目根目录执行:
```bash
bash build-and-deploy.sh
```
- 当脚本检测到现有打包(out.zip)时,可选择:
- 1. 直接部署现有包
- 2. 重新构建并部署
- 脚本流程(简述):
- 本地构建前端与后端
- 将产物打包为 `out.zip`
- 上传至服务器 `/tmp/out.zip`
- 服务器侧备份当前版本到 `/tmp/privydrop_backup/YYYYmmdd_HHMMSS/`
- 解压替换:
- 前端:`frontend/.next`(包含 `.next/standalone` 与 `.next/static`
- 前端静态资源:`frontend/public`
- 前端内容:`frontend/content`(用于博客文件读取)
- 后端:`backend/dist`
- 使用 `pm2 start ecosystem.config.js` 重启应用
3. 发布校验
- 服务器上查看进程状态:
```bash
ssh root@<server> 'sudo pm2 status'
```
- 核对前端 BUILD_ID(可选):
```bash
ssh root@<server> 'cat /root/PrivyDrop/frontend/.next/BUILD_ID'
```
- 浏览器强制刷新或使用隐身模式,确认页面为新版本。
4. 备份和回退(手工)
- 每次部署会在服务器保存结构化备份:`/tmp/privydrop_backup/YYYYmmdd_HHMMSS/`
- 前端:`frontend/.next`
- 后端:`backend/dist`
- 如需回退,可手工执行(示例):
```bash
# 停止 PM2
sudo pm2 stop all && sudo pm2 delete all
# 假设选定备份目录为 /tmp/privydrop_backup/20241024_235959
export DEPLOY_PATH=/root/PrivyDrop
export BACKUP=/tmp/privydrop_backup/20241024_235959
# 恢复前端与后端构建产物
rm -rf "$DEPLOY_PATH/frontend/.next" "$DEPLOY_PATH/backend/dist"
cp -a "$BACKUP/frontend/.next" "$DEPLOY_PATH/frontend/.next"
cp -a "$BACKUP/backend/dist" "$DEPLOY_PATH/backend/dist"
# 重启 PM2
sudo pm2 start ecosystem.config.js
```
5. 常见问题
- 页面仍显示旧版本:清除浏览器缓存/强制刷新;核对 BUILD_ID;检查 Nginx/CDN 缓存。
- 前端博客文章为空:确认服务器目录存在 `frontend/content/blog`,并确保 PM2 前端进程的 `cwd` 为 `./frontend`。
- 部署脚本报错 `out.zip not found`:先选择“重新构建并部署”。
## 5. 故障排除
- **连接问题:** 检查防火墙、Nginx 代理设置、CORS_ORIGIN 配置,确保所有 PM2 进程都在运行。
- **Nginx 错误:** `sudo nginx -t` 检查语法,查看 `/var/log/nginx/error.log`。
- **PM2 问题:** `pm2 logs <app_name>` 查看应用日志。
- **证书权限 (生产环境)** 如果 Coturn 或 Nginx 无法读取 SSL 证书,请仔细检查 `第 4.5 节` 中的文件权限和用户/组设置。
## 6. 安全与维护
- **SSL 证书续订:** 当你使用 `certbot --nginx` 并配合 `--deploy-hook` 成功配置证书后,Certbot 会自动处理 Nginx 证书的续订和 Coturn 服务的重启。你无需手动干预或使用额外的脚本。
- **防火墙:** 保持防火墙规则严格,仅允许必要的端口。
+54 -22
View File
@@ -14,8 +14,8 @@ bash ./deploy.sh --mode lan-http --with-turn
# LAN HTTPS (self-signed; dev/managed env; explicitly enable 8443)
bash ./deploy.sh --mode lan-tls --enable-web-https --with-nginx
# Public IP without domain (with TURN)
bash ./deploy.sh --mode public --with-turn
# Public IP without domain (with TURN; recommended with Nginx for same-origin)
bash ./deploy.sh --mode public --with-turn --with-nginx
# Public domain (HTTPS + Nginx + TURN + SNI 443, auto-issue/renew certs)
bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn --le-email you@domain.com
@@ -60,6 +60,25 @@ Compared to traditional deployment methods, Docker deployment offers the followi
- **Disk**: 5GB+ available space
- **Network**: 100Mbps+
### Low-Memory Server Notes (important for 1GB2GB hosts)
- The frontend Docker build runs `next build`, which can be killed by the kernel on very small hosts.
- On fresh 1GB2GB servers, add at least **1GB swap** before the first production build if memory pressure is high.
- Typical symptom: the frontend image build exits unexpectedly during `next build` or the host shows OOM messages in `dmesg`.
Recommended one-time swap setup on Ubuntu:
```bash
sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -h
```
After swap is enabled, rerun the deploy command.
### Software Dependencies
- Docker 20.10+
@@ -80,8 +99,8 @@ cd PrivyDrop
### 2. One-Click Deployment
```bash
# Basic deployment (recommended for beginners)
bash ./deploy.sh
# Always pass an explicit deployment mode
bash ./deploy.sh --mode lan-http
```
That's it! 🎉
@@ -128,10 +147,12 @@ bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn -
**Features**:
- ✅ HTTPS secure access (Lets Encrypt auto-issue/renew, zero downtime)
- ✅ HTTPS secure access (Lets Encrypt auto-issue/renew)
- ✅ Nginx reverse proxy
- ✅ Built-in TURN server (default port range 49152-49252/udp)
- ✅ SNI 443 multiplexing (turn.<domain> → coturn:5349; others → web:8443)
- ✅ Same-origin frontend/API gateway by default when `--with-nginx` is enabled (`NEXT_PUBLIC_API_URL` is generated as an empty string, so the browser uses `/api` and `/socket.io/`)
- ✅ Production CORS generation covers the canonical domain and its `www` variant by default (for example `https://example.com,https://www.example.com`)
- ✅ Complete production setup
> Tip: The script no longer auto-detects the deployment mode; always pass `--mode lan-http|lan-tls|public|full`. If the detected LAN IP is not the one you expect, add `--local-ip 192.168.x.x` to override.
@@ -162,17 +183,14 @@ NO_PROXY=localhost,127.0.0.1,backend,frontend,redis,coturn
### Common Flags
```bash
# Enable only Nginx reverse proxy
bash ./deploy.sh --with-nginx
# Enable TURN (recommended in public/full)
bash ./deploy.sh --with-turn
# Explicitly enable SNI 443 (auto-enabled in full+domain; use --no-sni443 to disable)
bash ./deploy.sh --with-sni443
# Always include an explicit --mode (examples)
bash ./deploy.sh --mode lan-http --with-nginx
bash ./deploy.sh --mode lan-http --with-turn
bash ./deploy.sh --mode public --with-turn --with-nginx
bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn --with-sni443 --le-email you@domain.com
# Adjust TURN port range (default 49152-49252/udp)
bash ./deploy.sh --mode full --with-turn --turn-port-range 55000-55100
bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn --le-email you@domain.com --turn-port-range 55000-55100
```
## 🌐 Access Methods
@@ -191,7 +209,7 @@ bash ./deploy.sh --mode full --with-turn --turn-port-range 55000-55100
### HTTPS Access (lan-tls/full)
- lan-tls: with `--enable-web-https`, access via `https://localhost:8443` (certs in `docker/ssl/`). Import `docker/ssl/ca-cert.pem` into your browser or trust store on first use.
- full: after Lets Encrypt issuance, access via `https://<your-domain>` (443). Certs auto-issue/renew; hot-reload is handled via deploy hook.
- full: after Lets Encrypt issuance, access via `https://<your-domain>` (443). Certs auto-issue/renew; the deploy hook hot-reloads edge services on renewal, and the initial full-mode deploy also force-recreates `nginx` (and `coturn` when enabled) to guarantee the new HTTPS/SNI config is active.
## 🔍 Management Commands
@@ -325,8 +343,10 @@ sudo ufw status
**Solution**:
```bash
# Enable TURN server
bash ./deploy.sh --with-turn
# Enable TURN server (re-run with an explicit --mode)
bash ./deploy.sh --mode lan-http --with-turn
# or (public IP, recommended same-origin via Nginx)
bash ./deploy.sh --mode public --with-turn --with-nginx
# Check network connectivity
curl -I http://localhost:3001/api/get_room
@@ -366,7 +386,8 @@ docker system prune -f
1. **Enable Nginx Caching**:
```bash
bash deploy.sh --with-nginx
# Example (public IP, same-origin via Nginx)
bash ./deploy.sh --mode public --with-turn --with-nginx
```
2. **Configure Resource Limits**:
@@ -408,7 +429,7 @@ networks:
```bash
# Auto-enabled (requires HTTPS)
bash deploy.sh --mode full --with-nginx
bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn --le-email you@domain.com
```
## 🔒 Security Configuration
@@ -444,6 +465,7 @@ Usage (strongly recommended)
3) CORS
- For convenience, common dev origins are allowed by default: `https://<LAN IP>:8443`, `https://localhost:8443`, `http://localhost`, `http://<LAN IP>`, `http://localhost:3002`, `http://<LAN IP>:3002`.
- To minimize allowed origins, edit `CORS_ORIGIN` in `.env` and then `docker compose restart backend`.
- In production, `CORS_ORIGIN` is a comma-separated list consumed by both Express and Socket.IO. Example: `CORS_ORIGIN=https://example.com,https://www.example.com`.
4) Health checks
- `curl -kfsS https://localhost:8443/api/health` → 200
@@ -456,6 +478,11 @@ Usage (strongly recommended)
1) Point your domain A record to the server IP (optional: also `turn.<your-domain>` to the same IP)
Recommended DNS / CDN layout:
- Web entry: choose one canonical hostname for `--domain` (for example `example.com`) and redirect alternate hostnames such as `www.example.com` at the CDN/DNS layer if desired.
- TURN entry: keep `turn.<your-domain>` as DNS-only when using Cloudflare so TURN traffic does not go through the HTTP proxy.
- Current certificate issuance covers `--domain` and `turn.<your-domain>` by default. If you need direct HTTPS on an additional hostname such as `www.<your-domain>`, add that certificate handling separately or make it redirect to the canonical host.
2) Run:
```bash
@@ -471,7 +498,8 @@ Usage (strongly recommended)
In full mode, certificates are auto-issued and auto-renewed:
- Initial issuance: webroot (no downtime); system certs live under `/etc/letsencrypt/live/<domain>/`; copied to `docker/ssl/` and 443 is enabled.
- Renewal: `certbot.timer` or `/etc/cron.d/certbot` runs daily; the deploy-hook copies new certs to `docker/ssl/` and hot-reloads Nginx/Coturn.
- Initial issuance is followed by `docker compose up -d --force-recreate nginx` (and `coturn` when enabled) so the freshly generated HTTPS/SNI config is guaranteed to be mounted and active. Expect a brief reconnect window during this first cutover.
- Renewal: `certbot.timer` or `/etc/cron.d/certbot` runs daily; the deploy-hook copies new certs to `docker/ssl/`, sends `HUP` to coturn when possible, and hot-reloads Nginx/Coturn (falling back to container restart if needed).
- Lineage suffixes (-0001/-0002) are handled automatically.
### Network Security
@@ -512,8 +540,12 @@ logs/
# Pull latest code
git pull origin main
# Redeploy
bash deploy.sh
# Re-run the same deployment command you used initially (examples)
bash ./deploy.sh --mode lan-http
# or (public IP)
bash ./deploy.sh --mode public --with-turn --with-nginx
# or (full domain)
bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn --le-email you@domain.com
```
### Data Backup
+52 -20
View File
@@ -14,8 +14,8 @@ bash ./deploy.sh --mode lan-http --with-turn
# 内网 HTTPS(自签,开发/受管环境,需显式开启 8443)
bash ./deploy.sh --mode lan-tls --enable-web-https --with-nginx
# 公网IP(无域名),含 TURN
bash ./deploy.sh --mode public --with-turn
# 公网IP(无域名),含 TURN(推荐同源经 Nginx
bash ./deploy.sh --mode public --with-turn --with-nginx
# 公网域名(HTTPS + Nginx + TURN + SNI 443 分流,自动申请/续期证书)
bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn --le-email you@domain.com
@@ -60,6 +60,25 @@ bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn -
- **磁盘**: 5GB 及以上可用空间
- **网络**: 100Mbps 及以上
### 低内存服务器说明(1GB–2GB 主机建议必看)
- 前端镜像构建会执行 `next build`,在小内存服务器上可能被内核直接 OOM 杀掉。
- 对于全新 1GB–2GB 服务器,如果首轮生产构建时内存紧张,建议先加至少 **1GB swap**
- 典型现象:前端镜像在 `next build` 阶段异常退出,或 `dmesg` 中出现 OOM / killed process 记录。
Ubuntu 推荐一次性 swap 配置:
```bash
sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -h
```
启用 swap 后,再重新执行部署命令。
### 软件依赖
- Docker 20.10+
@@ -126,10 +145,12 @@ bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn -
**特性**:
- ✅ HTTPS 安全访问(Lets Encrypt 自动签发/续期,无停机
- ✅ HTTPS 安全访问(Lets Encrypt 自动签发/续期)
- ✅ Nginx 反向代理
- ✅ 内置 TURN 服务器(默认端口段 49152-49252/udp,可覆盖)
- ✅ SNI 443 分流(turn.<domain> → coturn:5349,其余 → web:8443
- ✅ 启用 `--with-nginx` 时默认前后端同源(`NEXT_PUBLIC_API_URL` 会生成为空字符串,浏览器统一走 `/api``/socket.io/`
- ✅ 生产环境默认同时生成主域名与 `www` 变体的 CORS 来源(例如 `https://example.com,https://www.example.com`
- ✅ 完整生产环境配置
> 提示:脚本不再自动判断部署模式,请显式传递 `--mode lan-http|lan-tls|public|full`。若自动检测到的局域网 IP 与预期不符,可使用 `--local-ip 192.168.x.x` 进行覆盖。
@@ -160,17 +181,14 @@ NO_PROXY=localhost,127.0.0.1,backend,frontend,redis,coturn
### 常用开关
```bash
# 仅启用 Nginx
bash ./deploy.sh --with-nginx
# 启用 TURNpublic/full 建议)
bash ./deploy.sh --with-turn
# 显式启用 SNI 443full+domain 默认开启,可用 --no-sni443 关闭)
bash ./deploy.sh --with-sni443
# 部署命令请始终显式传递 --mode(示例)
bash ./deploy.sh --mode lan-http --with-nginx
bash ./deploy.sh --mode lan-http --with-turn
bash ./deploy.sh --mode public --with-turn --with-nginx
bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn --with-sni443 --le-email you@domain.com
# 调整 TURN 端口段(默认 49152-49252/udp
bash ./deploy.sh --mode full --with-turn --turn-port-range 55000-55100
bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn --le-email you@domain.com --turn-port-range 55000-55100
```
## 🌐 访问方式
@@ -189,7 +207,7 @@ bash ./deploy.sh --mode full --with-turn --turn-port-range 55000-55100
### HTTPS 访问(lan-tls/full
- lan-tls:开启 `--enable-web-https` 后通过 `https://localhost:8443` 访问(证书在 `docker/ssl/`)。首次访问需导入 `docker/ssl/ca-cert.pem` 到浏览器或系统信任。
- full:签发 Lets Encrypt 后通过 `https://<your-domain>` 访问(443)。
- full:签发 Lets Encrypt 后通过 `https://<your-domain>` 访问(443)。续期阶段由 deploy-hook 热重载边缘服务;首次 full 部署还会强制重建 `nginx`(启用 TURN 时也会重建 `coturn`),确保新的 HTTPS/SNI 配置立即生效。
## 🔍 管理命令
@@ -323,8 +341,10 @@ sudo ufw status
**解决方案**:
```bash
# 启用TURN服务器
bash ./deploy.sh --with-turn
# 启用 TURN(重新执行部署命令并显式指定 --mode)
bash ./deploy.sh --mode lan-http --with-turn
# 或(公网IP,推荐同源经 Nginx)
bash ./deploy.sh --mode public --with-turn --with-nginx
# 检查网络连接
curl -I http://localhost:3001/api/get_room
@@ -368,7 +388,8 @@ docker system prune -f
1. **启用 Nginx 缓存**:
```bash
bash ./deploy.sh --with-nginx
# 示例(公网IP,同源经 Nginx
bash ./deploy.sh --mode public --with-turn --with-nginx
```
2. **配置资源限制**:
@@ -410,7 +431,7 @@ networks:
```bash
# 自动启用 (需要 HTTPS)
bash ./deploy.sh --mode full --with-nginx
bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn --le-email you@domain.com
```
## 🔒 HTTPS 与安全
@@ -446,6 +467,7 @@ bash ./deploy.sh --mode lan-tls --enable-web-https --with-nginx
3) 跨域(CORS)说明
- 为方便开发与调试,脚本已默认放开常见来源:`https://<局域网IP>:8443``https://localhost:8443``http://localhost``http://<局域网IP>``http://localhost:3002``http://<局域网IP>:3002`
- 若仍需最小化来源,请在 `.env` 中精准收敛 `CORS_ORIGIN`,并 `docker compose restart backend`
- 生产环境里,`CORS_ORIGIN` 是供 Express 与 Socket.IO 共用的逗号分隔来源列表。例如:`CORS_ORIGIN=https://example.com,https://www.example.com`
4) 健康检查
- `curl -kfsS https://localhost:8443/api/health` → 200
@@ -458,6 +480,11 @@ bash ./deploy.sh --mode lan-tls --enable-web-https --with-nginx
1) 将域名 A 记录解析至服务器 IP(可选:`turn.<your-domain>` 指向相同 IP
推荐的 DNS / CDN 布局:
- Web 入口:为 `--domain` 选择一个规范主机名(例如 `example.com`),其他主机名(例如 `www.example.com`)建议在 CDN / DNS 层做跳转到规范主机名。
- TURN 入口:如果使用 Cloudflare,建议将 `turn.<your-domain>` 设为 DNS only,避免 TURN 流量经过 HTTP 代理。
- 当前脚本默认申请 `--domain``turn.<your-domain>` 的证书;如果你还想直接提供 `https://www.<your-domain>`,请额外处理该证书,或让它跳转到规范主机名。
2) 运行:
```bash
@@ -473,7 +500,8 @@ bash ./deploy.sh --mode lan-tls --enable-web-https --with-nginx
full 模式自动申请并续期证书:
- 首次签发:webroot 模式(无停机),系统证书在 `/etc/letsencrypt/live/<domain>/`,脚本复制到 `docker/ssl/` 并启用 443
- 续期:`certbot.timer``/etc/cron.d/certbot` 每日尝试 `certbot renew`deploy-hook 自动复制新证书并热重载 Nginx/Coturn
- 首次签发后,脚本还会执行 `docker compose up -d --force-recreate nginx`(启用 TURN 时也会重建 `coturn`),确保新生成的 HTTPS / SNI 配置已经挂载并生效;首次切换时可能会有短暂重连。
- 续期:`certbot.timer``/etc/cron.d/certbot` 每日尝试 `certbot renew`deploy-hook 自动复制新证书,优先对 coturn 发送 `HUP`,并热重载 Nginx/Coturn(必要时回退到容器重启);
- 证书谱系(-0001/-0002)已自动适配,无需手动处理。
### 网络安全
@@ -514,8 +542,12 @@ logs/
# 拉取最新代码
git pull origin main
# 重新部署
bash ./deploy.sh
# 重新执行你最初的部署命令(示例)
bash ./deploy.sh --mode lan-http
# 或(公网IP
bash ./deploy.sh --mode public --with-turn --with-nginx
# 或(公网域名 full
bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn --le-email you@domain.com
```
### 数据备份
+168
View File
@@ -0,0 +1,168 @@
# PrivyDrop AI Playbook — Code Map
This map is designed for quick orientation. It lists directories and key entry files with short notes, and intentionally does not cover “frequently changed spots” or detailed impact analysis.
## Frontend (Next.js, TypeScript)
- `frontend/app/` — App Router routes and pages.
- `frontend/app/[lang]/page.tsx` — Home page entry; generates metadata and SEO structured data (JsonLd), with multilingual canonical links.
- `frontend/app/[lang]/*/page.tsx` — Static pages: features, about, faq, help, terms, privacy; each generates multilingual SEO metadata.
- `frontend/app/[lang]/blog/page.tsx` — Blog list page; renders multilingual post lists.
- `frontend/app/[lang]/blog/[slug]/page.tsx` — Blog post page; MDX rendering, TOC, breadcrumbs, and JSON-LD structured data.
- `frontend/app/[lang]/blog/tag/[tag]/page.tsx` — Blog tag page; lists posts by tag.
- `frontend/app/[lang]/layout.tsx` — Global layout & providers (ThemeProvider, Header/Footer); generates organization/site structured data.
- `frontend/app/[lang]/HomeClient.tsx` — Main client component composing the home layout (Hero, ClipboardApp, HowItWorks, video demo, system diagram, features, FAQ), with multi-platform video links (YouTube/Bilibili).
- `frontend/app/api/health/route.ts` — Basic health API.
- `frontend/app/api/health/detailed/route.ts` — Detailed health API.
- `frontend/app/sitemap.ts` — Sitemap generator; multilingual URLs and dynamic blog entries.
- `frontend/middleware.ts` — i18n and routing middleware.
- `frontend/app/config/environment.ts` — Runtime/env config (ICE, endpoints, etc.).
- `frontend/app/config/api.ts` — Backend API client wrapper.
- `frontend/components/` — UI layer, including the orchestrator and child components.
- `frontend/components/ClipboardApp.tsx` — Top-level UI orchestrator. Integrates 5 business hooks (useWebRTCConnection/useFileTransferHandler/useRoomManager/usePageSetup/useClipboardAppMessages), and handles global drag events plus the Send/Retrieve tabs.
- UX: when switching to Retrieve and all of the following hold—“not in a room, no roomId in URL, empty input, cached ID exists”—it auto-fills and joins (reads `frontend/lib/roomIdCache.ts`).
- Connection feedback: integrates `useConnectionFeedback` (`frontend/hooks/useConnectionFeedback.ts`) to map WebRTC states to UI messages (negotiating, 8s slow hint, disconnect/reconnect/restored hints when visible). Slow hints reuse `frontend/utils/useOneShotSlowHint.ts`.
- `frontend/hooks/` — Business logic hub (React Hooks).
- `useRoomManager.ts`
- Join flow: `join_inProgress` (immediate), `join_slow` (3s, reuses `useOneShotSlowHint`), `join_timeout` (15s); timers are cleared on both success and failure.
- Equivalent success signals: before `joinResponse`, receiving `ready/recipient-ready/offer` is treated as “joined”, and clears the 3s/15s timers.
- Others: room status copy, share-link generation, leave room, input validation (750ms debounce).
- `useConnectionFeedback.ts`
- State normalization: `new/connecting``negotiating`; `failed/closed``disconnected` (reuses `utils/rtcPhase.ts`).
- Negotiation slow hint: an 8s timer (`rtc_slow`), shown at most once per negotiation attempt; if it fires in background, its deferred and emitted once on foreground if still negotiating (reuses `useOneShotSlowHint`).
- One-shot hints: first `connected` (`rtc_connected`) is shown once; foreground reconnecting (`rtc_reconnecting`) and restored (`rtc_restored`) hints.
- i18n copy & types
- Copy: `frontend/constants/messages/*.{ts}` (zh/en/ja/es/de/fr/ko filled).
- Types: `frontend/types/messages.ts` (ClipboardApp includes `join_*` and `rtc_*` message keys).
- `frontend/components/ClipboardApp/SendTabPanel.tsx` — Send panel: rich-text editor, file upload, room ID generation (4-digit vs UUID), share-link generation.
- UX: clicking “Use cached ID” triggers join immediately on the sender side, saving one manual click.
- `frontend/components/ClipboardApp/RetrieveTabPanel.tsx` — Retrieve panel: room join, file receiving, directory selection (File System Access API), rich-text display.
- `frontend/components/ClipboardApp/FileListDisplay.tsx` — File list: file/folder grouping, progress tracking, browser-specific download strategies (Chrome auto download; others prompt manual save), download count stats.
- `frontend/components/ClipboardApp/FullScreenDropZone.tsx` — Full-screen drag overlay/feedback.
- `frontend/components/ClipboardApp/*` — Other subcomponents: FileUploadHandler, ShareCard (QR code sharing), TransferProgress, CachedIdActionButton, FileTransferButton.
- `frontend/components/Editor/` — Rich-text editor module: RichTextEditor, toolbar components (BasicFormatTools/FontTools/AlignmentTools/InsertTools), SelectMenu, types, and editor hooks.
- `frontend/components/blog/` — Blog components: TableOfContents (Chinese heading ID generation + scroll tracking), Mermaid rendering, MDXComponents, ArticleListItem, etc.
- `frontend/components/common/` — Shared components: clipboard_btn (clipboard read/write buttons), AutoPopupDialog, LazyLoadWrapper, YouTubePlayer.
- `frontend/components/web/` — Site components: Header (responsive nav + language), Footer (copyright + language links), FAQSection, HowItWorks, SystemDiagram, KeyFeatures, theme-provider.
- `frontend/components/web/ThemeToggle.tsx` — Theme toggle (single Light/Dark button), used in Header (desktop & mobile).
- `frontend/components/seo/JsonLd.tsx` — SEO structured data component for multiple JSON-LD types.
- `frontend/components/LanguageSwitcher.tsx` — Language switcher.
- `frontend/components/ui/*` — Base UI atoms (Radix UI + shadcn/ui): Button (variants), Accordion, Dialog, Card, Tooltip, Select, Input, Textarea, Checkbox, DropdownMenu, Toast system, AnimatedButton.
- `frontend/hooks/` — Business logic hub (React Hooks).
- `frontend/hooks/useWebRTCConnection.ts` — WebRTC lifecycle and orchestration APIs.
- `frontend/hooks/useRoomManager.ts` — Room create/join/validate and UI state; supports cached-ID reconnect (≥8 chars auto-sends initiator-online).
- `frontend/hooks/useFileTransferHandler.ts` — File/text payload orchestration and callbacks; uses getState() to avoid stale closures; supports JSZip folder downloads.
- `frontend/hooks/useClipboardActions.ts` — Clipboard actions/state; supports modern APIs and document.execCommand fallback; handles HTML/rich-text paste.
- `frontend/hooks/useClipboardAppMessages.ts` — App messaging (shareMessage/retrieveMessage) with a 4-second auto-dismiss mechanism.
- `frontend/hooks/useLocale.ts` — Language selection by parsing pathname.
- `frontend/hooks/usePageSetup.ts` — Page setup & SEO; auto-join from URL roomId; referrer tracking.
- `frontend/hooks/useRichTextToPlainText.ts` — Rich-text → plain-text helper; block-level line breaks and text-node wrapping.
- `frontend/lib/` — Core libraries and utilities.
- WebRTC base & roles
- `frontend/lib/webrtc_base.ts` — WebRTC base class: Socket.IO signaling, RTCPeerConnection management, ICE candidate queues, dual-disconnect reconnection, wake lock management, DataChannel send retries (5 attempts with increasing delays), graceful disconnect tracking (`gracefullyDisconnectedPeers` Set), and multi-format payload compatibility (ArrayBuffer/Blob/Uint8Array/TypedArray). joinRoom uses a 15s timeout and an “equivalent success signal” fallback: Initiator treats `ready/recipient-ready` as joined; Recipient treats `offer` as joined; once triggered it sets inRoom and clears listeners/timers to reduce false timeouts on weak networks.
- `frontend/lib/webrtc_Initiator.ts` — Initiator role: handles `ready`/`recipient-ready`, creates RTCPeerConnection and a proactive DataChannel, sends offers, handles answers, supports a 256KB buffer threshold.
- `frontend/lib/webrtc_Recipient.ts` — Recipient role: handles `offer`, creates RTCPeerConnection and a reactive DataChannel (`ondatachannel`), generates and sends answers, handles `initiator-online` reconnect signals and connection cleanup.
- `frontend/lib/webrtcService.ts` — WebRTC service singleton (persists across routes): manages sender/receiver instances, exposes a unified business API, handles connection-state changes, broadcasting, file requests, and disconnect cleanup.
- Sending (sender)
- `frontend/lib/fileSender.ts` — Backward-compatible sender wrapper; internally uses FileTransferOrchestrator.
- `frontend/lib/transfer/FileTransferOrchestrator.ts` — Sender main orchestrator; manages the file transfer lifecycle.
- `frontend/lib/transfer/StreamingFileReader.ts` — High-performance streaming reader using the 32MB batch + 64KB network chunk dual-layer buffer design.
- `frontend/lib/transfer/NetworkTransmitter.ts` — Network transmitter; uses native WebRTC backpressure control and supports embedded-metadata chunk packets.
- `frontend/lib/transfer/StateManager.ts` — State hub; tracks peer state, pending files, folder metadata.
- `frontend/lib/transfer/ProgressTracker.ts` — Progress tracker; computes file/folder progress and speed stats; triggers callbacks.
- `frontend/lib/transfer/MessageHandler.ts` — Message routing (fileRequest/fileReceiveComplete/folderReceiveComplete).
- `frontend/lib/transfer/TransferConfig.ts` — Transfer config: 4MB file read chunks, 32MB batches, 64KB network chunks.
- Receiving (receiver)
- `frontend/lib/fileReceiver.ts` — Backward-compatible receiver wrapper; internally uses FileReceiveOrchestrator.
- `frontend/lib/receive/FileReceiveOrchestrator.ts` — Receiver main orchestrator; manages reception lifecycle with resume support and streaming disk writes.
- `frontend/lib/receive/ReceptionStateManager.ts` — State hub; manages file metadata, active reception state, folder progress, and save-mode config.
- `frontend/lib/receive/ChunkProcessor.ts` — Chunk processor: payload conversion, embedded-metadata parsing, validation, and index mapping.
- `frontend/lib/receive/StreamingFileWriter.ts` — Streaming writer with SequencedDiskWriter for strict in-order disk writes; supports large streaming files.
- `frontend/lib/receive/FileAssembler.ts` — In-memory assembler for small files; reassembles, checks integrity, and creates a File object.
- `frontend/lib/receive/MessageProcessor.ts` — Message routing (fileMeta/stringMetadata/fileRequest/folderReceiveComplete).
- `frontend/lib/receive/ProgressReporter.ts` — Progress reporter: progress/speed stats and throttled callbacks.
- `frontend/lib/receive/ReceptionConfig.ts` — Reception config: 1GB “large file” threshold, 64KB chunks, buffer sizes, debug toggles.
- Tools & helpers
- `frontend/lib/fileReceiver.ts`, `frontend/lib/fileUtils.ts`, `frontend/lib/speedCalculator.ts`, `frontend/lib/utils.ts` — general utilities.
- `frontend/lib/roomIdCache.ts` — room ID cache management.
- `frontend/lib/wakeLockManager.tsx` — wake lock manager (mobile optimization).
- `frontend/lib/utils/ChunkRangeCalculator.ts` — chunk-range calculations.
- `frontend/lib/browserUtils.ts` — browser compatibility helpers.
- `frontend/lib/tracking.ts` — user behavior tracking.
- `frontend/lib/dictionary.ts`, `frontend/lib/mdx-config.ts`, `frontend/lib/blog.ts` — i18n/content/SEO helpers.
- `frontend/stores/` — Shared app state (Zustand).
- `frontend/stores/fileTransferStore.ts` — Single source of truth for transfer progress/state (Zustand singleton, persists across routes).
- `frontend/types/`, `frontend/constants/` — Types and constants.
- `frontend/types/global.d.ts` — Global types (lodash module, FileSystemDirectoryHandle).
- `frontend/types/messages.ts` — i18n message and UI content types (Meta, Text, Messages, etc.).
- `frontend/types/webrtc.ts` — WebRTC transfer protocol types (metadata, chunk shape, state machine interfaces).
- `frontend/constants/messages/` — i18n message files (7 languages: en, zh, de, es, fr, ja, ko).
- `frontend/constants/i18n-config.ts` — i18n config (default language, supported languages, display-name mapping).
- `frontend/content/` — Content.
- `frontend/content/blog/` — Blog posts (MDX, multilingual), including OSS release, WebRTC file transfer, resume, etc.
- `frontend/lib/blog.ts` — Blog utilities: multilingual post loading, frontmatter parsing, tag extraction, content validation.
- **Config & build**
- `frontend/package.json`, `frontend/tsconfig.json`, `frontend/tailwind.config.ts` — project configuration.
- `frontend/next.config.mjs`, `frontend/postcss.config.mjs`, `frontend/components.json` — Next.js and component config.
- `frontend/.eslintrc.json` — lint configuration.
- `frontend/Dockerfile`, `frontend/health-check.js` — Docker deploy and health checks.
## Backend (Express, Socket.IO, Redis)
- `backend/src/server.ts` — Server entry: Express + Socket.IO init and listen.
- `backend/src/config/env.ts`, `backend/src/config/server.ts` — Environment and server config.
- `backend/src/config/env.ts` — Env var validation (port, CORS, Redis), supports per-env `.env` loading.
- `backend/src/config/server.ts` — CORS config for dev/prod, supports multi-origin and LAN regex matching.
- `backend/src/routes/api.ts` — REST: room create/validate, tracking, debug logs.
- `backend/src/routes/health.ts` — Health checks.
- `backend/src/socket/handlers.ts` — Signaling events: `join`, `initiator-online`, `recipient-ready`, `offer`, `answer`, `ice-candidate`.
- `backend/src/services/redis.ts` — Redis client.
- `backend/src/services/room.ts` — Room/member storage and helpers.
- `backend/src/services/rateLimit.ts` — Redis Sorted Set IP rate limiter.
- `backend/src/types/room.ts`, `backend/src/types/socket.ts` — Types and interfaces.
- `backend/src/types/socket.ts` — Socket.IO types: JoinData, SignalingData (offer/answer/candidate), InitiatorData, RecipientData.
- `backend/src/types/room.ts` — Room types: RoomInfo (createdAt), ReferrerTrack, LogMessage.
- **Backend config & scripts**
- `backend/package.json`, `backend/tsconfig.json` — project configuration.
- `backend/Dockerfile`, `backend/.dockerignore` — Docker configuration.
- `backend/health-check.js` — health-check script.
- `backend/scripts/export-tracking-data.js` — data export script.
## Deployment & Ops
- **Root-level config**
- `deploy.sh` — Docker one-click entry (env checks, config generation, cert automation, start/clean).
- `docker-compose.yml` — Docker Compose services (frontend/backend/redis/nginx/turn).
- `.env` — Docker deployment env vars (generated/maintained by scripts).
- **Docker infrastructure**
- `docker/nginx/` — Nginx reverse proxy config.
- `docker/scripts/` — deployment scripts (env checks, config generation, deployment tests).
- `docker/ssl/` — SSL certificate directory.
- `docker/coturn/` — TURN server config.
- `docker/letsencrypt-www/` — Lets Encrypt config.
- **Build & docs**
- `build/` — ignore this temporary directory.
- `test-health-apis.sh` — health API test script.
- `README.md`, `README.zh-CN.md`, `ROADMAP.md`, `ROADMAP.zh-CN.md` — project docs.
+19 -5
View File
@@ -24,6 +24,21 @@
- `frontend/components/ClipboardApp.tsx` — 顶层 UI 协调器,集成 5 个业务 hooksuseWebRTCConnection/useFileTransferHandler/useRoomManager/usePageSetup/useClipboardAppMessages),处理全局拖拽事件和双标签页(发送/接收)管理。
- 体验增强:切到接收端(retrieve)且满足“未在房间、URL 无 roomId、输入为空、存在缓存ID”时自动填充并加入房间(读取 `frontend/lib/roomIdCache.ts`)。
- 连接反馈:集成 `useConnectionFeedback``frontend/hooks/useConnectionFeedback.ts`),桥接 WebRTC 连接态到 UI 文案,含协商中提示、8s 慢连接提示、断开/重连/恢复提示(前台可见时提示)。慢提示统一复用 `frontend/utils/useOneShotSlowHint.ts`
- `frontend/hooks/` — 业务中枢 Hooks。
- `useRoomManager.ts`
- 入房流程:`join_inProgress`(立即)、`join_slow`3s,复用 `useOneShotSlowHint`)、`join_timeout`(15s);join 成功/失败均清理定时器。
- 等效成功信号:在 `joinResponse` 之前若收到 `ready/recipient-ready/offer`,提前判定入房成功并清理 3s/15s 定时器。
- 其他:房间状态文案、分享链接生成、离开房间、输入校验(750ms 防抖)。
- `useConnectionFeedback.ts`
- 状态归一化:`new/connecting``negotiating``failed/closed``disconnected`(复用 `utils/rtcPhase.ts`)。
- 协商慢提示:8s 定时器(`rtc_slow`),单次协商仅提示一次;若在后台到时则挂起,回到前台且仍协商时补发一次(复用 `useOneShotSlowHint`)。
- 一次性提示:首次 `connected``rtc_connected`)仅提示一次;断开前台重连(`rtc_reconnecting`)与恢复(`rtc_restored`)。
- i18n 文案与类型
- 文案定义:`frontend/constants/messages/*.{ts}`(已补齐 zh/en/ja/es/de/fr/ko)。
- 类型定义:`frontend/types/messages.ts``ClipboardApp` 下包含 `join_*``rtc_*` 文案键)。
- `frontend/components/ClipboardApp/SendTabPanel.tsx` — 发送面板,集成富文本编辑器、文件上传、房间 ID 生成(支持 4 位数字/UUID 两种模式)、分享链接生成。
- 体验增强:点击“使用缓存ID”将立即触发加入房间(sender 侧),减少一次手动点击。
- `frontend/components/ClipboardApp/RetrieveTabPanel.tsx` — 接收面板,处理房间加入、文件接收、目录选择(File System Access API)、富文本内容显示。
@@ -53,7 +68,7 @@
- `frontend/lib/` — 核心库与工具。
- WebRTC 基础与角色
- `frontend/lib/webrtc_base.ts` — WebRTC 基础类,提供 Socket.IO 信令、RTCPeerConnection 管理、ICE 候选者队列、双重断开检测重连机制、唤醒锁管理、数据通道发送重试(5 次递增间隔)、优雅断开跟踪(gracefullyDisconnectedPeers Set)和多格式数据类型兼容性支持(ArrayBuffer/Blob/Uint8Array/TypedArray)。
- `frontend/lib/webrtc_base.ts` — WebRTC 基础类,提供 Socket.IO 信令、RTCPeerConnection 管理、ICE 候选者队列、双重断开检测重连机制、唤醒锁管理、数据通道发送重试(5 次递增间隔)、优雅断开跟踪(gracefullyDisconnectedPeers Set)和多格式数据类型兼容性支持(ArrayBuffer/Blob/Uint8Array/TypedArray)。加入房间(joinRoom)采用 15 秒超时,并在 join 未返回时启用“等效成功信号”提前判定成功:Initiator 收到 `ready/recipient-ready`Recipient 收到 `offer`;触发后立即设置 inRoom 并清理监听/定时器,降低弱网下误报。
- `frontend/lib/webrtc_Initiator.ts` — 发起方实现,处理`ready`/`recipient-ready`事件,创建 RTCPeerConnection 和主动式 DataChannel,发送 offer,处理 answer 响应,支持 256KB 缓冲阈值配置。
- `frontend/lib/webrtc_Recipient.ts` — 接收方实现,处理`offer`事件,创建 RTCPeerConnection 和响应式 DataChannelondatachannel),生成并发送 answer,处理`initiator-online`重连信号和现有连接清理。
- `frontend/lib/webrtcService.ts` — WebRTC 服务单例封装(跨路由常驻),管理 sender/receiver 实例,提供统一业务接口,处理连接状态变更、数据广播、文件请求和连接断开清理。
@@ -130,15 +145,14 @@
- `backend/Dockerfile``backend/.dockerignore` — Docker 配置。
- `backend/health-check.js` — 健康检查脚本。
- `backend/scripts/export-tracking-data.js` — 数据导出脚本。
- `backend/docker/` — Docker 相关配置与脚本(包含 Nginx、TURN 服务器配置)。
## 部署与运维
- **根目录配置**
- `docker-compose.yml``ecosystem.config.js` — Docker Compose 与 PM2 配置
- `build-and-deploy.sh``deploy.sh` — 构建与部署脚本
- `deploy.config_prod``deploy.config_test` — 生产与测试环境配置
- `deploy.sh` — Docker 一键部署入口(环境检测、配置生成、证书自动化、启动/清理)
- `docker-compose.yml` — Docker Compose 编排(frontend/backend/redis/nginx/turn
- `.env` — Docker 部署环境变量(由脚本生成/维护)
- **Docker 基础设施**
+150
View File
@@ -0,0 +1,150 @@
# PrivyDrop AI Playbook — Collaboration Rules
These rules govern how “human developers + AI assistants” collaborate to evolve the codebase efficiently and safely, without breaking the privacy stance or technical baseline. This document complements the index (`index.md`) and the flow map (`flows.md`): it constrains “how we work” without re-stating “what the system does”.
- Scope: the entire repository (code + docs)
- Audience: human developers, AI assistants, reviewers
- Change principles: best practices first, one goal per change, reversible, verifiable
## 1. Collaboration Principles
- Best practices first: prefer proven approaches consistent with the existing stack; avoid reinventing the wheel.
- One change, one purpose: each change should focus on one goal; avoid “while Im here” fixes.
- Privacy stance: never introduce (or suggest) server-relayed file transfers; the backend is for signaling and room coordination only.
- Small steps: keep PRs small and easy to roll back; prefer the minimum viable change.
- Traceability: commit messages, PR descriptions, and code comments should be clear and reproducible.
## 2. Plan First (Hard Requirement)
Before implementing anything, you must propose a “change plan” and get approval. The plan must include: goals, scope + file list, approach, risks + mitigations, acceptance criteria, rollback, docs to update, and validation.
Recommended reading before implementation (and reference in the plan):
- `docs/ai-playbook/index.md`
- `docs/ai-playbook/code-map.md`
- `docs/ai-playbook/flows.md`
## 3. Language & Comments
- Communication: always use Simplified Chinese when communicating with the project owner/maintainers.
- Code comments, exported symbol naming, commit messages, and PR titles/descriptions must be in English.
- User/marketing docs may be bilingual; this collaboration guide is maintained in both languages.
- Use TSDoc/JSDoc (English) for exported functions, complex flows, and shared types to keep APIs readable.
## 4. Next.js (Frontend) Conventions
- App Router defaults to Server Components; use `"use client"` only when interaction truly requires it.
- Reuse existing UI (Tailwind + shadcn/ui + Radix); do not introduce new UI libraries without approval.
- i18n: all visible copy must go through dictionaries and the `frontend/app/[lang]` routes; do not hardcode strings in components.
- Naming & files:
- Components: PascalCase file names and exports (ExampleCard.tsx)
- Hooks: camelCase file names; exports start with use* (useSomething.ts)
- Centralize types/constants; avoid circular deps
- SEO: use Next Metadata and `frontend/components/seo/JsonLd.tsx`; pages must include canonical and multilingual links.
- Performance & a11y: dynamically import heavy components when needed; ensure basic aria/focus behavior.
## 5. TypeScript & Style
- Keep types strict; avoid any. Use unknown when needed and narrow explicitly; exported functions should have explicit return types.
- Follow existing ESLint/Prettier and path aliases (`@/...`); do not introduce new formatters.
- Keep functions small and clear; move complex logic into service/util layers. Components should consume state, not mutate global state.
- Avoid 1-letter variable names; avoid magic numbers—centralize them as constants.
## 6. WebRTC / Transfer Guardrails (Do Not Break)
- Keep established strategy: 32MB batches + 64KB network chunks; do not casually change DataChannel bufferedAmountLowThreshold or maxBuffer strategy.
- Resume, strict sequential disk writes, and multi-format compatibility are baseline capabilities—do not downgrade/remove them.
- Signaling and message names (offer/answer/ice-candidate, etc.) must stay compatible; any breaking change must follow the “must ask” process.
- Reconnect and queue handling (ICE candidate caching, backpressure, send retries) must remain consistent; changes require risk assessment and thorough validation.
- Never send file contents (in any form) to the backend or third-party services.
## 7. Backend Constraints (Signaling Service)
- Signaling + room management only. Never persist user file data; logs must not include sensitive content or raw payloads.
- Keep rate limiting and abuse protection; if extending APIs, ensure backward compatibility or provide a migration path.
## 8. Dependencies & Security
- New dependencies require justification in the plan: size (ESM/SSR compatibility), maintenance health, license, alternatives, security impact.
- Do not add telemetry/tracking; do not log sensitive data; follow least privilege.
- Inject config via env vars; never hardcode secrets or service endpoints in the repo.
## 9. Documentation Sync
- If code changes affect flows, interfaces, or key entry points, update in the same PR:
- `docs/ai-playbook/flows.zh-CN.md`
- `docs/ai-playbook/code-map.zh-CN.md`
- PRs must list “docs impacted” to avoid the playbook going stale; keep the index page lean and only update it when adding new links.
## 10. Validation & Regression
- Frontend: must build (`next build`); include manual verification for key paths (at least: create/join room, single/multi file, folder, large files, resume, cross-browser transfers, i18n routes + SEO metadata).
- Backend: Socket.IO core flow works.
- Regression checklist: reconnect flow, download counts + state cleanup, store single-source-of-truth constraint, browser compatibility (Chromium/Firefox).
## 11. Must Ask First (Approval Required)
- Breaking changes to protocols/message names/public APIs/storage formats.
- Architecture changes impacting privacy stance or crossing boundaries (any relay or persistence).
- New dependencies, new infrastructure, or large refactors.
- Changes to transfer guardrail parameters (chunking, backpressure, retries, etc.).
## 12. Common Pitfalls
- Mutating global state from inside components (breaks one-way dataflow).
- Changing code without updating docs, leaving the playbook stale.
- Using any to bypass type and boundary checks.
- Hardcoding UI copy in components instead of using dictionaries/i18n.
- Tweaking critical WebRTC parameters without validation, causing silent regressions.
## 13. Templates
Change Plan Template
```
Title: <concise title>
Goals
- <what you intend to achieve>
Scope / Files
- <files to change/add + why>
Approach
- <implementation approach and key design points>
Risks & Mitigations
- <major risk> -> <mitigation>
Acceptance Criteria
- <verifiable acceptance items>
Rollback
- <how to roll back quickly>
Docs to Update
- code-map.zh-CN.md / flows.zh-CN.md / README(.zh-CN).md / others?
Validation
- Build: next build / backend health
- Manual: <key scenarios>
```
PR Checklist
```
- [ ] Single-topic change only
- [ ] Code comments and commit messages are in English
- [ ] No unapproved dependencies/UI libraries added
- [ ] i18n and SEO follow conventions (if applicable)
- [ ] Transfer guardrails are unchanged (or approved + validated)
- [ ] flows / code-map docs are updated in sync
- [ ] Validation notes and regression checklist included
```
## 14. References & Quick Entry Points
- Index & context: `docs/ai-playbook/index.md`
- Code map: `docs/ai-playbook/code-map.md`
- Key flows: `docs/ai-playbook/flows.md`
+233
View File
@@ -0,0 +1,233 @@
# PrivyDrop AI Playbook — Flows (with Micro-Plan Template)
This page summarizes the core P2P transfer flows and signaling/reconnection sequences, plus practical debugging notes and a compact “micro-plan template”. Use it to align on phases, events, and entry points before making changes.
## Quick Navigation
- Fast path: this page contains 15 (key flows/messages/debug notes) and 11 (micro-plan template).
- Deep dives (split out from this page):
- Frontend component collaboration (was Section 6): [`docs/ai-playbook/flows/frontend.md`](./flows/frontend.md)
- Backpressure & chunking (was Section 7): [`docs/ai-playbook/flows/backpressure-chunking.md`](./flows/backpressure-chunking.md)
- Resume / partial transfer (was Section 9): [`docs/ai-playbook/flows/resume.md`](./flows/resume.md)
- Reconnect consistency (was Section 10): [`docs/ai-playbook/flows/reconnect-consistency.md`](./flows/reconnect-consistency.md)
## 1) File Transfer (Single File)
Sequence (via DataChannel, sender ↔ receiver):
1. Sender → `fileMetadata` (id, name, size, type, fullName, folderName).
2. Receiver → `fileRequest` (ack metadata; supports offset-based resume).
3. Sender → chunk stream (high-performance, dual-layer buffering):
- StreamingFileReader reads in 32MB batches and sends 64KB network chunks
- NetworkTransmitter uses native WebRTC backpressure (bufferedAmountLowThreshold)
- Each send embeds metadata (chunkIndex, totalChunks, fileOffset, fileId)
4. Receiver → integrity checks & assembly (strict sequential disk writes or in-memory assembly; supports resume).
5. Receiver → `fileReceiveComplete` (success receipt, includes receivedSize).
6. Sender → MessageHandler fires 100% progress callback and clears sending state.
Sender-side detailed flow:
1. FileTransferOrchestrator.sendFileMeta() → StateManager records folder/file sizes
2. Receive `fileRequest` → FileTransferOrchestrator.handleFileRequest()
3. Initialize StreamingFileReader (supports startOffset resume)
4. processSendQueue() loop:
- getNextNetworkChunk() returns the next 64KB chunk (efficient slicing within a batch)
- NetworkTransmitter.sendEmbeddedChunk() sends with backpressure control
- ProgressTracker.updateFileProgress() updates progress and speed
5. Wait for `fileReceiveComplete`, then clear isSending state
Entry points:
- Sender: `frontend/lib/fileSender.ts` (compat wrapper) → `frontend/lib/transfer/FileTransferOrchestrator.ts` (main orchestrator)
- Key components: StreamingFileReader (fast reads), NetworkTransmitter (backpressure sending), StateManager (state), ProgressTracker (progress)
Receiver-side detailed flow:
1. MessageProcessor.handleFileMetadata() → ReceptionStateManager stores file metadata
2. FileReceiveOrchestrator.requestFile() → check resume (getPartialFileSize)
3. Initialize reception: compute expected chunk count; choose storage mode (memory vs disk) based on size
4. Send `fileRequest` (with offset if needed) → wait for sender to start
5. handleBinaryChunkData() loop:
- ChunkProcessor.convertToArrayBuffer() handles multiple payload types (Blob/Uint8Array/ArrayBuffer)
- ChunkProcessor.parseEmbeddedChunkPacket() parses the “embedded metadata” packet format
- ChunkProcessor.validateChunk() validates fileId, chunkIndex, chunkSize
- Store chunks in an array (or write sequentially via SequencedDiskWriter)
- ProgressReporter.updateFileProgress() throttles progress updates (100ms)
6. Auto completion detection: checkAndAutoFinalize() validates completeness
7. Finalize based on storage mode:
- Large/disk: StreamingFileWriter.finalizeWrite()
- Small/memory: FileAssembler.assembleFileFromChunks()
8. Send `fileReceiveComplete` with receivedSize and receivedChunks
Entry points:
- Sender: `frontend/lib/fileSender.ts` (compat wrapper) → `frontend/lib/transfer/FileTransferOrchestrator.ts` (main orchestrator)
- Receiver: `frontend/lib/fileReceiver.ts` (compat wrapper) → `frontend/lib/receive/FileReceiveOrchestrator.ts` (main orchestrator)
- Key components: StreamingFileReader (fast reads), NetworkTransmitter (backpressure), ChunkProcessor (format handling), StreamingFileWriter (disk writes), FileAssembler (memory assembly)
Notes:
- **Sender**: dual-layer buffering (32MB batches + 64KB network chunks), native WebRTC backpressure, resume support
- **Receiver**: strict sequential disk writer (SequencedDiskWriter), multi-format conversion, smart storage selection (≥1GB auto disk mode)
- **Compatibility**: ChunkProcessor supports Blob/Uint8Array/ArrayBuffer to address Firefox quirks
- **Progress throttling**: ProgressReporter updates at different rates (file 100ms, folder 200ms) to avoid UI overload
- **Resume**: getPartialFileSize() checks local partial files; fileRequest.offset drives resume
- **Debug support**: ReceptionConfig provides verbose chunk/progress logs for investigation
## 2) File Transfer (Folder)
Sequence (run the single-file flow for each file):
1. Sender → send `fileMetadata` for all files in the folder.
2. Receiver → `folderRequest` (confirm starting the batch transfer).
3. For each file: run the single-file flow, but do not mark global 100% on individual file completion.
4. Receiver → after all files finish, send `folderReceiveComplete`.
5. Sender → mark folder-level progress as 100% (fire final callback).
## 3) Signaling & Reconnect (Socket.IO)
High-level sequence:
1. Client → REST: create or fetch a `roomId` (`backend/src/routes/api.ts`).
2. Client → Socket.IO: `join` the room (backend validates and binds socket ↔ room).
3. Online state & reconnection coordination within the room:
- Initiator → `initiator-online` (online/ready; tells the peer it can rebuild the connection).
- Recipient → `recipient-ready` (ready; initiator may start offer).
4. WebRTC negotiation relay:
- `offer` → backend → peer.
- `answer` → backend → peer.
- `ice-candidate` → backend → peer.
### Join Success Conditions & Timeout Strategy (Frontend Fault-Tolerance)
- Primary success condition: receive `joinResponse(success=true)` (backend completed validation and socket↔room binding).
- Equivalent “success signals” (fault-tolerance; any one means “were effectively in” and should clear listeners/timers):
- Initiator: receives `ready` or `recipient-ready`
- Recipient: receives `offer`
- Timeout: 15 seconds. This covers weak networks, mobile, and Socket.IO polling fallback where joinResponse can arrive late.
- Why its safe: `ready/recipient-ready` are room broadcast events; `offer` is the P2P handshake starting point. If you can receive these, youre in the room and negotiation has begun—treat it as success to avoid false “Join room timeout” errors.
Reconnect mechanics (mobile network switching support):
- **Dual disconnect detection**: Socket.IO `disconnect` → set `isSocketDisconnected = true`; P2P disconnect → set `isPeerDisconnected = true` and call `cleanupExistingConnection()`
- **Reconnect trigger**: only when both socket and P2P are disconnected → `attemptReconnection()`, guarded by `reconnectionInProgress` to avoid concurrent reconnects
- **State restoration**: reconnect calls `joinRoom(roomId, isInitiator, sendInitiatorOnline)`; the initiator auto-sends `initiator-online` and the recipient replies `recipient-ready`
- **ICE candidate queue**: cache candidates in `iceCandidatesQueue` until ready, then flush; support re-queuing invalid candidates with connection-state validation
- **Wake lock**: request via WakeLockManager when connected; release on disconnect to stabilize mobile transfers
- **Graceful disconnect tracking**: `gracefullyDisconnectedPeers` tracks intentionally closed peers; send retries skip them
- **DataChannel send retries**: up to 5 attempts with backoff from 100ms to 1000ms; skip peers marked as gracefully disconnected
**Backend signaling & room management**:
Socket.IO event handling flow:
1. **join**: IP rate limit → validate room existence → bind socket-room → success response → broadcast `ready`
2. **Reconnect state sync**: initiator reconnect sends `initiator-online`; recipient replies `recipient-ready`
3. **Relay signaling**: offer/answer/ice-candidate are forwarded with `socket.to(peerId).emit()`, including a `from` field
4. **Disconnect cleanup**: broadcast `peer-disconnected` → unbind socket-room → delete empty rooms after 15 minutes
**Room management**:
- Redis structures:
- `room:<roomId>` (Hash): room creation time
- `room:<roomId>:sockets` (Set): sockets in the room
- `socket:<socketId>` (String): the roomId for a socket
- ID generation: prefer 4-digit numeric IDs; fall back to 4-character alphanumeric on collision
- Idempotency: long IDs (≥8 chars) can be reused across reconnects
- TTL: 24 hours, refreshed on activity
**Rate limiting**:
- Redis Sorted Set based IP rate limit
- Up to 2 requests per 5-second window
- Uses pipeline to keep operations atomic
Entry points:
- Frontend: `frontend/hooks/useWebRTCConnection.ts`, `frontend/lib/webrtc_base.ts`, `frontend/lib/webrtc_Initiator.ts`, `frontend/lib/webrtc_Recipient.ts`
- Backend: `backend/src/socket/handlers.ts` (all signaling events), `backend/src/services/room.ts`, `backend/src/routes/api.ts`
## 4) DataChannel Messages & Constraints (Overview)
- Messages (example names): `fileMetadata`, `fileRequest`, `chunk`, `fileReceiveComplete`, `folderRequest`, `folderReceiveComplete`, plus potential flow-control/keepalive messages.
- Core fields: file/folder id, indices/ranges, sizes, names, optional checksums.
- Key constraints:
- Chunk size: choose a safe range per browser/network; mind channel buffer thresholds.
- Backpressure: check `RTCDataChannel.bufferedAmount` and throttle as needed.
- Completion: mark 100% only after `fileReceiveComplete` / `folderReceiveComplete`.
- Resume: `fileRequest` can carry offset/range to support partial transfers.
## 5) Debugging Notes (Distilled from Experience)
- Download races / double counting: treat `frontend/stores/fileTransferStore.ts` as the single source of truth; provide cleanup APIs at the store level (e.g. `clearSendProgress`, `clearReceiveProgress`) rather than deleting objects locally in components.
- Recipient reconnect & room state: reset state correctly; UI must strictly derive from the store; leaving/rejoining should clear related state; follow `initiator-online`/`recipient-ready` ordering before starting an offer; verify room membership after reconnect.
- Reconnect with cached roomId: if `roomId` is cached, ensure renegotiation is triggered via online state sync (`initiator-online`/`recipient-ready`); backend must correctly clean and restore socket↔room mappings on disconnect/reconnect.
- Multiple transfers: dont over-dedupe and mask real “second downloads”; rely on correct state cleanup.
- Dataflow principle: one-way dataflow (Store → Hooks → Components); hooks adapt, components consume but do not mutate shared state.
- **Practical debugging tactics**:
- Add structured logs for connection state changes and store updates; for timing/race issues, `setTimeout(..., 0)` can help reorder updates
- DataChannel send retries: `sendToPeer()` supports 5 retries with backoff from 100ms→1000ms; skip gracefully disconnected peers
- WebRTC payload compatibility: handle `ArrayBuffer`/`Blob`/`Uint8Array`/`TypedArray` to cover Firefox quirks
- Connection state monitoring: react to `connectionState` changes (connected/disconnected/failed/closed)
- Backpressure control: DataChannel uses `bufferedAmountLowThreshold = 256KB`; check `bufferedAmount` before sending
- Join false positives: if “Join room timeout” appears but you immediately see `offer/answer/connected` logs, its often a late joinResponse rather than a real failure; the 15s window plus “equivalent success signals” corrects it automatically
## 6) Frontend Component System & Core-Orchestrator Collaboration
This section is split into: [`docs/ai-playbook/flows/frontend.md`](./flows/frontend.md)
- Use it for: understanding boundaries and collaboration among UI components, hooks, and the store
- Includes: ClipboardApp orchestrator, hook layering, connection feedback state machine, dataflow patterns, etc.
## 7) Backpressure & Chunking Strategy (Deep Dive)
This section is split into: [`docs/ai-playbook/flows/backpressure-chunking.md`](./flows/backpressure-chunking.md)
- Use it for: verifying thresholds, chunk/batch strategy, embedded packet format, and performance tuning points
- Includes: sender dual-buffering, receiver storage strategy, debugging/monitoring tips, etc.
## 9) Resume / Partial Transfer (Deep Dive)
This section is split into: [`docs/ai-playbook/flows/resume.md`](./flows/resume.md)
- Use it for: verifying resume detection, offset negotiation, and chunk-range calculations
- Includes: ChunkRangeCalculator, sender/receiver resume flows, limitations, and debugging notes
## 10) Reconnect & State Consistency (Deep Dive)
This section is split into: [`docs/ai-playbook/flows/reconnect-consistency.md`](./flows/reconnect-consistency.md)
- Use it for: verifying dual disconnect rules, ICE candidate queues, send retries, and consistency safeguards
- Includes: reconnect triggers, retry strategy, mobile-specific additions, debugging notes
## 11) Micro-Plan Template (for Aligning on Small Changes)
Title: <short summary>
Background / Problem
- What user scenario or defect are we fixing?
Goals & Non-Goals
- Whats in scope, and whats explicitly out of scope?
Impacted Files & Messages
- Code: list key files (e.g. `frontend/lib/webrtc_base.ts`, `backend/src/socket/handlers.ts`).
- Protocol: list DataChannel messages/fields to be changed.
State Machine / Flow Changes
- Add/remove/modify phases; include a short sequence diagram or steps.
Tests & Regression Checklist
- Unit/integration (if applicable), manual scenarios, performance/boundaries, reconnect cases.
Docs to Update
- `code-map.md` (if new entry points appear)
- `flows.md` (if flows/messages/constraints change)
- Other architecture or deployment docs (if involved)
+89 -365
View File
@@ -2,6 +2,15 @@
本文汇总 P2P 传输与信令重连的关键流程与消息序列,并给出简明的调试要点与“微方案模板”。用于在改动前快速对齐阶段、事件与入口文件。
## 快速导航
- 速查:本页包含 1–5(关键流程/消息/调试要点)与 11(微方案模板)。
- 深度阅读(已从本页拆分):
- 前端组件协作(原第 6 节):[`docs/ai-playbook/flows/frontend.zh-CN.md`](./flows/frontend.zh-CN.md)
- 背压与分片(原第 7 节):[`docs/ai-playbook/flows/backpressure-chunking.zh-CN.md`](./flows/backpressure-chunking.zh-CN.md)
- 断点续传(原第 9 节):[`docs/ai-playbook/flows/resume.zh-CN.md`](./flows/resume.zh-CN.md)
- 重连一致性(原第 10 节):[`docs/ai-playbook/flows/reconnect-consistency.zh-CN.md`](./flows/reconnect-consistency.zh-CN.md)
## 1)文件传输(单文件)
序列(通过 DataChannel,发送端 ↔ 接收端):
@@ -89,6 +98,15 @@
- `answer` → 后端 → 转发给对端。
- `ice-candidate` → 后端 → 转发给对端。
### Join 成功条件与超时策略(前端容错增强)
- 首选成功条件:收到 `joinResponse(success=true)`(后端完成房间校验与 socket↔room 绑定)。
- 等效成功信号(容错,任一满足即判定已入房并清理监听/定时器):
- 发起方(Initiator):收到 `ready``recipient-ready`
- 接收方(Recipient):收到 `offer`
- 超时时间:15 秒。用于兼容弱网、移动端与 Socket.IO 轮询降级造成的 joinResponse 迟到。
- 说明:`ready/recipient-ready` 为房间广播事件,`offer` 为点对点握手起点;能收到这些事件即表明“已在房间且握手已开始”,可视为等效成功,避免“Join room timeout”的误报。
重连机制细节(移动端网络切换支持):
- **双重断开检测**Socket.IO 断开触发 `disconnect` 事件 → 标记 `isSocketDisconnected = true`P2P 连接断开触发 `disconnected` 状态 → 标记 `isPeerDisconnected = true`,自动调用 `cleanupExistingConnection()` 清理资源
@@ -152,10 +170,11 @@ Socket.IO 事件处理流程:
- WebRTC 数据类型兼容性:支持 `ArrayBuffer`/`Blob`/`Uint8Array`/`TypedArray` 多种格式,解决 Firefox 兼容性问题
- 连接状态监控:`connectionState` 变化时触发相应的处理逻辑(connected/disconnected/failed/closed
- 背压控制:DataChannel 设置 `bufferedAmountLowThreshold = 256KB`,发送时检查 `bufferedAmount` 状态
- Join 误报识别:若出现“Join room timeout”但紧接着能看到 `offer/answer/connected` 等日志,通常是 joinResponse 迟到所致,并非真实失败;15 秒超时窗口与“等效成功信号”会自动纠偏。
## 6)前端组件系统与业务中枢协作流程
### 组件架构层级
本节已拆分到:[`docs/ai-playbook/flows/frontend.zh-CN.md`](./flows/frontend.zh-CN.md)
```
App Router (page.tsx/layout.tsx)
@@ -248,8 +267,65 @@ Core Services (webrtcService) + Store (fileTransferStore)
7. **剪贴板兼容性**useClipboardActions 支持现代 navigator.clipboard API 和 document.execCommand 降级方案
8. **富文本安全处理**useRichTextToPlainText 服务端渲染安全,客户端 DOM 转换处理块级元素
9. **站内导航不中断(同一标签页)**:依赖 `frontend/stores/fileTransferStore.ts`Zustand 单例)与 `frontend/lib/webrtcService.ts`(服务单例)。App Router 页面切换不打断传输且保留已选择/已接收内容。注意不要在路由切换副作用中调用 `webrtcService.leaveRoom()` 或重置 Store;刷新/新标签不在保证范围内。
10. **切到接收端自动加入(缓存ID**:当用户切换到接收端、未在房间、URL 无 `roomId`、输入框为空且本地存在缓存 ID 时,自动填充并直接调用加入房间以提升体验。入口:`frontend/components/ClipboardApp.tsx`(监听 `activeTab` 变化,读取 `frontend/lib/roomIdCache.ts`)。
11. **发送端“使用缓存ID”即刻加入**:发送端在 `SendTabPanel` 点击“使用缓存ID”后会立即调用加入房间(而非仅填充输入框)。入口:`frontend/components/ClipboardApp/CachedIdActionButton.tsx``onUseCached` 回调)+ `frontend/components/ClipboardApp/SendTabPanel.tsx`
## 7)SSR 与 DOM 访问防护(必读)
为避免“服务端异常(Application error)”这类 SSR 侧报错,前端改动需遵循以下守卫清单:
- 仅在客户端生命周期中访问 DOM/Navigator
-`document/window/navigator` 的访问放入 `useEffect`、事件回调或显式的客户端组件(文件顶部包含 `"use client";`)。
- 在回调内使用前加守卫:`typeof document !== 'undefined'``typeof window !== 'undefined'``typeof navigator !== 'undefined'`
- 定时器与可见性判断
- 使用全局 `setTimeout`/`clearTimeout`,避免直接引用 `window.setTimeout`
- 监听可见性:注册/移除 `visibilitychange` 事件前先判断 `typeof document !== 'undefined'`
- 事件监听的清理
- 所有 `addEventListener` 都应在 `useEffect` 返回函数中对称 `removeEventListener`,并在服务端(无 `document`)时跳过注册。
- 单例与模块副作用(重要)
- 禁止在模块顶层初始化依赖浏览器环境的实例(如 Socket、WebRTC、WakeLock 等)。应在客户端首次需要时惰性创建(lazy-init)。
参考实现:
- `frontend/utils/useOneShotSlowHint.ts`:在 `useEffect` 中对 `document` 做判空;定时器使用全局 `setTimeout`
- `frontend/hooks/useConnectionFeedback.ts`:读取 `document.visibilityState` 前判空;仅在客户端注册 `visibilitychange`
- `frontend/hooks/usePageSetup.ts``frontend/lib/tracking.ts`:读取 `window.location` 前判空。
### UI 连接反馈状态机(弱网/VPN 提示)
- 入房阶段(join
- 立即:`join_inProgress`(“正在加入房间…”)。
- 3s 未完成:`join_slow`(“连接较慢,建议检查网络/VPN…”)。
- 15s 超时:`join_timeout`(“加入超时…”)。
- 等效成功信号:在等待 `joinResponse` 期间,若收到 `ready/recipient-ready/offer`,视为提前入房成功并即时清理 3s/15s 定时器与提示,避免“成功后再出现慢/超时提示”。
- 协商阶段(WebRTC
- 进入 `new/connecting`:归一为 “协商中” → `rtc_negotiating`
- 8s 未连上:`rtc_slow`(“网络可能受限,尝试关闭 VPN 或稍后再试”)。仅在页面前台可见时触发;同一次协商尝试仅提示一次(发送端/接收端任一进入协商即启动计时,提示归属以最先进入协商的一侧为准)。
- 连接与重连
- 首次 `connected``rtc_connected`(仅一次)。
- 前台断开:`rtc_reconnecting` → 恢复后 `rtc_restored`
- 后台断开不提示;回到前台若仍断开立即提示 `rtc_reconnecting`
- 已断开期间若页面在后台,返回前台时若仍处于协商态且此前触发了慢协商计时,则会补发一次 `rtc_slow` 并标记本次协商已提示,以避免重复。
实现位置:
- `frontend/hooks/useRoomManager.ts`:入房阶段提示与定时器管理(3s 慢网、15s 超时),并在 join 成功/失败时清理定时器;支持“等效成功信号”提前判定成功(`ready/recipient-ready/offer`)。
- `frontend/hooks/useConnectionFeedback.ts`:桥接 WebRTC 连接态到 UI 提示。
- 状态归一化(mapPhase):`new/connecting``negotiating``failed/closed``disconnected`
- 协商慢提示:8s 定时器、前后台可见性节制、单次协商尝试仅提示一次(含挂起 → 前台补发)。
- 一次性提示:首次 `connected` 只显示一次;断开 → 恢复显示 `rtc_restored`;仅前台显示 `rtc_reconnecting`
- 复用:慢提示定时与前后台补发由 `frontend/utils/useOneShotSlowHint.ts` 统一实现;状态归一化由 `frontend/utils/rtcPhase.ts` 提供。
文案与 i18n
- 文案键均位于 `frontend/constants/messages/*.{ts}`,类型定义见 `frontend/types/messages.ts`
- 关键键:`join_inProgress``join_slow``join_timeout``rtc_negotiating``rtc_slow``rtc_connected``rtc_reconnecting``rtc_restored`(已在 en/ja/es/de/fr/ko 全部补齐)。
节流与展示:
- 所有提示默认 4–6 秒自动消失;通过 `useClipboardAppMessages.putMessageInMs(message, isShareEnd, ms)` 统一展示。
- 连接反馈提示在“状态迁移 + ever/wasDisc 标记 + 可见性判断”三重约束下触发,避免提示风暴。
10. **切到接收端自动加入(缓存 ID**:当用户切换到接收端、未在房间、URL 无 `roomId`、输入框为空且本地存在缓存 ID 时,自动填充并直接调用加入房间以提升体验。入口:`frontend/components/ClipboardApp.tsx`(监听 `activeTab` 变化,读取 `frontend/lib/roomIdCache.ts`)。
11. **发送端“使用缓存 ID”即刻加入**:发送端在 `SendTabPanel` 点击“使用缓存 ID”后会立即调用加入房间(而非仅填充输入框)。入口:`frontend/components/ClipboardApp/CachedIdActionButton.tsx``onUseCached` 回调)+ `frontend/components/ClipboardApp/SendTabPanel.tsx`
12. **深色主题切换**:提供单按钮 Light/Dark 切换,入口:`frontend/components/web/ThemeToggle.tsx`;集成位置:`frontend/components/web/Header.tsx`(桌面与移动);局部样式从硬编码颜色迁移为设计令牌(例如接收面板使用 `bg-card text-card-foreground`)。
### 前端组件架构特化
@@ -289,378 +365,26 @@ Core Services (webrtcService) + Store (fileTransferStore)
- **错误处理标准化**:统一的消息提示机制(putMessageInMs
- **国际化集成**useLocale + getDictionary 提供多语言支持
## 7)背压与分片策略深度分析
## 8)背压与分片策略深度分析
### 发送侧双层缓冲架构
本节已拆分到:[`docs/ai-playbook/flows/backpressure-chunking.zh-CN.md`](./flows/backpressure-chunking.zh-CN.md)
**设计原理**
- **文件读取层**4MB 分片减少 FileReader 调用,8 个分片组成 32MB 批次
- **网络传输层**64KB 小块适配 WebRTC DataChannel 限制,避免 sendData failed 错误
- **性能优化**:批次内高效切片,一次 FileReader.read()产生 512 个网络块
**配置参数**
```typescript
TransferConfig.FILE_CONFIG = {
CHUNK_SIZE: 4194304, // 4MB - 文件读取分片
BATCH_SIZE: 8, // 8个分片 = 32MB批次
NETWORK_CHUNK_SIZE: 65536, // 64KB - WebRTC安全发送大小
};
```
**背压控制机制**
- **DataChannel 阈值**`bufferedAmountLowThreshold = 256KB`Initiator)和`512KB`NetworkTransmitter
- **最大缓冲限制**`maxBuffer = 1MB`,超过时等待背压释放
- **异步等待策略**:监听`bufferedamountlow`事件,支持超时机制(10 秒)
**嵌入元数据包格式**
```
[4字节长度][JSON元数据][实际数据块]
```
- 每个网络块都包含:chunkIndex、totalChunks、fileOffset、fileId、isLastChunk
- 接收端可独立解析,无需依赖额外状态
### 接收侧智能存储策略
**存储选择逻辑**
```typescript
ReceptionConfig.shouldSaveToDisk(fileSize, hasSaveDirectory);
```
- **内存存储**:文件 < 1GB 且未指定保存目录
- **磁盘存储**:文件 ≥ 1GB 或用户选择了保存目录
- **缓冲管理**:最多缓存 100 个分片(约 6.4MB)
**分片验证机制**
- **格式兼容**:支持 ArrayBuffer/Blob/Uint8Array/TypedArray 多种格式
- **完整性检查**:验证 fileId、chunkIndex、chunkSize 一致性
- **Firefox 兼容**Blob size 检测和转换错误处理
**严格顺序写入**
- **SequencedDiskWriter**:确保分片按序写入磁盘,支持大文件流式处理
- **断点续传**:通过`getPartialFileSize()`检查本地部分文件
- **自动完成检测**`checkAndAutoFinalize()`验证分片完整性
### 性能优化细节
**发送侧优化**
- **批量读取**:32MB 批次减少 I/O 操作,提升大文件读取性能
- **网络适配**:64KB 块平衡传输效率与浏览器兼容性
- **背压响应**:利用 WebRTC 原生背压控制,避免数据丢失
**接收侧优化**
- **格式转换**ChunkProcessor 统一处理多种数据格式
- **进度节流**:文件 100ms、文件夹 200ms 间隔更新,避免 UI 过载
- **内存管理**:小文件内存组装,大文件直接写入磁盘
**错误处理**
- **发送重试**NetworkTransmitter 返回 boolean 状态,支持上层重试逻辑
- **转换容错**Blob conversion failed 时返回 null,不中断整体传输
- **超时保护**:文件完成 30 秒超时,优雅关闭 5 秒超时
### 调试与监控
**开发环境日志**
- **分片跟踪**:每 100 个分片或最后分片记录详细信息
- **背压监控**:缓冲区大小变化和等待时间统计
- **性能指标**:传输速度、批次处理时间、格式转换耗时
**生产环境优化**
- **条件日志**`ENABLE_CHUNK_LOGGING``ENABLE_PROGRESS_LOGGING`开关
- **错误上报**:关键错误通过`postLogToBackend`发送到后端
- **性能采样**:通过`performance.now()`精确测量耗时
- 适用:核对背压阈值、分片/批次策略、嵌入元数据包格式与性能调优点
- 包含:发送侧双层缓冲、接收侧存储策略、调试与监控建议等
## 9)断点续传深度分析
### 断点续传核心机制
本节已拆分到:[`docs/ai-playbook/flows/resume.zh-CN.md`](./flows/resume.zh-CN.md)
**续传检测与状态恢复**
- **发送侧初始化**`StreamingFileReader constructor(file, startOffset)` 支持从任意偏移量开始
- **接收侧检测**`StreamingFileWriter.getPartialFileSize()` 通过 File System Access API 检查部分文件
- **状态同步**fileRequest 消息包含 offset 参数,通知发送方从指定位置继续传输
**分片索引计算**
```typescript
// 统一的分片计算逻辑
const startChunk = Math.floor(startOffset / chunkSize);
const expectedChunks = Math.ceil((fileSize - startOffset) / chunkSize);
```
### ChunkRangeCalculator 统一计算器
**设计目的**:确保发送端和接收端使用完全相同的分片计算逻辑
```typescript
getChunkRange(fileSize, startOffset, chunkSize) {
const startChunk = Math.floor(startOffset / chunkSize);
const endChunk = Math.floor((fileSize - 1) / chunkSize);
return { startChunk, endChunk, totalChunks: endChunk - startChunk + 1 };
}
```
**关键方法**
- `getRelativeChunkIndex()`:绝对索引转相对索引,用于接收端数组映射
- `isChunkIndexValid()`:验证分片索引是否在预期范围内
- `calculateExpectedChunks()`:计算预期分片数量,与 ReceptionConfig 保持一致
### 接收侧续传流程
**部分文件检测**
1. **目录准备**`createFolderStructure()` 确保目标目录存在
2. **文件查询**:通过 `getFileHandle(fileName, {create: false})` 检查文件是否存在
3. **大小获取**`file.getFile()` 获取当前文件大小作为续传起点
**续传决策逻辑**
```typescript
// FileReceiveOrchestrator.ts
const offset = await this.streamingFileWriter.getPartialFileSize(
fileInfo.name,
fileInfo.fullName
);
if (offset === fileInfo.size) {
// 文件已完整,跳过传输
return;
}
if (offset > 0) {
// 发现部分文件,准备续传
// 发送包含 offset 的 fileRequest
}
```
### 发送侧续传响应
**续传准备**
- **重置读取器**`StreamingFileReader.reset(startOffset)` 从新的偏移量开始
- **批次调整**`currentBatchStartOffset``totalFileOffset` 同步更新
- **分片索引**`startChunkIndex` 记录传输起始点,用于边界检测
**续传日志**
```typescript
const chunkRange = ChunkRangeCalculator.getChunkRange(
fileSize,
startOffset,
chunkSize
);
postLogToBackend(
`[SEND-SUMMARY] File: ${file.name}, offset: ${startOffset}, startChunk: ${chunkRange.startChunk}, endChunk: ${chunkRange.endChunk}`
);
```
### 续传的优势与限制
**优势**
- **带宽节省**:避免重新传输已接收的数据
- **时间效率**:大文件传输中断后可快速恢复
- **用户体验**:网络波动不会导致传输进度完全丢失
**限制与注意点**
- **文件一致性**:依赖文件内容未发生变化,续传前应验证文件大小/修改时间
- **存储位置**:仅在使用 File System Access API 选择保存目录时支持
- **浏览器兼容**File System Access API 主要支持 Chrome/Edge,其他浏览器降级为内存存储
**调试支持**
- **详细日志**:开发环境下记录续传起点、分片范围、预期传输量
- **错误处理**:文件访问失败时回退到从头开始传输
- **状态跟踪**:Store 层记录续传状态和实际接收大小
- 适用:核对续传检测、offset 协商与分片范围计算的一致性
- 包含:ChunkRangeCalculator、接收侧/发送侧续传流程、限制与调试要点等
## 10)重连与状态一致性深度分析
### WebRTC 基础层重连机制
本节已拆分到:[`docs/ai-playbook/flows/reconnect-consistency.zh-CN.md`](./flows/reconnect-consistency.zh-CN.md)
**双重断开检测架构**
```typescript
// webrtc_base.ts
private isSocketDisconnected = false; // Socket.IO 连接状态
private isPeerDisconnected = false; // P2P 连接状态
private gracefullyDisconnectedPeers = new Set(); // 优雅断开的 peer 列表
```
**重连触发条件**:仅当 Socket.IO 和 P2P 连接都断开时才启动重连:
```typescript
// 避免重复重连:socket 断开 ≠ P2P 断开
if (
this.isSocketDisconnected &&
this.isPeerDisconnected &&
!this.reconnectionInProgress
) {
this.attemptReconnection();
}
```
### ICE 候选者队列管理
**候选者缓存策略**
- **连接未就绪时**:候选者缓存到 `iceCandidatesQueue` Map,按 peerId 分组
- **连接就绪后**:批量处理缓存的候选者,按序添加到 RTCPeerConnection
- **失效处理**:候选者失效时重新入队,验证连接状态后重试
**实现细节**
```typescript
private iceCandidatesQueue = new Map<string, RTCIceCandidate[]>();
// 缓存候选项直到连接就绪
if (dataChannel?.readyState !== 'open') {
this.queueIceCandidate(candidate, peerId);
} else {
this.addIceCandidate(candidate, peerId);
}
```
### 数据通道发送重试机制
**5 次重试策略**
```typescript
async sendToPeer(data: string | ArrayBuffer, peerId: string): Promise<boolean> {
for (let attempt = 1; attempt <= 5; attempt++) {
try {
dataChannel.send(data);
return true;
} catch (error) {
if (this.gracefullyDisconnectedPeers.has(peerId)) {
return false; // 跳过已优雅断开的 peer
}
if (attempt === 5) throw error;
await new Promise(resolve => setTimeout(resolve, attempt * 100)); // 100ms→1000ms
}
}
}
```
**重试间隔递增**100ms → 200ms → 300ms → 400ms → 500ms,最大 5 次尝试
### 房间管理层的重连支持
**幂等性设计**
- **长 ID 重连**:≥8 字符的 roomId 支持断线重连时复用房间
- **短 ID 限制**:4 位数字 ID 断线后需重新生成房间,避免冲突
**缓存 ID 重连优化**
```typescript
// useRoomManager.ts
if (roomId.length >= 8) {
// 长ID自动发送 initiator-online 信号
this.sendInitiatorOnline();
}
```
**状态同步序列**
1. **发送方重连**`initiator-online` 信号通知接收方准备重建连接
2. **接收方响应**`recipient-ready` 确认就绪状态
3. **WebRTC 协商**:重新开始 offer/answer/ICE 候选者交换
4. **传输恢复**:在新的 DataChannel 上恢复文件传输
### 状态一致性保证机制
**Store 层单一事实来源**
```typescript
// fileTransferStore.ts
export const useFileTransferStore = create<TransferState>((set, get) => ({
sendProgress: new Map(),
receiveProgress: new Map(),
// 提供清理 API 避免重复计数
clearSendProgress: (fileId: string) =>
set((state) => {
const newProgress = new Map(state.sendProgress);
newProgress.delete(fileId);
return { sendProgress: newProgress };
}),
}));
```
**连接状态机**
```typescript
type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'failed' | 'closed';
// 状态变更时触发相应处理
connectionStateChangeHandler(status: ConnectionStatus) {
switch (status) {
case 'connected':
this.gracefullyDisconnectedPeers.clear(peerId);
this.resetReconnectionState();
break;
case 'disconnected':
case 'failed':
this.cleanupExistingConnection(peerId);
break;
}
}
```
### 移动端优化策略
**唤醒锁管理**
```typescript
// WakeLockManager
async requestWakeLock(): Promise<void> {
try {
this.wakeLock = await navigator.wakeLock.request('screen');
this.wakeLock.addEventListener('release', () => {
this.wakeLock = null;
});
} catch (error) {
console.warn('Wake lock request failed:', error);
}
}
```
**网络切换适应**
- **连接检测**:监听 `connectionstatechange` 事件检测网络质量变化
- **自动重连**`connectionState: 'disconnected' | 'failed' | 'closed'` 时均触发重连流程(统一走 attemptReconnection
- **状态恢复**:重连成功后恢复房间状态和传输进度
**移动端后台/前台切换补充策略**
- **socket 连接恢复自动入房**`socket.on('connect')` 时,若已持有 `roomId` 且(`lastJoinedSocketId !== socket.id``!isInRoom`),则强制重新 `joinRoom(roomId, isInitiator, isInitiator)`;发送端会自动广播 `initiator-online`,接收端回复 `recipient-ready`
- **身份追踪**:成功 `joinRoom` 后记录 `lastJoinedSocketId = socket.id`,用以检测“后台恢复时 socketId 更换”的情形。
- **门槛放宽**`attemptReconnection` 只要满足“`roomId` 存在,且满足任一:P2P 断开 / socket 断开 / socketId 改变”,即可发起重连;不再强依赖“socket 与 P2P 同时断开”。
### 重连调试要点
**关键日志点**
- **双重断开检测**:记录 Socket.IO 和 P2P 断开的具体时间戳
- **候选者队列**:统计缓存的 ICE 候选者数量和处理时间
- **发送重试**:记录重试次数、间隔和最终结果
- **状态恢复**:追踪 `initiator-online``recipient-ready` 的时序
**常见问题诊断**
- **重复重连**:检查 `reconnectionInProgress` 标志和 `gracefullyDisconnectedPeers` 集合
- **候选者失效**:验证 `iceConnectionState``iceGatheringState` 状态
- **状态不一致**:确认 Store 层的进度清理和连接状态同步
- 适用:核对 WebRTC/Socket 双重断开判定、ICE 候选者队列、发送重试与一致性保障
- 包含:重连触发条件、重试策略、移动端补充策略、调试要点等
## 11)微方案模板(用于小改动前的对齐)
@@ -0,0 +1,99 @@
# PrivyDrop AI Playbook — Backpressure & Chunking Strategy (Deep Dive)
← Back to flow index: [`docs/ai-playbook/flows.md`](../flows.md)
(This page is the English edition of content split out from `docs/ai-playbook/flows.zh-CN.md`, preserving the original section numbering and structure.)
## 7) Backpressure & Chunking Strategy (Deep Dive)
### Sender Dual-Layer Buffering Architecture
**Design rationale**:
- **File read layer**: 4MB chunks reduce FileReader calls; 8 chunks form a 32MB batch
- **Network layer**: 64KB pieces fit WebRTC DataChannel limits and avoid sendData failed errors
- **Performance**: efficient slicing inside each batch; a single FileReader.read() yields 512 network chunks
**Key parameters**:
```typescript
TransferConfig.FILE_CONFIG = {
CHUNK_SIZE: 4194304, // 4MB - file read chunks
BATCH_SIZE: 8, // 8 chunks = 32MB batch
NETWORK_CHUNK_SIZE: 65536, // 64KB - safe WebRTC send size
};
```
**Backpressure control**:
- **DataChannel thresholds**: `bufferedAmountLowThreshold = 256KB` (Initiator) and `512KB` (NetworkTransmitter)
- **Max buffer**: `maxBuffer = 1MB`; wait until pressure releases when exceeded
- **Async waiting**: listens to `bufferedamountlow`, with a timeout safeguard (10 seconds)
**Embedded metadata packet format**:
```
[4-byte length][JSON metadata][payload bytes]
```
- Each network chunk includes: chunkIndex, totalChunks, fileOffset, fileId, isLastChunk
- Receiver can parse each packet independently without relying on extra shared state
### Receiver Smart Storage Strategy
**Storage decision logic**:
```typescript
ReceptionConfig.shouldSaveToDisk(fileSize, hasSaveDirectory);
```
- **In-memory**: file < 1GB and no save directory chosen
- **Disk**: file ≥ 1GB or the user selected a save directory
- **Buffer cap**: up to 100 chunks buffered (≈ 6.4MB)
**Chunk validation**:
- **Format compatibility**: ArrayBuffer/Blob/Uint8Array/TypedArray supported
- **Integrity checks**: validate fileId, chunkIndex, chunkSize consistency
- **Firefox quirks**: Blob size checks and conversion error handling
**Strict sequential disk writes**:
- **SequencedDiskWriter**: guarantees in-order writes; enables streaming for large files
- **Resume**: `getPartialFileSize()` checks existing partial files
- **Auto completion**: `checkAndAutoFinalize()` verifies completeness
### Performance Tuning Details
**Sender-side optimizations**:
- **Batch reads**: 32MB batches reduce I/O operations and improve large file read throughput
- **Network fit**: 64KB balances transfer efficiency with cross-browser compatibility
- **Backpressure response**: leverages native WebRTC backpressure to prevent drops
**Receiver-side optimizations**:
- **Unified format conversion**: ChunkProcessor handles multiple payload formats in one place
- **Progress throttling**: 100ms for files, 200ms for folders to avoid UI overload
- **Memory management**: small files assemble in memory; large files stream to disk
**Error handling**:
- **Send retries**: NetworkTransmitter returns boolean for upper-layer retry logic
- **Conversion tolerance**: when Blob conversion fails, return null instead of aborting the transfer
- **Timeout safeguards**: 30s completion timeout; 5s graceful close timeout
### Debugging & Monitoring
**Dev logs**:
- **Chunk tracking**: log details every 100 chunks and for the last chunk
- **Backpressure monitoring**: buffer size changes and wait-time stats
- **Performance metrics**: transfer speed, batch processing time, conversion cost
**Production optimizations**:
- **Conditional logging**: `ENABLE_CHUNK_LOGGING` and `ENABLE_PROGRESS_LOGGING`
- **Error reporting**: critical errors are sent to the backend via `postLogToBackend`
- **Performance sampling**: use `performance.now()` for precise timings
@@ -0,0 +1,98 @@
# PrivyDrop AI Playbook — 背压与分片策略深度分析(中文)
← 返回流程入口:[`docs/ai-playbook/flows.zh-CN.md`](../flows.zh-CN.md)
(本页从 `docs/ai-playbook/flows.zh-CN.md` 拆分,保留原章节编号与内容。)
## 7)背压与分片策略深度分析
### 发送侧双层缓冲架构
**设计原理**
- **文件读取层**4MB 分片减少 FileReader 调用,8 个分片组成 32MB 批次
- **网络传输层**64KB 小块适配 WebRTC DataChannel 限制,避免 sendData failed 错误
- **性能优化**:批次内高效切片,一次 FileReader.read()产生 512 个网络块
**配置参数**
```typescript
TransferConfig.FILE_CONFIG = {
CHUNK_SIZE: 4194304, // 4MB - 文件读取分片
BATCH_SIZE: 8, // 8个分片 = 32MB批次
NETWORK_CHUNK_SIZE: 65536, // 64KB - WebRTC安全发送大小
};
```
**背压控制机制**
- **DataChannel 阈值**`bufferedAmountLowThreshold = 256KB`Initiator)和`512KB`NetworkTransmitter
- **最大缓冲限制**`maxBuffer = 1MB`,超过时等待背压释放
- **异步等待策略**:监听`bufferedamountlow`事件,支持超时机制(10 秒)
**嵌入元数据包格式**
```
[4字节长度][JSON元数据][实际数据块]
```
- 每个网络块都包含:chunkIndex、totalChunks、fileOffset、fileId、isLastChunk
- 接收端可独立解析,无需依赖额外状态
### 接收侧智能存储策略
**存储选择逻辑**
```typescript
ReceptionConfig.shouldSaveToDisk(fileSize, hasSaveDirectory);
```
- **内存存储**:文件 < 1GB 且未指定保存目录
- **磁盘存储**:文件 ≥ 1GB 或用户选择了保存目录
- **缓冲管理**:最多缓存 100 个分片(约 6.4MB)
**分片验证机制**
- **格式兼容**:支持 ArrayBuffer/Blob/Uint8Array/TypedArray 多种格式
- **完整性检查**:验证 fileId、chunkIndex、chunkSize 一致性
- **Firefox 兼容**Blob size 检测和转换错误处理
**严格顺序写入**
- **SequencedDiskWriter**:确保分片按序写入磁盘,支持大文件流式处理
- **断点续传**:通过`getPartialFileSize()`检查本地部分文件
- **自动完成检测**`checkAndAutoFinalize()`验证分片完整性
### 性能优化细节
**发送侧优化**
- **批量读取**:32MB 批次减少 I/O 操作,提升大文件读取性能
- **网络适配**:64KB 块平衡传输效率与浏览器兼容性
- **背压响应**:利用 WebRTC 原生背压控制,避免数据丢失
**接收侧优化**
- **格式转换**ChunkProcessor 统一处理多种数据格式
- **进度节流**:文件 100ms、文件夹 200ms 间隔更新,避免 UI 过载
- **内存管理**:小文件内存组装,大文件直接写入磁盘
**错误处理**
- **发送重试**NetworkTransmitter 返回 boolean 状态,支持上层重试逻辑
- **转换容错**Blob conversion failed 时返回 null,不中断整体传输
- **超时保护**:文件完成 30 秒超时,优雅关闭 5 秒超时
### 调试与监控
**开发环境日志**
- **分片跟踪**:每 100 个分片或最后分片记录详细信息
- **背压监控**:缓冲区大小变化和等待时间统计
- **性能指标**:传输速度、批次处理时间、格式转换耗时
**生产环境优化**
- **条件日志**`ENABLE_CHUNK_LOGGING``ENABLE_PROGRESS_LOGGING`开关
- **错误上报**:关键错误通过`postLogToBackend`发送到后端
- **性能采样**:通过`performance.now()`精确测量耗时
+178
View File
@@ -0,0 +1,178 @@
# PrivyDrop AI Playbook — Frontend Component System & Core-Orchestrator Collaboration
← Back to flow index: [`docs/ai-playbook/flows.md`](../flows.md)
(This page is the English edition of content split out from `docs/ai-playbook/flows.zh-CN.md`, preserving the original section numbering and structure.)
## 6) Frontend Component System & Core-Orchestrator Collaboration
### Component Architecture Layers
```
App Router (page.tsx/layout.tsx)
HomeClient (layout & SEO)
ClipboardApp (top-level UI orchestrator)
SendTabPanel/RetrieveTabPanel (feature panels)
Business Hooks (state + orchestration)
Core Services (webrtcService) + Store (fileTransferStore)
```
### ClipboardApp Orchestrator Pattern
**Core responsibilities**:
- Integrates 5 key business hooks: useWebRTCConnection, useFileTransferHandler, useRoomManager, usePageSetup, useClipboardAppMessages
- Handles global drag events: dragenter/dragleave/dragover/drop, supports multi-file and folder-tree traversal
- Manages the Send/Retrieve tabs via activeTab
- Unified messaging: shareMessage/retrieveMessage auto-dismiss after 4 seconds
### Hook Layering & Separation of Concerns
**useWebRTCConnection** (state bridge):
- Computes global transfer state (isAnyFileTransferring)
- Exposes webrtcService methods (broadcastDataToAllPeers, requestFile, requestFolder)
- Provides reset methods (resetSenderConnection, resetReceiverConnection)
**useFileTransferHandler** (files and content):
- File ops: addFilesToSend (dedupe), removeFileToSend
- Downloads: handleDownloadFile (supports folder ZIP downloads)
- Key fix: uses `useFileTransferStore.getState()` to read the latest state and avoid stale closures
- Retry: up to 3 retries with 50ms interval and detailed error logs
**useRoomManager** (room lifecycle):
- Room ops: joinRoom (supports cached-ID reconnect), processRoomIdInput (750ms debounce)
- Leave protection: confirmation prompt while transferring (checks isAnyFileTransferring)
- Status text: dynamic room status copy
- Link generation: auto-generates share links
**usePageSetup** (page initialization):
- i18n dictionary loading and error handling
- URL param handling: extracts roomId and auto-joins (200ms delay to ensure DOM readiness)
- Referrer tracking (trackReferrer)
**useClipboardAppMessages** (messages):
- Split message states: shareMessage (send side) and retrieveMessage (receive side)
- Unified API: putMessageInMs(message, isShareEnd, displayTimeMs)
- Auto cleanup: clears message state after 4 seconds
### Panel-Specific Design
**SendTabPanel**:
- Dual-mode room ID generation: 4-digit numbers (via backend API) and UUID (via Web Crypto)
- Rich-text editor integration (dynamic import, SSR disabled)
- File upload handling + file list management
- Share link + QR code
**RetrieveTabPanel**:
- File System Access API integration: directory selection and direct disk saves
- Rich-text rendering (dangerouslySetInnerHTML)
- File requests + download state management
- Save-location selection and large file/folder hints
**FileListDisplay**:
- Smart grouping and stats for files/folders
- Cross-browser download strategy: Chrome auto-download; other browsers show manual save guidance
- Download count stats + transfer progress tracking
- Resume state and storage mode display (memory/disk)
### Key UX Improvements
1. **Stale-closure fix for download state**: `useFileTransferHandler.ts:110` uses `useFileTransferStore.getState()`
2. **Debounced roomId validation**: `useRoomManager.ts:247` uses lodash debounce (750ms)
3. **Leaving while transferring**: `useRoomManager.ts:164,218` checks `isAnyFileTransferring` and shows a confirmation dialog
4. **Cached-ID reconnect**: `useRoomManager.ts:91` detects long IDs (≥8 chars) and auto-sends `initiator-online`
5. **Folder ZIP downloads**: `useFileTransferHandler.ts:89` builds ZIPs on the fly with JSZip
6. **Global drag-and-drop robustness**: ClipboardApp uses dragCounter to avoid mis-detecting drag state; supports webkitGetAsEntry folder traversal
7. **Clipboard compatibility**: useClipboardActions supports modern navigator.clipboard APIs with document.execCommand fallback
8. **Rich-text safety**: useRichTextToPlainText is safe on server render; client-side DOM conversion handles block elements
9. **In-app navigation without breaking transfers (same tab)**: relies on `frontend/stores/fileTransferStore.ts` (Zustand singleton) and `frontend/lib/webrtcService.ts` (service singleton). App Router navigation keeps transfers and selected/received content intact. Avoid calling `webrtcService.leaveRoom()` or resetting the store in route-change side effects. Refresh/new tab is not covered.
### UI Connection Feedback State Machine (Weak Network / VPN Hints)
- Join phase
- Immediate: `join_inProgress` (“Joining the room…”).
- Not finished after 3s: `join_slow` (“Connection seems slow—check your network/VPN…”).
- Timeout after 15s: `join_timeout` (“Join timed out…”).
- Equivalent success signal: while waiting for `joinResponse`, if `ready/recipient-ready/offer` arrives, treat it as “joined” and immediately clear the 3s/15s timers and hints to avoid “slow/timeout hints after success”.
- Negotiation phase (WebRTC)
- Enter `new/connecting`: normalize to “negotiating” → `rtc_negotiating`.
- Not connected after 8s: `rtc_slow` (“Your network may be restricted—try turning off VPN or try again later”). Only fires when the page is visible. Only once per negotiation attempt (timer starts when either side enters negotiating; ownership goes to the side that entered negotiating first).
- Connection & reconnection
- First `connected`: `rtc_connected` (one-time).
- Foreground disconnect: `rtc_reconnecting` → upon recovery `rtc_restored`.
- Background disconnect does not notify; when returning to foreground, if still disconnected, notify `rtc_reconnecting` immediately.
- If the page is backgrounded during a disconnect, when returning to foreground, if still negotiating and the slow timer had fired, emit `rtc_slow` once and mark it as already shown to avoid repeats.
Implementation locations:
- `frontend/hooks/useRoomManager.ts`: join-phase hints and timers (3s slow, 15s timeout), cleared on join success/failure; supports early “equivalent success signals” (`ready/recipient-ready/offer`).
- `frontend/hooks/useConnectionFeedback.ts`: maps WebRTC connection states to UI hints.
- Phase normalization (mapPhase): `new/connecting``negotiating`; `failed/closed``disconnected`.
- Negotiation slow hint: 8s timer, foreground/background throttling, only once per attempt (including deferred background → emitted on foreground).
- One-shot hints: first `connected` only once; disconnected → restored shows `rtc_restored`; `rtc_reconnecting` only in foreground.
- Shared helpers: timer + visibility control via `frontend/utils/useOneShotSlowHint.ts`; phase normalization via `frontend/utils/rtcPhase.ts`.
Copy & i18n:
- Message keys live in `frontend/constants/messages/*.{ts}`; types in `frontend/types/messages.ts`.
- Key messages: `join_inProgress`, `join_slow`, `join_timeout`, `rtc_negotiating`, `rtc_slow`, `rtc_connected`, `rtc_reconnecting`, `rtc_restored` (filled across en/ja/es/de/fr/ko).
Throttling & display:
- All hints auto-dismiss after ~46 seconds; use `useClipboardAppMessages.putMessageInMs(message, isShareEnd, ms)` as the unified display channel.
- Connection feedback fires under three constraints: state transition + ever/wasDisc markers + visibility checks, preventing “hint storms”.
10. **Auto-join on switching to Retrieve (cached ID)**: when switching to Retrieve, not in a room, no `roomId` in URL, empty input, and a cached ID exists locally, auto-fill and call joinRoom. Entry: `frontend/components/ClipboardApp.tsx` (watches activeTab, reads `frontend/lib/roomIdCache.ts`).
11. **Sender “Use cached ID” joins immediately**: clicking “Use cached ID” in SendTabPanel triggers joining right away (not just filling the input). Entry: `frontend/components/ClipboardApp/CachedIdActionButton.tsx` (`onUseCached`) + `frontend/components/ClipboardApp/SendTabPanel.tsx`.
12. **Dark theme toggle**: single-button Light/Dark toggle in `frontend/components/web/ThemeToggle.tsx`, integrated into `frontend/components/web/Header.tsx` (desktop & mobile). Some local styles are migrated from hardcoded colors to tokens (e.g. retrieve panel uses `bg-card text-card-foreground`).
### Frontend Architecture Specializations
**Rich-text editor module**:
- **RichTextEditor**: main editor component; contentEditable, image paste, formatting tools; SSR disabled
- **Toolbar separation**: BasicFormatTools (bold/italic/underline), FontTools (font/size/color), AlignmentTools, InsertTools (link/image/code block)
- **Type-safe design**: complete TypeScript types (FormatType, AlignmentType, FontStyleType, CustomClipboardEvent)
- **Editor hooks**: useEditorCommands (commands), useSelection (selection), useStyleManagement (style)
**Website page components**:
- **Header responsive nav**: desktop horizontal nav + mobile hamburger; integrates GitHub link and language switcher
- **Footer i18n**: dynamic copyright year and multilingual support links via languageDisplayNames
- **FAQSection**: configurable “tool page vs standalone page”, heading level control, and automatic FAQ array generation
- **Content components**: HowItWorks (animated steps + video), SystemDiagram, KeyFeatures
**UI component library architecture**:
- **Built on Radix UI**: Button (CVA variants), Accordion, Dialog, Select, DropdownMenu
- **Design-system consistency**: shared cn utility, theme token system, animation transitions
- **Composable patterns**: DialogHeader/DialogFooter/DialogTitle/DialogDescription
- **Lazy-load optimizations**: LazyLoadWrapper uses react-intersection-observer with rootMargin tuning to reduce layout shifts
**Shared components as utilities**:
- **clipboard_btn**: WriteClipboardButton/ReadClipboardButton split; integrates useClipboardActions; supports i18n messages
- **TableOfContents**: Chinese heading ID generation, scroll tracking, indentation, IntersectionObserver
- **JsonLd SEO**: multi-type support, suppressHydrationWarning, array vs single object handling
- **AutoPopupDialog/YouTubePlayer**: scenario-driven wrappers designed for reuse
### Dataflow Pattern
- **One-way dataflow**: Store → Hooks → Components
- **Centralized state**: all state is owned by `useFileTransferStore`
- **Standardized error handling**: unified message channel (putMessageInMs)
- **i18n integration**: useLocale + getDictionary provide multilingual content
+173
View File
@@ -0,0 +1,173 @@
# PrivyDrop AI Playbook — 前端组件系统与业务中枢协作流程(中文)
← 返回流程入口:[`docs/ai-playbook/flows.zh-CN.md`](../flows.zh-CN.md)
(本页从 `docs/ai-playbook/flows.zh-CN.md` 拆分,保留原章节编号与内容。)
## 6)前端组件系统与业务中枢协作流程
### 组件架构层级
```
App Router (page.tsx/layout.tsx)
HomeClient (页面布局与SEO)
ClipboardApp (顶层UI协调器)
SendTabPanel/RetrieveTabPanel (功能面板)
业务中枢 Hooks (状态管理与业务逻辑)
Core Services (webrtcService) + Store (fileTransferStore)
```
### ClipboardApp 顶层协调器模式
**核心职责**
- 集成 5 个关键业务 hooksuseWebRTCConnection、useFileTransferHandler、useRoomManager、usePageSetup、useClipboardAppMessages
- 全局拖拽事件处理:dragenter/dragleave/dragover/drop,支持多文件和文件夹树遍历
- 双标签页状态管理:发送/接收面板切换,通过 activeTab 控制
- 统一消息系统:shareMessage/retrieveMessage 4 秒自动消失机制
### Hook 层级与职责分离
**useWebRTCConnection**(状态桥梁):
- 计算全局传输状态(isAnyFileTransferring
- 暴露 webrtcService 方法(broadcastDataToAllPeers、requestFile、requestFolder
- 提供连接重置方法(resetSenderConnection、resetReceiverConnection
**useFileTransferHandler**(文件与内容管理):
- 文件操作:addFilesToSend(去重)、removeFileToSend
- 下载功能:handleDownloadFile(支持文件夹压缩下载)
- 关键修复:使用 `useFileTransferStore.getState()` 获取最新状态,避免闭包问题
- 重试机制:最大 3 次重试,50ms 间隔,详细错误日志
**useRoomManager**(房间生命周期管理):
- 房间操作:joinRoom(支持缓存 ID 重连)、processRoomIdInput750ms 防抖)
- 离开保护:传输中确认提示(isAnyFileTransferring 检查)
- 状态文本:动态更新房间状态文本
- 链接生成:自动生成分享链接
**usePageSetup**(页面初始化):
- 国际化消息加载与错误处理
- URL 参数处理:roomId 自动提取并触发加入房间(200ms 延迟确保 DOM 就绪)
- 引荐来源追踪(trackReferrer
**useClipboardAppMessages**(消息管理):
- 分离式消息状态:shareMessage(发送相关)和 retrieveMessage(接收相关)
- 统一消息显示接口:putMessageInMs(message, isShareEnd, displayTimeMs)
- 自动清理机制:4 秒后自动清空消息状态
### 面板组件特化设计
**SendTabPanel 发送面板**
- 房间 ID 双模式生成:4 位数字(后端 API 生成)和 UUID(前端 crypto API
- 富文本编辑器集成(动态导入,SSR 禁用)
- 文件上传处理和文件列表管理
- 分享链接生成与二维码显示
**RetrieveTabPanel 接收面板**
- File System Access API 集成:目录选择和直接保存到磁盘
- 富文本内容渲染(dangerouslySetInnerHTML
- 文件请求和下载状态管理
- 保存位置选择和大文件/文件夹提示
**FileListDisplay 文件列表**
- 智能文件/文件夹分组和统计显示
- 多浏览器下载策略:Chrome 自动下载,其他浏览器手动保存提示
- 下载计数统计和传输进度跟踪
- 断点续传和存储方式显示(内存/磁盘)
### 关键用户体验优化
1. **下载状态闭包修复**`useFileTransferHandler.ts:110` 使用 `useFileTransferStore.getState()` 获取最新状态
2. **房间 ID 输入防抖**`useRoomManager.ts:247` 使用 lodash debounce 750ms 延迟验证
3. **传输中离开保护**`useRoomManager.ts:164,218` 检查 `isAnyFileTransferring` 状态并显示确认对话框
4. **缓存 ID 重连**`useRoomManager.ts:91` 检测长 ID(≥8 字符)自动发送 `initiator-online`
5. **文件夹压缩下载**`useFileTransferHandler.ts:89` 使用 JSZip 动态创建 ZIP 文件
6. **全局拖拽优化**ClipboardApp 使用 dragCounter 防止拖拽状态误判,支持 webkitGetAsEntry 文件树遍历
7. **剪贴板兼容性**useClipboardActions 支持现代 navigator.clipboard API 和 document.execCommand 降级方案
8. **富文本安全处理**useRichTextToPlainText 服务端渲染安全,客户端 DOM 转换处理块级元素
9. **站内导航不中断(同一标签页)**:依赖 `frontend/stores/fileTransferStore.ts`Zustand 单例)与 `frontend/lib/webrtcService.ts`(服务单例)。App Router 页面切换不打断传输且保留已选择/已接收内容。注意不要在路由切换副作用中调用 `webrtcService.leaveRoom()` 或重置 Store;刷新/新标签不在保证范围内。
### UI 连接反馈状态机(弱网/VPN 提示)
- 入房阶段(join
- 立即:`join_inProgress`(“正在加入房间…”)。
- 3s 未完成:`join_slow`(“连接较慢,建议检查网络/VPN…”)。
- 15s 超时:`join_timeout`(“加入超时…”)。
- 等效成功信号:在等待 `joinResponse` 期间,若收到 `ready/recipient-ready/offer`,视为提前入房成功并即时清理 3s/15s 定时器与提示,避免“成功后再出现慢/超时提示”。
- 协商阶段(WebRTC
- 进入 `new/connecting`:归一为 “协商中” → `rtc_negotiating`
- 8s 未连上:`rtc_slow`(“网络可能受限,尝试关闭 VPN 或稍后再试”)。仅在页面前台可见时触发;同一次协商尝试仅提示一次(发送端/接收端任一进入协商即启动计时,提示归属以最先进入协商的一侧为准)。
- 连接与重连
- 首次 `connected``rtc_connected`(仅一次)。
- 前台断开:`rtc_reconnecting` → 恢复后 `rtc_restored`
- 后台断开不提示;回到前台若仍断开立即提示 `rtc_reconnecting`
- 已断开期间若页面在后台,返回前台时若仍处于协商态且此前触发了慢协商计时,则会补发一次 `rtc_slow` 并标记本次协商已提示,以避免重复。
实现位置:
- `frontend/hooks/useRoomManager.ts`:入房阶段提示与定时器管理(3s 慢网、15s 超时),并在 join 成功/失败时清理定时器;支持“等效成功信号”提前判定成功(`ready/recipient-ready/offer`)。
- `frontend/hooks/useConnectionFeedback.ts`:桥接 WebRTC 连接态到 UI 提示。
- 状态归一化(mapPhase):`new/connecting``negotiating``failed/closed``disconnected`
- 协商慢提示:8s 定时器、前后台可见性节制、单次协商尝试仅提示一次(含挂起→前台补发)。
- 一次性提示:首次 `connected` 只显示一次;断开→恢复显示 `rtc_restored`;仅前台显示 `rtc_reconnecting`
- 复用:慢提示定时与前后台补发由 `frontend/utils/useOneShotSlowHint.ts` 统一实现;状态归一化由 `frontend/utils/rtcPhase.ts` 提供。
文案与 i18n
- 文案键均位于 `frontend/constants/messages/*.{ts}`,类型定义见 `frontend/types/messages.ts`
- 关键键:`join_inProgress``join_slow``join_timeout``rtc_negotiating``rtc_slow``rtc_connected``rtc_reconnecting``rtc_restored`(已在 en/ja/es/de/fr/ko 全部补齐)。
节流与展示:
- 所有提示默认 4–6 秒自动消失;通过 `useClipboardAppMessages.putMessageInMs(message, isShareEnd, ms)` 统一展示。
- 连接反馈提示在“状态迁移 + ever/wasDisc 标记 + 可见性判断”三重约束下触发,避免提示风暴。
10. **切到接收端自动加入(缓存ID**:当用户切换到接收端、未在房间、URL 无 `roomId`、输入框为空且本地存在缓存 ID 时,自动填充并直接调用加入房间以提升体验。入口:`frontend/components/ClipboardApp.tsx`(监听 `activeTab` 变化,读取 `frontend/lib/roomIdCache.ts`)。
11. **发送端“使用缓存ID”即刻加入**:发送端在 `SendTabPanel` 点击“使用缓存ID”后会立即调用加入房间(而非仅填充输入框)。入口:`frontend/components/ClipboardApp/CachedIdActionButton.tsx``onUseCached` 回调)+ `frontend/components/ClipboardApp/SendTabPanel.tsx`
12. **深色主题切换**:提供单按钮 Light/Dark 切换,入口:`frontend/components/web/ThemeToggle.tsx`;集成位置:`frontend/components/web/Header.tsx`(桌面与移动);局部样式从硬编码颜色迁移为设计令牌(例如接收面板使用 `bg-card text-card-foreground`)。
### 前端组件架构特化
**富文本编辑器模块**
- **RichTextEditor**:主编辑器组件,支持 contentEditable、图片粘贴、格式化工具、SSR 禁用
- **工具栏组件分离**BasicFormatTools(粗体/斜体/下划线)、FontTools(字体/大小/颜色)、AlignmentTools(对齐)、InsertTools(链接/图片/代码块)
- **类型安全设计**:完整的 TypeScript 类型定义(FormatType、AlignmentType、FontStyleType、CustomClipboardEvent
- **编辑器 Hooks**useEditorCommands(命令执行)、useSelection(选择管理)、useStyleManagement(样式管理)
**网站页面组件设计**
- **Header 响应式导航**:桌面端水平导航+移动端汉堡菜单,集成 GitHub 链接和语言切换器
- **Footer 国际化**:动态版权年份、多语言支持链接显示,使用 languageDisplayNames 配置
- **FAQSection 灵活配置**:支持工具页面/独立页面切换、标题级别控制、自动 FAQ 数组生成
- **内容展示组件**:HowItWorks(步骤动画+视频)、SystemDiagram(架构图)、KeyFeatures(图标+特性说明)
**UI 组件库架构**
- **基于 Radix UI**ButtonCVA 多变体系统)、Accordion(手风琴)、Dialog(模态对话框)、Select、DropdownMenu
- **设计系统一致性**:统一的 cn 工具函数、主题色彩系统、动画过渡效果
- **组件组合模式**DialogHeader/DialogFooter/DialogTitle/DialogDescription 组合设计
- **懒加载优化**LazyLoadWrapper 使用 react-intersection-observer,支持 rootMargin 配置防止布局跳动
**通用组件工具化**
- **clipboard_btn**WriteClipboardButton/ReadClipboardButton 分离设计,集成 useClipboardActions hook,支持国际化消息
- **TableOfContents**:支持中文标题 ID 生成、滚动跟踪、层级缩进、IntersectionObserver 监听
- **JsonLd SEO**:多类型数据支持、suppressHydrationWarning、数组/单对象处理
- **AutoPopupDialog/YouTubePlayer**:业务场景封装,复用性设计
### 数据流模式
- **单向数据流**Store → Hooks → Components
- **状态管理集中化**:所有状态通过 `useFileTransferStore` 统一管理
- **错误处理标准化**:统一的消息提示机制(putMessageInMs
- **国际化集成**useLocale + getDictionary 提供多语言支持
@@ -0,0 +1,183 @@
# PrivyDrop AI Playbook — Reconnect & State Consistency (Deep Dive)
← Back to flow index: [`docs/ai-playbook/flows.md`](../flows.md)
(This page is the English edition of content split out from `docs/ai-playbook/flows.zh-CN.md`, preserving the original section numbering and structure.)
## 10) Reconnect & State Consistency (Deep Dive)
### WebRTC Base-Layer Reconnect Mechanics
**Dual disconnect detection**:
```typescript
// webrtc_base.ts
private isSocketDisconnected = false; // Socket.IO connection state
private isPeerDisconnected = false; // P2P connection state
private gracefullyDisconnectedPeers = new Set(); // peers closed gracefully
```
**Reconnect trigger**: only start reconnection when both Socket.IO and P2P are disconnected:
```typescript
// Avoid duplicate reconnects: socket disconnect != P2P disconnect
if (
this.isSocketDisconnected &&
this.isPeerDisconnected &&
!this.reconnectionInProgress
) {
this.attemptReconnection();
}
```
### ICE Candidate Queue Management
**Candidate caching strategy**:
- **Before ready**: cache candidates in the `iceCandidatesQueue` Map, grouped by peerId
- **After ready**: flush cached candidates and add them to RTCPeerConnection in order
- **Invalid handling**: re-queue invalid candidates and retry after validating connection state
**Implementation detail**:
```typescript
private iceCandidatesQueue = new Map<string, RTCIceCandidate[]>();
// Cache candidates until the connection is ready
if (dataChannel?.readyState !== 'open') {
this.queueIceCandidate(candidate, peerId);
} else {
this.addIceCandidate(candidate, peerId);
}
```
### DataChannel Send-Retry Mechanism
**5-attempt retry policy**:
```typescript
async sendToPeer(data: string | ArrayBuffer, peerId: string): Promise<boolean> {
for (let attempt = 1; attempt <= 5; attempt++) {
try {
dataChannel.send(data);
return true;
} catch (error) {
if (this.gracefullyDisconnectedPeers.has(peerId)) {
return false; // skip peers that were closed gracefully
}
if (attempt === 5) throw error;
await new Promise(resolve => setTimeout(resolve, attempt * 100)); // 100ms→1000ms
}
}
}
```
**Backoff**: 100ms → 200ms → 300ms → 400ms → 500ms, up to 5 attempts
### Room-Layer Reconnect Support
**Idempotency**:
- **Long IDs**: roomId length ≥ 8 supports room reuse across reconnects
- **Short IDs**: 4-digit numeric IDs must be re-generated after disconnect to avoid collisions
**Cached-ID reconnect optimization**:
```typescript
// useRoomManager.ts
if (roomId.length >= 8) {
// long IDs auto-send initiator-online
this.sendInitiatorOnline();
}
```
**State sync sequence**:
1. **Initiator reconnects**: sends `initiator-online` to signal readiness
2. **Recipient replies**: `recipient-ready` confirms readiness
3. **WebRTC negotiation**: re-run offer/answer/ICE exchange
4. **Transfer continues**: resume file transfer on the new DataChannel
### State Consistency Safeguards
**Store as the single source of truth**:
```typescript
// fileTransferStore.ts
export const useFileTransferStore = create<TransferState>((set, get) => ({
sendProgress: new Map(),
receiveProgress: new Map(),
// cleanup APIs to avoid double counting
clearSendProgress: (fileId: string) =>
set((state) => {
const newProgress = new Map(state.sendProgress);
newProgress.delete(fileId);
return { sendProgress: newProgress };
}),
}));
```
**Connection state machine**:
```typescript
type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'failed' | 'closed';
// react to state transitions
connectionStateChangeHandler(status: ConnectionStatus) {
switch (status) {
case 'connected':
this.gracefullyDisconnectedPeers.clear(peerId);
this.resetReconnectionState();
break;
case 'disconnected':
case 'failed':
this.cleanupExistingConnection(peerId);
break;
}
}
```
### Mobile Optimizations
**Wake lock management**:
```typescript
// WakeLockManager
async requestWakeLock(): Promise<void> {
try {
this.wakeLock = await navigator.wakeLock.request('screen');
this.wakeLock.addEventListener('release', () => {
this.wakeLock = null;
});
} catch (error) {
console.warn('Wake lock request failed:', error);
}
}
```
**Adapting to network changes**:
- **Detection**: listen to `connectionstatechange` to infer network quality changes
- **Auto-reconnect**: `connectionState: 'disconnected' | 'failed' | 'closed'` all route into the same reconnect path (attemptReconnection)
- **Restore state**: after reconnect, restore room status and transfer progress
**Mobile background/foreground addendum**:
- **Auto re-join on socket reconnect**: on `socket.on('connect')`, if a `roomId` exists and (`lastJoinedSocketId !== socket.id` or `!isInRoom`), force `joinRoom(roomId, isInitiator, isInitiator)`. The initiator auto-broadcasts `initiator-online`; the recipient replies `recipient-ready`.
- **Identity tracking**: after a successful `joinRoom`, record `lastJoinedSocketId = socket.id` to detect “socketId changed after background resume”.
- **Lowered threshold**: `attemptReconnection` can start as long as `roomId` exists and any of the following hold: P2P disconnected / socket disconnected / socketId changed. It no longer requires “socket and P2P disconnected at the same time”.
### Reconnect Debugging Notes
**Key log points**:
- **Dual disconnect detection**: record timestamps for Socket.IO vs P2P disconnects
- **Candidate queue**: count cached ICE candidates and flush durations
- **Send retries**: record retry attempts, delays, and the final result
- **State restoration**: trace `initiator-online``recipient-ready` ordering
**Common diagnostics**:
- **Duplicate reconnects**: check `reconnectionInProgress` and the `gracefullyDisconnectedPeers` set
- **Invalid candidates**: validate `iceConnectionState` and `iceGatheringState`
- **State divergence**: confirm store progress cleanup and connection-state synchronization
@@ -0,0 +1,182 @@
# PrivyDrop AI Playbook — 重连与状态一致性深度分析(中文)
← 返回流程入口:[`docs/ai-playbook/flows.zh-CN.md`](../flows.zh-CN.md)
(本页从 `docs/ai-playbook/flows.zh-CN.md` 拆分,保留原章节编号与内容。)
## 10)重连与状态一致性深度分析
### WebRTC 基础层重连机制
**双重断开检测架构**
```typescript
// webrtc_base.ts
private isSocketDisconnected = false; // Socket.IO 连接状态
private isPeerDisconnected = false; // P2P 连接状态
private gracefullyDisconnectedPeers = new Set(); // 优雅断开的 peer 列表
```
**重连触发条件**:仅当 Socket.IO 和 P2P 连接都断开时才启动重连:
```typescript
// 避免重复重连:socket 断开 ≠ P2P 断开
if (
this.isSocketDisconnected &&
this.isPeerDisconnected &&
!this.reconnectionInProgress
) {
this.attemptReconnection();
}
```
### ICE 候选者队列管理
**候选者缓存策略**
- **连接未就绪时**:候选者缓存到 `iceCandidatesQueue` Map,按 peerId 分组
- **连接就绪后**:批量处理缓存的候选者,按序添加到 RTCPeerConnection
- **失效处理**:候选者失效时重新入队,验证连接状态后重试
**实现细节**
```typescript
private iceCandidatesQueue = new Map<string, RTCIceCandidate[]>();
// 缓存候选项直到连接就绪
if (dataChannel?.readyState !== 'open') {
this.queueIceCandidate(candidate, peerId);
} else {
this.addIceCandidate(candidate, peerId);
}
```
### 数据通道发送重试机制
**5 次重试策略**
```typescript
async sendToPeer(data: string | ArrayBuffer, peerId: string): Promise<boolean> {
for (let attempt = 1; attempt <= 5; attempt++) {
try {
dataChannel.send(data);
return true;
} catch (error) {
if (this.gracefullyDisconnectedPeers.has(peerId)) {
return false; // 跳过已优雅断开的 peer
}
if (attempt === 5) throw error;
await new Promise(resolve => setTimeout(resolve, attempt * 100)); // 100ms→1000ms
}
}
}
```
**重试间隔递增**100ms → 200ms → 300ms → 400ms → 500ms,最大 5 次尝试
### 房间管理层的重连支持
**幂等性设计**
- **长 ID 重连**:≥8 字符的 roomId 支持断线重连时复用房间
- **短 ID 限制**:4 位数字 ID 断线后需重新生成房间,避免冲突
**缓存 ID 重连优化**
```typescript
// useRoomManager.ts
if (roomId.length >= 8) {
// 长ID自动发送 initiator-online 信号
this.sendInitiatorOnline();
}
```
**状态同步序列**
1. **发送方重连**`initiator-online` 信号通知接收方准备重建连接
2. **接收方响应**`recipient-ready` 确认就绪状态
3. **WebRTC 协商**:重新开始 offer/answer/ICE 候选者交换
4. **传输恢复**:在新的 DataChannel 上恢复文件传输
### 状态一致性保证机制
**Store 层单一事实来源**
```typescript
// fileTransferStore.ts
export const useFileTransferStore = create<TransferState>((set, get) => ({
sendProgress: new Map(),
receiveProgress: new Map(),
// 提供清理 API 避免重复计数
clearSendProgress: (fileId: string) =>
set((state) => {
const newProgress = new Map(state.sendProgress);
newProgress.delete(fileId);
return { sendProgress: newProgress };
}),
}));
```
**连接状态机**
```typescript
type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'failed' | 'closed';
// 状态变更时触发相应处理
connectionStateChangeHandler(status: ConnectionStatus) {
switch (status) {
case 'connected':
this.gracefullyDisconnectedPeers.clear(peerId);
this.resetReconnectionState();
break;
case 'disconnected':
case 'failed':
this.cleanupExistingConnection(peerId);
break;
}
}
```
### 移动端优化策略
**唤醒锁管理**
```typescript
// WakeLockManager
async requestWakeLock(): Promise<void> {
try {
this.wakeLock = await navigator.wakeLock.request('screen');
this.wakeLock.addEventListener('release', () => {
this.wakeLock = null;
});
} catch (error) {
console.warn('Wake lock request failed:', error);
}
}
```
**网络切换适应**
- **连接检测**:监听 `connectionstatechange` 事件检测网络质量变化
- **自动重连**`connectionState: 'disconnected' | 'failed' | 'closed'` 时均触发重连流程(统一走 attemptReconnection
- **状态恢复**:重连成功后恢复房间状态和传输进度
**移动端后台/前台切换补充策略**
- **socket 连接恢复自动入房**`socket.on('connect')` 时,若已持有 `roomId` 且(`lastJoinedSocketId !== socket.id``!isInRoom`),则强制重新 `joinRoom(roomId, isInitiator, isInitiator)`;发送端会自动广播 `initiator-online`,接收端回复 `recipient-ready`
- **身份追踪**:成功 `joinRoom` 后记录 `lastJoinedSocketId = socket.id`,用以检测“后台恢复时 socketId 更换”的情形。
- **门槛放宽**`attemptReconnection` 只要满足“`roomId` 存在,且满足任一:P2P 断开 / socket 断开 / socketId 改变”,即可发起重连;不再强依赖“socket 与 P2P 同时断开”。
### 重连调试要点
**关键日志点**
- **双重断开检测**:记录 Socket.IO 和 P2P 断开的具体时间戳
- **候选者队列**:统计缓存的 ICE 候选者数量和处理时间
- **发送重试**:记录重试次数、间隔和最终结果
- **状态恢复**:追踪 `initiator-online``recipient-ready` 的时序
**常见问题诊断**
- **重复重连**:检查 `reconnectionInProgress` 标志和 `gracefullyDisconnectedPeers` 集合
- **候选者失效**:验证 `iceConnectionState``iceGatheringState` 状态
- **状态不一致**:确认 Store 层的进度清理和连接状态同步
+109
View File
@@ -0,0 +1,109 @@
# PrivyDrop AI Playbook — Resume / Partial Transfer (Deep Dive)
← Back to flow index: [`docs/ai-playbook/flows.md`](../flows.md)
(This page is the English edition of content split out from `docs/ai-playbook/flows.zh-CN.md`, preserving the original section numbering and structure.)
## 9) Resume / Partial Transfer (Deep Dive)
### Core Resume Mechanism
**Resume detection & state restoration**:
- **Sender init**: `StreamingFileReader constructor(file, startOffset)` supports starting from any offset
- **Receiver detection**: `StreamingFileWriter.getPartialFileSize()` checks partial files via the File System Access API
- **State sync**: the fileRequest message includes an offset parameter to tell the sender where to continue
**Chunk index calculation**:
```typescript
// unified chunk calculation logic
const startChunk = Math.floor(startOffset / chunkSize);
const expectedChunks = Math.ceil((fileSize - startOffset) / chunkSize);
```
### ChunkRangeCalculator (Single Source of Truth)
**Purpose**: ensure sender and receiver use the exact same chunk-range math
```typescript
getChunkRange(fileSize, startOffset, chunkSize) {
const startChunk = Math.floor(startOffset / chunkSize);
const endChunk = Math.floor((fileSize - 1) / chunkSize);
return { startChunk, endChunk, totalChunks: endChunk - startChunk + 1 };
}
```
**Key methods**:
- `getRelativeChunkIndex()`: convert absolute index to relative index for receiver-side array mapping
- `isChunkIndexValid()`: validate that a chunk index is within the expected range
- `calculateExpectedChunks()`: compute expected chunk count, aligned with ReceptionConfig
### Receiver-Side Resume Flow
**Partial-file detection**:
1. **Prepare directories**: `createFolderStructure()` ensures the target directory exists
2. **Lookup file**: `getFileHandle(fileName, {create: false})` checks if a file already exists
3. **Get size**: `file.getFile()` returns the current size as the resume starting point
**Resume decision logic**:
```typescript
// FileReceiveOrchestrator.ts
const offset = await this.streamingFileWriter.getPartialFileSize(
fileInfo.name,
fileInfo.fullName
);
if (offset === fileInfo.size) {
// file is already complete; skip transfer
return;
}
if (offset > 0) {
// partial file found; resume
// send fileRequest with offset
}
```
### Sender-Side Resume Response
**Preparation**:
- **Reset reader**: `StreamingFileReader.reset(startOffset)` starts reading from the new offset
- **Batch alignment**: `currentBatchStartOffset` and `totalFileOffset` are updated in sync
- **Chunk indices**: `startChunkIndex` records the transfer start point for boundary checks
**Resume log**:
```typescript
const chunkRange = ChunkRangeCalculator.getChunkRange(
fileSize,
startOffset,
chunkSize
);
postLogToBackend(
`[SEND-SUMMARY] File: ${file.name}, offset: ${startOffset}, startChunk: ${chunkRange.startChunk}, endChunk: ${chunkRange.endChunk}`
);
```
### Benefits & Limitations
**Benefits**:
- **Saves bandwidth**: avoids re-sending already received bytes
- **Faster recovery**: large transfers can resume quickly after interruption
- **Better UX**: transient network issues dont reset progress to zero
**Limitations / caveats**:
- **File consistency**: assumes file content hasnt changed; consider validating size/mtime before resuming
- **Save location requirement**: supported when the user chose a save directory via the File System Access API
- **Browser support**: File System Access API is mainly Chrome/Edge; other browsers fall back to in-memory storage
**Debug support**:
- **Verbose logs**: record resume offset, chunk range, and expected transfer volume in dev
- **Error handling**: if file access fails, fall back to a full transfer from the start
- **State tracking**: the store records resume state and actual received size
+108
View File
@@ -0,0 +1,108 @@
# PrivyDrop AI Playbook — 断点续传深度分析(中文)
← 返回流程入口:[`docs/ai-playbook/flows.zh-CN.md`](../flows.zh-CN.md)
(本页从 `docs/ai-playbook/flows.zh-CN.md` 拆分,保留原章节编号与内容。)
## 9)断点续传深度分析
### 断点续传核心机制
**续传检测与状态恢复**
- **发送侧初始化**`StreamingFileReader constructor(file, startOffset)` 支持从任意偏移量开始
- **接收侧检测**`StreamingFileWriter.getPartialFileSize()` 通过 File System Access API 检查部分文件
- **状态同步**fileRequest 消息包含 offset 参数,通知发送方从指定位置继续传输
**分片索引计算**
```typescript
// 统一的分片计算逻辑
const startChunk = Math.floor(startOffset / chunkSize);
const expectedChunks = Math.ceil((fileSize - startOffset) / chunkSize);
```
### ChunkRangeCalculator 统一计算器
**设计目的**:确保发送端和接收端使用完全相同的分片计算逻辑
```typescript
getChunkRange(fileSize, startOffset, chunkSize) {
const startChunk = Math.floor(startOffset / chunkSize);
const endChunk = Math.floor((fileSize - 1) / chunkSize);
return { startChunk, endChunk, totalChunks: endChunk - startChunk + 1 };
}
```
**关键方法**
- `getRelativeChunkIndex()`:绝对索引转相对索引,用于接收端数组映射
- `isChunkIndexValid()`:验证分片索引是否在预期范围内
- `calculateExpectedChunks()`:计算预期分片数量,与 ReceptionConfig 保持一致
### 接收侧续传流程
**部分文件检测**
1. **目录准备**`createFolderStructure()` 确保目标目录存在
2. **文件查询**:通过 `getFileHandle(fileName, {create: false})` 检查文件是否存在
3. **大小获取**`file.getFile()` 获取当前文件大小作为续传起点
**续传决策逻辑**
```typescript
// FileReceiveOrchestrator.ts
const offset = await this.streamingFileWriter.getPartialFileSize(
fileInfo.name,
fileInfo.fullName
);
if (offset === fileInfo.size) {
// 文件已完整,跳过传输
return;
}
if (offset > 0) {
// 发现部分文件,准备续传
// 发送包含 offset 的 fileRequest
}
```
### 发送侧续传响应
**续传准备**
- **重置读取器**`StreamingFileReader.reset(startOffset)` 从新的偏移量开始
- **批次调整**`currentBatchStartOffset``totalFileOffset` 同步更新
- **分片索引**`startChunkIndex` 记录传输起始点,用于边界检测
**续传日志**
```typescript
const chunkRange = ChunkRangeCalculator.getChunkRange(
fileSize,
startOffset,
chunkSize
);
postLogToBackend(
`[SEND-SUMMARY] File: ${file.name}, offset: ${startOffset}, startChunk: ${chunkRange.startChunk}, endChunk: ${chunkRange.endChunk}`
);
```
### 续传的优势与限制
**优势**
- **带宽节省**:避免重新传输已接收的数据
- **时间效率**:大文件传输中断后可快速恢复
- **用户体验**:网络波动不会导致传输进度完全丢失
**限制与注意点**
- **文件一致性**:依赖文件内容未发生变化,续传前应验证文件大小/修改时间
- **存储位置**:仅在使用 File System Access API 选择保存目录时支持
- **浏览器兼容**File System Access API 主要支持 Chrome/Edge,其他浏览器降级为内存存储
**调试支持**
- **详细日志**:开发环境下记录续传起点、分片范围、预期传输量
- **错误处理**:文件访问失败时回退到从头开始传输
- **状态跟踪**:Store 层记录续传状态和实际接收大小
+51
View File
@@ -0,0 +1,51 @@
# PrivyDrop AI Playbook — Context & Index
This playbook is a high signal-to-noise entry point for AI and developers, helping you jump to the right place in the codebase fast. It contains project context and an index of links, not step-by-step task guides.
## Project Snapshot
- Product: WebRTC-based P2P file/text sharing. Data transfers directly between browsers via RTCDataChannel with end-to-end encryption.
- Frontend: Next.js 14 (App Router), React 18, TypeScript, Tailwind, shadcn/ui.
- Backend: Node.js, Express, Socket.IO, Redis; optional STUN/TURN for NAT traversal.
- Privacy stance: The server must never relay file data; the backend is for signaling and room coordination only.
## Document Index
- README
- `README.md`
- AI Playbook
- Code map: `docs/ai-playbook/code-map.md`
- Flows (includes micro-plan template): `docs/ai-playbook/flows.md`
- Flows (deep dives, split out): `docs/ai-playbook/flows/frontend.md`, `docs/ai-playbook/flows/backpressure-chunking.md`, `docs/ai-playbook/flows/resume.md`, `docs/ai-playbook/flows/reconnect-consistency.md`
- Collaboration rules: `docs/ai-playbook/collab-rules.md`
- System & Architecture
- System architecture: `docs/ARCHITECTURE.md` / `docs/ARCHITECTURE.zh-CN.md`
- Frontend architecture: `docs/FRONTEND_ARCHITECTURE.md` / `docs/FRONTEND_ARCHITECTURE.zh-CN.md`
- Backend architecture: `docs/BACKEND_ARCHITECTURE.md` / `docs/BACKEND_ARCHITECTURE.zh-CN.md`
- Deployment
- Docker deployment: `docs/DEPLOYMENT_docker.md` / `docs/DEPLOYMENT_docker.zh-CN.md`
## Key Modules at a Glance
- Frontend core
- Hooks: `frontend/hooks/useWebRTCConnection.ts` (connection orchestration), `useRoomManager.ts` (room lifecycle), `useFileTransferHandler.ts` (payload orchestration).
- WebRTC base: `frontend/lib/webrtc_base.ts` (Socket.IO signaling, RTCPeerConnection, data channel).
- Roles: `frontend/lib/webrtc_Initiator.ts`, `frontend/lib/webrtc_Recipient.ts` (initiator/recipient behavior).
- Sending: `frontend/lib/transfer/*`, `frontend/lib/fileSender.ts` (metadata, chunking, progress).
- Receiving: `frontend/lib/receive/*`, `frontend/lib/fileReceiver.ts` (assembly, validation, persistence).
- Store: `frontend/stores/fileTransferStore.ts` (single source of truth for progress/state).
- Backend core
- Socket.IO: `backend/src/socket/handlers.ts` (join, initiator-online, recipient-ready, offer/answer/ice-candidate).
- Services: `backend/src/services/{room,redis,rateLimit}.ts`.
- REST: `backend/src/routes/api.ts` (rooms, tracking, debug logs).
## Maintenance
- Keep it lean and factual; avoid duplicating system-level docs.
- This playbook exists to support collaboration and quick orientation.
+5 -1
View File
@@ -11,10 +11,15 @@
## 文档索引
- README
- `README.zh-CN.md`
- AI Playbook
- 代码地图:`docs/ai-playbook/code-map.zh-CN.md`
- 流程(含微方案模板):`docs/ai-playbook/flows.zh-CN.md`
- 流程(深度阅读拆分):`docs/ai-playbook/flows/frontend.zh-CN.md``docs/ai-playbook/flows/backpressure-chunking.zh-CN.md``docs/ai-playbook/flows/resume.zh-CN.md``docs/ai-playbook/flows/reconnect-consistency.zh-CN.md`
- 协作规则:`docs/ai-playbook/collab-rules.zh-CN.md`
- 系统与架构
@@ -24,7 +29,6 @@
- 后端架构:`docs/BACKEND_ARCHITECTURE.md` / `docs/BACKEND_ARCHITECTURE.zh-CN.md`
- 部署
- 部署指南:`docs/DEPLOYMENT.md` / `docs/DEPLOYMENT.zh-CN.md`
- Docker 部署:`docs/DEPLOYMENT_docker.md` / `docs/DEPLOYMENT_docker.zh-CN.md`
## 关键模块速览
-36
View File
@@ -1,36 +0,0 @@
module.exports = {
apps: [
{
name: "signaling-server",
cwd: "./backend",
script: "./dist/server.js",
watch: false,
env: {
NODE_ENV: "production",
BACKEND_PORT: 3001,
},
log_date_format: "YYYY-MM-DD HH:mm:ss",
error_file: "/var/log/signaling-server-error.log",
out_file: "/var/log/signaling-server-out.log",
max_memory_restart: "500M",
instances: 1,
exec_mode: "fork",
group: "ssl-cert",
},
{
name: "privydrop-frontend",
cwd: "./frontend",
script: "node",
args: ".next/standalone/server.js",
watch: false,
env: {
NODE_ENV: "production",
PORT: 3002,
HOSTNAME: "0.0.0.0"
},
log_date_format: "YYYY-MM-DD HH:mm:ss",
error_file: "/var/log/privydrop-frontend-error.log",
out_file: "/var/log/privydrop-frontend-out.log",
}
]
};
+1 -1
View File
@@ -41,7 +41,7 @@ Before you start, please ensure you have **installed and started the backend ser
- To understand the complete project architecture and how components collaborate, please see the [**Overall Project Architecture**](../docs/ARCHITECTURE.md).
- To dive deep into the frontend's code structure, Hooks design, and state management, please read the [**Frontend Architecture Deep Dive**](../docs/FRONTEND_ARCHITECTURE.md).
- For instructions on deploying in a production environment, please refer to the [**Deployment Guide**](../docs/DEPLOYMENT.md).
- For production deployment, see the [**Docker Deployment Guide**](../docs/DEPLOYMENT_docker.md).
## 🤝 Contributing
+1 -1
View File
@@ -41,7 +41,7 @@
- 要了解完整的项目架构和组件协作方式,请参阅 [**项目整体架构**](../docs/ARCHITECTURE.zh-CN.md)。
- 要深入理解前端的代码结构、Hooks 设计和状态管理,请阅读 [**前端架构详解**](../docs/FRONTEND_ARCHITECTURE.zh-CN.md)。
- 有关生产环境部署方法,请参考 [**部署指南**](../docs/DEPLOYMENT.zh-CN.md)。
- 有关生产环境部署,请参考 [**Docker 部署指南**](../docs/DEPLOYMENT_docker.zh-CN.md)。
## 🤝 参与贡献
+16 -31
View File
@@ -1,29 +1,24 @@
"use client";
import ClipboardApp from "@/components/ClipboardApp";
import { useLocale, useTranslations } from "next-intl";
import { cn } from "@/lib/utils";
import SystemDiagram from "@/components/web/SystemDiagram";
import FAQSection from "@/components/web/FAQSection";
import HowItWorks from "@/components/web/HowItWorks";
import YouTubePlayer from "@/components/common/YouTubePlayer";
import KeyFeatures from "@/components/web/KeyFeatures";
import type { Messages } from "@/types/messages";
import LazyLoadWrapper from "@/components/common/LazyLoadWrapper";
interface PageContentProps {
messages: Messages;
lang: string;
}
export default function HomeClient({ messages, lang }: PageContentProps) {
export default function HomeClient() {
const t = useTranslations("text.home");
const lang = useLocale();
const youtube_videoId = lang === "zh" ? "I0RLCpcbUXs" : "ypt-po_R2Ds";
const bilibili_videoId = lang === "zh" ? "BV1knrjYZEfn" : "BV1yErjYFEV7";
return (
<main className="container mx-auto px-4 py-8">
{/* Hero Section */}
<h1 className="text-4xl font-bold mb-2 text-center">
{messages.text.home.h1}
</h1>
<p className="text-xl mb-4 text-center">{messages.text.home.h1P}</p>
<h1 className="text-4xl font-bold mb-2 text-center">{t("hero.title")}</h1>
<p className="text-xl mb-4 text-center">{t("hero.subtitle")}</p>
{/* App Section */}
<section
id="clipboard-app"
@@ -33,7 +28,7 @@ export default function HomeClient({ messages, lang }: PageContentProps) {
<div className="w-full max-w-none">
{/* sr-only--screen-only: visually hidden */}
<h2 className={cn("sr-only", "text-3xl font-bold mb-8 text-center")}>
{messages.text.home.h2_screenOnly}
{t("hero.screenOnlyTitle")}
</h2>
<ClipboardApp />
</div>
@@ -41,31 +36,29 @@ export default function HomeClient({ messages, lang }: PageContentProps) {
{/* How It Works Section */}
<section aria-label="How It Works">
<LazyLoadWrapper>
<HowItWorks messages={messages} />
<HowItWorks />
</LazyLoadWrapper>
</section>
{/* Demo Video Section */}
<section className="mb-12" aria-label="Product Demo">
<LazyLoadWrapper>
<h2 className="text-3xl font-bold mb-6 text-center">
{messages.text.home.h2_demo}
{t("demo.title")}
</h2>
<p className="text-center mb-6 text-muted-foreground">
{messages.text.home.h2P_demo}
{t("demo.description")}
</p>
<YouTubePlayer videoId={youtube_videoId} />
<div className="mt-4 text-center">
<p className="mb-3 text-foreground">
{messages.text.home.watch_tips}
</p>
<p className="mb-3 text-foreground">{t("demo.watchTip")}</p>
<a
className="flex justify-center gap-4 text-blue-500 hover:underline transition-colors"
href={`https://www.youtube.com/watch?v=${youtube_videoId}`}
target="_blank"
rel="noopener noreferrer"
>
{messages.text.home.youtube_tips}
{t("demo.youtube")}
</a>
<a
className="flex justify-center gap-4 text-blue-500 hover:underline transition-colors"
@@ -73,7 +66,7 @@ export default function HomeClient({ messages, lang }: PageContentProps) {
target="_blank"
rel="noopener noreferrer"
>
{messages.text.home.bilibili_tips}
{t("demo.bilibili")}
</a>
</div>
</LazyLoadWrapper>
@@ -81,27 +74,19 @@ export default function HomeClient({ messages, lang }: PageContentProps) {
{/* System Architecture Section */}
<section aria-label="System Architecture">
<LazyLoadWrapper>
<SystemDiagram messages={messages} />
<SystemDiagram />
</LazyLoadWrapper>
</section>
{/* Key Features */}
<section aria-label="Key Features">
<LazyLoadWrapper>
<KeyFeatures
messages={messages}
isInToolPage
titleClassName="text-2xl md:text-3xl"
/>
<KeyFeatures isInToolPage titleClassName="text-2xl md:text-3xl" />
</LazyLoadWrapper>
</section>
{/* FAQ Section */}
<section aria-label="Frequently Asked Questions">
<LazyLoadWrapper>
<FAQSection
messages={messages}
isInToolPage
titleClassName="text-2xl md:text-3xl"
/>
<FAQSection isInToolPage titleClassName="text-2xl md:text-3xl" />
</LazyLoadWrapper>
</section>
</main>
+18 -17
View File
@@ -1,38 +1,39 @@
import type { Messages } from "@/types/messages";
"use client";
interface AboutContentProps {
messages: Messages;
lang: string;
}
import { useLocale, useTranslations } from "next-intl";
export default function AboutContent() {
const aboutT = useTranslations("text.about");
const privacyT = useTranslations("text.privacy");
const termsT = useTranslations("text.terms");
const helpT = useTranslations("text.help");
const lang = useLocale();
export default function AboutContent({ messages, lang }: AboutContentProps) {
return (
<div className="container mx-auto p-6">
<h1 className="text-3xl font-bold text-center mb-6">
{messages.text.about.h1}
</h1>
<p className="text-lg mb-4">{messages.text.about.P1}</p>
<p className="text-lg mb-4">{messages.text.about.P2}</p>
<p className="text-lg mb-4">{messages.text.about.P3}</p>
<p className="text-lg mb-4">{messages.text.about.P4}</p>
<p className="text-lg mb-4">{messages.text.about.P5}</p>
<h1 className="text-3xl font-bold text-center mb-6">{aboutT("h1")}</h1>
<p className="text-lg mb-4">{aboutT("paragraphs.0")}</p>
<p className="text-lg mb-4">{aboutT("paragraphs.1")}</p>
<p className="text-lg mb-4">{aboutT("paragraphs.2")}</p>
<p className="text-lg mb-4">{aboutT("paragraphs.3")}</p>
<p className="text-lg mb-4">{aboutT("paragraphs.4")}</p>
<ul className="list-disc pl-6">
<li>
<a
href={`/${lang}/privacy`}
className="text-blue-500 hover:underline"
>
{messages.text.privacy.PrivacyPolicy_dis}
{privacyT("policyLabel")}
</a>
</li>
<li>
<a href={`/${lang}/terms`} className="text-blue-500 hover:underline">
{messages.text.terms.TermsOfUse_dis}
{termsT("useLabel")}
</a>
</li>
<li>
<a href={`/${lang}/help`} className="text-blue-500 hover:underline">
{messages.text.help.Help_dis}
{helpT("label")}
</a>
</li>
</ul>
+10 -14
View File
@@ -1,21 +1,23 @@
import { getDictionary } from "@/lib/dictionary";
import AboutContent from "./AboutContent";
import { Metadata } from "next";
import { supportedLocales } from "@/constants/i18n-config";
import { getMessages } from "next-intl/server";
import { supportedLocales, type Locale } from "@/constants/i18n-config";
import type { Messages } from "@/types/messages";
export async function generateMetadata({
params,
}: {
params: { lang: string };
}): Promise<Metadata> {
const messages = await getDictionary(params.lang);
const lang = params.lang as Locale;
const messages = (await getMessages({ locale: lang })) as Messages;
return {
title: messages.meta.about.title,
description: messages.meta.about.description,
metadataBase: new URL("https://www.privydrop.app"),
alternates: {
canonical: `/${params.lang}/about`,
canonical: `/${lang}/about`,
languages: Object.fromEntries(
supportedLocales.map((lang) => [lang, `/${lang}/about`])
),
@@ -23,20 +25,14 @@ export async function generateMetadata({
openGraph: {
title: messages.meta.about.title,
description: messages.meta.about.description,
url: `https://www.privydrop.app/${params.lang}/about`,
url: `https://www.privydrop.app/${lang}/about`,
siteName: "PrivyDrop",
locale: params.lang,
locale: lang,
type: "website",
},
};
}
export default async function About({
params: { lang },
}: {
params: { lang: string };
}) {
const messages = await getDictionary(lang);
return <AboutContent messages={messages} lang={lang} />;
export default function About() {
return <AboutContent />;
}
+11 -8
View File
@@ -1,25 +1,28 @@
// app/[lang]/blog/[slug]/metadata.ts
import { Metadata } from "next";
import { getMessages } from "next-intl/server";
import { getPostBySlug } from "@/lib/blog";
import { generateMetadata as generateBlogMetadata } from "../metadata";
import { getDictionary } from "@/lib/dictionary";
import { supportedLocales } from "@/constants/i18n-config";
import type { Messages } from "@/types/messages";
import type { Locale } from "@/constants/i18n-config";
export async function generateMetadata({
params,
}: {
params: { slug: string; lang: string };
}): Promise<Metadata> {
const post = await getPostBySlug(params.slug, params.lang);
const lang = params.lang as Locale;
const post = await getPostBySlug(params.slug, lang);
if (!post) {
//blog not found
// Call the generateMetadata function of the blog homepage and pass in the parameters
return generateBlogMetadata({ params: { lang: params.lang } });
return generateBlogMetadata({ params: { lang } });
}
const messages = await getDictionary(params.lang);
const blogWord = messages.text.Header.Blog_dis;
const messages = (await getMessages({ locale: lang })) as Messages;
const blogWord = messages.text.navigation.blog;
const blogCap = blogWord.charAt(0).toUpperCase() + blogWord.slice(1);
return {
@@ -30,7 +33,7 @@ export async function generateMetadata({
)}, secure file sharing, p2p transfer, privacy`,
metadataBase: new URL("https://www.privydrop.app"),
alternates: {
canonical: `/${params.lang}/blog/${params.slug}`,
canonical: `/${lang}/blog/${params.slug}`,
languages: Object.fromEntries(
supportedLocales.map((l) => [l, `/${l}/blog/${params.slug}`])
),
@@ -38,9 +41,9 @@ export async function generateMetadata({
openGraph: {
title: post.frontmatter.title,
description: post.frontmatter.description,
url: `https://www.privydrop.app/${params.lang}/blog/${params.slug}`,
url: `https://www.privydrop.app/${lang}/blog/${params.slug}`,
siteName: "PrivyDrop",
locale: params.lang,
locale: lang,
type: "article",
publishedTime: post.frontmatter.date,
modifiedTime: post.frontmatter.date,
+12 -9
View File
@@ -1,5 +1,6 @@
//Article detail page
import { MDXRemote } from "next-mdx-remote/rsc";
import { getMessages } from "next-intl/server";
import { getPostBySlug } from "@/lib/blog";
import * as React from "react";
import { mdxOptions } from "@/lib/mdx-config";
@@ -13,7 +14,8 @@ import {
buildBreadcrumbJsonLd,
getSiteUrl,
} from "@/lib/seo/jsonld";
import { getDictionary } from "@/lib/dictionary";
import type { Messages } from "@/types/messages";
import type { Locale } from "@/constants/i18n-config";
export { generateMetadata };
@@ -22,11 +24,12 @@ export default async function BlogPost({
}: {
params: { slug: string; lang: string };
}) {
const post = await getPostBySlug(params.slug, params.lang);
const messages = await getDictionary(params.lang);
const locale = params.lang as Locale;
const post = await getPostBySlug(params.slug, locale);
const messages = (await getMessages({ locale })) as Messages;
if (!post) {
return <div>{messages.text.blog.post_not_found}</div>;
return <div>{messages.text.blog.postNotFound}</div>;
}
const siteUrl = getSiteUrl();
@@ -41,12 +44,12 @@ export default async function BlogPost({
dateModified: post.frontmatter.date,
authorName: post.frontmatter.author,
imageUrl,
inLanguage: params.lang,
inLanguage: locale,
});
const breadcrumbsLd = buildBreadcrumbJsonLd({
items: [
{ name: messages.text.Header.Home_dis, item: `${siteUrl}/${params.lang}` },
{ name: messages.text.Header.Blog_dis, item: `${siteUrl}/${params.lang}/blog` },
{ name: messages.text.navigation.home, item: `${siteUrl}/${locale}` },
{ name: messages.text.navigation.blog, item: `${siteUrl}/${locale}/blog` },
{ name: post.frontmatter.title, item: postUrl },
],
});
@@ -64,7 +67,7 @@ export default async function BlogPost({
</h1>
<div className="flex flex-wrap items-center text-muted-foreground gap-2 sm:gap-4">
<time className="text-sm">
{new Date(post.frontmatter.date).toLocaleDateString(params.lang, {
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
year: "numeric",
month: "long",
day: "numeric",
@@ -92,7 +95,7 @@ export default async function BlogPost({
/>
</div>
</article>
<TableOfContents content={post.content} title={messages.text.blog.toc_title} />
<TableOfContents content={post.content} title={messages.text.blog.tocTitle} />
</div>
</div>
);
+8 -5
View File
@@ -1,13 +1,16 @@
import { supportedLocales } from "@/constants/i18n-config";
import { Metadata } from "next";
import { getDictionary } from "@/lib/dictionary";
import { getMessages } from "next-intl/server";
import type { Messages } from "@/types/messages";
import type { Locale } from "@/constants/i18n-config";
export async function generateMetadata({
params,
}: {
params: { lang: string };
}): Promise<Metadata> {
const messages = await getDictionary(params.lang);
const lang = params.lang as Locale;
const messages = (await getMessages({ locale: lang })) as Messages;
return {
title: messages.meta.blog.title,
@@ -15,7 +18,7 @@ export async function generateMetadata({
keywords: messages.meta.blog.keywords,
metadataBase: new URL("https://www.privydrop.app"),
alternates: {
canonical: `/${params.lang}/blog`,
canonical: `/${lang}/blog`,
languages: Object.fromEntries(
supportedLocales.map((l) => [l, `/${l}/blog`])
),
@@ -23,9 +26,9 @@ export async function generateMetadata({
openGraph: {
title: messages.meta.blog.title,
description: messages.meta.blog.description,
url: `https://www.privydrop.app/${params.lang}/blog`,
url: `https://www.privydrop.app/${lang}/blog`,
siteName: "PrivyDrop",
locale: params.lang,
locale: lang,
type: "website",
},
};
+10 -7
View File
@@ -1,9 +1,11 @@
import { getAllPosts } from "@/lib/blog";
import { ArticleListItem } from "@/components/blog/ArticleListItem";
import Link from "next/link";
import { getMessages } from "next-intl/server";
import { slugifyTag } from "@/utils/tagUtils";
import { generateMetadata } from "./metadata";
import { getDictionary } from "@/lib/dictionary";
import type { Messages } from "@/types/messages";
import type { Locale } from "@/constants/i18n-config";
export { generateMetadata };
@@ -12,8 +14,9 @@ export default async function BlogPage({
}: {
params: { lang: string };
}) {
const posts = await getAllPosts(lang);
const messages = await getDictionary(lang);
const locale = lang as Locale;
const posts = await getAllPosts(locale);
const messages = (await getMessages({ locale })) as Messages;
return (
<div className="max-w-[1400px] mx-auto px-4 sm:px-6 lg:px-8 py-12">
@@ -21,14 +24,14 @@ export default async function BlogPage({
{/* Main Content */}
<main className="lg:col-span-8">
<div className="mb-12">
<h1 className="text-4xl font-bold mb-4">{messages.text.blog.list_title}</h1>
<p className="text-muted-foreground text-lg">{messages.text.blog.list_subtitle}</p>
<h1 className="text-4xl font-bold mb-4">{messages.text.blog.listTitle}</h1>
<p className="text-muted-foreground text-lg">{messages.text.blog.listSubtitle}</p>
</div>
{/* Articles List */}
<div className="space-y-12">
{posts.map((post) => (
<ArticleListItem key={post.slug} post={post} lang={lang} messages={messages} />
<ArticleListItem key={post.slug} post={post} />
))}
</div>
</main>
@@ -38,7 +41,7 @@ export default async function BlogPage({
<div className="sticky top-8">
{/* Recent Posts */}
<div className="bg-card rounded-xl shadow-lg p-8 mb-8">
<h2 className="text-xl font-bold mb-6">{messages.text.blog.recent_posts}</h2>
<h2 className="text-xl font-bold mb-6">{messages.text.blog.recentPosts}</h2>
<div className="space-y-4">
{posts.slice(0, 5).map((post) => (
<Link
+17 -14
View File
@@ -1,26 +1,28 @@
import { Metadata } from "next";
import { getMessages } from "next-intl/server";
import { getPostsByTag } from "@/lib/blog";
import { ArticleListItem } from "@/components/blog/ArticleListItem";
import { supportedLocales } from "@/constants/i18n-config";
import { supportedLocales, type Locale } from "@/constants/i18n-config";
import { unslugifyTag } from "@/utils/tagUtils";
import { getDictionary } from "@/lib/dictionary";
import type { Messages } from "@/types/messages";
export async function generateMetadata({
params: { tag, lang },
}: {
params: { tag: string; lang: string };
}): Promise<Metadata> {
const locale = lang as Locale;
const decodedTag = unslugifyTag(tag);
const messages = await getDictionary(lang);
const messages = (await getMessages({ locale })) as Messages;
// Note: metadata text kept concise and localized
return {
title: `${messages.text.blog.tag_title_prefix}: ${decodedTag} - PrivyDrop`,
description: messages.text.blog.tag_subtitle_template.replace("{tag}", decodedTag),
title: `${messages.text.blog.tagTitlePrefix}: ${decodedTag} - PrivyDrop`,
description: messages.text.blog.tagSubtitleTemplate.replace("{tag}", decodedTag),
keywords: `${decodedTag}, blog, privydrop`,
metadataBase: new URL("https://www.privydrop.app"),
alternates: {
canonical: `/${lang}/blog/tag/${encodeURIComponent(tag)}`,
canonical: `/${locale}/blog/tag/${encodeURIComponent(tag)}`,
languages: Object.fromEntries(
supportedLocales.map((l) => [l, `/${l}/blog/tag/${encodeURIComponent(tag)}`])
),
@@ -28,9 +30,9 @@ export async function generateMetadata({
openGraph: {
title: `${decodedTag} - PrivyDrop`,
description: `Articles tagged: ${decodedTag}`,
url: `https://www.privydrop.app/${lang}/blog/tag/${encodeURIComponent(tag)}`,
url: `https://www.privydrop.app/${locale}/blog/tag/${encodeURIComponent(tag)}`,
siteName: "PrivyDrop",
locale: lang,
locale,
type: "website",
},
};
@@ -40,9 +42,10 @@ export default async function TagPage({
}: {
params: { tag: string; lang: string };
}) {
const locale = lang as Locale;
const decodedTag = unslugifyTag(tag);
const posts = await getPostsByTag(decodedTag, lang);
const messages = await getDictionary(lang);
const posts = await getPostsByTag(decodedTag, locale);
const messages = (await getMessages({ locale })) as Messages;
return (
<div className="max-w-[1400px] mx-auto px-4 sm:px-6 lg:px-8 py-12">
@@ -50,9 +53,9 @@ export default async function TagPage({
{/* Main Content */}
<main className="lg:col-span-8">
<div className="mb-12">
<h1 className="text-4xl font-bold mb-4">{messages.text.blog.tag_title_prefix}: {decodedTag}</h1>
<h1 className="text-4xl font-bold mb-4">{messages.text.blog.tagTitlePrefix}: {decodedTag}</h1>
<p className="text-muted-foreground text-lg">
{messages.text.blog.tag_subtitle_template.replace("{tag}", decodedTag)}
{messages.text.blog.tagSubtitleTemplate.replace("{tag}", decodedTag)}
</p>
</div>
@@ -60,10 +63,10 @@ export default async function TagPage({
<div className="space-y-12">
{posts.length > 0 ? (
posts.map((post) => (
<ArticleListItem key={post.slug} post={post} lang={lang} messages={messages} />
<ArticleListItem key={post.slug} post={post} />
))
) : (
<p>{messages.text.blog.tag_empty}</p>
<p>{messages.text.blog.tagEmpty}</p>
)}
</div>
</main>
+13 -20
View File
@@ -1,7 +1,8 @@
import FAQSection from "@/components/web/FAQSection";
import type { Metadata } from "next";
import { getDictionary } from "@/lib/dictionary";
import { supportedLocales } from "@/constants/i18n-config";
import { getMessages } from "next-intl/server";
import type { Messages } from "@/types/messages";
import { supportedLocales, type Locale } from "@/constants/i18n-config";
import JsonLd from "@/components/seo/JsonLd";
import { buildFaqJsonLd } from "@/lib/seo/jsonld";
@@ -10,7 +11,8 @@ export async function generateMetadata({
}: {
params: { lang: string };
}): Promise<Metadata> {
const messages = await getDictionary(params.lang);
const lang = params.lang as Locale;
const messages = (await getMessages({ locale: lang })) as Messages;
return {
title: messages.meta.faq.title,
@@ -26,9 +28,9 @@ export async function generateMetadata({
openGraph: {
title: messages.meta.faq.title,
description: messages.meta.faq.description,
url: `https://www.privydrop.app/${params.lang}/faq`,
url: `https://www.privydrop.app/${lang}/faq`,
siteName: "PrivyDrop",
locale: params.lang,
locale: lang,
type: "website",
},
};
@@ -39,26 +41,17 @@ export default async function FAQ({
}: {
params: { lang: string };
}) {
const messages = await getDictionary(lang);
const faqsData = (messages as any).text.faqs as Record<string, string>;
const questionKeys = Object.keys(faqsData).filter((k) => k.startsWith("question_"));
const faqs = questionKeys
.map((qKey) => {
const idx = qKey.split("_")[1];
const aKey = `answer_${idx}`;
const q = faqsData[qKey];
const a = faqsData[aKey];
if (q && a) return { question: q, answer: a };
return null;
})
.filter(Boolean) as { question: string; answer: string }[];
const locale = lang as Locale;
const messages = (await getMessages({ locale })) as Messages;
const faqItems = (messages.text.faq.items ?? []) as { question: string; answer: string }[];
const faqs = faqItems.filter((item) => item.question && item.answer);
const faqLd = buildFaqJsonLd({ inLanguage: lang, faqs });
const faqLd = buildFaqJsonLd({ inLanguage: locale, faqs });
return (
<>
<JsonLd id="faq-ld" data={faqLd} />
<FAQSection messages={messages} />
<FAQSection />
</>
);
}
+9 -12
View File
@@ -1,14 +1,16 @@
import KeyFeatures from "@/components/web/KeyFeatures";
import type { Metadata } from "next";
import { getDictionary } from "@/lib/dictionary";
import { supportedLocales } from "@/constants/i18n-config";
import { getMessages } from "next-intl/server";
import { supportedLocales, type Locale } from "@/constants/i18n-config";
import type { Messages } from "@/types/messages";
export async function generateMetadata({
params,
}: {
params: { lang: string };
}): Promise<Metadata> {
const messages = await getDictionary(params.lang);
const lang = params.lang as Locale;
const messages = (await getMessages({ locale: lang })) as Messages;
return {
title: messages.meta.features.title,
@@ -24,19 +26,14 @@ export async function generateMetadata({
openGraph: {
title: messages.meta.features.title,
description: messages.meta.features.description,
url: `https://www.privydrop.app/${params.lang}/features`,
url: `https://www.privydrop.app/${lang}/features`,
siteName: "PrivyDrop",
locale: params.lang,
locale: lang,
type: "website",
},
};
}
export default async function Features({
params: { lang },
}: {
params: { lang: string };
}) {
const messages = await getDictionary(lang);
return <KeyFeatures messages={messages} />;
export default function Features() {
return <KeyFeatures />;
}
+21 -18
View File
@@ -1,28 +1,31 @@
import type { Messages } from "@/types/messages";
"use client";
interface HelpContentProps {
messages: Messages;
lang: string;
}
import { useLocale, useTranslations } from "next-intl";
export default function HelpContent() {
const helpT = useTranslations("text.help");
const aboutT = useTranslations("text.about");
const termsT = useTranslations("text.terms");
const privacyT = useTranslations("text.privacy");
const lang = useLocale();
export default function HelpContent({ messages, lang }: HelpContentProps) {
return (
<div className="container mx-auto py-12">
<h1 className="text-4xl font-bold mb-6">{messages.text.help.h1}</h1>
<p className="text-lg mb-4">{messages.text.help.h1_P}</p>
<h2 className="text-2xl font-bold mb-4">{messages.text.help.h2_1}</h2>
<h1 className="text-4xl font-bold mb-6">{helpT("h1")}</h1>
<p className="text-lg mb-4">{helpT("h1Paragraph")}</p>
<h2 className="text-2xl font-bold mb-4">{helpT("sections.contactUs")}</h2>
<p className="text-lg mb-4">
{messages.text.help.h2_1_P1}{" "}
{helpT("sections.contactUsParagraph1")}{" "}
<a
href="mailto:david.vision66@gmail.com"
className="text-blue-500 hover:underline"
>
david.vision66@gmail.com
</a>
{messages.text.help.h2_1_P2}
{helpT("sections.contactUsParagraph2")}
</p>
<h2 className="text-2xl font-bold mb-4">{messages.text.help.h2_2}</h2>
<p className="text-lg mb-4">{messages.text.help.h2_2_P}</p>
<h2 className="text-2xl font-bold mb-4">{helpT("sections.socialMedia")}</h2>
<p className="text-lg mb-4">{helpT("sections.socialMediaParagraph")}</p>
<ul className="list-disc pl-6">
<li>
<a
@@ -36,17 +39,17 @@ export default function HelpContent({ messages, lang }: HelpContentProps) {
<li><a href="https://www.linkedin.com/company/PrivyDrop" className="text-blue-500 hover:underline">LinkedIn</a></li> */}
</ul>
<h2 className="text-2xl font-bold mb-4">{messages.text.help.h2_3}</h2>
<p className="text-lg mb-4">{messages.text.help.h2_3_P}</p>
<h2 className="text-2xl font-bold mb-4">{helpT("sections.additionalResources")}</h2>
<p className="text-lg mb-4">{helpT("sections.additionalResourcesParagraph")}</p>
<ul className="list-disc pl-6">
<li>
<a href={`/${lang}/about`} className="text-blue-500 hover:underline">
{messages.text.about.h1}
{aboutT("h1")}
</a>
</li>
<li>
<a href={`/${lang}/terms`} className="text-blue-500 hover:underline">
{messages.text.terms.TermsOfUse_dis}
{termsT("useLabel")}
</a>
</li>
<li>
@@ -54,7 +57,7 @@ export default function HelpContent({ messages, lang }: HelpContentProps) {
href={`/${lang}/privacy`}
className="text-blue-500 hover:underline"
>
{messages.text.privacy.PrivacyPolicy_dis}
{privacyT("policyLabel")}
</a>
</li>
</ul>
+9 -12
View File
@@ -1,14 +1,16 @@
import { getDictionary } from "@/lib/dictionary";
import HelpContent from "./HelpContent";
import { Metadata } from "next";
import { supportedLocales } from "@/constants/i18n-config";
import { getMessages } from "next-intl/server";
import { supportedLocales, type Locale } from "@/constants/i18n-config";
import type { Messages } from "@/types/messages";
export async function generateMetadata({
params,
}: {
params: { lang: string };
}): Promise<Metadata> {
const messages = await getDictionary(params.lang);
const lang = params.lang as Locale;
const messages = (await getMessages({ locale: lang })) as Messages;
return {
title: messages.meta.help.title,
@@ -23,18 +25,13 @@ export async function generateMetadata({
openGraph: {
title: messages.meta.help.title,
description: messages.meta.help.description,
url: `https://www.privydrop.app/${params.lang}/help`,
url: `https://www.privydrop.app/${lang}/help`,
siteName: "PrivyDrop",
locale: params.lang,
locale: lang,
type: "website",
},
};
}
export default async function Help({
params: { lang },
}: {
params: { lang: string };
}) {
const messages = await getDictionary(lang);
return <HelpContent messages={messages} lang={lang} />;
export default function Help() {
return <HelpContent />;
}
+14 -4
View File
@@ -1,8 +1,11 @@
import "./globals.css";
import { hasLocale, NextIntlClientProvider } from "next-intl";
import { getMessages, setRequestLocale } from "next-intl/server";
import { notFound } from "next/navigation";
import Header from "@/components/web/Header";
import Footer from "@/components/web/Footer";
import { ThemeProvider } from "@/components/web/theme-provider";
import { getDictionary } from "@/lib/dictionary";
import { routing } from "@/i18n/routing";
import JsonLd from "@/components/seo/JsonLd";
import {
absoluteUrl,
@@ -18,7 +21,12 @@ export default async function RootLayout({
children: React.ReactNode;
params: { lang: string };
}>) {
const messages = await getDictionary(lang);
if (!hasLocale(routing.locales, lang)) {
notFound();
}
setRequestLocale(lang);
const messages = await getMessages();
const siteUrl = getSiteUrl();
const logoUrl = absoluteUrl("/logo.png", siteUrl);
const orgJson = buildOrganizationJsonLd({
@@ -47,9 +55,11 @@ export default async function RootLayout({
disableTransitionOnChange
storageKey="theme-preference"
>
<Header messages={messages} lang={lang} />
<NextIntlClientProvider locale={lang} messages={messages}>
<Header />
<div className="flex-1">{children}</div>
<Footer messages={messages} lang={lang} />
<Footer />
</NextIntlClientProvider>
</ThemeProvider>
</body>
</html>
+12 -9
View File
@@ -1,7 +1,8 @@
import HomeClient from "./HomeClient";
import { getDictionary } from "@/lib/dictionary";
import { Metadata } from "next";
import { supportedLocales } from "@/constants/i18n-config";
import { getMessages } from "next-intl/server";
import { supportedLocales, type Locale } from "@/constants/i18n-config";
import type { Messages } from "@/types/messages";
import JsonLd from "@/components/seo/JsonLd";
import { buildWebAppJsonLd, getSiteUrl, absoluteUrl } from "@/lib/seo/jsonld";
@@ -10,7 +11,8 @@ export async function generateMetadata({
}: {
params: { lang: string };
}): Promise<Metadata> {
const messages = await getDictionary(params.lang);
const lang = params.lang as Locale;
const messages = (await getMessages({ locale: lang })) as Messages;
return {
title: messages.meta.home.title,
@@ -18,7 +20,7 @@ export async function generateMetadata({
keywords: messages.meta.home.keywords,
metadataBase: new URL("https://www.privydrop.app"),
alternates: {
canonical: `/${params.lang}`,
canonical: `/${lang}`,
languages: Object.fromEntries(
supportedLocales.map((lang) => [lang, `/${lang}`])
),
@@ -27,9 +29,9 @@ export async function generateMetadata({
openGraph: {
title: messages.meta.home.title,
description: messages.meta.home.description,
url: `https://www.privydrop.app/${params.lang}`,
url: `https://www.privydrop.app/${lang}`,
siteName: "PrivyDrop",
locale: params.lang,
locale: lang,
type: "website",
},
};
@@ -40,7 +42,8 @@ export default async function Home({
}: {
params: { lang: string };
}) {
const messages = await getDictionary(lang);
const locale = lang as Locale;
const messages = (await getMessages({ locale })) as Messages;
const siteUrl = getSiteUrl();
const webAppLd = buildWebAppJsonLd({
siteUrl,
@@ -52,7 +55,7 @@ export default async function Home({
"Open-source web-based AirDrop alternative",
],
description: messages.meta.home.description,
inLanguage: lang,
inLanguage: locale,
imageUrl: absoluteUrl("/logo.png", siteUrl),
applicationCategory: "UtilityApplication",
operatingSystem: "Web Browser",
@@ -61,7 +64,7 @@ export default async function Home({
return (
<>
<JsonLd id="home-ld" data={webAppLd} />
<HomeClient messages={messages} lang={lang} />
<HomeClient />
</>
);
}
+17 -19
View File
@@ -1,27 +1,25 @@
import type { Messages } from "@/types/messages";
"use client";
interface PageContentProps {
messages: Messages;
}
import { useTranslations } from "next-intl";
export default function PrivacyContent() {
const t = useTranslations("text.privacy");
export default function PrivacyContent({ messages }: PageContentProps) {
return (
<div className="container mx-auto p-6">
<h1 className="text-3xl font-bold text-center mb-6">
{messages.text.privacy.h1}
</h1>
<p className="text-lg mb-4">{messages.text.privacy.h1_P}</p>
<h2 className="text-2xl font-bold mb-4">{messages.text.privacy.h2_1}</h2>
<p className="text-lg mb-4">{messages.text.privacy.h2_1_P}</p>
<h2 className="text-2xl font-bold mb-4">{messages.text.privacy.h2_2}</h2>
<p className="text-lg mb-4">{messages.text.privacy.h2_2_P}</p>
<h2 className="text-2xl font-bold mb-4">{messages.text.privacy.h2_3}</h2>
<p className="text-lg mb-4">{messages.text.privacy.h2_3_P}</p>
<h2 className="text-2xl font-bold mb-4">{messages.text.privacy.h2_4}</h2>
<p className="text-lg mb-4">{messages.text.privacy.h2_4_P}</p>
<h2 className="text-2xl font-bold mb-4">{messages.text.privacy.h2_5}</h2>
<h1 className="text-3xl font-bold text-center mb-6">{t("h1")}</h1>
<p className="text-lg mb-4">{t("h1Paragraph")}</p>
<h2 className="text-2xl font-bold mb-4">{t("sections.informationCollection")}</h2>
<p className="text-lg mb-4">{t("sections.informationCollectionParagraph")}</p>
<h2 className="text-2xl font-bold mb-4">{t("sections.dataStorage")}</h2>
<p className="text-lg mb-4">{t("sections.dataStorageParagraph")}</p>
<h2 className="text-2xl font-bold mb-4">{t("sections.thirdPartyServices")}</h2>
<p className="text-lg mb-4">{t("sections.thirdPartyServicesParagraph")}</p>
<h2 className="text-2xl font-bold mb-4">{t("sections.amendments")}</h2>
<p className="text-lg mb-4">{t("sections.amendmentsParagraph")}</p>
<h2 className="text-2xl font-bold mb-4">{t("sections.contactUs")}</h2>
<p className="text-lg mb-4">
{messages.text.privacy.h2_5_P}{" "}
{t("sections.contactUsParagraph")}{" "}
<a
href="mailto:david.vision66@gmail.com"
className="text-blue-500 hover:underline"
+9 -12
View File
@@ -1,14 +1,16 @@
import type { Metadata } from "next";
import { getDictionary } from "@/lib/dictionary";
import { getMessages } from "next-intl/server";
import PrivacyContent from "./PrivacyContent";
import { supportedLocales } from "@/constants/i18n-config";
import { supportedLocales, type Locale } from "@/constants/i18n-config";
import type { Messages } from "@/types/messages";
export async function generateMetadata({
params,
}: {
params: { lang: string };
}): Promise<Metadata> {
const messages = await getDictionary(params.lang);
const lang = params.lang as Locale;
const messages = (await getMessages({ locale: lang })) as Messages;
return {
title: messages.meta.privacy.title,
@@ -23,18 +25,13 @@ export async function generateMetadata({
openGraph: {
title: messages.meta.privacy.title,
description: messages.meta.privacy.description,
url: `https://www.privydrop.app/${params.lang}/privacy`,
url: `https://www.privydrop.app/${lang}/privacy`,
siteName: "PrivyDrop",
locale: params.lang,
locale: lang,
type: "website",
},
};
}
export default async function Privacy({
params: { lang },
}: {
params: { lang: string };
}) {
const messages = await getDictionary(lang);
return <PrivacyContent messages={messages} />;
export default function Privacy() {
return <PrivacyContent />;
}
+17 -19
View File
@@ -1,26 +1,24 @@
import type { Messages } from "@/types/messages";
"use client";
interface PageContentProps {
messages: Messages;
}
import { useTranslations } from "next-intl";
export default function TermsContent() {
const t = useTranslations("text.terms");
export default function TermsContent({ messages }: PageContentProps) {
return (
<div className="container mx-auto p-6">
<h1 className="text-3xl font-bold text-center mb-6">
{messages.text.terms.h1}
</h1>
<p className="text-lg mb-4">{messages.text.terms.h1_P}</p>
<h2 className="text-2xl font-bold mb-4">{messages.text.terms.h2_1}</h2>
<p className="text-lg mb-4">{messages.text.terms.h2_1_P}</p>
<h2 className="text-2xl font-bold mb-4">{messages.text.terms.h2_2}</h2>
<p className="text-lg mb-4">{messages.text.terms.h2_2_P}</p>
<h2 className="text-2xl font-bold mb-4">{messages.text.terms.h2_3}</h2>
<p className="text-lg mb-4">{messages.text.terms.h2_3_P}</p>
<h2 className="text-2xl font-bold mb-4">{messages.text.terms.h2_4}</h2>
<p className="text-lg mb-4">{messages.text.terms.h2_4_P}</p>
<h2 className="text-2xl font-bold mb-4">{messages.text.terms.h2_5}</h2>
<p className="text-lg mb-4">{messages.text.terms.h2_5_P}</p>
<h1 className="text-3xl font-bold text-center mb-6">{t("h1")}</h1>
<p className="text-lg mb-4">{t("h1Paragraph")}</p>
<h2 className="text-2xl font-bold mb-4">{t("sections.useOfService")}</h2>
<p className="text-lg mb-4">{t("sections.useOfServiceParagraph")}</p>
<h2 className="text-2xl font-bold mb-4">{t("sections.dataPrivacy")}</h2>
<p className="text-lg mb-4">{t("sections.dataPrivacyParagraph")}</p>
<h2 className="text-2xl font-bold mb-4">{t("sections.acceptableUse")}</h2>
<p className="text-lg mb-4">{t("sections.acceptableUseParagraph")}</p>
<h2 className="text-2xl font-bold mb-4">{t("sections.liability")}</h2>
<p className="text-lg mb-4">{t("sections.liabilityParagraph")}</p>
<h2 className="text-2xl font-bold mb-4">{t("sections.changes")}</h2>
<p className="text-lg mb-4">{t("sections.changesParagraph")}</p>
</div>
);
}
+9 -12
View File
@@ -1,14 +1,16 @@
import { getDictionary } from "@/lib/dictionary";
import TermsContent from "./TermsContent";
import { Metadata } from "next";
import { supportedLocales } from "@/constants/i18n-config";
import { getMessages } from "next-intl/server";
import { supportedLocales, type Locale } from "@/constants/i18n-config";
import type { Messages } from "@/types/messages";
export async function generateMetadata({
params,
}: {
params: { lang: string };
}): Promise<Metadata> {
const messages = await getDictionary(params.lang);
const lang = params.lang as Locale;
const messages = (await getMessages({ locale: lang })) as Messages;
return {
title: messages.meta.terms.title,
@@ -23,18 +25,13 @@ export async function generateMetadata({
openGraph: {
title: messages.meta.terms.title,
description: messages.meta.terms.description,
url: `https://www.privydrop.app/${params.lang}/terms`,
url: `https://www.privydrop.app/${lang}/terms`,
siteName: "PrivyDrop",
locale: params.lang,
locale: lang,
type: "website",
},
};
}
export default async function TermsOfUse({
params: { lang },
}: {
params: { lang: string };
}) {
const messages = await getDictionary(lang);
return <TermsContent messages={messages} />;
export default function TermsOfUse() {
return <TermsContent />;
}
+43 -24
View File
@@ -1,5 +1,6 @@
"use client";
import React, { useRef, useCallback, useEffect, useMemo } from "react";
import { useMessages, useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import useRichTextToPlainText from "../hooks/useRichTextToPlainText";
import QRCodeComponent from "./ClipboardApp/ShareCard";
@@ -15,21 +16,29 @@ import FullScreenDropZone from "./ClipboardApp/FullScreenDropZone";
import { traverseFileTree } from "@/lib/fileUtils";
import { useFileTransferStore } from "@/stores/fileTransferStore";
import { getCachedId } from "@/lib/roomIdCache";
import { useConnectionFeedback } from "@/hooks/useConnectionFeedback";
import type { Messages } from "@/types/messages";
const ClipboardApp = () => {
const messages = useMessages() as Messages;
const tTabs = useTranslations("text.clipboard.tabs");
const tTitles = useTranslations("text.clipboard.titles");
const { shareMessage, retrieveMessage, putMessageInMs } =
useClipboardAppMessages();
const roomText = messages.text.clipboard;
const fileTransferText = messages.text.clipboard.messages;
const connectionText = messages.text.clipboard.rtc;
const dragCounter = useRef(0);
const retrieveJoinRoomBtnRef = useRef<HTMLButtonElement>(null);
const { messages, isLoadingMessages } = usePageSetup({
usePageSetup({
setRetrieveRoomId: useFileTransferStore.getState().setRetrieveRoomIdInput,
setActiveTab: useFileTransferStore.getState().setActiveTab,
retrieveJoinRoomBtnRef,
});
// 从 store 中获取状态
// Get state from store
const {
activeTab,
isDragging,
@@ -51,7 +60,7 @@ const ClipboardApp = () => {
addFilesToSend,
removeFileToSend,
handleDownloadFile,
} = useFileTransferHandler({ messages, putMessageInMs });
} = useFileTransferHandler({ text: fileTransferText, putMessageInMs });
// Simplified WebRTC connection initialization
const {
@@ -60,7 +69,6 @@ const ClipboardApp = () => {
setReceiverDirectoryHandle,
getReceiverSaveType,
} = useWebRTCConnection({
messages,
putMessageInMs,
});
@@ -72,7 +80,28 @@ const ClipboardApp = () => {
handleLeaveReceiverRoom,
handleLeaveSenderRoom,
} = useRoomManager({
messages,
text: {
join: roomText.join,
messages: {
waiting: roomText.messages.waiting,
confirmLeave: roomText.messages.confirmLeave,
leaveSuccess: roomText.messages.leaveSuccess,
fetchRoomError: roomText.messages.fetchRoomError,
generateShareLinkError: roomText.messages.generateShareLinkError,
leaveRoomError: roomText.messages.leaveRoomError,
validateRoomError: roomText.messages.validateRoomError,
resetSenderStateError: roomText.messages.resetSenderStateError,
},
roomCheck: roomText.roomCheck,
status: {
roomEmpty: roomText.status.roomEmpty,
receiverCanAccept: roomText.status.receiverCanAccept,
onlyOne: roomText.status.onlyOne,
peopleCount: roomText.status.peopleCount,
connected: roomText.status.connected,
leftRoom: roomText.status.leftRoom,
},
},
putMessageInMs,
});
@@ -174,20 +203,12 @@ const ClipboardApp = () => {
joinRoom,
]);
if (isLoadingMessages || !messages) {
return (
<div className="container mx-auto px-4 py-8 w-full md:max-w-4xl">
<div className="min-h-[1000px] w-full bg-gray-200/50 dark:bg-gray-800/50 rounded-lg animate-pulse">
{" "}
Loading Editor...{" "}
</div>
</div>
);
}
// Connection feedback observer (Hook)
useConnectionFeedback({ text: connectionText, putMessageInMs });
return (
<div className="w-full mx-auto px-1 sm:px-1 py-3 sm:py-8 md:max-w-4xl md:container">
<FullScreenDropZone isDragging={isDragging} messages={messages} />
<FullScreenDropZone isDragging={isDragging} />
<div className="flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-2 mb-4">
<Button
variant={activeTab === "send" ? "default" : "outline"}
@@ -197,7 +218,7 @@ const ClipboardApp = () => {
id="send-tab"
aria-selected={activeTab === "send"}
>
{messages.text.ClipboardApp.html.senderTab}
{tTabs("send")}
</Button>
<Button
variant={activeTab === "retrieve" ? "default" : "outline"}
@@ -207,21 +228,20 @@ const ClipboardApp = () => {
id="retrieve-tab"
aria-selected={activeTab === "retrieve"}
>
{messages.text.ClipboardApp.html.retrieveTab}
{tTabs("retrieve")}
</Button>
</div>
<Card className="border-4 sm:border-8 shadow-md">
<CardHeader className="px-3 sm:px-6 py-3 sm:py-6">
<CardTitle className="text-lg sm:text-xl">
{activeTab === "send"
? messages.text.ClipboardApp.html.shareTitle_dis
: messages.text.ClipboardApp.html.retrieveTitle_dis}
? tTitles("share")
: tTitles("retrieve")}
</CardTitle>
</CardHeader>
<CardContent className="px-3 sm:px-6">
{activeTab === "send" ? (
<SendTabPanel
messages={messages}
updateShareContent={updateShareContent}
addFilesToSend={addFilesToSend}
removeFileToSend={removeFileToSend}
@@ -236,7 +256,6 @@ const ClipboardApp = () => {
/>
) : (
<RetrieveTabPanel
messages={messages}
putMessageInMs={putMessageInMs}
setRetrieveRoomIdInput={setRetrieveRoomIdInput}
joinRoom={joinRoom}
@@ -253,11 +272,11 @@ const ClipboardApp = () => {
)}
</CardContent>
</Card>
{activeTab === "send" && shareLink && messages && (
{activeTab === "send" && shareLink && (
<Card className="border-2 sm:border-4 shadow-md mt-2 sm:mt-4">
<CardHeader className="pb-3 sm:pb-6">
<CardTitle className="text-base sm:text-lg">
{messages.text.ClipboardApp.html.RetrieveMethodTitle}
{tTitles("retrieveMethod")}
</CardTitle>
</CardHeader>
<CardContent className="pt-0 px-3 sm:px-6">
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import Tooltip from "@/components/Tooltip";
import type { Messages } from "@/types/messages";
import { getCachedId, setCachedId } from "@/lib/roomIdCache";
/**
@@ -42,7 +42,6 @@ import { getCachedId, setCachedId } from "@/lib/roomIdCache";
*/
type Props = {
messages: Messages;
getInputValue: () => string;
setInputValue: (val: string) => void;
putMessageInMs: (
@@ -69,7 +68,6 @@ type Props = {
};
export default function CachedIdActionButton({
messages,
getInputValue,
setInputValue,
putMessageInMs,
@@ -82,6 +80,7 @@ export default function CachedIdActionButton({
onUseCached,
disabled = false,
}: Props) {
const tCachedId = useTranslations("text.clipboard.cachedId");
const [hasCachedId, setHasCachedId] = useState<boolean>(false);
const [showSaveOverride, setShowSaveOverride] = useState<boolean>(false);
const clickCountRef = useRef(0);
@@ -120,7 +119,7 @@ export default function CachedIdActionButton({
clearTimeout(saveTimerRef.current);
saveTimerRef.current = null;
}
putMessageInMs(messages.text.ClipboardApp.saveId_success, isShareEnd);
putMessageInMs(tCachedId("saveSuccess"), isShareEnd);
}
return;
}
@@ -165,7 +164,7 @@ export default function CachedIdActionButton({
getInputValue,
setInputValue,
putMessageInMs,
messages.text.ClipboardApp.saveId_success,
tCachedId,
isShareEnd,
dblClickWindowMs,
saveModeDurationMs,
@@ -176,8 +175,8 @@ export default function CachedIdActionButton({
<Tooltip
content={
isSaveMode
? messages.text.ClipboardApp.html.saveId_tips
: messages.text.ClipboardApp.html.useCachedId_tips
? tCachedId("saveTip")
: tCachedId("useTip")
}
>
<span className="inline-block">
@@ -191,8 +190,8 @@ export default function CachedIdActionButton({
}
>
{isSaveMode
? messages.text.ClipboardApp.html.saveId_dis
: messages.text.ClipboardApp.html.useCachedId_dis}
? tCachedId("save")
: tCachedId("use")}
</Button>
</span>
</Tooltip>
@@ -1,4 +1,5 @@
import React, { useState, useEffect, useRef } from "react";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { Download, Trash2 } from "lucide-react";
import { Tooltip } from "@/components/Tooltip";
@@ -7,9 +8,6 @@ import { formatFileSize, generateFileId } from "@/lib/fileUtils";
import { AutoPopupDialog } from "@/components/common/AutoPopupDialog";
import { FileMeta, CustomFile, Progress } from "@/types/webrtc";
import FileTransferButton from "./FileTransferButton";
import { getDictionary } from "@/lib/dictionary";
import { useLocale } from "@/hooks/useLocale";
import type { Messages } from "@/types/messages";
import { useFileTransferStore } from "@/stores/fileTransferStore";
import { supportsAutoDownload } from "@/lib/browserUtils";
import { postLogToBackend } from "@/app/config/api";
@@ -68,8 +66,7 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
saveType,
largeFileThreshold = 500 * 1024 * 1024, // 500MB default
}) => {
const locale = useLocale();
const [messages, setMessages] = useState<Messages | null>(null);
const t = useTranslations("text.fileList");
// Get the cleaning method of the store
const { clearSendProgress, clearReceiveProgress } = useFileTransferStore();
@@ -111,12 +108,6 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
}
};
useEffect(() => {
getDictionary(locale)
.then((dict) => setMessages(dict))
.catch((error) => console.error("Failed to load messages:", error));
}, [locale]);
useEffect(() => {
// Separate single files and folders
const tempSingleFiles: FileMeta[] = [];
@@ -234,6 +225,9 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
fileProgresses,
showFinished,
activeTransfers,
mode,
clearSendProgress,
clearReceiveProgress,
folders,
singleFiles,
]);
@@ -295,7 +289,15 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
// Update the last status
prevShowFinishedRef.current[item.fileId] = currentShowFinished;
});
}, [showFinished, singleFiles, folders, saveType, onDownload]);
}, [
showFinished,
singleFiles,
folders,
saveType,
onDownload,
activeTransfers,
fileProgresses,
]);
//Actions corresponding to each file - progress, download, delete
const renderItemActions = (item: FileMeta) => {
@@ -309,25 +311,20 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
// Get download count
const downloadCount = downloadCounts[item.fileId] || 0;
if (messages === null) {
return <div>Loading...</div>;
}
return (
<div className="flex flex-col sm:flex-row sm:items-center gap-2 min-w-0 flex-shrink-0">
{progress && progress.progress < 1 ? ( //Show progress or completed
<div className="w-full sm:w-auto">
<TransferProgress
message={
mode === "sender"
? messages.text.FileListDisplay.sending_dis
: messages.text.FileListDisplay.receiving_dis
mode === "sender" ? t("sending") : t("receiving")
}
progress={progress}
/>
</div>
) : showCompletion ? (
<span className="text-sm text-green-500 whitespace-nowrap">
{messages.text.FileListDisplay.finish_dis}
{t("finished")}
</span>
) : null}
@@ -351,7 +348,7 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
{/* display download Num*/}
{mode === "sender" && (
<span className="text-xs sm:text-sm whitespace-nowrap">
{messages.text.FileListDisplay.downloadNum_dis}: {downloadCount}
{t("downloadCount")}: {downloadCount}
</span>
)}
{mode === "sender" && onDelete && (
@@ -369,9 +366,7 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
className="text-xs sm:text-sm px-2 sm:px-3"
>
<Trash2 className="h-3 w-3 sm:h-4 sm:w-4 sm:mr-2" />
<span className="hidden sm:inline">
{messages.text.FileListDisplay.delete_dis}
</span>
<span className="hidden sm:inline">{t("delete")}</span>
</Button>
)}
</div>
@@ -384,7 +379,7 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
const formatSize = formatFileSize(item.size);
const tooltipContent = isFolder
? `${formatFolderTips(
messages!.text.FileListDisplay.folder_tips_template,
t("folderSummary"),
item.name,
item.fileCount || 0,
formatSize
@@ -407,7 +402,7 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
<span className="text-xs sm:text-sm text-muted-foreground">
{isFolder
? `${formatFolderDis(
messages!.text.FileListDisplay.folder_dis_template,
t("folderInline"),
item.fileCount || 0,
formatSize
)}`
@@ -421,9 +416,6 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
</div>
);
};
if (messages === null) {
return <div>Loading...</div>;
}
return (
<>
{(singleFiles.length > 0 || folders.length > 0) && (
@@ -433,17 +425,13 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
<div className="mb-2">
<AutoPopupDialog
storageKey="Choose-location-popup-shown"
title={messages.text.FileListDisplay.PopupDialog_title}
description={
messages.text.FileListDisplay.PopupDialog_description
}
title={t("saveDialog.title")}
description={t("saveDialog.description")}
condition={() => needPickLocation}
/>
{/* Regular reminder to select the save directory */}
<div className="flex items-center">
<p className="text-red-500 mb-2">
{messages.text.FileListDisplay.chooseSavePath_tips}
</p>
<p className="text-red-500 mb-2">{t("saveDialog.tip")}</p>
{onLocationPick && (
<Button
onClick={async () => {
@@ -454,7 +442,7 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
size="sm"
className="mr-2 text-red-500"
>
{messages.text.FileListDisplay.chooseSavePath_dis}
{t("saveDialog.button")}
</Button>
)}
</div>
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from "react";
import React from "react";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { Download } from "lucide-react";
import {
@@ -7,9 +8,6 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { getDictionary } from "@/lib/dictionary";
import { useLocale } from "@/hooks/useLocale";
import type { Messages } from "@/types/messages";
interface FileTransferButtonProps {
onRequest: () => void;
@@ -28,30 +26,20 @@ const FileTransferButton = ({
isSavedToDisk,
isPendingSave = false,
}: FileTransferButtonProps) => {
const locale = useLocale();
const [messages, setMessages] = useState<Messages | null>(null);
const t = useTranslations("text.fileTransfer");
// Button status judgment - 待保存状态时按钮应该可点击
const isDisabled =
isCurrentFileTransferring ||
isSavedToDisk ||
(isOtherFileTransferring && !isPendingSave);
useEffect(() => {
getDictionary(locale)
.then((dict) => setMessages(dict))
.catch((error) => console.error("Failed to load messages:", error));
}, [locale]);
// Display different tooltips based on status
const getTooltipContent = () => {
if (isSavedToDisk)
return messages!.text.FileTransferButton.SavedToDisk_tips;
if (isCurrentFileTransferring)
return messages!.text.FileTransferButton.CurrentFileTransferring_tips;
if (isPendingSave)
return messages!.text.FileTransferButton.PendingSave_tips;
if (isOtherFileTransferring)
return messages!.text.FileTransferButton.OtherFileTransferring_tips;
return messages!.text.FileTransferButton.download_tips;
if (isSavedToDisk) return t("savedToDisk");
if (isCurrentFileTransferring) return t("currentTransferring");
if (isPendingSave) return t("pendingSave");
if (isOtherFileTransferring) return t("otherTransferring");
return t("download");
};
// Set different button styles and class names based on status
@@ -87,9 +75,6 @@ const FileTransferButton = ({
};
const buttonStyles = getButtonStyles();
if (messages === null) {
return <div>Loading...</div>;
}
return (
<TooltipProvider delayDuration={100}>
<Tooltip>
@@ -108,12 +93,12 @@ const FileTransferButton = ({
}`}
/>
{isSavedToDisk
? messages.text.FileTransferButton.Saved_dis
? t("saved")
: isPendingSave
? messages.text.FileTransferButton.Save_dis
? t("pendingSave")
: isOtherFileTransferring
? messages.text.FileTransferButton.Waiting_dis
: messages.text.FileTransferButton.Download_dis}
? t("waiting")
: t("download")}
</Button>
</span>
</TooltipTrigger>
@@ -5,6 +5,7 @@ import React, {
useRef,
useCallback,
} from "react";
import { useTranslations } from "next-intl";
import { Input } from "@/components/ui/input";
import { Upload } from "lucide-react";
import { FileMeta, CustomFile } from "@/types/webrtc";
@@ -23,11 +24,6 @@ declare module "@/components/ui/input" {
}
}
import { getDictionary } from "@/lib/dictionary";
import { useLocale } from "@/hooks/useLocale";
import type { Messages } from "@/types/messages";
import { en } from "@/constants/messages/en"; // Import English dictionary as default
function formatFileChosen(
template: string,
fileNum: number,
@@ -45,28 +41,17 @@ interface FileUploadHandlerProps {
const FileUploadHandler: React.FC<FileUploadHandlerProps> = ({
onFilePicked,
}) => {
const locale = useLocale();
const [messages, setMessages] = useState<Messages>(en); // Use English dictionary as initial value
const t = useTranslations("text.fileUpload");
const folderInputRef = useRef<HTMLInputElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
// File selector -- message prompt
const [fileText, setFileText] = useState<string>(
en.text.fileUploadHandler.NoFileChosen_tips
);
const [fileText, setFileText] = useState<string>(t("noFileChosen"));
const [isModalOpen, setIsModalOpen] = useState(false);
useEffect(() => {
if (locale !== "en") {
// Only load other language packs if not English
getDictionary(locale)
.then((dict) => {
setMessages(dict);
setFileText(dict.text.fileUploadHandler.NoFileChosen_tips);
})
.catch((error) => console.error("Failed to load messages:", error));
}
}, [locale]);
setFileText(t("noFileChosen"));
}, [t]);
const handleFileChange = useCallback(
(newFiles: CustomFile[]) => {
@@ -77,16 +62,13 @@ const FileUploadHandler: React.FC<FileUploadHandlerProps> = ({
const folderNum = newFiles.filter((file) => file.folderName).length;
const choose_dis = formatFileChosen(
messages!.text.fileUploadHandler.fileChosen_tips_template,
t("fileChosen"),
fileNum,
folderNum
);
setFileText(choose_dis);
setTimeout(
() => setFileText(messages!.text.fileUploadHandler.NoFileChosen_tips),
2000
);
setTimeout(() => setFileText(t("noFileChosen")), 2000);
// Reset the file input
if (fileInputRef.current) {
fileInputRef.current.value = "";
@@ -95,7 +77,7 @@ const FileUploadHandler: React.FC<FileUploadHandlerProps> = ({
folderInputRef.current.value = "";
}
},
[messages, onFilePicked]
[t, onFilePicked]
);
// Click to upload file processing
@@ -154,9 +136,6 @@ const FileUploadHandler: React.FC<FileUploadHandlerProps> = ({
const handleSelectFolder = () => {
folderInputRef.current?.click();
};
if (messages === null) {
return <div>Loading...</div>;
}
return (
<>
<div
@@ -164,7 +143,7 @@ const FileUploadHandler: React.FC<FileUploadHandlerProps> = ({
onClick={handleZoneClick}
>
<p className="text-sm text-muted-foreground mb-4">
{messages.text.fileUploadHandler.chooseFileTips}
{t("chooseTip")}
</p>
<Upload className="h-12 w-12 mx-auto mb-4 text-primary" />
<p className="text-sm text-muted-foreground">{fileText}</p>
@@ -193,10 +172,10 @@ const FileUploadHandler: React.FC<FileUploadHandlerProps> = ({
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle className="text-xl font-semibold">
{messages.text.fileUploadHandler.chosenDiagTitle}
{t("dialog.title")}
</DialogTitle>
<DialogDescription className="mt-2 text-muted-foreground">
{messages.text.fileUploadHandler.chosenDiagDescription}
{t("dialog.description")}
</DialogDescription>
</DialogHeader>
<div className="flex justify-center gap-4 mt-6">
@@ -204,13 +183,13 @@ const FileUploadHandler: React.FC<FileUploadHandlerProps> = ({
onClick={handleSelectFile}
className="px-4 py-2 rounded transition-colors bg-primary text-primary-foreground hover:bg-primary/90"
>
{messages.text.fileUploadHandler.SelectFile_dis}
{t("dialog.selectFile")}
</button>
<button
onClick={handleSelectFolder}
className="px-4 py-2 rounded transition-colors bg-secondary text-secondary-foreground hover:bg-secondary/80"
>
{messages.text.fileUploadHandler.SelectFolder_dis}
{t("dialog.selectFolder")}
</button>
</div>
</DialogContent>
@@ -1,24 +1,20 @@
import React from "react";
import { useTranslations } from "next-intl";
import { Upload } from "lucide-react";
import type { Messages } from "@/types/messages";
interface FullScreenDropZoneProps {
isDragging: boolean;
messages: Messages;
}
const FullScreenDropZone: React.FC<FullScreenDropZoneProps> = ({
isDragging,
messages,
}) => {
const FullScreenDropZone: React.FC<FullScreenDropZoneProps> = ({ isDragging }) => {
const t = useTranslations("text.fileUpload");
if (!isDragging) return null;
return (
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-black bg-opacity-70 backdrop-blur-sm">
<Upload className="h-24 w-24 text-white animate-bounce" />
<p className="mt-6 text-2xl font-bold text-white">
{messages.text.fileUploadHandler.dragTips}
</p>
<p className="mt-6 text-2xl font-bold text-white">{t("dragTip")}</p>
</div>
);
};
@@ -1,4 +1,5 @@
import React, { useCallback, useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
@@ -7,13 +8,11 @@ import {
} from "@/components/common/clipboard_btn";
import CachedIdActionButton from "@/components/ClipboardApp/CachedIdActionButton";
import FileListDisplay from "@/components/ClipboardApp/FileListDisplay";
import type { Messages } from "@/types/messages";
import type { FileMeta } from "@/types/webrtc";
import { useFileTransferStore } from "@/stores/fileTransferStore";
interface RetrieveTabPanelProps {
messages: Messages;
putMessageInMs: (
message: string,
isShareEnd?: boolean,
@@ -36,7 +35,6 @@ interface RetrieveTabPanelProps {
}
export function RetrieveTabPanel({
messages,
putMessageInMs,
setRetrieveRoomIdInput,
joinRoom,
@@ -50,6 +48,12 @@ export function RetrieveTabPanel({
retrieveMessage,
handleLeaveRoom,
}: RetrieveTabPanelProps) {
const tActions = useTranslations("text.clipboard.actions");
const tPlaceholders = useTranslations("text.clipboard.placeholders");
const tStatus = useTranslations("text.clipboard.status");
const tSaveLocation = useTranslations("text.clipboard.saveLocation");
const tCommon = useTranslations("text.common");
const t = useTranslations("text.clipboard");
// Get the status from the store
const {
retrieveRoomStatusText,
@@ -62,25 +66,24 @@ export function RetrieveTabPanel({
} = useFileTransferStore();
const onLocationPick = useCallback(async (): Promise<boolean> => {
if (!messages) return false; // Should not happen if panel is rendered
if (!window.showDirectoryPicker) {
putMessageInMs(messages.text.ClipboardApp.pickSaveUnsupported, false);
putMessageInMs(tSaveLocation("unsupported"), false);
return false;
}
if (!window.confirm(messages.text.ClipboardApp.pickSaveMsg)) return false;
if (!window.confirm(tSaveLocation("pickMsg"))) return false;
try {
const directoryHandle = await window.showDirectoryPicker();
await setReceiverDirectoryHandle(directoryHandle);
putMessageInMs(messages.text.ClipboardApp.pickSaveSuccess, false);
putMessageInMs(tSaveLocation("success"), false);
return true;
} catch (err: any) {
if (err.name !== "AbortError") {
console.error("Failed to set up folder receive:", err);
putMessageInMs(messages.text.ClipboardApp.pickSaveError, false);
putMessageInMs(tSaveLocation("error"), false);
}
return false;
}
}, [messages, putMessageInMs, setReceiverDirectoryHandle]);
}, [tSaveLocation, putMessageInMs, setReceiverDirectoryHandle]);
const handleFileRequestFromPanel = useCallback(
(meta: FileMeta) => {
@@ -101,20 +104,19 @@ export function RetrieveTabPanel({
<div className="mb-3 text-sm text-muted-foreground">
{retrieveRoomStatusText ||
(isReceiverInRoom
? messages.text.ClipboardApp.roomStatus.connected_dis
: messages.text.ClipboardApp.roomStatus.receiverEmptyMsg)}
? tStatus("connected")
: tStatus("receiverCanAccept"))}
</div>
<div className="space-y-3 mb-4">
{/* Room ID input section */}
<div className="space-y-2">
<div className="flex flex-col sm:flex-row gap-2">
<ReadClipboardButton
title={messages.text.ClipboardApp.html.readClipboard_dis}
title={tActions("readClipboard")}
onRead={setRetrieveRoomIdInput}
/>
{/* Save/Use Cached ID Button placed after Paste button */}
<CachedIdActionButton
messages={messages}
getInputValue={() => retrieveRoomIdInput}
setInputValue={setRetrieveRoomIdInput}
putMessageInMs={putMessageInMs}
@@ -124,9 +126,7 @@ export function RetrieveTabPanel({
aria-label="Retrieve Room ID"
value={retrieveRoomIdInput}
onChange={(e) => setRetrieveRoomIdInput(e.target.value)}
placeholder={
messages.text.ClipboardApp.html.retrieveRoomId_placeholder
}
placeholder={tPlaceholders("roomId")}
className="flex-1 min-w-0"
/>
</div>
@@ -140,7 +140,7 @@ export function RetrieveTabPanel({
ref={retrieveJoinRoomBtnRef}
disabled={isReceiverInRoom || !retrieveRoomIdInput.trim()}
>
{messages.text.ClipboardApp.html.joinRoom_dis}
{tCommon("buttons.joinRoom")}
</Button>
<Button
variant={isAnyFileTransferring ? "destructive" : "outline"}
@@ -149,8 +149,8 @@ export function RetrieveTabPanel({
className="w-full sm:w-auto px-4 order-2"
>
{isAnyFileTransferring
? messages.text.ClipboardApp.roomStatus.leaveRoomBtn + " ⚠️"
: messages.text.ClipboardApp.roomStatus.leaveRoomBtn}
? tCommon("buttons.leaveRoom") + " ⚠️"
: tCommon("buttons.leaveRoom")}
</Button>
</div>
</div>
@@ -161,7 +161,7 @@ export function RetrieveTabPanel({
</div>
<div className="flex justify-start">
<WriteClipboardButton
title={messages.text.ClipboardApp.html.Copy_dis}
title={tCommon("buttons.copy")}
textToCopy={richTextToPlainText(retrievedContent)}
/>
</div>
@@ -1,4 +1,5 @@
import React, { useState, useEffect, useCallback } from "react";
import { useTranslations } from "next-intl";
import dynamic from "next/dynamic";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
@@ -10,7 +11,6 @@ import {
import { FileUploadHandler } from "@/components/ClipboardApp/FileUploadHandler";
import FileListDisplay from "@/components/ClipboardApp/FileListDisplay";
import AnimatedButton from "@/components/ui/AnimatedButton";
import type { Messages } from "@/types/messages";
import type { CustomFile, FileMeta } from "@/types/webrtc";
import { useFileTransferStore } from "@/stores/fileTransferStore";
@@ -29,7 +29,6 @@ const RichTextEditor = dynamic(
);
interface SendTabPanelProps {
messages: Messages;
updateShareContent: (content: string) => void;
addFilesToSend: (files: CustomFile[]) => void;
removeFileToSend: (meta: FileMeta) => void;
@@ -48,7 +47,6 @@ interface SendTabPanelProps {
}
export function SendTabPanel({
messages,
updateShareContent,
addFilesToSend,
removeFileToSend,
@@ -61,6 +59,12 @@ export function SendTabPanel({
handleLeaveSenderRoom,
putMessageInMs,
}: SendTabPanelProps) {
const tActions = useTranslations("text.clipboard.actions");
const tPlaceholders = useTranslations("text.clipboard.placeholders");
const tGenerateId = useTranslations("text.clipboard.generateId");
const tTitles = useTranslations("text.clipboard.titles");
const tStatus = useTranslations("text.clipboard.status");
const tCommon = useTranslations("text.common");
// Get the status from the store
const {
shareContent,
@@ -139,17 +143,17 @@ export function SendTabPanel({
<div className="mb-3 text-sm text-muted-foreground">
{shareRoomStatusText ||
(isSenderInRoom
? messages.text.ClipboardApp.roomStatus.onlyOneMsg
: messages.text.ClipboardApp.roomStatus.senderEmptyMsg)}
? tStatus("onlyOne")
: tStatus("roomEmpty"))}
</div>
<RichTextEditor value={shareContent} onChange={updateShareContent} />
<div className="flex flex-col sm:flex-row gap-2 my-3">
<ReadClipboardButton
title={messages.text.ClipboardApp.html.Paste_dis}
title={tCommon("buttons.paste")}
onRead={updateShareContent}
/>
<WriteClipboardButton
title={messages.text.ClipboardApp.html.Copy_dis}
title={tCommon("buttons.copy")}
textToCopy={richTextToPlainText(shareContent)}
/>
</div>
@@ -167,7 +171,7 @@ export function SendTabPanel({
{/* Room ID input section */}
<div className="space-y-2">
<p className="text-sm text-muted-foreground">
{messages.text.ClipboardApp.html.inputRoomId_tips}
{tTitles("share")}
</p>
<div className="flex flex-col sm:flex-row gap-2">
<Input
@@ -176,9 +180,7 @@ export function SendTabPanel({
onChange={handleInputChange}
onPaste={handlePaste}
className="flex-1 min-w-0"
placeholder={
messages.text.ClipboardApp.html.retrieveRoomId_placeholder
}
placeholder={tPlaceholders("roomId")}
/>
<Button
variant="outline"
@@ -187,12 +189,11 @@ export function SendTabPanel({
disabled={isSenderInRoom}
>
{isSimpleIdMode
? messages.text.ClipboardApp.html.generateRandomId_tips
: messages.text.ClipboardApp.html.generateSimpleId_tips}
? tGenerateId("random")
: tGenerateId("simple")}
</Button>
{/* Save/Use Cached ID Button in between */}
<CachedIdActionButton
messages={messages}
getInputValue={() => inputFieldValue}
setInputValue={setInputFieldValue}
putMessageInMs={putMessageInMs}
@@ -208,7 +209,7 @@ export function SendTabPanel({
onClick={() => joinRoom(true, inputFieldValue.trim())}
disabled={isSenderInRoom || !inputFieldValue.trim()}
>
{messages.text.ClipboardApp.html.joinRoom_dis}
{tCommon("buttons.joinRoom")}
</Button>
</div>
</div>
@@ -218,9 +219,7 @@ export function SendTabPanel({
<AnimatedButton
className="flex-1 order-1"
onClick={generateShareLinkAndBroadcast}
loadingText={
messages.text.ClipboardApp.html.SyncSending_loadingText
}
loadingText={tActions("syncLoading")}
disabled={
!isSenderInRoom ||
(sendFiles.length === 0 && shareContent.trim() === "") ||
@@ -228,7 +227,7 @@ export function SendTabPanel({
isAnyFileTransferring
}
>
{messages.text.ClipboardApp.html.SyncSending_dis}
{tActions("sync")}
</AnimatedButton>
<Button
variant={isAnyFileTransferring ? "destructive" : "outline"}
@@ -237,8 +236,8 @@ export function SendTabPanel({
className="w-full sm:w-auto px-4 order-2"
>
{isAnyFileTransferring
? messages.text.ClipboardApp.roomStatus.leaveRoomBtn + " ⚠️"
: messages.text.ClipboardApp.roomStatus.leaveRoomBtn}
? tCommon("buttons.leaveRoom") + " ⚠️"
: tCommon("buttons.leaveRoom")}
</Button>
</div>
</div>
+67 -77
View File
@@ -1,13 +1,10 @@
import React, { useRef, useState, useEffect } from "react";
import dynamic from "next/dynamic";
import { useTranslations } from "next-intl";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Copy, Download, Check } from "lucide-react";
import { WriteClipboardButton } from "../common/clipboard_btn";
import { getDictionary } from "@/lib/dictionary";
import { useLocale } from "@/hooks/useLocale";
import type { Messages } from "@/types/messages";
interface ShareCardProps {
RoomID: string;
shareLink: string;
@@ -22,10 +19,23 @@ const QRCodeSVG = dynamic(
}
);
const ShareCard: React.FC<ShareCardProps> = ({ RoomID, shareLink }) => {
const locale = useLocale();
const [messages, setMessages] = useState<Messages | null>(null);
const t = useTranslations("text.retrieveMethod");
const qrRef = useRef<HTMLDivElement>(null);
const [isCopied, setIsCopied] = useState<boolean>(false);
const [isRoomIdCopied, setIsRoomIdCopied] = useState<boolean>(false);
const [isUrlCopied, setIsUrlCopied] = useState<boolean>(false);
const copyRoomId = async () => {
await navigator.clipboard.writeText(RoomID);
setIsRoomIdCopied(true);
setTimeout(() => setIsRoomIdCopied(false), 2000);
};
const copyUrl = async () => {
await navigator.clipboard.writeText(shareLink);
setIsUrlCopied(true);
setTimeout(() => setIsUrlCopied(false), 2000);
};
const copyToClipboard = async () => {
if (!qrRef.current) return;
@@ -86,12 +96,6 @@ const ShareCard: React.FC<ShareCardProps> = ({ RoomID, shareLink }) => {
downloadQRCode(); // Fallback to download on any error
}
};
useEffect(() => {
getDictionary(locale)
.then((dict) => setMessages(dict))
.catch((error) => console.error("Failed to load messages:", error));
}, [locale]);
const downloadQRCode = () => {
if (!qrRef.current) return;
@@ -116,61 +120,51 @@ const ShareCard: React.FC<ShareCardProps> = ({ RoomID, shareLink }) => {
};
img.src = "data:image/svg+xml;base64," + btoa(svgData);
};
if (messages === null) {
return <div>Loading...</div>;
}
return (
<div className="bg-primary/10 p-2 sm:p-4 rounded-lg border border-primary/20">
<p className="text-primary mb-3 sm:mb-4 text-sm sm:text-base">
{messages.text.RetrieveMethod.P}
{t("intro")}
</p>
{/* Mobile-first responsive layout */}
<div className="space-y-3 sm:space-y-4">
{/* RoomID section */}
<div className="bg-card p-2 sm:p-3 rounded-lg border border-border">
<div className="space-y-2">
<p className="text-sm font-medium text-foreground">
{messages.text.RetrieveMethod.RoomId_tips}
</p>
<div className="flex flex-col sm:flex-row sm:items-center gap-2">
<code className="flex-1 bg-muted px-2 py-1 rounded text-sm font-mono break-all">
{RoomID}
</code>
<WriteClipboardButton
title={messages.text.RetrieveMethod.copyRoomId_tips}
textToCopy={RoomID}
/>
</div>
</div>
</div>
{/* URL section */}
<div className="bg-card p-2 sm:p-3 rounded-lg border border-border">
<div className="space-y-2">
<p className="text-sm font-medium text-foreground">
{messages.text.RetrieveMethod.url_tips}
</p>
<div className="bg-muted px-2 py-2 rounded text-xs sm:text-sm break-all font-mono">
{shareLink}
</div>
<div className="flex justify-start">
<WriteClipboardButton
title={messages.text.RetrieveMethod.copyUrl_tips}
textToCopy={shareLink}
/>
</div>
</div>
</div>
{/* QR Code section */}
<div className="bg-card p-2 sm:p-3 rounded-lg border border-border">
<div className="space-y-3">
<p className="text-sm font-medium text-foreground">
{messages.text.RetrieveMethod.scanQR_tips}
<div className="flex flex-col sm:flex-row sm:items-center gap-2">
<span className="text-sm font-medium">{t("roomIdTip")}</span>
<div className="flex items-center gap-2 flex-1">
<Input value={RoomID} readOnly className="font-mono text-sm" />
<Button
variant="outline"
size="sm"
onClick={copyRoomId}
title={t("copyRoomId")}
>
{isRoomIdCopied ? (
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
</div>
<div className="flex flex-col sm:flex-row sm:items-center gap-2">
<span className="text-sm font-medium">{t("urlTip")}</span>
<div className="flex items-center gap-2 flex-1">
<Input value={shareLink} readOnly className="font-mono text-sm" />
<Button
variant="outline"
size="sm"
onClick={copyUrl}
title={t("copyUrl")}
>
{isUrlCopied ? (
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
</div>
<p className="text-sm text-center text-muted-foreground pt-2">
{t("scanQr")}
</p>
{/* QR Code display area - moved up for better mobile UX */}
<div className="flex justify-center">
<div className="inline-block border-2 p-2 sm:p-4 bg-muted rounded-lg">
<div ref={qrRef}>
@@ -182,41 +176,37 @@ const ShareCard: React.FC<ShareCardProps> = ({ RoomID, shareLink }) => {
</div>
</div>
</div>
{/* QR Code action buttons */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
</div>
<div className="flex justify-center gap-2 mt-4">
<Button
onClick={copyToClipboard}
variant="outline"
size="sm"
className="w-full"
onClick={copyToClipboard}
className="flex items-center gap-2"
>
{isCopied ? (
<>
<Check className="w-4 h-4 mr-2" />
{messages.text.RetrieveMethod.Copied_dis}
<Check className="h-4 w-4" />
{t("copied")}
</>
) : (
<>
<Copy className="mr-2 h-4 w-4" />
{messages.text.RetrieveMethod.Copy_QR_dis}
<Copy className="h-4 w-4" />
{t("copyQr")}
</>
)}
</Button>
<Button
onClick={downloadQRCode}
variant="outline"
size="sm"
className="w-full"
onClick={downloadQRCode}
className="flex items-center gap-2"
>
<Download className="mr-2 h-4 w-4" />
{messages.text.RetrieveMethod.download_QR_dis}
<Download className="h-4 w-4" />
{t("downloadQr")}
</Button>
</div>
</div>
</div>
</div>
</div>
);
};
@@ -1,4 +1,4 @@
import { Link2, Image, Code } from "lucide-react";
import { Link2, Image as ImageIcon, Code } from "lucide-react";
interface InsertToolsProps {
insertLink: () => void;
@@ -25,7 +25,7 @@ export function InsertTools({
onClick={insertImage}
title="Upload image"
>
<Image className="w-3.5 h-3.5" />
<ImageIcon className="w-3.5 h-3.5" />
</button>
<button
className="p-1.5 hover:bg-accent rounded"
@@ -76,7 +76,7 @@ export const useEditorCommands = (
// Update HTML
handleChange();
},
[findStyleParent, getSelection, removeStyle]
[editorRef, findStyleParent, getSelection, handleChange]
);
// Align text
@@ -129,7 +129,7 @@ export const useEditorCommands = (
// Update HTML
handleChange();
},
[getSelection]
[editorRef, getSelection, handleChange]
);
// Set font style
@@ -218,7 +218,7 @@ export const useEditorCommands = (
handleChange();
},
[getSelection, findStyleParent, cleanupSpan]
[cleanupSpan, findStyleParent, getSelection, handleChange]
);
// Insert link
@@ -258,7 +258,7 @@ export const useEditorCommands = (
handleChange();
}
}
}, [getSelection]);
}, [getSelection, handleChange]);
// Insert image
const insertImage = useCallback(() => {
@@ -290,7 +290,7 @@ export const useEditorCommands = (
}
};
input.click();
}, [getSelection]);
}, [getSelection, handleChange]);
// Insert code block
const insertCodeBlock = useCallback(() => {
@@ -318,7 +318,7 @@ export const useEditorCommands = (
range.deleteContents();
range.insertNode(pre);
handleChange();
}, [getSelection]);
}, [getSelection, handleChange]);
return {
formatText,
+6 -5
View File
@@ -7,17 +7,18 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { usePathname, useRouter } from "next/navigation";
import { useLocale } from "next-intl";
import { usePathname, useRouter } from "@/i18n/navigation";
import { i18n, Locale, languageDisplayNames } from "@/constants/i18n-config";
const LanguageSwitcher = () => {
const locale = useLocale();
const pathname = usePathname();
const router = useRouter();
const switchLanguage = (locale: Locale) => {
const segments = pathname.split("/");
segments[1] = locale;
router.push(segments.join("/"));
const switchLanguage = (nextLocale: Locale) => {
if (nextLocale === locale) return;
router.replace(pathname, { locale: nextLocale });
};
return (
+9 -6
View File
@@ -1,15 +1,18 @@
"use client";
import { useLocale, useTranslations } from "next-intl";
import Link from "next/link";
import Image from "next/image";
import { type BlogPost } from "@/lib/blog";
import { Messages } from "@/types/messages";
interface ArticleListItemProps {
post: BlogPost;
lang: string;
messages: Messages;
}
export function ArticleListItem({ post, lang, messages }: ArticleListItemProps) {
export function ArticleListItem({ post }: ArticleListItemProps) {
const t = useTranslations("text.blog");
const lang = useLocale();
return (
<article className="bg-card rounded-xl shadow-lg hover:shadow-xl transition-shadow overflow-hidden">
<div className="relative h-80 w-full">
@@ -59,7 +62,7 @@ export function ArticleListItem({ post, lang, messages }: ArticleListItemProps)
href={`/${lang}/blog/${post.slug}`}
className="text-primary hover:text-primary/80 font-medium inline-flex items-center text-lg"
>
{messages.text.blog.read_more}
{t("readMore")}
<svg
className="w-5 h-5 ml-2"
viewBox="0 0 24 24"
@@ -77,7 +80,7 @@ export function ArticleListItem({ post, lang, messages }: ArticleListItemProps)
<div className="flex items-center gap-3">
<span className="text-sm">
{messages.text.blog.by} <span className="font-bold">{post.frontmatter.author}</span>
{t("by")} <span className="font-bold">{post.frontmatter.author}</span>
</span>
</div>
</div>
+4 -1
View File
@@ -1,6 +1,7 @@
"use client";
import React, { useState, useEffect } from "react";
import { Play } from "lucide-react";
import Image from "next/image";
interface YouTubePlayerProps {
videoId: string;
@@ -21,9 +22,11 @@ const YouTubePlayer: React.FC<YouTubePlayerProps> = ({
<div className="relative pb-[56.25%]">
{!isPlaying ? (
<div className="absolute top-0 left-0 w-full h-full">
<img
<Image
src={localThumbnail}
alt="Video preview"
fill
sizes="(max-width: 1024px) 100vw, 1024px"
className="w-full h-full object-cover"
/>
<button
+7 -4
View File
@@ -1,5 +1,6 @@
import React from "react";
import { Clipboard, FileText, Check } from "lucide-react";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { useClipboardActions } from "@/hooks/useClipboardActions";
@@ -17,16 +18,17 @@ export const WriteClipboardButton: React.FC<WriteClipboardButtonProps> = ({
title,
textToCopy,
}) => {
const tButtons = useTranslations("text.common.buttons");
const { copyText, isCopied, isLoadingMessages, clipboardMessages, error } =
useClipboardActions();
const buttonText = title || clipboardMessages.copyError || "Copy"; // Fallback title
const buttonText = title || tButtons("copy");
if (isLoadingMessages && !clipboardMessages.copiedSuccess) {
// Only show loading if messages truly not ready
return (
<Button variant="outline" disabled>
{clipboardMessages.loading || "Loading..."}
{clipboardMessages.loading}
</Button>
);
}
@@ -57,6 +59,7 @@ export const ReadClipboardButton: React.FC<ReadClipboardButtonProps> = ({
title,
onRead,
}) => {
const tButtons = useTranslations("text.common.buttons");
const {
readClipboard,
isPasted,
@@ -70,13 +73,13 @@ export const ReadClipboardButton: React.FC<ReadClipboardButtonProps> = ({
onRead(text); // Pass null if read failed or no suitable content
};
const buttonText = title || clipboardMessages.readError || "Paste"; // Fallback title
const buttonText = title || tButtons("paste");
if (isLoadingMessages && !clipboardMessages.pastedSuccess) {
// Only show loading if messages truly not ready
return (
<Button variant="outline" disabled>
{clipboardMessages.loading || "Loading..."}
{clipboardMessages.loading}
</Button>
);
}
+20 -39
View File
@@ -1,45 +1,20 @@
"use client";
import { useTranslations } from "next-intl";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import type { Messages } from "@/types/messages";
interface FAQMessage {
[key: string]: string;
}
interface FAQ {
question: string;
answer: string;
}
const generateFAQs = (messages: { text: { faqs: FAQMessage } }): FAQ[] => {
const faqs: FAQ[] = [];
const faqsData = messages.text.faqs;
// Get the total number of questions (by finding keys starting with question_)
const questionKeys = Object.keys(faqsData).filter((key) =>
key.startsWith("question_")
);
// Automatically generate FAQ array based on the number of questions
questionKeys.forEach((qKey) => {
const index = qKey.split("_")[1]; // Get the numeric index
const aKey = `answer_${index}`;
if (faqsData[aKey]) {
// Ensure the corresponding answer exists
faqs.push({
question: faqsData[qKey],
answer: faqsData[aKey],
});
}
});
return faqs;
};
// Static FAQ count based on messages structure (indices 0-13)
const FAQ_COUNT = 14;
interface FAQSectionProps {
isInToolPage?: boolean; // Whether it is in the tool page (e.g. homepage)
@@ -47,7 +22,6 @@ interface FAQSectionProps {
showTitle?: boolean; // Whether to display the title
titleClassName?: string; // Title style class
lang?: string;
messages: Messages;
}
// Control the level and style of the title through props, so it can be used on other pages as well as on a standalone page
export default function FAQSection({
@@ -55,9 +29,20 @@ export default function FAQSection({
className = "",
showTitle = true,
titleClassName = "",
messages,
}: FAQSectionProps) {
const faqs = generateFAQs(messages);
const t = useTranslations("text.faq");
// Generate FAQs using useTranslations with dynamic keys
// We use type assertion since next-intl doesn't support dynamic keys in type system
const faqs: FAQ[] = [];
for (let i = 0; i < FAQ_COUNT; i++) {
const question = t(`items.${i}.question` as never);
const answer = t(`items.${i}.answer` as never);
// Only add if both question and answer exist (not fallback keys)
if (question && answer && !question.startsWith("items.")) {
faqs.push({ question, answer });
}
}
// Set default styles for different scenarios
const containerClasses = `container mx-auto px-4 py-8 ${className}`;
@@ -68,13 +53,9 @@ export default function FAQSection({
<div className={containerClasses}>
{showTitle &&
(isInToolPage ? (
<h2 className={`text-3xl ${titleClasses}`}>
{messages.text.faqs.FAQ_dis}
</h2>
<h2 className={`text-3xl ${titleClasses}`}>{t("title")}</h2>
) : (
<h1 className={`text-4xl ${titleClasses}`}>
{messages.text.faqs.FAQ_dis}
</h1>
<h1 className={`text-4xl ${titleClasses}`}>{t("title")}</h1>
))}
<Accordion type="single" collapsible className="w-full">
{faqs.map((faq, index) => (
+10 -11
View File
@@ -1,14 +1,14 @@
"use client";
import Link from "next/link";
import Image from "next/image";
import { Messages } from "@/types/messages";
import { useLocale, useTranslations } from "next-intl";
import { languageDisplayNames } from "@/constants/i18n-config";
interface FooterProps {
messages: Messages;
lang: string;
}
export function Footer() {
const t = useTranslations("text.footer");
const lang = useLocale();
export function Footer({ messages, lang }: FooterProps) {
return (
<footer className="bg-background border-t mt-auto">
<div className="container mx-auto px-4 py-6">
@@ -24,8 +24,7 @@ export function Footer({ messages, lang }: FooterProps) {
priority
/>
<p className="text-sm text-muted-foreground">
&copy; {new Date().getFullYear()}{" "}
{messages.text.Footer.CopyrightNotice}
&copy; {new Date().getFullYear()} {t("copyright")}
</p>
</div>
@@ -38,7 +37,7 @@ export function Footer({ messages, lang }: FooterProps) {
href={`/${lang}/terms`}
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
{messages.text.Footer.Terms_dis}
{t("terms")}
</Link>
</li>
<li>
@@ -46,14 +45,14 @@ export function Footer({ messages, lang }: FooterProps) {
href={`/${lang}/privacy`}
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
{messages.text.Footer.Privacy_dis}
{t("privacy")}
</Link>
</li>
{/* Entry for supported languages */}
<li>
<span className="text-sm text-muted-foreground font-bold">
{messages.text.Footer.SupportedLanguages}:
{t("supportedLanguages")}:
</span>
</li>
{Object.entries(languageDisplayNames).map(([code, name]) => (
+12 -18
View File
@@ -1,5 +1,6 @@
"use client";
import { useState } from "react";
import { useLocale, useTranslations } from "next-intl";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
@@ -7,35 +8,28 @@ import { Button } from "@/components/ui/button";
import Image from "next/image";
import { Menu, X, Github } from "lucide-react";
import LanguageSwitcher from "@/components/LanguageSwitcher";
import { Messages } from "@/types/messages";
import ThemeToggle from "@/components/web/ThemeToggle";
/**
* Props interface for the Header component
*/
interface HeaderProps {
messages: Messages;
lang: string;
}
/**
* Header component providing navigation, language switching, and GitHub link
* Features responsive design with mobile menu support
*/
const Header = ({ messages, lang }: HeaderProps) => {
const Header = () => {
const t = useTranslations("text.navigation");
const lang = useLocale();
const pathname = usePathname();
const [isOpen, setIsOpen] = useState(false);
// Configuration for navigation items
const navItems = [
{ href: `/${lang}`, label: messages.text.Header.Home_dis },
{ href: `/${lang}/features`, label: messages.text.Header.Features_dis },
{ href: `/${lang}/blog`, label: messages.text.Header.Blog_dis },
{ href: `/${lang}/about`, label: messages.text.Header.About_dis },
{ href: `/${lang}/help`, label: messages.text.Header.Help_dis },
{ href: `/${lang}/faq`, label: messages.text.Header.FAQ_dis },
{ href: `/${lang}/terms`, label: messages.text.Header.Terms_dis },
{ href: `/${lang}/privacy`, label: messages.text.Header.Privacy_dis },
{ href: `/${lang}`, label: t("home") },
{ href: `/${lang}/features`, label: t("features") },
{ href: `/${lang}/blog`, label: t("blog") },
{ href: `/${lang}/about`, label: t("about") },
{ href: `/${lang}/help`, label: t("help") },
{ href: `/${lang}/faq`, label: t("faq") },
{ href: `/${lang}/terms`, label: t("terms") },
{ href: `/${lang}/privacy`, label: t("privacy") },
];
// GitHub repository URL
+14 -14
View File
@@ -1,28 +1,28 @@
"use client";
import React from "react";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import Image from "next/image";
import type { Messages } from "@/types/messages";
interface PageContentProps {
messages: Messages;
}
export default function HowItWorks() {
const t = useTranslations("text.howItWorks");
export default function HowItWorks({ messages }: PageContentProps) {
const steps = [
{
number: 1,
title: messages!.text.HowItWorks.step1_title,
description: messages!.text.HowItWorks.step1_description,
title: t("step1Title"),
description: t("step1Description"),
},
{
number: 2,
title: messages!.text.HowItWorks.step2_title,
description: messages!.text.HowItWorks.step2_description,
title: t("step2Title"),
description: t("step2Description"),
},
{
number: 3,
title: messages!.text.HowItWorks.step3_title,
description: messages!.text.HowItWorks.step3_description,
title: t("step3Title"),
description: t("step3Description"),
},
];
@@ -31,11 +31,11 @@ export default function HowItWorks({ messages }: PageContentProps) {
{/* Header Section */}
<div className="text-center mb-16">
<h2 className="text-3xl md:text-4xl font-bold mb-6">
{messages.text.HowItWorks.h2}
{t("title")}
</h2>
<p className="text-muted-foreground mb-8">{messages.text.HowItWorks.h2_P}</p>
<p className="text-muted-foreground mb-8">{t("description")}</p>
<Button className="bg-gradient-to-r from-purple-500 to-blue-500 hover:from-purple-600 hover:to-blue-600 text-white rounded-full px-8 py-6 text-lg">
{messages.text.HowItWorks.btn_try}
{t("tryNow")}
</Button>
</div>
+19 -17
View File
@@ -1,12 +1,13 @@
"use client";
import { useTranslations } from "next-intl";
import Image from "next/image";
import type { Messages } from "@/types/messages";
interface KeyFeaturesProps {
isInToolPage?: boolean; // Whether it is in the tool page (e.g. homepage)
className?: string; // Custom style class
showTitle?: boolean; // Whether to display the title
titleClassName?: string; // Title style class
messages: Messages;
}
export default function KeyFeatures({
@@ -14,8 +15,9 @@ export default function KeyFeatures({
className = "",
showTitle = true,
titleClassName = "",
messages
}: KeyFeaturesProps) {
const t = useTranslations("text.keyFeatures");
// Set container styles
const containerClasses = `container mx-auto px-4 py-8 ${className}`;
const defaultTitleClasses = "font-semibold mb-6";
@@ -26,48 +28,48 @@ export default function KeyFeatures({
{showTitle &&
(isInToolPage ? (
<h2 className={`text-3xl ${titleClasses}`}>
{messages.text.KeyFeatures.h2}
{t("title")}
</h2>
) : (
<h1 className={`text-4xl ${titleClasses}`}>
{messages.text.KeyFeatures.h2}
{t("title")}
</h1>
))}
<div className="space-y-6">
<div>
<h3 className="text-xl font-semibold mb-2 flex items-center">
<Image src="/lock.png" alt="Icon" width={80} height={80} />
<span className="ml-6">{messages.text.KeyFeatures.h3_1}</span>
<span className="ml-6">{t("items.directSecure.title")}</span>
</h3>
<p>{messages.text.KeyFeatures.h3_1_P}</p>
<p>{t("items.directSecure.description")}</p>
</div>
<div>
<h3 className="text-xl font-semibold mb-2 flex items-center">
<Image src="/teamwork.png" alt="Icon" width={80} height={80} />
<span className="ml-6">{messages.text.KeyFeatures.h3_2}</span>
<span className="ml-6">{t("items.teamSynergy.title")}</span>
</h3>
<p>{messages.text.KeyFeatures.h3_2_P}</p>
<p>{t("items.teamSynergy.description")}</p>
</div>
<div>
<h3 className="text-xl font-semibold mb-2 flex items-center">
<Image src="/rocket.png" alt="Icon" width={80} height={80} />
<span className="ml-6">{messages.text.KeyFeatures.h3_3}</span>
<span className="ml-6">{t("items.noLimits.title")}</span>
</h3>
<p>{messages.text.KeyFeatures.h3_3_P}</p>
<p>{t("items.noLimits.description")}</p>
</div>
<div>
<h3 className="text-xl font-semibold mb-2 flex items-center">
<Image src="/fresh-air.png" alt="Icon" width={80} height={80} />
<span className="ml-6">{messages.text.KeyFeatures.h3_4}</span>
<span className="ml-6">{t("items.swift.title")}</span>
</h3>
<p>{messages.text.KeyFeatures.h3_4_P}</p>
<p>{t("items.swift.description")}</p>
</div>
<div>
<h3 className="text-xl font-semibold mb-2 flex items-center">
<Image src="/planet-earth.png" alt="Icon" width={80} height={80} />
<span className="ml-6">{messages.text.KeyFeatures.h3_5}</span>
<span className="ml-6">{t("items.greenClean.title")}</span>
</h3>
<p>{messages.text.KeyFeatures.h3_5_P}</p>
<p>{t("items.greenClean.description")}</p>
</div>
<div>
<h3 className="text-xl font-semibold mb-2 flex items-center">
@@ -77,9 +79,9 @@ export default function KeyFeatures({
width={100}
height={83}
/>
<span className="ml-6">{messages.text.KeyFeatures.h3_6}</span>
<span className="ml-6">{t("items.resumable.title")}</span>
</h3>
<p>{messages.text.KeyFeatures.h3_6_P}</p>
<p>{t("items.resumable.description")}</p>
</div>
</div>
</section>
+7 -9
View File
@@ -1,17 +1,15 @@
"use client";
import { useTranslations } from "next-intl";
import Image from "next/image";
import type { Messages } from "@/types/messages";
interface PageContentProps {
messages: Messages;
}
export default function SystemDiagram() {
const t = useTranslations("text.systemDiagram");
export default function SystemDiagram({ messages }: PageContentProps) {
return (
<section className="py-16 bg-background">
<div className="container mx-auto px-4">
<h2 className="text-3xl font-bold mb-12 text-center">
{messages.text.SystemDiagram.h2}
</h2>
<h2 className="text-3xl font-bold mb-12 text-center">{t("title")}</h2>
<Image
src="/SystemDiagram.webp"
alt="PrivyDrop system diagram: Peer-to-peer file and clipboard sharing"
@@ -20,7 +18,7 @@ export default function SystemDiagram({ messages }: PageContentProps) {
className="mx-auto mb-6"
/>
<p className="mt-8 text-center max-w-2xl mx-auto">
{messages.text.SystemDiagram.h2_P}
{t("description")}
</p>
</div>
</section>
+13 -1
View File
@@ -11,6 +11,19 @@ export default function ThemeToggle() {
useEffect(() => setMounted(true), []);
if (!mounted) {
return (
<Button
variant="ghost"
size="icon"
aria-label="Toggle theme"
disabled
>
<span className="h-5 w-5" />
</Button>
);
}
const isDark = resolvedTheme === "dark";
const toggle = () => setTheme(isDark ? "light" : "dark");
@@ -20,7 +33,6 @@ export default function ThemeToggle() {
size="icon"
aria-label="Toggle theme"
onClick={toggle}
disabled={!mounted}
>
{isDark ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
<span className="sr-only">Toggle theme</span>
+334 -322
View File
@@ -3,380 +3,392 @@ import { Messages } from "@/types/messages";
export const de: Messages = {
meta: {
home: {
title:
"PrivyDrop: Open-Source & kostenlose P2P-Datei- & Clipboard-Freigabe | Datenschutz-fokussiert",
description:
"PrivyDrop ist ein quelloffenes, kostenloses und sicheres P2P-Dateiübertragungstool ohne Größenbeschränkungen, ohne Registrierung und mit Unterstützung für fortsetzbare Übertragungen. Basierend auf WebRTC für eine Ende-zu-Ende-Verschlüsselung ist der Code transparent und vertrauenswürdig. Unterstützt den Austausch von Text, Bildern und Ordnern über Geräte hinweg, perfekt für Teamkollaboration und private Dateiübertragungen.",
keywords:
"fortsetzbare Dateiübertragung,zuverlässiger Dateiaustausch,Wiederherstellung unterbrochener Übertragungen,P2P-Datei-Fortsetzung,WebRTC fortsetzbare Übertragung,Open-Source-Dateiaustausch,P2P-Dateiübertragung,sichere Dateiübertragung,WebRTC-Dateiaustausch,private Zwischenablage,transparenter Quellcode,GitHub-Projekt,Teamkollaboration,geräteübergreifender Austausch,verschlüsselte Dateiübertragung,Dateiaustausch ohne Registrierung,unbegrenzte Dateiübertragung,Ordnersynchronisierung,mobile Dateiübertragung,sichere Kommunikation,sofortiger Dateiaustausch,private Datenübertragung",
title: "PrivyDrop: Open-Source & kostenlose P2P-Datei- & Clipboard-Freigabe | Datenschutz-fokussiert",
description: "PrivyDrop ist ein quelloffenes, kostenloses und sicheres P2P-Dateiübertragungstool ohne Größenbeschränkungen, ohne Registrierung und mit Unterstützung für fortsetzbare Übertragungen. Basierend auf WebRTC für eine Ende-zu-Ende-Verschlüsselung ist der Code transparent und vertrauenswürdig. Unterstützt den Austausch von Text, Bildern und Ordnern über Geräte hinweg, perfekt für Teamkollaboration und private Dateiübertragungen.",
keywords: "fortsetzbare Dateiübertragung,zuverlässiger Dateiaustausch,Wiederherstellung unterbrochener Übertragungen,P2P-Datei-Fortsetzung,WebRTC fortsetzbare Übertragung,Open-Source-Dateiaustausch,P2P-Dateiübertragung,sichere Dateiübertragung,WebRTC-Dateiaustausch,private Zwischenablage,transparenter Quellcode,GitHub project,Teamkollaboration,geräteübergreifend,verschlüsselte Dateiübertragung,Dateiaustausch ohne Registrierung,unbegrenzte Dateiübertragung,Ordnersynchronisierung,mobile Dateiübertragung,sichere Kommunikation,sofortiger Dateiaustausch,private Datenübertragung",
},
about: {
title: "Über PrivyDrop",
description:
"Erfahren Sie mehr über PrivyDrop, unsere Mission, einen sicheren und privaten Dateiübertragungs- und Clipboard-Sharing-Dienst bereitzustellen, und unser Engagement für den Datenschutz und den Schutz der Benutzerdaten.",
description: "Erfahren Sie mehr über PrivyDrop, unsere Mission, einen sicheren und privaten Dateiübertragungs- und Clipboard-Sharing-Dienst bereitzustellen, und unser Engagement für den Datenschutz und den Schutz der Nutzerdaten.",
},
faq: {
title: "PrivyDrop FAQ",
description:
"Finden Sie Antworten auf häufig gestellte Fragen zu PrivyDrop, einschließlich wie man Dateien sendet, Zwischenablage-Inhalte teilt und sichere und private Datenübertragungen gewährleistet.",
keywords:
"PrivyDrop FAQ,häufig gestellte Fragen,sichere Dateifreigabe FAQ,private Datenfreigabe Hilfe,Ende-zu-Ende verschlüsselte Dateiübertragung,sichere Zwischenablage-Freigabe Unterstützung,wie man PrivyDrop verwendet,Dateiübertragung FAQ,datenschutzorientierte Freigabe Fragen,PrivyDrop Fehlerbehebung",
description: "Finden Sie Antworten auf häufig gestellte Fragen zu PrivyDrop, einschließlich wie man Dateien sendet, Zwischenablage-Inhalte teilt und sichere und private Datenübertragungen gewährleistet.",
},
features: {
title:
"PrivyDrop Kernfunktionen: 6 Hauptvorteile der P2P-Dateiübertragung | Sicher & Unbegrenzt",
description:
"Entdecken Sie PrivyDrops Kernfunktionen: Ende-zu-Ende verschlüsselte P2P-Direktübertragung, unbegrenzte Dateigröße, fortsetzbare Übertragungen, Teamzusammenarbeit, umweltfreundliches Design und Open-Source-Transparenz. Erleben Sie wirklich sichere und private Dateifreigabe.",
keywords:
"P2P-Dateiübertragungsfunktionen,Ende-zu-Ende verschlüsselte Übertragung,fortsetzbare Dateiübertragung,unbegrenzte Dateifreigabe,Teamzusammenarbeit Tool,umweltfreundliche Dateiübertragung,Open-Source-Dateifreigabe,private Datenübertragung,WebRTC-Dateifreigabe,sichere Dateisynchronisation,geräteübergreifende Dateiübertragung,lokale Dateiverarbeitung",
title: "PrivyDrop Kernfunktionen: 6 Hauptvorteile der P2P-Dateiübertragung | Sicher & Unbegrenzt",
description: "Entdecken Sie PrivyDrops Kernfunktionen: Ende-zu-Ende verschlüsselte P2P-Direktübertragung, unbegrenzte Dateigröße, fortsetzbare Übertragungen, Teamzusammenarbeit, umweltfreundliches Design und Open-Source-Transparenz. Erleben Sie wirklich sichere und private Dateifreigabe.",
},
help: {
title: "PrivyDrop Hilfe und Support",
description:
"Finden Sie Informationen darüber, wie Sie den PrivyDrop-Support kontaktieren können, sowie Links zu unseren Seiten Über uns, Nutzungsbedingungen und Datenschutzrichtlinie für weitere Details über unseren Service.",
description: "Finden Sie Informationen darüber, wie Sie den PrivyDrop-Support kontaktieren können, sowie Links zu unseren Seiten Über uns, Nutzungsbedingungen und Datenschutzrichtlinie für weitere Details über unseren Service.",
},
privacy: {
title: "PrivyDrop Datenschutzrichtlinie",
description:
"Verstehen Sie, wie PrivyDrop Ihre Privatsphäre und Daten schützt, einschließlich Details zur Informationssammlung, Datenspeicherung und -sicherheit sowie unserem Engagement, Ihre Daten nicht an Dritte weiterzugeben.",
description: "Verstehen Sie, wie PrivyDrop Ihre Privatsphäre und Daten schützt, einschließlich Details zur Informationssammlung, Datenspeicherung und -sicherheit sowie unserer Verpflichtung, Ihre Daten nicht an Dritte weiterzugeben.",
},
terms: {
title: "PrivyDrop Nutzungsbedingungen",
description:
"Überprüfen Sie die Nutzungsbedingungen für PrivyDrop, einschließlich Informationen zur akzeptablen Nutzung des Dienstes, Datenschutz und -sicherheit sowie Haftungsbeschränkungen.",
description: "Überprüfen Sie die Nutzungsbedingungen für PrivyDrop, einschließlich Informationen zur akzeptablen Nutzung des Dienstes, Datenschutz und -sicherheit sowie Haftungsbeschränkungen.",
},
blog: {
title:
"PrivyDrop Blog - Private P2P-Dateifreigabe & Zusammenarbeit",
description:
"Entdecken Sie Tipps für sicheres Dateifreigeben, datenschutzorientierte Zusammenarbeit und wie man P2P und WebRTC für sicherere Übertragungen nutzt.",
keywords:
"sichere Dateifreigabe,p2p Dateiübertragung,private Zusammenarbeit,webrtc,Ende-zu-Ende-Verschlüsselung,Teamzusammenarbeit,Datenschutz-Tools",
title: "PrivyDrop Blog - Private P2P-Dateifreigabe & Zusammenarbeit",
description: "Entdecken Sie Tipps für sicheres Dateifreigeben, datenschutzorientierte Zusammenarbeit und wie man P2P und WebRTC für sicherere Übertragungen nutzt.",
},
},
text: {
Header: {
Home_dis: "Startseite",
Blog_dis: "Blog",
About_dis: "Über uns",
Help_dis: "Hilfe",
FAQ_dis: "FAQ",
Features_dis: "Kernfunktionen",
Terms_dis: "Nutzungsbedingungen",
Privacy_dis: "Datenschutz",
navigation: {
home: "Startseite",
blog: "Blog",
about: "Über uns",
help: "Hilfe",
faq: "FAQ",
features: "Kernfunktionen",
terms: "Nutzungsbedingungen",
privacy: "Datenschutz",
},
Footer: {
CopyrightNotice: "PrivyDrop. Alle Rechte vorbehalten.",
Terms_dis: "Nutzungsbedingungen",
Privacy_dis: "Datenschutzrichtlinie",
SupportedLanguages: "Unterstützte Sprachen",
footer: {
copyright: "PrivyDrop. Alle Rechte vorbehalten.",
terms: "Nutzungsbedingungen",
privacy: "Datenschutzrichtlinie",
supportedLanguages: "Unterstützte Sprachen",
},
blog: {
list_title: "Blog",
list_subtitle: "Neueste Artikel und Updates",
recent_posts: "Neueste Beiträge",
tags: "Schlagwörter",
read_more: "Weiterlesen",
by: "von",
post_not_found: "Beitrag nicht gefunden",
toc_title: "Inhaltsverzeichnis",
tag_title_prefix: "Schlagwort",
tag_subtitle_template: "Artikel mit dem Schlagwort {tag}",
tag_empty: "Keine Artikel für dieses Schlagwort gefunden.",
},
privacy: {
PrivacyPolicy_dis: "Datenschutzrichtlinie",
policyLabel: "Datenschutzrichtlinie",
h1: "PrivyDrop Datenschutzrichtlinie",
h1_P: "Bei PrivyDrop sind wir bestrebt, Ihre Privatsphäre zu schützen und Ihre persönlichen Daten zu sichern. Diese Datenschutzrichtlinie beschreibt, wie wir die Daten, die Sie bei der Nutzung unseres Dienstes bereitstellen, sammeln, verwenden und schützen.",
h2_1: "Informationssammlung",
h2_1_P:
"PrivyDrop sammelt keine personenbezogenen Daten von Benutzern. Wir verlangen keine Registrierung oder Kontoerstellung, um unseren Dienst zu nutzen. Die einzigen Informationen, die wir sammeln, sind die Raum-ID und die Datei-/Clipboard-Daten, die Sie mit anderen Benutzern teilen möchten.",
h2_2: "Datenspeicherung und -sicherheit",
h2_2_P:
"Wir speichern keine Ihrer Daten auf unseren Servern. Alle Dateiübertragungen und Clipboard-Freigaben werden mit Ende-zu-Ende-Verschlüsselung verarbeitet, um sicherzustellen, dass Ihre Informationen sicher und nur für den vorgesehenen Empfänger zugänglich bleiben. Sobald die Übertragung abgeschlossen ist, werden die Daten aus unseren Systemen entfernt.",
h2_3: "Drittanbieterdienste",
h2_3_P:
"PrivyDrop integriert keine Drittanbieterdienste oder -plattformen. Wir geben oder verkaufen Ihre Daten nicht an Dritte weiter.",
h2_4: "Änderungen der Datenschutzrichtlinie",
h2_4_P:
"Wir können diese Datenschutzrichtlinie von Zeit zu Zeit aktualisieren, um Änderungen in unseren Praktiken oder geltenden Gesetzen widerzuspiegeln. Alle Änderungen werden wirksam, sobald die aktualisierte Richtlinie auf unserer Website veröffentlicht wird. Es liegt in Ihrer Verantwortung, die Datenschutzrichtlinie regelmäßig auf Aktualisierungen zu überprüfen.",
h2_5: "Kontaktieren Sie uns",
h2_5_P:
"Wenn Sie Fragen oder Bedenken zu unseren Datenschutzpraktiken haben, kontaktieren Sie uns bitte unter",
h1Paragraph: "Bei PrivyDrop sind wir bestrebt, Ihre Privatsphäre zu schützen und Ihre persönlichen Daten zu sichern. Diese Datenschutzrichtlinie beschreibt, wie wir die Daten, die Sie bei der Nutzung unseres Dienstes bereitstellen, sammeln, verwenden und schützen.",
sections: {
informationCollection: "Informationssammlung",
informationCollectionParagraph: "PrivyDrop sammelt keine personenbezogenen Daten von Nutzern. Wir benötigen keine Registrierung oder Kontoerstellung, um unseren Dienst zu nutzen. Die einzigen Informationen, die wir sammeln, sind die Raum-ID und die Datei-/Zwischenablagedaten, die Sie mit anderen Nutzern teilen möchten.",
dataStorage: "Datenspeicherung und Sicherheit",
dataStorageParagraph: "Wir speichern keine Ihrer Daten auf unseren Servern. Alle Dateiübertragungen und das Teilen der Zwischenablage werden mittels Ende-zu-Ende-Verschlüsselung abgewickelt, um sicherzustellen, dass Ihre Informationen sicher bleiben und nur für den beabsichtigten Empfänger zugänglich sind. Sobald die Übertragung abgeschlossen ist, werden die Daten aus unseren Systemen entfernt.",
thirdPartyServices: "Dienste von Drittanbietern",
thirdPartyServicesParagraph: "PrivyDrop ist nicht in Dienste oder Plattformen von Drittanbietern integriert. Wir geben Ihre Daten nicht an Dritte weiter und verkaufen sie auch nicht.",
amendments: "Änderungen der Datenschutzrichtlinie",
amendmentsParagraph: "Wir können diese Datenschutzrichtlinie von Zeit zu Zeit aktualisieren, um Änderungen in unseren Praktiken oder geltenden Gesetzen widerzuspiegeln. Alle Änderungen werden wirksam, sobald die aktualisierte Richtlinie auf unserer Website veröffentlicht wird. Es liegt in Ihrer Verantwortung, die Datenschutzrichtlinie regelmäßig auf Aktualisierungen zu überprüfen.",
contactUs: "Kontaktieren Sie uns",
contactUsParagraph: "Wenn Sie Fragen oder Bedenken zu unseren Datenschutzpraktiken haben, kontaktieren Sie uns bitte unter",
},
},
terms: {
TermsOfUse_dis: "Nutzungsbedingungen",
useLabel: "Nutzungsbedingungen",
h1: "PrivyDrop Nutzungsbedingungen",
h1_P: "Durch die Nutzung des PrivyDrop-Dienstes erklären Sie sich mit diesen Nutzungsbedingungen einverstanden. Wenn Sie diesen Bedingungen nicht zustimmen, nutzen Sie den Dienst bitte nicht.",
h2_1: "Nutzung des Dienstes",
h2_1_P:
"PrivyDrop wird als kostenloser Dienst ohne Einschränkungen bereitgestellt.",
h2_2: "Datenschutz und -sicherheit",
h2_2_P:
"Wir nehmen den Schutz und die Sicherheit Ihrer Daten sehr ernst. Alle Dateiübertragungen und Clipboard-Freigaben werden mit Ende-zu-Ende-Verschlüsselung gesichert, und wir speichern keine Ihrer Daten auf unseren Servern. Wir können jedoch die Sicherheit Ihrer Daten während des Übertragungsprozesses nicht garantieren, und Sie nutzen den Dienst auf eigenes Risiko.",
h2_3: "Akzeptable Nutzung",
h2_3_P:
"Sie erklären sich damit einverstanden, PrivyDrop nicht für unlautere, missbräuchliche oder schädliche Zwecke zu verwenden. Dies umfasst, ist aber nicht beschränkt auf die Übertragung von illegalen, urheberrechtlich geschützten oder bösartigen Inhalten sowie die Nutzung des Dienstes, um andere zu belästigen oder zu imitieren.",
h2_4: "Haftungsbeschränkung",
h2_4_P:
"PrivyDrop wird „wie besehen“ ohne jegliche Gewährleistungen oder Garantien bereitgestellt. Wir haften nicht für direkte, indirekte oder Folgeschäden, die aus der Nutzung unseres Dienstes entstehen, einschließlich, aber nicht beschränkt auf Datenverlust, Systemausfälle oder Dienstunterbrechungen.",
h2_5: "Änderungen der Nutzungsbedingungen",
h2_5_P:
"Wir behalten uns das Recht vor, diese Nutzungsbedingungen jederzeit zu aktualisieren. Alle Änderungen werden wirksam, sobald die aktualisierten Bedingungen auf unserer Website veröffentlicht werden. Es liegt in Ihrer Verantwortung, die Nutzungsbedingungen regelmäßig auf Änderungen zu überprüfen.",
h1Paragraph: "Durch die Nutzung des PrivyDrop-Dienstes erklären Sie sich mit diesen Nutzungsbedingungen einverstanden. Wenn Sie diesen Bedingungen nicht zustimmen, nutzen Sie den Dienst bitte nicht.",
sections: {
useOfService: "Nutzung des Dienstes",
useOfServiceParagraph: "PrivyDrop wird als kostenloser Dienst ohne Einschränkungen bereitgestellt.",
dataPrivacy: "Datenschutz und Sicherheit",
dataPrivacyParagraph: "Wir nehmen den Datenschutz und die Sicherheit Ihrer Daten sehr ernst. Alle Dateiübertragungen und das Teilen der Zwischenablage sind mit einer Ende-zu-Ende-Verschlüsselung gesichert, und wir speichern keine Ihrer Daten auf unseren Servern. Wir können jedoch die Sicherheit Ihrer Daten während des Übertragungsvorgangs nicht garantieren, und Sie nutzen den Dienst auf eigenes Risiko.",
acceptableUse: "Akzeptable Nutzung",
acceptableUseParagraph: "Sie erklären sich damit einverstanden, PrivyDrop nicht für rechtswidrige, missbräuchliche oder schädliche Zwecke zu nutzen. Dies beinhaltet, ist aber nicht beschränkt auf, die Übertragung illegaler, urheberrechtlich geschützter oder bösartiger Inhalte sowie die Nutzung des Dienstes zur Belästigung oder zum Identitätsdiebstahl.",
liability: "Haftungsbeschränkung",
liabilityParagraph: "PrivyDrop wird ohne Mängelgewähr und ohne jegliche Garantien bereitgestellt. Wir haften nicht für direkte, indirekte oder Folgeschäden, die sich aus der Nutzung unseres Dienstes ergeben, einschließlich, aber nicht beschränkt auf Datenverlust, Systemausfälle oder Dienstunterbrechungen.",
changes: "Änderungen der Nutzungsbedingungen",
changesParagraph: "Wir behalten uns das Recht vor, diese Nutzungsbedingungen jederzeit zu aktualisieren. Alle Änderungen werden wirksam, sobald die aktualisierten Bedingungen auf unserer Website veröffentlicht werden. Es liegt in Ihrer Verantwortung, die Nutzungsbedingungen regelmäßig auf Änderungen zu überprüfen.",
},
},
help: {
Help_dis: "Hilfe",
label: "Hilfe",
h1: "PrivyDrop Hilfe und Support",
h1_P: "Wir sind hier, um Ihnen zu helfen, das Beste aus PrivyDrop herauszuholen. Wenn Sie Fragen haben oder Unterstützung benötigen, zögern Sie bitte nicht, uns zu kontaktieren.",
h2_1: "Kontaktieren Sie uns",
h2_1_P1: "Sie können uns eine E-Mail senden an",
h2_1_P2: ". Wir werden uns innerhalb von 24 Stunden bei Ihnen melden.",
h2_2: "Soziale Medien",
h2_2_P: "Sie können uns auch in sozialen Medien finden:",
h2_3: "Zusätzliche Ressourcen",
h2_3_P:
"Weitere Informationen zu PrivyDrop finden Sie auf den folgenden Seiten:",
h1Paragraph: "Wir sind hier, um Ihnen zu helfen, das Beste aus PrivyDrop herauszuholen. Wenn Sie Fragen haben oder Unterstützung benötigen, zögern Sie bitte nicht, uns zu kontaktieren.",
sections: {
contactUs: "Kontaktieren Sie uns",
contactUsParagraph1: "Sie können uns eine E-Mail senden an",
contactUsParagraph2: ". Wir werden uns innerhalb von 24 Stunden bei Ihnen melden.",
socialMedia: "Soziale Medien",
socialMediaParagraph: "Sie finden uns auch in den sozialen Medien:",
additionalResources: "Zusätzliche Ressourcen",
additionalResourcesParagraph: "Weitere Informationen über PrivyDrop finden Sie auf den folgenden Seiten:",
},
},
about: {
h1: "Über PrivyDrop",
P1: "PrivyDrop ist ein kostenloses und sicheres Dateiübertragungs- und Clipboard-Sharing-Tool, das mit Fokus auf Privatsphäre und Benutzerfreundlichkeit entwickelt wurde. Unsere Mission ist es, eine einfache, aber leistungsstarke Lösung für die Übertragung von Dateien und die Freigabe von Inhalten geräteübergreifend ohne Einschränkungen bereitzustellen.",
P2: "Im Kern von PrivyDrop steht unser Engagement für Sicherheit und Privatsphäre. Wir verwenden Ende-zu-Ende-Verschlüsselung, um sicherzustellen, dass Ihre Daten während des Übertragungsprozesses geschützt sind, und wir speichern Ihre Dateien oder Clipboard-Inhalte niemals auf unseren Servern. Dies bedeutet, dass Ihre Daten lokal und unter Ihrer Kontrolle bleiben.",
P3: "Mit PrivyDrop können Sie mühelos Text, Bilder und Dateien jeder Größe teilen, ohne Registrierung oder Anmeldung. Unsere Plattform ist darauf ausgelegt, schnell, effizient und umweltfreundlich zu sein, mit einem Fokus auf ein nahtloses und benutzerfreundliches Erlebnis.",
P4: "Wir glauben daran, Benutzern die Kontrolle über ihr digitales Leben zu geben, und PrivyDrop ist unser Beitrag zu dieser Vision. Wir hoffen, dass unser Tool Ihnen hilft, sicher mit Freunden, Familie und Kollegen zu teilen und zusammenzuarbeiten, ohne Ihre Privatsphäre oder Sicherheit zu gefährden.",
P5: "Weitere Informationen oder Fragen finden Sie auf den folgenden Seiten:",
paragraphs: [
"PrivyDrop ist ein kostenloses und sicheres Dateiübertragungs- und Clipboard-Sharing-Tool, das mit Blick auf Datenschutz und Benutzerfreundlichkeit entwickelt wurde. Unsere Mission ist es, eine einfache, aber leistungsstarke Lösung für das Übertragen von Dateien und das Teilen von Inhalten über Geräte hinweg ohne Einschränkungen bereitzustellen.",
"Im Mittelpunkt von PrivyDrop steht unser Engagement für Sicherheit und Datenschutz. Wir verwenden Ende-zu-Ende-Verschlüsselung, um sicherzustellen, dass Ihre Daten während des Übertragungsvorgangs geschützt sind, und wir speichern Ihre Dateien oder Zwischenablageinhalte niemals auf unseren Servern. Das bedeutet, dass Ihre Daten lokal und unter Ihrer Kontrolle bleiben.",
"Mit PrivyDrop können Sie mühelos Text, Bilder und Dateien jeder Größe freigeben, ohne dass eine Registrierung oder Anmeldung erforderlich ist. Unsere Plattform ist so konzipiert, dass sie schnell, effizient und umweltfreundlich ist, wobei der Fokus auf einer nahtlosen und benutzerfreundlichen Erfahrung liegt.",
"Wir glauben daran, Nutzer in die Lage zu versetzen, die Kontrolle über ihr digitales Leben zu übernehmen, und PrivyDrop ist unser Beitrag zu dieser Vision. Wir hoffen, dass unser Tool Ihnen hilft, sicher mit Ihren Freunden, Familienmitgliedern und Kollegen zu teilen und zusammenzuarbeiten, ohne Kompromisse bei Ihrer Privatsphäre oder Sicherheit einzugehen.",
"Für weitere Informationen oder Fragen besuchen Sie bitte die folgenden Seiten:",
],
},
HowItWorks: {
h2: "Wie es funktioniert",
h2_P: "Teilen Sie Dateien und Nachrichten sofort in drei einfachen Schritten",
btn_try: "Jetzt ausprobieren →",
step1_title: "Text eingeben oder Dateien auswählen",
step1_description:
"Geben Sie Ihre Nachricht ein oder ziehen Sie Dateien/Ordner in den Auswahlbereich",
step2_title: "Raum beitreten",
step2_description:
"Klicken Sie auf die Schaltfläche 'Raum beitreten', um eine Freigabesitzung zu erstellen",
step3_title: "Empfangen",
step3_description:
"Geben Sie die Raum-ID auf der Empfangsseite ein und klicken Sie auf 'Raum beitreten', um die freigegebenen Inhalte zu erhalten",
howItWorks: {
title: "Wie es funktioniert",
description: "Teilen Sie Dateien und Nachrichten sofort in drei einfachen Schritten",
tryNow: "Jetzt ausprobieren →",
step1Title: "Text eingeben oder Dateien auswählen",
step1Description: "Geben Sie Ihre Nachricht ein oder ziehen Sie Dateien/Ordner in den Auswahlbereich",
step2Title: "Raum beitreten",
step2Description: "Klicken Sie auf die Schaltfläche 'Raum beitreten', um eine Freigabesitzung zu erstellen",
step3Title: "Empfangen",
step3Description: "Geben Sie die Raum-ID auf der Empfangsseite ein und klicken Sie auf 'Raum beitreten', um die freigegebenen Inhalte zu erhalten",
},
SystemDiagram: {
h2: "Systemdiagramm",
h2_P: "PrivyDrop: Ihre Daten, Ihre Kontrolle. Einfach, schnell und privat.",
systemDiagram: {
title: "Systemdiagramm",
description: "PrivyDrop: Ihre Daten, Ihre Kontrolle. Einfach, schnell und privat.",
},
KeyFeatures: {
h2: "Hauptmerkmale",
h3_1: "Direkt und sicher",
h3_1_P:
"Ihre Dateien reisen direkt von Ihrem Gerät zum Empfänger, wie ein geheimer Tunnel, den nur Sie beide nutzen können. Mit Ende-zu-Ende-Verschlüsselung ist es, als ob Ihre Daten eine Sprache sprechen, die nur der vorgesehene Empfänger verstehen kann. Möchten Sie nicht mehr teilen? Schließen Sie einfach Ihren Browser-Tab, und es ist, als ob Sie ein Telefongespräch beenden Sie haben die Kontrolle.",
h3_2: "Team-Synergie",
h3_2_P:
"Teilen Sie mit Ihrem gesamten Team so einfach wie mit einer Person. Wie bei einem digitalen Rundtisch erhalten alle die Dateien gleichzeitig. Egal, ob Sie an einem kreativen Projekt zusammenarbeiten oder wichtige Dokumente verteilen, es ist, als ob alle im selben Raum wären und Ihre gemeinsame Vision gleichzeitig erhalten. Perfekt für Brainstorming-Sitzungen, Team-Präsentationen oder jeden Moment, in dem mehrere Köpfe verbunden sein müssen.",
h3_3: "Keine Grenzen, intelligente Handhabung",
h3_3_P:
"Stellen Sie sich eine magische Pipeline vor, die alles transportieren kann, egal wie groß! Senden Sie Dateien jeder Größe, begrenzt nur durch Ihren Speicherplatz. Für besonders große Dateien können Sie auswählen, wo Sie sie auf Ihrem Gerät speichern möchten. Es ist wie ein spezieller Lieferdienst, der Ihren Computer nicht verlangsamt Dateien gehen direkt auf die Festplatte, sodass Ihr Gerät schnell und reaktionsschnell bleibt.",
h3_4: "Schnell wie ein Gedanke",
h3_4_P:
"Teilen Sie Text, Bilder und sogar ganze Ordner so schnell, wie Sie daran denken können. Es ist, als ob Sie Ihre digitalen Sachen sofort teleportieren. Müssen Sie ein ganzes Fotoalbum oder einen Ordner voller Dokumente senden? Kein Problem! Es ist so einfach wie das Teilen einer einzelnen Datei.",
h3_5: "Grün und sauber",
h3_5_P:
"Wir sind wie eine digitale Version eines persönlichen Gesprächs nichts wird woanders gespeichert. Das bedeutet, dass wir sehr umweltfreundlich sind und minimale Ressourcen verwenden. Es ist, als ob wir keine Spuren in der digitalen Welt hinterlassen und alles sauber und grün für alle halten.",
h3_6: "Fortsetzbare Übertragungen",
h3_6_P:
"Legen Sie ein Speicherverzeichnis fest, um fortsetzbare Übertragungen automatisch zu aktivieren. Machen Sie sich keine Sorgen mehr über Netzwerkunterbrechungen; Ihr Dateiaustausch wird genau dort fortgesetzt, wo er unterbrochen wurde.",
keyFeatures: {
title: "Hauptmerkmale",
items: {
directSecure: {
title: "Direkt und sicher",
description: "Ihre Dateien reisen direkt von Ihrem Gerät zum Empfänger, wie ein geheimer Tunnel, auf den nur Sie beide Zugriff haben. Dank Ende-zu-Ende-Verschlüsselung ist es, als ob Ihre Daten eine Sprache sprechen würden, die nur der vorgesehene Empfänger versteht. Möchten Sie nicht mehr teilen? Schließen Sie einfach Ihren Browser-Tab, und es ist, als ob Sie ein Telefonat beenden würden - Sie haben die Kontrolle.",
},
faqs: {
FAQ_dis: "Häufig gestellte Fragen",
question_0:
"Werden die Daten wirklich lokal gespeichert und nicht auf andere Server übertragen?",
answer_0:
"Ja, alle Daten werden lokal verarbeitet. Sie können das YouTube-Video auf unserer Startseite ansehen Dateien können auch in einem lokalen Netzwerk übertragen werden, selbst wenn das Internet nach dem Herstellen der Verbindung getrennt wird. In Zukunft planen wir, den Code zu öffnen, damit jeder ihn überprüfen kann.",
question_1: "Wie sende und empfange ich Ordner?",
answer_1:
"Das Senden eines Ordners ist so einfach wie das Senden einer Datei. Ziehen Sie den Ordner in den Dateiauswahlbereich oder klicken Sie auf den Bereich, um ihn auszuwählen, und drücken Sie dann auf „Senden starten“. Auf der Empfängerseite können Benutzer direkt herunterladen oder ein Speicherverzeichnis auswählen, bevor sie herunterladen. Ersteres speichert im Speicher, während Letzteres direkt auf die Festplatte speichert.",
question_2: "Kann ich die Raum-ID ändern?",
answer_2:
"Ja, Sie können die Raum-ID in eine beliebige Zeichenfolge ändern, die Sie bevorzugen.",
question_3: "Kann ich Inhalte kontinuierlich teilen?",
answer_3:
"Solange Sie verbunden bleiben, können Sie manuell auf die Schaltfläche „Senden starten“ klicken, um die freigegebenen Inhalte zu aktualisieren, sobald sie sich ändern.",
question_4:
"Kann ich Dateien gleichzeitig mit mehreren Empfängern teilen?",
answer_4:
"Natürlich! Es gibt keinen Unterschied zwischen einem Empfänger und mehreren Empfängern, die gleichzeitig empfangen.",
question_5: "Sind meine Daten sicher, wenn ich PrivyDrop verwende?",
answer_5:
"Absolut sicher. Ihre Daten bleiben immer lokal und werden zwischen Geräten über eine verschlüsselte Ende-zu-Ende-Verbindung übertragen. Alle übertragenen Daten sind verschlüsselt, sodass nur Sie und der Empfänger darauf zugreifen können.",
question_6: "Muss ich ein Konto erstellen, um PrivyDrop zu verwenden?",
answer_6:
"Keine Registrierung oder Anmeldung erforderlich öffnen Sie einfach die Website und beginnen Sie mit der Nutzung. Bequemlichkeit und Geschwindigkeit haben für uns Priorität.",
question_7: "Gibt es Einschränkungen bei der Dateigröße?",
answer_7:
"Keine Einschränkungen bei der Dateigröße oder Geschwindigkeit. Solange Sie genügend Speicherplatz haben, können Sie Dateien jeder Größe übertragen, indem Sie ein Speicherverzeichnis vor dem Herunterladen festlegen.",
question_8: "Kann ich Ordner oder mehrere Dateien gleichzeitig teilen?",
answer_8:
"Ja, das Teilen mehrerer Dateien oder Ordner ist so einfach wie das Teilen einer einzelnen Datei. Sie können auch Dateien zur Übertragung hinzufügen klicken Sie einfach auf „Senden starten“, um sie für den Empfänger zu aktualisieren.",
question_9:
"Wie kann ich das Teilen beenden, wenn ich es mir anders überlege?",
answer_9:
"Das Beenden einer Freigabe ist so einfach wie das Schließen des Browser-Tabs oder -Fensters. Sobald Sie dies tun, wird die Verbindung beendet, und es können keine weiteren Daten übertragen werden.",
question_10: "Verlangsamt PrivyDrop mein Gerät?",
answer_10:
"Nein, PrivyDrop ist darauf ausgelegt, leichtgewichtig und effizient zu sein. Wenn Sie ein Speicherverzeichnis festlegen, werden alle empfangenen Daten direkt auf die Festplatte geschrieben, wodurch der Speicher umgangen wird. Dies hilft, die Leistung Ihres Geräts aufrechtzuerhalten.",
question_11: "Kann ich PrivyDrop offline verwenden?",
answer_11:
"Ja, wenn sich Sender und Empfänger im selben lokalen Netzwerk befinden, können sie sich bei bestehender Internetverbindung demselben Raum anschließen und dann die Verbindung trennen. Die Dateifreigabe funktioniert weiterhin. Einzelheiten finden Sie im YouTube-Video auf der Startseite.",
question_12: "Verwendet PrivyDrop Server?",
answer_12:
"Ja, es gibt tatsächlich einen leichtgewichtigen Server, der nur für die Signalisierung verwendet wird, um eine verschlüsselte Verbindung herzustellen. Sobald die Verbindung hergestellt ist, werden alle Daten direkt zwischen den Geräten über die verschlüsselte Verbindung übertragen.",
question_13: "Wie lange ist eine Raum-ID gültig?",
answer_13:
"Die anfängliche Gültigkeit einer Raum-ID beträgt 24 Stunden. Wenn ein Empfänger dem Raum beitritt, wird die Gültigkeit automatisch um 24 Stunden ab diesem Zeitpunkt verlängert.",
teamSynergy: {
title: "Team-Synergie",
description: "Teilen Sie mit Ihrem gesamten Team so einfach wie mit einer einzelnen Person. Wie bei einem digitalen runden Tisch erhält jeder die Dateien gleichzeitig. Egal, ob Sie an einem kreativen Projekt zusammenarbeiten oder wichtige Dokumente verteilen, es ist, als hätten Sie alle im selben Raum und erhielten Ihre gemeinsame Vision auf einmal. Perfekt für Brainstorming-Sitzungen, Team-Präsentationen oder jeden Moment, in dem mehrere Köpfe miteinander verbunden werden müssen.",
},
clipboard_btn: {
Pasted_dis: "Eingefügt",
Copied_dis: "Kopiert",
noLimits: {
title: "Keine Grenzen, intelligente Handhabung",
description: "Stellen Sie sich eine magische Pipeline vor, die alles transportieren kann, egal wie groß! Senden Sie Dateien jeder Größe, nur begrenzt durch Ihren Speicherplatz. Wählen Sie bei besonders großen Dateien aus, wo Sie diese auf Ihrem Gerät speichern möchten. Es ist, als hätten Sie einen speziellen Lieferservice, der Ihren Computer nicht verlangsamt - Dateien gehen direkt auf die Festplatte, wodurch Ihr Gerät schnell und reaktionsschnell bleibt.",
},
fileUploadHandler: {
NoFileChosen_tips: "Keine Datei ausgewählt",
fileChosen_tips_template:
"{fileNum} Datei(en) und {folderNum} Ordner ausgewählt",
chooseFileTips:
"Ziehen Sie Dateien/Ordner an eine beliebige Stelle auf dieser Seite, oder klicken Sie hier, um auszuwählen.",
dragTips: "Ziehen Sie hierhin auf die Seite.",
chosenDiagTitle: "Upload-Typ auswählen",
chosenDiagDescription:
"Wählen Sie aus, ob Sie Dateien oder einen Ordner hochladen möchten",
SelectFile_dis: "Dateien auswählen",
SelectFolder_dis: "Ordner auswählen",
swift: {
title: "Schnell wie ein Gedanke",
description: "Teilen Sie Text, Bilder und sogar ganze Ordner so schnell, wie Sie daran denken können. Es ist wie das sofortige Teleportieren Ihrer digitalen Sachen. Müssen Sie ein ganzes Fotoalbum oder einen Ordner voller Dokumente senden? Kein Problem! Es ist so einfach wie das Teilen einer einzelnen Datei.",
},
FileTransferButton: {
SavedToDisk_tips: "Datei bereits auf Festplatte gespeichert",
CurrentFileTransferring_tips: "Datei wird übertragen",
OtherFileTransferring_tips:
"Bitte warten Sie, bis die aktuelle Übertragung abgeschlossen ist",
download_tips: "Klicken Sie, um die Datei herunterzuladen",
PendingSave_tips: "Klicken Sie, um die Datei lokal zu speichern", // 新增
Saved_dis: "Gespeichert",
Waiting_dis: "Warten",
Download_dis: "Herunterladen",
Save_dis: "Speichern", // 新增
greenClean: {
title: "Grün und sauber",
description: "Wir sind wie eine digitale Version eines persönlichen Gesprächs nichts wird woanders gespeichert. Das bedeutet, dass wir sehr umweltfreundlich sind und minimale Ressourcen verwenden. Es ist, als ob wir keine Spuren in der digitalen Welt hinterlassen und alles sauber und grün für alle halten.",
},
FileListDisplay: {
sending_dis: "Senden",
receiving_dis: "Empfangen",
finish_dis: "abgeschlossen",
delete_dis: "Löschen",
downloadNum_dis: "Anzahl der Downloads",
folder_tips_template:
"Ordnername: {name} ({num} Dateien und {size}) insgesamt",
folder_dis_template: " ({num} Dateien, {size})",
PopupDialog_title: "Empfohlen: Speicherverzeichnis auswählen",
PopupDialog_description:
"Wir empfehlen, ein Speicherverzeichnis auszuwählen, um Dateien direkt auf Ihre Festplatte zu speichern. Dies erleichtert die Übertragung großer Dateien und die effiziente Synchronisierung von Ordnern.",
chooseSavePath_tips:
"Speichern Sie große Dateien oder Ordner direkt in einem ausgewählten Verzeichnis. 👉",
chooseSavePath_dis: "Speicherort auswählen",
resumable: {
title: "Fortsetzbare Übertragungen",
description: "Legen Sie ein Speicherverzeichnis fest, um fortsetzbare Übertragungen automatisch zu aktivieren. Machen Sie sich keine Sorgen mehr über Netzwerkunterbrechungen; Ihr Dateiaustausch wird genau dort fortgesetzt, wo er unterbrochen wurde.",
},
RetrieveMethod: {
P: "Glückwunsch 🎉 Freigegebene Inhalte warten darauf, abgerufen zu werden:",
RoomId_tips: "Raum-ID abrufen: ",
copyRoomId_tips: "Raum-ID kopieren",
url_tips: "Abrufen über URL: ",
copyUrl_tips: "Freigabe-URL kopieren",
scanQR_tips: "Scannen Sie den QR-Code, um zu empfangen 👇",
Copied_dis: "Kopiert",
Copy_QR_dis: "QR-Code kopieren",
download_QR_dis: "QR-Code herunterladen",
},
ClipboardApp: {
fetchRoom_err:
"Fehler beim Abrufen eines Raums. Bitte versuchen Sie es erneut.",
roomCheck: {
//handleShareRoomCheck
empty_msg: "Raum-ID darf nicht leer sein",
available_msg: "Raum ist verfügbar",
notAvailable_msg:
"Raum ist nicht verfügbar, bitte versuchen Sie einen anderen",
},
channelOpen_msg:
"'Datenkanal ist geöffnet, bereit zum Empfangen von Daten...'",
waitting_tips:
"Warten auf den Empfänger, der sich verbindet. Bitte lassen Sie diese Seite geöffnet, bis die Übertragung abgeschlossen ist. Auf dem Desktop können Sie den Browser minimieren oder zwischen Tabs wechseln. Auf mobilen Geräten sollte der Browser im Vordergrund bleiben.",
joinRoom: {
EmptyMsg: "Warnung, die Raum-ID ist leer",
DuplicateMsg: "Diese Raum-ID ist bereits vergeben. Bitte wählen Sie eine andere ID.",
successMsg:
"Raum erfolgreich betreten! Schließen Sie diese Seite nicht, bis die Übertragung abgeschlossen ist. (Am Desktop können Sie den Browser minimieren oder Tabs wechseln; auf mobilen Geräten bringen Sie den Browser nicht in den Hintergrund.)",
notExist:
"Der Raum, dem Sie beitreten möchten, existiert nicht. Nur der Sender kann einen Raum erstellen.",
failMsg: "Fehler beim Beitreten zum Raum:",
faq: {
title: "Häufig gestellte Fragen",
items: [
{
question: "Werden die Daten wirklich lokal gespeichert und nicht auf andere Server übertragen?",
answer: "Ja, alle Daten werden lokal verarbeitet. Sie können sich das YouTube-Video auf unserer Homepage ansehen Dateien können sogar innerhalb eines lokalen Netzwerks übertragen werden, wenn das Internet nach dem Aufbau einer Verbindung getrennt wird. Wir planen, den Code in Zukunft als Open Source zur Verfügung zu stellen, damit jeder ihn überprüfen kann.",
},
pickSaveMsg: "Direkt auf Festplatte speichern?",
pickSaveUnsupported: "Verzeichnisauswahl nicht unterstützt.",
pickSaveSuccess: "Speicherort festgelegt.",
pickSaveError: "Speicherort konnte nicht festgelegt werden.",
roomStatus: {
senderEmptyMsg: "Raum ist leer",
receiverEmptyMsg:
"Sie können eine Einladung annehmen, um dem Raum beizutreten",
onlyOneMsg: "Sie sind der Einzige hier",
peopleMsg_template: "{peerCount} Personen im Raum",
connected_dis: "Verbunden",
senderDisconnectedMsg: "Sender getrennt",
leftRoomMsg: "Sie haben den Raum verlassen.",
leaveRoomBtn: "Raum Verlassen",
{
question: "Wie sende und empfange ich Ordner?",
answer: 'Das Versenden eines Ordners ist so einfach wie das Versenden einer Datei. Ziehen Sie den Ordner in den Dateiauswahlbereich oder klicken Sie auf den Bereich, um ihn auszuwählen, und klicken Sie dann auf "Synchronisieren". Auf der Empfängerseite können die Benutzer die Dateien direkt herunterladen oder vor dem Herunterladen ein Speicherverzeichnis auswählen. Ersteres speichert im Arbeitsspeicher, während letzteres direkt auf die Festplatte speichert.',
},
fileExistMsg: "Einige Dateien wurden bereits hinzugefügt.",
noFilesForFolderMsg: "Keine Dateien im Ordner '{folderName}' gefunden.",
zipError: "Fehler beim Erstellen der ZIP-Datei.",
fileNotFoundMsg: "Datei '{fileName}' zum Herunterladen nicht gefunden.",
confirmLeaveWhileTransferring:
"Übertragung wird unterbrochen. Bei Speicherverzeichnis kann fortgesetzt werden. Verlassen?",
leaveWhileTransferringSuccess: "Raum verlassen, Übertragung unterbrochen",
html: {
senderTab: "Senden",
retrieveTab: "Abrufen",
shareTitle_dis: "Inhalte teilen",
retrieveTitle_dis: "Inhalte abrufen",
RoomStatus_dis: "Status:",
Paste_dis: "Einfügen",
Copy_dis: "Kopieren",
inputRoomIdprompt: "Ihre Raum-ID (bearbeitbar):",
joinRoomBtn: "Raum beitreten",
generateSimpleId_tips: "Einfache ID",
generateRandomId_tips: "Zufällige ID",
readClipboardToRoomId: "Raum-ID einfügen",
enterRoomID_placeholder: "Raum-ID eingeben",
{
question: "Kann ich die Raum-ID ändern?",
answer: "Ja, Sie können die Raum-ID in eine beliebige Zeichenfolge ändern.",
},
{
question: "Kann ich Inhalte kontinuierlich teilen?",
answer: 'Solange Sie verbunden bleiben, können Sie manuell auf die Schaltfläche "Synchronisieren" klicken, um den freigegebenen Inhalt zu aktualisieren, wann immer er sich ändert.',
},
{
question: "Kann ich Dateien gleichzeitig an mehrere Empfänger freigeben?",
answer: "Natürlich! Es gibt keinen Unterschied, ob eine Person empfängt oder mehrere Personen gleichzeitig empfangen.",
},
{
question: "Sind meine Daten bei der Verwendung von PrivyDrop sicher?",
answer: "Absolut sicher. Ihre Daten bleiben immer lokal und werden über eine verschlüsselte Ende-zu-Ende-Verbindung zwischen den Geräten übertragen. Alle übertragenen Daten sind verschlüsselt, so dass nur Sie und der Empfänger darauf zugreifen können.",
},
{
question: "Muss ich ein Konto erstellen, um PrivyDrop zu nutzen?",
answer: "Keine Registrierung oder Anmeldung erforderlich öffnen Sie einfach die Seite und fangen Sie an, sie zu nutzen. Bequemlichkeit und Geschwindigkeit sind unsere Prioritäten.",
},
{
question: "Gibt es Beschränkungen für die Dateigröße?",
answer: "Keine Beschränkungen für Dateigröße oder Geschwindigkeit. Solange Sie genügend Festplattenspeicher haben, können Sie Dateien jeder Größe übertragen, indem Sie vor dem Herunterladen ein Speicherverzeichnis festlegen.",
},
{
question: "Kann ich Ordner oder mehrere Dateien auf einmal freigeben?",
answer: 'Ja, das Freigeben mehrerer Dateien oder Ordner ist so einfach wie das Freigeben einer einzelnen Datei. Sie können der Übertragung auch Dateien hinzufügen klicken Sie einfach auf "Synchronisieren", um sie für den Empfänger zu aktualisieren.',
},
{
question: "Wie beende ich die Freigabe, wenn ich es mir anders überlege?",
answer: "Das Beenden einer Freigabe ist so einfach wie das Schließen des Browser-Tabs oder -Fensters. Sobald Sie dies tun, wird die Verbindung beendet und es können keine weiteren Daten übertragen werden.",
},
{
question: "Wird mein Gerät durch die Verwendung von PrivyDrop verlangsamt?",
answer: "Nein, PrivyDrop ist so konzipiert, dass es leichtgewichtig und effizient ist. Wenn Sie ein Speicherverzeichnis festlegen, werden alle empfangenen Daten direkt auf die Festplatte geschrieben, wobei der Arbeitsspeicher umgangen wird, was zur Aufrechterhaltung der Leistung Ihres Geräts beiträgt.",
},
{
question: "Kann ich PrivyDrop offline verwenden?",
answer: "Ja, wenn sich Sender und Empfänger im selben lokalen Netzwerk befinden, können sie demselben Raum beitreten, während sie mit dem Internet verbunden sind, und die Verbindung dann trennen. Die Dateifreigabe wird weiterhin funktionieren. Einzelheiten finden Sie im YouTube-Video auf der Homepage.",
},
{
question: "Verwendet PrivyDrop irgendwelche Server?",
answer: "Ja, es gibt in der Tat einen leichtgewichtigen Server, der nur für die Signalisierung zum Aufbau einer verschlüsselten Verbindung verwendet wird. Sobald die Verbindung hergestellt ist, werden alle Daten über die verschlüsselte Verbindung direkt zwischen den Geräten übertragen.",
},
{
question: "Wie lange ist die Gültigkeitsdauer für Raum-IDs?",
answer: "Die anfängliche Gültigkeit einer Raum-ID beträgt 24 Stunden. Wenn ein Empfänger dem Raum beitritt, wird die Gültigkeit automatisch um 24 Stunden ab diesem Zeitpunkt verlängert.",
},
],
},
blog: {
listTitle: "Blog",
listSubtitle: "Neueste Artikel und Updates",
recentPosts: "Neueste Beiträge",
tags: "Schlagwörter",
readMore: "Weiterlesen",
by: "von",
postNotFound: "Beitrag nicht gefunden",
tocTitle: "Inhaltsverzeichnis",
tagTitlePrefix: "Schlagwort",
tagSubtitleTemplate: "Artikel mit dem Schlagwort {tag}",
tagEmpty: "Keine Artikel für dieses Schlagwort gefunden.",
},
common: {
clipboard: {
pasted: "Eingefügt",
copied: "Kopiert",
copyError: "Kopieren fehlgeschlagen.",
readError: "Lesen der Zwischenablage fehlgeschlagen.",
loading: "Laden...",
},
buttons: {
request: "Anfordern",
download: "Herunterladen",
save: "Speichern",
copy: "Kopieren",
paste: "Einfügen",
joinRoom: "Raum beitreten",
leaveRoom: "Raum verlassen",
},
},
clipboard: {
tabs: {
send: "Senden",
retrieve: "Abrufen",
},
titles: {
share: "Inhalte teilen",
retrieve: "Inhalte abrufen",
retrieveMethod: "Abrufmethode",
inputRoomId_tips: "Ihre Raum-ID (bearbeitbar):",
joinRoom_dis: "Raum beitreten",
SyncSending_loadingText: "Synchronisiert",
SyncSending_dis: "Synchronisieren",
readClipboard_dis: "Raum-ID einfügen",
retrieveRoomId_placeholder: "Raum-ID eingeben",
RetrieveMethodTitle: "Abrufmethode",
// New: cached ID utils
saveId_dis: "ID speichern",
useCachedId_dis: "Gespeicherte ID verwenden",
saveId_tips: "Aktuelle ID für spätere schnelle Nutzung speichern",
useCachedId_tips: "Gespeicherte ID schnell nutzen; Doppelklick zum Speichermodus wechseln",
},
// New: cache messages
saveId_success: "Erfolgreich im Cache gespeichert",
actions: {
sync: "Synchronisieren",
syncLoading: "Synchronisiert",
readClipboard: "Raum-ID einfügen",
},
placeholders: {
roomId: "Raum-ID eingeben",
},
status: {
roomEmpty: "Raum ist leer",
receiverCanAccept: "Sie können eine Einladung annehmen, um dem Raum beizutreten",
onlyOne: "Sie sind der Einzige hier",
peopleCount: "{peerCount} Personen im Raum",
connected: "Verbunden",
senderDisconnected: "Sender getrennt",
leftRoom: "Sie haben den Raum verlassen.",
},
messages: {
fileExist: "Einige Dateien wurden bereits hinzugefügt.",
noFilesForFolder: "Keine Dateien im Ordner '{folderName}' gefunden.",
zipError: "Fehler beim Erstellen der ZIP-Datei.",
fileNotFound: "Datei '{fileName}' zum Herunterladen nicht gefunden.",
confirmLeave: "Übertragung wird unterbrochen. Bei Speicherverzeichnis kann fortgesetzt werden. Verlassen?",
leaveSuccess: "Raum verlassen, Übertragung unterbrochen",
fetchRoomError: "Fehler beim Abrufen eines Raums. Bitte versuchen Sie es erneut.",
generateShareLinkError: "Fehler beim Erstellen des Freigabelinks.",
leaveRoomError: "Fehler beim Verlassen des Raums.",
validateRoomError: "Fehler beim Validieren des Raums.",
resetSenderStateError: "Fehler beim Zurücksetzen des Senderstatus.",
channelOpen: "'Datenkanal ist geöffnet, bereit zum Empfangen von Daten...'",
waiting: "Warten auf den Empfänger, der sich verbindet. Bitte lassen Sie diese Seite geöffnet, bis die Übertragung abgeschlossen ist. Auf dem Desktop können Sie den Browser minimieren oder zwischen Tabs wechseln. Auf mobilen Geräten sollte der Browser im Vordergrund bleiben.",
},
join: {
empty: "Warnung, die Raum-ID ist leer",
duplicate: "Diese Raum-ID ist bereits vergeben. Bitte wählen Sie eine andere ID.",
success: "Raum erfolgreich betreten! Schließen Sie diese Seite nicht, bis die Übertragung abgeschlossen ist. (Am Desktop können Sie den Browser minimieren oder Tabs wechseln; auf mobilen Geräten bringen Sie den Browser nicht in den Hintergrund.)",
notFound: "Der Raum, dem Sie beitreten möchten, existiert nicht. Nur der Sender kann einen Raum erstellen.",
failure: "Fehler beim Beitreten zum Raum:",
inProgress: "Beitritt zum Raum… (in langsamen Netzen 530 Sekunden)",
slow: "Wirkt etwas langsam Netzwerk/VPN prüfen oder später erneut versuchen",
timeout: "Beitritt zeitüberschritten (mögliche Netzbeschränkung). Bitte erneut versuchen",
},
rtc: {
slow: "Netzwerk möglicherweise eingeschränkt — VPN deaktivieren oder später erneut versuchen",
negotiating: "Im Raum direkte P2PVerbindung wird aufgebaut…",
connected: "Verbunden",
reconnecting: "Wiederverbinden…",
restored: "Verbindung wiederhergestellt",
},
roomCheck: {
empty: "Raum-ID darf nicht leer sein",
available: "Raum ist verfügbar",
notAvailable: "Raum ist nicht verfügbar, bitte versuchen Sie einen anderen",
},
saveLocation: {
pickMsg: "Direkt auf Festplatte speichern?",
unsupported: "Verzeichnisauswahl nicht unterstützt.",
success: "Speicherort festgelegt.",
error: "Speicherort konnte nicht festgelegt werden.",
},
cachedId: {
save: "ID speichern",
use: "Gespeicherte ID verwenden",
saveTip: "Aktuelle ID für spätere schnelle Nutzung speichern",
useTip: "Gespeicherte ID schnell nutzen; Doppelklick zum Speichermodus wechseln",
saveSuccess: "Erfolgreich im Cache gespeichert",
},
generateId: {
simple: "Einfache ID",
random: "Zufällige ID",
},
},
fileUpload: {
noFileChosen: "Keine Datei ausgewählt",
fileChosen: "{fileNum} Datei(en) und {folderNum} Ordner ausgewählt",
chooseTip: "Ziehen Sie Dateien/Ordner an eine beliebige Stelle auf dieser Seite, oder klicken Sie hier, um auszuwählen.",
dragTip: "Ziehen Sie hierhin auf die Seite.",
dialog: {
title: "Upload-Typ auswählen",
description: "Wählen Sie aus, ob Sie Dateien oder einen Ordner hochladen möchten",
selectFile: "Dateien auswählen",
selectFolder: "Ordner auswählen",
},
},
fileList: {
sending: "Senden",
receiving: "Empfangen",
finished: "abgeschlossen",
delete: "Löschen",
downloadCount: "Anzahl der Downloads",
folderSummary: "Ordnername: {name} ({num} Dateien und {size}) insgesamt",
folderInline: " ({num} Dateien, {size})",
saveDialog: {
title: "Empfohlen: Speicherverzeichnis auswählen",
description: "Wir empfehlen, ein Speicherverzeichnis auszuwählen, um Dateien direkt auf Ihre Festplatte zu speichern. Dies erleichtert die Übertragung großer Dateien und die effiziente Synchronisierung von Ordnern.",
tip: "Speichern Sie große Dateien oder Ordner direkt in einem ausgewählten Verzeichnis. 👉",
button: "Speicherort auswählen",
},
},
fileTransfer: {
savedToDisk: "Datei bereits auf Festplatte gespeichert",
currentTransferring: "Datei wird übertragen",
otherTransferring: "Bitte warten Sie, bis die aktuelle Übertragung abgeschlossen ist",
download: "Herunterladen",
pendingSave: "Klicken Sie, um die Datei lokal zu speichern",
saved: "Gespeichert",
waiting: "Warten",
},
retrieveMethod: {
intro: "Glückwunsch 🎉 Freigegebene Inhalte warten darauf, abgerufen zu werden:",
roomIdTip: "Raum-ID abrufen: ",
copyRoomId: "Raum-ID kopieren",
urlTip: "Abrufen über URL: ",
copyUrl: "Freigabe-URL kopieren",
scanQr: "Scannen Sie den QR-Code, um zu empfangen 👇",
copied: "Kopiert",
copyQr: "QR-Code kopieren",
downloadQr: "QR-Code herunterladen",
},
home: {
h1: "Kostenloses sicheres Online-Clipboard & Dateiübertragungstool",
h1P: "P2P-Übertragungen werden dort fortgesetzt, wo sie unterbrochen wurden. Erleben Sie einen wirklich zuverlässigen, privaten und unaufhaltsamen Datei- und Textaustausch keine Registrierung, keine Beschränkungen bei Dateigröße oder Geschwindigkeit, völlig kostenlos.",
h2_screenOnly:
"Jetzt sicheres Clipboard & Dateiübertragungstool ausprobieren",
h2_demo: "Sichere Dateifreigabe in Aktion sehen",
h2P_demo:
"Sehen Sie, wie unsere lokale, Ende-zu-Ende-verschlüsselte Dateifreigabe Ihre Privatsphäre schützt",
watch_tips: "Sie können das Video auch auf diesen Plattformen ansehen:",
youtube_tips: "PrivyDrop auf YouTube ansehen",
bilibili_tips: "PrivyDrop auf Bilibili ansehen",
hero: {
title: "Kostenloses sicheres Online-Clipboard & Dateiübertragungstool",
subtitle: "P2P-Übertragungen werden dort fortgesetzt, wo sie unterbrochen wurden. Erleben Sie einen wirklich zuverlässigen, privaten und unaufhaltsamen Datei- und Textaustausch keine Registrierung, keine Beschränkungen bei Dateigröße oder Geschwindigkeit, völlig kostenlos.",
screenOnlyTitle: "Jetzt sicheres Clipboard & Dateiübertragungstool ausprobieren",
},
demo: {
title: "Sichere Dateifreigabe in Aktion sehen",
description: "Sehen Sie, wie unsere lokale, Ende-zu-Ende-verschlüsselte Dateifreigabe Ihre Privatsphäre schützt",
watchTip: "Sie können das Video auch auf diesen Plattformen ansehen:",
youtube: "PrivyDrop auf YouTube ansehen",
bilibili: "PrivyDrop auf Bilibili ansehen",
},
},
},
};
+378 -264
View File
@@ -19,16 +19,12 @@ export const en: Messages = {
title: "PrivyDrop FAQ",
description:
"Find answers to frequently asked questions about PrivyDrop, including how to send files, share clipboard content, and ensure secure and private data transfers.",
keywords:
"PrivyDrop FAQ,frequently asked questions,secure file sharing FAQ,private data sharing help,end-to-end encrypted file transfer,secure clipboard sharing support,how to use PrivyDrop,file transfer FAQ,privacy-focused sharing questions,PrivyDrop troubleshooting",
},
features: {
title:
"PrivyDrop Core Features: 6 Key Advantages of P2P File Transfer | Secure & Unlimited",
description:
"Discover PrivyDrop's core features: end-to-end encrypted P2P direct transfer, unlimited file size, resumable transfers, team collaboration, eco-friendly design, and open-source transparency. Experience truly secure and private file sharing.",
keywords:
"P2P file transfer features,end-to-end encrypted transfer,resumable file transfer,unlimited file sharing,team collaboration tool,eco-friendly file transfer,open source file sharing,private data transfer,WebRTC file sharing,secure file sync,cross-device file transfer,local file processing",
},
help: {
title: "PrivyDrop Help and Support",
@@ -50,324 +46,442 @@ export const en: Messages = {
"PrivyDrop Blog - Private P2P File Sharing & Collaboration",
description:
"Discover secure file sharing tips, privacy-first collaboration strategies, and how to leverage P2P and WebRTC for safer data transfer.",
keywords:
"secure file sharing,p2p file transfer,private collaboration,webrtc,end-to-end encryption,team collaboration,privacy tools",
},
},
text: {
Header: {
Home_dis: "Home",
Blog_dis: "blog",
About_dis: "About",
Help_dis: "Help",
FAQ_dis: "FAQ",
Features_dis: "Features",
Terms_dis: "Terms",
Privacy_dis: "Privacy",
},
Footer: {
CopyrightNotice: "PrivyDrop. All rights reserved.",
Terms_dis: "Terms of Use",
Privacy_dis: "Privacy Policy",
SupportedLanguages: "Supported Languages",
},
blog: {
list_title: "Blog",
list_subtitle: "Latest articles and updates",
recent_posts: "Recent Posts",
tags: "Tags",
read_more: "Read more",
by: "by",
post_not_found: "Post not found",
toc_title: "Table of contents",
tag_title_prefix: "Tag",
tag_subtitle_template: "Articles tagged with {tag}",
tag_empty: "No articles found for this tag.",
// Navigation (formerly Header)
navigation: {
home: "Home",
blog: "blog",
about: "About",
help: "Help",
faq: "FAQ",
features: "Features",
terms: "Terms",
privacy: "Privacy",
},
// Footer
footer: {
copyright: "PrivyDrop. All rights reserved.",
terms: "Terms of Use",
privacy: "Privacy Policy",
supportedLanguages: "Supported Languages",
},
// Privacy Policy
privacy: {
PrivacyPolicy_dis: "Privacy Policy",
policyLabel: "Privacy Policy",
h1: "PrivyDrop Privacy Policy",
h1_P: "At PrivyDrop, we are committed to protecting your privacy and safeguarding your personal information. This privacy policy outlines how we collect, use, and protect the data you provide while using our service.",
h2_1: "Information Collection",
h2_1_P:
h1Paragraph:
"At PrivyDrop, we are committed to protecting your privacy and safeguarding your personal information. This privacy policy outlines how we collect, use, and protect the data you provide while using our service.",
sections: {
informationCollection: "Information Collection",
informationCollectionParagraph:
"PrivyDrop does not collect any personally identifiable information from users. We do not require registration or account creation to use our service. The only information we collect is the Room ID and the file/clipboard data you choose to share with other users.",
h2_2: "Data Storage and Security",
h2_2_P:
dataStorage: "Data Storage and Security",
dataStorageParagraph:
"We do not store any of your data on our servers. All file transfers and clipboard sharing are handled using end-to-end encryption, ensuring that your information remains secure and accessible only to the intended recipient. Once the transfer is complete, the data is removed from our systems.",
h2_3: "Third-Party Services",
h2_3_P:
thirdPartyServices: "Third-Party Services",
thirdPartyServicesParagraph:
"PrivyDrop does not integrate with any third-party services or platforms. We do not share or sell your data to any third parties.",
h2_4: "Amendments to the Privacy Policy",
h2_4_P:
amendments: "Amendments to the Privacy Policy",
amendmentsParagraph:
"We may update this privacy policy from time to time to reflect changes in our practices or applicable laws. Any changes will be effective immediately upon posting the updated policy on our website. It is your responsibility to review the privacy policy periodically for any updates.",
h2_5: "Contact Us",
h2_5_P:
contactUs: "Contact Us",
contactUsParagraph:
"If you have any questions or concerns about our privacy practices, please feel free to contact us at",
},
},
// Terms of Use
terms: {
TermsOfUse_dis: "Terms of Use",
useLabel: "Terms of Use",
h1: "PrivyDrop Terms of Use",
h1_P: "By using the PrivyDrop service, you agree to be bound by these terms of use. If you do not agree to these terms, please do not use the service.",
h2_1: "Use of the Service",
h2_1_P:
h1Paragraph:
"By using the PrivyDrop service, you agree to be bound by these terms of use. If you do not agree to these terms, please do not use the service.",
sections: {
useOfService: "Use of the Service",
useOfServiceParagraph:
"PrivyDrop is provided as a free service without any restrictions.",
h2_2: "Data Privacy and Security",
h2_2_P:
dataPrivacy: "Data Privacy and Security",
dataPrivacyParagraph:
"We take the privacy and security of your data very seriously. All file transfers and clipboard sharing are secured with end-to-end encryption, and we do not store any of your data on our servers. However, we cannot guarantee the security of your data during the transfer process, and you use the service at your own risk.",
h2_3: "Acceptable Use",
h2_3_P:
acceptableUse: "Acceptable Use",
acceptableUseParagraph:
"You agree not to use PrivyDrop for any unlawful, abusive, or harmful purpose. This includes, but is not limited to, the transfer of illegal, copyrighted, or malicious content, as well as the use of the service to harass or impersonate others.",
h2_4: "Limitation of Liability",
h2_4_P:
liability: "Limitation of Liability",
liabilityParagraph:
'PrivyDrop is provided "as is" without any warranties or guarantees. We shall not be liable for any direct, indirect, or consequential damages arising from the use of our service, including but not limited to data loss, system failures, or interruptions in service.',
h2_5: "Changes to the Terms of Use",
h2_5_P:
changes: "Changes to the Terms of Use",
changesParagraph:
"We reserve the right to update these terms of use at any time. Any changes will be effective immediately upon posting the updated terms on our website. It is your responsibility to review the terms of use periodically for any changes.",
},
},
// Help & Support
help: {
Help_dis: "Help",
label: "Help",
h1: "PrivyDrop Help and Support",
h1_P: "We're here to help you make the most out of PrivyDrop. If you have any questions or need assistance, please don't hesitate to reach out to us.",
h2_1: "Contact Us",
h2_1_P1: "You can send us an email at",
h2_1_P2: ". We will get back to you within 24 hours.",
h2_2: "Social Media",
h2_2_P: "You can also find us on social media:",
h2_3: "Additional Resources",
h2_3_P:
h1Paragraph:
"We're here to help you make the most out of PrivyDrop. If you have any questions or need assistance, please don't hesitate to reach out to us.",
sections: {
contactUs: "Contact Us",
contactUsParagraph1: "You can send us an email at",
contactUsParagraph2: ". We will get back to you within 24 hours.",
socialMedia: "Social Media",
socialMediaParagraph: "You can also find us on social media:",
additionalResources: "Additional Resources",
additionalResourcesParagraph:
"For more information about PrivyDrop, please check out the following pages:",
},
},
// About
about: {
h1: "About PrivyDrop",
P1: "PrivyDrop is a free and secure file transfer and clipboard sharing tool designed with privacy and ease-of-use in mind. Our mission is to provide a simple, yet powerful solution for transferring files and sharing content across devices without any restrictions.",
P2: "At the core of PrivyDrop is our commitment to security and privacy. We use end-to-end encryption to ensure that your data is protected during the transfer process, and we never store your files or clipboard content on our servers. This means that your data stays local and under your control.",
P3: "With PrivyDrop, you can effortlessly share text, images, and files of any size without the need for registration or logins. Our platform is designed to be fast, efficient, and environmentally friendly, with a focus on providing a seamless and user-friendly experience.",
P4: "We believe in empowering users to take control of their digital lives, and PrivyDrop is our contribution to that vision. We hope that our tool will help you securely share and collaborate with your friends, family, and colleagues, without compromising your privacy or security.",
P5: "For more information or questions, please visit the following pages:",
paragraphs: [
"PrivyDrop is a free and secure file transfer and clipboard sharing tool designed with privacy and ease-of-use in mind. Our mission is to provide a simple, yet powerful solution for transferring files and sharing content across devices without any restrictions.",
"At the core of PrivyDrop is our commitment to security and privacy. We use end-to-end encryption to ensure that your data is protected during the transfer process, and we never store your files or clipboard content on our servers. This means that your data stays local and under your control.",
"With PrivyDrop, you can effortlessly share text, images, and files of any size without the need for registration or logins. Our platform is designed to be fast, efficient, and environmentally friendly, with a focus on providing a seamless and user-friendly experience.",
"We believe in empowering users to take control of their digital lives, and PrivyDrop is our contribution to that vision. We hope that our tool will help you securely share and collaborate with your friends, family, and colleagues, without compromising your privacy or security.",
"For more information or questions, please visit the following pages:",
],
},
HowItWorks: {
h2: "How it works",
h2_P: "Share files and messages instantly in three simple steps",
btn_try: "Try it now →",
step1_title: "Type or Choose Files",
step1_description:
// How It Works
howItWorks: {
title: "How it works",
description: "Share files and messages instantly in three simple steps",
tryNow: "Try it now →",
step1Title: "Type or Choose Files",
step1Description:
"Type your message or drag & drop files/folders into the selection area",
step2_title: "Join Room",
step2_description:
step2Title: "Join Room",
step2Description:
"Click the 'Join Room' button to create a sharing session",
step3_title: "Receive",
step3_description:
step3Title: "Receive",
step3Description:
"Enter the Room ID on the receive page and click 'Join Room' to get the shared content",
},
SystemDiagram: {
h2: "System diagram",
h2_P: "PrivyDrop: Your data, your control. Simple, fast, and private.",
// System Diagram
systemDiagram: {
title: "System diagram",
description: "PrivyDrop: Your data, your control. Simple, fast, and private.",
},
KeyFeatures: {
h2: "Key Features",
h3_1: "Direct and Secure",
h3_1_P:
// Key Features
keyFeatures: {
title: "Key Features",
items: {
directSecure: {
title: "Direct and Secure",
description:
"Your files travel straight from your device to the recipient's, like a secret tunnel only you two can access. With end-to-end encryption, it's like your data is speaking a language only the intended recipient can understand. Don't want to share anymore? Simply close your browser tab, and it's like hanging up a phone call - you're in control.",
h3_2: "Team Synergy",
h3_2_P:
},
teamSynergy: {
title: "Team Synergy",
description:
"Share with your entire team as easily as sharing with one person. Like hosting a digital roundtable, everyone gets the files simultaneously. Whether you're collaborating on a creative project or distributing important documents, it's like having everyone in the same room, receiving your shared vision at once. Perfect for brainstorming sessions, team presentations, or any moment when multiple minds need to connect.",
h3_3: "No Limits, Smart Handling",
h3_3_P:
},
noLimits: {
title: "No Limits, Smart Handling",
description:
"Imagine a magical pipeline that can transport anything, no matter how big! Send files of any size, limited only by your disk space. For those extra-large files, choose where to save them on your device. It's like having a special delivery service that doesn't slow down your computer - files go straight to disk, keeping your device speedy and responsive.",
h3_4: "Swift as a Thought",
h3_4_P:
},
swift: {
title: "Swift as a Thought",
description:
"Share text, images, and even entire folders as quickly as you can think of them. It's like teleporting your digital stuff instantly. Need to send a whole photo album or a folder full of documents? No problem! It's as easy as sharing a single file.",
h3_5: "Green and Clean",
h3_5_P:
},
greenClean: {
title: "Green and Clean",
description:
"We're like a digital version of a face-to-face conversation - nothing gets stored anywhere else. This means we're super environmentally friendly, using minimal resources. It's like leaving no footprint in the digital world, keeping things clean and green for everyone.",
h3_6: "Resumable Transfers",
h3_6_P:
},
resumable: {
title: "Resumable Transfers",
description:
"Set a save directory to automatically enable resumable transfers. No more worrying about network interruptions; your file sharing will pick up right where it left off.",
},
faqs: {
FAQ_dis: "Frequently Asked Questions",
question_0:
},
},
// FAQ
faq: {
title: "Frequently Asked Questions",
items: [
{
question:
"Is the data truly stored locally and not transferred to other servers?",
answer_0:
answer:
"Yes, all data is handled locally. You can check the YouTube video on our homepage—files can still be transferred within a local network even if the internet is disconnected after establishing a connection. In the future, we plan to open source the code so everyone can review it.",
question_1: "How do I send and receive folders?",
answer_1:
},
{
question: "How do I send and receive folders?",
answer:
'Sending a folder is as simple as sending a file. Drag the folder into the file selection area or click the area to select it, then hit "Start Sending." button On the receiving end, users can download directly or choose a save directory before downloading. The former saves to memory, while the latter saves directly to disk.',
question_2: "Can I change the Room ID?",
answer_2: "Yes, you can change the Room ID to any string you prefer.",
question_3: "Can I share content continuously?",
answer_3:
},
{
question: "Can I change the Room ID?",
answer: "Yes, you can change the Room ID to any string you prefer.",
},
{
question: "Can I share content continuously?",
answer:
'As long as you remain connected, you can manually click the "Start Sending" button to update the shared content whenever it changes.',
question_4: "Can I share files with multiple recipients simultaneously?",
answer_4:
},
{
question: "Can I share files with multiple recipients simultaneously?",
answer:
"Of course! There's no difference between one person receiving and multiple people receiving simultaneously.",
question_5: "Is my data secure when using PrivyDrop?",
answer_5:
},
{
question: "Is my data secure when using PrivyDrop?",
answer:
"Absolutely secure. Your data always stays local, transferring between devices through an encrypted, end-to-end connection. All transmitted data is encrypted, ensuring only you and the recipient can access it.",
question_6: "Do I need to create an account to use PrivyDrop?",
answer_6:
},
{
question: "Do I need to create an account to use PrivyDrop?",
answer:
"No registration or login required—just open the site and start using it. Convenience and speed are our priorities.",
question_7: "Are there any file size limits?",
answer_7:
},
{
question: "Are there any file size limits?",
answer:
"No limits on file size or speed. As long as you have enough disk space, you can transfer files of any size by setting a save directory before downloading.",
question_8: "Can I share folders or multiple files at once?",
answer_8:
},
{
question: "Can I share folders or multiple files at once?",
answer:
'Yes, sharing multiple files or folders is as simple as sharing a single file. You can also add files to the transfer—just click "Start Sending" to update them for the recipient.',
question_9: "How do I stop sharing if I change my mind?",
answer_9:
},
{
question: "How do I stop sharing if I change my mind?",
answer:
"Stopping a share is as simple as closing the browser tab or window. Once you do this, the connection is terminated, and no further data can be transferred.",
question_10: "Does using PrivyDrop slow down my device?",
answer_10:
},
{
question: "Does using PrivyDrop slow down my device?",
answer:
"No, PrivyDrop is designed to be lightweight and efficient. If you set a save directory, all received data is written directly to disk, bypassing memory, which helps maintain your device's performance.",
question_11: "Can I use PrivyDrop offline?",
answer_11:
},
{
question: "Can I use PrivyDrop offline?",
answer:
"Yes, if the sender and receiver are on the same local network, they can join the same room while connected to the internet and then disconnect from it. File sharing will still work. You can refer to the YouTube video on the homepage for details.",
question_12: "Does PrivyDrop use any servers?",
answer_12:
},
{
question: "Does PrivyDrop use any servers?",
answer:
"Yes, there is indeed a lightweight server, which is used only for signaling to establish an encrypted connection. Once the connection is established, all data is transferred directly between devices through the encrypted connection.",
question_13: "What is the expiration period for Room IDs?",
answer_13:
},
{
question: "What is the expiration period for Room IDs?",
answer:
"The initial validity of a RoomId is 24 hours. If a recipient joins the room, the validity is automatically extended by 24 hours from that moment.",
},
clipboard_btn: {
Pasted_dis: "Pasted",
Copied_dis: "Copied",
],
},
fileUploadHandler: {
NoFileChosen_tips: "No file chosen",
fileChosen_tips_template:
"{fileNum} file(s) and {folderNum} folder(s) selected",
chooseFileTips:
"Drag and drop files/folders anywhere on this page, or click here to choose.",
dragTips: "Drag to anywhere on this page.",
chosenDiagTitle: "Choose Upload Type",
chosenDiagDescription:
"Select whether you want to upload files or a folder",
SelectFile_dis: "Select Files",
SelectFolder_dis: "Select Folder",
},
FileTransferButton: {
SavedToDisk_tips: "File already saved to disk",
CurrentFileTransferring_tips: "File is being transferred",
OtherFileTransferring_tips:
"Please wait for current transfer to complete",
download_tips: "Click to download file",
PendingSave_tips: "Click to save file locally",
Saved_dis: "Saved",
Waiting_dis: "Waiting",
Download_dis: "Download",
Save_dis: "Save",
},
FileListDisplay: {
sending_dis: "Sending",
receiving_dis: "Receiving",
finish_dis: "finished",
delete_dis: "Delete",
downloadNum_dis: "Download count",
folder_tips_template:
"folder name:{name} ({num} files and {size}) in total",
folder_dis_template: " ({num} files, {size})",
PopupDialog_title: "Recommended: Choose a Save Directory",
PopupDialog_description:
"We recommend selecting a save directory to directly save files to your disk. This makes it easier to transfer large files and synchronize folders efficiently.",
chooseSavePath_tips:
"Save large files or folders directly to a selected directory. 👉",
chooseSavePath_dis: "Choose save location",
},
RetrieveMethod: {
P: "Congrats 🎉 Share content is waiting to be retrieved:",
RoomId_tips: "Retrieve RoomID: ",
copyRoomId_tips: "Copy RoomID",
url_tips: "Retrieve using URL: ",
copyUrl_tips: "Copy share url",
scanQR_tips: "Scan the QR code to receive 👇",
Copied_dis: "Copied",
Copy_QR_dis: "Copy QR code",
download_QR_dis: "Download QR code",
},
ClipboardApp: {
fetchRoom_err: "Failed to get a room. Please try again.",
roomCheck: {
//handleShareRoomCheck
empty_msg: "RoomID should not be empty",
available_msg: "Room is available",
notAvailable_msg: "Room is not available, please try another",
},
channelOpen_msg: "'data channel is opened,ready to receive data...'",
waitting_tips:
"Waiting for receiver to connect. Please keep this page open until the transfer is complete. On desktop, you can minimize the browser or switch tabs. On mobile, please keep the browser in the foreground.",
joinRoom: {
EmptyMsg: "Warning, the roomID is empty",
DuplicateMsg: "This room ID is already in use. Please choose another ID.",
successMsg:
"Successfully joined the room! Do not close this page until the transfer is complete. (On desktop, you can minimize the browser or switch tabs; on mobile, do not move the browser to the background.)",
notExist:
"The room you are trying to join does not exist. Only the sender can create a room.",
failMsg: "Failed to join room:",
},
pickSaveMsg: "Save Directly to Disk ?",
pickSaveUnsupported: "Directory picker not supported.",
pickSaveSuccess: "Save location set.",
pickSaveError: "Could not set save location.",
roomStatus: {
senderEmptyMsg: "Room is empty",
receiverEmptyMsg: "You can accept an invitation to join the room",
onlyOneMsg: "You're the only one here",
peopleMsg_template: "{peerCount} People in the room",
connected_dis: "Connected",
senderDisconnectedMsg: "Sender disconnected",
leftRoomMsg: "You have left the room.",
leaveRoomBtn: "Leave Room",
},
fileExistMsg: "Some files were already added.",
noFilesForFolderMsg: "No files found for folder '{folderName}'.",
zipError: "Error creating ZIP.",
fileNotFoundMsg: "File '{fileName}' not found for download.",
confirmLeaveWhileTransferring: "Transfer will be interrupted. Can be resumed if save directory is set. Exit anyway?",
leaveWhileTransferringSuccess: "Left room, transfer interrupted",
html: {
senderTab: "Send",
retrieveTab: "Retrieve",
shareTitle_dis: "Share Content",
retrieveTitle_dis: "Retrieve Content",
RoomStatus_dis: "Status:",
Paste_dis: "Paste",
Copy_dis: "Copy",
inputRoomIdprompt: "Your RoomID (Editable):",
joinRoomBtn: "Join room",
generateSimpleId_tips: "Simple ID",
generateRandomId_tips: "Random ID",
readClipboardToRoomId: "Paste RoomID",
enterRoomID_placeholder: "enter RoomID",
retrieveMethod: "Retrieve method",
inputRoomId_tips: "Your RoomID (Editable):",
joinRoom_dis: "Join room",
SyncSending_loadingText: "Synced",
SyncSending_dis: "Sync",
readClipboard_dis: "Paste RoomID",
retrieveRoomId_placeholder: "Enter RoomID",
RetrieveMethodTitle: "Retrieve method",
// New: cached ID utils
saveId_dis: "Save ID",
useCachedId_dis: "Use cached ID",
saveId_tips: "Save current ID for quick reuse later",
useCachedId_tips: "Quick use saved ID; double-click to switch save mode",
},
// New: cache messages
saveId_success: "Saved to cache",
},
home: {
h1: "Free Secure Online Clipboard & File Transfer Tool",
h1P: "P2P transfers that pick up where you left off. Experience truly reliable, private, and unstoppable file and text sharing—no registration, no file size or speed limits, completely free.",
h2_screenOnly: "Try Secure Clipboard & File Transfer Tool Now",
h2_demo: "See Secure File Sharing in Action",
h2P_demo:
// Blog
blog: {
listTitle: "Blog",
listSubtitle: "Latest articles and updates",
recentPosts: "Recent Posts",
tags: "Tags",
readMore: "Read more",
by: "by",
postNotFound: "Post not found",
tocTitle: "Table of contents",
tagTitlePrefix: "Tag",
tagSubtitleTemplate: "Articles tagged with {tag}",
tagEmpty: "No articles found for this tag.",
},
// Common UI elements
common: {
clipboard: {
pasted: "Pasted",
copied: "Copied",
copyError: "Failed to copy.",
readError: "Failed to read clipboard.",
loading: "Loading...",
},
buttons: {
request: "Request",
download: "Download",
save: "Save",
copy: "Copy",
paste: "Paste",
joinRoom: "Join room",
leaveRoom: "Leave Room",
},
},
// Clipboard Core
clipboard: {
tabs: {
send: "Send",
retrieve: "Retrieve",
},
titles: {
share: "Share Content",
retrieve: "Retrieve Content",
retrieveMethod: "Retrieve method",
},
actions: {
sync: "Sync",
syncLoading: "Synced",
readClipboard: "Paste RoomID",
},
placeholders: {
roomId: "Enter RoomID",
},
status: {
roomEmpty: "Room is empty",
receiverCanAccept: "You can accept an invitation to join the room",
onlyOne: "You're the only one here",
peopleCount: "{peerCount} People in the room",
connected: "Connected",
senderDisconnected: "Sender disconnected",
leftRoom: "You have left the room.",
},
messages: {
fileExist: "Some files were already added.",
noFilesForFolder: "No files found for folder '{folderName}'.",
zipError: "Error creating ZIP.",
fileNotFound: "File '{fileName}' not found for download.",
confirmLeave:
"Transfer will be interrupted. Can be resumed if save directory is set. Exit anyway?",
leaveSuccess: "Left room, transfer interrupted",
fetchRoomError: "Failed to get a room. Please try again.",
generateShareLinkError: "Failed to generate share link.",
leaveRoomError: "Failed to leave room.",
validateRoomError: "Failed to validate room.",
resetSenderStateError: "Failed to reset sender state.",
channelOpen: "'data channel is opened,ready to receive data...'",
waiting:
"Waiting for receiver to connect. Please keep this page open until the transfer is complete. On desktop, you can minimize the browser or switch tabs. On mobile, please keep the browser in the foreground.",
},
join: {
empty: "Warning, the roomID is empty",
duplicate: "This room ID is already in use. Please choose another ID.",
success:
"Successfully joined the room! Do not close this page until the transfer is complete. (On desktop, you can minimize the browser or switch tabs; on mobile, do not move the browser to the background.)",
notFound:
"The room you are trying to join does not exist. Only the sender can create a room.",
failure: "Failed to join room:",
inProgress:
"Joining the room… this may take 530 seconds on slow networks",
slow: "Feels slow—check your network/VPN or try again shortly",
timeout: "Join timed out (network may be restricted). Please try again",
},
rtc: {
slow: "Network may be restricted — try turning off VPN or try again shortly",
negotiating: "In the room—establishing a direct P2P connection…",
connected: "Connected",
reconnecting: "Reconnecting…",
restored: "Connection restored",
},
roomCheck: {
empty: "RoomID should not be empty",
available: "Room is available",
notAvailable: "Room is not available, please try another",
},
saveLocation: {
pickMsg: "Save Directly to Disk ?",
unsupported: "Directory picker not supported.",
success: "Save location set.",
error: "Could not set save location.",
},
cachedId: {
save: "Save ID",
use: "Use cached ID",
saveTip: "Save current ID for quick reuse later",
useTip: "Quick use saved ID; double-click to switch save mode",
saveSuccess: "Saved to cache",
},
generateId: {
simple: "Simple ID",
random: "Random ID",
},
},
// File Upload
fileUpload: {
noFileChosen: "No file chosen",
fileChosen: "{fileNum} file(s) and {folderNum} folder(s) selected",
chooseTip:
"Drag and drop files/folders anywhere on this page, or click here to choose.",
dragTip: "Drag to anywhere on this page.",
dialog: {
title: "Choose Upload Type",
description: "Select whether you want to upload files or a folder",
selectFile: "Select Files",
selectFolder: "Select Folder",
},
},
// File List
fileList: {
sending: "Sending",
receiving: "Receiving",
finished: "finished",
delete: "Delete",
downloadCount: "Download count",
folderSummary: "folder name:{name} ({num} files and {size}) in total",
folderInline: " ({num} files, {size})",
saveDialog: {
title: "Recommended: Choose a Save Directory",
description:
"We recommend selecting a save directory to directly save files to your disk. This makes it easier to transfer large files and synchronize folders efficiently.",
tip: "Save large files or folders directly to a selected directory. 👉",
button: "Choose save location",
},
},
// File Transfer Button
fileTransfer: {
savedToDisk: "File already saved to disk",
currentTransferring: "File is being transferred",
otherTransferring: "Please wait for current transfer to complete",
download: "Download",
pendingSave: "Click to save file locally",
saved: "Saved",
waiting: "Waiting",
},
// Retrieve Method (Share Card)
retrieveMethod: {
intro: "Congrats 🎉 Share content is waiting to be retrieved:",
roomIdTip: "Retrieve RoomID: ",
copyRoomId: "Copy RoomID",
urlTip: "Retrieve using URL: ",
copyUrl: "Copy share url",
scanQr: "Scan the QR code to receive 👇",
copied: "Copied",
copyQr: "Copy QR code",
downloadQr: "Download QR code",
},
// Home Page
home: {
hero: {
title: "Free Secure Online Clipboard & File Transfer Tool",
subtitle:
"P2P transfers that pick up where you left off. Experience truly reliable, private, and unstoppable file and text sharing—no registration, no file size or speed limits, completely free.",
screenOnlyTitle: "Try Secure Clipboard & File Transfer Tool Now",
},
demo: {
title: "See Secure File Sharing in Action",
description:
"Watch how our local-first, end-to-end encrypted file sharing protects your privacy",
watch_tips: "You can also watch the video on these platforms:",
youtube_tips: "Watch PrivyDrop on YouTube",
bilibili_tips: "Watch PrivyDrop on Bilibili",
watchTip: "You can also watch the video on these platforms:",
youtube: "Watch PrivyDrop on YouTube",
bilibili: "Watch PrivyDrop on Bilibili",
},
},
},
};
+395 -283
View File
@@ -19,16 +19,12 @@ export const es: Messages = {
title: "PrivyDrop FAQ",
description:
"Encuentra respuestas a las preguntas frecuentes sobre PrivyDrop, incluyendo cómo enviar archivos, compartir contenido del portapapeles y asegurar transferencias de datos seguras y privadas.",
keywords:
"PrivyDrop FAQ,preguntas frecuentes,FAQ de compartir archivos seguros,ayuda de compartir datos privados,transferencia de archivos cifrada de extremo a extremo,soporte de compartir portapapeles seguro,cómo usar PrivyDrop,FAQ de transferencia de archivos,preguntas de compartir centrado en privacidad,solución de problemas de PrivyDrop",
},
features: {
title:
"Características Principales de PrivyDrop: 6 Ventajas Clave de la Transferencia P2P | Seguro e Ilimitado",
description:
"Descubre las características principales de PrivyDrop: transferencia directa P2P cifrada de extremo a extremo, tamaño de archivo ilimitado, transferencias reanudables, colaboración en equipo, diseño ecológico y transparencia de código abierto. Experimenta el intercambio de archivos verdaderamente seguro y privado.",
keywords:
"características de transferencia de archivos P2P,transferencia cifrada de extremo a extremo,transferencia de archivos reanudable,compartir archivos ilimitado,herramienta de colaboración en equipo,transferencia de archivos ecológica,compartir archivos de código abierto,transferencia de datos privados,compartir archivos WebRTC,sincronización de archivos segura,transferencia de archivos entre dispositivos,procesamiento de archivos local",
},
help: {
title: "Ayuda y Soporte de PrivyDrop",
@@ -38,7 +34,7 @@ export const es: Messages = {
privacy: {
title: "Política de Privacidad de PrivyDrop",
description:
"Comprenda cómo PrivyDrop protege su privacidad y datos, incluyendo detalles sobre recopilación de información, almacenamiento y seguridad de datos, y nuestro compromiso de no compartir sus datos con terceros.",
"Entienda cómo PrivyDrop protege su privacidad y datos, incluyendo detalles sobre la recopilación de información, el almacenamiento y la seguridad de los datos, y nuestro compromiso de no compartir sus datos con terceros.",
},
terms: {
title: "Términos de Uso de PrivyDrop",
@@ -50,327 +46,443 @@ export const es: Messages = {
"Blog de PrivyDrop - Compartición de archivos P2P privada y colaboración",
description:
"Descubre consejos de compartición segura de archivos, estrategias de colaboración con enfoque en la privacidad y cómo aprovechar P2P y WebRTC para transferencias más seguras.",
keywords:
"compartición segura de archivos,transferencia de archivos p2p,colaboración privada,webrtc,cifrado de extremo a extremo,colaboración en equipo,herramientas de privacidad",
},
},
text: {
Header: {
Home_dis: "Inicio",
Blog_dis: "Blog",
About_dis: "Acerca de",
Help_dis: "Ayuda",
FAQ_dis: "FAQ",
Terms_dis: "Términos",
Privacy_dis: "Privacidad",
Features_dis: "Características",
// Navigation (formerly Header)
navigation: {
home: "Inicio",
blog: "Blog",
about: "Acerca de",
help: "Ayuda",
faq: "FAQ",
features: "Características",
terms: "Términos",
privacy: "Privacidad",
},
Footer: {
CopyrightNotice: "PrivyDrop. Todos los derechos reservados.",
Terms_dis: "Términos de Uso",
Privacy_dis: "Política de Privacidad",
SupportedLanguages: "Idiomas soportados",
},
blog: {
list_title: "Blog",
list_subtitle: "Últimos artículos y actualizaciones",
recent_posts: "Entradas recientes",
tags: "Etiquetas",
read_more: "Leer más",
by: "por",
post_not_found: "Artículo no encontrado",
toc_title: "Tabla de contenidos",
tag_title_prefix: "Etiqueta",
tag_subtitle_template: "Artículos etiquetados con {tag}",
tag_empty: "No se encontraron artículos para esta etiqueta.",
// Footer
footer: {
copyright: "PrivyDrop. Todos los derechos reservados.",
terms: "Términos de Uso",
privacy: "Política de Privacidad",
supportedLanguages: "Idiomas soportados",
},
// Privacy Policy
privacy: {
PrivacyPolicy_dis: "Política de Privacidad",
policyLabel: "Política de Privacidad",
h1: "Política de Privacidad de PrivyDrop",
h1_P: "En PrivyDrop, estamos comprometidos con proteger su privacidad y salvaguardar su información personal. Esta política de privacidad describe cómo recopilamos, usamos y protegemos los datos que proporciona al usar nuestro servicio.",
h2_1: "Recopilación de Información",
h2_1_P:
"PrivyDrop no recopila ninguna información personal identificable de los usuarios. No requerimos registro ni creación de cuenta para usar nuestro servicio. La única información que recopilamos es el ID de Sala y los datos de archivo/portapapeles que elige compartir con otros usuarios.",
h2_2: "Almacenamiento y Seguridad de Datos",
h2_2_P:
"No almacenamos ninguno de sus datos en nuestros servidores. Todas las transferencias de archivos y compartición de portapapeles se manejan usando cifrado de extremo a extremo, asegurando que su información permanezca segura y accesible solo para el destinatario previsto. Una vez completada la transferencia, los datos se eliminan de nuestros sistemas.",
h2_3: "Servicios de Terceros",
h2_3_P:
h1Paragraph:
"En PrivyDrop, estamos comprometidos con proteger su privacidad y salvaguardar su información personal. Esta política de privacidad describe cómo recopilamos, usamos y protegemos los datos que proporciona al usar nuestro servicio.",
sections: {
informationCollection: "Recopilación de Información",
informationCollectionParagraph:
"PrivyDrop no recopila ninguna información de identificación personal de los usuarios. No requerimos registro ni creación de cuenta para usar nuestro servicio. La única información que recopilamos es el ID de Sala y los datos de archivos/portapapeles que elige compartir con otros usuarios.",
dataStorage: "Almacenamiento y Seguridad de Datos",
dataStorageParagraph:
"No almacenamos ninguno de sus datos en nuestros servidores. Todas las transferencias de archivos y el intercambio de portapapeles se gestionan mediante cifrado de extremo a extremo, lo que garantiza que su información permanezca segura y accesible solo para el destinatario previsto. Una vez completada la transferencia, los datos se eliminan de nuestros sistemas.",
thirdPartyServices: "Servicios de Terceros",
thirdPartyServicesParagraph:
"PrivyDrop no se integra con ningún servicio o plataforma de terceros. No compartimos ni vendemos sus datos a terceros.",
h2_4: "Modificaciones a la Política de Privacidad",
h2_4_P:
"Podemos actualizar esta política de privacidad ocasionalmente para reflejar cambios en nuestras prácticas o leyes aplicables. Cualquier cambio será efectivo inmediatamente al publicar la política actualizada en nuestro sitio web. Es su responsabilidad revisar la política de privacidad periódicamente para cualquier actualización.",
h2_5: "Contáctenos",
h2_5_P:
amendments: "Enmiendas a la Política de Privacidad",
amendmentsParagraph:
"Podemos actualizar esta política de privacidad de vez en cuando para reflejar cambios en nuestras prácticas o leyes aplicables. Cualquier cambio será efectivo inmediatamente al publicar la política actualizada en nuestro sitio web. Es su responsabilidad revisar la política de privacidad periódicamente para cualquier actualización.",
contactUs: "Contáctenos",
contactUsParagraph:
"Si tiene alguna pregunta o inquietud sobre nuestras prácticas de privacidad, no dude en contactarnos en",
},
},
// Terms of Use
terms: {
TermsOfUse_dis: "Términos de Uso",
useLabel: "Términos de Uso",
h1: "Términos de Uso de PrivyDrop",
h1_P: "Al usar el servicio PrivyDrop, acepta estar sujeto a estos términos de uso. Si no está de acuerdo con estos términos, por favor no use el servicio.",
h2_1: "Uso del Servicio",
h2_1_P:
"PrivyDrop se proporciona como un servicio gratuito sin restricciones.",
h2_2: "Privacidad y Seguridad de Datos",
h2_2_P:
"Tomamos muy en serio la privacidad y seguridad de sus datos. Todas las transferencias de archivos y compartición de portapapeles están aseguradas con cifrado de extremo a extremo, y no almacenamos ninguno de sus datos en nuestros servidores. Sin embargo, no podemos garantizar la seguridad de sus datos durante el proceso de transferencia, y usa el servicio bajo su propio riesgo.",
h2_3: "Uso Aceptable",
h2_3_P:
"Acepta no usar PrivyDrop para ningún propósito ilegal, abusivo o dañino. Esto incluye, pero no se limita a, la transferencia de contenido ilegal, con derechos de autor o malicioso, así como el uso del servicio para acosar o suplantar a otros.",
h2_4: "Limitación de Responsabilidad",
h2_4_P:
'PrivyDrop se proporciona "tal cual" sin ninguna garantía. No seremos responsables por ningún daño directo, indirecto o consecuente que surja del uso de nuestro servicio, incluyendo pero no limitado a pérdida de datos, fallos del sistema o interrupciones del servicio.',
h2_5: "Cambios en los Términos de Uso",
h2_5_P:
h1Paragraph:
"Al usar el servicio PrivyDrop, acepta estar sujeto a estos términos de uso. Si no está de acuerdo con estos términos, por favor no use el servicio.",
sections: {
useOfService: "Uso del Servicio",
useOfServiceParagraph:
"PrivyDrop se proporciona como un servicio gratuito sin ninguna restricción.",
dataPrivacy: "Privacidad y Seguridad de Datos",
dataPrivacyParagraph:
"Nos tomamos muy en serio la privacidad y seguridad de sus datos. Todas las transferencias de archivos y el intercambio de portapapeles están protegidos con cifrado de extremo a extremo, y no almacenamos ninguno de sus datos en nuestros servidores. Sin embargo, no podemos garantizar la seguridad de sus datos durante el proceso de transferencia, y usted usa el servicio bajo su propio riesgo.",
acceptableUse: "Uso Aceptable",
acceptableUseParagraph:
"Usted acepta no utilizar PrivyDrop para ningún propósito ilegal, abusivo o dañino. Esto incluye, pero no se limita a, la transferencia de contenido ilegal, con derechos de autor o malicioso, así como el uso del servicio para acosar o suplantar a otros.",
liability: "Limitación de Responsabilidad",
liabilityParagraph:
'PrivyDrop se proporciona "tal cual" sin ninguna garantía. No seremos responsables de ningún daño directo, indirecto o consecuente que surja del uso de nuestro servicio, incluyendo pero no limitado a la pérdida de datos, fallos del sistema o interrupciones en el servicio.',
changes: "Cambios en los Términos de Uso",
changesParagraph:
"Nos reservamos el derecho de actualizar estos términos de uso en cualquier momento. Cualquier cambio será efectivo inmediatamente al publicar los términos actualizados en nuestro sitio web. Es su responsabilidad revisar los términos de uso periódicamente para cualquier cambio.",
},
},
// Help & Support
help: {
Help_dis: "Ayuda",
label: "Ayuda",
h1: "Ayuda y Soporte de PrivyDrop",
h1_P: "Estamos aquí para ayudarte a aprovechar al máximo PrivyDrop. Si tienes alguna pregunta o necesitas asistencia, no dudes en contactarnos.",
h2_1: "Contáctanos",
h2_1_P1: "Puedes enviarnos un correo electrónico a",
h2_1_P2: ". Te responderemos dentro de 24 horas.",
h2_2: "Redes Sociales",
h2_2_P: "También puedes encontrarnos en redes sociales:",
h2_3: "Recursos Adicionales",
h2_3_P:
h1Paragraph:
"Estamos aquí para ayudarte a aprovechar al máximo PrivyDrop. Si tienes alguna pregunta o necesitas asistencia, no dudes en contactarnos.",
sections: {
contactUs: "Contáctanos",
contactUsParagraph1: "Puedes enviarnos un correo electrónico a",
contactUsParagraph2: ". Te responderemos en un plazo de 24 horas.",
socialMedia: "Redes Sociales",
socialMediaParagraph: "También puedes encontrarnos en las redes sociales:",
additionalResources: "Recursos Adicionales",
additionalResourcesParagraph:
"Para más información sobre PrivyDrop, por favor consulta las siguientes páginas:",
},
},
// About
about: {
h1: "Acerca de PrivyDrop",
P1: "PrivyDrop es una herramienta gratuita y segura de transferencia de archivos y compartición de portapapeles diseñada pensando en la privacidad y facilidad de uso. Nuestra misión es proporcionar una solución simple pero potente para transferir archivos y compartir contenido entre dispositivos sin restricciones.",
P2: "En el núcleo de PrivyDrop está nuestro compromiso con la seguridad y privacidad. Usamos cifrado de extremo a extremo para asegurar que sus datos estén protegidos durante el proceso de transferencia, y nunca almacenamos sus archivos o contenido del portapapeles en nuestros servidores. Esto significa que sus datos permanecen locales y bajo su control.",
P3: "Con PrivyDrop, puede compartir sin esfuerzo texto, imágenes y archivos de cualquier tamaño sin necesidad de registro o inicio de sesión. Nuestra plataforma está diseñada para ser rápida, eficiente y amigable con el medio ambiente, con un enfoque en proporcionar una experiencia fluida y fácil de usar.",
P4: "Creemos en empoderar a los usuarios para tomar control de sus vidas digitales, y PrivyDrop es nuestra contribución a esa visión. Esperamos que nuestra herramienta le ayude a compartir y colaborar de forma segura con sus amigos, familia y colegas, sin comprometer su privacidad o seguridad.",
P5: "Para más información o preguntas, por favor visite las siguientes páginas:",
paragraphs: [
"PrivyDrop es una herramienta gratuita y segura de transferencia de archivos e intercambio de portapapeles diseñada pensando en la privacidad y la facilidad de uso. Nuestra misión es proporcionar una solución simple pero potente para transferir archivos y compartir contenido entre dispositivos sin ninguna restricción.",
"En el núcleo de PrivyDrop está nuestro compromiso con la seguridad y la privacidad. Utilizamos cifrado de extremo a extremo para garantizar que sus datos estén protegidos durante el proceso de transferencia, y nunca almacenamos sus archivos o el contenido del portapapeles en nuestros servidores. Esto significa que sus datos permanecen locales y bajo su control.",
"Con PrivyDrop, puede compartir sin esfuerzo texto, imágenes y archivos de cualquier tamaño sin necesidad de registro o inicio de sesión. Nuestra plataforma está diseñada para ser rápida, eficiente y respetuosa con el medio ambiente, con el enfoque en proporcionar una experiencia fluida y fácil de usar.",
"Creemos en empoderar a los usuarios para que tomen el control de sus vidas digitales, y PrivyDrop es nuestra contribución a esa visión. Esperamos que nuestra herramienta le ayude a compartir y colaborar de forma segura con sus amigos, familiares y colegas, sin comprometer su privacidad o seguridad.",
"Para más información o preguntas, por favor visite las siguientes páginas:",
],
},
HowItWorks: {
h2: "Cómo funciona",
h2_P: "Comparte archivos y mensajes instantáneamente en tres simples pasos",
btn_try: "Pruébalo ahora →",
step1_title: "Escribe o Elige Archivos",
step1_description:
// How It Works
howItWorks: {
title: "Cómo funciona",
description: "Comparte archivos y mensajes instantáneamente en tres simples pasos",
tryNow: "Pruébalo ahora →",
step1Title: "Escribe o Elige Archivos",
step1Description:
"Escribe tu mensaje o arrastra y suelta archivos/carpetas en el área de selección",
step2_title: "Únete a la Sala",
step2_description:
step2Title: "Únete a la Sala",
step2Description:
"Haz clic en el botón 'Unirse a Sala' para crear una sesión de compartición",
step3_title: "Recibe",
step3_description:
step3Title: "Recibe",
step3Description:
"Ingresa el ID de Sala en la página de recepción y haz clic en 'Unirse a Sala' para obtener el contenido compartido",
},
SystemDiagram: {
h2: "Diagrama del sistema",
h2_P: "PrivyDrop: Tus datos, tu control. Simple, rápido y privado.",
// System Diagram
systemDiagram: {
title: "Diagrama del sistema",
description: "PrivyDrop: Tus datos, tu control. Simple, rápido y privado.",
},
KeyFeatures: {
h2: "Características Principales",
h3_1: "Directo y Seguro",
h3_1_P:
"Tus archivos viajan directamente desde tu dispositivo al del destinatario, como un túnel secreto que solo ustedes dos pueden acceder. Con cifrado de extremo a extremo, es como si tus datos hablaran un idioma que solo el destinatario previsto puede entender. ¿No quieres compartir más? Simplemente cierra la pestaña del navegador, y es como colgar una llamada telefónica - tú tienes el control.",
h3_2: "Sinergia de Equipo",
h3_2_P:
"Comparte con todo tu equipo tan fácilmente como compartir con una persona. Como organizar una mesa redonda digital, todos reciben los archivos simultáneamente. Ya sea que estés colaborando en un proyecto creativo o distribuyendo documentos importantes, es como tener a todos en la misma sala, recibiendo tu visión compartida al mismo tiempo. Perfecto para sesiones de lluvia de ideas, presentaciones de equipo o cualquier momento en que múltiples mentes necesiten conectarse.",
h3_3: "Sin Límites, Manejo Inteligente",
h3_3_P:
"¡Imagina una tubería mágica que puede transportar cualquier cosa, sin importar qué tan grande! Envía archivos de cualquier tamaño, limitado solo por tu espacio en disco. Para esos archivos extra grandes, elige dónde guardarlos en tu dispositivo. Es como tener un servicio de entrega especial que no ralentiza tu computadora - los archivos van directamente al disco, manteniendo tu dispositivo rápido y receptivo.",
h3_4: "Rápido como un Pensamiento",
h3_4_P:
"Comparte texto, imágenes e incluso carpetas enteras tan rápido como puedas pensarlos. Es como teletransportar tus cosas digitales instantáneamente. ¿Necesitas enviar un álbum de fotos completo o una carpeta llena de documentos? ¡No hay problema! Es tan fácil como compartir un solo archivo.",
h3_5: "Verde y Limpio",
h3_5_P:
"Somos como una versión digital de una conversación cara a cara - nada se almacena en ningún otro lugar. Esto significa que somos super amigables con el medio ambiente, usando recursos mínimos. Es como no dejar huella en el mundo digital, manteniendo las cosas limpias y verdes para todos.",
h3_6: "Transferencias Reanudables",
h3_6_P:
// Key Features
keyFeatures: {
title: "Características Principales",
items: {
directSecure: {
title: "Directo y Seguro",
description:
"Tus archivos viajan directamente desde tu dispositivo al del destinatario, como un túnel secreto al que solo ustedes dos pueden acceder. Con el cifrado de extremo a extremo, es como si tus datos hablaran un idioma que solo el destinatario previsto puede entender. ¿Ya no quieres compartir? Simplemente cierra la pestaña de tu navegador y es como colgar una llamada telefónica: tú tienes el control.",
},
teamSynergy: {
title: "Sinergia de Equipo",
description:
"Comparte con todo tu equipo tan fácilmente como compartes con una sola persona. Como organizar una mesa redonda digital, todos reciben los archivos simultáneamente. Ya sea que estés colaborando en un proyecto creativo o distribuyendo documentos importantes, es como tener a todos en la misma sala, recibiendo tu visión compartida a la vez. Perfecto para sesiones de lluvia de ideas, presentaciones de equipo o cualquier momento en que varias mentes necesiten conectarse.",
},
noLimits: {
title: "Sin Límites, Manejo Inteligente",
description:
"¡Imagina una tubería mágica que puede transportar cualquier cosa, sin importar cuán grande sea! Envía archivos de cualquier tamaño, limitados solo por tu espacio en disco. Para esos archivos extra grandes, elige dónde guardarlos en tu dispositivo. Es como tener un servicio de entrega especial que no ralentiza tu computadora: los archivos van directamente al disco, manteniendo tu dispositivo rápido y receptivo.",
},
swift: {
title: "Rápido como un Pensamiento",
description:
"Comparte texto, imágenes e incluso carpetas completas tan rápido como puedas pensarlo. Es como teletransportar tus cosas digitales al instante. ¿Necesitas enviar un álbum de fotos completo o una carpeta llena de documentos? ¡No hay problema! Es tan fácil como compartir un solo archivo.",
},
greenClean: {
title: "Verde y Limpio",
description:
"Somos como una versión digital de una conversación cara a cara: nada se almacena en ningún otro lugar. Esto significa que somos super amigables con el medio ambiente, usando recursos mínimos. Es como no dejar huella en el mundo digital, manteniendo las cosas limpias y verdes para todos.",
},
resumable: {
title: "Transferencias Reanudables",
description:
"Establece un directorio de guardado para habilitar automáticamente las transferencias reanudables. No más preocupaciones por interrupciones de red; tu intercambio de archivos continuará justo donde lo dejaste.",
},
faqs: {
FAQ_dis: "Preguntas Frecuentes",
question_0:
},
},
// FAQ
faq: {
title: "Preguntas Frecuentes",
items: [
{
question:
"¿Los datos realmente se almacenan localmente y no se transfieren a otros servidores?",
answer_0:
"Sí, todos los datos se manejan localmente. Puedes verificar el video de YouTube en nuestra página de inicio: los archivos aún se pueden transferir dentro de una red local incluso si se desconecta internet después de establecer una conexión. En el futuro, planeamos hacer el código de código abierto para que todos puedan revisarlo.",
question_1: "¿Cómo envío y recibo carpetas?",
answer_1:
'Enviar una carpeta es tan simple como enviar un archivo. Arrastra la carpeta al área de selección de archivos o haz clic en el área para seleccionarla, luego presiona el botón "Comenzar a Enviar". En el lado receptor, los usuarios pueden descargar directamente o elegir un directorio de guardado antes de descargar. El primero guarda en memoria, mientras que el último guarda directamente en disco.',
question_2: "¿Puedo cambiar el ID de Sala?",
answer_2:
"Sí, puedes cambiar el ID de Sala a cualquier cadena que prefieras.",
question_3: "¿Puedo compartir contenido continuamente?",
answer_3:
'Mientras permanezcas conectado, puedes hacer clic manualmente en el botón "Comenzar a Enviar" para actualizar el contenido compartido cuando cambie.',
question_4:
"¿Puedo compartir archivos con múltiples destinatarios simultáneamente?",
answer_4:
"¡Por supuesto! No hay diferencia entre que una persona reciba y que múltiples personas reciban simultáneamente.",
question_5: "¿Mis datos están seguros al usar PrivyDrop?",
answer_5:
"Absolutamente seguros. Tus datos siempre permanecen locales, transfiriéndose entre dispositivos a través de una conexión cifrada de extremo a extremo. Todos los datos transmitidos están cifrados, asegurando que solo tú y el destinatario puedan acceder a ellos.",
question_6: "¿Necesito crear una cuenta para usar PrivyDrop?",
answer_6:
"No se requiere registro ni inicio de sesión, solo abre el sitio y comienza a usarlo. La conveniencia y la velocidad son nuestras prioridades.",
question_7: "¿Hay algún límite de tamaño de archivo?",
answer_7:
"No hay límites en el tamaño o velocidad del archivo. Mientras tengas suficiente espacio en disco, puedes transferir archivos de cualquier tamaño estableciendo un directorio de guardado antes de descargar.",
question_8: "¿Puedo compartir carpetas o múltiples archivos a la vez?",
answer_8:
'Sí, compartir múltiples archivos o carpetas es tan simple como compartir un solo archivo. También puedes agregar archivos a la transferencia: solo haz clic en "Comenzar a Enviar" para actualizarlos para el destinatario.',
question_9: "¿Cómo dejo de compartir si cambio de opinión?",
answer_9:
"Detener una compartición es tan simple como cerrar la pestaña o ventana del navegador. Una vez que hagas esto, la conexión se termina y no se pueden transferir más datos.",
question_10: "¿Usar PrivyDrop ralentiza mi dispositivo?",
answer_10:
"No, PrivyDrop está diseñado para ser ligero y eficiente. Si estableces un directorio de guardado, todos los datos recibidos se escriben directamente en el disco, evitando la memoria, lo que ayuda a mantener el rendimiento de tu dispositivo.",
question_11: "¿Puedo usar PrivyDrop sin conexión?",
answer_11:
"Sí, si el remitente y el receptor están en la misma red local, pueden unirse a la misma sala mientras están conectados a internet y luego desconectarse. La compartición de archivos seguirá funcionando. Puedes consultar el video de YouTube en la página de inicio para más detalles.",
question_12: "¿PrivyDrop usa algún servidor?",
answer_12:
"Sí, hay un servidor ligero que se usa solo para señalización para establecer una conexión cifrada. Una vez que se establece la conexión, todos los datos se transfieren directamente entre dispositivos a través de la conexión cifrada.",
question_13: "¿Cuál es el período de expiración para los ID de Sala?",
answer_13:
answer:
"Sí, todos los datos se manejan localmente. Puedes ver el video de YouTube en nuestra página de inicio: los archivos aún se pueden transferir dentro de una red local incluso si Internet se desconecta después de establecer una conexión. En el futuro, planeamos abrir el código fuente para que todos puedan revisarlo.",
},
{
question: "¿Cómo envío y recibo carpetas?",
answer:
'Enviar una carpeta es tan simple como enviar un archivo. Arrastra la carpeta al área de selección de archivos o haz clic en el área para seleccionarla, luego presiona el botón "Sincronizar". En el extremo receptor, los usuarios pueden descargar directamente o elegir un directorio de guardado antes de descargar. Lo primero guarda en la memoria, mientras que lo segundo guarda directamente en el disco.',
},
{
question: "¿Puedo cambiar el ID de Sala?",
answer: "Sí, puedes cambiar el ID de Sala por cualquier cadena que prefieras.",
},
{
question: "¿Puedo compartir contenido continuamente?",
answer:
'Mientras permanezcas conectado, puedes hacer clic manualmente en el botón "Sincronizar" para actualizar el contenido compartido siempre que cambie.',
},
{
question: "¿Puedo compartir archivos con varios destinatarios simultáneamente?",
answer:
"¡Por supuesto! No hay diferencia entre una persona recibiendo y varias personas recibiendo simultáneamente.",
},
{
question: "¿Mis datos están seguros al usar PrivyDrop?",
answer:
"Absolutamente seguros. Tus datos siempre permanecen locales, transfiriéndose entre dispositivos a través de una conexión cifrada de extremo a extremo. Todos los datos transmitidos están cifrados, lo que garantiza que solo tú y el destinatario puedan acceder a ellos.",
},
{
question: "¿Necesito crear una cuenta para usar PrivyDrop?",
answer:
"No se requiere registro ni inicio de sesión; simplemente abra el sitio y comience a usarlo. La conveniencia y la velocidad son nuestras prioridades.",
},
{
question: "¿Hay algún límite de tamaño de archivo?",
answer:
"Sin límites de tamaño de archivo ni de velocidad. Siempre que tenga suficiente espacio en disco, puede transferir archivos de cualquier tamaño estableciendo un directorio de guardado antes de descargar.",
},
{
question: "¿Puedo compartir carpetas o varios archivos a la vez?",
answer:
'Sí, compartir varios archivos o carpetas es tan simple como compartir un solo archivo. También puede agregar archivos a la transferencia; simplemente haga clic en "Sincronizar" para actualizarlos para el destinatario.',
},
{
question: "¿Cómo dejo de compartir si cambio de opinión?",
answer:
"Dejar de compartir es tan simple como cerrar la pestaña o ventana del navegador. Una vez que lo hagas, la conexión se termina y no se pueden transferir más datos.",
},
{
question: "¿El uso de PrivyDrop ralentiza mi dispositivo?",
answer:
"No, PrivyDrop está diseñado para ser ligero y eficiente. Si establece un directorio de guardado, todos los datos recibidos se escriben directamente en el disco, evitando la memoria, lo que ayuda a mantener el rendimiento de su dispositivo.",
},
{
question: "¿Puedo usar PrivyDrop sin conexión?",
answer:
"Sí, si el remitente y el receptor están en la misma red local, pueden unirse a la misma sala mientras están conectados a Internet y luego desconectarse de ella. El intercambio de archivos seguirá funcionando. Puede consultar el video de YouTube en la página de inicio para obtener más detalles.",
},
{
question: "¿PrivyDrop utiliza algún servidor?",
answer:
"Sí, de hecho hay un servidor ligero, que se utiliza solo para la señalización para establecer una conexión cifrada. Una vez establecida la conexión, todos los datos se transfieren directamente entre dispositivos a través de la conexión cifrada.",
},
{
question: "¿Cuál es el período de vencimiento de los ID de Sala?",
answer:
"La validez inicial de un ID de Sala es de 24 horas. Si un destinatario se une a la sala, la validez se extiende automáticamente por 24 horas desde ese momento.",
},
clipboard_btn: {
Pasted_dis: "Pegado",
Copied_dis: "Copiado",
],
},
fileUploadHandler: {
NoFileChosen_tips: "Ningún archivo seleccionado",
fileChosen_tips_template:
"{fileNum} archivo(s) y {folderNum} carpeta(s) seleccionados",
chooseFileTips:
"Arrastra y suelta archivos/carpetas en cualquier lugar de esta página, o haz clic aquí para seleccionar.",
dragTips: "Arrastra a cualquier parte de esta página.",
chosenDiagTitle: "Elegir Tipo de Carga",
chosenDiagDescription:
"Selecciona si deseas cargar archivos o una carpeta",
SelectFile_dis: "Seleccionar Archivos",
SelectFolder_dis: "Seleccionar Carpeta",
// Blog
blog: {
listTitle: "Blog",
listSubtitle: "Últimos artículos y actualizaciones",
recentPosts: "Entradas recientes",
tags: "Etiquetas",
readMore: "Leer más",
by: "por",
postNotFound: "Artículo no encontrado",
tocTitle: "Tabla de contenidos",
tagTitlePrefix: "Etiqueta",
tagSubtitleTemplate: "Artículos etiquetados con {tag}",
tagEmpty: "No se encontraron artículos para esta etiqueta.",
},
FileTransferButton: {
SavedToDisk_tips: "Archivo ya guardado en disco",
CurrentFileTransferring_tips: "El archivo se está transfiriendo",
OtherFileTransferring_tips:
"Por favor espera a que se complete la transferencia actual",
download_tips: "Haz clic para descargar el archivo",
PendingSave_tips: "Haz clic para guardar el archivo localmente", // 新增
Saved_dis: "Guardado",
Waiting_dis: "Esperando",
Download_dis: "Descargar",
Save_dis: "Guardar", // 新增
// Common UI elements
common: {
clipboard: {
pasted: "Pegado",
copied: "Copiado",
copyError: "Error al copiar.",
readError: "Error al leer el portapapeles.",
loading: "Cargando...",
},
FileListDisplay: {
sending_dis: "Enviando",
receiving_dis: "Recibiendo",
finish_dis: "terminado",
delete_dis: "Eliminar",
downloadNum_dis: "Número de descargas",
folder_tips_template:
"nombre de carpeta:{name} ({num} archivos y {size}) en total",
folder_dis_template: " ({num} archivos, {size})",
PopupDialog_title: "Recomendado: Elige un Directorio de Guardado",
PopupDialog_description:
"Recomendamos seleccionar un directorio de guardado para guardar archivos directamente en tu disco. Esto facilita la transferencia de archivos grandes y la sincronización de carpetas de manera eficiente.",
chooseSavePath_tips:
"Guarda archivos grandes o carpetas directamente en un directorio seleccionado. 👉",
chooseSavePath_dis: "Elegir ubicación de guardado",
buttons: {
request: "Solicitar",
download: "Descargar",
save: "Guardar",
copy: "Copiar",
paste: "Pegar",
joinRoom: "Unirse a sala",
leaveRoom: "Salir de la Sala",
},
RetrieveMethod: {
P: "¡Felicitaciones 🎉 El contenido compartido está esperando ser recuperado:",
RoomId_tips: "ID de Sala para recuperar: ",
copyRoomId_tips: "Copiar ID de Sala",
url_tips: "Recuperar usando URL: ",
copyUrl_tips: "Copiar URL de compartición",
scanQR_tips: "Escanea el código QR para recibir 👇",
Copied_dis: "Copiado",
Copy_QR_dis: "Copiar código QR",
download_QR_dis: "Descargar código QR",
},
ClipboardApp: {
fetchRoom_err: "Error al obtener una sala. Por favor intenta de nuevo.",
roomCheck: {
empty_msg: "El ID de Sala no debe estar vacío",
available_msg: "La sala está disponible",
notAvailable_msg: "La sala no está disponible, por favor intenta otra",
// Clipboard Core
clipboard: {
tabs: {
send: "Enviar",
retrieve: "Recuperar",
},
channelOpen_msg: "'canal de datos abierto, listo para recibir datos...'",
waitting_tips:
"Esperando que el receptor se conecte. Por favor mantén esta página abierta hasta que se complete la transferencia. En escritorio, puedes minimizar el navegador o cambiar pestañas. En móvil, por favor mantén el navegador en primer plano.",
joinRoom: {
EmptyMsg: "Advertencia, el ID de sala está vacío",
DuplicateMsg: "Este ID de sala ya está en uso. Por favor, elige otro ID.",
successMsg:
"¡Ingreso exitoso al cuarto! No cierres esta página hasta que se complete la transferencia. (En escritorio, puedes minimizar el navegador o cambiar de pestaña; en móvil, no lleves el navegador al fondo.)",
notExist:
"La sala a la que intentas unirte no existe. Solo el remitente puede crear una sala.",
failMsg: "Error al unirse a la sala:",
},
pickSaveMsg: "¿Guardar Directamente en Disco?",
pickSaveUnsupported: "Selector de directorio no compatible.",
pickSaveSuccess: "Ubicación de guardado establecida.",
pickSaveError: "No se pudo establecer la ubicación de guardado.",
roomStatus: {
senderEmptyMsg: "La sala está vacía",
receiverEmptyMsg: "Puedes aceptar una invitación para unirte a la sala",
onlyOneMsg: "Eres el único aquí",
peopleMsg_template: "{peerCount} Personas en la sala",
connected_dis: "Conectado",
senderDisconnectedMsg: "Remitente desconectado",
leftRoomMsg: "Has salido de la sala.",
leaveRoomBtn: "Salir de la Sala",
},
fileExistMsg: "Algunos archivos ya han sido añadidos.",
noFilesForFolderMsg:
"No se encontraron archivos en la carpeta '{folderName}'.",
zipError: "Error al crear el archivo ZIP.",
fileNotFoundMsg: "Archivo '{fileName}' no encontrado para descargar.",
confirmLeaveWhileTransferring:
"Transferencia se interrumpirá. Se puede reanudar si hay directorio de guardado. ¿Salir de todos modos?",
leaveWhileTransferringSuccess:
"Saliste de la sala, transferencia interrumpida",
html: {
senderTab: "Enviar",
retrieveTab: "Recuperar",
shareTitle_dis: "Compartir Contenido",
retrieveTitle_dis: "Recuperar Contenido",
RoomStatus_dis: "Estado:",
Paste_dis: "Pegar",
Copy_dis: "Copiar",
inputRoomIdprompt: "Tu ID de Sala (Editable):",
joinRoomBtn: "Unirse a sala",
generateSimpleId_tips: "ID Simple",
generateRandomId_tips: "ID Aleatorio",
readClipboardToRoomId: "Pegar ID de Sala",
enterRoomID_placeholder: "ingresa ID de Sala",
titles: {
share: "Compartir Contenido",
retrieve: "Recuperar Contenido",
retrieveMethod: "Método de recuperación",
inputRoomId_tips: "Tu ID de Sala (Editable):",
joinRoom_dis: "Unirse a sala",
SyncSending_loadingText: "Sincronizado",
SyncSending_dis: "Sincronizar",
readClipboard_dis: "Pegar ID de Sala",
retrieveRoomId_placeholder: "Ingresa ID de Sala",
RetrieveMethodTitle: "Método de recuperación",
// New: cached ID utils
saveId_dis: "Guardar ID",
useCachedId_dis: "Usar ID en caché",
saveId_tips: "Guarda el ID actual para reutilizarlo rápidamente",
useCachedId_tips: "Usar ID guardado rápido; doble clic para cambiar modo guardar",
},
// New: cache messages
saveId_success: "Guardado en caché",
actions: {
sync: "Sincronizar",
syncLoading: "Sincronizado",
readClipboard: "Pegar ID de Sala",
},
placeholders: {
roomId: "Ingresa ID de Sala",
},
status: {
roomEmpty: "La sala está vacía",
receiverCanAccept: "Puedes aceptar una invitación para unirte a la sala",
onlyOne: "Eres el único aquí",
peopleCount: "{peerCount} Personas en la sala",
connected: "Conectado",
senderDisconnected: "Remitente desconectado",
leftRoom: "Has salido de la sala.",
},
messages: {
fileExist: "Algunos archivos ya han sido añadidos.",
noFilesForFolder: "No se encontraron archivos en la carpeta '{folderName}'.",
zipError: "Error al crear el archivo ZIP.",
fileNotFound: "Archivo '{fileName}' no encontrado para descargar.",
confirmLeave:
"La transferencia se interrumpirá. Se puede reanudar si hay directorio de guardado. ¿Salir de todos modos?",
leaveSuccess: "Saliste de la sala, transferencia interrumpida",
fetchRoomError: "Error al obtener una sala. Por favor intenta de nuevo.",
generateShareLinkError: "Error al generar el enlace de compartición.",
leaveRoomError: "Error al salir de la sala.",
validateRoomError: "Error al validar la sala.",
resetSenderStateError: "Error al restablecer el estado del remitente.",
channelOpen: "'canal de datos abierto, listo para recibir datos...'",
waiting:
"Esperando que el receptor se conecte. Por favor mantén esta página abierta hasta que se complete la transferencia. En escritorio, puedes minimizar el navegador o cambiar pestañas. En móvil, por favor mantén el navegador en primer plano.",
},
join: {
empty: "Advertencia, el ID de sala está vacío",
duplicate: "Este ID de sala ya está en uso. Por favor, elige otro ID.",
success:
"¡Ingreso exitoso al cuarto! No cierres esta página hasta que se complete la transferencia. (En escritorio, puedes minimizar el navegador o cambiar de pestaña; en móvil, no lleves el navegador al fondo.)",
notFound:
"La sala a la que intentas unirte no existe. Solo el remitente puede crear una sala.",
failure: "Error al unirse a la sala:",
inProgress: "Uniéndose a la sala… (en redes lentas puede tardar 530 s)",
slow: "Va algo lento—revisa tu red/VPN o inténtalo de nuevo en breve",
timeout:
"La unión ha caducado (posibles restricciones de red). Vuelve a intentarlo",
},
rtc: {
slow: "La red puede estar restringida — prueba desactivar la VPN o inténtalo de nuevo en breve",
negotiating: "Dentro de la sala—estableciendo conexión P2P directa…",
connected: "Conectado",
reconnecting: "Reconectando…",
restored: "Conexión restaurada",
},
roomCheck: {
empty: "El ID de Sala no debe estar vacío",
available: "La sala está disponible",
notAvailable: "La sala no está disponible, por favor intenta otra",
},
saveLocation: {
pickMsg: "¿Guardar Directamente en Disco?",
unsupported: "Selector de directorio no compatible.",
success: "Ubicación de guardado establecida.",
error: "No se pudo establecer la ubicación de guardado.",
},
cachedId: {
save: "Guardar ID",
use: "Usar ID en caché",
saveTip: "Guarda el ID actual para reutilizarlo rápidamente",
useTip: "Usar ID guardado rápido; doble clic para cambiar modo guardar",
saveSuccess: "Guardado en caché",
},
generateId: {
simple: "ID Simple",
random: "ID Aleatorio",
},
},
// File Upload
fileUpload: {
noFileChosen: "Ningún archivo seleccionado",
fileChosen: "{fileNum} archivo(s) y {folderNum} carpeta(s) seleccionados",
chooseTip:
"Arrastra y suelta archivos/carpetas en cualquier lugar de esta página, o haz clic aquí para seleccionar.",
dragTip: "Arrastra a cualquier parte de esta página.",
dialog: {
title: "Elegir Tipo de Carga",
description: "Selecciona si deseas cargar archivos o una carpeta",
selectFile: "Seleccionar Archivos",
selectFolder: "Seleccionar Carpeta",
},
},
// File List
fileList: {
sending: "Enviando",
receiving: "Recibiendo",
finished: "terminado",
delete: "Eliminar",
downloadCount: "Número de descargas",
folderSummary: "nombre de carpeta:{name} ({num} archivos y {size}) en total",
folderInline: " ({num} archivos, {size})",
saveDialog: {
title: "Recomendado: Elige un Directorio de Guardado",
description:
"Recomendamos seleccionar un directorio de guardado para guardar archivos directamente en tu disco. Esto facilita la transferencia de archivos grandes y la sincronización de carpetas de manera eficiente.",
tip: "Guarda archivos grandes o carpetas directamente en un directorio seleccionado. 👉",
button: "Elegir ubicación de guardado",
},
},
// File Transfer Button
fileTransfer: {
savedToDisk: "Archivo ya guardado en disco",
currentTransferring: "El archivo se está transfiriendo",
otherTransferring: "Por favor espera a que se complete la transferencia actual",
download: "Descargar",
pendingSave: "Haz clic para guardar el archivo localmente",
saved: "Guardado",
waiting: "Esperando",
},
// Retrieve Method (Share Card)
retrieveMethod: {
intro: "¡Felicitaciones 🎉 El contenido compartido está esperando ser recuperado:",
roomIdTip: "ID de Sala para recuperar: ",
copyRoomId: "Copiar ID de Sala",
urlTip: "Recuperar usando URL: ",
copyUrl: "Copiar URL de compartición",
scanQr: "Escanea el código QR para recibir 👇",
copied: "Copiado",
copyQr: "Copiar código QR",
downloadQr: "Descargar código QR",
},
// Home Page
home: {
h1: "Herramienta Gratuita de Portapapeles y Transferencia de Archivos en Línea Segura",
h1P: "Las transferencias P2P se reanudan desde donde se interrumpieron. Experimente un intercambio de archivos y texto verdaderamente fiable, privado e imparable: sin registro, sin límites de tamaño o velocidad de archivo, y completamente gratis.",
h2_screenOnly:
hero: {
title: "Herramienta Gratuita de Portapapeles y Transferencia de Archivos en Línea Segura",
subtitle:
"Las transferencias P2P se reanudan desde donde se interrumpieron. Experimente un intercambio de archivos y texto verdaderamente fiable, privado e imparable: sin registro, sin límites de tamaño o velocidad de archivo, y completamente gratis.",
screenOnlyTitle:
"Prueba la Herramienta Segura de Portapapeles y Transferencia de Archivos Ahora",
h2_demo: "Ve la Compartición Segura de Archivos en Acción",
h2P_demo:
},
demo: {
title: "Ve la Compartición Segura de Archivos en Acción",
description:
"Mira cómo nuestra compartición de archivos local y cifrada de extremo a extremo protege tu privacidad",
watch_tips: "También puedes ver el video en estas plataformas:",
youtube_tips: "Ver PrivyDrop en YouTube",
bilibili_tips: "Ver PrivyDrop en Bilibili",
watchTip: "También puedes ver el video en estas plataformas:",
youtube: "Ver PrivyDrop on YouTube",
bilibili: "Ver PrivyDrop on Bilibili",
},
},
},
};
+358 -293
View File
@@ -19,16 +19,12 @@ export const fr: Messages = {
title: "PrivyDrop FAQ",
description:
"Trouvez des réponses aux questions fréquemment posées sur PrivyDrop, y compris comment envoyer des fichiers, partager le contenu du presse-papiers et assurer des transferts de données sécurisés et privés.",
keywords:
"PrivyDrop FAQ,questions fréquemment posées,FAQ de partage de fichiers sécurisé,aide au partage de données privées,transfert de fichiers chiffré de bout en bout,support de partage de presse-papiers sécurisé,comment utiliser PrivyDrop,FAQ de transfert de fichiers,questions de partage axées sur la confidentialité,dépannage PrivyDrop",
},
features: {
title:
"Fonctionnalités Principales de PrivyDrop : 6 Avantages Clés du Transfert P2P | Sécurisé et Illimité",
description:
"Découvrez les fonctionnalités principales de PrivyDrop : transfert direct P2P chiffré de bout en bout, taille de fichier illimitée, transferts reprenables, collaboration d'équipe, conception écologique et transparence open source. Vivez un partage de fichiers vraiment sécurisé et privé.",
keywords:
"fonctionnalités de transfert de fichiers P2P,transfert chiffré de bout en bout,transfert de fichiers reprendre,partage de fichiers illimité,outil de collaboration d'équipe,transfert de fichiers écologique,partage de fichiers open source,transfert de données privées,partage de fichiers WebRTC,synchronisation de fichiers sécurisée,transfert de fichiers inter-appareils,traitement de fichiers local",
},
help: {
title: "Aide et Support PrivyDrop",
@@ -38,7 +34,7 @@ export const fr: Messages = {
privacy: {
title: "Politique de confidentialité de PrivyDrop",
description:
"Comprenez comment PrivyDrop protège votre vie privée et vos données, y compris des détails sur la collecte d'informations, le stockage et la sécurité des données, ainsi que notre engagement à ne pas partager vos données avec des tiers.",
"Comprenez comment PrivyDrop protège votre vie privée et vos données, y compris les détails sur la collecte d'informations, le stockage et la sécurité des données, et notre engagement à ne pas partager vos données avec des tiers.",
},
terms: {
title: "Conditions d'utilisation de PrivyDrop",
@@ -50,337 +46,406 @@ export const fr: Messages = {
"Blog PrivyDrop - Partage de fichiers P2P privé et collaboration",
description:
"Découvrez des conseils pour un partage de fichiers sécurisé, des stratégies de collaboration axées sur la confidentialité et comment tirer parti de P2P et WebRTC pour des transferts plus sûrs.",
keywords:
"partage de fichiers sécurisé,transfert de fichiers p2p,collaboration privée,webrtc,chiffrement de bout en bout,collaboration d'équipe,outils de confidentialité",
},
},
text: {
Header: {
Home_dis: "Accueil",
Blog_dis: "Blog",
About_dis: "À propos",
Help_dis: "Aide",
FAQ_dis: "FAQ",
Terms_dis: "Conditions",
Privacy_dis: "Confidentialité",
Features_dis: "Fonctionnalités",
navigation: {
home: "Accueil",
blog: "Blog",
about: "À propos",
help: "Aide",
faq: "FAQ",
features: "Fonctionnalités",
terms: "Conditions",
privacy: "Confidentialité",
},
Footer: {
CopyrightNotice: "PrivyDrop. Tous droits réservés.",
Terms_dis: "Conditions d'utilisation",
Privacy_dis: "Politique de confidentialité",
SupportedLanguages: "Langues prises en charge",
footer: {
copyright: "PrivyDrop. Tous droits réservés.",
terms: "Conditions d'utilisation",
privacy: "Politique de confidentialité",
supportedLanguages: "Langues prises en charge",
},
blog: {
list_title: "Blog",
list_subtitle: "Derniers articles et mises à jour",
recent_posts: "Articles récents",
tags: "Étiquettes",
read_more: "En savoir plus",
by: "par",
post_not_found: "Article introuvable",
toc_title: "Table des matières",
tag_title_prefix: "Étiquette",
tag_subtitle_template: "Articles marqués avec {tag}",
tag_empty: "Aucun article trouvé pour cette étiquette.",
},
privacy: {
PrivacyPolicy_dis: "Politique de confidentialité",
policyLabel: "Politique de confidentialité",
h1: "Politique de confidentialité de PrivyDrop",
h1_P: "Chez PrivyDrop, nous nous engageons à protéger votre vie privée et à sécuriser vos informations personnelles. Cette politique de confidentialité décrit comment nous collectons, utilisons et protégeons les données que vous fournissez lors de l'utilisation de notre service.",
h2_1: "Collecte d'informations",
h2_1_P:
"PrivyDrop ne collecte aucune information personnelle identifiable des utilisateurs. Nous n'exigeons pas d'inscription ou de création de compte pour utiliser notre service. Les seules informations que nous collectons sont l'ID de la salle et les données de fichiers/presse-papiers que vous choisissez de partager avec d'autres utilisateurs.",
h2_2: "Stockage et sécurité des données",
h2_2_P:
"Nous ne stockons aucune de vos données sur nos serveurs. Tous les transferts de fichiers et partages de presse-papiers sont gérés avec un chiffrement de bout en bout, garantissant que vos informations restent sécurisées et accessibles uniquement par le destinataire prévu. Une fois le transfert terminé, les données sont supprimées de nos systèmes.",
h2_3: "Services tiers",
h2_3_P:
"PrivyDrop n'intègre aucun service ou plateforme tiers. Nous ne partageons ni ne vendons vos données à des tiers.",
h2_4: "Modifications de la politique de confidentialité",
h2_4_P:
h1Paragraph:
"Chez PrivyDrop, nous nous engageons à protéger votre vie privée et à sécuriser vos informations personnelles. Cette politique de confidentialité décrit comment nous collectons, utilisons et protégeons les données que vous fournissez lors de l'utilisation de notre service.",
sections: {
informationCollection: "Collecte d'informations",
informationCollectionParagraph:
"PrivyDrop ne collecte aucune information personnellement identifiable des utilisateurs. Nous ne demandons pas d'inscription ou de création de compte pour utiliser notre service. Les seules informations que nous collectons sont l'ID de la salle et les données de fichiers/presse-papiers que vous choisissez de partager avec d'autres utilisateurs.",
dataStorage: "Stockage et sécurité des données",
dataStorageParagraph:
"Nous ne stockons aucune de vos données sur nos serveurs. Tous les transferts de fichiers et le partage de presse-papiers sont gérés par un chiffrement de bout en bout, garantissant que vos informations restent sécurisées et accessibles uniquement au destinataire prévu. Une fois le transfert terminé, les données sont supprimées de nos systèmes.",
thirdPartyServices: "Services tiers",
thirdPartyServicesParagraph:
"PrivyDrop ne s'intègre à aucun service ou plateforme tiers. Nous ne partageons ni ne vendons vos données à des tiers.",
amendments: "Modifications de la politique de confidentialité",
amendmentsParagraph:
"Nous pouvons mettre à jour cette politique de confidentialité de temps à autre pour refléter les changements dans nos pratiques ou les lois applicables. Toute modification sera effective immédiatement après la publication de la politique mise à jour sur notre site web. Il est de votre responsabilité de consulter périodiquement la politique de confidentialité pour toute mise à jour.",
h2_5: "Contactez-nous",
h2_5_P:
contactUs: "Contactez-nous",
contactUsParagraph:
"Si vous avez des questions ou des préoccupations concernant nos pratiques de confidentialité, n'hésitez pas à nous contacter à l'adresse suivante :",
},
},
terms: {
TermsOfUse_dis: "Conditions d'utilisation",
useLabel: "Conditions d'utilisation",
h1: "Conditions d'utilisation de PrivyDrop",
h1_P: "En utilisant le service PrivyDrop, vous acceptez d'être lié par ces conditions d'utilisation. Si vous n'acceptez pas ces conditions, veuillez ne pas utiliser le service.",
h2_1: "Utilisation du service",
h2_1_P:
"PrivyDrop est fourni comme un service gratuit sans aucune restriction.",
h2_2: "Confidentialité et sécurité des données",
h2_2_P:
"Nous prenons très au sérieux la confidentialité et la sécurité de vos données. Tous les transferts de fichiers et partages de presse-papiers sont sécurisés avec un chiffrement de bout en bout, et nous ne stockons aucune de vos données sur nos serveurs. Cependant, nous ne pouvons pas garantir la sécurité de vos données pendant le processus de transfert, et vous utilisez le service à vos propres risques.",
h2_3: "Utilisation acceptable",
h2_3_P:
"Vous acceptez de ne pas utiliser PrivyDrop à des fins illégales, abusives ou nuisibles. Cela inclut, mais sans s'y limiter, le transfert de contenu illégal, protégé par le droit d'auteur ou malveillant, ainsi que l'utilisation du service pour harceler ou usurper l'identité d'autrui.",
h2_4: "Limitation de responsabilité",
h2_4_P:
"PrivyDrop est fourni « tel quel » sans aucune garantie. Nous ne serons pas responsables des dommages directs, indirects ou consécutifs résultant de l'utilisation de notre service, y compris, mais sans s'y limiter, la perte de données, les défaillances du système ou les interruptions de service.",
h2_5: "Modifications des conditions d'utilisation",
h2_5_P:
h1Paragraph:
"En utilisant le service PrivyDrop, vous acceptez d'être lié par ces conditions d'utilisation. Si vous n'acceptez pas ces conditions, veuillez ne pas utiliser le service.",
sections: {
useOfService: "Utilisation du service",
useOfServiceParagraph:
"PrivyDrop est fourni en tant que service gratuit sans aucune restriction.",
dataPrivacy: "Confidentialité et sécurité des données",
dataPrivacyParagraph:
"Nous prenons la confidentialité et la sécurité de vos données très au sérieux. Tous les transferts de fichiers et le partage de presse-papiers sont sécurisés par un chiffrement de bout en bout, et nous ne stockons aucune de vos données sur nos serveurs. Cependant, nous ne pouvons pas garantir la sécurité de vos données pendant le processus de transfert, et vous utilisez le service à vos propres risques.",
acceptableUse: "Utilisation acceptable",
acceptableUseParagraph:
"Vous acceptez de ne pas utiliser PrivyDrop à des fins illégales, abusives ou nuisibles. Cela inclut, sans s'y limiter, le transfert de contenu illégal, protégé par le droit d'auteur ou malveillant, ainsi que l'utilisation du service pour harceler ou usurper l'identité d'autrui.",
liability: "Limitation de responsabilité",
liabilityParagraph:
"PrivyDrop est fourni 'en l'état' sans aucune garantie. Nous ne serons pas responsables des dommages directs, indirects ou consécutifs découlant de l'utilisation de notre service, y compris, mais sans s'y limiter, la perte de données, les défaillances du système ou les interruptions de service.",
changes: "Modifications des conditions d'utilisation",
changesParagraph:
"Nous nous réservons le droit de mettre à jour ces conditions d'utilisation à tout moment. Toute modification sera effective immédiatement après la publication des conditions mises à jour sur notre site web. Il est de votre responsabilité de consulter périodiquement les conditions d'utilisation pour toute modification.",
},
},
help: {
Help_dis: "Aide",
label: "Aide",
h1: "Aide et support de PrivyDrop",
h1_P: "Nous sommes là pour vous aider à tirer le meilleur parti de PrivyDrop. Si vous avez des questions ou besoin d'assistance, n'hésitez pas à nous contacter.",
h2_1: "Contactez-nous",
h2_1_P1: "Vous pouvez nous envoyer un e-mail à l'adresse suivante :",
h2_1_P2: ". Nous vous répondrons dans les 24 heures.",
h2_2: "Réseaux sociaux",
h2_2_P: "Vous pouvez également nous trouver sur les réseaux sociaux :",
h2_3: "Ressources supplémentaires",
h2_3_P:
"Pour plus d'informations sur PrivyDrop, consultez les pages suivantes :",
h1Paragraph:
"Nous sommes là pour vous aider à tirer le meilleur parti de PrivyDrop. Si vous avez des questions ou besoin d'assistance, n'hésitez pas à nous contacter.",
sections: {
contactUs: "Contactez-nous",
contactUsParagraph1: "Vous pouvez nous envoyer un e-mail à",
contactUsParagraph2: ". Nous vous répondrons dans les 24 heures.",
socialMedia: "Médias sociaux",
socialMediaParagraph: "Vous pouvez également nous trouver sur les réseaux sociaux :",
additionalResources: "Ressources supplémentaires",
additionalResourcesParagraph:
"Pour plus d'informations sur PrivyDrop, veuillez consulter les pages suivantes :",
},
},
about: {
h1: "À propos de PrivyDrop",
P1: "PrivyDrop est un outil gratuit et sécurisé de transfert de fichiers et de partage de presse-papiers conçu avec la confidentialité et la facilité d'utilisation à l'esprit. Notre mission est de fournir une solution simple mais puissante pour transférer des fichiers et partager du contenu entre appareils sans aucune restriction.",
P2: "Au cœur de PrivyDrop se trouve notre engagement envers la sécurité et la confidentialité. Nous utilisons un chiffrement de bout en bout pour garantir que vos données sont protégées pendant le processus de transfert, et nous ne stockons jamais vos fichiers ou le contenu de votre presse-papiers sur nos serveurs. Cela signifie que vos données restent locales et sous votre contrôle.",
P3: "Avec PrivyDrop, vous pouvez partager facilement du texte, des images et des fichiers de toute taille sans avoir besoin de vous inscrire ou de vous connecter. Notre plateforme est conçue pour être rapide, efficace et respectueuse de l'environnement, avec un accent sur une expérience fluide et conviviale.",
P4: "Nous croyons en l'autonomisation des utilisateurs pour qu'ils prennent le contrôle de leur vie numérique, et PrivyDrop est notre contribution à cette vision. Nous espérons que notre outil vous aidera à partager et à collaborer en toute sécurité avec vos amis, votre famille et vos collègues, sans compromettre votre vie privée ou votre sécurité.",
P5: "Pour plus d'informations ou des questions, veuillez consulter les pages suivantes :",
paragraphs: [
"PrivyDrop est un outil gratuit et sécurisé de transfert de fichiers et de partage de presse-papiers, conçu pour la confidentialité et la facilité d'utilisation. Notre mission est de fournir une solution simple mais puissante pour transférer des fichiers et partager du contenu entre appareils sans aucune restriction.",
"Au cœur de PrivyDrop se trouve notre engagement envers la sécurité et la confidentialité. Nous utilisons un chiffrement de bout en bout pour garantir que vos données sont protégées pendant le processus de transfert, et nous ne stockons jamais vos fichiers ou le contenu de votre presse-papiers sur nos serveurs. Cela signifie que vos données restent locales et sous votre contrôle.",
"Avec PrivyDrop, vous pouvez partager sans effort du texte, des images et des fichiers de n'importe quelle taille sans avoir besoin d'inscription ou de connexion. Notre plateforme est conçue pour être rapide, efficace et respectueuse de l'environnement, avec un accent sur une expérience fluide et conviviale.",
"Nous croyons en l'autonomisation des utilisateurs pour qu'ils prennent le contrôle de leur vie numérique, et PrivyDrop est notre contribution à cette vision. Nous espérons que notre outil vous aidera à partager et à collaborer en toute sécurité avec vos amis, votre famille et vos collègues, sans compromettre votre vie privée ou votre sécurité.",
"Pour plus d'informations ou pour toute question, veuillez consulter les pages suivantes :",
],
},
HowItWorks: {
h2: "Comment ça marche",
h2_P: "Partagez des fichiers et des messages instantanément en trois étapes simples",
btn_try: "Essayez maintenant →",
step1_title: "Tapez ou choisissez des fichiers",
step1_description:
howItWorks: {
title: "Comment ça marche",
description: "Partagez des fichiers et des messages instantanément en trois étapes simples",
tryNow: "Essayez maintenant →",
step1Title: "Tapez ou choisissez des fichiers",
step1Description:
"Tapez votre message ou glissez-déposez des fichiers/dossiers dans la zone de sélection",
step2_title: "Rejoindre une salle",
step2_description:
step2Title: "Rejoindre une salle",
step2Description:
"Cliquez sur le bouton 'Rejoindre une salle' pour créer une session de partage",
step3_title: "Recevoir",
step3_description:
step3Title: "Recevoir",
step3Description:
"Entrez l'ID de la salle sur la page de réception et cliquez sur 'Rejoindre une salle' pour obtenir le contenu partagé",
},
SystemDiagram: {
h2: "Diagramme du système",
h2_P: "PrivyDrop : Vos données, votre contrôle. Simple, rapide et privé.",
systemDiagram: {
title: "Diagramme du système",
description: "PrivyDrop : Vos données, votre contrôle. Simple, rapide et privé.",
},
KeyFeatures: {
h2: "Fonctionnalités clés",
h3_1: "Direct et sécurisé",
h3_1_P:
"Vos fichiers voyagent directement de votre appareil à celui du destinataire, comme un tunnel secret auquel seuls vous deux avez accès. Avec un chiffrement de bout en bout, c'est comme si vos données parlaient une langue que seul le destinataire peut comprendre. Vous ne voulez plus partager ? Fermez simplement l'onglet de votre navigateur, et c'est comme raccrocher un appel téléphonique vous avez le contrôle.",
h3_2: "Synergie d'équipe",
h3_2_P:
"Partagez avec toute votre équipe aussi facilement qu'avec une seule personne. Comme organiser une table ronde numérique, tout le monde reçoit les fichiers simultanément. Que vous collaboriez à un projet créatif ou distribuiez des documents importants, c'est comme si tout le monde était dans la même pièce, recevant votre vision partagée en même temps. Parfait pour les séances de brainstorming, les présentations d'équipe ou tout moment où plusieurs esprits doivent se connecter.",
h3_3: "Aucune limite, gestion intelligente",
h3_3_P:
"Imaginez un pipeline magique qui peut transporter n'importe quoi, quelle que soit sa taille ! Envoyez des fichiers de toute taille, limités uniquement par l'espace disque disponible. Pour les fichiers particulièrement volumineux, choisissez où les enregistrer sur votre appareil. C'est comme avoir un service de livraison spécial qui ne ralentit pas votre ordinateur les fichiers vont directement sur le disque, gardant votre appareil rapide et réactif.",
h3_4: "Rapide comme une pensée",
h3_4_P:
"Partagez du texte, des images et même des dossiers entiers aussi vite que vous pouvez y penser. C'est comme téléporter instantanément vos affaires numériques. Besoin d'envoyer un album photo entier ou un dossier rempli de documents ? Aucun problème ! C'est aussi simple que de partager un seul fichier.",
h3_5: "Écologique et propre",
h3_5_P:
keyFeatures: {
title: "Fonctionnalités clés",
items: {
directSecure: {
title: "Direct et sécurisé",
description:
"Vos fichiers voyagent directement de votre appareil à celui du destinataire, comme un tunnel secret auquel vous seuls avez accès. Avec le chiffrement de bout en bout, c'est comme si vos données parlaient une langue que seul le destinataire prévu peut comprendre. Vous ne voulez plus partager ? Fermez simplement l'onglet de votre navigateur, et c'est comme raccrocher un appel téléphonique - vous avez le contrôle.",
},
teamSynergy: {
title: "Synergie d'équipe",
description:
"Partagez avec toute votre équipe aussi facilement qu'avec une seule personne. Comme l'organisation d'une table ronde numérique, tout le monde reçoit les fichiers simultanément. Que vous collaboriez sur un projet créatif ou que vous distribuiez des documents importants, c'est comme si tout le monde était dans la même pièce, recevant votre vision partagée en une fois. Parfait pour les sessions de remue-méninges, les présentations d'équipe ou tout moment où plusieurs esprits ont besoin de se connecter.",
},
noLimits: {
title: "Pas de limites, gestion intelligente",
description:
"Imaginez un pipeline magique capable de transporter n'importe quoi, quelle que soit sa taille ! Envoyez des fichiers de n'importe quelle taille, limités uniquement par votre espace disque. Pour ces fichiers extra-larges, choisissez où les enregistrer sur votre appareil. C'est comme avoir un service de livraison spécial qui ne ralentit pas votre ordinateur - les fichiers vont directement sur le disque, gardant votre appareil rapide et réactif.",
},
swift: {
title: "Rapide comme la pensée",
description:
"Partagez du texte, des images et même des dossiers entiers aussi vite que vous y pensez. C'est comme téléporter instantanément vos objets numériques. Besoin d'envoyer tout un album photo ou un dossier rempli de documents ? Aucun problème ! C'est aussi simple que de partager un seul fichier.",
},
greenClean: {
title: "Écologique et propre",
description:
"Nous sommes comme une version numérique d'une conversation en face à face rien n'est stocké ailleurs. Cela signifie que nous sommes très respectueux de l'environnement, utilisant un minimum de ressources. C'est comme ne laisser aucune empreinte dans le monde numérique, en gardant les choses propres et vertes pour tout le monde.",
h3_6: "Reprise des Transferts",
h3_6_P:
},
resumable: {
title: "Transferts reprenables",
description:
"Définissez un répertoire de sauvegarde pour activer automatiquement la reprise des transferts. Ne vous souciez plus des interruptions réseau ; votre partage de fichiers reprendra là où il s'est arrêté.",
},
faqs: {
FAQ_dis: "Questions fréquemment posées",
question_0:
},
},
faq: {
title: "Questions fréquemment posées",
items: [
{
question:
"Les données sont-elles vraiment stockées localement et non transférées vers d'autres serveurs ?",
answer_0:
"Oui, toutes les données sont traitées localement. Vous pouvez regarder la vidéo YouTube sur notre page d'accueil les fichiers peuvent toujours être transférés dans un réseau local même si Internet est déconnecté après avoir établi la connexion. À l'avenir, nous prévoyons d'ouvrir le code source pour que tout le monde puisse l'examiner.",
question_1: "Comment envoyer et recevoir des dossiers ?",
answer_1:
"Envoyer un dossier est aussi simple qu'envoyer un fichier. Glissez-déposez le dossier dans la zone de sélection de fichiers ou cliquez sur la zone pour le sélectionner, puis appuyez sur \"Commencer l'envoi\". Côté réception, les utilisateurs peuvent télécharger directement ou choisir un répertoire de sauvegarde avant de télécharger. Le premier enregistre en mémoire, tandis que le second enregistre directement sur le disque.",
question_2: "Puis-je changer l'ID de la salle ?",
answer_2:
"Oui, vous pouvez changer l'ID de la salle en toute chaîne de caractères que vous préférez.",
question_3: "Puis-je partager du contenu en continu ?",
answer_3:
"Tant que vous restez connecté, vous pouvez cliquer manuellement sur le bouton \"Commencer l'envoi\" pour mettre à jour le contenu partagé chaque fois qu'il change.",
question_4:
"Puis-je partager des fichiers avec plusieurs destinataires simultanément ?",
answer_4:
"Bien sûr ! Il n'y a aucune différence entre une personne qui reçoit et plusieurs personnes qui reçoivent simultanément.",
question_5:
"Mes données sont-elles sécurisées lorsque j'utilise PrivyDrop ?",
answer_5:
"Absolument sécurisées. Vos données restent toujours locales, transférées entre appareils via une connexion chiffrée de bout en bout. Toutes les données transmises sont chiffrées, garantissant que seuls vous et le destinataire pouvez y accéder.",
question_6: "Dois-je créer un compte pour utiliser PrivyDrop ?",
answer_6:
"Aucune inscription ou connexion n'est requise ouvrez simplement le site et commencez à l'utiliser. La commodité et la rapidité sont nos priorités.",
question_7: "Y a-t-il des limites de taille de fichier ?",
answer_7:
"Aucune limite de taille de fichier ou de vitesse. Tant que vous avez suffisamment d'espace disque, vous pouvez transférer des fichiers de toute taille en définissant un répertoire de sauvegarde avant le téléchargement.",
question_8:
"Puis-je partager des dossiers ou plusieurs fichiers à la fois ?",
answer_8:
'Oui, partager plusieurs fichiers ou dossiers est aussi simple que de partager un seul fichier. Vous pouvez également ajouter des fichiers au transfert il suffit de cliquer sur "Commencer l\'envoi" pour les mettre à jour pour le destinataire.',
question_9: "Comment puis-je arrêter de partager si je change d'avis ?",
answer_9:
"Arrêter un partage est aussi simple que de fermer l'onglet ou la fenêtre du navigateur. Une fois que vous faites cela, la connexion est terminée, et aucune autre donnée ne peut être transférée.",
question_10: "L'utilisation de PrivyDrop ralentit-elle mon appareil ?",
answer_10:
answer:
"Oui, toutes les données sont gérées localement. Vous pouvez consulter la vidéo YouTube sur notre page d'accueil les fichiers peuvent toujours être transférés au sein d'un réseau local même si Internet est déconnecté après l'établissement d'une connexion. À l'avenir, nous prévoyons de rendre le code open source afin que tout le monde puisse l'examiner.",
},
{
question: "Comment envoyer et recevoir des dossiers ?",
answer:
"L'envoi d'un dossier est aussi simple que l'envoi d'un fichier. Faites glisser le dossier dans la zone de sélection des fichiers ou cliquez sur la zone pour le sélectionner, puis appuyez sur le bouton 'Synchroniser'. Du côté de la réception, les utilisateurs peuvent télécharger directement ou choisir un répertoire de sauvegarde avant le téléchargement. Le premier enregistre en mémoire, tandis que le second enregistre directement sur le disque.",
},
{
question: "Puis-je changer l'ID de la salle ?",
answer: "Oui, vous pouvez changer l'ID de la salle pour n'importe quelle chaîne que vous préférez.",
},
{
question: "Puis-je partager du contenu en continu ?",
answer:
"Tant que vous restez connecté, vous pouvez cliquer manuellement sur le bouton 'Synchroniser' pour mettre à jour le contenu partagé chaque fois qu'il change.",
},
{
question: "Puis-je partager des fichiers avec plusieurs destinataires simultanément ?",
answer:
"Bien sûr ! Il n'y a aucune différence entre une personne recevant et plusieurs personnes recevant simultanément.",
},
{
question: "Mes données sont-elles sécurisées lors de l'utilisation de PrivyDrop ?",
answer:
"Absolument sécurisées. Vos données restent toujours locales, transférées entre les appareils via une connexion chiffrée de bout en bout. Toutes les données transmises sont chiffrées, garantissant que seuls vous et le destinataire pouvez y accéder.",
},
{
question: "Dois-je créer un compte pour utiliser PrivyDrop ?",
answer:
"Aucune inscription ou connexion n'est requise — il suffit d'ouvrir le site et de commencer à l'utiliser. La commodité et la rapidité sont nos priorités.",
},
{
question: "Y a-t-il des limites de taille de fichier ?",
answer:
"Aucune limite de taille de fichier ou de vitesse. Tant que vous avez suffisamment d'espace disque, vous pouvez transférer des fichiers de n'importe quelle taille en définissant un répertoire de sauvegarde avant le téléchargement.",
},
{
question: "Puis-je partager des dossiers ou plusieurs fichiers à la fois ?",
answer:
"Oui, partager plusieurs fichiers ou dossiers est aussi simple que de partager un seul fichier. Vous pouvez également ajouter des fichiers au transfert — cliquez simplement sur 'Synchroniser' pour les mettre à jour pour le destinataire.",
},
{
question: "Comment arrêter le partage si je change d'avis ?",
answer:
"Arrêter un partage est aussi simple que de fermer l'onglet ou la fenêtre du navigateur. Une fois que vous faites cela, la connexion est terminée et aucune donnée supplémentaire ne peut être transférée.",
},
{
question: "L'utilisation de PrivyDrop ralentit-elle mon appareil ?",
answer:
"Non, PrivyDrop est conçu pour être léger et efficace. Si vous définissez un répertoire de sauvegarde, toutes les données reçues sont écrites directement sur le disque, contournant la mémoire, ce qui aide à maintenir les performances de votre appareil.",
question_11: "Puis-je utiliser PrivyDrop hors ligne ?",
answer_11:
"Oui, si l'expéditeur et le destinataire sont sur le même réseau local, ils peuvent rejoindre la même salle tout en étant connectés à Internet, puis se déconnecter. Le partage de fichiers fonctionnera toujours. Vous pouvez vous référer à la vidéo YouTube sur la page d'accueil pour plus de détails.",
question_12: "PrivyDrop utilise-t-il des serveurs ?",
answer_12:
"Oui, il y a effectivement un serveur léger, qui est utilisé uniquement pour la signalisation afin d'établir une connexion chiffrée. Une fois la connexion établie, toutes les données sont transférées directement entre les appareils via la connexion chiffrée.",
question_13: "Quelle est la durée de validité des ID de salle ?",
answer_13:
},
{
question: "Puis-je utiliser PrivyDrop hors ligne ?",
answer:
"Oui, si l'expéditeur et le destinataire sont sur le même réseau local, ils peuvent rejoindre la même salle tout en étant connectés à Internet, puis s'en déconnecter. Le partage de fichiers fonctionnera toujours. Vous pouvez vous référer à la vidéo YouTube sur la page d'accueil pour plus de détails.",
},
{
question: "PrivyDrop utilise-t-il des serveurs ?",
answer:
"Oui, il y a en effet un serveur léger, utilisé uniquement pour la signalisation afin d'établir une connexion chiffrée. Une fois la connexion établie, toutes les données sont transférées directement entre les appareils via la connexion chiffrée.",
},
{
question: "Quelle est la période d'expiration des ID de salle ?",
answer:
"La validité initiale d'un ID de salle est de 24 heures. Si un destinataire rejoint la salle, la validité est automatiquement prolongée de 24 heures à partir de ce moment.",
},
clipboard_btn: {
Pasted_dis: "Collé",
Copied_dis: "Copié",
],
},
fileUploadHandler: {
NoFileChosen_tips: "Aucun fichier sélectionné",
fileChosen_tips_template:
"{fileNum} fichier(s) et {folderNum} dossier(s) sélectionné(s)",
chooseFileTips:
"Faites glisser des fichiers/dossiers n'importe où sur cette page, ou cliquez ici pour choisir.",
dragTips: "Faites glisser ici sur la page.",
chosenDiagTitle: "Choisir le type de téléversement",
chosenDiagDescription:
"Sélectionnez si vous souhaitez téléverser des fichiers ou un dossier",
SelectFile_dis: "Sélectionner des fichiers",
SelectFolder_dis: "Sélectionner un dossier",
blog: {
listTitle: "Blog",
listSubtitle: "Derniers articles et mises à jour",
recentPosts: "Articles récents",
tags: "Étiquettes",
readMore: "En savoir plus",
by: "par",
postNotFound: "Article introuvable",
tocTitle: "Table des matières",
tagTitlePrefix: "Étiquette",
tagSubtitleTemplate: "Articles marqués avec {tag}",
tagEmpty: "Aucun article trouvé pour cette étiquette.",
},
FileTransferButton: {
SavedToDisk_tips: "Fichier déjà enregistré sur le disque",
CurrentFileTransferring_tips: "Le fichier est en cours de transfert",
OtherFileTransferring_tips:
"Veuillez attendre que le transfert actuel soit terminé",
download_tips: "Cliquez pour télécharger le fichier",
PendingSave_tips: "Cliquez pour enregistrer le fichier localement", // 新增
Saved_dis: "Enregistré",
Waiting_dis: "En attente",
Download_dis: "Télécharger",
Save_dis: "Enregistrer", // 新增
common: {
clipboard: {
pasted: "Collé",
copied: "Copié",
copyError: "Échec de la copie.",
readError: "Échec de la lecture du presse-papiers.",
loading: "Chargement...",
},
FileListDisplay: {
sending_dis: "Envoi",
receiving_dis: "Réception",
finish_dis: "terminé",
delete_dis: "Supprimer",
downloadNum_dis: "Nombre de téléchargements",
folder_tips_template:
"Nom du dossier : {name} ({num} fichiers et {size}) au total",
folder_dis_template: " ({num} fichiers, {size})",
PopupDialog_title: "Recommandé : Choisir un répertoire de sauvegarde",
PopupDialog_description:
"Nous recommandons de sélectionner un répertoire de sauvegarde pour enregistrer directement les fichiers sur votre disque. Cela facilite le transfert de fichiers volumineux et la synchronisation efficace des dossiers.",
chooseSavePath_tips:
"Enregistrez des fichiers volumineux ou des dossiers directement dans un répertoire sélectionné. 👉",
chooseSavePath_dis: "Choisir l'emplacement de sauvegarde",
buttons: {
request: "Demander",
download: "Télécharger",
save: "Enregistrer",
copy: "Copier",
paste: "Coller",
joinRoom: "Rejoindre la salle",
leaveRoom: "Quitter la salle",
},
RetrieveMethod: {
P: "Félicitations 🎉 Le contenu partagé attend d'être récupéré :",
RoomId_tips: "Récupérer l'ID de salle : ",
copyRoomId_tips: "Copier l'ID de salle",
url_tips: "Récupérer via URL : ",
copyUrl_tips: "Copier l'URL de partage",
scanQR_tips: "Scannez le code QR pour recevoir 👇",
Copied_dis: "Copié",
Copy_QR_dis: "Copier le code QR",
download_QR_dis: "Télécharger le code QR",
},
ClipboardApp: {
fetchRoom_err: "Échec de l'obtention d'une salle. Veuillez réessayer.",
roomCheck: {
//handleShareRoomCheck
empty_msg: "L'ID de salle ne doit pas être vide",
available_msg: "La salle est disponible",
notAvailable_msg:
"La salle n'est pas disponible, veuillez en essayer une autre",
clipboard: {
tabs: {
send: "Envoyer",
retrieve: "Récupérer",
},
channelOpen_msg:
"'Le canal de données est ouvert, prêt à recevoir des données...'",
waitting_tips:
"En attente de la connexion du destinataire. Veuillez garder cette page ouverte jusqu'à la fin du transfert. Sur ordinateur, vous pouvez minimiser le navigateur ou changer d'onglet. Sur mobile, veuillez garder le navigateur au premier plan.",
joinRoom: {
EmptyMsg: "Avertissement, l'ID de salle est vide",
DuplicateMsg: "Cet ID de salle est déjà utilisé. Veuillez choisir un autre ID.",
successMsg:
"Rejoignez le salon avec succès ! Ne fermez pas cette page tant que le transfert n'est pas terminé. (Sur ordinateur, vous pouvez réduire le navigateur ou changer d'onglet ; sur mobile, ne mettez pas le navigateur en arrière-plan.)",
notExist:
"La salle que vous essayez de rejoindre n'existe pas. Seul l'expéditeur peut créer une salle.",
failMsg: "Échec de la connexion à la salle :",
},
pickSaveMsg: "Enregistrer directement sur le disque ?",
pickSaveUnsupported: "Sélecteur de répertoire non pris en charge.",
pickSaveSuccess: "Emplacement de sauvegarde défini.",
pickSaveError: "Impossible de définir l'emplacement de sauvegarde.",
roomStatus: {
senderEmptyMsg: "La salle est vide",
receiverEmptyMsg:
"Vous pouvez accepter une invitation pour rejoindre la salle",
onlyOneMsg: "Vous êtes le seul ici",
peopleMsg_template: "{peerCount} personnes dans la salle",
connected_dis: "Connecté",
senderDisconnectedMsg: "Expéditeur déconnecté",
leftRoomMsg: "Vous avez quitté la salle.",
leaveRoomBtn: "Quitter la Salle",
},
fileExistMsg: "Certains fichiers ont déjà été ajoutés.",
noFilesForFolderMsg:
"Aucun fichier trouvé dans le dossier '{folderName}'.",
zipError: "Erreur lors de la création du fichier ZIP.",
fileNotFoundMsg:
"Fichier '{fileName}' introuvable pour le téléchargement.",
confirmLeaveWhileTransferring:
"Le transfert sera interrompu. Reprenez si un répertoire de sauvegarde est défini. Quitter quand même ?",
leaveWhileTransferringSuccess: "Salle quittée, transfert interrompu",
html: {
senderTab: "Envoyer",
retrieveTab: "Récupérer",
shareTitle_dis: "Contenu partagé",
retrieveTitle_dis: "Récupérer le contenu",
RoomStatus_dis: "Statut :",
Paste_dis: "Coller",
Copy_dis: "Copier",
inputRoomIdprompt: "Votre ID de salle (modifiable) :",
joinRoomBtn: "Rejoindre la salle",
generateSimpleId_tips: "ID Simple",
generateRandomId_tips: "ID Aléatoire",
readClipboardToRoomId: "Coller l'ID de salle",
enterRoomID_placeholder: "entrez l'ID de salle",
titles: {
share: "Contenu partagé",
retrieve: "Récupérer le contenu",
retrieveMethod: "Méthode de récupération",
inputRoomId_tips: "Votre ID de salle (modifiable) :",
joinRoom_dis: "Rejoindre la salle",
SyncSending_loadingText: "Synchronisé",
SyncSending_dis: "Synchroniser",
readClipboard_dis: "Coller l'ID de salle",
retrieveRoomId_placeholder: "Entrez l'ID de salle",
RetrieveMethodTitle: "Méthode de récupération",
// New: cached ID utils
saveId_dis: "Enregistrer lID",
useCachedId_dis: "Utiliser lID en cache",
saveId_tips:
"Enregistrez lID actuel pour une réutilisation rapide",
useCachedId_tips: "Utiliser ID enregistré rapide; double-clic pour changer mode sauvegarde",
},
// New: cache messages
saveId_success: "Enregistré dans le cache",
actions: {
sync: "Synchroniser",
syncLoading: "Synchronisé",
readClipboard: "Coller l'ID de salle",
},
placeholders: {
roomId: "entrez l'ID de salle",
},
status: {
roomEmpty: "La salle est vide",
receiverCanAccept: "Vous pouvez accepter une invitation pour rejoindre la salle",
onlyOne: "Vous êtes le seul ici",
peopleCount: "{peerCount} personnes dans la salle",
connected: "Connecté",
senderDisconnected: "Expéditeur déconnecté",
leftRoom: "Vous avez quitté la salle.",
},
messages: {
fileExist: "Certains fichiers ont déjà été ajoutés.",
noFilesForFolder: "Aucun fichier trouvé dans le dossier '{folderName}'.",
zipError: "Erreur lors de la création du fichier ZIP.",
fileNotFound: "Fichier '{fileName}' introuvable pour le téléchargement.",
confirmLeave:
"Le transfert sera interrompu. Reprenez si un répertoire de sauvegarde est défini. Quitter quand même ?",
leaveSuccess: "Salle quittée, transfert interrompu",
fetchRoomError: "Échec de l'obtention d'une salle. Veuillez réessayer.",
generateShareLinkError: "Échec de la génération du lien de partage.",
leaveRoomError: "Échec de la sortie de la salle.",
validateRoomError: "Échec de la validation de la salle.",
resetSenderStateError: "Échec de la réinitialisation de l'état de l'expéditeur.",
channelOpen: "'Le canal de données est ouvert, prêt à recevoir des données...'",
waiting:
"En attente de la connexion du destinataire. Veuillez garder cette page ouverte jusqu'à la fin du transfert. Sur ordinateur, vous pouvez minimiser le navigateur ou changer d'onglet. Sur mobile, veuillez garder le navigateur au premier plan.",
},
join: {
empty: "Avertissement, l'ID de salle est vide",
duplicate: "Cet ID de salle est déjà utilisé. Veuillez choisir un autre ID.",
success:
"Rejoignez le salon avec succès ! Ne fermez pas cette page tant que le transfert n'est pas terminé. (Sur ordinateur, vous pouvez réduire le navigateur ou changer d'onglet ; sur mobile, ne mettez pas le navigateur en arrière-plan.)",
notFound:
"La salle que vous essayez de rejoindre n'existe pas. Seul l'expéditeur peut créer une salle.",
failure: "Échec de la connexion à la salle :",
inProgress: "Rejoindre la salle… (sur un réseau lent, 530 s possibles)",
slow: "Cest un peu lent — vérifiez votre réseau/VPN ou réessayez bientôt",
timeout: "Délai dépassé pour rejoindre (réseau possiblement restreint). Réessayez",
},
rtc: {
slow: "Réseau possiblement restreint — essayez de désactiver le VPN ou réessayez bientôt",
negotiating: "Dans la salle — établissement dun lien P2P direct…",
connected: "Connecté",
reconnecting: "Reconnexion…",
restored: "Connexion rétablie",
},
roomCheck: {
empty: "L'ID de salle ne doit pas être vide",
available: "La salle est disponible",
notAvailable: "La salle n'est pas disponible, veuillez en essayer une autre",
},
saveLocation: {
pickMsg: "Enregistrer directement sur le disque ?",
unsupported: "Sélecteur de répertoire non pris en charge.",
success: "Emplacement de sauvegarde défini.",
error: "Impossible de définir l'emplacement de sauvegarde.",
},
cachedId: {
save: "Enregistrer lID",
use: "Utiliser lID en cache",
saveTip: "Enregistrez lID actuel pour une réutilisation rapide",
useTip: "Utiliser ID enregistré rapide; double-clic pour changer mode sauvegarde",
saveSuccess: "Enregistré dans le cache",
},
generateId: {
simple: "ID Simple",
random: "ID Aléatoire",
},
},
fileUpload: {
noFileChosen: "Aucun fichier sélectionné",
fileChosen: "{fileNum} fichier(s) et {folderNum} dossier(s) sélectionné(s)",
chooseTip:
"Faites glisser des fichiers/dossiers n'importe où sur cette page, ou cliquez ici pour choisir.",
dragTip: "Faites glisser ici sur la page.",
dialog: {
title: "Choisir le type de téléversement",
description: "Sélectionnez si vous souhaitez téléverser des fichiers ou un dossier",
selectFile: "Sélectionner des fichiers",
selectFolder: "Sélectionner un dossier",
},
},
fileList: {
sending: "Envoi",
receiving: "Réception",
finished: "terminé",
delete: "Supprimer",
downloadCount: "Nombre de téléchargements",
folderSummary: "Nom du dossier : {name} ({num} fichiers et {size}) au total",
folderInline: " ({num} fichiers, {size})",
saveDialog: {
title: "Recommandé : Choisir un répertoire de sauvegarde",
description:
"Nous recommandons de sélectionner un répertoire de sauvegarde pour enregistrer directement les fichiers sur votre disque. Cela facilite le transfert de fichiers volumineux et la synchronisation efficace des dossiers.",
tip: "Enregistrez des fichiers volumineux ou des dossiers directement dans un répertoire sélectionné. 👉",
button: "Choisir l'emplacement de sauvegarde",
},
},
fileTransfer: {
savedToDisk: "Fichier déjà enregistré sur le disque",
currentTransferring: "Le fichier est en cours de transfert",
otherTransferring: "Veuillez attendre que le transfert actuel soit terminé",
download: "Télécharger",
pendingSave: "Cliquez pour enregistrer le fichier localement",
saved: "Enregistré",
waiting: "En attente",
},
retrieveMethod: {
intro: "Félicitations 🎉 Le contenu partagé attend d'être récupéré :",
roomIdTip: "Récupérer l'ID de salle : ",
copyRoomId: "Copier l'ID de salle",
urlTip: "Récupérer via URL : ",
copyUrl: "Copier l'URL de partage",
scanQr: "Scannez le code QR pour recevoir 👇",
copied: "Copié",
copyQr: "Copier le code QR",
downloadQr: "Télécharger le code QR",
},
home: {
h1: "Outil gratuit de transfert de fichiers et de presse-papiers en ligne sécurisé",
h1P: "Les transferts P2P reprennent là où ils ont été interrompus. Découvrez un partage de fichiers et de texte vraiment fiable, privé et inarrêtable — sans inscription, sans limite de taille ou de vitesse de fichier, et entièrement gratuit.",
h2_screenOnly:
"Essayez maintenant l'outil de transfert de fichiers et de presse-papiers sécurisé",
h2_demo: "Voyez le partage de fichiers sécurisé en action",
h2P_demo:
hero: {
title: "Outil gratuit de transfert de fichiers et de presse-papiers en ligne sécurisé",
subtitle:
"Les transferts P2P reprennent là où ils ont été interrompus. Découvrez un partage de fichiers et de texte vraiment fiable, privé et inarrêtable — sans inscription, sans limite de taille ou de vitesse de fichier, et entièrement gratuit.",
screenOnlyTitle: "Essayez maintenant l'outil de transfert de fichiers et de presse-papiers sécurisé",
},
demo: {
title: "Voyez le partage de fichiers sécurisé en action",
description:
"Découvrez comment notre partage de fichiers local et chiffré de bout en bout protège votre vie privée",
watch_tips:
"Vous pouvez également regarder la vidéo sur ces plateformes :",
youtube_tips: "Regarder PrivyDrop sur YouTube",
bilibili_tips: "Regarder PrivyDrop sur Bilibili",
watchTip: "Vous pouvez également regarder la vidéo sur ces plateformes :",
youtube: "Regarder PrivyDrop sur YouTube",
bilibili: "Regarder PrivyDrop sur Bilibili",
},
},
},
};
+334 -308
View File
@@ -3,366 +3,392 @@ import { Messages } from "@/types/messages";
export const ja: Messages = {
meta: {
home: {
title:
"PrivyDrop: オープンソース無料のP2Pファイル転送&クリップボード共有 | プライバシー第一",
description:
"PrivyDropは、サイズ制限なし、登録不要で、再開可能な転送をサポートする、オープンソースで無料の安全なP2Pファイル転送ツールです。WebRTCに基づいたエンドツーエンドの暗号化により、コードは透明で信頼性があります。テキスト、画像、フォルダのクロスデバイス共有をサポートし、チームコラボレーションやプライベートなファイル転送に最適です。",
keywords:
"再開可能なファイル転送,信頼性の高いファイル共有,中断された転送の回復,P2Pファイル再開,WebRTC再開可能転送,オープンソースファイル共有,P2Pファイル転送,安全なファイル転送,webrtcファイル共有,プライベートクリップボード,ソースコードの透明性,GitHubプロジェクト,チームコラボレーション,クロスデバイス共有,暗号化ファイル転送,登録不要のファイル共有,無制限のファイル転送,フォルダ同期,モバイルファイル転送,安全な通信,インスタントファイル共有,プライベートデータ転送",
title: "PrivyDrop: オープンソース&無料のP2Pファイル転送&クリップボード共有 | プライバシー第一",
description: "PrivyDropは、サイズ制限なし、登録不要で、再開可能な転送をサポートする、オープンソース無料の安全なP2Pファイル転送ツールです。WebRTCに基づいたエンドツーエンドの暗号化により、コードは透明で信頼性があります。テキスト、画像、フォルダのクロスデバイス共有をサポートし、チームコラボレーションやプライベートなファイル転送に最適です。",
keywords: "再開可能なファイル転送,信頼性の高いファイル共有,中断された転送の回復,P2Pファイル再開,WebRTC再開可能転送,オープンソースファイル共有,P2Pファイル転送,安全なファイル転送,webrtc file sharing,プライベートクリップボード,ソースコードの透明性,GitHubプロジェクト,チームコラボレーション,クロスデバイス共有,暗号化ファイル転送,登録不要のファイル共有,無制限のファイル転送,フォルダ同期,モバイルファイル転送,安全な通信,インスタントファイル共有,プライベートデータ転送",
},
about: {
title: "PrivyDropについて",
description:
"PrivyDropについて学び、安全でプライベートなファイル転送とクリップボード共有サービスを提供する私たちの使命、およびユーザーのプライバシーとデータ保護への取り組みについてご紹介します。",
description: "PrivyDropについて、安全でプライベートなファイル転送とクリップボード共有サービスを提供するという私たちの使命、そしてユーザーのプライバシーとデータ保護への取り組みについて学びましょう。",
},
faq: {
title: "PrivyDrop FAQ",
description:
"PrivyDropに関するよくある質問の回答を見つけましょう。ファイルの送信方法、クリップボードコンテンツの共有方法、安全でプライベートなデータ転送を確保する方法などが含まれます。",
keywords:
"PrivyDrop FAQ,よくある質問,安全なファイル共有FAQ,プライベートデータ共有ヘルプ,エンドツーエンド暗号化ファイル転送,安全なクリップボード共有サポート,PrivyDropの使用方法,ファイル転送FAQ,プライバシー重視の共有質問,PrivyDropトラブルシューティング",
description: "ファイルの送信方法、クリップボードの内容の共有方法、安全でプライベートなデータ転送の確保方法など、PrivyDropに関するよくある質問への回答をご覧ください。",
},
features: {
title:
"PrivyDropコア機能:P2Pファイル転送の6つの主要な利点 | 安全で無制限",
description:
"PrivyDropのコア機能を発見しましょう:エンドツーエンド暗号化P2P直接転送、無制限のファイルサイズ、再開可能な転送、チームコラボレーション、エコフレンドリーなデザイン、オープンソースの透明性。真に安全でプライベートなファイル共有を体験してください。",
keywords:
"P2Pファイル転送機能,エンドツーエンド暗号化転送,再開可能なファイル転送,無制限ファイル共有,チームコラボレーションツール,エコフレンドリーファイル転送,オープンソースファイル共有,プライベートデータ転送,WebRTCファイル共有,安全なファイル同期,クロスデバイスファイル転送,ローカルファイル処理",
title: "PrivyDropコア機能:P2Pファイル転送の6つの主要な利点 | 安全で無制限",
description: "PrivyDropコア機能を発見しましょう:エンドツーエンド暗号化P2P直接転送、無制限のファイルサイズ、再開可能な転送、チームコラボレーション、エコフレンドリーなデザイン、オープンソースの透明性。真に安全でプライベートなファイル共有を体験してください。",
},
help: {
title: "PrivyDropヘルプとサポート",
description:
"PrivyDropサポートへの連絡方法に関する情報や、私たちのサービスについての詳細を提供するAbout、利用規約、プライバシーポリシーページへのリンクを見つけましょう。",
description: "PrivyDropサポートへの連絡方法、およびサービスの詳細については、[について]、[利用規約]、[プライバシーポリシー]ページへのリンクをご覧ください。",
},
privacy: {
title: "PrivyDropプライバシーポリシー",
description:
"PrivyDropがどのようにあなたのプライバシーとデータを保護するか、情報収集、データ保存とセキュリティ、第三者とのデータ共有を行わないという私たちの取り組みについて理解しましょう。",
description: "情報の収集、データの保存とセキュリティ、および第三者へのデータ提供を行わないという私たちの取り組みなど、PrivyDropがあなたのプライバシーとデータをどのように保護するかを理解してください。",
},
terms: {
title: "PrivyDrop利用規約",
description:
"PrivyDropの利用規約を確認しましょう。サービスの適切な使用、データプライバシーとセキュリティ、責任の制限に関する情報が含まれます。",
description: "サービスの適切な使用、データプライバシーとセキュリティ、責任の制限に関する情報など、PrivyDropの利用規約を確認してください。",
},
blog: {
title:
"PrivyDrop ブログ - プライベートなP2Pファイル共有とコラボレーション",
description:
"安全なファイル共有のヒント、プライバシー重視のコラボレーション戦略、そしてP2PとWebRTCを活用したより安全なデータ転送について学びましょう。",
keywords:
"安全なファイル共有,p2pファイル転送,プライベートコラボレーション,webrtc,エンドツーエンド暗号化,チームコラボレーション,プライバシーツール",
title: "PrivyDrop ブログ - プライベートなP2Pファイル共有とコラボレーション",
description: "安全なファイル共有のヒント、プライバシー重視のコラボレーション戦略、そしてP2PとWebRTCを活用したより安全なデータ転送について学びましょう。",
},
},
text: {
Header: {
Home_dis: "ホーム",
Blog_dis: "ブログ",
About_dis: "について",
Help_dis: "ヘルプ",
FAQ_dis: "FAQ",
Features_dis: "機能",
Terms_dis: "利用規約",
Privacy_dis: "プライバシー",
navigation: {
home: "ホーム",
blog: "ブログ",
about: "について",
help: "ヘルプ",
faq: "FAQ",
features: "機能",
terms: "利用規約",
privacy: "プライバシー",
},
Footer: {
CopyrightNotice: "PrivyDrop. All rights reserved.",
Terms_dis: "利用規約",
Privacy_dis: "プライバシーポリシー",
SupportedLanguages: "対応言語",
},
blog: {
list_title: "ブログ",
list_subtitle: "最新の記事と更新",
recent_posts: "最新の投稿",
tags: "タグ",
read_more: "続きを読む",
by: "著者",
post_not_found: "記事が見つかりません",
toc_title: "目次",
tag_title_prefix: "タグ",
tag_subtitle_template: "「{tag}」のタグが付いた記事",
tag_empty: "このタグの記事は見つかりません。",
footer: {
copyright: "PrivyDrop. All rights reserved.",
terms: "利用規約",
privacy: "プライバシーポリシー",
supportedLanguages: "対応言語",
},
privacy: {
PrivacyPolicy_dis: "プライバシーポリシー",
policyLabel: "プライバシーポリシー",
h1: "PrivyDropプライバシーポリシー",
h1_P: "PrivyDropでは、あなたのプライバシーを保護し、個人情報を守ることに尽力しています。このプライバシーポリシーでは、私たちがサービスを利用する際に提供されるデータをどのように収集、使用、保護するかを説明します。",
h2_1: "情報収集",
h2_1_P:
"PrivyDropは、ユーザーから個人を特定できる情報を収集しません。私たちのサービスを利用するために登録やアカウント作成は必要ありません。収集する唯一の情報は、ルームIDと他のユーザーと共有するファイル/クリップボードデータです。",
h2_2: "データ保存とセキュリティ",
h2_2_P:
"私たちはあなたのデータをサーバーに保存しません。すべてのファイル転送とクリップボード共有はエンドツーエンド暗号化を使用して処理され、あなたの情報は安全に保たれ、意図した受信者のみがアクセスできます。転送が完了すると、データは私たちのシステムから削除されます。",
h2_3: "第三者サービス",
h2_3_P:
"PrivyDropは、いかなる第三者サービスやプラットフォームとも統合しません。私たちはあなたのデータを第三者と共有または販売しません。",
h2_4: "プライバシーポリシーの変更",
h2_4_P:
"私たちは、プライバシーポリシーを随時更新して、私たちの慣行や適用される法律の変更を反映する場合があります。変更は、更新されたポリシーをウェブサイトに掲載した時点で即座に有効になります。定期的にプライバシーポリシーを確認し、更新を確認するのはあなたの責任です。",
h2_5: "お問い合わせ",
h2_5_P:
"私たちのプライバシー慣行について質問や懸念がある場合は、お気軽にお問い合わせください。",
h1Paragraph: "PrivyDropでは、あなたのプライバシーを保護し、個人情報を守ることに尽力しています。このプライバシーポリシーでは、私たちがサービスを利用する際に提供されるデータをどのように収集、使用、保護するかを説明します。",
sections: {
informationCollection: "情報収集",
informationCollectionParagraph: "PrivyDropは、ユーザーから個人を特定できる情報を収集しません。私たちのサービスを利用するために登録やアカウント作成は必要ありません。収集する唯一の情報は、ルームIDと他のユーザーと共有するために選択したファイル/クリップボードデータです。",
dataStorage: "データ保存とセキュリティ",
dataStorageParagraph: "私たちはあなたのデータをサーバーに保存しません。すべてのファイル転送とクリップボード共有はエンドツーエンド暗号化を使用して処理され、あなたの情報が安全に保たれ、意図した受信者のみがアクセスできるようにします。転送が完了すると、データは私たちのシステムから削除されます。",
thirdPartyServices: "第三者サービス",
thirdPartyServicesParagraph: "PrivyDropは、第三者サービスやプラットフォームと統合しません。私たちはあなたのデータを第三者と共有したり、販売したりしません。",
amendments: "プライバシーポリシーの改訂",
amendmentsParagraph: "私たちは、私たちの慣行や適用法の変更を反映するために、このプライバシーポリシーを随時更新することがあります。変更は、更新されたポリシーをウェブサイトに掲載した時点で即座に有効になります。定期的にプライバシーポリシーを確認し、更新を確認するのはあなたの責任です。",
contactUs: "お問い合わせ",
contactUsParagraph: "私たちのプライバシー慣行について質問や懸念がある場合は、お気軽にお問い合わせください。",
},
},
terms: {
TermsOfUse_dis: "利用規約",
useLabel: "利用規約",
h1: "PrivyDrop利用規約",
h1_P: "PrivyDropサービスを利用することにより、あなたはこれらの利用規約に拘束されることに同意します。これらの規約に同意しない場合は、サービスを利用しないでください。",
h2_1: "サービスの使用",
h2_1_P: "PrivyDropは、いかなる制限もなく無料で提供されます。",
h2_2: "データプライバシーとセキュリティ",
h2_2_P:
"私たちはあなたのデータのプライバシーとセキュリティを非常に重視しています。すべてのファイル転送とクリップボード共有はエンドツーエンド暗号化で保護され、私たちはあなたのデータをサーバーに保存しません。ただし、転送プロセス中のデータのセキュリティを保証することはできず、あなたは自己責任でサービスを利用します。",
h2_3: "適切な使用",
h2_3_P:
"あなたは、PrivyDropを違法、虐待的、または有害な目的で使用しないことに同意します。これには、違法、著作権保護、または悪意のあるコンテンツの転送、および他の人を嫌がらせたりなりすましたりするためのサービスの使用が含まれますが、これに限定されません。",
h2_4: "責任の制限",
h2_4_P:
"PrivyDropは「現状のまま」で提供され、いかなる保証もありません。私たちは、データ損失、システム障害、またはサービスの中断など、私たちのサービスの使用に起因する直接的、間接的、または結果的な損害について責任を負いません。",
h2_5: "利用規約の変更",
h2_5_P:
"私たちは、これらの利用規約を随時更新する権利を留保します。変更は、更新された規約をウェブサイトに掲載した時点で即座に有効になります。定期的に利用規約を確認し、変更を確認するのはあなたの責任です。",
h1Paragraph: "PrivyDropサービスを利用することにより、あなたはこれらの利用規約に拘束されることに同意します。これらの規約に同意しない場合は、サービスを利用しないでください。",
sections: {
useOfService: "サービスの使用",
useOfServiceParagraph: "PrivyDropは、いかなる制限もなく無料サービスとして提供されます。",
dataPrivacy: "データプライバシーとセキュリティ",
dataPrivacyParagraph: "私たちはあなたのデータのプライバシーとセキュリティを非常に重視しています。すべてのファイル転送とクリップボード共有はエンドツーエンド暗号化で保護されており、私たちはあなたのデータをサーバーに保存しません。しかし、転送プロセス中のデータのセキュリティを保証することはできず、あなたは自己責任でサービスを利用するものとします。",
acceptableUse: "許容される使用",
acceptableUseParagraph: "あなたは、不法、虐待的、または有害な目的でPrivyDropを使用しないことに同意します。これには、違法、著作権で保護された、または悪意のあるコンテンツの転送、および他人を嫌がらせしたり、なりすましたりするためのサービスの使用が含まれますが、これらに限定されません。",
liability: "責任の制限",
liabilityParagraph: "PrivyDropは「現状有姿」で提供され、いかなる保証もありません。私たちは、データの損失、システム障害、またはサービスの中断を含むがこれらに限定されない、私たちのサービスの使用から生じる直接的、間接的、または結果的な損害について責任を負いません。",
changes: "利用規約の変更",
changesParagraph: "私たちは、これらの利用規約を随時更新する権利を留保します。変更は、更新された規約をウェブサイトに掲載した時点で即座に有効になります。定期的に利用規約を確認し、変更を確認するのはあなたの責任です。",
},
},
help: {
Help_dis: "ヘルプ",
label: "ヘルプ",
h1: "PrivyDropヘルプとサポート",
h1_P: "PrivyDropを最大限に活用するためのお手伝いをします。質問やサポートが必要な場合は、お気軽にお問い合わせください。",
h2_1: "お問い合わせ",
h2_1_P1: "メールでお問い合わせください。",
h2_1_P2: "。24時間以内に返信いたします。",
h2_2: "ソーシャルメディア",
h2_2_P: "ソーシャルメディアでも私たちを見つけることができます:",
h2_3: "追加リソース",
h2_3_P: "PrivyDropの詳細については、以下のページをご確認ください:",
h1Paragraph: "PrivyDropを最大限に活用するためのお手伝いをします。質問やサポートが必要な場合は、お気軽にお問い合わせください。",
sections: {
contactUs: "お問い合わせ",
contactUsParagraph1: "メールでのお問い合わせは",
contactUsParagraph2: "まで。24時間以内に返信いたします。",
socialMedia: "ソーシャルメディア",
socialMediaParagraph: "ソーシャルメディアでも私たちを見つけることができます:",
additionalResources: "追加のリソース",
additionalResourcesParagraph: "PrivyDropの詳細については、以下のページをご覧ください:",
},
},
about: {
h1: "PrivyDropについて",
P1: "PrivyDropは、プライバシーと使いやすさを考慮して設計された無料で安全なファイル転送とクリップボード共有ツールです。私たちの使命は、制限なくデバイス間でファイルを転送し、コンテンツを共有するためのシンプルで強力なソリューションを提供することです。",
P2: "PrivyDropの核心は、セキュリティとプライバシーへの取り組みです。私たちはエンドツーエンド暗号化を使用して、転送プロセス中にあなたのデータが保護されることを保証し、ファイルやクリップボードコンテンツをサーバーに保存しません。これにより、あなたのデータはローカルに保たれ、あなたの管理下に置かれます。",
P3: "PrivyDropを使用すると、登録やログインなしでテキスト、画像、任意のサイズのファイルを簡単に共有できます。私たちのプラットフォームは、高速で効率的、環境に優しい設計で、シームレスでユーザーフレンドリーな体験を提供することに重点を置いています。",
P4: "私たちは、ユーザーがデジタルライフをコントロールできるようにすることを信じており、PrivyDropはそのビジョンへの貢献です。私たちのツールが、プライバシーやセキュリティを損なうことなく、友人、家族、同僚と安全に共有し、コラボレーションするのに役立つことを願っています。",
P5: "詳細や質問については、以下のページをご覧ください:",
paragraphs: [
"PrivyDropは、プライバシーと使いやすさを念頭に置いて設計された、無料で安全なファイル転送およびクリップボード共有ツールです。私たちの使命は、制限なくデバイス間でファイルを転送し、コンテンツを共有するためのシンプルで強力なソリューションを提供することです。",
"PrivyDropの核心は、セキュリティとプライバシーへの取り組みです。転送プロセス中にデータが保護されるようにエンドツーエンド暗号化を使用し、ファイルやクリップボードの内容をサーバーに保存することはありません。つまり、データはローカルに留まり、あなたの管理下にあります。",
"PrivyDropを使用すると、登録やログインの必要なく、テキスト、画像、あらゆるサイズのファイルを簡単に共有できます。私たちのプラットフォームは、シームレスでユーザーフレンドリーな体験を提供することに重点を置いて、高速で効率的、かつ環境に優しいように設計されています。",
"私たちは、ユーザーがデジタルライフをコントロールできるようにすることを信じており、PrivyDropはそのビジョンへの私たちの貢献です。私たちのツールが、プライバシーやセキュリティを損なうことなく、友人、家族、同僚と安全に共有し、協力するのに役立つことを願っています。",
"詳細情報や質問については、以下のページをご覧ください:",
],
},
HowItWorks: {
h2: "使い方",
h2_P: "3つの簡単なステップでファイルやメッセージを即座に共有",
btn_try: "今すぐ試す →",
step1_title: "入力またはファイルを選択",
step1_description:
"メッセージを入力するか、ファイル/フォルダを選択エリアにドラッグ&ドロップ",
step2_title: "ルームに参加",
step2_description:
"「ルームに参加」ボタンをクリックして共有セッションを作成",
step3_title: "受信",
step3_description:
"受信ページでルームIDを入力し、「ルームに参加」をクリックして共有コンテンツを取得",
howItWorks: {
title: "使い方",
description: "3つの簡単なステップでファイルやメッセージを即座に共有",
tryNow: "今すぐ試す →",
step1Title: "入力またはファイルを選択",
step1Description: "メッセージを入力するか、ファイル/フォルダを選択エリアにドラッグ&ドロップ",
step2Title: "ルームに参加",
step2Description: "ルームに参加」ボタンをクリックして共有セッションを作成",
step3Title: "受信",
step3Description: "受信ページでルームIDを入力し、「ルームに参加」をクリックして共有コンテンツを取得",
},
SystemDiagram: {
h2: "システム図",
h2_P: "PrivyDrop: あなたのデータ、あなたの管理。シンプルで高速、プライベート。",
systemDiagram: {
title: "システム図",
description: "PrivyDrop: あなたのデータ、あなたの管理。シンプルで高速、プライベート。",
},
KeyFeatures: {
h2: "主な特徴",
h3_1: "直接かつ安全",
h3_1_P:
"あなたのファイルは、あなたのデバイスから受信者のデバイスに直接送信されます。エンドツーエンド暗号化により、データは意図した受信者のみが理解できる言語で話しているかのようです。共有をやめたい場合は、ブラウザタブを閉じるだけで、電話を切るかのように簡単です。あなたがコントロールします。",
h3_2: "チームシナジー",
h3_2_P:
"1人と共有するのと同じくらい簡単に、チーム全体と共有できます。デジタル円卓をホストするかのように、全員が同時にファイルを受け取ります。クリエイティブプロジェクトでのコラボレーションや重要なドキュメントの配布に最適です。全員が同じ部屋にいるかのように、共有ビジョンを同時に受け取ります。ブレインストーミングセッション、チームプレゼンテーション、または複数の人が同時に接続する必要がある場面に最適です。",
h3_3: "制限なし、スマートな処理",
h3_3_P:
"どんなに大きくても何でも運べる魔法のパイプラインを想像してください!ディスク容量さえあれば、どんなサイズのファイルでも送信できます。特に大きなファイルの場合は、デバイス上の保存場所を選択できます。コンピュータの速度を低下させない特別な配達サービスのように、ファイルは直接ディスクに保存され、デバイスの高速性と応答性を維持します。",
h3_4: "思考のように迅速",
h3_4_P:
"テキスト、画像、さらにはフォルダ全体を瞬時に共有できます。デジタルデータをテレポートさせるかのようです。写真アルバム全体やドキュメントが詰まったフォルダを送信する必要がありますか?問題ありません!単一のファイルを共有するのと同じくらい簡単です。",
h3_5: "環境に優しくクリーン",
h3_5_P:
"私たちは、デジタル版の対面会話のようなものです。他の場所には何も保存されません。これは、最小限のリソースを使用し、非常に環境に優しいことを意味します。デジタル世界に足跡を残さず、すべての人にとってクリーンで環境に優しい状態を保ちます。",
h3_6: "再開可能な転送",
h3_6_P:
"保存ディレクトリを設定すると、再開可能な転送が自動的に有効になります。ネットワークの中断を心配する必要はもうありません。ファイル共有は中断したところから再開されます。",
keyFeatures: {
title: "主な特徴",
items: {
directSecure: {
title: "直接かつ安全",
description: "あなたのファイルは、あなたたち二人だけがアクセスできる秘密のトンネルのように、あなたのデバイスから受信者のデバイスへ直接移動します。エンドツーエンド暗号化により、あなたのデータは意図した受信者だけが理解できる言語で話しているようなものです。共有を停止したいですか?ブラウザのタブを閉じるだけで、電話を切るのと同じように、あなたがコントロールできます。",
},
faqs: {
FAQ_dis: "よくある質問",
question_0:
"データは本当にローカルに保存され、他のサーバーに転送されませんか?",
answer_0:
"はい、すべてのデータはローカルで処理されます。ホームページのYouTubeビデオを確認してください。接続が確立された後、インターネットが切断されてもローカルネットワーク内でファイルを転送できます。将来的には、コードをオープンソース化して誰でも確認できるようにする予定です。",
question_1: "フォルダを送受信するにはどうすればいいですか?",
answer_1:
"フォルダを送信するのは、ファイルを送信するのと同じくらい簡単です。フォルダをファイル選択エリアにドラッグするか、エリアをクリックして選択し、「送信開始」ボタンをクリックします。受信側では、ユーザーは直接ダウンロードするか、ダウンロード前に保存ディレクトリを選択できます。前者はメモリに保存され、後者は直接ディスクに保存されます。",
question_2: "ルームIDを変更できますか?",
answer_2: "はい、ルームIDを任意の文字列に変更できます。",
question_3: "コンテンツを継続的に共有できますか?",
answer_3:
"接続が維持されている限り、共有コンテンツが変更されるたびに手動で「送信開始」ボタンをクリックして更新できます。",
question_4: "複数の受信者と同時にファイルを共有できますか?",
answer_4:
"もちろんです!1人が受信するのと複数人が同時に受信するのに違いはありません。",
question_5: "PrivyDropを使用する際にデータは安全ですか?",
answer_5:
"完全に安全です。あなたのデータは常にローカルに保たれ、暗号化されたエンドツーエンド接続を介してデバイス間で転送されます。すべての転送データは暗号化され、あなたと受信者のみがアクセスできます。",
question_6:
"PrivyDropを使用するためにアカウントを作成する必要がありますか?",
answer_6:
"登録やログインは不要です。サイトを開いてすぐに使用できます。便利さとスピードを優先しています。",
question_7: "ファイルサイズに制限はありますか?",
answer_7:
"ファイルサイズや速度に制限はありません。十分なディスク容量があれば、ダウンロード前に保存ディレクトリを設定することで、任意のサイズのファイルを転送できます。",
question_8: "フォルダや複数のファイルを一度に共有できますか?",
answer_8:
"はい、複数のファイルやフォルダを共有するのは、単一のファイルを共有するのと同じくらい簡単です。転送にファイルを追加することもできます。「送信開始」をクリックして、受信者に更新します。",
question_9: "気が変わった場合、共有を停止するにはどうすればいいですか?",
answer_9:
"共有を停止するのは、ブラウザタブやウィンドウを閉じるのと同じくらい簡単です。これを行うと、接続が終了し、それ以上のデータ転送は行われません。",
question_10: "PrivyDropを使用するとデバイスが遅くなりますか?",
answer_10:
"いいえ、PrivyDropは軽量で効率的に設計されています。保存ディレクトリを設定すると、すべての受信データはメモリをバイパスして直接ディスクに書き込まれるため、デバイスのパフォーマンスが維持されます。",
question_11: "オフラインでPrivyDropを使用できますか?",
answer_11:
"はい、送信者と受信者が同じローカルネットワーク上にある場合、インターネットに接続している間に同じルームに参加し、その後インターネットから切断してもファイル共有は機能します。詳細については、ホームページのYouTubeビデオを参照してください。",
question_12: "PrivyDropはサーバーを使用しますか?",
answer_12:
"はい、軽量のサーバーが存在しますが、暗号化接続を確立するためのシグナリングにのみ使用されます。接続が確立されると、すべてのデータは暗号化接続を介してデバイス間で直接転送されます。",
question_13: "ルームIDの有効期間はどのくらいですか?",
answer_13:
"ルームIDの初期有効期間は24時間です。受信者がルームに参加すると、その時点から24時間自動的に延長されます。",
teamSynergy: {
title: "チームシナジー",
description: "一人と共有するのと同じくらい簡単に、チーム全体と共有できます。デジタル円卓会議を開催するように、全員が同時にファイルを受け取ります。クリエイティブなプロジェクトで協力している場合でも、重要なドキュメントを配布している場合でも、全員が同じ部屋にいて、あなたの共有されたビジョンを一度に受け取っているようなものです。ブレインストーミングセッション、チームプレゼンテーション、または複数のマインドがつながる必要があるあらゆる瞬間に最適です。",
},
clipboard_btn: {
Pasted_dis: "貼り付け済み",
Copied_dis: "コピー済み",
noLimits: {
title: "制限なし、スマートな処理",
description: "どんなに大きくても運べる魔法のパイプラインを想像してみてください!ディスク容量のみが制限で、あらゆるサイズのファイルを送信できます。特大のファイルの場合は、デバイス上の保存先を選択してください。コンピュータを遅くしない特別な配送サービスのようなものです。ファイルはディスクに直接保存され、デバイスを高速でレスポンシブに保ちます。",
},
fileUploadHandler: {
NoFileChosen_tips: "ファイルが選択されていません",
fileChosen_tips_template:
"{fileNum} ファイルと {folderNum} フォルダが選択されました",
chooseFileTips:
"このページ上のどこかにファイル/フォルダをドラッグ&ドロップするか、ここをクリックして選択してください。",
dragTips: "このページ上のどこかにドラッグしてください。",
chosenDiagTitle: "アップロードタイプを選択",
chosenDiagDescription:
"ファイルまたはフォルダをアップロードするか選択してください",
SelectFile_dis: "ファイルを選択",
SelectFolder_dis: "フォルダを選択",
swift: {
title: "思考のように素早く",
description: "テキスト、画像、さらにはフォルダ全体を、思いついた瞬間に共有できます。デジタルの持ち物を即座にテレポーションさせるようなものです。フォトアルバム全体やドキュメントが詰まったフォルダを送信する必要がありますか?問題ありません!1つのファイルを共有するのと同じくらい簡単です。",
},
FileTransferButton: {
SavedToDisk_tips: "ファイルは既にディスクに保存されています",
CurrentFileTransferring_tips: "ファイルが転送中です",
OtherFileTransferring_tips: "現在の転送が完了するまでお待ちください",
download_tips: "クリックしてファイルをダウンロード",
PendingSave_tips: "クリックしてファイルをローカルに保存", // 新增
Saved_dis: "保存済み",
Waiting_dis: "待機中",
Download_dis: "ダウンロード",
Save_dis: "保存", // 新增
greenClean: {
title: "クリーンでエコ",
description: "私たちは、デジタル版の対面会話のようなものです。他の場所には何も保存されません。これは、最小限のリソースを使用し、非常に環境に優しいことを意味します。デジタル世界に足跡を残さず、すべての人にとってクリーンで環境に優しい状態を保ちます。",
},
FileListDisplay: {
sending_dis: "送信中",
receiving_dis: "受信中",
finish_dis: "完了",
delete_dis: "削除",
downloadNum_dis: "ダウンロード回数",
folder_tips_template: "フォルダ名:{name} ({num} ファイルと {size})",
folder_dis_template: " ({num} ファイル, {size})",
PopupDialog_title: "推奨: 保存ディレクトリを選択",
PopupDialog_description:
"大きなファイルを転送し、フォルダを効率的に同期するために、保存ディレクトリを選択することをお勧めします。",
chooseSavePath_tips:
"大きなファイルやフォルダを選択したディレクトリに直接保存します。👉",
chooseSavePath_dis: "保存場所を選択",
resumable: {
title: "再開可能な転送",
description: "保存ディレクトリを設定すると、再開可能な転送が自動的に有効になります。ネットワークの中断を心配する必要はもうありません。ファイル共有は中断したところから再開されます。",
},
RetrieveMethod: {
P: "おめでとう 🎉 共有コンテンツが取得待ちです:",
RoomId_tips: "ルームIDを取得:",
copyRoomId_tips: "ルームIDをコピー",
url_tips: "URLを使用して取得:",
copyUrl_tips: "共有URLをコピー",
scanQR_tips: "QRコードをスキャンして受信 👇",
Copied_dis: "コピー済み",
Copy_QR_dis: "QRコードをコピー",
download_QR_dis: "QRコードをダウンロード",
},
ClipboardApp: {
fetchRoom_err: "ルームの取得に失敗しました。もう一度お試しください。",
roomCheck: {
empty_msg: "ルームIDは空にできません",
available_msg: "ルームは利用可能です",
notAvailable_msg: "ルームは利用できません。別のルームをお試しください",
},
channelOpen_msg:
"データチャネルが開かれ、データを受信する準備ができました...",
waitting_tips:
"受信者が接続するのを待っています。転送が完了するまでこのページを開いたままにしてください。デスクトップでは、ブラウザを最小化したり、タブを切り替えたりできます。モバイルでは、ブラウザをフォアグラウンドに保ってください。",
joinRoom: {
EmptyMsg: "警告、ルームIDが空です",
DuplicateMsg: "このルームIDは既に使用されています。別のIDをご利用ください。",
successMsg:
"ルームに成功して参加しました!転送が完了するまでこのページを閉じないでください。(PCではブラウザを最小化したりタブを切り替えたりできます。モバイルではブラウザをバックグラウンドにしないでください。)",
notExist:
"参加しようとしているルームは存在しません。送信者のみがルームを作成できます。",
failMsg: "ルームへの参加に失敗しました:",
faq: {
title: "よくある質問",
items: [
{
question: "データは本当にローカルに保存され、他のサーバーに転送されませんか?",
answer: "はい、すべてのデータはローカルで処理されます。ホームページのYouTubeビデオで確認できます。接続を確立した後、インターネットが切断されてもローカルネットワーク内でファイルを転送できます。将来的に、コードをオープンソース化して誰もが確認できるようにする予定です。",
},
pickSaveMsg: "ディスクに直接保存しますか?",
pickSaveUnsupported: "ディレクトリピッカーはサポートされていません。",
pickSaveSuccess: "保存場所が設定されました。",
pickSaveError: "保存場所を設定できませんでした。",
roomStatus: {
senderEmptyMsg: "ルームは空です",
receiverEmptyMsg: "招待を受けてルームに参加できます",
onlyOneMsg: "あなただけがここにいます",
peopleMsg_template: "{peerCount} 人がルームにいます",
connected_dis: "接続済み",
senderDisconnectedMsg: "送信者が切断されました",
leftRoomMsg: "ルームを離れました。",
leaveRoomBtn: "ルームを離れる",
{
question: "フォルダを送信および受信するにはどうすればよいですか?",
answer: "フォルダの送信はファイルの送信と同じくらい簡単です。フォルダをファイル選択エリアにドラッグするか、エリアをクリックして選択し、「同期」ボタンを押してください。受信側では、ユーザーは直接ダウンロードするか、ダウンロード前に保存ディレクトリを選択できます。前者はメモリに保存され、後者はディスクに直接保存されま。",
},
fileExistMsg: "一部のファイルは既に追加されています。",
noFilesForFolderMsg: "フォルダ '{folderName}' にファイルが見つかりません。",
zipError: "ZIP の作成中にエラーが発生しました。",
fileNotFoundMsg: "ダウンロードするファイル '{fileName}' が見つかりません。",
confirmLeaveWhileTransferring: "転送が中断されます。保存先設定時は再開可能。退出しますか?",
leaveWhileTransferringSuccess: "ルームを退出しました。転送が中断されました",
html: {
senderTab: "送信",
retrieveTab: "取得",
shareTitle_dis: "共有コンテンツ",
retrieveTitle_dis: "取得コンテンツ",
RoomStatus_dis: "ステータス:",
Paste_dis: "貼り付け",
Copy_dis: "コピー",
inputRoomIdprompt: "ルームID(編集可能):",
joinRoomBtn: "ルームに参加",
generateSimpleId_tips: "シンプルID",
generateRandomId_tips: "ランダムID",
readClipboardToRoomId: "ルームIDを貼り付け",
enterRoomID_placeholder: "ルームIDを入力",
{
question: "ルームIDを変更できますか?",
answer: "はい、ルームIDを任意の文字列に変更できます。",
},
{
question: "継続的にコンテンツを共有できますか?",
answer: "接続が維持されている限り、コンテンツが変更されるたびに「同期」ボタンを手動でクリックして、共有コンテンツを更新できます。",
},
{
question: "複数の受信者と同時にファイルを共有できますか?",
answer: "もちろんです!一人が受信するのと、複数の人が同時に受信するのに違いはありません。",
},
{
question: "PrivyDropを使用する際、データは安全ですか?",
answer: "完全に安全です。データは常にローカルに留まり、暗号化されたエンドツーエンド接続を通じてデバイス間を転送されます。送信されるすべてのデータは暗号化され、あなたと受信者だけがアクセスできるようになります。",
},
{
question: "PrivyDropを使用するためにアカウントを作成する必要がありますか?",
answer: "登録やログインは不要です。サイトを開いて使い始めるだけです。利便性とスピードが私たちの優先事項です。",
},
{
question: "ファイルサイズに制限はありますか?",
answer: "ファイルサイズや速度に制限はありません。十分なディスク容量があれば、ダウンロード前に保存ディレクトリを設定することで、あらゆるサイズのファイルを転送できます。",
},
{
question: "フォルダや複数のファイルを一度に共有できますか?",
answer: "はい、複数のファイルやフォルダを共有するのは、単一のファイルを共有するのと同じくらい簡単です。転送にファイルを追加することもできます。「同期」をクリックして受信者のために更新するだけです。",
},
{
question: "気が変わった場合、共有を停止するにはどうすればよいですか?",
answer: "共有の停止は、ブラウザのタブやウィンドウを閉じるだけです。これにより接続が切断され、それ以上のデータ転送は行われません。",
},
{
question: "PrivyDropを使用するとデバイスが遅くなりますか?",
answer: "いいえ、PrivyDropは軽量で効率的に設計されています。保存ディレクトリを設定すると、受信したすべてのデータがメモリをバイパスしてディスクに直接書き込まれるため、デバイスのパフォーマンスを維持できます。",
},
{
question: "オフラインでPrivyDropを使用できますか?",
answer: "はい、送信者と受信者が同じローカルネットワーク上にいる場合、インターネットに接続した状態で同じルームに参加し、その後インターネットを切断できます。ファイル共有は引き続き機能します。詳細はホームページのYouTubeビデオを参照してください。",
},
{
question: "PrivyDropはサーバーを使用していますか?",
answer: "はい、確かに軽量なサーバーがありますが、これは暗号化された接続を確立するためのシグナリングにのみ使用されます。接続が確立されると、すべてのデータは暗号化された接続を通じてデバイス間で直接転送されます。",
},
{
question: "ルームIDの有効期間はどのくらいですか?",
answer: "ルームIDの初期有効期間は24時間です。受信者がルームに参加すると、その時点から24時間自動的に延長されます。",
},
],
},
blog: {
listTitle: "ブログ",
listSubtitle: "最新の記事と更新",
recentPosts: "最新の投稿",
tags: "タグ",
readMore: "続きを読む",
by: "著者",
postNotFound: "記事が見つかりません",
tocTitle: "目次",
tagTitlePrefix: "タグ",
tagSubtitleTemplate: "「{tag}」のタグが付いた記事",
tagEmpty: "このタグの記事は見つかりません。",
},
common: {
clipboard: {
pasted: "貼り付け済み",
copied: "コピー済み",
copyError: "コピーに失敗しました。",
readError: "クリップボードの読み取りに失敗しました。",
loading: "読み込み中...",
},
buttons: {
request: "リクエスト",
download: "ダウンロード",
save: "保存",
copy: "コピー",
paste: "貼り付け",
joinRoom: "ルームに参加",
leaveRoom: "ルームを離れる",
},
},
clipboard: {
tabs: {
send: "送信",
retrieve: "取得",
},
titles: {
share: "共有コンテンツ",
retrieve: "取得コンテンツ",
retrieveMethod: "取得方法",
inputRoomId_tips: "ルームID(編集可能):",
joinRoom_dis: "ルームに参加",
SyncSending_loadingText: "同期完了",
SyncSending_dis: "同期(どうき)",
readClipboard_dis: "ルームIDを貼り付け",
retrieveRoomId_placeholder: "ルームIDを入力",
RetrieveMethodTitle: "取得方法",
// New: cached ID utils
saveId_dis: "ID を保存",
useCachedId_dis: "保存済みIDを使用",
saveId_tips: "現在のIDを保存して次回すぐに使えるようにします",
useCachedId_tips: "保存済みIDを即使用;ダブルクリックで保存モード切替",
},
// New: cache messages
saveId_success: "キャッシュに保存しました",
actions: {
sync: "同期(どうき)",
syncLoading: "同期完了",
readClipboard: "ルームIDを貼り付け",
},
placeholders: {
roomId: "ルームIDを入力",
},
status: {
roomEmpty: "ルームは空です",
receiverCanAccept: "招待を受けてルームに参加できます",
onlyOne: "あなただけがここにいます",
peopleCount: "{peerCount} 人がルームにいます",
connected: "接続済み",
senderDisconnected: "送信者が切断されました",
leftRoom: "ルームを離れました。",
},
messages: {
fileExist: "一部のファイルは既に追加されています。",
noFilesForFolder: "フォルダ '{folderName}' にファイルが見つかりません。",
zipError: "ZIP の作成中にエラーが発生しました。",
fileNotFound: "ダウンロードするファイル '{fileName}' が見つかりません。",
confirmLeave: "転送が中断されます。保存先設定時は再開可能。退出しますか?",
leaveSuccess: "ルームを退出しました。転送が中断されました",
fetchRoomError: "ルームの取得に失敗しました。もう一度お試しください。",
generateShareLinkError: "共有リンクの生成に失敗しました。",
leaveRoomError: "ルームを離れるのに失敗しました。",
validateRoomError: "ルームの検証に失敗しました。",
resetSenderStateError: "送信者の状態のリセットに失敗しました。",
channelOpen: "データチャネルが開かれ、データを受信する準備ができました...",
waiting: "受信者が接続するのを待っています。転送が完了するまでこのページを開いたままにしてください。デスクトップでは、ブラウザを最小化したり、タブを切り替えたりできます。モバイルでは、ブラウザをフォアグラウンドに保ってください。",
},
join: {
empty: "警告、ルームIDが空です",
duplicate: "このルームIDは既に使用されています。別のIDをご利用ください。",
success: "ルームに成功して参加しました!転送が完了するまでこのページを閉じないでください。(PCではブラウザを最小化したりタブを切り替えたりできます。モバイルではブラウザをバックグラウンドにしないでください。)",
notFound: "参加しようとしているルームは存在しません。送信者のみがルームを作成できます。",
failure: "ルームへの参加に失敗しました:",
inProgress: "ルームに参加中…(回線が遅い環境では 5〜30 秒かかることがあります)",
slow: "少し時間がかかっています—ネットワーク/VPN をご確認のうえ、しばらくしてからお試しください",
timeout: "参加がタイムアウトしました(ネットワーク制限の可能性)。再試行してください",
},
rtc: {
slow: "ネットワークが制限されている可能性があります — VPN をオフにするか、しばらくしてから再試行してください",
negotiating: "入室済み—P2P 接続を確立しています…",
connected: "接続しました",
reconnecting: "再接続中…",
restored: "接続が回復しました",
},
roomCheck: {
empty: "ルームIDは空にできません",
available: "ルームは利用可能です",
notAvailable: "ルームは利用できません。別のルームをお試しください",
},
saveLocation: {
pickMsg: "ディスクに直接保存しますか?",
unsupported: "ディレクトリピッカーはサポートされていません。",
success: "保存場所が設定されました。",
error: "保存場所を設定できませんでした。",
},
cachedId: {
save: "ID を保存",
use: "保存済みIDを使用",
saveTip: "現在のIDを保存して次回すぐに使えるようにします",
useTip: "保存済みIDを即使用;ダブルクリックで保存モード切替",
saveSuccess: "キャッシュに保存しました",
},
generateId: {
simple: "シンプルID",
random: "ランダムID",
},
},
fileUpload: {
noFileChosen: "ファイルが選択されていません",
fileChosen: "{fileNum} ファイルと {folderNum} フォルダが選択されました",
chooseTip: "このページ上のどこかにファイル/フォルダをドラッグ&ドロップするか、ここをクリックして選択してください。",
dragTip: "このページ上のどこかにドラッグしてください。",
dialog: {
title: "アップロードタイプを選択",
description: "ファイルまたはフォルダをアップロードするか選択してください",
selectFile: "ファイルを選択",
selectFolder: "フォルダを選択",
},
},
fileList: {
sending: "送信中",
receiving: "受信中",
finished: "完了",
delete: "削除",
downloadCount: "ダウンロード回数",
folderSummary: "フォルダ名:{name} ({num} ファイルと {size})",
folderInline: " ({num} ファイル, {size})",
saveDialog: {
title: "推奨: 保存ディレクトリを選択",
description: "大きなファイルを転送し、フォルダを効率的に同期するために、保存ディレクトリを選択することをお勧めします。",
tip: "大きなファイルやフォルダを選択したディレクトリに直接保存します。👉",
button: "保存場所を選択",
},
},
fileTransfer: {
savedToDisk: "ファイルは既にディスクに保存されています",
currentTransferring: "ファイルが転送中です",
otherTransferring: "現在の転送が完了するまでお待ちください",
download: "ダウンロード",
pendingSave: "クリックしてファイルをローカルに保存",
saved: "保存済み",
waiting: "待機中",
},
retrieveMethod: {
intro: "おめでとう 🎉 共有コンテンツが取得待ちです:",
roomIdTip: "ルームIDを取得:",
copyRoomId: "ルームIDをコピー",
urlTip: "URLを使用して取得:",
copyUrl: "共有URLをコピー",
scanQr: "QRコードをスキャンして受信 👇",
copied: "コピー済み",
copyQr: "QRコードをコピー",
downloadQr: "QRコードをダウンロード",
},
home: {
h1: "無料で安全なオンラインクリップボード&ファイル転送ツール",
h1P: "P2P転送が中断した場所から再開します。真に信頼でき、プライベートで、止められないファイルとテキストの共有を体験してください—登録不要、ファイルサイズや速度の制限なし、完全無料です。",
h2_screenOnly: "今すぐ安全なクリップボード&ファイル転送ツールを試す",
h2_demo: "安全なファイル共有のデモを見る",
h2P_demo:
"ローカルファースト、エンドツーエンド暗号化されたファイル共有がどのようにプライバシーを保護するかを見てください",
watch_tips: "これらのプラットフォームでもビデオを視聴できます:",
youtube_tips: "YouTubeでPrivyDropを見る",
bilibili_tips: "BilibiliでPrivyDropを見る",
hero: {
title: "無料で安全なオンラインクリップボード&ファイル転送ツール",
subtitle: "P2P転送が中断した場所から再開します。真に信頼でき、プライベートで、止められないファイルとテキストの共有を体験してください—登録不要、ファイルサイズや速度の制限なし、完全無料です。",
screenOnlyTitle: "今すぐ安全なクリップボード&ファイル転送ツールを試す",
},
demo: {
title: "安全なファイル共有のデモを見る",
description: "ローカルファースト、エンドツーエンド暗号化されたファイル共有がどのようにプライバシーを保護するかを見てください",
watchTip: "これらのプラットフォームでもビデオを視聴できます:",
youtube: "YouTubeでPrivyDropを見る",
bilibili: "BilibiliでPrivyDropを見る",
},
},
},
};
+334 -308
View File
@@ -3,366 +3,392 @@ import { Messages } from "@/types/messages";
export const ko: Messages = {
meta: {
home: {
title:
"PrivyDrop: 오픈소스 무료 P2P 파일 전송 및 클립보드 공유 | 개인 정보 보호 우선",
description:
"PrivyDrop은 크기 제한이 없고 등록이 필요 없으며 이어받기 전송을 지원하는 오픈 소스, 무료, 보안 P2P 파일 전송 도구입니다. WebRTC 기반의 종단 간 암호화로 코드가 투명하고 신뢰할 수 있습니다. 텍스트, 이미지, 폴더의 기기 간 공유를 지원하여 팀 협업 및 개인 파일 전송에 적합합니다.",
keywords:
"이어받기 가능한 파일 전송,신뢰할 수 있는 파일 공유,중단된 전송 복구,P2P 파일 이어받기,WebRTC 이어받기 가능한 전송,오픈 소스 파일 공유,P2P 파일 전송,안전한 파일 전송,webrtc 파일 공유,개인 클립보드,소스 코드 투명성,GitHub 프로젝트,팀 협업,기기 간 공유,암호화된 파일 전송,등록 없는 파일 공유,무제한 파일 전송,폴더 동기화,모바일 파일 전송,안전한 통신,즉시 파일 공유,개인데이터 전송",
title: "PrivyDrop: 오픈소스 및 무료 P2P 파일 전송 및 클립보드 공유 | 개인 정보 보호 우선",
description: "PrivyDrop은 크기 제한이 없고 등록이 필요 없으며 이어받기 전송을 지원하는 오픈 소스, 무료, 보안 P2P 파일 전송 도구입니다. WebRTC 기반의 종단 간 암호화로 코드가 투명하고 신뢰할 수 있습니다. 텍스트, 이미지, 폴더의 기기 간 공유를 지원하여 팀 협업 및 개인 파일 전송에 적합합니다.",
keywords: "이어받기 가능한 파일 전송,신뢰할 수 있는 파일 공유,중단된 전송 복구,P2P 파일 이어받기,WebRTC 이어받기 가능한 전송,오픈 소스 파일 공유,P2P 파일 전송,안전한 파일 전송,webrtc 파일 공유,개인 클립보드,소스 코드 투명성,GitHub 프로젝트,팀 협업,기기 간 공유,암호화된 파일 전송,등록 없는 파일 공유,무제한 파일 전송,폴더 동기화,모바일 파일 전송,안전한 통신,즉시 파일 공유,개인데이터 전송",
},
about: {
title: "PrivyDrop 소개",
description:
"PrivyDrop에 대해 알아보세요. 우리는 안전하고 개인적인 파일 전송 및 클립보드 공유 서비스를 제공하기 위해 노력하며, 사용자의 개인 정보와 데이터 보호를 최우선으로 합니다.",
description: "보안 및 개인 정보 보호를 최우선으로 하는 파일 전송 및 클립보드 공유 서비스를 제공하려는 PrivyDrop의 사명과 사용자 개인 정보 및 데이터 보호에 대한 우리의 약속에 대해 알아보세요.",
},
faq: {
title: "PrivyDrop FAQ",
description:
"파일 전송 방법, 클립보드 콘텐츠 공유 방법, 안전하고 개인적인 데이터 전송 보장 방법을 포함하여 PrivyDrop에 대한 자주 묻는 질문의 답변을 찾아보세요.",
keywords:
"PrivyDrop FAQ,자주 묻는 질문,안전한 파일 공유 FAQ,개인 데이터 공유 도움말,종단간 암호화 파일 전송,안전한 클립보드 공유 지원,PrivyDrop 사용 방법,파일 전송 FAQ,개인정보 중심 공유 질문,PrivyDrop 문제 해결",
description: "파일 전송 방법, 클립보드 콘텐츠 공유 방법, 안전하고 개인적인 데이터 전송 보장 방법을 포함하여 PrivyDrop에 대한 자주 묻는 질문의 답변을 찾아보세요.",
},
features: {
title:
"PrivyDrop 핵심 기능: P2P 파일 전송의 6가지 주요 장점 | 안전하고 무제한",
description:
"PrivyDrop의 핵심 기능을 발견하세요: 종단간 암호화 P2P 직접 전송, 무제한 파일 크기, 재개 가능한 전송, 팀 협업, 친환경 설계, 오픈 소스 투명성. 진정으로 안전하고 개인적인 파일 공유를 경험하세요.",
keywords:
"P2P 파일 전송 기능,종단간 암호화 전송,재개 가능한 파일 전송,무제한 파일 공유,팀 협업 도구,친환경 파일 전송,오픈 소스 파일 공유,개인 데이터 전송,WebRTC 파일 공유,안전한 파일 동기화,교차 장치 파일 전송,로컬 파일 처리",
title: "PrivyDrop 핵심 기능: P2P 파일 전송의 6가지 주요 장점 | 안전하고 무제한",
description: "PrivyDrop 핵심 기능을 발견하세요: 종단간 암호화 P2P 직접 전송, 무제한 파일 크기, 재개 가능한 전송, 팀 협업, 친환경 설계, 오픈 소스 투명성. 진정으로 안전하고 개인적인 파일 공유를 경험하세요.",
},
help: {
title: "PrivyDrop 도움말 및 지원",
description:
"PrivyDrop 지원팀에 연락하는 방법에 대한 정보와 우리 서비스에 대한 자세한 내용을 제공하는 소개, 이용약관, 개인정보처리방침 페이지로의 링크를 찾아보세요.",
description: "PrivyDrop 지원팀에 연락하는 방법에 대한 정보와 우리 서비스에 대한 자세한 내용을 제공하는 소개, 이용약관, 개인정보처리방침 페이지로의 링크를 찾아보세요.",
},
privacy: {
title: "PrivyDrop 개인정보 보호정책",
description:
"PrivyDrop가 어떻게 귀하의 개인 정보와 데이터를 보호하는지 이해하세요. 정보 수집, 데이터 저장 및 보안에 대한 세부 사항과 제3자와 데이터를 공유하지 않겠다는 우리의 약속을 포함합니다.",
description: "정보 수집, 데이터 저장 및 보안, 제3자에게 데이터를 공유하지 않겠다는 약속을 포함하여 PrivyDrop이 귀하의 개인 정보와 데이터를 어떻게 보호하는지 이해하십시오.",
},
terms: {
title: "PrivyDrop 이용 약관",
description:
"PrivyDrop의 이용 약관을 검토하세요. 서비스의 허용 가능한 사용, 데이터 개인 정보 보호 및 보안, 책임 제한에 대한 정보를 포함합니다.",
description: "서비스의 허용 가능한 사용, 데이터 개인 정보 보호 및 보안, 책임 제한에 대한 정보를 포함하여 PrivyDrop의 이용 약관을 검토하세요.",
},
blog: {
title:
"PrivyDrop 블로그 - 개인 P2P 파일 공유 및 협업",
description:
"안전한 파일 공유 팁, 개인 정보 중심의 협업 전략, 그리고 P2P와 WebRTC를 활용한 더 안전한 데이터 전송 방법을 알아보세요.",
keywords:
"안전한 파일 공유,p2p 파일 전송,개인 협업,webrtc,종단간 암호화,팀 협업,프라이버시 도구",
title: "PrivyDrop 블로그 - 개인 P2P 파일 공유 및 협업",
description: "안전한 파일 공유 팁, 개인 정보 중심의 협업 전략, 그리고 P2P와 WebRTC를 활용한 더 안전한 데이터 전송 방법을 알아보세요.",
},
},
text: {
Header: {
Home_dis: "홈",
Blog_dis: "블로그",
About_dis: "소개",
Help_dis: "도움말",
FAQ_dis: "FAQ",
Terms_dis: "이용 약관",
Privacy_dis: "개인정보 보호",
Features_dis: "기능",
navigation: {
home: "홈",
blog: "블로그",
about: "소개",
help: "도움말",
faq: "FAQ",
features: "기능",
terms: "이용 약관",
privacy: "개인정보 보호",
},
Footer: {
CopyrightNotice: "PrivyDrop. 모든 권리 보유.",
Terms_dis: "이용 약관",
Privacy_dis: "개인정보 보호정책",
SupportedLanguages: "지원 언어",
footer: {
copyright: "PrivyDrop. 모든 권리 보유.",
terms: "이용 약관",
privacy: "개인정보 보호정책",
supportedLanguages: "지원 언어",
},
blog: {
list_title: "블로그",
list_subtitle: "최신 글과 업데이트",
recent_posts: "최근 글",
tags: "태그",
read_more: "더 보기",
by: "작성자",
post_not_found: "게시글을 찾을 수 없습니다",
toc_title: "목차",
tag_title_prefix: "태그",
tag_subtitle_template: "{tag} 태그가 달린 글",
tag_empty: "해당 태그의 글이 없습니다.",
},
privacy: {
PrivacyPolicy_dis: "개인정보 보호정책",
policyLabel: "개인정보 보호정책",
h1: "PrivyDrop 개인정보 보호정책",
h1_P: "PrivyDrop는 귀하의 개인 정보를 보호하고 안전하게 지키기 위해 최선을 다하고 있습니다. 이 개인정보 보호정책은 귀하가 우리 서비스를 사용하는 동안 제공하는 데이터를 어떻게 수집, 사용 및 보호하는지 설명합니다.",
h2_1: "정보 수집",
h2_1_P:
"PrivyDrop는 사용자로부터 개인 식별 정보를 수집하지 않습니다. 우리 서비스를 사용하기 위해 등록이나 계정 생성이 필요하지 않습니다. 우리가 수집하는 유일한 정보는 방 ID와 귀하가 다른 사용자와 공유하기 선택한 파일/클립보드 데이터입니다.",
h2_2: "데이터 저장 및 보안",
h2_2_P:
"우리는 귀하의 데이터를 서버에 저장하지 않습니다. 모든 파일 전송 및 클립보드 공유는 엔드투엔드 암호화를 사용하여 처리되며, 귀하의 정보는 안전하게 보호되고 의도된 수신자만 접근할 수 있습니다. 전송이 완료되면 데이터는 우리 시스템에서 제거됩니다.",
h2_3: "제3자 서비스",
h2_3_P:
"PrivyDrop는 제3자 서비스나 플랫폼과 통합되지 않습니다. 우리는 귀하의 데이제3자와 공유하거나 판매하지 않습니다.",
h2_4: "개인정보 보호정책 변경",
h2_4_P:
"우리는 때때로 이 개인정보 보호정책을 업데이트하여 우리의 관행이나 적용 가능한 법률의 변경 사항을 반영할 수 있습니다. 모든 변경 사항은 업데이트된 정책을 웹사이트에 게시함으로써 즉시 효력이 발생합니다. 귀하는 정기적으로 개인정보 보호정책을 검토하여 업데이트를 확인할 책임이 있습니다.",
h2_5: "문의하기",
h2_5_P:
"개인정보 보호 관행에 대해 궁금한 점이 있으면 언제든지 다음 주소로 문의하십시오:",
h1Paragraph: "PrivyDrop는 귀하의 개인 정보를 보호하고 안전하게 지키기 위해 최선을 다하고 있습니다. 이 개인정보 보호정책은 귀하가 우리 서비스를 사용하는 동안 제공하는 데이터를 어떻게 수집, 사용 및 보호하는지 설명합니다.",
sections: {
informationCollection: "정보 수집",
informationCollectionParagraph: "PrivyDrop는 사용자로부터 개인 식별 정보를 수집하지 않습니다. 서비스를 사용하기 위해 등록이나 계정 생성이 필요하지 않습니다. 우리가 수집하는 유일한 정보는 귀하가 다른 사용자와 공유하기 위해 선택한 방 ID와 파일/클립보드 데이터입니다.",
dataStorage: "데이터 저장 및 보안",
dataStorageParagraph: "우리는 귀하의 데이터를 서버에 저장하지 않습니다. 모든 파일 전송 및 클립보드 공유는 엔드투엔드 암호화를 사용하여 처리되므로 귀하의 정보는 안전하게 유지되며 의도한 수신자만 접근할 수 있습니다. 전송이 완료되면 데이터는 우리 시스템에서 삭제됩니다.",
thirdPartyServices: "제3자 서비스",
thirdPartyServicesParagraph: "PrivyDrop는 제3자 서비스 또는 플랫폼과 통합되지 않습니다. 우리는 귀하의 데이터를 제3자에게 공유하거나 판매하지 않습니다.",
amendments: "개인정보 보호정책의 수정",
amendmentsParagraph: "우리는 관행이나 관련 법률의 변경을 반영하기 위해 이 개인정보 보호정책을 수시로 업데이트할 수 있습니다. 변경 사항은 업데이트된 정책을 웹사이트에 게시하는 즉시 효력이 발생합니다. 정기적으로 개인정보 보호정책을 검토하여 업데이확인하는 것은 귀하의 책임입니다.",
contactUs: "문의하기",
contactUsParagraph: "개인정보 보호 관행에 대해 궁금한 점이 있으면 언제든지 다음 주소로 문의하십시오:",
},
},
terms: {
TermsOfUse_dis: "이용 약관",
useLabel: "이용 약관",
h1: "PrivyDrop 이용 약관",
h1_P: "PrivyDrop 서비스를 사용함으로써 귀하는 본 이용 약관에 동의하게 됩니다. 이 약관에 동의하지 않으면 서비스를 사용하지 마십시오.",
h2_1: "서비스 사용",
h2_1_P: "PrivyDrop는 어떠한 제한 없이 무료 서비스로 제공됩니다.",
h2_2: "데이터 개인 정보 보호 및 보안",
h2_2_P:
"우리는 귀하의 데이터 개인 정보 보호와 보안을 매우 중요하게 생각합니다. 모든 파일 전송 및 클립보드 공유는 엔드투엔드 암호화로 보호되며, 우리는 귀하의 데이터를 서버에 저장하지 않습니다. 그러나 전송 과정에서 데이터 보안을 보장할 수 없으며, 귀하는 자신의 책임 하에 서비스를 사용합니다.",
h2_3: "허용 가능한 사용",
h2_3_P:
"귀하는 PrivyDrop를 불법적, 남용적 또는 유해한 목적으로 사용하지 않기로 동의합니다. 이는 불법적, 저작권이 있는 또는 악성 콘텐츠의 전송 및 타인을 괴롭히거나 사칭하기 위해 서비스를 사용하는 것을 포함하되 이에 국한되지 않습니다.",
h2_4: "책임 제한",
h2_4_P:
"PrivyDrop는 어떠한 보증도 없이 '있는 그대로' 제공됩니다. 우리는 데이터 손실, 시스템 오류 또는 서비스 중단을 포함하되 이에 국한되지 않는 서비스 사용으로 인한 직접적, 간접적 또는 결과적 손해에 대해 책임을 지지 않습니다.",
h2_5: "이용 약관 변경",
h2_5_P:
"우리는 언제든지 본 이용 약관을 업데이트할 권리를 보유합니다. 모든 변경 사항은 업데이트된 약관을 웹사이트에 게시함으로써 즉시 효력이 발생합니다. 귀하는 정기적으로 이용 약관을 검토하여 변경 사항을 확인할 책임이 있습니다.",
h1Paragraph: "PrivyDrop 서비스를 사용함으로써 귀하는 본 이용 약관에 동의하게 됩니다. 이 약관에 동의하지 않으면 서비스를 사용하지 마십시오.",
sections: {
useOfService: "서비스 사용",
useOfServiceParagraph: "PrivyDrop는 아무런 제한 없이 무료 서비스로 제공됩니다.",
dataPrivacy: "데이터 개인 정보 보호 및 보안",
dataPrivacyParagraph: "우리는 귀하의 데이터 개인 정보 보호와 보안을 매우 중요하게 생각합니다. 모든 파일 전송 및 클립보드 공유는 엔드투엔드 암호화로 보호되며, 우리는 귀하의 데이터를 서버에 저장하지 않습니다. 그러나 전송 과정에서 데이터 보안을 보장할 수 없으며, 서비스 사용에 따른 위험은 귀하가 감수해야 합니다.",
acceptableUse: "허용되는 사용",
acceptableUseParagraph: "귀하는 불법적, 학대적 또는 유해한 목적으로 PrivyDrop를 사용하지 않는 데 동의합니다. 이는 불법, 저작권 침해 또는 악성 콘텐츠의 전송뿐만 아니라 타인을 괴롭히거나 사칭하기 위한 서비스 사용을 포함하되 이에 국한되지 않습니다.",
liability: "책임의 제한",
liabilityParagraph: "PrivyDrop는 어떠한 보증이나 보장 없이 \"있는 그대로\" 제공됩니다. 우리는 데이터 손실, 시스템 장애 또는 서비스 중단을 포함하여 우리 서비스의 사용으로 인해 발생하는 직접적, 간접적 또는 결과적 손해에 대해 책임을 지지 않습니다.",
changes: "이용 약관의 변경",
changesParagraph: "우리는 언제든지 본 이용 약관을 업데이트할 권리를 보유합니다. 모든 변경 사항은 업데이트된 약관을 웹사이트에 게시함으로써 즉시 효력이 발생합니다. 귀하는 정기적으로 이용 약관을 검토하여 변경 사항을 확인할 책임이 있습니다.",
},
},
help: {
Help_dis: "도움말",
label: "도움말",
h1: "PrivyDrop 도움말 및 지원",
h1_P: "PrivyDrop를 최대한 활용할 수 있도록 도와드리겠습니다. 질문이 있거나 도움이 필요하면 언제든지 문의하십시오.",
h2_1: "문의하기",
h2_1_P1: "다음 주소로 이메일을 보내주십시오:",
h2_1_P2: ". 24시간 이내에 답변드리겠습니다.",
h2_2: "소셜 미디어",
h2_2_P: "소셜 미디어에서도 저희를 찾을 수 있습니다:",
h2_3: "추가 자료",
h2_3_P: "PrivyDrop에 대한 자세한 정보는 다음 페이지를 확인하십시오:",
h1Paragraph: "PrivyDrop를 최대한 활용할 수 있도록 도와드리겠습니다. 질문이 있거나 도움이 필요하면 언제든지 문의하십시오.",
sections: {
contactUs: "문의하기",
contactUsParagraph1: "다음 주소로 이메일을 보내주실 수 있습니다:",
contactUsParagraph2: ". 24시간 이내에 답변해 드리겠습니다.",
socialMedia: "소셜 미디어",
socialMediaParagraph: "소셜 미디어에서도 우리를 찾을 수 있습니다:",
additionalResources: "추가 리소스",
additionalResourcesParagraph: "PrivyDrop에 대한 자세한 내용은 다음 페이지를 확인하십시오:",
},
},
about: {
h1: "PrivyDrop 소개",
P1: "PrivyDrop는 개인 정보 보호와 사용 편의성을 염두에 두고 설계된 무료 및 안전한 파일 전송 및 클립보드 공유 도구입니다. 우리의 목표는 제한 없이 기기 간에 파일을 전송하고 콘텐츠를 공유할 수 있는 간단하지만 강력한 솔루션을 제공하는 것입니다.",
P2: "PrivyDrop의 핵심은 보안과 개인 정보 보호에 대한 우리의 약속입니다. 우리는 엔드투엔드 암호화를 사용하여 전송 과정에서 귀하의 데이터가 보호되도록 하며, 귀하의 파일이나 클립보드 콘텐츠를 서버에 저장하지 않습니다. 이는 귀하의 데이터가 로컬로 유지되고 귀하의 통제 하에 있음을 의미합니다.",
P3: "PrivyDrop를 사용하면 등록이나 로그인 없이 텍스트, 이미지 및 모든 크기의 파일을 쉽게 공유할 수 있습니다. 우리의 플랫폼은 빠르고 효율적이며 환경 친화적으로 설계되었으며, 원활하고 사용자 친화적인 경험을 제공하는 데 중점을 둡니다.",
P4: "우리는 사용자가 디지털 생활을 통제할 수 있도록 돕는 것을 믿으며, PrivyDrop는 그 비전에 대한 우리의 기여입니다. 우리의 도구가 귀하의 개인 정보나 보안을 손상시키지 않고 친구, 가족 및 동료와 안전하게 공유하고 협력할 수 있도록 도와줄 것을 희망합니다.",
P5: "자세한 정보나 질문이 있으면 다음 페이지를 방문하십시오:",
paragraphs: [
"PrivyDrop은 개인 정보 보호와 사용 편의성을 염두에 두고 설계된 무료 보안 파일 전송 및 클립보드 공유 도구입니다. 우리의 사명은 아무런 제한 없이 기기 간에 파일을 전송하고 콘텐츠를 공유할 수 있는 단순하면서도 강력한 솔루션을 제공하는 것입니다.",
"PrivyDrop의 핵심은 보안과 개인 정보 보호에 대한 우리의 약속입니다. 우리는 엔드투엔드 암호화를 사용하여 전송 과정에서 귀하의 데이터를 보호하며, 귀하의 파일이나 클립보드 콘텐츠를 우리 서버에 저장하지 않습니다. 이는 귀하의 데이터가 로컬에 유지되며 귀하의 통제 하에 있음을 의미합니다.",
"PrivyDrop을 사용하면 등록이나 로그인 없이도 텍스트, 이미지 및 모든 크기의 파일을 손쉽게 공유할 수 있습니다. 우리의 플랫폼은 빠르고 효율적이며 환경 친화적으로 설계되었으며, 원활하고 사용자 친화적인 경험을 제공하는 데 중점을 둡니다.",
"우리는 사용자가 자신의 디지털 삶을 스스로 통제할 수 있도록 돕는 것을 믿으며, PrivyDrop은 그 비전에 대한 우리의 기여입니다. 우리의 도구가 귀하의 개인 정보나 보안을 타협하지 않고 친구, 가족 및 동료와 안전하게 공유하고 협업하는 데 도움이 되기를 바랍니다.",
"추가 정보나 질문은 다음 페이지를 방문하십시오:",
],
},
HowItWorks: {
h2: "작동 방식",
h2_P: "세 가지 간단한 단계로 파일과 메시지를 즉시 공유하세요",
btn_try: "지금 사용해 보기 →",
step1_title: "텍스트 입력 또는 파일 선택",
step1_description:
"메시지를 입력하거나 파일/폴더를 선택 영역으로 드래그 앤 드롭하세요",
step2_title: "방 참가",
step2_description: "'방 참가' 버튼을 클릭하여 공유 세션을 생성하세요",
step3_title: "받기",
step3_description:
"받기 페이지에서 방 ID를 입력하고 '방 참가'를 클릭하여 공유된 콘텐츠를 받으세요",
howItWorks: {
title: "작동 방식",
description: "세 가지 간단한 단계로 파일과 메시지를 즉시 공유하세요",
tryNow: "지금 사용해 보기 →",
step1Title: "텍스트 입력 또는 파일 선택",
step1Description: "메시지를 입력하거나 파일/폴더를 선택 영역으로 드래그 앤 드롭하세요",
step2Title: "방 참가",
step2Description: "'방 참가' 버튼을 클릭하여 공유 세션을 생성하세요",
step3Title: "받기",
step3Description: "받기 페이지에서 방 ID를 입력하고 '방 참가'를 클릭하여 공유된 콘텐츠를 받으세요",
},
SystemDiagram: {
h2: "시스템 다이어그램",
h2_P: "PrivyDrop: 귀하의 데이터, 귀하의 통제. 간단하고 빠르며 개인적입니다.",
systemDiagram: {
title: "시스템 다이어그램",
description: "PrivyDrop: 귀하의 데이터, 귀하의 통제. 간단하고 빠르며 개인적입니다.",
},
KeyFeatures: {
h2: "주요 기능",
h3_1: "직접적이고 안전한",
h3_1_P:
"귀하의 파일은 귀하의 기기에서 수신자의 기기로 직접 이동하며, 오직 귀하와 수신자만 접근할 수 있는 비밀 터널과 같습니다. 엔드투엔드 암호화를 사용하면 귀하의 데이터는 오직 의도된 수신자만 이해할 수 있는 언어를 사용하는 것과 같습니다. 더 이상 공유하고 싶지 않으세요? 브라우저 탭을 닫기만 하면 전화를 끊는 것과 같습니다 – 귀하가 통제합니다.",
h3_2: "팀 시너지",
h3_2_P:
"전체 팀과 한 사람과 공유하는 것처럼 쉽게 공유하세요. 디지털 원탁 회의를 주최하는 것처럼 모든 사람이 동시에 파일을 받습니다. 창의적인 프로젝트를 협업하거나 중요한 문서를 배포할 때, 모든 사람이 같은 방에 있는 것처럼 귀하의 공유된 비전을 동시에 받습니다. 브레인스토밍 세션, 팀 프레젠테이션 또는 여러 사람이 연결되어야 하는 순간에 완벽합니다.",
h3_3: "제한 없음, 스마트 처리",
h3_3_P:
"크기에 상관없이 모든 것을 전송할 수 있는 마법의 파이프라인을 상상해보세요! 디스크 공간만 충분하다면 어떤 크기의 파일도 전송할 수 있습니다. 특히 큰 파일의 경우 저장 위치를 선택할 수 있습니다. 이는 컴퓨터를 느리게 하지 않는 특별한 배달 서비스와 같습니다 – 파일은 직접 디스크에 저장되어 기기가 빠르고 반응적입니다.",
h3_4: "생각만큼 빠른",
h3_4_P:
"텍스트, 이미지 및 전체 폴더를 생각하는 속도만큼 빠르게 공유하세요. 디지털 자료를 순간적으로 전송하는 것과 같습니다. 전체 사진 앨범이나 문서가 가득한 폴더를 보내야 하나요? 문제 없습니다! 단일 파일을 공유하는 것만큼 쉽습니다.",
h3_5: "친환경적이고 깨끗한",
h3_5_P:
"우리는 디지털 세계에서 대면 대화와 같은 존재입니다 – 아무것도 다른 곳에 저장되지 않습니다. 이는 우리가 최소한의 리소스를 사용하여 매우 환경 친화적임을 의미합니다. 디지털 세계에 발자국을 남기지 않고 모든 사람을 위해 깨끗하고 친환경적으로 유지합니다.",
h3_6: "이어받기 가능한 전송",
h3_6_P:
"저장 디렉토리를 설정하여 이어받기 가능한 전송을 자동으로 활성화하세요. 더 이상 네트워크 중단을 걱정할 필요가 없습니다. 파일 공유는 중단된 지점에서 바로 다시 시작됩니다.",
keyFeatures: {
title: "주요 기능",
items: {
directSecure: {
title: "직접적이고 안전함",
description: "귀하의 파일은 귀하와 수신자만이 접근할 수 있는 비밀 터널처럼 귀하의 기기에서 수신자의 기기로 직접 이동합니다. 엔드투엔드 암호화를 통해 귀하의 데이터는 의도한 수신자만 이해할 수 있는 언어로 말하는 것과 같습니다. 더 이상 공유하고 싶지 않으신가요? 브라우저 탭을 닫기만 하면 전화를 끊는 것과 같습니다 - 귀하가 통제권을 가집니다.",
},
faqs: {
FAQ_dis: "자주 묻는 질문",
question_0:
"데이터가 정말로 로컬에 저장되고 다른 서버로 전송되지 않나요?",
answer_0:
"예, 모든 데이터는 로컬로 처리됩니다. 홈페이지의 YouTube 동영상을 확인하세요 – 인터넷 연결이 끊긴 후에도 로컬 네트워크 내에서 파일을 전송할 수 있습니다. 앞으로 코드를 오픈소스로 공개하여 누구나 검토할 수 있도록 할 계획입니다.",
question_1: "폴더를 어떻게 보내고 받나요?",
answer_1:
"폴더를 보내는 것은 파일을 보내는 것만큼 간단합니다. 폴더를 파일 선택 영역으로 드래그하거나 영역을 클릭하여 선택한 후 '전송 시작' 버튼을 누르세요. 받는 쪽에서는 사용자가 직접 다운로드하거나 다운로드 전 저장 디렉토리를 선택할 수 있습니다. 전자는 메모리에 저장되고 후자는 직접 디스크에 저장됩니다.",
question_2: "방 ID를 변경할 수 있나요?",
answer_2: "예, 방 ID를 원하는 문자열로 변경할 수 있습니다.",
question_3: "콘텐츠를 지속적으로 공유할 수 있나요?",
answer_3:
"연결된 상태라면 콘텐츠가 변경될 때마다 '전송 시작' 버튼을 수동으로 클릭하여 공유 콘텐츠를 업데이트할 수 있습니다.",
question_4: "여러 명의 수신자와 동시에 파일을 공유할 수 있나요?",
answer_4:
"물론입니다! 한 명이 받는 것과 여러 명이 동시에 받는 것 사이에 차이가 없습니다.",
question_5: "PrivyDrop를 사용할 때 내 데이터는 안전한가요?",
answer_5:
"절대적으로 안전합니다. 귀하의 데이터는 항상 로컬에 유지되며, 암호화된 엔드투엔드 연결을 통해 기기 간에 전송됩니다. 전송된 모든 데이터는 암호화되어 귀하와 수신자만 접근할 수 있습니다.",
question_6: "PrivyDrop를 사용하려면 계정을 만들어야 하나요?",
answer_6:
"등록이나 로그인이 필요 없습니다 – 사이트를 열고 바로 사용하세요. 편의성과 속도가 우리의 우선 순위입니다.",
question_7: "파일 크기 제한이 있나요?",
answer_7:
"파일 크기나 속도에 제한이 없습니다. 디스크 공간만 충분하다면 다운로드 전 저장 디렉토리를 설정하여 어떤 크기의 파일도 전송할 수 있습니다.",
question_8: "폴더나 여러 파일을 한 번에 공유할 수 있나요?",
answer_8:
"예, 여러 파일이나 폴더를 공유하는 것은 단일 파일을 공유하는 것만큼 간단합니다. 전송에 파일을 추가하려면 '전송 시작'을 클릭하여 수신자에게 업데이트하면 됩니다.",
question_9: "마음이 바뀌면 공유를 중지할 수 있나요?",
answer_9:
"공유를 중지하는 것은 브라우저 탭이나 창을 닫는 것만큼 간단합니다. 이렇게 하면 연결이 종료되고 더 이상 데이터를 전송할 수 없습니다.",
question_10: "PrivyDrop를 사용하면 내 기기가 느려지나요?",
answer_10:
"아니요, PrivyDrop는 가볍고 효율적으로 설계되었습니다. 저장 디렉토리를 설정하면 모든 수신 데이터는 메모리를 우회하여 직접 디스크에 기록되므로 기기 성능을 유지하는 데 도움이 됩니다.",
question_11: "오프라인에서 PrivyDrop를 사용할 수 있나요?",
answer_11:
"예, 보내는 사람과 받는 사람이 동일한 로컬 네트워크에 있다면 인터넷에 연결된 상태에서 동일한 방에 참여한 후 인터넷 연결을 끊을 수 있습니다. 파일 공유는 계속 작동합니다. 홈페이지의 YouTube 동영상을 참조하세요.",
question_12: "PrivyDrop는 서버를 사용하나요?",
answer_12:
"예, 실제로 경량 서버가 있습니다. 이 서버는 암호화된 연결을 설정하기 위한 시그널링에만 사용됩니다. 연결이 설정되면 모든 데이터는 암호화된 연결을 통해 기기 간에 직접 전송됩니다.",
question_13: "방 ID의 유효 기간은 얼마인가요?",
answer_13:
"방 ID의 초기 유효 기간은 24시간입니다. 수신자가 방에 참여하면 해당 시점부터 24시간 동안 유효 기간이 자동으로 연장됩니다.",
teamSynergy: {
title: "팀 시너지",
description: "한 사람과 공유하는 것만큼 쉽게 팀 전체와 공유하세요. 디지털 원탁 회의를 여는 것처럼 모두가 동시에 파일을 받습니다. 창의적인 프로젝트를 위해 협업하거나 중요한 문서를 배포할 때, 모든 사람이 같은 방에 모여 귀하의 공유된 비전을 한 번에 받는 것과 같습니다. 브레인스토밍 세션, 팀 프레젠테이션 또는 여러 사람이 연결되어야 하는 모든 순간에 완벽합니다.",
},
clipboard_btn: {
Pasted_dis: "붙여넣기 완료",
Copied_dis: "복사 완료",
noLimits: {
title: "제한 없음, 스마트한 처리",
description: "크기에 상관없이 무엇이든 운송할 수 있는 마법의 파이프라인을 상상해 보세요! 디스크 공간에 의해서만 제한되는 모든 크기의 파일을 보내세요. 초대형 파일의 경우 기기에서 저장 위치를 선택하세요. 컴퓨터 속도를 늦추지 않는 특별 배송 서비스와 같습니다 - 파일이 디스크로 직접 전달되어 기기를 빠르고 반응성 있게 유지합니다.",
},
fileUploadHandler: {
NoFileChosen_tips: "선택된 파일 없음",
fileChosen_tips_template:
"{fileNum}개의 파일 및 {folderNum}개의 폴더 선택됨",
chooseFileTips:
"파일/폴더를 이 페이지의 아무 곳이나 드래그 앤 드롭하거나, 여기서 선택하세요.",
dragTips: "이 페이지의 아무 곳이나 드래그하세요.",
chosenDiagTitle: "업로드 유형 선택",
chosenDiagDescription: "파일 또는 폴더 업로드를 선택하세요",
SelectFile_dis: "파일 선택",
SelectFolder_dis: "폴더 선택",
swift: {
title: "생각만큼 빠른 속도",
description: "생각하는 즉시 텍스트, 이미지 및 전체 폴더를 공유하세요. 디지털 물건을 즉시 텔레포트하는 것과 같습니다. 전체 사진 앨범이나 문서로 가득 찬 폴더를 보내야 하나요? 문제 없습니다! 단일 파일을 공유하는 것만큼 쉽습니다.",
},
FileTransferButton: {
SavedToDisk_tips: "파일이 이미 디스크에 저장됨",
CurrentFileTransferring_tips: "파일 전송 중",
OtherFileTransferring_tips: "현재 전송이 완료될 때까지 기다려주세요",
download_tips: "파일을 다운로드하려면 클릭하세요",
PendingSave_tips: "로컬에 파일을 저장하려면 클릭하세요", // 新增
Saved_dis: "저장됨",
Waiting_dis: "대기 중",
Download_dis: "다운로드",
Save_dis: "저장", // 新增
greenClean: {
title: "친환경적이고 깨끗한",
description: "우리는 디지털 세계에서 대면 대화와 같은 존재입니다 – 아무것도 다른 곳에 저장되지 않습니다. 이는 우리가 최소한의 리소스를 사용하여 매우 환경 친화적임을 의미합니다. 디지털 세계에 발자국을 남기지 않고 모든 사람을 위해 깨끗하고 친환경적으로 유지합니다.",
},
FileListDisplay: {
sending_dis: "전송",
receiving_dis: "수신 중",
finish_dis: "완료됨",
delete_dis: "삭제",
downloadNum_dis: "다운로드 횟수",
folder_tips_template: "폴더 이름: {name} ({num}개의 파일 및 {size})",
folder_dis_template: " ({num}개의 파일, {size})",
PopupDialog_title: "권장: 저장 디렉토리 선택",
PopupDialog_description:
"대용량 파일이나 폴더를 직접 디스크에 저장하는 것을 권장합니다. 이를 통해 대용량 파일 전송 및 폴더 동기화가 더 효율적으로 이루어집니다.",
chooseSavePath_tips:
"큰 파일이나 폴더를 선택한 디렉터리에 직접 저장합니다. 👉",
chooseSavePath_dis: "저장 위치 선택",
resumable: {
title: "이어받기 가능한 전송",
description: "저장 디렉토리를 설정하여 이어받기 가능한 전송을 자동으로 활성화하세요. 더 이상 네트워크 중단을 걱정할 필요가 없습니다. 파일 공유는 중단된 지점에서 바로 다시 시작됩니다.",
},
RetrieveMethod: {
P: "축하합니다 🎉 공유된 콘텐츠가 검색을 기다리고 있습니다:",
RoomId_tips: "방 ID 검색: ",
copyRoomId_tips: "방 ID 복사",
url_tips: "URL로 검색: ",
copyUrl_tips: "공유 URL 복사",
scanQR_tips: "QR 코드를 스캔하여 받기 👇",
Copied_dis: "복사됨",
Copy_QR_dis: "QR 코드 복사",
download_QR_dis: "QR 코드 다운로드",
},
ClipboardApp: {
fetchRoom_err: "방을 가져오지 못했습니다. 다시 시도해주세요.",
roomCheck: {
//handleShareRoomCheck
empty_msg: "방 ID는 비어 있을 수 없습니다",
available_msg: "방을 사용할 수 있습니다",
notAvailable_msg: "방을 사용할 수 없습니다. 다른 방을 시도해주세요",
},
channelOpen_msg: "'데이터 채널이 열렸습니다. 데이터 수신 준비 중...'",
waitting_tips:
"수신자가 연결될 때까지 기다리는 중입니다. 전송이 완료될 때까지 이 페이지를 열어 두세요. 데스크톱에서는 브라우저를 최소화하거나 탭을 전환할 수 있습니다. 모바일에서는 브라우저를 포그라운드에 유지하세요.",
joinRoom: {
EmptyMsg: "경고, 방 ID가 비어 있습니다",
DuplicateMsg: "이 방 ID는 이미 사용 중입니다. 다른 ID를 선택해주세요.",
successMsg:
"방에 성공적으로 입장했습니다! 전송이 완료되기 전까지 현재 페이지를 닫지 마세요. (데스크톱에서는 브라우저를 최소화하거나 탭을 전환할 수 있으며, 모바일에서는 브라우저를 백그라운드로 이동하지 마세요.)",
notExist:
"참여하려는 방이 존재하지 않습니다. 보내는 사람만 방을 만들 수 있습니다.",
failMsg: "방 참여 실패:",
faq: {
title: "자주 묻는 질문",
items: [
{
question: "데이터가 정말로 로컬에 저장되고 다른 서버로 전송되지 않나요?",
answer: "네, 모든 데이터는 로컬에서 처리됩니다. 홈페이지의 YouTube 동영상을 확인하실 수 있습니다. 연결이 설정된 후 인터넷이 끊겨도 로컬 네트워크 내에서 파일을 전송할 수 있습니다. 향후에는 모든 사람이 검토할 수 있도록 코드를 오픈 소스로 공개할 계획입니다.",
},
pickSaveMsg: "직접 디스크에 저장하시겠습니까?",
pickSaveUnsupported: "디렉토리 선택기가 지원되지 않습니다.",
pickSaveSuccess: "저장 위치가 설정되었습니다.",
pickSaveError: "저장 위치를 설정할 수 없습니다.",
roomStatus: {
senderEmptyMsg: "방이 비어 있습니다",
receiverEmptyMsg: "초대를 수락하여 방에 참여할 수 있습니다",
onlyOneMsg: "현재 방에 혼자 있습니다",
peopleMsg_template: "방에 {peerCount}명이 있습니다",
connected_dis: "연결됨",
senderDisconnectedMsg: "발신자가 연결 해제됨",
leftRoomMsg: "방을 나갔습니다.",
leaveRoomBtn: "방 나가기",
{
question: "폴더를 보내고 받는 방법은 무엇인가요?",
answer: "폴더를 보내는 것은 파일을 보내는 것만큼 간단합니다. 폴더를 파일 선택 영역으로 드래그하거나 영역을 클릭하여 선택한 다음 \"전송 시작\" 버튼을 누르세요. 받는 쪽에서는 직접 다운로드하거나 다운로드하기 전에 저장 디렉토리를 선택할 수 있습니다. 전자는 메모리에 저장되고 후자는 디스크에 직접 저장됩니다.",
},
fileExistMsg: "일부 파일이 이미 추가되었습니다.",
noFilesForFolderMsg: "폴더 '{folderName}'에서 파일을 찾을 수 없습니다.",
zipError: "ZIP 파일 생성 중 오류가 발생했습니다.",
fileNotFoundMsg: "다운로드할 파일 '{fileName}'을(를) 찾을 수 없습니다.",
confirmLeaveWhileTransferring: "전송이 중단됩니다. 저장 경로 설정 시 재개 가능. 나가시겠습니까?",
leaveWhileTransferringSuccess: "방을 나갔습니다. 전송이 중단되었습니다",
html: {
senderTab: "보내기",
retrieveTab: "검색",
shareTitle_dis: "콘텐츠 공유",
retrieveTitle_dis: "콘텐츠 검색",
RoomStatus_dis: "상태:",
Paste_dis: "붙여넣기",
Copy_dis: "복사",
inputRoomIdprompt: "방 ID (편집 가능):",
joinRoomBtn: "방 참여",
generateSimpleId_tips: "간단 ID",
generateRandomId_tips: "랜덤 ID",
readClipboardToRoomId: "방 ID 붙여넣기",
enterRoomID_placeholder: "방 ID 입력",
{
question: "방 ID를 변경할 수 있나요?",
answer: "네, 방 ID를 원하는 문자열로 변경할 수 있습니다.",
},
{
question: "콘텐츠를 지속적으로 공유할 수 있나요?",
answer: "연결이 유지되는 동안에는 콘텐츠가 변경될 때마다 \"전송 시작\" 버튼을 수동으로 클릭하여 공유된 콘텐츠를 업데이트할 수 있습니다.",
},
{
question: "여러 수신자에게 동시에 파일을 공유할 수 있나요?",
answer: "물론입니다! 한 명이 받는 것과 여러 명이 동시에 받는 것 사이에는 차이가 없습니다.",
},
{
question: "PrivyDrop을 사용할 때 내 데이터는 안전한가요?",
answer: "절대적으로 안전합니다. 귀하의 데이터는 항상 로컬에 머물며 암호화된 엔드투엔드 연결을 통해 기기 간에 전송됩니다. 모든 전송된 데이터는 암호화되어 귀하와 수신자만 접근할 수 있습니다.",
},
{
question: "PrivyDrop을 사용하려면 계정을 만들어야 하나요?",
answer: "등록이나 로그인이 필요하지 않습니다. 사이트를 열고 바로 사용을 시작하세요. 편리함과 속도가 우리의 우선 순위입니다.",
},
{
question: "파일 크기 제한이 있나요?",
answer: "파일 크기나 속도에 제한이 없습니다. 디스크 공간이 충분하다면 다운로드 전에 저장 디렉토리를 설정하여 모든 크기의 파일을 전송할 수 있습니다.",
},
{
question: "폴더나 여러 파일을 한 번에 공유할 수 있나요?",
answer: "네, 여러 파일이나 폴더를 공유하는 것은 단일 파일을 공유하는 것만큼 간단합니다. 전송 중에 파일을 추가할 수도 있습니다. \"전송 시작\"을 클릭하여 수신자에게 업데이트된 내용을 제공하세요.",
},
{
question: "공유를 중단하고 싶으면 어떻게 하나요?",
answer: "공유 중단은 브라우저 탭이나 창을 닫는 것만큼 간단합니다. 그렇게 하면 연결이 종료되고 더 이상의 데이터 전송이 불가능해집니다.",
},
{
question: "PrivyDrop을 사용하면 기기가 느려지나요?",
answer: "아니요, PrivyDrop은 가볍고 효율적으로 설계되었습니다. 저장 디렉토리를 설정하면 모든 수신된 데이터가 메모리를 거치지 않고 디스크에 직접 기록되어 기기의 성능을 유지하는 데 도움이 됩니다.",
},
{
question: "PrivyDrop을 오프라인에서 사용할 수 있나요?",
answer: "네, 발신자와 수신자가 동일한 로컬 네트워크에 있는 경우 인터넷에 연결된 상태에서 같은 방에 참여한 후 인터넷 연결을 끊을 수 있습니다. 파일 공유는 여전히 작동합니다. 자세한 내용은 홈페이지의 YouTube 동영상을 참조하십시오.",
},
{
question: "PrivyDrop은 서버를 사용하나요?",
answer: "네, 암호화된 연결을 설정하기 위한 시그널링 용도로만 사용되는 가벼운 서버가 있습니다. 연결이 설정되면 모든 데이터는 암호화된 연결을 통해 기기 간에 직접 전송됩니다.",
},
{
question: "방 ID의 만료 기간은 어떻게 되나요?",
answer: "방 ID의 초기 유효 기간은 24시간입니다. 수신자가 방에 참여하면 해당 시점부터 유효 기간이 자동으로 24시간 연장됩니다.",
},
],
},
blog: {
listTitle: "블로그",
listSubtitle: "최신 글과 업데이트",
recentPosts: "최근 글",
tags: "태그",
readMore: "더 보기",
by: "작성자",
postNotFound: "게시글을 찾을 수 없습니다",
tocTitle: "목차",
tagTitlePrefix: "태그",
tagSubtitleTemplate: "{tag} 태그가 달린 글",
tagEmpty: "해당 태그의 글이 없습니다.",
},
common: {
clipboard: {
pasted: "붙여넣기 완료",
copied: "복사 완료",
copyError: "복사에 실패했습니다.",
readError: "클립보드 읽기에 실패했습니다.",
loading: "로딩 중...",
},
buttons: {
request: "요청",
download: "다운로드",
save: "저장",
copy: "복사",
paste: "붙여넣기",
joinRoom: "방 참여",
leaveRoom: "방 나가기",
},
},
clipboard: {
tabs: {
send: "보내기",
retrieve: "검색",
},
titles: {
share: "콘텐츠 공유",
retrieve: "콘텐츠 검색",
retrieveMethod: "검색 방법",
inputRoomId_tips: "방 ID (편집 가능):",
joinRoom_dis: "방 참여",
SyncSending_loadingText: "동기화 완료",
SyncSending_dis: "동기화(동기화)",
readClipboard_dis: "방 ID 붙여넣기",
retrieveRoomId_placeholder: "방 ID 입력",
RetrieveMethodTitle: "검색 방법",
// New: cached ID utils
saveId_dis: "ID 저장",
useCachedId_dis: "저장된 ID 사용",
saveId_tips:
"현재 ID를 저장하여 다음에 빠르게 사용할 수 있어요",
useCachedId_tips: "저장된 ID 빠르게 사용;더블클릭으로 저장 모드 전환",
},
// New: cache messages
saveId_success: "캐시에 저장되었습니다",
actions: {
sync: "동기화",
syncLoading: "동기화 완료",
readClipboard: "방 ID 붙여넣기",
},
placeholders: {
roomId: "방 ID 입력",
},
status: {
roomEmpty: "방이 비어 있습니다",
receiverCanAccept: "초대를 수락하여 방에 참여할 수 있습니다",
onlyOne: "현재 방에 혼자 있습니다",
peopleCount: "방에 {peerCount}명이 있습니다",
connected: "연결됨",
senderDisconnected: "발신자가 연결 해제됨",
leftRoom: "방을 나갔습니다.",
},
messages: {
fileExist: "일부 파일이 이미 추가되었습니다.",
noFilesForFolder: "폴더 '{folderName}'에서 파일을 찾을 수 없습니다.",
zipError: "ZIP 파일 생성 중 오류가 발생했습니다.",
fileNotFound: "다운로드할 파일 '{fileName}'을(를) 찾을 수 없습니다.",
confirmLeave: "전송이 중단됩니다. 저장 경로 설정 시 재개 가능. 나가시겠습니까?",
leaveSuccess: "방을 나갔습니다. 전송이 중단되었습니다",
fetchRoomError: "방을 가져오지 못했습니다. 다시 시도해주세요.",
generateShareLinkError: "공유 링크를 생성하지 못했습니다.",
leaveRoomError: "방을 나가지 못했습니다.",
validateRoomError: "방을 검증하지 못했습니다.",
resetSenderStateError: "발신자 상태를 재설정하지 못했습니다.",
channelOpen: "'데이터 채널이 열렸습니다. 데이터 수신 준비 중...'",
waiting: "수신자가 연결될 때까지 기다리는 중입니다. 전송이 완료될 때까지 이 페이지를 열어 두세요. 데스크톱에서는 브라우저를 최소화하거나 탭을 전환할 수 있습니다. 모바일에서는 브라우저를 포그라운드에 유지하세요.",
},
join: {
empty: "경고, 방 ID가 비어 있습니다",
duplicate: "이 방 ID는 이미 사용 중입니다. 다른 ID를 선택해주세요.",
success: "방에 성공적으로 입장했습니다! 전송이 완료되기 전까지 현재 페이지를 닫지 마세요. (데스크톱에서는 브라우저를 최소화하거나 탭을 전환할 수 있으며, 모바일에서는 브라우저를 백그라운드로 이동하지 마세요.)",
notFound: "참여하려는 방이 존재하지 않습니다. 보내는 사람만 방을 만들 수 있습니다.",
failure: "방 참여 실패:",
inProgress: "방에 참여 중… (느린 네트워크에서는 5–30초가 걸릴 수 있어요)",
slow: "조금 느려 보여요 — 네트워크/VPN을 확인하거나 잠시 후 다시 시도해 주세요",
timeout: "참여 시간이 초과되었습니다(네트워크가 제한될 수 있음). 다시 시도해 주세요",
},
rtc: {
slow: "네트워크가 제한되어 있을 수 있어요 — VPN을 끄거나 잠시 후 다시 시도해 주세요",
negotiating: "입장 완료 — P2P 직접 연결을 설정하는 중…",
connected: "연결되었습니다",
reconnecting: "재연결 중…",
restored: "연결이 복구되었습니다",
},
roomCheck: {
empty: "방 ID는 비어 있을 수 없습니다",
available: "방을 사용할 수 있습니다",
notAvailable: "방을 사용할 수 없습니다. 다른 방을 시도해주세요",
},
saveLocation: {
pickMsg: "직접 디스크에 저장하시겠습니까?",
unsupported: "디렉토리 선택기가 지원되지 않습니다.",
success: "저장 위치가 설정되었습니다.",
error: "저장 위치를 설정할 수 없습니다.",
},
cachedId: {
save: "ID 저장",
use: "저장된 ID 사용",
saveTip: "현재 ID를 저장하여 다음에 빠르게 사용할 수 있어요",
useTip: "저장된 ID 빠르게 사용;더블클릭으로 저장 모드 전환",
saveSuccess: "캐시에 저장되었습니다",
},
generateId: {
simple: "간단 ID",
random: "랜덤 ID",
},
},
fileUpload: {
noFileChosen: "선택된 파일 없음",
fileChosen: "{fileNum}개의 파일 및 {folderNum}개의 폴더 선택됨",
chooseTip: "파일/폴더를 이 페이지의 아무 곳이나 드래그 앤 드롭하거나, 여기서 선택하세요.",
dragTip: "이 페이지의 아무 곳이나 드래그하세요.",
dialog: {
title: "업로드 유형 선택",
description: "파일 또는 폴더 업로드를 선택하세요",
selectFile: "파일 선택",
selectFolder: "폴더 선택",
},
},
fileList: {
sending: "전송 중",
receiving: "수신 중",
finished: "완료됨",
delete: "삭제",
downloadCount: "다운로드 횟수",
folderSummary: "폴더 이름: {name} ({num}개의 파일 및 {size})",
folderInline: " ({num}개의 파일, {size})",
saveDialog: {
title: "권장: 저장 디렉토리 선택",
description: "대용량 파일이나 폴더를 직접 디스크에 저장하는 것을 권장합니다. 이를 통해 대용량 파일 전송 및 폴더 동기화가 더 효율적으로 이루어집니다.",
tip: "큰 파일이나 폴더를 선택한 디렉터리에 직접 저장합니다. 👉",
button: "저장 위치 선택",
},
},
fileTransfer: {
savedToDisk: "파일이 이미 디스크에 저장됨",
currentTransferring: "파일 전송 중",
otherTransferring: "현재 전송이 완료될 때까지 기다려주세요",
download: "다운로드",
pendingSave: "로컬에 파일을 저장하려면 클릭하세요",
saved: "저장됨",
waiting: "대기 중",
},
retrieveMethod: {
intro: "축하합니다 🎉 공유된 콘텐츠가 검색을 기다리고 있습니다:",
roomIdTip: "방 ID 검색: ",
copyRoomId: "방 ID 복사",
urlTip: "URL로 검색: ",
copyUrl: "공유 URL 복사",
scanQr: "QR 코드를 스캔하여 받기 👇",
copied: "복사됨",
copyQr: "QR 코드 복사",
downloadQr: "QR 코드 다운로드",
},
home: {
h1: "무료 보안 온라인 클립보드 및 파일 전송 도구",
h1P: "P2P 전송이 중단된 지점에서 다시 시작됩니다. 진정으로 신뢰할 수 있고, 비공개이며, 멈출 수 없는 파일 및 텍스트 공유를 경험하세요 — 등록 필요 없음, 파일 크기나 속도 제한 없음, 완전 무료입니다.",
h2_screenOnly: "지금 보안 클립보드 및 파일 전송 도구를 사용해보세요",
h2_demo: "보안 파일 공유 작동 방식 보기",
h2P_demo:
"로컬 우선, 엔드투엔드 암호화 파일 공유가 어떻게 개인 정보를 보호하는지 확인하세요",
watch_tips: "다음 플랫폼에서도 동영상을 시청할 수 있습니다:",
youtube_tips: "YouTube에서 PrivyDrop 보기",
bilibili_tips: "Bilibili에서 PrivyDrop 보기",
hero: {
title: "무료 보안 온라인 클립보드 및 파일 전송 도구",
subtitle: "P2P 전송이 중단된 지점에서 다시 시작됩니다. 진정으로 신뢰할 수 있고, 비공개이며, 멈출 수 없는 파일 및 텍스트 공유를 경험하세요 — 등록 필요 없음, 파일 크기나 속도 제한 없음, 완전 무료입니다.",
screenOnlyTitle: "지금 보안 클립보드 및 파일 전송 도구를 사용해보세요",
},
demo: {
title: "보안 파일 공유 작동 방식 보기",
description: "로컬 우선, 엔드투엔드 암호화 파일 공유가 어떻게 개인 정보를 보호하는지 확인하세요",
watchTip: "다음 플랫폼에서도 동영상을 시청할 수 있습니다:",
youtube: "YouTube에서 PrivyDrop 보기",
bilibili: "Bilibili에서 PrivyDrop 보기",
},
},
},
};
+373 -243
View File
@@ -51,301 +51,431 @@ export const zh: Messages = {
},
},
text: {
Header: {
Home_dis: "首页",
Blog_dis: "博客",
About_dis: "关于",
Help_dis: "帮助",
FAQ_dis: "常见问题",
Features_dis: "核心特性",
Terms_dis: "条款",
Privacy_dis: "隐私",
// 导航 (原 Header)
navigation: {
home: "首页",
blog: "博客",
about: "关于",
help: "帮助",
faq: "常见问题",
features: "核心特性",
terms: "条款",
privacy: "隐私",
},
Footer: {
CopyrightNotice: "PrivyDrop 版权所有",
Terms_dis: "使用条款",
Privacy_dis: "隐私政策",
SupportedLanguages: "支持的语言",
},
blog: {
list_title: "博客",
list_subtitle: "最新文章与更新",
recent_posts: "最新文章",
tags: "标签",
read_more: "阅读更多",
by: "作者",
post_not_found: "未找到文章",
toc_title: "目录",
tag_title_prefix: "标签",
tag_subtitle_template: "包含 {tag} 标签的文章",
tag_empty: "没有找到相关文章。",
// 页脚
footer: {
copyright: "PrivyDrop 版权所有",
terms: "使用条款",
privacy: "隐私政策",
supportedLanguages: "支持的语言",
},
// 隐私政策
privacy: {
PrivacyPolicy_dis: "隐私政策",
policyLabel: "隐私政策",
h1: "PrivyDrop隐私政策",
h1_P: "PrivyDrop致力于保护您的隐私和个人信息安全。本隐私政策说明了我们如何收集、使用和保护您在使用服务时提供的数据。",
h2_1: "信息收集",
h2_1_P:
h1Paragraph:
"PrivyDrop致力于保护您的隐私和个人信息安全。本隐私政策说明了我们如何收集、使用和保护您在使用服务时提供的数据。",
sections: {
informationCollection: "信息收集",
informationCollectionParagraph:
"PrivyDrop不收集任何个人身份信息。我们不需要注册或创建账户。我们仅收集房间ID和您选择与其他用户共享的文件/剪贴板数据。",
h2_2: "数据存储和安全",
h2_2_P:
dataStorage: "数据存储和安全",
dataStorageParagraph:
"我们不在服务器上存储任何数据。所有文件传输和剪贴板共享都使用端到端加密,确保信息安全且仅供预期接收者访问。传输完成后,数据将从系统中删除。",
h2_3: "第三方服务",
h2_3_P:
thirdPartyServices: "第三方服务",
thirdPartyServicesParagraph:
"PrivyDrop不与任何第三方服务或平台集成。我们不会与任何第三方共享或出售您的数据。",
h2_4: "隐私政策修订",
h2_4_P:
amendments: "隐私政策修订",
amendmentsParagraph:
"我们可能会不时更新本隐私政策以反映我们的做法或适用法律的变更。更新后的政策将在网站上发布时立即生效。请定期查看隐私政策以了解任何更新。",
h2_5: "联系我们",
h2_5_P: "如果您对我们的隐私实践有任何问题或疑虑,请联系我们:",
contactUs: "联系我们",
contactUsParagraph:
"如果您对我们的隐私实践有任何问题或疑虑,请联系我们:",
},
},
// 使用条款
terms: {
TermsOfUse_dis: "使用条款",
useLabel: "使用条款",
h1: "PrivyDrop使用条款",
h1_P: "使用PrivyDrop服务即表示您同意遵守这些使用条款。如果您不同意这些条款,请不要使用本服务。",
h2_1: "服务使用",
h2_1_P: "PrivyDrop是一项免费服务,没有任何限制。",
h2_2: "数据隐私和安全",
h2_2_P:
h1Paragraph:
"使用PrivyDrop服务即表示您同意遵守这些使用条款。如果您不同意这些条款,请不要使用本服务。",
sections: {
useOfService: "服务使用",
useOfServiceParagraph: "PrivyDrop是一项免费服务,没有任何限制。",
dataPrivacy: "数据隐私和安全",
dataPrivacyParagraph:
"我们非常重视您的数据隐私和安全。所有文件传输和剪贴板共享都采用端到端加密,我们不在服务器上存储任何数据。但我们无法保证传输过程中的数据安全,使用本服务需自行承担风险。",
h2_3: "可接受使用",
h2_3_P:
acceptableUse: "可接受使用",
acceptableUseParagraph:
"您同意不将PrivyDrop用于任何非法、滥用或有害目的。这包括但不限于传输非法、受版权保护或恶意内容,以及使用服务骚扰或冒充他人。",
h2_4: "责任限制",
h2_4_P:
liability: "责任限制",
liabilityParagraph:
'PrivyDrop按"原样"提供,不提供任何保证。对于使用我们服务而导致的任何直接、间接或后果性损害,包括但不限于数据丢失、系统故障或服务中断,我们不承担责任。',
h2_5: "条款变更",
h2_5_P:
changes: "条款变更",
changesParagraph:
"我们保留随时更新这些使用条款的权利。更新后的条款将在网站上发布时立即生效。请定期查看使用条款以了解任何变更。",
},
help: {
Help_dis: "帮助",
h1: "PrivyDrop帮助与支持",
h1_P: "我们随时为您提供帮助,让您充分利用PrivyDrop。如果您有任何问题或需要协助,请随时联系我们。",
h2_1: "联系我们",
h2_1_P1: "您可以发送邮件至",
h2_1_P2: "。我们将在24小时内回复。",
h2_2: "社交媒体",
h2_2_P: "您也可以在社交媒体上找到我们:",
h2_3: "更多资源",
h2_3_P: "关于PrivyDrop的更多信息,请查看以下页面:",
},
// 帮助与支持
help: {
label: "帮助",
h1: "PrivyDrop帮助与支持",
h1Paragraph:
"我们随时为您提供帮助,让您充分利用PrivyDrop。如果您有任何问题或需要协助,请随时联系我们。",
sections: {
contactUs: "联系我们",
contactUsParagraph1: "您可以发送邮件至",
contactUsParagraph2: "。我们将在24小时内回复。",
socialMedia: "社交媒体",
socialMediaParagraph: "您也可以在社交媒体上找到我们:",
additionalResources: "更多资源",
additionalResourcesParagraph:
"关于PrivyDrop的更多信息,请查看以下页面:",
},
},
// 关于
about: {
h1: "关于PrivyDrop",
P1: "PrivyDrop是一款免费且安全的文件传输和剪贴板共享工具,专注于隐私保护和易用性。我们的使命是提供一个简单但强大的解决方案,让您可以不受限制地跨设备传输文件和共享内容。",
P2: "PrivyDrop的核心是我们对安全和隐私的承诺。我们使用端到端加密确保您的数据在传输过程中受到保护,绝不在服务器上存储您的文件或剪贴板内容。这意味着您的数据始终保持在本地,由您完全控制。",
P3: "使用PrivyDrop,您可以轻松共享文本、图片和任意大小的文件,无需注册或登录。我们的平台设计注重快速、高效和环保,为您提供流畅和友好的使用体验。",
P4: "我们致力于帮助用户掌控自己的数字生活,PrivyDrop正是这一愿景的体现。我们希望这个工具能帮助您安全地与朋友、家人和同事共享和协作,同时不影响您的隐私或安全。",
P5: "如需更多信息或有任何问题,请访问以下页面:",
paragraphs: [
"PrivyDrop是一款免费且安全的文件传输和剪贴板共享工具,专注于隐私保护和易用性。我们的使命是提供一个简单但强大的解决方案,让您可以不受限制地跨设备传输文件和共享内容。",
"PrivyDrop的核心是我们对安全和隐私的承诺。我们使用端到端加密确保您的数据在传输过程中受到保护,绝不在服务器上存储您的文件或剪贴板内容。这意味着您的数据始终保持在本地,由您完全控制。",
"使用PrivyDrop,您可以轻松共享文本、图片和任意大小的文件,无需注册或登录。我们的平台设计注重快速、高效和环保,为您提供流畅和友好的使用体验。",
"我们致力于帮助用户掌控自己的数字生活,PrivyDrop正是这一愿景的体现。我们希望这个工具能帮助您安全地与朋友、家人和同事共享和协作,同时不影响您的隐私或安全。",
"如需更多信息或有任何问题,请访问以下页面:",
],
},
HowItWorks: {
h2: "使用方法",
h2_P: "三步即可实现即时文件和消息共享",
btn_try: "立即体验 →",
step1_title: "输入或选择文件",
step1_description: "输入消息或拖放文件/文件夹到选择区域",
step2_title: "加入房间",
step2_description: '点击"加入房间"按钮创建共享会话',
step3_title: "接收",
step3_description: '在接收页面输入房间ID并点击"加入房间"获取共享内容',
// 使用方法
howItWorks: {
title: "使用方法",
description: "三步即可实现即时文件和消息共享",
tryNow: "立即体验 →",
step1Title: "输入或选择文件",
step1Description: "输入消息或拖放文件/文件夹到选择区域",
step2Title: "加入房间",
step2Description: '点击"加入房间"按钮创建共享会话',
step3Title: "接收",
step3Description:
'在接收页面输入房间ID并点击"加入房间"获取共享内容',
},
SystemDiagram: {
h2: "系统架构",
h2_P: "PrivyDrop:您掌控数据。简单、快速、私密。",
// 系统架构
systemDiagram: {
title: "系统架构",
description: "PrivyDrop:您掌控数据。简单、快速、私密。",
},
KeyFeatures: {
h2: "核心特点",
h3_1: "直接且安全",
h3_1_P:
// 核心特点
keyFeatures: {
title: "核心特点",
items: {
directSecure: {
title: "直接且安全",
description:
"文件直接从您的设备传输到接收方,如同一条只有你们能访问的秘密通道。通过端到端加密,您的数据就像说着只有预期接收者才能理解的语言。不想继续共享?只需关闭浏览器标签页,就像挂断电话一样简单,一切尽在掌控。",
h3_2: "团队协作",
h3_2_P:
},
teamSynergy: {
title: "团队协作",
description:
"与整个团队共享就像与一个人共享一样简单。就像主持数字圆桌会议,每个人同时接收文件。无论是创意项目协作还是重要文档分发,都像让所有人同处一室,共同接收您的共享愿景。完美适用于头脑风暴、团队展示或任何需要多人连接的场合。",
h3_3: "无限制,智能处理",
h3_3_P:
},
noLimits: {
title: "无限制,智能处理",
description:
"想象一条能传输任何东西的魔法管道!发送任意大小的文件,仅受磁盘空间限制。对于超大文件,可以选择保存位置。就像有一个特殊的传送服务,不会降低计算机速度 - 文件直接写入磁盘,保持设备运行流畅。",
h3_4: "快如闪电",
h3_4_P:
},
swift: {
title: "快如闪电",
description:
"分分钟共享文本、图片,甚至整个文件夹。就像瞬间传送您的数字内容。需要发送整个相册或文档文件夹?轻而易举,就像分享单个文件一样简单。",
h3_5: "环保简洁",
h3_5_P:
},
greenClean: {
title: "环保简洁",
description:
"我们就像面对面交谈的数字版本 - 不在任何地方存储内容。这意味着我们极其环保,资源消耗最小化。就像在数字世界不留痕迹,为每个人保持清洁和环保。",
h3_6: "断点续传",
h3_6_P:
},
resumable: {
title: "断点续传",
description:
"设置保存目录即可自动启用断点续传。不再担心网络中断,您的文件共享将从上次离开的地方继续。",
},
faqs: {
FAQ_dis: "常见问题",
question_0: "数据真的是本地存储,不会传输到其他服务器吗?",
answer_0:
},
},
// 常见问题
faq: {
title: "常见问题",
items: [
{
question: "数据真的是本地存储,不会传输到其他服务器吗?",
answer:
"是的,所有数据都在本地处理。您可以查看主页上的YouTube视频 - 在建立连接后断开互联网,文件仍然可以在本地网络内传输。未来我们计划开源代码,供所有人审查。",
question_1: "如何发送和接收文件夹?",
answer_1:
},
{
question: "如何发送和接收文件夹?",
answer:
'发送文件夹和发送文件一样简单。将文件夹拖入文件选择区域或点击区域选择,然后点击"开始发送"按钮。接收方可以直接下载或在下载前选择保存目录。前者保存到内存,后者直接保存到磁盘。',
question_2: "可以更改房间ID吗?",
answer_2: "可以,您可以将房间ID更改为任何您喜欢的字符串。",
question_3: "可以持续共享内容吗?",
answer_3:
},
{
question: "可以更改房间ID吗?",
answer: "可以,您可以将房间ID更改为任何您喜欢的字符串。",
},
{
question: "可以持续共享内容吗?",
answer:
'只要保持连接状态,您可以在内容变更时随时点击"开始发送"按钮更新共享内容。',
question_4: "可以同时与多个接收者共享文件吗?",
answer_4: "当然可以!一个人接收和多人同时接收没有任何区别。",
question_5: "使用PrivyDrop时我的数据安全吗?",
answer_5:
},
{
question: "可以同时与多个接收者共享文件吗?",
answer: "当然可以!一个人接收和多人同时接收没有任何区别。",
},
{
question: "使用PrivyDrop时我的数据安全吗?",
answer:
"绝对安全。您的数据始终保持在本地,通过加密的端到端连接在设备间传输。所有传输的数据都经过加密,确保只有您和接收者能访问。",
question_6: "使用PrivyDrop需要创建账号吗?",
answer_6:
},
{
question: "使用PrivyDrop需要创建账号吗?",
answer:
"无需注册或登录,打开网站即可使用。便捷和速度是我们的首要考虑。",
question_7: "有文件大小限制吗?",
answer_7:
},
{
question: "有文件大小限制吗?",
answer:
"没有文件大小或速度限制。只要您有足够的磁盘空间,通过在下载前设置保存目录,就可以传输任意大小的文件。",
question_8: "可以同时共享多个文件或文件夹吗?",
answer_8:
},
{
question: "可以同时共享多个文件或文件夹吗?",
answer:
'可以,共享多个文件或文件夹和共享单个文件一样简单。您还可以添加文件到传输列表中,只需点击"开始发送"即可为接收方更新。',
question_9: "如果我改变主意,如何停止共享?",
answer_9:
},
{
question: "如果我改变主意,如何停止共享?",
answer:
"停止共享非常简单,只需关闭浏览器标签页或窗口即可。这样连接就会断开,无法继续传输数据。",
question_10: "使用PrivyDrop会降低我的设备速度吗?",
answer_10:
},
{
question: "使用PrivyDrop会降低我的设备速度吗?",
answer:
"不会,PrivyDrop设计轻量高效。如果您设置了保存目录,所有接收的数据会直接写入磁盘,绕过内存,有助于保持设备性能。",
question_11: "可以离线使用PrivyDrop吗?",
answer_11:
},
{
question: "可以离线使用PrivyDrop吗?",
answer:
"可以,如果发送方和接收方在同一个本地网络中,他们可以在连接互联网时加入同一个房间,然后断开互联网连接。文件共享仍然可以工作。具体细节可以参考主页上的YouTube视频。",
question_12: "PrivyDrop使用任何服务器吗?",
answer_12:
},
{
question: "PrivyDrop使用任何服务器吗?",
answer:
"是的,确实有一个轻量级服务器,但仅用于建立加密连接的信令。一旦连接建立,所有数据都通过加密连接直接在设备之间传输。",
question_13: "房间ID的有效期是多久?",
answer_13:
},
{
question: "房间ID的有效期是多久?",
answer:
"房间ID的初始有效期为24小时。如果有接收者加入房间,有效期会自动从那一刻起延长24小时。",
},
clipboard_btn: {
Pasted_dis: "已粘贴",
Copied_dis: "已复制",
],
},
fileUploadHandler: {
NoFileChosen_tips: "未选择文件",
fileChosen_tips_template: "已选择{fileNum}个文件和{folderNum}个文件夹",
chooseFileTips: "将文件/文件夹拖拽到此页面的任意位置,或点击此处选择。",
dragTips: "拖拽到此页面的任意位置",
chosenDiagTitle: "选择上传类型",
chosenDiagDescription: "选择是要上传文件还是文件夹",
SelectFile_dis: "选择文件",
SelectFolder_dis: "选择文件夹",
// 博客
blog: {
listTitle: "博客",
listSubtitle: "最新文章与更新",
recentPosts: "最新文章",
tags: "标签",
readMore: "阅读更多",
by: "作者",
postNotFound: "未找到文章",
tocTitle: "目录",
tagTitlePrefix: "标签",
tagSubtitleTemplate: "包含 {tag} 标签的文章",
tagEmpty: "没有找到相关文章。",
},
FileTransferButton: {
SavedToDisk_tips: "文件已保存到磁盘",
CurrentFileTransferring_tips: "文件正在传输中",
OtherFileTransferring_tips: "请等待当前传输完成",
download_tips: "点击下载文件",
PendingSave_tips: "点击保存文件到本地", // 新增
Saved_dis: "已保存",
Waiting_dis: "等待中",
Download_dis: "下载",
Save_dis: "保存", // 新增
// 常用UI元素
common: {
clipboard: {
pasted: "已粘贴",
copied: "已复制",
copyError: "复制失败。",
readError: "读取剪贴板失败。",
loading: "加载中...",
},
FileListDisplay: {
sending_dis: "发送中",
receiving_dis: "接收中",
finish_dis: "已完成",
delete_dis: "删除",
downloadNum_dis: "下载次数",
folder_tips_template: "文件夹名称:{name}(共{num}个文件,总大小{size}",
folder_dis_template: "{num}个文件,{size}",
PopupDialog_title: "建议:选择保存目录",
PopupDialog_description:
"我们建议选择一个保存目录来直接将文件保存到磁盘。这样可以更方便地传输大文件和同步文件夹。",
chooseSavePath_tips: "大文件或文件夹可直接保存到指定目录 👉",
chooseSavePath_dis: "选择保存位置",
buttons: {
request: "请求",
download: "下载",
save: "保存",
copy: "复制",
paste: "粘贴",
joinRoom: "加入房间",
leaveRoom: "离开房间",
},
RetrieveMethod: {
P: "恭喜 🎉 共享内容等待接收:",
RoomId_tips: "接收用的房间ID是:",
copyRoomId_tips: "复制房间ID",
url_tips: "分享链接:",
copyUrl_tips: "复制分享链接",
scanQR_tips: "扫描二维码接收 👇",
Copied_dis: "已复制",
Copy_QR_dis: "复制二维码",
download_QR_dis: "下载二维码",
},
ClipboardApp: {
fetchRoom_err: "获取房间失败,请重试。",
roomCheck: {
empty_msg: "房间ID不能为空",
available_msg: "房间可用",
notAvailable_msg: "房间不可用,请尝试其他房间",
// 剪贴板核心功能
clipboard: {
tabs: {
send: "发送",
retrieve: "接收",
},
channelOpen_msg: "数据通道已开启,准备接收数据...",
waitting_tips:
"等待接收方连接。请保持此页面打开直到传输完成。在桌面端,您可以最小化浏览器或切换标签页。在移动端,请保持浏览器在前台。",
joinRoom: {
EmptyMsg: "警告,房间ID为空",
DuplicateMsg: "该房间ID已被使用,请更换其他ID。",
successMsg:
"成功加入房间!在被接收之前不要关闭当前页(电脑端可以最小化浏览器或切换tab页,移动端不要将浏览器切到后台)。",
notExist: "您尝试加入的房间不存在。只有发送方可以创建房间。",
failMsg: "加入房间失败:",
},
pickSaveMsg: "直接保存到磁盘?",
pickSaveUnsupported: "不支持目录选择器。",
pickSaveSuccess: "保存位置已设置。",
pickSaveError: "无法设置保存位置。",
roomStatus: {
senderEmptyMsg: "房间为空",
receiverEmptyMsg: "您可以接受邀请加入房间",
onlyOneMsg: "只有您一人在房间内",
peopleMsg_template: `房间内共{peerCount}人`,
connected_dis: "已连接",
senderDisconnectedMsg: "发送端已断开连接",
leftRoomMsg: "您已离开房间。",
leaveRoomBtn: "离开房间",
},
fileExistMsg: "某些文件已添加。",
noFilesForFolderMsg: "在文件夹 '{folderName}' 中未找到文件。",
zipError: "创建 ZIP 文件时出错。",
fileNotFoundMsg: "未找到要下载的文件 '{fileName}'。",
confirmLeaveWhileTransferring:
"传输将中断,已设置保存目录时可续传。确定退出?",
leaveWhileTransferringSuccess: "已退出房间,传输已中断",
html: {
senderTab: "发送",
retrieveTab: "接收",
shareTitle_dis: "分享内容",
retrieveTitle_dis: "接收内容",
RoomStatus_dis: "状态:",
Paste_dis: "粘贴",
Copy_dis: "复制",
inputRoomIdprompt: "您的房间ID(可编辑):",
joinRoomBtn: "加入房间",
generateSimpleId_tips: "简单ID",
generateRandomId_tips: "随机ID",
readClipboardToRoomId: "粘贴房间ID",
enterRoomID_placeholder: "输入房间ID",
titles: {
share: "分享内容",
retrieve: "接收内容",
retrieveMethod: "接收方式",
inputRoomId_tips: "您的房间ID(可编辑):",
joinRoom_dis: "加入房间",
SyncSending_loadingText: "已同步",
SyncSending_dis: "同步",
readClipboard_dis: "粘贴房间ID",
retrieveRoomId_placeholder: "输入房间ID",
RetrieveMethodTitle: "接收方式",
// New: cached ID utils
saveId_dis: "保存ID",
useCachedId_dis: "使用缓存ID",
saveId_tips: "保存ID后,下次可以快捷使用该ID",
useCachedId_tips: "快捷使用已保存ID;双击可切换保存模式",
},
// New: cache messages
saveId_success: "缓存成功",
actions: {
sync: "同步",
syncLoading: "已同步",
readClipboard: "粘贴房间ID",
},
placeholders: {
roomId: "输入房间ID",
},
status: {
roomEmpty: "房间为空",
receiverCanAccept: "您可以接受邀请加入房间",
onlyOne: "只有您一人在房间内",
peopleCount: "房间内共{peerCount}人",
connected: "已连接",
senderDisconnected: "发送端已断开连接",
leftRoom: "您已离开房间。",
},
messages: {
fileExist: "某些文件已添加。",
noFilesForFolder: "在文件夹 '{folderName}' 中未找到文件。",
zipError: "创建 ZIP 文件时出错。",
fileNotFound: "未找到要下载的文件 '{fileName}'。",
confirmLeave:
"传输将中断,已设置保存目录时可续传。确定退出?",
leaveSuccess: "已退出房间,传输已中断",
fetchRoomError: "获取房间失败,请重试。",
generateShareLinkError: "生成分享链接失败。",
leaveRoomError: "退出房间失败。",
validateRoomError: "校验房间失败。",
resetSenderStateError: "重置发送端状态失败。",
channelOpen: "数据通道已开启,准备接收数据...",
waiting:
"等待接收方连接。请保持此页面打开直到传输完成。在桌面端,您可以最小化浏览器或切换标签页。在移动端,请保持浏览器在前台。",
},
join: {
empty: "警告,房间ID为空",
duplicate: "该房间ID已被使用,请更换其他ID。",
success:
"成功加入房间!在被接收之前不要关闭当前页(电脑端可以最小化浏览器或切换tab页,移动端不要将浏览器切到后台)。",
notFound: "您尝试加入的房间不存在。只有发送方可以创建房间。",
failure: "加入房间失败:",
inProgress: "正在加入房间…(慢网可需 5–30 秒)",
slow: "连接较慢,建议检查网络/VPN 或稍后重试",
timeout: "加入超时(网络可能受限),请重试",
},
rtc: {
slow: "网络可能受限,尝试关闭 VPN 或稍后再试",
negotiating: "已入房,正在建立 P2P 连接…",
connected: "已连接",
reconnecting: "重连中…",
restored: "已恢复连接",
},
roomCheck: {
empty: "房间ID不能为空",
available: "房间可用",
notAvailable: "房间不可用,请尝试其他房间",
},
saveLocation: {
pickMsg: "直接保存到磁盘?",
unsupported: "不支持目录选择器。",
success: "保存位置已设置。",
error: "无法设置保存位置。",
},
cachedId: {
save: "保存ID",
use: "使用缓存ID",
saveTip: "保存ID后,下次可以快捷使用该ID",
useTip: "快捷使用已保存ID;双击可切换保存模式",
saveSuccess: "缓存成功",
},
generateId: {
simple: "简单ID",
random: "随机ID",
},
},
// 文件上传
fileUpload: {
noFileChosen: "未选择文件",
fileChosen: "已选择{fileNum}个文件和{folderNum}个文件夹",
chooseTip: "将文件/文件夹拖拽到此页面的任意位置,或点击此处选择。",
dragTip: "拖拽到此页面的任意位置",
dialog: {
title: "选择上传类型",
description: "选择是要上传文件还是文件夹",
selectFile: "选择文件",
selectFolder: "选择文件夹",
},
},
// 文件列表
fileList: {
sending: "发送中",
receiving: "接收中",
finished: "已完成",
delete: "删除",
downloadCount: "下载次数",
folderSummary: "文件夹名称:{name}(共{num}个文件,总大小{size}",
folderInline: "{num}个文件,{size}",
saveDialog: {
title: "建议:选择保存目录",
description:
"我们建议选择一个保存目录来直接将文件保存到磁盘。这样可以更方便地传输大文件和同步文件夹。",
tip: "大文件或文件夹可直接保存到指定目录 👉",
button: "选择保存位置",
},
},
// 文件传输按钮
fileTransfer: {
savedToDisk: "文件已保存到磁盘",
currentTransferring: "文件正在传输中",
otherTransferring: "请等待当前传输完成",
download: "下载",
pendingSave: "点击保存文件到本地",
saved: "已保存",
waiting: "等待中",
},
// 接收方式(分享卡片)
retrieveMethod: {
intro: "恭喜 🎉 共享内容等待接收:",
roomIdTip: "接收用的房间ID是:",
copyRoomId: "复制房间ID",
urlTip: "分享链接:",
copyUrl: "复制分享链接",
scanQr: "扫描二维码接收 👇",
copied: "已复制",
copyQr: "复制二维码",
downloadQr: "下载二维码",
},
// 首页
home: {
h1: "免费安全的在线剪贴板与文件传输工具",
h1P: "P2P传输从中断处恢复。体验真正可靠、私密、不可阻挡的文件和文本共享——无需注册,无文件大小和速度限制,完全免费。",
h2_screenOnly: "立即体验安全剪贴板与文件传输工具",
h2_demo: "观看安全文件共享演示",
h2P_demo: "了解我们如何通过本地优先、端到端加密的文件共享保护您的隐私",
watch_tips: "也可以在以下平台观看视频:",
youtube_tips: "在 YouTube 观看 PrivyDrop",
bilibili_tips: "在 Bilibili 观看 PrivyDrop",
hero: {
title: "免费安全的在线剪贴板与文件传输工具",
subtitle:
"P2P传输从中断处恢复。体验真正可靠、私密、不可阻挡的文件和文本共享——无需注册,无文件大小和速度限制,完全免费。",
screenOnlyTitle: "立即体验安全剪贴板与文件传输工具",
},
demo: {
title: "观看安全文件共享演示",
description:
"了解我们如何通过本地优先、端到端加密的文件共享保护您的隐私",
watchTip: "也可以在以下平台观看视频:",
youtube: "在 YouTube 观看 PrivyDrop",
bilibili: "在 Bilibili 观看 PrivyDrop",
},
},
},
};
@@ -0,0 +1,338 @@
---
title: "Damit KI nicht mehr abschweift: ein praxistaugliches CollaborationPlaybook (mit Vorlagen)"
description: "Geht KICode ständig am Thema vorbei? Mit AGENTS.md, einem AI Playbook und einem plan-firstWorkflow wird Codex/Claude zum echten Teammate: weniger Nacharbeit, bessere Wartbarkeit. Mit Next.jsPraxisbeispielen."
date: "2025-12-26"
author: "david bai"
cover: "/blog-assets/ai-collaboration-playbook.webp"
tags:
[
"KIZusammenarbeit",
"Codex",
"EngineeringProzess",
"Open Source",
"Next.js",
]
status: "published"
---
![](/blog-assets/ai-collaboration-playbook.webp)
Kommt dir das bekannt vor?
- Du bittest KI, einen Bug zu fixen—und sie ändert gleich „hilfsbereit“ auch UnrelatedCode. Am Ende revertierst du von Hand.
- Du schreibst endlose Prompts, aber die KI findet die richtigen Dateien nicht und „rät“.
- Mitten in einer langen Unterhaltung vergisst sie Constraints; die Qualität fällt abrupt ab.
Wenn du KI nur als „schnellere Suche“ nutzt, fallen diese Probleme kaum auf. Sobald du KI aber als „kollaborierenden Teammate“ behandelst, wirken sie direkt auf DeliveryQualität.
Dieser Artikel zeigt einen **umsetzbaren EngineeringProzess**, der KIZusammenarbeit von „PromptMagie“ zu einem **reproduzierbaren Workflow** macht. In PrivyDrop wurden FeatureEntwicklung und Bugfixing spürbar schneller und stabiler—nicht durch mehr Risiko, sondern durch weniger Nacharbeit.
Am Ende hast du eine minimale Struktur, die du in jedes Repo kopieren kannst:
- `AGENTS.md`: harte RepoConstraints (Red Lines, Defaults, Definition of Done)
- `docs/ai-playbook/index.md`: ein einseitiger HighSignalIndex
- `docs/ai-playbook/code-map.md`: CodeLandkarte (wo ändern)
- `docs/ai-playbook/flows.md`: zentrale Flows (wie es läuft)
- `docs/ai-playbook/collab-rules.md`: Kollaborationsregeln + ChangePlanTemplate (wie wir arbeiten)
Alle Beispiele stammen aus dem OpenSourceRepo PrivyDrop:
[<u>**https://github.com/david-bai00/PrivyDrop**</u>](https://github.com/david-bai00/PrivyDrop)
Und OpenAI hat kürzlich eine sehr ähnliche Praxisperspektive veröffentlicht:
[<u>**https://openai.com/index/shipping-sora-for-android-with-codex/**</u>](https://openai.com/index/shipping-sora-for-android-with-codex/)
---
## Step 0: Grenzen und „done“ definieren (nicht mit Prompts starten)
Dieser Schritt macht nur eins: „Was heißt fertig?“ glasklar definieren. Sonst optimiert KI auf „läuft irgendwie“, nicht auf „läuft langfristig wartbar nach Teamstandard“.
Drei minimale Constraints:
1. **Boundary**: was niemals passieren darf (Privacy/ArchitekturRedlines, ProtokollKompatibilität, Guardrails für kritische Parameter)
2. **Scope**: ein Ziel pro Änderung, keine „wenn ich schon dabei bin…“
3. **Done**: Build/Tests + manuelle RegressionCheckliste müssen stehen
In einem Satz zusammengefasst (und an den Anfang jeder Anfrage):
```text
Ein Ziel, zuerst der Plan; Privacy/ArchitekturRedlines nie brechen; done heißt: Build grün + RegressionChecklist.
```
---
## Häufige AntiPatterns (bitte vermeiden)
Drei Klassiker:
1. **„Ändere den Code“ ohne Plan**
- Folge: 10 Dateien später stellst du fest, dass die Richtung falsch war—Rollback wird teuer
- Besser: zuerst ChangePlan, Implementierung erst nach Approval
2. **Alle Dokus in den Prompt kippen**
- Folge: KontextOverload; die KI verliert das Wesentliche (findet nicht mal Entry Points)
- Besser: HighSignalIndex + CodeMap
3. **„Nebenbei optimieren“ erlauben**
- Folge: ein PR mit mehreren Zielen; Review wird schwer, Bugs schwerer revertierbar
- Besser: SingleScope, minimal, gut rollbackbar
---
## Step 1: `AGENTS.md` schreiben (harte Constraints, stabil wiederverwendet)
Sieh `AGENTS.md` als maschinenlesbare Version eurer „Defaults“ und „Red Lines“. Nicht Theorie—sondern **wiederverwendbare Regeln pro Session**.
In PrivyDrop reichen fünf Punkte:
- Plan first: `AGENTS.en.md:7`
- One change, one purpose: `AGENTS.en.md:8`
- Privacy & architecture red line: `AGENTS.en.md:9`
- Docs must stay in sync: `AGENTS.en.md:12`
- Verification required: `AGENTS.en.md:13`
Datei: [<u>**AGENTS.en.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/AGENTS.en.md)
Minimale StarterStruktur:
```md
# AGENTS — Repo Rules
First Principles
- Plan-first: Propose a change plan and get approval before writing code
- Single-scope: One PR solves one goal; avoid “while Im here” fixes
- Redlines: Never cross privacy/architecture/protocol/key-parameter guardrails
- Docs-sync: Keep the playbook docs in sync when entry points/flows/interfaces change
- Validation: Must include build/tests and key manual regression checklist
```
### Mehrsprachigkeit
Pragmatischer Ansatz:
- `AGENTS.en.md` als kanonische Version
- Lokalisierte Varianten nach Bedarf (z. B. `AGENTS.<locale>.md`)
- Nach dem Clone lokal den passenden Symlink setzen:
```bash
# English users
ln -s AGENTS.en.md AGENTS.md
```
- `AGENTS.md` in `.gitignore`, um SymlinkKonflikte zu vermeiden
> **KernInsight**
> Zuverlässige KIZusammenarbeit entsteht nicht durch „bessere Prompts“, sondern indem Constraints Teil des Repos werden. `AGENTS.md` macht Regeln wiederverwendbar, der AI Playbook macht Kontext langlebig.
---
## Step 2: `docs/ai-playbook/index.md` schreiben (HighSignalEinstieg)
Ein Hauptgrund für Drift: **KI kennt eure echten Einstiegspunkte nicht**. Dann werden Dateien „erraten“ statt gezielt gefunden.
Das IndexDokument soll:
- in 30 Sekunden lesbar sein (Snapshot + LinkIndex)
- per Klick zu code-map / flows / collab-rules führen
Referenz: [<u>**docs/ai-playbook/index.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/index.md)
MinimalTemplate:
```md
# AI Playbook — Index
## Project Snapshot
- Stack: Next.js / Node / ...
- Red lines: ...
## Document Index
- Code map: docs/ai-playbook/code-map.md
- Key flows: docs/ai-playbook/flows.md
- Collaboration rules: docs/ai-playbook/collab-rules.md
```
---
## Step 3: `code-map.md` schreiben (wo ändern)
Ziel: schnelle Orientierung, nicht Vollständigkeit.
- nur Schlüsselverzeichnisse und EntryFiles
- pro EntryFile ein Satz: Verantwortlichkeit
- bei einer Anfrage: erst 38 Kandidaten aus der CodeMap, dann DeepDive
Referenz: [<u>**docs/ai-playbook/code-map.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/code-map.md)
Optional: „Common requests → Entry Points“:
```md
Common request routing
- New page / SEO: frontend/app/\*\*/page.tsx + metadata.ts
- i18n copy: frontend/constants/messages/\*
- Blog: frontend/content/blog/\* + frontend/lib/blog.ts
```
---
## Step 4: `flows.md` schreiben (wie es läuft)
Wenn code-map „wo“ beantwortet, beantwortet flows „wie“. Das verhindert KIVoodoo:
- Sequenz + Invarianten => weniger „BauchgefühlPatches“
- DebugPitfalls werden zur wiederverwendbaren Checklist
Referenz: [<u>**docs/ai-playbook/flows.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/flows.md)
Mindestens enthalten:
1. **Key flow / sequence** (Mermaid wenn sinnvoll)
2. **DebugCheckliste**
3. **MicroPlanTemplate** (Planfirst erzwingen)
---
## Step 5: „Plan first“ erzwingen (Plan = MiniDesignDoc)
Der echte Hebel: Reviews nach vorne ziehen—vom „Diff lesen“ zum „Plan lesen“.
Template in `collab-rules.md` fixieren:
[<u>**docs/ai-playbook/collab-rules.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/collab-rules.md)
```text
Title: <short, clear title>
Goals
- <what you want to achieve>
Scope / Files
- <list of files youll change/add + why>
Approach
- <implementation plan and key design points>
Risks & Mitigations
- <risk> → <mitigation>
Acceptance Criteria
- <verifiable acceptance items>
Rollback
- <how to revert quickly>
Docs to Update
- docs/ai-playbook/index.md / code-map.md / flows.md / collab-rules.md / others?
Validation
- Build: next build
- Manual: <key cases & regression points>
```
Stabiler Ablauf:
1. index + code-map + flows lesen (readonly)
2. Zustand + Constraints in eigenen Worten restaten (einmal korrigieren)
3. ChangePlan schreiben (erst dann implementieren)
---
## Step 5.1: Context Endurance (Checkpoint → neuer Chat)
Bei langen Aufgaben sinkt Qualität oft. StandardMove: Zustand in eine Datei schreiben, dann im neuen Chat fortsetzen; oder Context vorher komprimieren.
```md
# Handoff
## Problem statement (35 sentences)
## Confirmed plan (bullets)
## Done / Not done
## Key files and entry points
## Red lines and invariants
## Acceptance & regression checklist
## Next-step checklist
```
---
## Step 6: Den Loop schließen (Agent wie ein neues Teammitglied behandeln)
Pipeline:
1. Anfrage → Constraints (`AGENTS.md`)
2. Navigation → Entry Points (`index + code-map`)
3. Alignment → Sequenzen (`flows`)
4. Plan → MiniDesignDoc (`collab-rules`)
5. Umsetzung → klein & singlescope
6. Verifikation → `next build` + manuelle Regression
7. Sync → PlaybookDocs aktuell halten
---
## PromptBeispiele (copyready)
---
**Role**
You are a senior Next.js full-stack engineer with strong product instincts. Your collaboration quality determines whether this repo can iterate sustainably—be thorough and professional.
**Task kickoff**
Please read `docs/ai-playbook/index.md` to understand the project context, code map, and collaboration rules. The current request is: "xxx".
**Working style**
Please deeply read relevant docs/code. Think systematically, ask clarifying questions, then propose analysis + a change plan for review. Implement only after approval.
---
## IndustryReferenz: OpenAI und Codex
[<u>**https://openai.com/index/shipping-sora-for-android-with-codex/**</u>](https://openai.com/index/shipping-sora-for-android-with-codex/)
---
## Minimale Ordnerstruktur (kopierbar)
```text
AGENTS.en.md # Canonical rules (English)
AGENTS.<locale>.md # Optional localized rules
AGENTS.md # Symlink (created locally after git clone)
docs/
ai-playbook/
index.md # High-signal entry point
code-map.md
flows.md
collab-rules.md
```
Wenn du bereits Dokus überall verstreut hast: starte mit `index.md`, um die Einstiegspunkte zu bündeln; danach füllst du code-map/flows/Templates auf.
---
## Nächste Schritte
1. **Jetzt starten**: kopiere die Minimalstruktur in dein Repo und beginne mit `AGENTS.md`
2. **Referenz-Implementierung**: besuche [<u>**PrivyDrop GitHub**</u>](https://github.com/david-bai00/PrivyDrop) und schau dir das komplette AI Playbook an
3. **Feedback**: wenn das hilft (oder du in Fallen läufst), eröffne ein Issue oder hinterlasse einen Kommentar
4. **Star**: wenn es dir Mehrwert bringt, gib PrivyDrop einen Star 🌟
---
## Schluss
KIgestützte Entwicklung reduziert Rigorosität nicht—sie erhöht sie. Nachhaltige Geschwindigkeit kommt aus starken Constraints: Regeln externalisieren, plan-first, Flows dokumentieren, Kontext langlebig machen.
Wenn du weiter gehen willst, kann daraus ein „kopierbares RepoScaffold“ werden: PRTemplates, IssueTemplates und ein sofort nutzbares `AGENTS.md` + PlaybookStarterKit.
@@ -0,0 +1,382 @@
---
title: "Keep AI on Track: A Practical, Repeatable Collaboration Playbook (with Templates)"
description: "Tired of AI-written code going off the rails? Turn Codex/Claude into a real teammate with AGENTS.md, an AI Playbook, and a plan-first workflow—less rework, more maintainable code. Includes real Next.js examples."
date: "2025-12-26"
author: "david bai"
cover: "/blog-assets/ai-collaboration-playbook.webp"
tags:
["AI Collaboration", "Codex", "Engineering Process", "Open Source", "Next.js"]
status: "published"
---
![](/blog-assets/ai-collaboration-playbook.webp)
Have you ever been in this situation?
- You ask AI to fix a bug, and it “helpfully” changes unrelated code—then you end up reverting by hand
- You write a mountain of prompts, and AI still cant find the right files; it just “guesses”
- Halfway through a long chat, AI forgets the constraints and quality drops off a cliff
If you treat AI as “a faster search box,” these issues stay hidden. But the moment you treat AI as “a collaborator,” they hit delivery quality head-on.
This post walks through an **actionable engineering workflow** that turns AI collaboration from “prompt witchcraft” into a **repeatable process**. After adopting it in PrivyDrop, shipping features and fixing bugs got noticeably faster and steadier—not by taking bigger risks, but by doing less rework.
By the end, youll have a minimal structure you can copy into any repo:
- `AGENTS.md`: repo-level hard constraints (red lines, defaults, definition of done)
- `docs/ai-playbook/index.md`: a one-page, high-signal entry point
- `docs/ai-playbook/code-map.md`: a code map (where to change)
- `docs/ai-playbook/flows.md`: key flows (how it runs)
- `docs/ai-playbook/collab-rules.md`: collaboration rules + change-plan template (how we work)
All examples come from the open-source project PrivyDrop—feel free to follow along:
[<u>**https://github.com/david-bai00/PrivyDrop**</u>](https://github.com/david-bai00/PrivyDrop)
Also, OpenAI recently published a hands-on write-up with a very similar mindset:
[<u>**https://openai.com/index/shipping-sora-for-android-with-codex/**</u>](https://openai.com/index/shipping-sora-for-android-with-codex/)
---
## Step 0: Define boundaries and “done” (dont start with prompts)
This step does only one thing: make “what done looks like” explicit. Otherwise AI will do everything it can to make the code “run,” not to make it “run in the maintainable way your team expects.”
Start with three minimal constraints:
1. **Boundary**: what must never happen (privacy/architecture red lines, protocol compatibility, guardrails on key parameters)
2. **Scope**: one change, one goal—no “while Im here”
3. **Done**: build/tests/manual regression checklist must be written down
Compress them into a single sentence and put it at the top of every request:
```text
Single goal, plan first; never cross privacy/architecture red lines; done means it builds and includes a regression checklist.
```
---
## Common anti-patterns (avoid these traps)
Before we begin, here are the three most common wrong approaches:
1. **Tell AI to “change code” without a plan**
- Result: 10 files changed, then you realize the direction is wrong—rollback becomes painful
- Better: require a change plan first; implement only after approval
2. **Dump every document into the prompt**
- Result: context overload; AI cant see the signal (it cant even locate entry points)
- Better: provide a high-signal index + code map
3. **Let AI “optimize things on the side”**
- Result: one PR mixes multiple goals; review cost doubles; bugs get harder to roll back
- Better: single-scope changes that are easy to revert
---
## Step 1: Write `AGENTS.md` (repo-level hard constraints, consistently reused)
Think of `AGENTS.md` as a machine-readable version of your teams “defaults” and “red lines.” Its job isnt to explain theory—its to be **reapplied in every session**.
In PrivyDrop, five lines are enough to cover the core of engineering collaboration constraints:
- Plan first: `AGENTS.en.md:7`
- One change, one purpose: `AGENTS.en.md:8`
- Privacy & architecture red line: `AGENTS.en.md:9`
- Docs must stay in sync: `AGENTS.en.md:12`
- Verification required: `AGENTS.en.md:13`
File: [<u>**AGENTS.en.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/AGENTS.en.md)
If you want a minimal starter, heres a good shape (keep it short, strict, and executable):
```md
# AGENTS — Repo Rules
First Principles
- Plan-first: Propose a change plan and get approval before writing code
- Single-scope: One PR solves one goal; avoid “while Im here” fixes
- Redlines: Never cross privacy/architecture/protocol/key-parameter guardrails
- Docs-sync: If flows/entry points/interfaces change, update the playbook docs
- Validation: Must include build/tests and key manual regression checklist
```
### Multi-language support
If your repo needs multilingual collaboration, a pragmatic pattern is:
- Keep `AGENTS.en.md` as the canonical version
- Add localized variants as needed (e.g. `AGENTS.<locale>.md`)
- After cloning, each contributor can create a language-specific symlink locally:
```bash
# English users
ln -s AGENTS.en.md AGENTS.md
```
- Add `AGENTS.md` to `.gitignore` to avoid symlink conflicts
> **Key insight**
> The secret of reliable AI collaboration isnt “better prompts.” Its “making constraints part of the repo.” `AGENTS.md` makes rules reusable, and the AI Playbook makes context durable.
---
## Step 2: Write `docs/ai-playbook/index.md` (a high-signal entry point)
One of the most common reasons AI goes off track: **it cant find your real entry points**. Then it “guesses” where to change things.
Your index page should do two things:
- Readable in 30 seconds: only “project snapshot + link index”
- One-click navigation: send readers/AI to code-map / flows / collab-rules
Reference implementation: [<u>**docs/ai-playbook/index.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/index.md)
Minimal template (copy-pasteable):
```md
# AI Playbook — Index
## Project Snapshot
- Stack: Next.js / Node / ...
- Red lines: ...
## Document Index
- Code map: docs/ai-playbook/code-map.md
- Key flows: docs/ai-playbook/flows.md
- Collaboration rules: docs/ai-playbook/collab-rules.md
```
---
## Step 3: Write `code-map.md` (where to change: entry points + one-line responsibilities)
The code map is for “fast navigation,” not “teaching you how to implement.” The writing rules are simple:
- List only key directories and key entry files
- One sentence per entry file: what its responsible for
- When a new request arrives: hit 38 candidate files in the code map before deep reading
Reference implementation: [<u>**docs/ai-playbook/code-map.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/code-map.md)
Optional: add a “common requests → entry points” cheat sheet to reduce search cost further:
```md
Common request routing
- New page / SEO: frontend/app/\*\*/page.tsx + metadata.ts
- i18n copy: frontend/constants/messages/\*
- Blog: frontend/content/blog/\* + frontend/lib/blog.ts
```
How to generate (and iterate) a code map:
- First version: ask AI to summarize “directories + key entry points” for navigation—not completeness.
- Iteration: treat it as a living doc. Update it incrementally per PR/commit (file list), instead of rewriting it from scratch.
---
## Step 4: Write `flows.md` (how it runs: key sequences + debug points + micro-plan template)
If code-map answers “where to change,” flows answers “how it runs.” This is hugely valuable for AI:
- If you write down the sequence and invariants, AI stops “patching by vibes”
- You can compress past pitfalls into a reusable debug checklist
Reference implementation: [<u>**docs/ai-playbook/flows.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/flows.md) (with split deep dives)
At minimum, include:
1. **Key flow / sequence** (Mermaid if useful)
2. **Debug checklist** (the most important logs/states)
3. **Micro-plan template** (force plan-first before coding)
How to generate (and iterate) flows:
- First version: ask AI to restate the end-to-end flow + key sequence + invariants. You review/correct (especially red lines and invariants), then commit it to docs.
- Iteration: keep it up to date incrementally when interfaces/sequences change.
---
## Step 5: Make “plan first” enforceable (a plan = a mini design doc)
This is where the speed-up really comes from: move the review earlier—from “reading diff” to “reading the plan.”
Put the plan template in `collab-rules.md` and treat it as a hard constraint.
PrivyDrop template: [<u>**docs/ai-playbook/collab-rules.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/collab-rules.md)
You can reuse this structure directly (goals/scope/approach/risks/acceptance/rollback/validation):
```text
Title: <short, clear title>
Goals
- <what you want to achieve>
Scope / Files
- <list of files youll change/add + why>
Approach
- <implementation plan and key design points>
Risks & Mitigations
- <risk> → <mitigation>
Acceptance Criteria
- <verifiable acceptance items>
Rollback
- <how to revert quickly>
Docs to Update
- docs/ai-playbook/index.md / code-map.md / flows.md / collab-rules.md / others?
Validation
- Build: next build
- Manual: <key cases & regression points>
```
In practice, a steadier workflow is:
1. Read playbook index + code-map + flows (read-only, no code changes)
2. Restate the current state and constraints in your own words (you correct once)
3. Produce a change plan (implementation only after approval)
> **Pitfall avoidance**
> Correct direction during the plan stage; avoid building and then tearing down. Single-scope changes keep rollback cheap and merging easier.
---
## Step 5.1: Context endurance (checkpoint → new chat)
In long tasks, output quality dropping is almost inevitable. Make “endurance” a standard move: when you see guessing, forgotten constraints, or drift—write the state into a file, then continue in a new chat; or run a “compress/summarize” step to shrink context before continuing.
Minimal handoff template (put it in `docs/ai-playbook/handoff.md` or a temporary file):
```md
# Handoff
## Problem statement (35 sentences)
## Confirmed plan (bullets)
## Done / Not done
## Key files and entry points
## Red lines and invariants
## Acceptance & regression checklist
## Next-step checklist
```
The goal isnt “beautiful docs.” Its moving context from the chat window into a file that the next session can read reliably.
---
## Step 6: Close the collaboration loop (treat the agent like a new teammate)
Once you have the pieces above, collaboration becomes a stable pipeline:
1. Request → constraints (cite `AGENTS.md`)
2. Navigation → entry points (cite `index + code-map`)
3. Alignment → sequences (cite `flows`)
4. Planning → mini design doc (cite `collab-rules` template)
5. Implementation → small, single-scope change (easy rollback)
6. Verification → `next build` + key manual regression points
7. Sync → keep playbook docs up to date
The most visible benefit for me: shipping features and fixing bugs is faster and steadier. More importantly, “fast” comes from **less rework**, not more risk:
- Correct direction at plan time; avoid throwing work away
- Single-scope changes keep rollback cheap and merges easy
- Flows turn past pitfalls into a checklist you can reuse
If you want a gate, put these two questions into your PR template:
- Does this PR include a change plan link/summary?
- Did you update `docs/ai-playbook/*` (if entry points/flows/interfaces changed)?
## Prompt examples (ready to copy)
Once your AI Playbook exists, you can start a collaboration session with something like this:
---
**Role**
You are a senior Next.js full-stack engineer with strong product instincts. Your collaboration quality determines whether this repo can iterate sustainably—be thorough and professional.
**Task kickoff**
Please read `docs/ai-playbook/index.md` to understand the project context, code map, and collaboration rules. The current request is: "xxx".
**Working style**
Please deeply read relevant docs/code. Think systematically, ask clarifying questions, then propose analysis + a change plan for review. Implement only after approval.
---
**Why this works**
- **Role**: sets quality bar and responsibility boundaries
- **Kickoff**: forces reading the playbook instead of guessing
- **Working style**: makes “plan first” part of the workflow
---
## Industry reference: How OpenAI uses Codex to run a sprint
In “How we used Codex to build Sora for Android in 28 days,” OpenAI describes a workflow that matches this playbook closely:
[<u>**https://openai.com/index/shipping-sora-for-android-with-codex/**</u>](https://openai.com/index/shipping-sora-for-android-with-codex/)
Key points to align with:
- Treat the agent like a new senior engineer: capable, but needs clear architecture/constraints
- Externalize rules: maintaining a strong `AGENTS.md` pays off
- Plan before real changes: plans are mini design docs—debug the plan before debugging the code
- Context endurance: when you hit context limits, write plans to files for the next session
- Multi-session parallelism: its closer to “managing a team” than using a single tool
Android and web are different, but the method transfers: **improve inputs, and outputs stabilize**.
---
## A minimal directory structure you can copy
```text
AGENTS.en.md # Canonical rules (English)
AGENTS.<locale>.md # Optional localized rules
AGENTS.md # Symlink (created locally after git clone)
docs/
ai-playbook/
index.md # High-signal entry point
code-map.md
flows.md
collab-rules.md
```
If you already have docs scattered everywhere: start with `index.md` to unify entry points; then fill in code-map/flows/templates.
---
## Next steps
1. **Start now**: copy the minimal structure into your repo and begin with `AGENTS.md`
2. **Reference implementation**: visit [<u>**PrivyDrop GitHub**</u>](https://github.com/david-bai00/PrivyDrop) and browse the full AI Playbook
3. **Feedback**: if this helps (or you hit pitfalls), open an issue on GitHub or leave a comment
4. **Star**: if its valuable, consider starring PrivyDrop 🌟
---
## Closing
AI-assisted development doesnt reduce the need for rigor—it increases it. The sustainable speed-up doesnt come from longer prompts, but from stronger engineering constraints: externalize rules, plan first, codify flows, and make context durable.
If you want to go further, this can evolve into a “copyable repo scaffold”: PR templates, issue templates, and a ready-to-use `AGENTS.md` + playbook starter kit.
@@ -0,0 +1,374 @@
---
title: "Que la IA no se desvíe: un método de colaboración realmente aplicable (con plantillas)"
description: "¿La IA escribe código y se sale del tema? Convierte Codex/Claude en un compañero de equipo con AGENTS.md, un AI Playbook y un flujo plan-first: menos retrabajo, más mantenibilidad. Con ejemplos reales en Next.js."
date: "2025-12-26"
author: "david bai"
cover: "/blog-assets/ai-collaboration-playbook.webp"
tags:
[
"Colaboración con IA",
"Codex",
"Proceso de ingeniería",
"Código abierto",
"Next.js",
]
status: "published"
---
![](/blog-assets/ai-collaboration-playbook.webp)
¿Te suena alguno de estos escenarios?
- Le pides a la IA que arregle un bug, pero termina tocando código que no tiene nada que ver—y luego te toca revertir a mano
- Escribes un montón de prompts y aun así no encuentra el archivo correcto; acaba “adivinando”
- A mitad de una conversación larga, empieza a olvidar restricciones y la calidad se desploma
Si tratas la IA como “un buscador más rápido”, estos problemas no se notan. Pero cuando la tratas como “un compañero de colaboración”, se convierten en un factor directo de calidad y entrega.
En este artículo muestro un **método de ingeniería accionable** para que la colaboración con IA deje de ser “magia de prompts” y se convierta en un **proceso repetible**. En PrivyDrop, después de aplicarlo, el ritmo de nuevas funciones y fixes se volvió notablemente más rápido y estable—no por asumir más riesgo, sino por reducir el retrabajo.
Al final tendrás una estructura mínima que puedes copiar a cualquier repositorio:
- `AGENTS.md`: restricciones duras a nivel de repo (líneas rojas, valores por defecto, definición de done)
- `docs/ai-playbook/index.md`: entrada de una sola página (alta señal)
- `docs/ai-playbook/code-map.md`: mapa de código (dónde tocar)
- `docs/ai-playbook/flows.md`: flujos clave (cómo corre)
- `docs/ai-playbook/collab-rules.md`: reglas de colaboración + plantilla de plan de cambio (cómo trabajamos)
Todos los ejemplos vienen del repo open source PrivyDrop (puedes comparar con la implementación):
[<u>**https://github.com/david-bai00/PrivyDrop**</u>](https://github.com/david-bai00/PrivyDrop)
Y esta práctica de OpenAI tiene una filosofía muy similar:
[<u>**https://openai.com/index/shipping-sora-for-android-with-codex/**</u>](https://openai.com/index/shipping-sora-for-android-with-codex/)
---
## Step 0: Define límites y “done” (no empieces por el prompt)
Este paso hace una sola cosa: dejar claro qué significa “terminado”. Si no, la IA va a optimizar por “que funcione”, no por “que funcione de forma mantenible como espera tu equipo”.
Define tres restricciones mínimas:
1. **Boundary (límite)**: lo que nunca debe ocurrir (líneas rojas de privacidad/arquitectura, compatibilidad de protocolo, guardrails de parámetros críticos)
2. **Scope (alcance)**: un objetivo por cambio; prohibido “ya que estoy, optimizo…”
3. **Done (hecho)**: build/tests + checklist de regresión manual deben quedar escritos
Resúmelo en una frase y ponlo al inicio de cada solicitud:
```text
Un solo objetivo, primero el plan; no cruzar líneas rojas de privacidad/arquitectura; “done” significa que compila y trae checklist de regresión.
```
---
## Antipatrones comunes (evita estas trampas)
Antes de empezar, tres errores típicos:
1. **Pedirle a la IA “cambia el código” sin plan**
- Resultado: cambia 10 archivos y luego descubres que la dirección era incorrecta; revertir duele
- Correcto: exigir un plan primero; implementar solo tras aprobarlo
2. **Meter todos los documentos en el prompt**
- Resultado: explosión de contexto; la IA pierde la señal (ni encuentra los entry points)
- Correcto: usar index + code-map como navegación de alta señal
3. **Dejar que la IA “arregle cosas de paso”**
- Resultado: un PR mezcla múltiples objetivos; el review se encarece y los bugs son más difíciles de revertir
- Correcto: single-scope, cambios mínimos y reversibles
---
## Step 1: Escribe `AGENTS.md` (restricciones duras, reutilizables)
Piensa en `AGENTS.md` como la versión “legible por máquina” de los valores por defecto y líneas rojas del equipo. No es teoría: es **un mecanismo de reutilización**.
En PrivyDrop, cinco puntos bastan para cubrir el núcleo:
- Plan first: `AGENTS.en.md:7`
- One change, one purpose: `AGENTS.en.md:8`
- Privacy & architecture red line: `AGENTS.en.md:9`
- Docs must stay in sync: `AGENTS.en.md:12`
- Verification required: `AGENTS.en.md:13`
Archivo: [<u>**AGENTS.en.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/AGENTS.en.md)
Plantilla mínima sugerida (corta, estricta y accionable):
```md
# AGENTS — Repo Rules
First Principles
- Plan-first: Propose a change plan and get approval before writing code
- Single-scope: One PR solves one goal; avoid “while Im here” fixes
- Redlines: Never cross privacy/architecture/protocol/key-parameter guardrails
- Docs-sync: Keep the playbook docs in sync when entry points/flows/interfaces change
- Validation: Must include build/tests and key manual regression checklist
```
### Soporte multi-idioma
Un patrón práctico:
- Mantén `AGENTS.en.md` como versión canónica
- Añade variantes localizadas si hace falta (p. ej. `AGENTS.<locale>.md`)
- Tras clonar, cada persona crea el symlink local según idioma:
```bash
# English users
ln -s AGENTS.en.md AGENTS.md
```
- Añade `AGENTS.md` a `.gitignore` para evitar conflictos de symlinks
> **Idea clave**
> La colaboración con IA no se arregla con “mejores prompts”, sino con “restricciones dentro del repo”. `AGENTS.md` hace que las reglas se reutilicen; el AI Playbook hace que el contexto dure.
---
## Step 2: Escribe `docs/ai-playbook/index.md` (entrada de alta señal)
Una razón común por la que la IA se desvía: **no sabe dónde están tus entry points reales**. Entonces propone cambios por suposiciones.
El index debe lograr dos cosas:
- Se lee en 30 segundos: solo “snapshot del proyecto + índice de enlaces”
- Navegación de un clic: llevar a code-map / flows / collab-rules
Referencia: [<u>**docs/ai-playbook/index.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/index.md)
Plantilla mínima:
```md
# AI Playbook — Index
## Project Snapshot
- Stack: Next.js / Node / ...
- Red lines: ...
## Document Index
- Code map: docs/ai-playbook/code-map.md
- Key flows: docs/ai-playbook/flows.md
- Collaboration rules: docs/ai-playbook/collab-rules.md
```
---
## Step 3: Escribe `code-map.md` (dónde tocar)
El objetivo es “localizar rápido”, no “enseñar a implementar”:
- Solo directorios clave y archivos de entrada clave
- Una frase por archivo: su responsabilidad
- Al llegar una solicitud: identifica 38 candidatos en code-map antes de leer en profundidad
Referencia: [<u>**docs/ai-playbook/code-map.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/code-map.md)
Opcional: añade una tabla de “solicitud común → entry points”:
```md
Common request routing
- New page / SEO: frontend/app/\*\*/page.tsx + metadata.ts
- i18n copy: frontend/constants/messages/\*
- Blog: frontend/content/blog/\* + frontend/lib/blog.ts
```
Cómo generarlo y mantenerlo:
- Primera versión: que la IA resuma “directorios + entry points” para navegación, no para exhaustividad
- Iteración: actualiza incrementalmente por PR/commit; evita reescrituras completas
---
## Step 4: Escribe `flows.md` (cómo corre)
Si code-map responde “dónde tocar”, flows responde “cómo corre”. Para la IA, esto es oro:
- Con secuencia + invariantes claros, deja de “parchear por intuición”
- Los pitfalls históricos se vuelven checklist reutilizable
Referencia: [<u>**docs/ai-playbook/flows.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/flows.md)
Incluye al menos:
1. **Secuencia / flow clave** (Mermaid si hace falta)
2. **Checklist de debug** (logs/estados cruciales)
3. **Plantilla de micro-plan** (plan-first antes de tocar código)
Guía de creación/iteración:
- Primera versión: que la IA reexponga el flujo E2E + secuencia clave + invariantes; tú corriges (sobre todo red lines e invariantes) y lo dejas en docs
- Iteración: actualizar por cambios en interfaces/secuencias
---
## Step 5: Haz “plan first” obligatorio (un plan = mini diseño)
Aquí está el acelerador: mover el review de “leer el diff” a “leer el plan”.
Fija una plantilla en `collab-rules.md` y úsala como regla dura.
Plantilla PrivyDrop: [<u>**docs/ai-playbook/collab-rules.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/collab-rules.md)
Estructura reutilizable:
```text
Title: <short, clear title>
Goals
- <what you want to achieve>
Scope / Files
- <list of files youll change/add + why>
Approach
- <implementation plan and key design points>
Risks & Mitigations
- <risk> → <mitigation>
Acceptance Criteria
- <verifiable acceptance items>
Rollback
- <how to revert quickly>
Docs to Update
- docs/ai-playbook/index.md / code-map.md / flows.md / collab-rules.md / others?
Validation
- Build: next build
- Manual: <key cases & regression points>
```
Un flujo más estable en la práctica:
1. Leer index + code-map + flows (solo lectura, sin tocar código)
2. Reexplicar estado actual y restricciones (tú corriges una vez)
3. Escribir el plan de cambio (implementación solo tras aprobación)
> **Evita pitfalls**
> Corrige la dirección en la fase de plan; evita construir y luego tirar. Single-scope baja el costo de rollback y facilita el merge.
---
## Step 5.1: Resistencia de contexto (checkpoint → chat nuevo)
En tareas largas, la caída de calidad es casi inevitable. Conviértelo en un hábito: cuando aparezcan suposiciones, olvidos o deriva, escribe el estado en un archivo y continúa en un chat nuevo; o comprime/resume el contexto antes de seguir.
Plantilla mínima de handoff (en `docs/ai-playbook/handoff.md` o archivo temporal):
```md
# Handoff
## Problem statement (35 sentences)
## Confirmed plan (bullets)
## Done / Not done
## Key files and entry points
## Red lines and invariants
## Acceptance & regression checklist
## Next-step checklist
```
No es “escribir bonito”: es hacer que el contexto sea reutilizable fuera del chat.
---
## Step 6: Cierra el loop (trata al agente como a un nuevo compañero)
Con todo lo anterior, la colaboración se vuelve una tubería estable:
1. Solicitud → restricciones (citar `AGENTS.md`)
2. Localización → entry points (citar `index + code-map`)
3. Alineación → secuencia (citar `flows`)
4. Plan → mini diseño (plantilla en `collab-rules`)
5. Implementación → cambios pequeños y single-scope (rollback fácil)
6. Verificación → `next build` + regresiones clave
7. Sincronización → docs del playbook al día
El beneficio más visible: más velocidad y estabilidad. Y esa velocidad viene de **menos retrabajo**:
- Corregir dirección al planear, no al final
- Single-scope hace barato el rollback y fácil el merge
- Flows convierte pitfalls en checklist
Si quieres una puerta de control, añade estas dos preguntas al PR template:
- ¿Incluye link/resumen del plan de cambio?
- ¿Actualizaste `docs/ai-playbook/*` si cambiaste entry points/flows/interfaces?
## Ejemplos de prompt (listos para copiar)
---
**Role**
You are a senior Next.js full-stack engineer with strong product instincts. Your collaboration quality determines whether this repo can iterate sustainably—be thorough and professional.
**Task kickoff**
Please read `docs/ai-playbook/index.md` to understand the project context, code map, and collaboration rules. The current request is: "xxx".
**Working style**
Please deeply read relevant docs/code. Think systematically, ask clarifying questions, then propose analysis + a change plan for review. Implement only after approval.
---
## Referencia de industria: cómo OpenAI organiza un sprint con Codex
OpenAI describe un flujo muy parecido aquí:
[<u>**https://openai.com/index/shipping-sora-for-android-with-codex/**</u>](https://openai.com/index/shipping-sora-for-android-with-codex/)
Puntos a alinear:
- Tratar al agente como un “nuevo senior”: capaz, pero necesita arquitectura/limitaciones claras
- Externalizar reglas: mantener un `AGENTS.md` fuerte compensa
- Plan antes de cambios reales: depura el plan antes que el código
- Resistencia de contexto: cuando llegues al límite, escribe el plan en un archivo y continúa
- Multi-sesión en paralelo: más parecido a “gestionar un equipo” que a usar una sola herramienta
---
## Estructura mínima de directorios (copiable)
```text
AGENTS.en.md # Canonical rules (English)
AGENTS.<locale>.md # Optional localized rules
AGENTS.md # Symlink (created locally after git clone)
docs/
ai-playbook/
index.md # High-signal entry point
code-map.md
flows.md
collab-rules.md
```
Si ya tienes docs dispersas: empieza por `index.md` para unificar el acceso; luego completa code-map/flows/plantillas.
---
## Próximos pasos
1. **Empieza ya**: copia la estructura mínima y comienza por `AGENTS.md`
2. **Implementación de referencia**: [<u>**PrivyDrop GitHub**</u>](https://github.com/david-bai00/PrivyDrop)
3. **Feedback**: abre un issue o comenta si te sirve (o te atoras)
4. **Star**: si te aporta valor, deja una estrella 🌟
---
## Cierre
El desarrollo asistido por IA no reduce la necesidad de rigor; la incrementa. La velocidad sostenible no viene de prompts más largos, sino de restricciones más fuertes: reglas externalizadas, plan-first, flows codificados y contexto durable.
@@ -0,0 +1,390 @@
---
title: "Empêcher lIA de dérailler : une méthode de collaboration vraiment applicable (avec templates)"
description: "Votre IA part dans tous les sens quand elle code ? Transformez Codex/Claude en vrai coéquipier grâce à AGENTS.md, un AI Playbook et un workflow plan-first : moins de retours en arrière, plus de maintenabilité. Exemples Next.js inclus."
date: "2025-12-26"
author: "david bai"
cover: "/blog-assets/ai-collaboration-playbook.webp"
tags:
[
"Collaboration IA",
"Codex",
"Processus dingénierie",
"Open Source",
"Next.js",
]
status: "published"
---
![](/blog-assets/ai-collaboration-playbook.webp)
Vous avez déjà vécu ça ?
- Vous demandez à lIA de corriger un bug, et elle modifie aussi du code hors sujet — puis vous devez revenir en arrière à la main
- Vous écrivez des tonnes de prompts, mais elle ne trouve pas les bons fichiers et finit par “deviner”
- Au milieu dune longue discussion, elle oublie les contraintes et la qualité chute brutalement
Si vous utilisez lIA comme “une recherche plus rapide”, ces problèmes restent invisibles. Mais dès que vous la considérez comme “un coéquipier”, ils impactent directement la qualité de livraison.
Cet article présente une **méthode dingénierie exécutable** pour transformer la collaboration IA — de la “magie de prompts” vers un **processus reproductible**. Dans PrivyDrop, après adoption, lajout de features et les bugfixes sont devenus plus rapides et plus stables : non pas en prenant plus de risques, mais en réduisant les retours en arrière.
À la fin, vous obtenez une structure minimale copiable dans nimporte quel repo :
- `AGENTS.md` : contraintes du repo (lignes rouges, valeurs par défaut, définition de done)
- `docs/ai-playbook/index.md` : point dentrée 1 page (haute densité de signal)
- `docs/ai-playbook/code-map.md` : carte du code (où changer)
- `docs/ai-playbook/flows.md` : flows clés (comment ça tourne)
- `docs/ai-playbook/collab-rules.md` : règles de collaboration + template de plan de changement (comment on travaille)
Tous les exemples viennent du repo open source PrivyDrop :
[<u>**https://github.com/david-bai00/PrivyDrop**</u>](https://github.com/david-bai00/PrivyDrop)
OpenAI a aussi publié un retour dexpérience très proche dans lesprit :
[<u>**https://openai.com/index/shipping-sora-for-android-with-codex/**</u>](https://openai.com/index/shipping-sora-for-android-with-codex/)
---
## Step 0 : définir les limites et le “done” (ne pas commencer par les prompts)
Ce step fait une seule chose : rendre explicite ce que “terminé” veut dire. Sinon, lIA optimise pour “ça marche”, pas pour “ça marche durablement et maintenablement”.
Trois contraintes minimales :
1. **Boundary** : ce qui ne doit jamais arriver (privacy/architecture red lines, compatibilité de protocole, guardrails de paramètres critiques)
2. **Scope** : un objectif par changement, pas de “tant quon y est…”
3. **Done** : build/tests + checklist de régression manuelle à écrire
À mettre en une phrase en tête de chaque demande :
```text
Un seul objectif, plan dabord ; ne jamais franchir les red lines privacy/architecture ; done = build OK + checklist de régression.
```
---
## Antipatterns fréquents
Avant de commencer, voici les 3 erreurs les plus courantes :
1. **Demander à lIA de “modifier le code” sans plan**
- Résultat : 10 fichiers modifiés, puis vous réalisez que la direction était mauvaise — rollback douloureux
- Mieux : exiger un plan de changement ; implémenter seulement après approbation
2. **Tout jeter dans le prompt (overload de contexte)**
- Résultat : surcharge ; lIA ne voit plus le signal (elle ne retrouve même pas les entry points)
- Mieux : un index haute-signal + une code-map
3. **Laisser lIA “optimiser au passage”**
- Résultat : un PR mélange plusieurs objectifs ; le coût de review explose ; les bugs sont plus difficiles à revert
- Mieux : single-scope, changements minimaux et facilement revertables
---
## Step 1 : écrire `AGENTS.md` (contraintes du repo, réutilisables)
Pensez à `AGENTS.md` comme à une version “machine-readable” des “valeurs par défaut” et des “lignes rouges” de votre équipe. Son rôle nest pas dexpliquer la théorie : cest d’être **réappliqué à chaque session**.
Dans PrivyDrop, cinq lignes suffisent pour capturer lessentiel :
- Plan first : `AGENTS.en.md:7`
- One change, one purpose : `AGENTS.en.md:8`
- Privacy & architecture red line : `AGENTS.en.md:9`
- Docs must stay in sync : `AGENTS.en.md:12`
- Verification required : `AGENTS.en.md:13`
Fichier : [<u>**AGENTS.en.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/AGENTS.en.md)
Si vous partez de zéro, voici une forme minimale (courte, stricte, actionnable) :
```md
# AGENTS — Repo Rules
First Principles
- Plan-first: Propose a change plan and get approval before writing code
- Single-scope: One PR solves one goal; avoid “while Im here” fixes
- Redlines: Never cross privacy/architecture/protocol/key-parameter guardrails
- Docs-sync: Keep the playbook docs in sync when entry points/flows/interfaces change
- Validation: Must include build/tests and key manual regression checklist
```
### Support multilingue
Si votre repo doit fonctionner dans plusieurs langues, un pattern pragmatique :
- Garder `AGENTS.en.md` comme version canonique
- Ajouter des variantes localisées au besoin (ex. `AGENTS.<locale>.md`)
- Après le clone, chaque contributeur crée localement un symlink selon sa langue :
```bash
# Utilisateurs anglais
ln -s AGENTS.en.md AGENTS.md
```
- Ajouter `AGENTS.md` à `.gitignore` pour éviter les conflits de symlink
> **Idée clé**
> Le secret dune collaboration IA fiable nest pas “de meilleurs prompts”. Cest “de faire des contraintes une partie du repo”. `AGENTS.md` rend les règles réutilisables, et lAI Playbook rend le contexte durable.
---
## Step 2 : `docs/ai-playbook/index.md` (entrée hautesignal)
Lune des raisons les plus fréquentes du “drift” : **lIA ne connaît pas vos vrais entry points**. Du coup, elle “devine” où changer.
Votre index doit faire deux choses :
- Se lire en 30 secondes : seulement “project snapshot + link index”
- Offrir une navigation en un clic : emmener le lecteur/lIA vers code-map / flows / collab-rules
Référence : [<u>**docs/ai-playbook/index.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/index.md)
Template minimal (copiable) :
```md
# AI Playbook — Index
## Project Snapshot
- Stack: Next.js / Node / ...
- Red lines: ...
## Document Index
- Code map: docs/ai-playbook/code-map.md
- Key flows: docs/ai-playbook/flows.md
- Collaboration rules: docs/ai-playbook/collab-rules.md
```
---
## Step 3 : `code-map.md` (où changer)
La code-map sert à “se repérer vite”, pas à “vous apprendre à implémenter”. Les règles d’écriture sont simples :
- Lister uniquement les dossiers clés et les fichiers dentrée (entry files)
- Une phrase par entry file : sa responsabilité
- À larrivée dune demande : viser 38 fichiers candidats via la code-map, puis lire en profondeur
Référence : [<u>**docs/ai-playbook/code-map.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/code-map.md)
Optionnel : ajouter un mini “requêtes courantes → entry points” pour réduire encore le coût de recherche :
```md
Routage des demandes courantes
- Nouvelle page / SEO: frontend/app/\*\*/page.tsx + metadata.ts
- Textes i18n: frontend/constants/messages/\*
- Blog: frontend/content/blog/\* + frontend/lib/blog.ts
```
Comment générer (et itérer) une code-map :
- Première version : demander à lIA de résumer “répertoires + entry points clés” pour naviguer — pas pour être exhaustif.
- Itération : la traiter comme un document vivant. La mettre à jour par incréments à chaque PR/commit (liste de fichiers), plutôt que de la réécrire.
---
## Step 4 : `flows.md` (comment ça tourne)
Si la code-map répond à “où changer”, `flows.md` répond à “comment ça tourne”. Cest extrêmement précieux pour lIA :
- Si vous écrivez la séquence et les invariants, lIA arrête de “patcher au feeling”
- Vous pouvez condenser les pièges historiques en checklist de debug réutilisable
Référence : [<u>**docs/ai-playbook/flows.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/flows.md)
Au minimum, inclure :
1. **Key flow / sequence** (Mermaid si utile)
2. **Debug checklist** (logs/états les plus importants)
3. **Micro-plan template** (forcer plan-first avant de coder)
Comment générer (et itérer) les flows :
- Première version : demander à lIA de reformuler le flux end-to-end + séquence + invariants. Vous relisez/corrigez (surtout les red lines et invariants), puis vous le figez en doc.
- Itération : le maintenir à jour par incréments dès quune interface/séquence change.
---
## Step 5 : rendre “plan first” obligatoire
Cest là que laccélération devient réelle : déplacer la review plus tôt — de “lire un diff” à “lire un plan”.
Mettez le template de plan dans `collab-rules.md` et traitez-le comme une contrainte dure.
Template PrivyDrop : [<u>**docs/ai-playbook/collab-rules.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/collab-rules.md)
Vous pouvez réutiliser directement cette structure (goals/scope/approach/risks/acceptance/rollback/validation) :
```text
Title: <short, clear title>
Goals
- <what you want to achieve>
Scope / Files
- <list of files youll change/add + why>
Approach
- <implementation plan and key design points>
Risks & Mitigations
- <risk> → <mitigation>
Acceptance Criteria
- <verifiable acceptance items>
Rollback
- <how to revert quickly>
Docs to Update
- docs/ai-playbook/index.md / code-map.md / flows.md / collab-rules.md / others?
Validation
- Build: next build
- Manual: <key cases & regression points>
```
En pratique, un workflow plus stable :
1. Lire lindex + code-map + flows (read-only, pas de changement de code)
2. Reformuler l’état actuel et les contraintes avec vos mots (vous corrigez une fois)
3. Produire un plan de changement (implémentation seulement après approbation)
> **Guide antipièges**
> Corriger la direction au stade du plan, éviter de construire puis démolir ; le single-scope rend le rollback moins coûteux et les merges plus simples.
---
## Step 5.1 : endurance du contexte (checkpoint → nouveau chat)
Sur une tâche longue, la baisse de qualité est presque inévitable. Transformez l’“endurance” en réflexe : quand vous voyez du guessing, des contraintes oubliées, ou du drift — écrivez l’état dans un fichier puis repartez dans une nouvelle conversation ; ou faites un passage “compress/summarize” pour réduire le contexte avant de continuer.
Template de handoff minimal (à mettre dans `docs/ai-playbook/handoff.md` ou un fichier temporaire) :
```md
# Handoff
## Problem statement (35 sentences)
## Confirmed plan (bullets)
## Done / Not done
## Key files and entry points
## Red lines and invariants
## Acceptance & regression checklist
## Next-step checklist
```
Le but nest pas “une belle doc”, mais de déplacer le contexte de la fenêtre de chat vers un fichier que la session suivante peut relire de façon fiable.
---
## Step 6 : boucler la boucle (traiter lagent comme un nouveau coéquipier)
Une fois les pièces ci-dessus en place, la collaboration devient un pipeline stable :
1. Demande → contraintes (citer `AGENTS.md`)
2. Navigation → entry points (citer `index + code-map`)
3. Alignement → séquences (citer `flows`)
4. Planification → mini design doc (citer le template `collab-rules`)
5. Implémentation → petit changement single-scope (rollback facile)
6. Vérification → `next build` + points de régression manuelle clés
7. Sync → garder les docs playbook à jour
Le bénéfice le plus visible : livrer des features et corriger des bugs devient plus rapide et plus stable. Surtout, le “rapide” vient de **moins de rework**, pas de plus de risque :
- Corriger la direction au stade du plan ; éviter de jeter du travail
- Le single-scope garde le rollback peu coûteux et les merges faciles
- Les flows transforment les pièges passés en checklist réutilisable
Si vous voulez ajouter une “barrière”, mettez ces deux questions dans votre template de PR :
- Est-ce que ce PR inclut un lien/résumé de plan de changement ?
- Est-ce que `docs/ai-playbook/*` a été mis à jour (si entry points/flows/interfaces ont changé) ?
## Exemples de prompt (copiables)
Une fois votre AI Playbook prêt, vous pouvez lancer une session avec quelque chose comme ça :
---
**Role**
You are a senior Next.js full-stack engineer with strong product instincts. Your collaboration quality determines whether this repo can iterate sustainably—be thorough and professional.
**Task kickoff**
Please read `docs/ai-playbook/index.md` to understand the project context, code map, and collaboration rules. The current request is: "xxx".
**Working style**
Please deeply read relevant docs/code. Think systematically, ask clarifying questions, then propose analysis + a change plan for review. Implement only after approval.
---
**Pourquoi ça marche**
- **Role** : fixe la barre de qualité et les responsabilités
- **Task kickoff** : force la lecture du playbook au lieu de deviner
- **Working style** : rend “plan first” obligatoire dans le workflow
---
## Référence du secteur : comment OpenAI utilise Codex pour organiser un sprint
Dans “How we used Codex to build Sora for Android in 28 days”, OpenAI décrit un workflow très proche de ce playbook :
[<u>**https://openai.com/index/shipping-sora-for-android-with-codex/**</u>](https://openai.com/index/shipping-sora-for-android-with-codex/)
Points clés à aligner :
- Traiter lagent comme un nouveau senior : capable, mais a besoin dune architecture/contraintes explicites
- Externaliser les règles : un `AGENTS.md` solide paie sur la durée
- Plan avant changements réels : un plan est une mini design doc — on debug le plan avant le code
- Endurance du contexte : quand on atteint la limite, on écrit le plan dans un fichier pour la session suivante
- Parallélisme multi-session : plus proche de “manager une équipe” que dutiliser un seul outil
Android et le web sont différents, mais la méthode transfère : **améliorez les inputs, et les outputs se stabilisent**.
---
## Structure minimale de dossiers (copiable)
```text
AGENTS.en.md # Canonical rules (English)
AGENTS.<locale>.md # Optional localized rules
AGENTS.md # Symlink (created locally after git clone)
docs/
ai-playbook/
index.md # High-signal entry point
code-map.md
flows.md
collab-rules.md
```
---
Si vos docs sont déjà dispersées partout : commencez par `index.md` pour rassembler les entry points, puis complétez code-map/flows/templates.
---
## Prochaines étapes
1. **Commencer maintenant** : copiez la structure minimale dans votre repo et démarrez par `AGENTS.md`
2. **Implémentation de référence** : visitez [<u>**PrivyDrop GitHub**</u>](https://github.com/david-bai00/PrivyDrop) et parcourez lAI Playbook complet
3. **Feedback** : si ça vous aide (ou si vous tombez sur des pièges), ouvrez un issue GitHub ou laissez un commentaire
4. **Star** : si ça a de la valeur, mettez une étoile à PrivyDrop 🌟
---
## Conclusion
Le développement assisté par IA ne réduit pas lexigence de rigueur : il laugmente. La vitesse durable vient de contraintes claires, de plans approuvés, de flows documentés et dun contexte qui survit aux conversations.
Si vous voulez aller plus loin, vous pouvez faire évoluer cette méthode en “scaffold de repo copiable” : templates de PR, templates dissues, et un starter kit `AGENTS.md` + playbook prêt à lemploi.
@@ -0,0 +1,382 @@
---
title: "AIを迷走させない:すぐ使える協働のエンジニアリング手法(テンプレ付き)"
description: "AIにコードを書かせると脱線しがち?AGENTS.md・AI Playbook・Plan-first の型で、Codex/Claude を“協働できる相棒”に。手戻りを減らし、保守性を上げる。Next.js 実戦例つき。"
date: "2025-12-26"
author: "david bai"
cover: "/blog-assets/ai-collaboration-playbook.webp"
tags:
["AI協働", "Codex", "エンジニアリングプロセス", "オープンソース", "Next.js"]
status: "published"
---
![](/blog-assets/ai-collaboration-playbook.webp)
こんな経験はありませんか?
- AI にバグ修正を頼んだら、関係ないコードまで“親切に”直されて、結局手で戻す羽目になる
- プロンプトを山ほど書いても、AI が正しいファイルに辿り着けず、最後は“当てずっぽう”
- 会話が長くなると制約を忘れはじめ、出力品質が急降下する
AI を「速い検索ボックス」として使うなら、これらの問題は表面化しません。けれど AI を「一緒に作る相棒」として扱った瞬間、納期と品質に直撃します。
この記事では、AI 協働を「プロンプトの呪文」から **再現可能なプロセス** に変える、**実行可能なエンジニアリング手法** を紹介します。PrivyDrop では、この型を導入してから機能追加・バグ修正のテンポが明らかに速く、そして安定しました。速さの理由は冒険ではなく、**手戻りが減った** ことです。
最終的に、どんなリポジトリにもそのまま移植できる最小セットが手に入ります:
- `AGENTS.md`:プロジェクトのハード制約(レッドライン、デフォルト、Done の定義)
- `docs/ai-playbook/index.md`:高シグナルな入口(1 ページ索引)
- `docs/ai-playbook/code-map.md`:コード地図(どこを触るか)
- `docs/ai-playbook/flows.md`:重要フロー(どう動くか)
- `docs/ai-playbook/collab-rules.md`:協働ルール + 変更計画テンプレ(どう進めるか)
例はすべて OSS の PrivyDrop から。実装と照らし合わせて読めます:
[<u>**https://github.com/david-bai00/PrivyDrop**</u>](https://github.com/david-bai00/PrivyDrop)
さらに、OpenAI の実践記事も考え方が近いです:
[<u>**https://openai.com/index/shipping-sora-for-android-with-codex/**</u>](https://openai.com/index/shipping-sora-for-android-with-codex/)
---
## Step 0:境界と Done を先に決める(プロンプトから始めない)
このステップでやることは 1 つだけ。「何をもって完了とするか」を明文化します。そうしないと AI は、とにかくコードを「動かす」方向に全力を出してしまい、チームが求める「長期的に保守できる形で動かす」にはなりません。
まずは最小限の 3 つを定義します:
1. **Boundary(境界)**:絶対にやってはいけないこと(プライバシー/アーキテクチャのレッドライン、プロトコル互換、重要パラメータの護栏)
2. **Scope(範囲)**:一度に一つの目的。「ついで修正」禁止
3. **Done(完了条件)**:ビルド/テスト/手動回帰チェックは必ず書く
短く 1 文に圧縮して、毎回の依頼の冒頭に置きます:
```text
目的は単一、まず計画。プライバシー/アーキテクチャのレッドラインを越えない。完了は build が通り、回帰チェックが書かれていること。
```
---
## よくあるアンチパターン:ここは踏まない
始める前に、ありがちな 3 つの間違い:
1. **計画なしで AI に「コードを直して」と言う**
- 結果:10 ファイル変わってから方向違いに気づき、戻すコストが爆発する
- 正解:先に変更計画を出させ、承認してから実装
2. **ドキュメントを全部 AI に投げる**
- 結果:文脈が飽和して要点が消える(入口ファイルすら見つけられない)
- 正解:高シグナルな index + code-map に絞る
3. **AI に「ついでに最適化」させる**
- 結果:PR が複数目的で混ざり、レビューが重く、バグの切り戻しも難しい
- 正解:単一テーマで最小・可逆に
---
## Step 1`AGENTS.md` を書く(ハード制約を強制的に再利用する)
`AGENTS.md` は、チームの「デフォルト」と「レッドライン」を機械可読にしたものだと捉えると分かりやすいです。目的は理屈の説明ではなく、**毎回の会話で確実に効かせること**。
PrivyDrop では、工学的な協働に必要な要点は 5 行に集約できます:
- Plan first`AGENTS.en.md:7`
- One change, one purpose`AGENTS.en.md:8`
- Privacy & architecture red line`AGENTS.en.md:9`
- Docs must stay in sync`AGENTS.en.md:12`
- Verification required`AGENTS.en.md:13`
ファイル: [<u>**AGENTS.en.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/AGENTS.en.md)
ゼロから最小版を作るなら、例えばこの形(短く・硬く・実行可能に):
```md
# AGENTS — Repo Rules
First Principles
- Plan-first: Propose a change plan and get approval before writing code
- Single-scope: One PR solves one goal; avoid “while Im here” fixes
- Redlines: Never cross privacy/architecture/protocol/key-parameter guardrails
- Docs-sync: Keep the playbook docs in sync when entry points/flows/interfaces change
- Validation: Must include build/tests and key manual regression checklist
```
### 多言語対応
多言語で運用するなら、現実的にはこうすると安定します:
- `AGENTS.en.md` をカノニカル(基準)として保つ
- 必要に応じてローカライズ版(例:`AGENTS.<locale>.md`)を追加する
- clone 後、各自がローカルで言語に合わせた symlink を作る:
```bash
# English users
ln -s AGENTS.en.md AGENTS.md
```
- symlink 衝突を避けるため `.gitignore` に `AGENTS.md` を入れる
> **核心の洞察**
> AI 協働の本質は「良いプロンプト」ではなく「制約をリポジトリの一部にすること」。`AGENTS.md` はルールを再利用可能にし、AI Playbook は文脈を持続可能にします。
---
## Step 2`docs/ai-playbook/index.md`(高シグナルな入口索引)を書く
AI が脱線する最大要因のひとつは、**あなたのコードの“入口”を知らない** こと。結果、ファイルを推測で探し、提案も推測になります。
索引ページの目的は 2 つ:
- 30 秒で読める:プロジェクト概要 + リンク索引だけ
- 1 クリックで飛べる:読者/AI を code-map / flows / collab-rules に誘導する
参考実装: [<u>**docs/ai-playbook/index.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/index.md)
最小テンプレ(そのままコピー可):
```md
# AI Playbook — Index
## Project Snapshot
- Stack: Next.js / Node / ...
- Red lines: ...
## Document Index
- Code map: docs/ai-playbook/code-map.md
- Key flows: docs/ai-playbook/flows.md
- Collaboration rules: docs/ai-playbook/collab-rules.md
```
---
## Step 3`code-map.md`(どこを触るか)を書く:入口ファイル + 役割 1 行
コード地図のゴールは「素早く当てる」ことで、「教科書」ではありません。書き方はシンプルです:
- 重要ディレクトリと入口ファイルだけ列挙
- 入口ファイルは 1 行で責務を書く
- 依頼が来たら、まず code-map で 3–8 個の候補を当ててから深掘り
参考実装: [<u>**docs/ai-playbook/code-map.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/code-map.md)
オプション:よくある依頼 → 入口の早見表も便利です:
```md
Common request routing
- New page / SEO: frontend/app/\*\*/page.tsx + metadata.ts
- i18n copy: frontend/constants/messages/\*
- Blog: frontend/content/blog/\* + frontend/lib/blog.ts
```
生成と運用:
- 初版:AI に「ディレクトリ + 入口ファイル」を要約させ、完璧さより“当てやすさ”を優先
- 更新:PR ごとに差分でメンテ。丸ごと書き直しは避ける
---
## Step 4`flows.md`(どう動くか)を書く:時系列 + デバッグ要点 + マイクロ計画
code-map が「どこを触るか」なら、flows は「どう動くか」です。AI にとって価値が大きい理由は:
- 時系列と不変条件が書かれていれば、AI は“雰囲気修正”をしなくなる
- 過去の落とし穴をチェックリストに圧縮して再利用できる
参考実装: [<u>**docs/ai-playbook/flows.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/flows.md)
最低限入れたい 3 つ:
1. **重要フロー / 時系列**(必要なら Mermaid
2. **デバッグ要点**(見るべきログ/状態)
3. **マイクロ計画テンプレ**(計画 → 承認 → 実装を強制)
運用のコツ:
- 初版:AI に E2E フロー + 重要時系列 + 不変条件を復唱させ、あなたが修正して文書化
- 更新:インターフェース/時系列が変わったら差分で追従
---
## Step 5:「Plan first」を工程化する(計画 = ミニ設計書)
ここが一番効きます。レビューを「diff を読む」から「計画を読む」へ前倒しします。
`collab-rules.md` に計画テンプレを固定し、強制ルールとして運用します。
PrivyDrop テンプレ: [<u>**docs/ai-playbook/collab-rules.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/collab-rules.md)
構造はこのまま再利用できます:
```text
Title: <short, clear title>
Goals
- <what you want to achieve>
Scope / Files
- <list of files youll change/add + why>
Approach
- <implementation plan and key design points>
Risks & Mitigations
- <risk> → <mitigation>
Acceptance Criteria
- <verifiable acceptance items>
Rollback
- <how to revert quickly>
Docs to Update
- docs/ai-playbook/index.md / code-map.md / flows.md / collab-rules.md / others?
Validation
- Build: next build
- Manual: <key cases & regression points>
```
実運用でさらに安定する流れ:
1. index + code-map + flows を読む(読み取り専用、コードは触らない)
2. 現状と制約を自分の言葉で復唱する(あなたが 1 回だけ矯正)
3. 変更計画を書く(承認後に実装)
> **避坑**
> 計画段階で方向を直す。書いてから壊すのを避ける。単一テーマは rollback とマージを軽くします。
---
## Step 5.1:文脈の持久力(チェックポイント → 新しい会話)
長いタスクで品質が落ちるのはほぼ必然です。そこで“持久力”を作業手順にします:推測が増えた/制約を忘れた/脱線したら、状態をファイルに書き出して新しい会話へ。あるいは compress/summarize で文脈を圧縮して続けます。
最小の引き継ぎテンプレ(`docs/ai-playbook/handoff.md` か一時ファイルに):
```md
# Handoff
## Problem statement (35 sentences)
## Confirmed plan (bullets)
## Done / Not done
## Key files and entry points
## Red lines and invariants
## Acceptance & regression checklist
## Next-step checklist
```
狙いは“綺麗な文書”ではなく、文脈をチャットからファイルへ移し、次のセッションでも安定して読めるようにすることです。
---
## Step 6:協働を閉ループにする(新入社員を迎えるように agent を扱う)
上の部品が揃うと、協働は安定したパイプラインになります:
1. 依頼 → 制約(`AGENTS.md` を引用)
2. 位置特定 → 入口(`index + code-map` を引用)
3. すり合わせ → 時系列(`flows` を引用)
4. 計画 → ミニ設計書(`collab-rules` テンプレ)
5. 実装 → 小さく単一テーマ(切り戻しやすい)
6. 検証 → `next build` + 手動回帰点
7. 同期 → playbook を最新に保つ
体感のメリットは明確で、機能追加/バグ修正が速く、そして安定します。重要なのは「速さ」は冒険ではなく **手戻りの削減** から来ること:
- 計画で方向を直し、作って壊すを減らす
- 単一テーマで rollback を軽く、マージを簡単に
- flows で過去の罠を checklist 化して再利用
門番を置くなら、PR テンプレにこの 2 行を入れると効きます:
- 変更計画のリンク/要約はあるか?
- 入口/フロー/IF が変わったなら `docs/ai-playbook/*` を更新したか?
## すぐ使えるプロンプト例
AI Playbook ができたら、例えばこう始められます:
---
**Role**
You are a senior Next.js full-stack engineer with strong product instincts. Your collaboration quality determines whether this repo can iterate sustainably—be thorough and professional.
**Task kickoff**
Please read `docs/ai-playbook/index.md` to understand the project context, code map, and collaboration rules. The current request is: "xxx".
**Working style**
Please deeply read relevant docs/code. Think systematically, ask clarifying questions, then propose analysis + a change plan for review. Implement only after approval.
---
**Why this works**
- **Role**: sets quality bar and responsibility boundaries
- **Kickoff**: forces reading the playbook instead of guessing
- **Working style**: makes “plan first” part of the workflow
---
## 業界参照:OpenAI は Codex でスプリントをどう回すか
OpenAI の「28 日で Android 版 Sora を作るために Codex をどう使ったか」は、この playbook と驚くほど一致します:
[<u>**https://openai.com/index/shipping-sora-for-android-with-codex/**</u>](https://openai.com/index/shipping-sora-for-android-with-codex/)
合わせたいポイント:
- agent は“新しいシニアエンジニア”として扱う:強いが、制約とアーキテクチャの明確化が必要
- ルールの外部化:強い `AGENTS.md` を保守するのは効く
- 実装前に計画:計画はミニ設計書。コードより先に計画をデバッグ
- 文脈の持久力:限界が来たら計画をファイルへ書き、次のセッションへ
- マルチセッション並列:単一ツールより“チームを運用する”に近い
領域は違っても方法は移植できます:**入力を良くすれば、出力は安定します**。
---
## そのままコピーできる最小ディレクトリ構成
```text
AGENTS.en.md # Canonical rules (English)
AGENTS.<locale>.md # Optional localized rules
AGENTS.md # Symlink (created locally after git clone)
docs/
ai-playbook/
index.md # High-signal entry point
code-map.md
flows.md
collab-rules.md
```
すでに文書が散らばっているなら、まず `index.md` で入口を収束させ、その後に code-map/flows/テンプレを埋めるのが近道です。
---
## 次にやること
1. **今すぐ始める**:最小構成をあなたのリポジトリにコピーし、まず `AGENTS.md` から
2. **参考実装** [<u>**PrivyDrop GitHub**</u>](https://github.com/david-bai00/PrivyDrop) で AI Playbook 全体を見る
3. **フィードバック**:役立った/詰まったら issue やコメントで共有
4. **Star**:価値があれば PrivyDrop に star を 🌟
---
## 結び
AI 支援開発は厳密さを減らすのではなく、むしろ増やします。持続可能なスピードは、長いプロンプトではなく強い工程制約から生まれます:ルールの外部化、Plan-first、フローの文書化、そして文脈の持久力。
さらに工程化するなら、PR/issue テンプレと、すぐ使える `AGENTS.md`/playbook 初期セットを含む「コピーできるリポジトリ雛形」に進化させるのも良いです。
@@ -0,0 +1,387 @@
---
title: "AI가 엇나가지 않게: 바로 적용 가능한 협업 엔지니어링 방법 (템플릿 포함)"
description: "AI로 코드를 쓰면 자꾸 탈선하나요? AGENTS.md, AI Playbook, plan-first 워크플로로 Codex/Claude를 ‘함께 일하는 동료’로 만들 수 있습니다. 재작업을 줄이고 유지보수성을 높입니다. Next.js 실전 예시 포함."
date: "2025-12-26"
author: "david bai"
cover: "/blog-assets/ai-collaboration-playbook.webp"
tags: ["AI 협업", "Codex", "엔지니어링 프로세스", "오픈 소스", "Next.js"]
status: "published"
---
![](/blog-assets/ai-collaboration-playbook.webp)
이런 상황을 겪어본 적 있나요?
- AI에게 버그를 고쳐 달라고 했더니, 상관없는 코드까지 손대서 결국 수동으로 되돌렸다
- 프롬프트를 잔뜩 써도 올바른 파일을 못 찾고, 결국 “찍어서” 수정한다
- 대화가 길어지면 제약을 잊기 시작하고 품질이 급격히 떨어진다
AI를 “더 빠른 검색창”으로만 쓰면 이런 문제는 드러나지 않습니다. 하지만 AI를 “협업하는 동료”로 다루는 순간, 이 문제들은 곧바로 납기와 품질에 영향을 줍니다.
이 글은 AI 협업을 “프롬프트 요술”이 아니라 **재현 가능한 프로세스**로 바꾸는, **실행 가능한 엔지니어링 방법**을 소개합니다. PrivyDrop에서는 이 방식을 적용한 뒤 기능 추가/버그 수정 속도가 더 빠르고 안정적으로 변했습니다. 비결은 더 큰 모험이 아니라 **재작업이 줄어든 것**입니다.
마지막에는 어떤 저장소든 그대로 복사해 넣을 수 있는 최소 구조를 얻게 됩니다:
- `AGENTS.md`: 저장소 레벨의 하드 제약(레드라인, 기본값, Done 정의)
- `docs/ai-playbook/index.md`: 1페이지 인덱스(고신호 진입점)
- `docs/ai-playbook/code-map.md`: 코드 맵(어디를 고칠지)
- `docs/ai-playbook/flows.md`: 핵심 플로우(어떻게 동작하는지)
- `docs/ai-playbook/collab-rules.md`: 협업 규칙 + 변경 계획 템플릿(어떻게 일할지)
모든 예시는 오픈소스 PrivyDrop에서 가져왔습니다:
[<u>**https://github.com/david-bai00/PrivyDrop**</u>](https://github.com/david-bai00/PrivyDrop)
OpenAI의 실전 글도 접근 방식이 매우 비슷합니다:
[<u>**https://openai.com/index/shipping-sora-for-android-with-codex/**</u>](https://openai.com/index/shipping-sora-for-android-with-codex/)
---
## Step 0: 경계와 Done을 먼저 정의하기 (프롬프트부터 시작하지 말기)
이 단계는 한 가지를 합니다. “무엇이 완료인가”를 명확히 쓰는 것. 그렇지 않으면 AI는 코드가 “돌아가게” 만드는 데 최적화하고, 팀이 원하는 “오래 유지보수 가능한 방식으로 돌아가게” 만드는 데는 최적화하지 않습니다.
최소 제약 3가지를 먼저 정합니다:
1. **Boundary(경계)**: 절대 하면 안 되는 것(프라이버시/아키텍처 레드라인, 프로토콜 호환, 핵심 파라미터 가드레일)
2. **Scope(범위)**: 한 번에 한 목표. “겸사겸사” 금지
3. **Done(완료)**: 빌드/테스트/수동 회귀 체크리스트를 반드시 작성
요청 맨 앞에 붙일 한 문장으로 압축하면:
```text
목표는 하나, 먼저 계획; 프라이버시/아키텍처 레드라인을 넘지 말 것; Done은 빌드 통과 + 회귀 체크리스트 포함.
```
---
## 흔한 안티패턴
시작하기 전에, 가장 흔한 3가지 실수부터 짚고 갑니다:
1. **계획 없이 “코드 고쳐줘”부터 시작**
- 결과: 10개 파일을 고친 뒤 방향이 틀렸다는 걸 깨닫고, 롤백 비용이 급격히 커진다
- 더 나은 방법: 먼저 변경 계획을 요구하고, 승인 후에만 구현한다
2. **문서를 전부 프롬프트에 던지기(문맥 과부하)**
- 결과: 문맥이 과부하되어 신호를 못 보고(엔트리 포인트조차 못 찾고) 결국 “찍어서” 고친다
- 더 나은 방법: 고신호 인덱스 + 코드 맵으로 길을 먼저 잡는다
3. **“겸사겸사 최적화” 허용**
- 결과: 한 PR에 여러 목표가 섞여 리뷰 비용이 폭증하고, 버그가 나면 되돌리기도 더 어려워진다
- 더 나은 방법: single-scope로 작게 쪼개서, 쉽게 되돌릴 수 있게 만든다
---
## Step 1: `AGENTS.md` 쓰기 (하드 제약을 반복 적용)
`AGENTS.md`는 팀의 “기본값과 레드라인”을 기계가 읽을 수 있게 만든 버전입니다. 원리를 설명하기 위한 문서가 아니라, **매 세션마다 재사용되는 규칙**이어야 합니다.
PrivyDrop에서는 아래 다섯 줄이면 충분합니다:
- Plan first: `AGENTS.en.md:7`
- One change, one purpose: `AGENTS.en.md:8`
- Privacy & architecture red line: `AGENTS.en.md:9`
- Docs must stay in sync: `AGENTS.en.md:12`
- Verification required: `AGENTS.en.md:13`
파일: [<u>**AGENTS.en.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/AGENTS.en.md)
처음부터 최소 버전을 만들고 싶다면, 아래 구조가 깔끔합니다(짧고, 단단하고, 실행 가능하게):
```md
# AGENTS — Repo Rules
First Principles
- Plan-first: Propose a change plan and get approval before writing code
- Single-scope: One PR solves one goal; avoid “while Im here” fixes
- Redlines: Never cross privacy/architecture/protocol/key-parameter guardrails
- Docs-sync: Keep the playbook docs in sync when entry points/flows/interfaces change
- Validation: Must include build/tests and key manual regression checklist
```
### 다국어 지원
여러 언어로 협업해야 한다면, 현실적인 패턴은 다음과 같습니다:
- `AGENTS.en.md`를 canonical(기준) 버전으로 둔다
- 필요에 따라 현지화 버전(예: `AGENTS.<locale>.md`)을 추가한다
- clone 이후 각 협업자가 자신의 언어에 맞는 symlink를 로컬에서 만든다:
```bash
# 영어 사용자
ln -s AGENTS.en.md AGENTS.md
```
- symlink 충돌을 피하려면 `.gitignore`에 `AGENTS.md`를 추가한다
> **핵심 인사이트**
> 신뢰할 수 있는 AI 협업의 비밀은 “더 나은 프롬프트”가 아니라, “제약을 저장소의 일부로 만드는 것”입니다. `AGENTS.md`는 규칙을 재사용 가능하게 만들고, AI Playbook은 문맥을 오래 살립니다.
---
## Step 2: `docs/ai-playbook/index.md` 만들기 (고신호 진입점)
참고 구현: [<u>**docs/ai-playbook/index.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/index.md)
AI가 엇나가는 가장 흔한 이유 중 하나는 **진짜 엔트리 포인트를 못 찾는 것**입니다. 그러면 “어디를 고쳐야 하는지”를 결국 추측으로 때우게 됩니다.
인덱스 페이지는 두 가지를 해야 합니다:
- 30초 안에 읽히기: “프로젝트 스냅샷 + 링크 인덱스”만
- 원클릭 내비게이션: code-map / flows / collab-rules로 바로 이동
최소 템플릿(복붙용):
```md
# AI Playbook — Index
## Project Snapshot
- Stack: Next.js / Node / ...
- Red lines: ...
## Document Index
- Code map: docs/ai-playbook/code-map.md
- Key flows: docs/ai-playbook/flows.md
- Collaboration rules: docs/ai-playbook/collab-rules.md
```
---
## Step 3: `code-map.md` 만들기 (어디를 고칠지)
참고 구현: [<u>**docs/ai-playbook/code-map.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/code-map.md)
코드 맵의 목표는 “빠른 탐색”이지 “구현 교육”이 아닙니다. 작성 규칙은 간단합니다:
- 핵심 디렉터리와 핵심 엔트리 파일만 나열
- 엔트리 파일당 한 문장: 책임(무슨 역할인지)
- 새 요청이 오면: 코드 맵에서 3–8개 후보 파일을 먼저 찍고, 그다음 깊게 읽기
옵션: “자주 쓰는 요청 → 엔트리 포인트” 치트시트를 넣으면 탐색 비용이 더 줄어듭니다:
```md
자주 쓰는 요청 라우팅
- 새 페이지 / SEO: frontend/app/\*\*/page.tsx + metadata.ts
- i18n 문구: frontend/constants/messages/\*
- 블로그: frontend/content/blog/\* + frontend/lib/blog.ts
```
code-map 생성/운영 팁:
- 초안: AI에게 “디렉터리 + 핵심 엔트리 포인트”를 탐색용으로 정리시키기(완벽함이 아니라 길 찾기가 목적)
- 유지: PR/커밋 단위(파일 리스트 기준)로 조금씩 갱신하기. 한 번에 갈아엎지 않기.
---
## Step 4: `flows.md` 만들기 (어떻게 동작하는지)
참고 구현: [<u>**docs/ai-playbook/flows.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/flows.md)
code-map이 “어디를 고칠지”를 알려준다면, flows는 “어떻게 돌아가는지”를 알려줍니다. AI에게는 특히 큰 가치가 있습니다:
- 시퀀스와 불변식을 써두면, AI가 “감으로 패치”하는 일을 줄일 수 있습니다
- 과거에 밟았던 함정을 디버그 체크리스트로 압축해 재사용할 수 있습니다
최소로라도 아래 3가지는 포함하세요:
1. **Key flow / sequence** (필요하면 Mermaid)
2. **Debug checklist** (가장 중요한 로그/상태)
3. **Micro-plan template** (코딩 전에 plan-first 강제)
flows 생성/운영 팁:
- 초안: AI에게 end-to-end 흐름 + 핵심 시퀀스 + 불변식을 먼저 복기시키기 → 사람이(특히 레드라인/불변식) 교정 → 문서화
- 유지: 인터페이스/시퀀스가 바뀔 때마다 인크리멘탈로 갱신하기
---
## Step 5: “plan first”를 강제하기
여기서 진짜 속도가 나옵니다. 리뷰를 “diff를 읽는 것”에서 “계획을 읽는 것”으로 앞당기기 때문입니다.
`collab-rules.md`에 계획 템플릿을 넣고, 하드 제약으로 취급하세요.
PrivyDrop 템플릿: [<u>**docs/ai-playbook/collab-rules.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/collab-rules.md)
아래 구조는 그대로 재사용할 수 있습니다(goals/scope/approach/risks/acceptance/rollback/validation):
```text
Title: <short, clear title>
Goals
- <what you want to achieve>
Scope / Files
- <list of files youll change/add + why>
Approach
- <implementation plan and key design points>
Risks & Mitigations
- <risk> → <mitigation>
Acceptance Criteria
- <verifiable acceptance items>
Rollback
- <how to revert quickly>
Docs to Update
- docs/ai-playbook/index.md / code-map.md / flows.md / collab-rules.md / others?
Validation
- Build: next build
- Manual: <key cases & regression points>
```
실제로 더 안정적인 워크플로는 다음과 같습니다:
1. index + code-map + flows를 먼저 읽기(read-only, 코드 수정 금지)
2. 현재 상태와 제약을 “자기 말로” 다시 말하기(사람이 1회 교정)
3. 변경 계획을 작성하기(승인 후에만 구현)
> **주의사항**
> 계획 단계에서 방향을 바로잡아 “만들고 나서 갈아엎는” 일을 줄이세요. single-scope는 롤백 비용을 낮추고, 머지도 쉽게 만듭니다.
---
## Step 5.1: 문맥 지속력 (checkpoint → 새 대화)
긴 작업에서는 품질 하락이 거의 필연입니다. “지속력”을 루틴으로 만드세요: 추측/제약 망각/드리프트가 보이면, 현재 상태를 파일로 남기고 새 대화에서 이어가거나, “compress/summarize”로 문맥을 먼저 압축한 뒤 계속 진행합니다.
최소 handoff 템플릿(`docs/ai-playbook/handoff.md` 또는 임시 파일에 저장):
```md
# Handoff
## Problem statement (35 sentences)
## Confirmed plan (bullets)
## Done / Not done
## Key files and entry points
## Red lines and invariants
## Acceptance & regression checklist
## Next-step checklist
```
목표는 “예쁜 문서”가 아니라, 문맥을 채팅창에서 파일로 옮겨 다음 세션이 안정적으로 읽게 만드는 것입니다.
---
## Step 6: 협업을 닫힌 루프로 만들기 (새 동료를 온보딩하듯 agent 대하기)
위의 조각들이 갖춰지면 협업은 안정적인 파이프라인이 됩니다:
1. 요청 → 제약( `AGENTS.md` 인용)
2. 탐색 → 엔트리 포인트( `index + code-map` 인용)
3. 정렬 → 시퀀스( `flows` 인용)
4. 계획 → 미니 설계 문서( `collab-rules` 템플릿 인용)
5. 구현 → 작고 single-scope(쉽게 롤백)
6. 검증 → `next build` + 핵심 수동 회귀 포인트
7. 동기화 → playbook 문서 최신화
제가 체감한 가장 큰 효과는 기능 추가/버그 수정이 더 빠르고 안정적으로 된다는 점입니다. 더 중요한 건 “빠름”이 **리스크**가 아니라 **재작업 감소**에서 나온다는 것:
- 계획 단계에서 방향을 바로잡아, 나중에 버리는 일을 줄인다
- single-scope는 롤백 비용을 낮추고 머지를 쉽게 만든다
- flows는 과거의 함정을 체크리스트로 바꿔 재사용한다
게이트를 하나 더 두고 싶다면 PR 템플릿에 이 두 질문을 넣으세요:
- 이 PR에 변경 계획 링크/요약이 포함되어 있나?
- 엔트리 포인트/플로우/인터페이스가 바뀌었다면 `docs/ai-playbook/*`를 업데이트했나?
## 바로 쓸 수 있는 프롬프트 예시
AI Playbook이 준비되면, 아래처럼 세션을 시작할 수 있습니다:
---
**Role**
You are a senior Next.js full-stack engineer with strong product instincts. Your collaboration quality determines whether this repo can iterate sustainably—be thorough and professional.
**Task kickoff**
Please read `docs/ai-playbook/index.md` to understand the project context, code map, and collaboration rules. The current request is: "xxx".
**Working style**
Please deeply read relevant docs/code. Think systematically, ask clarifying questions, then propose analysis + a change plan for review. Implement only after approval.
---
## OpenAI 참고
[<u>**https://openai.com/index/shipping-sora-for-android-with-codex/**</u>](https://openai.com/index/shipping-sora-for-android-with-codex/)
**왜 이렇게 쓰나**
- **Role**: 품질 기준과 책임 범위를 고정한다
- **Task kickoff**: playbook을 강제로 읽게 만들어, 추측을 줄인다
- **Working style**: “plan first”를 워크플로의 일부로 만든다
---
## 업계 사례: OpenAI는 Codex로 스프린트를 어떻게 굴리나
OpenAI는 “How we used Codex to build Sora for Android in 28 days”에서 이 글과 거의 같은 흐름을 설명합니다:
[<u>**https://openai.com/index/shipping-sora-for-android-with-codex/**</u>](https://openai.com/index/shipping-sora-for-android-with-codex/)
맞춰볼 포인트:
- agent를 “새로 온 시니어”로 대하기: 능력은 강하지만, 아키텍처/제약이 명확해야 한다
- 규칙 외부화: 강한 `AGENTS.md`는 시간이 갈수록 이득이다
- 실질 변경 전에 계획: 계획은 미니 설계 문서 — 코드를 디버그하기 전에 계획을 디버그한다
- 문맥 지속력: 문맥 한계에 닿으면, 계획을 파일로 남겨 다음 세션에서 이어간다
- 멀티 세션 병렬: 단일 도구가 아니라 “팀을 운영”하는 감각에 가깝다
Android와 웹은 다르지만, 방법은 그대로 옮겨옵니다: **입력을 개선하면 출력은 안정됩니다**.
---
## 그대로 복사 가능한 최소 디렉터리 구조
```text
AGENTS.en.md # Canonical rules (English)
AGENTS.<locale>.md # Optional localized rules
AGENTS.md # Symlink (created locally after git clone)
docs/
ai-playbook/
index.md # High-signal entry point
code-map.md
flows.md
collab-rules.md
```
---
문서가 이미 여기저기 흩어져 있다면: 먼저 `index.md`로 진입점을 묶고, 그다음 code-map/flows/템플릿을 채우는 게 가장 빠릅니다.
---
## 다음 행동
1. **바로 시작**: 최소 구조를 저장소에 복사하고, `AGENTS.md`부터 시작하기
2. **참고 구현**: [<u>**PrivyDrop GitHub**</u>](https://github.com/david-bai00/PrivyDrop)에서 전체 AI Playbook 보기
3. **피드백**: 도움이 됐거나(혹은 함정에 빠졌다면) GitHub issue를 열거나 댓글로 공유하기
4. **Star**: 가치가 있다면 PrivyDrop에 star를 🌟
---
## 마무리
AI 보조 개발은 엄밀함을 줄이지 않습니다. 오히려 더 필요합니다. 지속 가능한 속도는 더 긴 프롬프트가 아니라 더 강한 엔지니어링 제약에서 나옵니다: 규칙 외부화, 계획 선행, 플로우 문서화, 그리고 문맥의 지속성.
더 나아가고 싶다면, 이 방법을 “복사 가능한 저장소 스캐폴드”로 확장할 수도 있습니다: PR 템플릿, 이슈 템플릿, 그리고 바로 쓸 수 있는 `AGENTS.md` + playbook 스타터 킷.
@@ -0,0 +1,389 @@
---
title: "让 AI 不再跑偏:可落地的协作工程化方法(附模板)"
description: "用 AI 写代码总跑偏?这套方法让 Claude code/Codex 变成可协作队友:AGENTS.md、AI Playbook、变更计划模板,减少返工、提升代码可维护性。附完整实现与 Next.js 实战案例。"
date: "2025-12-26"
author: "david bai"
cover: "/blog-assets/ai-collaboration-playbook.webp"
tags: ["AI 协作", "Codex", "工程流程", "开源", "Next.js"]
status: "published"
---
![](/blog-assets/ai-collaboration-playbook.webp)
你是否遇到过这样的场景:
- 让 AI 修复一个 bug,结果它把不相关的代码也改了,最后还得手动回滚
- 写了一堆 prompt,AI 还是找不到正确的文件位置,只能靠"猜"
- 对话进行到一半,AI 开始遗忘之前的约束,输出质量断崖式下降
如果你把 AI 当成"更快的搜索框",这些问题不会暴露;但当你把 AI 当成"可协作的队友"时,这些问题会直接影响交付质量。
本文会展示一套**可执行的工程化方法**,让 AI 协作从"玄学 prompt"变成**可复制的流程**。PrivyDrop 项目实践后,新增特性/修复 bug 的节奏明显更快、更稳——不是靠冒险,而是靠减少返工。
你最终会得到一套可以直接复制进任意代码仓库的最小结构:
- `AGENTS.md`:项目级硬约束(红线、默认值、验收方式)
- `docs/ai-playbook/index.md`:一页式索引入口(高信噪比导航)
- `docs/ai-playbook/code-map.md`:代码地图(去哪改)
- `docs/ai-playbook/flows.md`:关键流程(怎么跑)
- `docs/ai-playbook/collab-rules.md`:协作规则 + 变更计划模板(怎么协作)
本文的所有示例都来自开源仓库 PrivyDrop,你可以直接对照实现:
[<u>**https://github.com/david-bai00/PrivyDrop**</u>](https://github.com/david-bai00/PrivyDrop)
另外,最近 OpenAI 的一篇实践文章也有相似的方法论:
[<u>**https://openai.com/index/shipping-sora-for-android-with-codex/**</u>](https://openai.com/index/shipping-sora-for-android-with-codex/)
---
## Step 0:先定义边界与 Done(不要从 prompt 开始)
这一步只做一件事:把“什么是做完”写清楚,否则 AI 只会拼命让代码“跑起来”,而不是按你团队期望的方式“长期可维护地跑起来”。
建议先定 3 类最小约束:
1. **边界(Boundary)**:哪些事永远不能做(隐私/架构红线、协议兼容、关键参数护栏)
2. **范围(Scope)**:一次只做一个目标,禁止“顺手修复”
3. **验收(Done)**:构建/测试/手测回归点,必须写出来
可以把它们压缩成一句话,放在每次需求的开头:
```text
目标明确、单一主题、先交计划再动手;不得越过隐私/架构红线;完成必须可构建并列出回归点。
```
---
## 常见反模式:别踩这些坑
在开始之前,先看看最常见的 3 个错误做法:
1. **直接让 AI "改代码",不要计划**
- 后果:改了 10 个文件,最后发现方向错了,回滚成本巨大
- 正确做法:先让 AI 输出变更计划,你批准后再动手
2. **把所有文档都塞给 AI**
- 后果:上下文爆炸,AI 反而抓不住重点(连"入口文件在哪"都找不到)
- 正确做法:用 index + code-map 提供高信噪比导航
3. **让 AI "顺手优化"其他代码**
- 后果:一次 PR 混杂多个目标,code review 成本翻倍,出 bug 难回滚
- 正确做法:单一主题,一次只解决一个明确目标
---
## Step 1:写 `AGENTS.md`(项目级硬约束,强制稳定复用)
把 `AGENTS.md` 理解为:你团队“默认值”和“红线”的机器可读版本。它的作用不是解释原理,而是**在每次会话中都能被重复应用**。
在 PrivyDrop 里,建议重点展示这 5 条(足够代表"工程协作约束"的核心):
- 计划先行:`AGENTS.zh-CN.md:7`
- 单一主题:`AGENTS.zh-CN.md:8`
- 隐私与架构红线:`AGENTS.zh-CN.md:9`
- 文档同步:`AGENTS.zh-CN.md:12`
- 验证要求:`AGENTS.zh-CN.md:13`
对应文件:[<u>**AGENTS.zh-CN.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/AGENTS.zh-CN.md)
如果你想从零写一个最小版本,可以用下面这个结构(建议保持短、硬、可执行):
```md
# AGENTS — Repo Rules
最重要的原则
- Plan-first:任何改动先交变更计划,获批后再写代码
- Single-scope:一次只解决一个目标,禁止顺手修复
- Redlines:隐私/架构/协议/关键参数不得突破
- Docs-sync:涉及流程/入口/接口变更必须同步更新 playbook
- Validation:必须给出构建/测试/关键手测回归点
```
### 多语言支持
如果你的项目需要支持多语言协作(比如中英文团队),建议:
- 创建 `AGENTS.zh-CN.md` 和 `AGENTS.en.md`
- 每个协作者首次 clone 仓库后,根据语言选择创建符号链接:
```bash
# 中文用户
ln -s AGENTS.zh-CN.md AGENTS.md
# 英文用户
ln -s AGENTS.en.md AGENTS.md
```
- 在 `.gitignore` 中添加 `AGENTS.md`,避免符号链接冲突
不同语言版本的 `AGENTS.md` 已经包含了各自语言的其他文档引用路径。
> **核心洞察**
> AI 协作的本质不是"写更好的 prompt",而是"把约束变成代码库的一部分"。AGENTS.md 让规则可复用,AI Playbook 让上下文可延续。
---
## Step 2:写 `docs/ai-playbook/index.md`(高信噪比入口索引)
AI 最常见的跑偏原因之一是:**不知道你家代码“入口在哪”**。这会导致它用“猜”的方式找文件、提方案。
索引页要做到两件事:
- 30 秒读完:只放"项目快照 + 链接索引"
- 一键跳转:把读者/AI 带到 code-map / flows / collab-rules
参考实现:[<u>**docs/ai-playbook/index.zh-CN.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/index.zh-CN.md)
最小模板(可直接复制):
```md
# AI Playbook — Index
## 项目快照
- 栈:Next.js / Node / ...
- 红线:...
## 文档索引
- 代码地图:docs/ai-playbook/code-map.md
- 关键流程:docs/ai-playbook/flows.md
- 协作规则:docs/ai-playbook/collab-rules.md
```
---
## Step 3:写 `code-map.md`(去哪改:入口文件 + 职责一句话)
代码地图的目标是“快速定位”,不是“教你怎么改”。写法也很简单:
- 只列关键目录与关键入口文件
- 每个入口文件只写一句话职责
- 看到需求时,先在 code-map 里命中 3–8 个候选文件,再深入读
参考实现:[<u>**docs/ai-playbook/code-map.zh-CN.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/code-map.zh-CN.md)
可选:也可以加一段“常见需求 → 入口文件”的速查表,进一步降低定位成本:
```md
常见需求定位
- 新增页面/SEOfrontend/app/\*\*/page.tsx + metadata.ts
- i18n 文案:frontend/constants/messages/\*
- 博客:frontend/content/blog/\* + frontend/lib/blog.ts
```
code-map 怎么生成(以及如何迭代):
- 初版:让 AI 基于对整个代码库的理解,先做一次“目录 + 关键入口文件”的梳理,目标是帮助定位而不是追求完备。
- 迭代:把它当作活文档维护。每次有实质性改动时,让 AI 根据本次变更(PR/commit/文件清单)做增量更新,避免一次性重写。
---
## Step 4:写 `flows.md`(怎么跑:关键流程 + 调试要点 + 微方案模板)
如果 code-map 解决“去哪改”,flows 解决的就是“怎么跑”。它对 AI 的价值非常大:
- 把时序与不变量写清楚,AI 就不需要"凭感觉改"
- 你可以把"历史上踩过的坑"压缩为调试要点,直接复用
参考实现:[<u>**docs/ai-playbook/flows.zh-CN.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/flows.zh-CN.md)(并拆分了深度阅读小节)
建议至少包含三块内容:
1. **关键流程/时序**(必要时给 Mermaid
2. **调试要点**(哪些日志/状态最关键)
3. **微方案模板**(让 AI 先写计划再动手)
flows 怎么生成(以及如何迭代):
- 初版:让 AI 先复述“端到端流程 + 关键时序 + 不变量”,你负责校正(尤其是边界/红线/不变量),再把结果沉淀进文档。
- 迭代:同样按增量维护。每次流程/接口/时序变化,优先更新 flows,而不是让它慢慢过时。
---
## Step 5:把“计划先行”工程化(计划 = 迷你设计文档)
这是提速的关键:把评审从"读 diff"前移到"读计划"。
建议在 `collab-rules.md` 里固化计划模板,并把它当成强约束。
PrivyDrop 的模板:[<u>**docs/ai-playbook/collab-rules.zh-CN.md**</u>](https://github.com/david-bai00/PrivyDrop/blob/main/docs/ai-playbook/collab-rules.zh-CN.md)
模板可以直接复用其中的结构(目标/范围/方案/风险/验收/回滚/验证),参考如下:
```text
Title: <简明标题>
Goals
- <预期达成的目标>
Scope / Files
- <将修改与新增的文件路径清单 + 原因>
Approach
- <实现思路与关键设计点>
Risks & Mitigations
- <主要风险> → <缓解策略>
Acceptance Criteria
- <可验证的验收项>
Rollback
- <如何快速回滚>
Docs to Update
- docs/ai-playbook/index.md / code-map.md / flows.md / collab-rules.md / others?
Validation
- Build: next build
- Manual: <列出关键用例与回归点>
```
实践中更稳的做法是:让 AI 先做两步,再进入写计划阶段:
1. 读 playbook 的 index + code-map + flows(只读,不动代码)
2. 用自己的话复述现状与约束(你纠正一次)
3. 再产出变更计划(你批准后才允许写代码)
> **避坑指南**
> 计划阶段先纠偏,避免写完再推倒重来;单一主题让回滚成本变低,合并也更容易。
---
## Step 5.1:上下文续航(对话变长就"存档换对话")
长任务进行到一半时,AI 输出质量下降几乎是必然的。可以把“续航”变成固定动作:当开始出现猜测、遗忘约束、跑偏时,先把当前状态写成一个文件,然后开新对话继续;或者先执行一次“压缩/总结(compress)”把上下文收敛,再继续推进。
最小续航文档模板(建议放到 `docs/ai-playbook/handoff.md` 或临时文件里):
```md
# Handoff
## 问题定义(35 句)
## 已确认的计划(逐条)
## 已完成 / 未完成
## 关键文件与入口
## 红线与不变量
## 验收与回归清单
## 下一步 checklist
```
这一步的目标不是“写漂亮文档”,而是把上下文从“对话窗口”转成“可被下一次会话稳定读取的文件”。
---
## Step 6:形成协作闭环(像带新同事一样带 agent)
当你把前面几步做完,协作会变成一种稳定的“流水线”:
1. 需求 → 约束(引用 `AGENTS.md`
2. 定位 → 入口(引用 `index + code-map`
3. 对齐 → 流程(引用 `flows`)
4. 计划 → 迷你设计文档(引用 `collab-rules` 模板)
5. 实现 → 小步单一主题(便于回滚)
6. 验证 → `next build` + 关键手测回归点
7. 同步 → 文档不落后(playbook 及时更新)
我最直观的收益是:现在新增特性/修复 bug 的节奏明显更快、更稳。更重要的是,“快”不是靠冒险,而是靠**减少返工**:
- 计划阶段先纠偏,避免写完再推倒重来
- 单一主题让回滚成本变低,合并也更容易
- flows 把历史坑浓缩成 checklist,减少重复踩坑
需要加一道“门禁”时,可以把这两条写进 PR 模板:
- 本 PR 是否包含变更计划链接/摘要?
- 是否更新了 `docs/ai-playbook/*`(如涉及入口/流程/接口)?
## 实战提示词样例
当你准备好 AI Playbook 后,可以用类似下面的提示词启动协作:
---
**角色设定**
你是一名资深的 Next.js 全栈工程师,擅长全栈开发、重构优化、bug 定位修复,做了很多成功的在线产品。你的协作质量直接决定了这个开源项目能否长期稳定迭代,请发挥你的专业能力。
**任务启动**
请你阅读 `docs/ai-playbook/index.zh-CN.md` 了解项目背景、代码地图、协作规范等,现在有一个需求为"xxx"。
**工作方式**
请你深入阅读项目相关文档和代码,在充分理解的基础上,结合你丰富的经验和知识,请你系统性、周全的思考,多向我提问来澄清可疑点,然后输出分析结果和行动计划和我讨论,待我同意后实施。
---
**为什么这样写?**
- **角色设定**:明确 AI 的专业身份和责任范围,避免它"不知道该以什么标准输出"
- **任务启动**:强制 AI 先读 playbook,避免凭空猜测
- **工作方式**:把"计划先行"固化为工作流程,避免直接动手改代码
---
## 业界对照:OpenAI 如何用 Codex 组织冲刺(外部佐证)
OpenAI 在《我们如何使用 Codex 在 28 天内构建 Android 版 Sora》里总结了一套非常相似的工作模型:
[<u>**https://openai.com/index/shipping-sora-for-android-with-codex/**</u>](https://openai.com/index/shipping-sora-for-android-with-codex/)
可以重点对齐这几个点(与本文的方法一一对应):
- 把 agent 当“新入职的资深工程师”:能力强,但需要明确架构/规范/限制条件
- 规则要外化:他们提到在代码库中创建并维护大量 `AGENTS.md` 很实用
- 实质性变更先做计划:计划像微型设计文档,先调试计划再调试代码
- 上下文续航:当达到背景窗口限制时,把计划写入文件供下一次会话继续
- 多会话并行:更像在“管理一个团队”,而不是使用单一工具
虽然安卓开发与 web 开发领域不一样,但方法值得借鉴:**先把输入变好,输出自然会稳定**。
---
## 可直接复制的最小目录结构
```text
AGENTS.zh-CN.md # 中文规则
AGENTS.en.md # 英文规则
AGENTS.md # 符号链接(git clone 后手动创建)
docs/
ai-playbook/
index.zh-CN.md # 中文索引
index.md # 英文索引
code-map.zh-CN.md
code-map.md
flows.zh-CN.md
flows.md
collab-rules.zh-CN.md
collab-rules.md
```
如果你已经有文档但分散在各处:先做 index,把入口收束;然后再补 code-map/flows/模板即可。
---
## 下一步行动
1. **立即开始**:复制最小目录结构到你的项目,从 `AGENTS.md` 开始
2. **参考实现**:访问 [<u>**PrivyDrop GitHub**</u>](https://github.com/david-bai00/PrivyDrop),查看完整 AI Playbook
3. **给我反馈**:如果这套方法对你有帮助(或踩坑了),欢迎在 GitHub 提 issue 或评论区交流
4. **Star 支持**:如果觉得有价值,给 PrivyDrop 一个 star 🌟
---
## 结语
AI 辅助开发不会减少严谨性要求,反而会增加。真正能把效率变成“可持续复利”的,不是更长的 prompt,而是更强的工程约束:规则外化、计划先行、流程固化、上下文续航。
如果希望进一步工程化,这套方法也可以演进为“可复制的仓库脚手架”:包含 PR 模板、issue 模板,以及一份可直接用的 `AGENTS.md`/playbook 初始化内容。
@@ -38,8 +38,8 @@ Beim Wechsel zum Empfangen füllen wir die letzte gespeicherte RaumID automat
Der Check läuft beim TabWechsel: Zuerst wird ausgefüllt, dann direkt die Beitrittslogik aufgerufen ein Einfügen/Klick weniger.
- CodeAnker:
- AutoJoin useEffect (Empfänger): https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/components/ClipboardApp.tsx#L151
- CacheHelfer (localStorage): https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/roomIdCache.ts#L1
- AutoJoin useEffect (Empfänger): [<u>**https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/components/ClipboardApp.tsx#L151**</u>](https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/components/ClipboardApp.tsx#L151)
- CacheHelfer (localStorage): [<u>**https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/roomIdCache.ts#L1**</u>](https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/roomIdCache.ts#L1)
Wann greift es nicht?
@@ -64,8 +64,8 @@ Implementierungsnotizen:
- IDs mit weniger als 8 Zeichen werden nicht gespeichert Schutz vor versehentlichen KurzIDs.
- CodeAnker:
- Einfach/Doppelklick inkl. TimerCleanup: https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/components/ClipboardApp/CachedIdActionButton.tsx#L112
- Sofortiger Beitritt bei „Gecachte ID verwenden“ (Sender): https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/components/ClipboardApp/SendTabPanel.tsx#L193
- Einfach/Doppelklick inkl. TimerCleanup: [<u>**https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/components/ClipboardApp/CachedIdActionButton.tsx#L112**</u>](https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/components/ClipboardApp/CachedIdActionButton.tsx#L112)
- Sofortiger Beitritt bei „Gecachte ID verwenden“ (Sender): [<u>**https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/components/ClipboardApp/SendTabPanel.tsx#L193**</u>](https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/components/ClipboardApp/SendTabPanel.tsx#L193)
---
@@ -78,16 +78,16 @@ Wir beobachten drei Einstiegspunkte und stoßen die Wiederverbindung an:
- Proaktiver `socketId`Check: Bei SocketRecovery erneut validieren.
- CodeAnker:
- AutoBeitritt nach SocketConnect: https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/webrtc_base.ts#L121
- Vereinheitlichter attemptReconnectionEinstieg: https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/webrtc_base.ts#L185
- `lastJoinedSocketId` verfolgen und bei Bedarf `initiator-online`: https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/webrtc_base.ts#L460
- Sender verarbeitet `recipient-ready` und startet Neuverhandlung: https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/webrtc_Initiator.ts#L12
- Empfänger antwortet auf `initiator-online` mit `recipient-ready`: https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/webrtc_Recipient.ts#L14
- AutoBeitritt nach SocketConnect: [<u>**https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/webrtc_base.ts#L121**</u>](https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/webrtc_base.ts#L121)
- Vereinheitlichter attemptReconnectionEinstieg: [<u>**https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/webrtc_base.ts#L185**</u>](https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/webrtc_base.ts#L185)
- `lastJoinedSocketId` verfolgen und bei Bedarf `initiator-online`: [<u>**https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/webrtc_base.ts#L460**</u>](https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/webrtc_base.ts#L460)
- Sender verarbeitet `recipient-ready` und startet Neuverhandlung: [<u>**https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/webrtc_Initiator.ts#L12**</u>](https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/webrtc_Initiator.ts#L12)
- Empfänger antwortet auf `initiator-online` mit `recipient-ready`: [<u>**https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/webrtc_Recipient.ts#L14**</u>](https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/webrtc_Recipient.ts#L14)
- BackendRelais:
- ready: https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/backend/src/socket/handlers.ts#L63
- initiator-online: https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/backend/src/socket/handlers.ts#L102
- recipient-ready: https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/backend/src/socket/handlers.ts#L108
- peer-disconnected: https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/backend/src/socket/handlers.ts#L119
- ready: [<u>**https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/backend/src/socket/handlers.ts#L63**</u>](https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/backend/src/socket/handlers.ts#L63)
- initiator-online: [<u>**https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/backend/src/socket/handlers.ts#L102**</u>](https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/backend/src/socket/handlers.ts#L102)
- recipient-ready: [<u>**https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/backend/src/socket/handlers.ts#L108**</u>](https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/backend/src/socket/handlers.ts#L108)
- peer-disconnected: [<u>**https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/backend/src/socket/handlers.ts#L119**</u>](https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/backend/src/socket/handlers.ts#L119)
### Sequenz (Mermaid)
@@ -112,15 +112,15 @@ sequenceDiagram
### Zuverlässigkeitsdetails
- ICEKandidatenQueue: Ist die RemoteDescription nicht bereit oder die Verbindung im Schließen, werden Kandidaten gepuffert und später geflusht; siehe https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/webrtc_base.ts#L219-L256.
- DataChannelBackpressure & Chunking: SenderSchwelle `bufferedAmountLowThreshold=256KB` (https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/webrtc_Initiator.ts#L82); Netzsteuerung `maxBuffer≈3MB / lowThreshold≈512KB / 64KBChunks` (https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/transfer/NetworkTransmitter.ts#L66-L111, https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/transfer/NetworkTransmitter.ts#L160-L210).
- ICEKandidatenQueue: Ist die RemoteDescription nicht bereit oder die Verbindung im Schließen, werden Kandidaten gepuffert und später geflusht; siehe [<u>**https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/webrtc_base.ts#L219-L256.**</u>](https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/webrtc_base.ts#L219-L256.)
- DataChannelBackpressure & Chunking: SenderSchwelle `bufferedAmountLowThreshold=256KB` ([<u>**https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/webrtc_Initiator.ts#L82**</u>](https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/webrtc_Initiator.ts#L82)); Netzsteuerung `maxBuffer≈3MB / lowThreshold≈512KB / 64KBChunks` ([<u>**https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/transfer/NetworkTransmitter.ts#L66-L111,**</u>](https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/transfer/NetworkTransmitter.ts#L66-L111,) [<u>**https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/transfer/NetworkTransmitter.ts#L160-L210**</u>](https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/frontend/lib/transfer/NetworkTransmitter.ts#L160-L210)).
- Mobile Wake Lock: Beim Verbinden anfordern, bei Trennung/Fehler freigeben reduziert Unterbrechungen im Hintergrund.
- Fehlerkapselung & Retries: seltene `sendData failed` werden gekapselt, angezeigt und erneut versucht (siehe `sendWithBackpressure`).
### Kurze vs. lange IDs: Wiederverwendungsstrategie
- Kurze IDs (4stellig) erhalten nach „leerem Raum + Trennung“ eine GnadenTTL von 15Min. (900s) schnelle Wiederverbindung im Fenster; siehe https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/backend/src/socket/handlers.ts#L119-L125.
- StandardAblaufzeit für Räume: 24h; nur bei leerem Raum nach Trennung wird temporär auf 15Min. umgestellt; siehe https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/backend/src/services/redis.ts#L6.
- Kurze IDs (4stellig) erhalten nach „leerem Raum + Trennung“ eine GnadenTTL von 15Min. (900s) schnelle Wiederverbindung im Fenster; siehe [<u>**https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/backend/src/socket/handlers.ts#L119-L125.**</u>](https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/backend/src/socket/handlers.ts#L119-L125.)
- StandardAblaufzeit für Räume: 24h; nur bei leerem Raum nach Trennung wird temporär auf 15Min. umgestellt; siehe [<u>**https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/backend/src/services/redis.ts#L6.**</u>](https://github.com/david-bai00/PrivyDrop/blob/v1.1.1/backend/src/services/redis.ts#L6.)
- Lange IDs (UUIDartig) eignen sich für Wiederverwendung über Sitzungen/Geräte hinweg am besten mit dem CacheButton kombinieren.
---
@@ -145,15 +145,15 @@ Mobil/Problemnetze:
Je geschmeidiger die Verbindung, desto größer der P2PWert. CachedIDAutoJoin und robuste Wiederverbindung machen PrivyDrop im echten Netz noch verlässlicher.
Wenn Ihnen das gefällt, freuen wir uns über einen Stern auf GitHub (<u>https://github.com/david-bai00/PrivyDrop</u>). Das hilft, entdeckt zu werden und treibt uns an, weiter zu feilen.
Wenn Ihnen das gefällt, freuen wir uns über einen Stern auf GitHub ([<u>**https://github.com/david-bai00/PrivyDrop**</u>](https://github.com/david-bai00/PrivyDrop)). Das hilft, entdeckt zu werden und treibt uns an, weiter zu feilen.
Jetzt online testen: <u>https://www.privydrop.app</u>. Feedback und Verbesserungsvorschläge gern als Issue helfen Sie uns, das „glatte Gefühl“ weiter auszubauen.
Jetzt online testen: [<u>**https://www.privydrop.app**</u>](https://www.privydrop.app). Feedback und Verbesserungsvorschläge gern als Issue helfen Sie uns, das „glatte Gefühl“ weiter auszubauen.
Zusätzlich sorgt Cloudflare CDN für Beschleunigung über Regionen hinweg schnellere, stabilere Zugriffe, weniger Ruckler.
Weiterführende Lektüre:
- [Warum ich PrivyDrop Open Source gestellt habe](/blog/privydrop-open-source)
- [Wie WebRTC BrowserDirekttransfer ermöglicht](/blog/webRTC-file-transfer)
- [Resumable Transfers: Schluss mit der GroßdateiAnxiety](/blog/resumable-transfers)
- [<u>**Warum ich PrivyDrop Open Source gestellt habe**</u>](/blog/privydrop-open-source)
- [<u>**Wie WebRTC BrowserDirekttransfer ermöglicht**</u>](/blog/webRTC-file-transfer)
- [<u>**Resumable Transfers: Schluss mit der GroßdateiAnxiety**</u>](/blog/resumable-transfers)

Some files were not shown because too many files have changed in this diff Show More