Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,13 @@ We maintain a comprehensive suite of unit, algorithmic, and end-to-end regressio

## 🌍 Adding or Improving Translations (i18n)

PDFSeal currently supports 5 languages:
PDFSeal currently supports 6 languages:
- `en` (English - default)
- `zh-CN` (Simplified Chinese)
- `de` (German)
- `es` (Spanish)
- `fr` (French)
- `ja` (Japanese)
- `zh` (Simplified Chinese)

To add missing keys or improve wording:
1. Locate language dictionaries under `src/locales/`.
Expand Down
3 changes: 3 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ body:
- "Smart Compress (CompressTool)"
- "Reorder & Rotate (OrganizeTool)"
- "Split & Extract (SplitTool)"
- "Dynamic Page Numbering (PageNumberTool)"
- "Content Redaction (RedactTool)"
- "Watermark & Stamp (WatermarkTool)"
- "Sign & Stamp (SignTool)"
- "Image to PDF (ImageToPdfTool)"
Expand All @@ -29,6 +31,7 @@ body:
- "Metadata Sanitizer (SanitizeTool)"
- "Privacy Vault (VaultTool)"
- "Pipeline Automation (PipelineTool)"
- "Zero-Knowledge P2P Transfer (ShareReceiveTool)"
- "PWA / Offline / General UI"
- "Docker / Self-Hosting"
- "Other"
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ Quickly return to the 14-in-1 hub anytime by clicking the brand mascot logo 🦭
- **Local Privacy Vault**: Secure in-browser document staging and archive powered by IndexedDB with zero network telemetry. Leverages native Web Crypto SHA-256 content hashing for automatic deduplication, toggleable grid/list views, automatic or one-click post-processing storage, and instant reloading into downstream editing tools.

> 💡 **Unified Delivery & Context-Aware Next-Action Workflow Engine**: All core interactive processing tools feature a standardized three-step workflow (« ① Upload File → ② Configure & Process → ③ Delivery & Next Relays »). Features before/after size & ratio metrics, full-screen preview, one-click re-download, « Return to Edit » state preservation, and an intelligent **Next-Action Workflow Engine** that infers the most relevant downstream operation (e.g., auto-recommending compression for files > 10MB, sequential re-pagination after merging, and tamper-proof AES encryption after contract signing) with zero-copy in-memory relay and clean silence on ZIP archives.
>
> 🛡️ **Smart Workspace Draft Preservation & Multi-Tool Navigation**: Powered by bounded `<KeepAlive>` caching with delivery-aware lifecycle management. Seamlessly jump across tools to cross-reference or configure documents without losing your draft, files, or settings. Completed tasks automatically reset upon return for instant next-job readiness.

---

Expand Down Expand Up @@ -128,10 +130,10 @@ Quickly return to the 14-in-1 hub anytime by clicking the brand mascot logo 🦭
- **Styling & UI**: [Tailwind CSS](https://tailwindcss.com/) & [Lucide Icons](https://lucide.dev/)
- **PDF Engine**: [pdf-lib](https://pdf-lib.js.org/) & [pdfjs-dist](https://mozilla.github.io/pdf.js/)
- **Security & Cryptography**: Native Web Crypto API (SubtleCrypto AES-GCM-256, SHA-256)
- **Internationalization**: Vue I18n (Full support for 🇺🇸 English, 🇩🇪 German, 🇪🇸 Spanish, 🇫🇷 French, 🇨🇳 Simplified Chinese)
- **Internationalization**: Custom lightweight reactive i18n architecture with zero runtime bloat (Full native support for English, Deutsch, Español, Français, 日本語, 简体中文). Clean language-based design without geopolitical flag bindings.
- **Local Storage**: IndexedDB with structured transactional stores (Privacy Vault)
- **Performance & Scheduling**: Active Event Loop Yielding to prevent UI freezing during intensive WASM/canvas computations
- **Testing Suite**: [Vitest](https://vitest.dev/) & [Puppeteer](https://pptr.dev/) (**32 unit test suites, 198 unit tests 100% passing**, plus headless browser E2E regression suites)
- **Testing Suite**: [Vitest](https://vitest.dev/) & [Puppeteer](https://pptr.dev/) (**36 unit test suites, 228 unit tests 100% passing**, plus headless browser E2E regression suites)

---

Expand Down
6 changes: 4 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
- **海豹收纳箱 / 本地隐私保险库 (Local Privacy Vault)**:设备专属的离线文档收纳与归档中心。基于浏览器原生 IndexedDB 事务存储,零网络外发。结合 Web Crypto SubtleCrypto SHA-256 算法实现内容指纹比对与重复检测,支持网格卡片与详细列表自由切换、各工具处理后一键直存或自动保存、以及随时调用历史文档快速继续接力处理。

> 💡 **统一极速交付与智能接力引擎 (Next-Action Workflow Engine)**:全部核心交互处理工具统一重构为「① 上传文件 → ② 参数配置与处理 → ③ 交付成果」极速流,提供处理前后体积比对徽章、全屏预览、一键重下、「返回调整」保留当前配置。搭载专属**上下文感知接力策略引擎**,根据文件体积与场景特征智能推导下一步最佳工序(如大于 10MB 自动强推压缩、合并后首推重排统一页码、签名后强推加密防篡改),纯内存零拷贝直传,免去重新上传;ZIP 归档产物则保持优雅静默,交付页极致清爽。
>
> 🛡️ **智能工作区草稿保留与多任务无损穿梭**:基于有界 `<KeepAlive>` 页面状态缓存与精准的交付感知生命周期管理。支持在多个工具之间自由切换参考或对照配置,返回原工具时草稿文件、选区与配置参数毫发无损;已交付完成的任务则在再次进入时自动干净重置,即刻准备好迎接下一项任务。

---

Expand Down Expand Up @@ -128,10 +130,10 @@
- **样式与 UI**:[Tailwind CSS](https://tailwindcss.com/) & [Lucide Icons](https://lucide.dev/)
- **PDF 引擎**:[pdf-lib](https://pdf-lib.js.org/) & [pdfjs-dist](https://mozilla.github.io/pdf.js/)
- **安全与加密**:原生 Web Crypto API(SubtleCrypto AES-GCM-256、SHA-256)
- **国际化**:Vue I18n(完整支持 🇺🇸 英语、🇩🇪 德语、🇪🇸 西班牙语、🇫🇷 法语、🇨🇳 简体中文)
- **国际化**:轻量级响应式 i18n 架构,零运行时负担(完整原生支持 English、Deutsch、Español、Français、日本語、简体中文)。采用纯粹的语言母语标识,无地缘政治国旗绑定。
- **本地存储**:IndexedDB 结构化事务存储(海豹收纳箱)
- **调度性能**:Event Loop 宏任务平滑分片(Active Event Loop Yielding,防长耗时计算卡顿)
- **测试体系**:[Vitest](https://vitest.dev/) + [Puppeteer](https://pptr.dev/)(**32 个算法与单元测试套件,198 项用例 100% 通过**,配合无头浏览器端到端 E2E 回归套件)
- **测试体系**:[Vitest](https://vitest.dev/) + [Puppeteer](https://pptr.dev/)(**36 个算法与单元测试套件,228 项用例 100% 通过**,配合无头浏览器端到端 E2E 回归套件)

---

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pdfseal",
"version": "1.2.0",
"version": "1.2.1",
"private": true,
"type": "module",
"license": "AGPL-3.0-only",
Expand Down
13 changes: 7 additions & 6 deletions scripts/e2e_i18n_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ if (!fs.existsSync(SCREENSHOT_DIR)) {
}

const LANGUAGES = [
{ code: 'en', name: 'English', flag: '🇺🇸' },
{ code: 'de', name: 'Deutsch', flag: '🇩🇪' },
{ code: 'fr', name: 'Français', flag: '🇫🇷' },
{ code: 'es', name: 'Español', flag: '🇪🇸' },
{ code: 'zh', name: '简体中文', flag: '🇨🇳' }
{ code: 'en', name: 'English' },
{ code: 'de', name: 'Deutsch' },
{ code: 'es', name: 'Español' },
{ code: 'fr', name: 'Français' },
{ code: 'ja', name: '日本語' },
{ code: 'zh', name: '简体中文' }
];

async function runI18nE2ETest() {
Expand Down Expand Up @@ -56,7 +57,7 @@ async function runI18nE2ETest() {

for (const lang of LANGUAGES) {
console.log(`\n------------------------------------------------------------`);
console.log(`🔍 [Testing Language: ${lang.flag} ${lang.name} (${lang.code})]`);
console.log(`🔍 [Testing Language: ${lang.name} (${lang.code})]`);
console.log(`------------------------------------------------------------`);

// Switch language using the top selector
Expand Down
242 changes: 242 additions & 0 deletions scripts/generate_ja_locale.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const enPath = path.resolve(__dirname, '../src/locales/en.json');
const zhPath = path.resolve(__dirname, '../src/locales/zh.json');
const jaPath = path.resolve(__dirname, '../src/locales/ja.json');

const en = JSON.parse(fs.readFileSync(enPath, 'utf-8'));
const zh = JSON.parse(fs.readFileSync(zhPath, 'utf-8'));

// Pre-defined expert domain overrides for core terms and navigation
const domainOverrides = {
page_title: 'PDFSeal - 100%ローカル・ブラウザ完結のPDFツールキット',
offline_banner: 'オフラインモードで稼働中 — すべてのPDF処理はお使いの端末上で安全にローカル実行されています',
local_badge: '100% ローカル処理',
privacy_modal_title: '100% ローカルプライバシー保証',
select_language: '言語の選択',
more_tools_tooltip: 'その他のツールを表示',
brand_subtitle: '100%安全・高速・ブラウザ完結のプライベートPDFツールキット',
support_coffee: '開発者を応援する',
support_fish: '開発者を支援',
feedback_btn: 'フィードバック',
tab_merge: 'PDF 結合',
tab_compress: 'PDF 圧縮',
tab_organize: 'ページ整理',
tab_split: 'PDF 分割',
tab_sign: '電子署名・印鑑',
tab_watermark: '透かし追加',
tab_sanitize: 'メタデータ削除',
tab_redact: '墨消し(黒塗り)',
tab_pipeline: '自動化フロー',
tab_more: 'その他のツール',
tab_unlock: 'パスワード解除',
tab_dewatermark: '透かし除去',
tab_image_to_pdf: '画像からPDF',
tab_pdf_to_image: 'PDFから画像',
tab_protect: 'パスワード保護',

// P2I
p2i_title: 'PDFを画像に変換',
p2i_desc: 'PDFの各ページを高画質なPNGまたはJPG画像に素早く変換します。サーバーへのアップロードは一切ありません。',
p2i_drop_title: 'ここにPDFをドロップ',
p2i_drop_subtitle: 'またはクリックしてファイルを選択',
p2i_format_label: '出力形式',
p2i_format_png: 'PNG(高画質・可逆圧縮)',
p2i_format_jpg: 'JPG(写真・軽量)',
p2i_dpi_label: '解像度(DPI)',
p2i_dpi_std: '150 DPI(標準・Web向け)',
p2i_dpi_high: '300 DPI(高解像度・印刷向け)',
p2i_btn_export_all: '全ページを一括ダウンロード(ZIP)',
p2i_btn_export_page: 'このページをダウンロード',

// I2P
i2p_title: '画像をPDFに変換',
i2p_desc: 'JPG、PNG、WebPなどの画像を1つのきれいなPDFドキュメントに変換します。',
i2p_drop_title: 'ここに画像をドロップ',
i2p_drop_subtitle: '複数画像のドラッグ&ドロップに対応',
i2p_page_size: 'ページサイズ',
i2p_orientation: 'ページの向き',
i2p_margin: '余白設定',
i2p_btn_convert: 'PDFに変換してダウンロード',

// Merge
merge_title: 'PDF 結合',
merge_desc: '複数のPDFファイルをドラッグ&ドロップで自由な順序に並べ替えて1つに結合します。',
merge_drop_title: 'ここにPDFをドロップして追加',
merge_btn_merge: 'PDFを結合する',
merge_btn_clear: 'すべてクリア',
merge_btn_from_local: 'ファイルを追加',
merge_btn_from_vault: '保管庫から追加',
merge_btn_reverse: '順序を反転',

// Split
split_title: 'PDF 分割',
split_desc: '指定ページやページ範囲ごとにPDFを分割・抽出します。',
split_mode_extract: '指定ページを抽出',
split_mode_split_all: '全ページを個別に分割',
split_mode_fixed: '固定ページ数ごとに分割',
split_mode_custom: 'カスタム範囲で分割',
split_btn_split: 'PDFを分割する',

// Compress
compress_title: 'PDF 圧縮',
compress_desc: '画質を維持しながらPDFのファイルサイズを軽量化します。',
compress_mode_smart: 'おすすめ圧縮(標準画質)',
compress_mode_strong: '高圧縮(最小サイズ)',
compress_mode_lossless: '可逆圧縮(構造最適化)',
compress_mode_target: '目標サイズ指定',
compress_btn_compress: 'PDFを圧縮する',

// Organize
organize_title: 'ページ整理・回転',
organize_desc: 'ページの並び替え、回転、不要ページの削除、白紙ページの挿入が直感的に行えます。',
organize_btn_rotate_cw: '右に90°回転',
organize_btn_rotate_ccw: '左に90°回転',
organize_btn_delete: '選択ページを削除',
organize_btn_save: '変更を保存してダウンロード',

// Sign & Stamp
sign_title: '電子署名・印鑑',
sign_desc: '手書きサイン、印鑑画像の配置、テキスト署名が可能です。プライバシーも万全です。',
sign_tab_draw: '手書きサイン',
sign_tab_type: 'タイプ署名',
sign_tab_upload: '印鑑・画像アップロード',
sign_btn_sign: '署名を適用してダウンロード',

// Watermark
watermark_title: '透かし追加',
watermark_desc: '社外秘・複製禁止などの透かし文字を自由な角度と透明度で配置します。',
watermark_btn_apply: '透かしを適用してダウンロード',

// Page Numbers
page_number_title: 'ページ番号の追加',
page_number_desc: 'ヘッダーやフッターに統一されたページ番号を自由な書式で一括挿入します。',
page_number_btn_apply: 'ページ番号を追加してダウンロード',

// Protect & Unlock
protect_title: 'PDF パスワード保護',
protect_desc: '軍用レベルのAES-256暗号化で、閲覧パスワードや印刷・コピー制限を設定します。',
protect_btn_protect: '暗号化して保存',
unlock_title: 'PDF パスワード解除',
unlock_desc: '保護されたPDFのパスワードと権限制限を解除し、制限のないPDFを作成します。',
unlock_btn_unlock: 'パスワードを解除する',

// Sanitize & Redact
sanitize_title: 'メタデータ削除・プライバシー保護',
sanitize_desc: '作成者、編集履歴、GPS位置情報などの隠れたメタデータを完全に消去します。',
sanitize_btn_sanitize: 'メタデータを消去して保存',
redact_title: '墨消し(黒塗り)',
redact_desc: '機密情報や個人情報をドラッグで黒塗りし、テキストと画像データをバイト単位で不可逆的に消去します。',
redact_btn_burn: '墨消しを実行して保存',

// Pipeline & Vault
pipeline_title: '自動化パイプライン',
pipeline_desc: '複数のPDF処理(メタデータ消去→圧縮→透かしなど)を1クリックの連続フローとして自動実行します。',
vault_title: 'ローカル保管庫(Vault)',
vault_desc: '端末内の安全なIndexedDBストレージ。ネットワーク送信なしで処理済みファイルを保管・管理できます。'
};

async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

async function translateText(text) {
if (!text || typeof text !== 'string') return text;
if (!text.trim()) return text;

// Protect {var} placeholders
const varMap = {};
let counter = 0;
const protectedText = text.replace(/\{([a-zA-Z0-9_-]+)\}/g, (match, p1) => {
const placeholder = `__VAR_${p1}_${counter++}__`;
varMap[placeholder] = `{${p1}}`;
return placeholder;
});

const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=ja&dt=t&q=${encodeURIComponent(protectedText)}`;

for (let attempt = 0; attempt < 3; attempt++) {
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
let translated = data[0].map(item => item[0]).join('');

// Restore {var} placeholders
for (const [ph, orig] of Object.entries(varMap)) {
const escapedPh = ph.replace(/_/g, '[_\\s]*');
const reg = new RegExp(escapedPh, 'gi');
translated = translated.replace(reg, orig);
}

// Also ensure any leftover { var } spaces are closed
translated = translated.replace(/\{\s*([a-zA-Z0-9_-]+)\s*\}/g, '{$1}');

return translated;
} catch (err) {
if (attempt === 2) {
console.warn(`Translation failed for: "${text.slice(0, 30)}...": ${err.message}`);
return text;
}
await sleep(1000 * (attempt + 1));
}
}
return text;
}

async function run() {
console.log(`Starting translation for ${Object.keys(en).length} keys...`);
const ja = {};

if (fs.existsSync(jaPath)) {
try {
const existing = JSON.parse(fs.readFileSync(jaPath, 'utf-8'));
Object.assign(ja, existing);
} catch (e) {}
}

const entries = Object.entries(en);
const total = entries.length;
let translatedCount = 0;

for (let i = 0; i < total; i++) {
const [key, enVal] = entries[i];

if (domainOverrides[key]) {
ja[key] = domainOverrides[key];
translatedCount++;
continue;
}

if (ja[key] && typeof ja[key] === 'string' && ja[key].trim().length > 0) {
continue;
}

const jaVal = await translateText(enVal);
ja[key] = jaVal;
translatedCount++;

if ((i + 1) % 50 === 0 || i === total - 1) {
console.log(`Progress: ${i + 1}/${total} (${Math.round(((i + 1) / total) * 100)}%)`);
fs.writeFileSync(jaPath, JSON.stringify(ja, null, 2), 'utf-8');
}

await sleep(35);
}

for (const k of Object.keys(en)) {
if (!ja[k]) {
ja[k] = en[k];
}
}

fs.writeFileSync(jaPath, JSON.stringify(ja, null, 2), 'utf-8');
console.log(`Done! Written ${Object.keys(ja).length} keys to ja.json.`);
}

run().catch(console.error);
4 changes: 2 additions & 2 deletions scripts/run_all_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import path from 'path';
const isWindows = process.platform === 'win32';

const testSuites = [
{ id: 'unit', name: 'Vitest Unit Test Suite (19 files, 57 tests)', cmd: isWindows ? 'npx.cmd' : 'npx', args: ['vitest', 'run'] },
{ id: 'i18n', name: 'i18n Multi-Language Verification (EN/ZH/DE/ES/FR)', cmd: 'node', args: ['scripts/e2e_i18n_test.js'] },
{ id: 'unit', name: 'Vitest Unit Test Suite (36 files, 228 tests)', cmd: isWindows ? 'npx.cmd' : 'npx', args: ['vitest', 'run'] },
{ id: 'i18n', name: 'i18n Multi-Language Verification (EN/DE/ES/FR/JA/ZH)', cmd: 'node', args: ['scripts/e2e_i18n_test.js'] },
{ id: 'merge', name: 'E2E Business: PDF Merge (合并多个 PDF)', cmd: 'node', args: ['scripts/e2e_merge_test.js'] },
{ id: 'compress', name: 'E2E Business: PDF Compress (智能模式与压缩率)', cmd: 'node', args: ['scripts/e2e_compress_test.js'] },
{ id: 'organize', name: 'E2E Business: PDF Organize (旋转/删除/纸张调整)', cmd: 'node', args: ['scripts/e2e_organize_test.js'] },
Expand Down
Loading
Loading