diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e3efc04..1bd0fb8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - site-experimental pull_request: workflow_dispatch: repository_dispatch: @@ -30,20 +31,6 @@ jobs: with: fetch-depth: 0 - - name: Checkout angrydata-app docs - uses: actions/checkout@v4 - with: - repository: ${{ github.repository_owner }}/angrydata-app - path: sources/angrydata-app - fetch-depth: 1 - - - name: Checkout angrydata-core docs - uses: actions/checkout@v4 - with: - repository: ${{ github.repository_owner }}/angrydata-core - path: sources/angrydata-core - fetch-depth: 1 - - name: Set up Python uses: actions/setup-python@v5 with: @@ -52,12 +39,6 @@ jobs: - name: Install dependencies run: pip install --upgrade pip && pip install -r requirements.txt - - name: Aggregate documentation - run: python scripts/sync_docs.py - - - name: Translate documentation - run: python scripts/translate_docs.py - - name: Build site run: mkdocs build --strict diff --git a/.gitignore b/.gitignore index 452ea98..225c319 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,33 @@ # Build artifacts site/ -# Temporary sources pulled during builds -sources/ - -# Aggregated documentation from upstream repositories -.docs-cache/ -docs/angrydata-app/ -docs/angrydata-core/ -docs/index.md - # Python __pycache__/ *.pyc +*.pyo +*.pyd +.Python + +# Virtual environments +venv/ +env/ +ENV/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ -# MacOS +# OS .DS_Store -.idea/ \ No newline at end of file +Thumbs.db + +# Logs +*.log + +# Build markers +.scripts_run_marker + +# Note: docs/ folder is required for MkDocs build, do not ignore it \ No newline at end of file diff --git a/README.md b/README.md index 7c9a404..66b2906 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,133 @@ -# Angry Data Scanner Docs +# Angry Data Scanner Website -This repository powers the public documentation site for the Angry Data Scanner project. The site -is built with [MkDocs](https://www.mkdocs.org/) and aggregates documentation from two -companion repositories: +This repository contains the source code for the [Angry Data Scanner](https://angryscan.org) website. -- [`angrydata-app`](https://github.com/angryscan/angrydata-app) -- [`angrydata-core`](https://github.com/angryscan/angrydata-core) +## Project Structure -The aggregation happens automatically in CI and whenever you run the helper script locally. +``` +. +├── src/ # Source files for the static website +│ ├── assets/ # Images, icons, screenshots +│ ├── css/ # Stylesheets +│ ├── js/ # JavaScript files (i18n, config, components, script) +│ └── *.html # HTML pages +├── static/ # Static files (robots.txt, BingSiteAuth.xml, etc.) +├── docs/ # Minimal MkDocs documentation (required for build) +├── hooks.py # MkDocs build hook (replaces site with src content) +├── mkdocs.yml # MkDocs configuration +└── requirements.txt # Python dependencies +``` -## Local development +## Local Development -1. Clone this repository alongside the other documentation sources: +1. Clone this repository: ```bash git clone https://github.com/angryscan/angryscan.org.git + cd angryscan.org ``` -2. Install the documentation dependencies: +2. Install dependencies: ```bash pip install -r requirements.txt ``` -3. Sync the external documentation and start the local development server: +3. Generate HTML pages from templates (optional, done automatically during build): + + ```bash + # Generate pages for all languages from templates and config.json + python3 generate_html.py + ``` + + This creates pages in `/src/` for all languages (`/ru/`, `/de/`, `/fr/`, `/es/`) using translations from `config.json`. + +4. Build the site: + + ```bash + mkdocs build + ``` + + This will copy files from `src/` and `static/` to `site/` directory, including language-specific pages. + +4. Start local development server: ```bash - python scripts/sync_docs.py --clone-repos - python scripts/translate_docs.py mkdocs serve ``` -The documentation from each project is copied verbatim into the built site. No pages authored in this -repository are published—when the site loads, visitors are automatically redirected into the AngryData -App documentation (as the default entry point), and the navigation exposes each upstream project. -Language switching happens through the Material UI selector (Russian, Spanish, and German are generated -from the English originals); the translated markdown files themselves are hidden from the navigation. -To add another language, simply append it to the `i18n.languages` list in `mkdocs.yml`—the helper -scripts discover the configuration automatically. + Or use a simple HTTP server with URL rewrite support: + + ```bash + python3 dev_server.py + ``` + + This server supports clean URLs (without .html extension): + - `/discovery` → `discovery.html` + - `/features` → `features.html` + - `/ru/discovery` → `ru/discovery.html` + - etc. + + Or use a basic HTTP server (without rewrite support): + + ```bash + cd src && python3 -m http.server 8000 + ``` + +## Features + +- **Multi-language support**: + - English (root pages: `/`, `/index.html`, etc.) + - Russian with custom translations (`/ru/`) + - German with Google Translate (`/de/`) + - French with Google Translate (`/fr/`) + - Spanish with Google Translate (`/es/`) +- **Analytics**: Yandex Metrika, Google Tag Manager, and Bing verification +- **Modern design**: Responsive, dark/light theme support +- **Static site**: Fast, SEO-friendly static HTML + +## Deployment + +### Автоматический деплой (GitHub Pages) + +Сайт автоматически деплоится на GitHub Pages при каждом push в ветку `main`: + +1. **Настройка GitHub Pages** (один раз): + - Перейдите в Settings → Pages + - Source: выберите "GitHub Actions" + - Сохраните настройки + +2. **Процесс деплоя**: + - При push в `main` запускается GitHub Actions workflow + - Устанавливаются зависимости (`pip install -r requirements.txt`) + - Запускается сборка (`mkdocs build`) + - `hooks.py` копирует файлы из `src/` и `static/` в `site/` + - Содержимое `site/` деплоится на GitHub Pages + - Сайт доступен по адресу: `https://.github.io//` или `https://angryscan.org/` + +3. **Ручной запуск деплоя**: + - Перейдите в Actions → Deploy documentation + - Нажмите "Run workflow" → "Run workflow" + +### Ручной деплой на сервер + +Если нужно задеплоить на другой сервер: + +```bash +# 1. Собрать сайт +mkdocs build + +# 2. Скопировать содержимое папки site/ на сервер +# Например, через rsync: +rsync -avz --delete site/ user@server:/var/www/html/ + +# Или через scp: +scp -r site/* user@server:/var/www/html/ +``` -## Continuous deployment +### Альтернативные варианты хостинга -GitHub Actions builds and deploys the site to GitHub Pages on every push to the default -branch. The workflow fetches the latest documentation from the two upstream repositories, -runs `scripts/sync_docs.py` and `scripts/translate_docs.py`, builds the MkDocs site, -and publishes the resulting static files. +- **Netlify**: подключите репозиторий, укажите build command: `mkdocs build`, publish directory: `site` +- **Vercel**: аналогично Netlify +- **Cloudflare Pages**: подключите репозиторий, укажите build command и output directory +- **Любой статический хостинг**: просто загрузите содержимое папки `site/` после сборки diff --git a/config.json b/config.json new file mode 100644 index 0000000..58e3f5b --- /dev/null +++ b/config.json @@ -0,0 +1,1636 @@ +{ + "site_config": { + "github_url": "https://github.com/angryscan/angrydata-app", + "email": "admin@angryscan.org", + "base_url": "https://angryscan.org", + "analytics": { + "yandex_metrika_id": "104860635", + "google_tag_manager_id": "GTM-WSLS4F4G", + "bing_verify": "2EF28BCEF4D8F18C669E8DB8C238B4C8" + } + }, + "data": { + "personal_data_numbers": [ + { + "type": "Phone number", + "local_name": "-", + "country": "RU", + "example": "+7 926 3847291" + }, + { + "type": "Phone number", + "local_name": "-", + "country": "US", + "example": "+1 212 5550198" + }, + { + "type": "Taxpayer number", + "local_name": "ИНН", + "country": "RU", + "example": "7707083893" + }, + { + "type": "Taxpayer number", + "local_name": "SSN", + "country": "US", + "example": "536-90-4399" + }, + { + "type": "Taxpayer number", + "local_name": "RIN", + "country": "CN", + "example": "110101199003078912" + }, + { + "type": "Passport", + "local_name": "-", + "country": "RU", + "example": "4505 857555" + }, + { + "type": "Passport", + "local_name": "-", + "country": "US", + "example": "847293641" + }, + { + "type": "Pension insurance number", + "local_name": "СНИЛС", + "country": "RU", + "example": "234-567-890 12" + }, + { + "type": "Medical insurance number", + "local_name": "ОМС", + "country": "RU", + "example": "9876543210987654" + }, + { + "type": "Medical insurance number", + "local_name": "Medicare", + "country": "US", + "example": "1A2B3C4D5E" + }, + { + "type": "Car insurance number", + "local_name": "полис ОСАГО", + "country": "RU", + "example": "ААА3847291847" + }, + { + "type": "Driver license", + "local_name": "Водительские права", + "country": "RU", + "example": "77АВ987654" + }, + { + "type": "Military ID", + "local_name": "Удостоверение личности военнослужащего", + "country": "RU", + "example": "3847291847" + }, + { + "type": "Birthday", + "local_name": "-", + "country": "-", + "example": "15.03.1985" + }, + { + "type": "VIN", + "local_name": "-", + "country": "-", + "example": "1HGBH41JXMN109186" + }, + { + "type": "Employer Identification Number", + "local_name": "EIN", + "country": "US", + "example": "12-3456789" + }, + { + "type": "Individual Taxpayer Identification Number", + "local_name": "ITIN", + "country": "US", + "example": "987-65-4321" + }, + { + "type": "Driver license", + "local_name": "-", + "country": "US", + "example": "D1234567" + }, + { + "type": "Visa number", + "local_name": "-", + "country": "US", + "example": "B12345678" + }, + { + "type": "Alien Registration Number", + "local_name": "A-Number", + "country": "US", + "example": "A123456789" + }, + { + "type": "USCIS receipt number", + "local_name": "USCIS", + "country": "US", + "example": "EAC2190012345" + }, + { + "type": "SEVIS ID", + "local_name": "SEVIS", + "country": "US", + "example": "N0001234567" + }, + { + "type": "Department of Defense ID", + "local_name": "DOD ID", + "country": "US", + "example": "1234567890" + }, + { + "type": "Military Mail Address", + "local_name": "APO/FPO/DPO", + "country": "US", + "example": "FPO AP 96677-1234" + }, + { + "type": "National Stock Number", + "local_name": "NSN", + "country": "US", + "example": "5330-00-123-4567" + }, + { + "type": "Transportation Control Number", + "local_name": "TCN", + "country": "US", + "example": "TCN12345678901234567" + }, + { + "type": "National Provider Identifier", + "local_name": "NPI", + "country": "US", + "example": "1234567890" + } + ], + "personal_data_text": [ + { + "type": "Full name", + "local_name": "ФИО", + "country": "RU", + "example": "Иван Иванович Иванов" + }, + { + "type": "Full name", + "local_name": "Full name", + "country": "US", + "example": "John Smith" + }, + { + "type": "E-mail", + "local_name": "-", + "country": "-", + "example": "captainbull@gmail.com" + }, + { + "type": "Address", + "local_name": "Адрес", + "country": "RU", + "example": "Москва, ул. Ленина, д. 1" + }, + { + "type": "Address", + "local_name": "Address", + "country": "US", + "example": "123 Main St CA 90210" + }, + { + "type": "Login", + "local_name": "-", + "country": "-", + "example": "username" + }, + { + "type": "Password", + "local_name": "-", + "country": "-", + "example": "password123" + } + ], + "pci_dss": [ + { + "type": "Payment card number", + "example": "4400 5678 9012 3456" + }, + { + "type": "CVV", + "example": "456" + } + ], + "banking_secrecy": [ + { + "type": "Bank account (Individual)", + "country": "RU", + "example": "408 028 103 3 5300 5405 83" + }, + { + "type": "Bank account (Legal entity)", + "country": "RU", + "example": "407 028 103 3 5300 5405 83" + }, + { + "type": "Routing Transit Number", + "country": "US", + "example": "123456789" + } + ], + "it_assets": [ + { + "type": "IPv4", + "example": "192.168.1.1" + }, + { + "type": "IPv6", + "example": "2001:db8::1" + }, + { + "type": "Source code files", + "example": "Finds files with source-code. Source code should be placed in git repository." + }, + { + "type": "TLS certificates", + "example": "Finds folders with the most amount of TLS certificates" + }, + { + "type": "Hash data", + "example": "SHA-256, MD5, NTLM (NT hash), SHA-1, SHA-512" + } + ], + "crypto": [ + { + "type": "Crypto wallet", + "example": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa" + }, + { + "type": "Crypto seed phrase", + "example": "A sequence of 12 to 24 words from the BIP39 standard wordlist, used for cryptographic wallet recovery and key derivation" + } + ], + "custom_signatures": { + "description": "It is possible to add custom data search signatures using plain text:", + "examples": [ + "Secret", + "Password", + "Central bank" + ] + }, + "file_types": [ + { + "category": "MS Office (tables)", + "formats": ".xlsx .xls" + }, + { + "category": "MS Office (text)", + "formats": ".docx .doc" + }, + { + "category": "MS Office (presentation)", + "formats": ".pptx .potx .ppsx .pptm .ppt .pps .pot" + }, + { + "category": "Open Office (tables)", + "formats": ".ods" + }, + { + "category": "Open Office (text)", + "formats": ".odt" + }, + { + "category": "Open Office (presentation)", + "formats": ".odp .otp" + }, + { + "category": "Adobe", + "formats": ".pdf" + }, + { + "category": "Archives", + "formats": ".zip .rar" + }, + { + "category": "Plain text", + "formats": ".txt .csv .xml .json .log" + } + ], + "data_sources": [ + { + "connector": "Network Folder", + "description": "Scans files on remote directory like Windows environment" + }, + { + "connector": "HDD/SDD", + "description": "Scan local hard drive" + }, + { + "connector": "S3", + "description": "Scan files in S3" + }, + { + "connector": "HTTP/HTTPS", + "description": "Scans web site content" + } + ], + "features": [ + { + "title_key": "features.ranking.title_h3", + "description_key": "features.ranking.description", + "icon": "check" + }, + { + "title_key": "features.history.title_h3", + "description_key": "features.history.description", + "icon": "history" + }, + { + "title_key": "features.export.title_h3", + "description_key": "features.export.description", + "icon": "download" + }, + { + "title_key": "features.schedule.title_h3", + "description_key": "features.schedule.description", + "icon": "clock" + }, + { + "title_key": "features.matchers.title_h3", + "description_key": "features.matchers.description", + "icon": "settings" + }, + { + "title_key": "features.formats.title_h3", + "description_key": "features.formats.description", + "icon": "file" + } + ], + "downloads": { + "windows": [ + { + "text": "Setup x64", + "href": "https://github.com/angryscan/angrydata-app/releases/latest/download/angry-data-scanner.exe" + }, + { + "text": "Portable x64", + "href": "https://github.com/angryscan/angrydata-app/releases/latest/download/angry-data-scanner-1.5.1-windows-amd64.zip" + } + ], + "linux": [ + { + "text": "DEB x64", + "href": "https://github.com/angryscan/angrydata-app/releases/latest/download/angry-data-scanner_1.5.1_amd64.deb" + }, + { + "text": "Portable x64", + "href": "https://github.com/angryscan/angrydata-app/releases/latest/download/angry-data-scanner-1.5.1-linux-amd64.tar.gz" + } + ], + "macos": [ + { + "text": "macOS x64", + "href": "https://github.com/angryscan/angrydata-app/releases/latest/download/angry-data-scanner-1.5.1-mac-amd64.zip" + }, + { + "text": "macOS ARM64", + "href": "https://github.com/angryscan/angrydata-app/releases/latest/download/angry-data-scanner-1.5.1-mac-aarch64.zip" + } + ] + }, + "system_requirements": "Windows, Linux, MacOS | 400MB HDD | 4GB RAM | 1.3Ghz CPU", + "use_cases": [ + "A leak hunting team scans network folder and ensure that it does not contain source code", + "An employee finds and deletes files containing card numbers to comply with PCI DSS", + "A banking employee scans network folder to ensure that it does not contain PII of VIP clients", + "A boss scans a shared folder of the sales team so they don't have client contacts there", + "Law enforcements need to discover a traces of cryptocurrency on a laptop", + "A cybersecurity officer need to validate that the database does not contain a personal data" + ] + }, + "translations": { + "en": { + "site": { + "title": "Angry Data Scanner - Free Sensitive Data Discovery Tool", + "description": "Advanced sensitive data discovery tool combining personal data discovery, payment card discovery, and passwords finder in one solution. Comprehensive data detector for compliance and security." + }, + "nav": { + "home": "Home", + "data_discovery": "Data Discovery", + "data_discovery_h3": "Data Discovery", + "features": "Features", + "features_h3": "Features", + "use_cases": "Use Cases", + "use_cases_h3": "Use Cases", + "download": "Download", + "download_h3": "Download", + "github": "GitHub", + "contact_us": "Contact us" + }, + "hero": { + "badge": "Free Open Source", + "title_h1": "Sensitive Data Discovery Tool", + "description": "Angry Data Scanner is a free sensitive data discovery tool designed to automatically find PII (Personally Identifiable Information), PHI (Protected Health Information) and intellectual property using advanced pattern matching. Perform unified data searches across local folders, web pages, AWS S3 buckets, and databases.", + "features": { + "simple": "Intuitive design meant for speed and ease of use.", + "two_clicks": "Detect sensitive data instantly with just 2 clicks.", + "no_admin": "No admin rights or installation required.", + "cross_platform": "Works seamlessly on Linux macOS and Windows.", + "privacy": "All scanning happens locally. Your data never leaves your PC." + }, + "cta": { + "download": "Download Now", + "github": "View on GitHub" + } + }, + "sections": { + "discovery": { + "title_h1": "Sensitive data discovery", + "description": "Angry Data Scanner can detect various types of sensitive data across multiple categories", + "filter_by_country": "Filter by country", + "all": "All", + "international": "International", + "russia": "Russia", + "united_states": "United States", + "china": "China" + }, + "file_types": { + "title_h2": "Supported file types" + }, + "data_sources": { + "title_h2": "Supported data sources", + "sources": [ + { + "connector": "Network Folder", + "description": "Scans files on remote directory like Windows environment" + }, + { + "connector": "HDD/SDD", + "description": "Scan local hard drive" + }, + { + "connector": "S3", + "description": "Scan files in S3" + }, + { + "connector": "HTTP/HTTPS", + "description": "Scans web site content" + } + ] + }, + "features": { + "title_h1": "Key features", + "description": "Discover the powerful capabilities that make Angry Data Scanner the ideal solution for sensitive data discovery" + }, + "use_cases": { + "title_h1": "Real life use cases", + "description": "Discover how organizations across different industries use Angry Data Scanner to protect sensitive data and ensure compliance", + "cases": [ + "A leak hunting team scans network folder and ensure that it does not contain source code", + "An employee finds and deletes files containing card numbers to comply with PCI DSS", + "A banking employee scans network folder to ensure that it does not contain PII of VIP clients", + "A boss scans a shared folder of the sales team so they don't have client contacts there", + "Law enforcements need to discover a traces of cryptocurrency on a laptop", + "A cybersecurity officer need to validate that the database does not contain a personal data" + ], + "who_should_use_h2": "Who Should Use Angry Data Scanner?", + "security_teams_h3": "Security Teams", + "security_teams_desc": "Conduct security audits, identify data leaks, and ensure sensitive information is properly protected across your infrastructure.", + "compliance_officers_h3": "Compliance Officers", + "compliance_officers_desc": "Ensure compliance with regulations like GDPR, PCI DSS, HIPAA, and other data protection standards.", + "developers_h3": "Developers & DevOps", + "developers_desc": "Scan repositories and infrastructure to prevent accidental exposure of sensitive data in code or configurations.", + "forensics_h3": "Forensics & Law Enforcement", + "forensics_desc": "Discover traces of sensitive data, cryptocurrency, and other evidence during digital investigations." + }, + "download": { + "title_h1": "Download", + "system_requirements": "System Requirements: Windows, Linux, macOS | 400MB HDD | 4GB RAM | 1.3Ghz CPU", + "cta_description": "Download Angry Data Scanner for Windows, Linux, or macOS. No admin rights required.", + "cta_button": "Download Now", + "getting_started_h2": "Getting Started", + "getting_started_desc": "Quick start guide to help you begin using Angry Data Scanner", + "step1": { + "title_h3": "Download", + "description": "Download the appropriate version for your operating system from the links above." + }, + "step2": { + "title_h3": "Install or Extract", + "description": "Run the installer (Windows/macOS) or extract the portable version. No admin rights required." + }, + "step3": { + "title_h3": "Start Scanning", + "description": "Launch the application, select your data source, and start scanning. Results appear instantly." + } + }, + "quick_links": { + "title_h2": "Explore More", + "description": "Discover all capabilities and features of Angry Data Scanner", + "discovery": "PII, PCI DSS, banking data, crypto wallets, and more", + "features": "Ranking, scheduling, export, and configurable matchers", + "use_cases": "Compliance, security audits, and data protection", + "download": "Windows, Linux, macOS - Get started now" + } + }, + "common": { + "search": "Search" + }, + "categories": { + "personal_data_numbers": "personal data (numbers)", + "personal_data_text": "personal data (text)", + "pci_dss": "PCI DSS data", + "banking_secrecy": "banking secrecy", + "crypto": "cryptocurrency", + "it_assets": "IT assets", + "custom_signatures": "custom signatures", + "custom_signatures_desc": "It is possible to add custom data search signatures using plain text:", + "custom_signatures_or": "or any other.", + "personal_data_numbers_title_h2": "Search personal data (numbers)", + "personal_data_text_title_h2": "Search personal data (text)", + "pci_dss_title_h2": "Search PCI DSS data", + "banking_secrecy_title_h2": "Search banking secrecy", + "crypto_title_h2": "Search cryptocurrency", + "it_assets_title_h2": "Search IT assets", + "custom_signatures_title_h2": "Search custom signatures" + }, + "table_headers": { + "data_type": "Data type", + "local_name": "Local name", + "country": "Country", + "example": "Example", + "file_type": "File Type", + "file_format": "File Format", + "connector": "Connector", + "description": "Description" + }, + "features": { + "ranking": { + "title_h3": "Ranking", + "description": "Scanner shows high-value files first" + }, + "history": { + "title_h3": "View scanning history", + "description": "Track all your previous scans" + }, + "export": { + "title_h3": "Export results", + "description": "Download results in a CSV file" + }, + "schedule": { + "title_h3": "Schedule scans", + "description": "Automate your scanning process" + }, + "matchers": { + "title_h3": "Configurable matchers", + "description": "Configure PII, PCI DSS and other matchers" + }, + "formats": { + "title_h3": "Multiple file formats", + "description": "Configure file formats (pdf, excel, etc.)" + } + }, + "downloads": { + "windows": { + "setup": "Setup x64", + "portable": "Portable x64" + }, + "linux": { + "deb": "DEB x64", + "portable": "Portable x64" + }, + "macos": { + "x64": "macOS x64", + "arm64": "macOS ARM64" + } + }, + "footer": { + "copyright": "© 2025, by admin@angryscan.org" + }, + "empty_message": "No data available for selected country", + "it_assets": [ + { + "type": "IPv4", + "example": "192.168.1.1" + }, + { + "type": "IPv6", + "example": "2001:db8::1" + }, + { + "type": "Source code files", + "example": "Finds files with source-code. Source code should be placed in git repository." + }, + { + "type": "TLS certificates", + "example": "Finds folders with the most amount of TLS certificates" + }, + { + "type": "Hash data", + "example": "SHA-256, MD5, NTLM (NT hash), SHA-1, SHA-512" + } + ], + "crypto": [ + { + "type": "Crypto wallet", + "example": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa" + }, + { + "type": "Crypto seed phrase", + "example": "A sequence of 12 to 24 words from the BIP39 standard wordlist, used for cryptographic wallet recovery and key derivation" + } + ] + }, + "ru": { + "site": { + "title": "Angry Data Scanner - Программа поиска конфиденциальных данных для Mac, Windows и Linux", + "description": "Инструмент поиска данных. С его помощью можно найти персональные данные, данные банковских карт и другие конфиденциальные данные в папках, S3, базах данных, веб-страницах." + }, + "nav": { + "home": "Главная", + "data_discovery": "Данные", + "data_discovery_h3": "Данные", + "features": "Возможности", + "features_h3": "Возможности", + "use_cases": "Примеры", + "use_cases_h3": "Примеры", + "download": "Скачать", + "download_h3": "Скачать", + "github": "GitHub", + "contact_us": "Связаться с нами" + }, + "hero": { + "badge": "Free Open Source", + "title_h1": "Бесплатная программа для быстрого поиска конфиденциальных данных", + "description": "Бесплатный инструмент для автоматического поиска PII, PHI и интеллектуальной собственности с помощью расширенного сопоставления шаблонов. Выполняйте поиск данных в локальных папках, веб-страницах, AWS S3 и базах данных.", + "features": { + "simple": "Интуитивный дизайн для скорости и удобства.", + "two_clicks": "Обнаружение конфиденциальных данных за 2 клика.", + "no_admin": "Не требуются права администратора или установка.", + "cross_platform": "Работает на Linux, macOS и Windows.", + "privacy": "Все сканирование происходит локально. Данные не покидают ваш компьютер." + }, + "cta": { + "download": "Скачать сейчас", + "github": "Посмотреть на GitHub" + } + }, + "sections": { + "discovery": { + "title_h1": "Поиск чувствительных данных", + "description": "Angry Data Scanner может обнаруживать различные типы конфиденциальных данных в нескольких категориях", + "filter_by_country": "Фильтр по стране", + "all": "Все", + "international": "Международные", + "russia": "Россия", + "united_states": "Соединенные Штаты", + "china": "Китай" + }, + "file_types": { + "title_h2": "Поддерживаемые типы файлов" + }, + "data_sources": { + "title_h2": "Поддерживаемые источники данных", + "sources": [ + { + "connector": "Сетевая папка", + "description": "Сканирует файлы в удаленной директории, например в среде Windows" + }, + { + "connector": "HDD/SSD", + "description": "Сканирование локального жесткого диска" + }, + { + "connector": "S3", + "description": "Сканирование файлов в S3" + }, + { + "connector": "HTTP/HTTPS", + "description": "Сканирование содержимого веб-сайта" + } + ] + }, + "features": { + "title_h1": "Ключевые возможности", + "description": "Откройте для себя мощные возможности, которые делают Angry Data Scanner идеальным решением для обнаружения конфиденциальных данных" + }, + "use_cases": { + "title_h1": "Реальные случаи использования", + "description": "Узнайте, как организации из разных отраслей используют Angry Data Scanner для защиты конфиденциальных данных и обеспечения соответствия требованиям", + "cases": [ + "Команда по поиску утечек сканирует сетевую папку и убеждается, что она не содержит исходный код", + "Сотрудник находит и удаляет файлы, содержащие номера карт, для соответствия PCI DSS", + "Банковский сотрудник сканирует сетевую папку, чтобы убедиться, что она не содержит PII VIP-клиентов", + "Руководитель сканирует общую папку отдела продаж, чтобы там не было контактов клиентов", + "Правоохранительным органам нужно обнаружить следы криптовалюты на ноутбуке", + "Специалист по кибербезопасности должен проверить, что база данных не содержит персональных данных" + ], + "who_should_use_h2": "Кому подходит Angry Data Scanner?", + "security_teams_h3": "Команды безопасности", + "security_teams_desc": "Проводите аудиты безопасности, выявляйте утечки данных и обеспечивайте надлежащую защиту конфиденциальной информации в вашей инфраструктуре.", + "compliance_officers_h3": "Специалисты по соответствию", + "compliance_officers_desc": "Обеспечивайте соответствие требованиям GDPR, PCI DSS, HIPAA и другим стандартам защиты данных.", + "developers_h3": "Разработчики и DevOps", + "developers_desc": "Сканируйте репозитории и инфраструктуру, чтобы предотвратить случайное раскрытие конфиденциальных данных в коде или конфигурациях.", + "forensics_h3": "Криминалистика и правоохранительные органы", + "forensics_desc": "Обнаруживайте следы конфиденциальных данных, криптовалюты и другие доказательства во время цифровых расследований." + }, + "download": { + "title_h1": "Скачать", + "system_requirements": "Системные требования: Windows, Linux, macOS | 400 МБ на жестком диске | 4 ГБ ОЗУ | процессор 1.3 ГГц", + "cta_description": "Скачайте Angry Data Scanner для Windows, Linux или macOS. Права администратора не требуются.", + "cta_button": "Скачать сейчас", + "getting_started_h2": "Начало работы", + "getting_started_desc": "Краткое руководство, которое поможет вам начать использовать Angry Data Scanner", + "step1": { + "title_h3": "Скачать", + "description": "Скачайте подходящую версию для вашей операционной системы по ссылкам выше." + }, + "step2": { + "title_h3": "Установить или распаковать", + "description": "Запустите установщик (Windows/macOS) или распакуйте портативную версию. Права администратора не требуются." + }, + "step3": { + "title_h3": "Начать сканирование", + "description": "Запустите приложение, выберите источник данных и начните сканирование. Результаты появятся мгновенно." + } + }, + "quick_links": { + "title_h2": "Узнать больше", + "description": "Откройте для себя все возможности и функции Angry Data Scanner", + "discovery": "PII, PCI DSS, банковские данные, криптокошельки и многое другое", + "features": "Ранжирование, планирование, экспорт и настраиваемые матчеры", + "use_cases": "Соответствие требованиям, аудиты безопасности и защита данных", + "download": "Windows, Linux, macOS - Начните прямо сейчас" + } + }, + "categories": { + "personal_data_numbers": "персональных данных (числа)", + "personal_data_text": "персональных данных (текст)", + "pci_dss": "данных PCI DSS", + "banking_secrecy": "банковской тайны", + "crypto": "криптовалюты", + "it_assets": "IT-активов", + "custom_signatures": "пользовательских сигнатур", + "custom_signatures_desc": "Можно добавить пользовательские сигнатуры поиска данных, используя обычный текст:", + "custom_signatures_or": "или любой другой.", + "personal_data_numbers_title_h2": "Поиск персональных данных (числа)", + "personal_data_text_title_h2": "Поиск персональных данных (текст)", + "pci_dss_title_h2": "Поиск данных PCI DSS", + "banking_secrecy_title_h2": "Поиск банковской тайны", + "crypto_title_h2": "Поиск криптовалюты", + "it_assets_title_h2": "Поиск IT-активов", + "custom_signatures_title_h2": "Поиск пользовательских сигнатур" + }, + "table_headers": { + "data_type": "Тип данных", + "local_name": "Локальное название", + "country": "Страна", + "example": "Пример", + "file_type": "Тип файла", + "file_format": "Формат файла", + "connector": "Коннектор", + "description": "Описание" + }, + "features": { + "ranking": { + "title_h3": "Ранжирование", + "description": "Сканер показывает файлы с высокой ценностью первыми" + }, + "history": { + "title_h3": "Просмотр истории сканирования", + "description": "Отслеживайте все ваши предыдущие сканирования" + }, + "export": { + "title_h3": "Экспорт результатов", + "description": "Скачайте результаты в CSV файл" + }, + "schedule": { + "title_h3": "Планирование сканирований", + "description": "Автоматизируйте процесс сканирования" + }, + "matchers": { + "title_h3": "Настраиваемые матчеры", + "description": "Настройте PII, PCI DSS и другие матчеры" + }, + "formats": { + "title_h3": "Множество форматов файлов", + "description": "Настройте форматы файлов (pdf, excel и т.д.)" + } + }, + "downloads": { + "windows": { + "setup": "Setup x64", + "portable": "Portable x64" + }, + "linux": { + "deb": "DEB x64", + "portable": "Portable x64" + }, + "macos": { + "x64": "macOS x64", + "arm64": "macOS ARM64" + } + }, + "footer": { + "copyright": "© 2025, от admin@angryscan.org" + }, + "empty_message": "Нет данных для выбранной страны", + "it_assets": [ + { + "type": "IPv4", + "example": "192.168.1.1" + }, + { + "type": "IPv6", + "example": "2001:db8::1" + }, + { + "type": "Source code files", + "example": "Находит файлы с исходным кодом. Исходный код должен быть размещен в git репозитории." + }, + { + "type": "TLS certificates", + "example": "Находит папки с наибольшим количеством TLS сертификатов" + }, + { + "type": "Hash data", + "example": "SHA-256, MD5, NTLM (NT hash), SHA-1, SHA-512" + } + ], + "crypto": [ + { + "type": "Crypto wallet", + "example": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa" + }, + { + "type": "Crypto seed phrase", + "example": "Последовательность из 12-24 слов из стандартного словаря BIP39, используемая для восстановления криптокошелька и вывода криптографических ключей" + } + ], + "common": { + "search": "Поиск" + } + }, + "es": { + "site": { + "title": "Angry Data Scanner: herramienta gratuita de descubrimiento de datos confidenciales", + "description": "Descubra PII, PCI DSS y datos confidenciales en carpetas, S3 y bases de datos. Herramienta gratuita de código abierto para Windows, Linux, macOS. No se requieren derechos de administrador." + }, + "nav": { + "home": "Hogar", + "data_discovery": "Descubrimiento de datos", + "data_discovery_h3": "Descubrimiento de datos", + "features": "Características", + "features_h3": "Características", + "use_cases": "Casos de uso", + "use_cases_h3": "Casos de uso", + "download": "Descargar", + "download_h3": "Descargar", + "github": "GitHub", + "contactUs": "Contáctenos" + }, + "hero": { + "badge": "Código abierto gratuito", + "title_h1": "Herramienta de descubrimiento de datos confidenciales", + "description": "Angry Data Scanner utiliza la coincidencia de patrones para descubrir automáticamente datos confidenciales almacenados en carpetas, páginas web, S3 y bases de datos. Ayuda a las organizaciones a identificar dónde se almacenan datos confidenciales, como información de identificación personal (PII) y propiedad intelectual.", + "features": { + "simple": "Interfaz de usuario sencilla: diseño intuitivo pensado para la velocidad y la facilidad de uso.", + "twoClicks": "Descubrimiento con un clic: detecte datos confidenciales al instante con solo 2 clics.", + "noAdmin": "Configuración sin complicaciones: no se requieren derechos de administrador ni instalación.", + "crossPlatform": "Multiplataforma: funciona perfectamente en Linux, macOS y Windows.", + "privacy": "Privacidad total: todo el escaneo se realiza localmente. Tus datos nunca salen de tu PC." + }, + "cta": { + "download": "Descargar ahora", + "github": "Ver en GitHub" + } + }, + "sections": { + "quickLinks": { + "title_h2": "Explora más", + "description": "Descubra todas las capacidades y características de Angry Data Scanner", + "discovery": "PII, PCI DSS, datos bancarios, billeteras criptográficas y más", + "features": "Clasificación, programación, exportación y comparadores configurables", + "useCases": "Cumplimiento, auditorías de seguridad y protección de datos", + "download": "Windows, Linux, macOS: comience ahora" + }, + "quick_links": { + "title_h2": "Explora más" + }, + "discovery": { + "title_h1": "Descubrimiento de datos confidenciales", + "description": "Angry Data Scanner puede detectar varios tipos de datos confidenciales en múltiples categorías", + "filterByCountry": "Filtrar por país", + "all": "Todo", + "international": "Internacional", + "russia": "Rusia", + "unitedStates": "Estados Unidos", + "china": "Porcelana" + }, + "fileTypes": { + "title_h2": "Tipos de archivos admitidos" + }, + "file_types": { + "title_h2": "Tipos de archivos admitidos" + }, + "dataSources": { + "title_h2": "Fuentes de datos admitidas" + }, + "data_sources": { + "title_h2": "Fuentes de datos admitidas" + }, + "features": { + "title_h1": "Características clave", + "description": "Descubra las poderosas capacidades que hacen de Angry Data Scanner la solución ideal para el descubrimiento de datos confidenciales" + }, + "use_cases": { + "title_h1": "Casos de uso de la vida real", + "description": "Descubra cómo organizaciones de diferentes industrias utilizan Angry Data Scanner para proteger datos confidenciales y garantizar el cumplimiento.", + "cases": [ + "Un equipo de caza de fugas escanea una carpeta de red y se asegura de que no contenga código fuente", + "Un empleado encuentra y elimina archivos que contienen números de tarjeta para cumplir con PCI DSS", + "Un empleado bancario escanea una carpeta de red para asegurarse de que no contenga PII de clientes VIP", + "Un jefe escanea una carpeta compartida del equipo de ventas para que no tengan contactos de clientes allí", + "Las fuerzas del orden necesitan descubrir rastros de criptomoneda en una laptop", + "Un oficial de ciberseguridad necesita validar que la base de datos no contenga datos personales" + ], + "who_should_use_h2": "¿Quién debería utilizar Angry Data Scanner?", + "security_teams_h3": "Equipos de seguridad", + "security_teams_desc": "Realice auditorías de seguridad, identifique fugas de datos y asegúrese de que la información confidencial esté protegida adecuadamente en toda su infraestructura.", + "compliance_officers_h3": "Oficiales de cumplimiento", + "compliance_officers_desc": "Garantice el cumplimiento de normativas como GDPR, PCI DSS, HIPAA y otros estándares de protección de datos.", + "developers_h3": "Desarrolladores y DevOps", + "developers_desc": "Escanee repositorios e infraestructura para evitar la exposición accidental de datos confidenciales en el código o las configuraciones.", + "forensics_h3": "Forense y aplicación de la ley", + "forensics_desc": "Descubra rastros de datos confidenciales, criptomonedas y otras pruebas durante las investigaciones digitales." + }, + "download": { + "title_h1": "Descargar", + "systemRequirements": "Requisitos del sistema: Windows, Linux, macOS | Disco duro de 400 MB | 4 GB de RAM | Procesador de 1,3 GHz", + "gettingStarted": "Empezando", + "getting_started_h2": "Empezando", + "gettingStartedDesc": "Guía de inicio rápido para ayudarle a comenzar a utilizar Angry Data Scanner", + "step1": { + "title_h3": "Descargar", + "description": "Descargue la versión adecuada para su sistema operativo desde los enlaces anteriores." + }, + "step2": { + "title_h3": "Instalar o extraer", + "description": "Ejecute el instalador (Windows/macOS) o extraiga la versión portátil. No se requieren derechos de administrador." + }, + "step3": { + "title_h3": "Iniciar escaneo", + "description": "Inicie la aplicación, seleccione su fuente de datos y comience a escanear. Los resultados aparecen al instante." + } + } + }, + "footer": { + "copyright": "© 2025, por admin@angryscan.org" + }, + "categories": { + "personalDataNumbers": "Datos personales (números)", + "personalDataText": "Datos personales (texto)", + "pciDss": "PCI DSS", + "bankingSecrecy": "Secreto bancario", + "crypto": "criptomonedas", + "itAssets": "Activos de TI", + "customSignatures": "Firmas personalizadas", + "customSignaturesDesc": "Es posible agregar firmas de búsqueda de datos personalizadas utilizando texto sin formato:", + "customSignaturesOr": "o cualquier otro.", + "personal_data_numbers": "datos personales (números)", + "personal_data_text": "datos personales (texto)", + "pci_dss": "datos PCI DSS", + "banking_secrecy": "secreto bancario", + "it_assets": "activos de TI", + "custom_signatures": "firmas personalizadas", + "personal_data_numbers_title_h2": "Búsqueda de datos personales (números)", + "personal_data_text_title_h2": "Búsqueda de datos personales (texto)", + "pci_dss_title_h2": "Búsqueda de datos PCI DSS", + "banking_secrecy_title_h2": "Búsqueda de secreto bancario", + "crypto_title_h2": "Búsqueda de criptomonedas", + "it_assets_title_h2": "Búsqueda de activos de TI", + "custom_signatures_title_h2": "Búsqueda de firmas personalizadas" + }, + "tableHeaders": { + "dataType": "tipo de datos", + "localName": "Nombre local", + "country": "País", + "example": "Ejemplo", + "fileType": "Tipo de archivo", + "fileFormat": "Formato de archivo", + "connector": "Conector", + "description": "Descripción" + }, + "features": { + "ranking": { + "title_h3": "Categoría", + "description": "El escáner muestra primero los archivos de alto valor, lo que le ayuda a priorizar los hallazgos más críticos. El sistema de clasificación inteligente analiza la sensibilidad de los datos y presenta los resultados en orden de importancia." + }, + "history": { + "title_h3": "Ver historial de escaneo", + "description": "Realice un seguimiento de todos sus escaneos anteriores con un historial detallado. Revise resultados anteriores, compare escaneos a lo largo del tiempo y mantenga un seguimiento de auditoría completo de sus actividades de descubrimiento de datos." + }, + "export": { + "title_h3": "Exportar resultados", + "description": "Descargue los resultados en un archivo CSV para realizar más análisis, generar informes o integrarlos con otras herramientas. La exportación incluye todos los tipos de datos, ubicaciones y metadatos detectados." + }, + "schedule": { + "title_h3": "Programar escaneos", + "description": "Automatice su proceso de escaneo con escaneos programados. Configure análisis recurrentes para monitorear continuamente sus fuentes de datos y garantizar el cumplimiento continuo." + }, + "matchers": { + "title_h3": "Comparadores configurables", + "description": "Configure PII, PCI DSS y otros comparadores para que coincidan con sus requisitos de cumplimiento específicos. Habilite o deshabilite los patrones de detección según sus necesidades." + }, + "formats": { + "title_h3": "Múltiples formatos de archivos", + "description": "Configure los formatos de archivos (pdf, excel, etc.) para escanear. Soporte para MS Office, Open Office, Adobe PDF, archivos y archivos de texto sin formato. Personalice qué formatos incluir en sus escaneos." + } + }, + "common": { + "search": "Búsqueda" + } + }, + "de": { + "site": { + "title": "Angry Data Scanner – kostenloses Tool zur Erkennung sensibler Daten", + "description": "Entdecken Sie PII, PCI DSS und vertrauliche Daten in Ordnern, S3 und Datenbanken. Kostenloses Open-Source-Tool für Windows, Linux, macOS. Keine Administratorrechte erforderlich." + }, + "nav": { + "home": "Heim", + "data_discovery": "Datenermittlung", + "data_discovery_h3": "Datenermittlung", + "features": "Merkmale", + "features_h3": "Merkmale", + "use_cases": "Anwendungsfälle", + "use_cases_h3": "Anwendungsfälle", + "download": "Herunterladen", + "download_h3": "Herunterladen", + "github": "GitHub", + "contactUs": "Kontaktieren Sie uns" + }, + "hero": { + "badge": "Kostenlose Open Source", + "title_h1": "Tool zur Erkennung sensibler Daten", + "description": "Angry Data Scanner nutzt Mustervergleich, um vertrauliche Daten, die in Ordnern, Webseiten, S3 und Datenbanken gespeichert sind, automatisch zu erkennen. Es hilft Unternehmen dabei, herauszufinden, wo sensible Daten wie personenbezogene Daten (PII) und geistiges Eigentum gespeichert sind.", + "features": { + "simple": "Einfache Benutzeroberfläche: Intuitives Design für Geschwindigkeit und Benutzerfreundlichkeit.", + "twoClicks": "One-Click Discovery: Erkennen Sie sensible Daten sofort mit nur 2 Klicks.", + "noAdmin": "Problemlose Einrichtung: Keine Administratorrechte oder Installation erforderlich.", + "crossPlatform": "Plattformübergreifend: Funktioniert nahtlos unter Linux, macOS und Windows.", + "privacy": "Absolute Privatsphäre: Alle Scanvorgänge erfolgen lokal. Ihre Daten verlassen nie Ihren PC." + }, + "cta": { + "download": "Jetzt herunterladen", + "github": "Auf GitHub ansehen" + } + }, + "sections": { + "quickLinks": { + "title_h2": "Entdecken Sie mehr", + "description": "Entdecken Sie alle Möglichkeiten und Features von Angry Data Scanner", + "discovery": "PII, PCI DSS, Bankdaten, Krypto-Wallets und mehr", + "features": "Ranking, Planung, Export und konfigurierbare Matcher", + "useCases": "Compliance, Sicherheitsaudits und Datenschutz", + "download": "Windows, Linux, macOS – Jetzt starten" + }, + "quick_links": { + "title_h2": "Entdecken Sie mehr" + }, + "discovery": { + "title_h1": "Sensible Datenerkennung", + "description": "Angry Data Scanner kann verschiedene Arten sensibler Daten in mehreren Kategorien erkennen", + "filterByCountry": "Nach Land filtern", + "all": "Alle", + "international": "International", + "russia": "Russland", + "unitedStates": "Vereinigte Staaten", + "china": "China" + }, + "fileTypes": { + "title_h2": "Unterstützte Dateitypen" + }, + "file_types": { + "title_h2": "Unterstützte Dateitypen" + }, + "dataSources": { + "title_h2": "Unterstützte Datenquellen" + }, + "data_sources": { + "title_h2": "Unterstützte Datenquellen" + }, + "features": { + "title_h1": "Hauptmerkmale", + "description": "Entdecken Sie die leistungsstarken Funktionen, die Angry Data Scanner zur idealen Lösung für die Erkennung sensibler Daten machen" + }, + "use_cases": { + "title_h1": "Anwendungsfälle aus dem wirklichen Leben", + "description": "Entdecken Sie, wie Unternehmen aus verschiedenen Branchen Angry Data Scanner nutzen, um sensible Daten zu schützen und Compliance sicherzustellen", + "cases": [ + "Ein Leck-Jagd-Team scannt einen Netzwerkordner und stellt sicher, dass er keinen Quellcode enthält", + "Ein Mitarbeiter findet und löscht Dateien mit Kartennummern, um PCI DSS zu entsprechen", + "Ein Bankmitarbeiter scannt einen Netzwerkordner, um sicherzustellen, dass er keine PII von VIP-Kunden enthält", + "Ein Chef scannt einen freigegebenen Ordner des Vertriebsteams, damit dort keine Kundenkontakte vorhanden sind", + "Strafverfolgungsbehörden müssen Spuren von Kryptowährung auf einem Laptop entdecken", + "Ein Cybersicherheitsbeauftragter muss überprüfen, dass die Datenbank keine persönlichen Daten enthält" + ], + "who_should_use_h2": "Wer sollte Angry Data Scanner verwenden?", + "security_teams_h3": "Sicherheitsteams", + "security_teams_desc": "Führen Sie Sicherheitsüberprüfungen durch, identifizieren Sie Datenlecks und stellen Sie sicher, dass vertrauliche Informationen in Ihrer gesamten Infrastruktur ordnungsgemäß geschützt sind.", + "compliance_officers_h3": "Compliance-Beauftragte", + "compliance_officers_desc": "Stellen Sie die Einhaltung von Vorschriften wie DSGVO, PCI DSS, HIPAA und anderen Datenschutzstandards sicher.", + "developers_h3": "Entwickler und DevOps", + "developers_desc": "Scannen Sie Repositorys und Infrastruktur, um zu verhindern, dass vertrauliche Daten in Code oder Konfigurationen versehentlich offengelegt werden.", + "forensics_h3": "Forensik und Strafverfolgung", + "forensics_desc": "Entdecken Sie bei digitalen Ermittlungen Spuren sensibler Daten, Kryptowährungen und anderer Beweise." + }, + "download": { + "title_h1": "Herunterladen", + "systemRequirements": "Systemanforderungen: Windows, Linux, macOS | 400 MB Festplatte | 4GB RAM | 1,3-GHz-CPU", + "gettingStarted": "Erste Schritte", + "getting_started_h2": "Erste Schritte", + "gettingStartedDesc": "Kurzanleitung, die Ihnen den Einstieg in die Verwendung von Angry Data Scanner erleichtert", + "step1": { + "title_h3": "Herunterladen", + "description": "Laden Sie über die obigen Links die entsprechende Version für Ihr Betriebssystem herunter." + }, + "step2": { + "title_h3": "Installieren oder extrahieren", + "description": "Führen Sie das Installationsprogramm aus (Windows/macOS) oder extrahieren Sie die portable Version. Keine Administratorrechte erforderlich." + }, + "step3": { + "title_h3": "Starten Sie den Scanvorgang", + "description": "Starten Sie die Anwendung, wählen Sie Ihre Datenquelle aus und beginnen Sie mit dem Scannen. Die Ergebnisse werden sofort angezeigt." + } + } + }, + "footer": { + "copyright": "© 2025, von admin@angryscan.org" + }, + "categories": { + "personalDataNumbers": "Persönliche Daten (Zahlen)", + "personalDataText": "Persönliche Daten (Text)", + "pciDss": "PCI DSS", + "bankingSecrecy": "Bankgeheimnis", + "crypto": "Kryptowährung", + "itAssets": "IT-Assets", + "customSignatures": "Benutzerdefinierte Signaturen", + "customSignaturesDesc": "Es ist möglich, benutzerdefinierte Datensuchsignaturen mithilfe von Klartext hinzuzufügen:", + "customSignaturesOr": "oder irgendein anderes.", + "personal_data_numbers": "personenbezogener Daten (Zahlen)", + "personal_data_text": "personenbezogener Daten (Text)", + "pci_dss": "PCI DSS-Daten", + "banking_secrecy": "Bankgeheimnis", + "it_assets": "IT-Assets", + "custom_signatures": "benutzerdefinierter Signaturen", + "personal_data_numbers_title_h2": "Suche personenbezogener Daten (Zahlen)", + "personal_data_text_title_h2": "Suche personenbezogener Daten (Text)", + "pci_dss_title_h2": "Suche PCI DSS-Daten", + "banking_secrecy_title_h2": "Suche Bankgeheimnis", + "crypto_title_h2": "Suche Kryptowährung", + "it_assets_title_h2": "Suche IT-Assets", + "custom_signatures_title_h2": "Suche benutzerdefinierter Signaturen" + }, + "tableHeaders": { + "dataType": "Datentyp", + "localName": "Lokaler Name", + "country": "Land", + "example": "Beispiel", + "fileType": "Dateityp", + "fileFormat": "Dateiformat", + "connector": "Stecker", + "description": "Beschreibung" + }, + "features": { + "ranking": { + "title_h3": "Rang", + "description": "Der Scanner zeigt hochwertige Dateien zuerst an und hilft Ihnen, die kritischsten Ergebnisse zu priorisieren. Das intelligente Ranking-System analysiert die Datensensitivität und präsentiert die Ergebnisse in der Reihenfolge ihrer Wichtigkeit." + }, + "history": { + "title_h3": "Scanverlauf anzeigen", + "description": "Verfolgen Sie alle Ihre vorherigen Scans mit detailliertem Verlauf. Überprüfen Sie frühere Ergebnisse, vergleichen Sie Scans im Laufe der Zeit und führen Sie einen vollständigen Prüfpfad Ihrer Datenerkennungsaktivitäten." + }, + "export": { + "title_h3": "Ergebnisse exportieren", + "description": "Laden Sie die Ergebnisse zur weiteren Analyse, Berichterstellung oder Integration mit anderen Tools in eine CSV-Datei herunter. Der Export umfasst alle erkannten Datentypen, Speicherorte und Metadaten." + }, + "schedule": { + "title_h3": "Planen Sie Scans", + "description": "Automatisieren Sie Ihren Scanvorgang mit geplanten Scans. Richten Sie wiederkehrende Scans ein, um Ihre Datenquellen kontinuierlich zu überwachen und die fortlaufende Compliance sicherzustellen." + }, + "matchers": { + "title_h3": "Konfigurierbare Matcher", + "description": "Konfigurieren Sie PII, PCI DSS und andere Matcher entsprechend Ihren spezifischen Compliance-Anforderungen. Aktivieren oder deaktivieren Sie Erkennungsmuster entsprechend Ihren Anforderungen." + }, + "formats": { + "title_h3": "Mehrere Dateiformate", + "description": "Konfigurieren Sie die zu scannenden Dateiformate (PDF, Excel usw.). Unterstützung für MS Office, Open Office, Adobe PDF, Archive und Nur-Text-Dateien. Passen Sie an, welche Formate in Ihre Scans einbezogen werden sollen." + } + }, + "common": { + "search": "Suche" + } + }, + "fr": { + "site": { + "title": "Angry Data Scanner - Outil gratuit de découverte de données sensibles", + "description": "Découvrez les informations PII, PCI DSS et les données sensibles dans les dossiers, S3 et bases de données. Outil open source gratuit pour Windows, Linux, macOS. Aucun droit d'administrateur requis." + }, + "nav": { + "home": "Maison", + "data_discovery": "Découverte de données", + "data_discovery_h3": "Découverte de données", + "features": "Caractéristiques", + "features_h3": "Caractéristiques", + "use_cases": "Cas d'utilisation", + "use_cases_h3": "Cas d'utilisation", + "download": "Télécharger", + "download_h3": "Télécharger", + "github": "GitHub", + "contactUs": "Contactez-nous" + }, + "hero": { + "badge": "Source ouverte gratuite", + "title_h1": "Outil de découverte de données sensibles", + "description": "Angry Data Scanner utilise la correspondance de modèles pour découvrir automatiquement les données sensibles stockées dans des dossiers, des pages Web, S3 et des bases de données. Il aide les organisations à identifier où sont stockées les données sensibles telles que les informations personnelles identifiables (PII) et la propriété intellectuelle.", + "features": { + "simple": "Interface utilisateur simple : conception intuitive conçue pour la rapidité et la facilité d'utilisation.", + "twoClicks": "Découverte en un clic : détectez instantanément les données sensibles en seulement 2 clics.", + "noAdmin": "Installation sans tracas : aucun droit d'administrateur ni installation requis.", + "crossPlatform": "Multiplateforme : fonctionne de manière transparente sur Linux macOS et Windows.", + "privacy": "Confidentialité totale : toutes les analyses s'effectuent localement. Vos données ne quittent jamais votre PC." + }, + "cta": { + "download": "Télécharger maintenant", + "github": "Voir sur GitHub" + } + }, + "sections": { + "quickLinks": { + "title_h2": "Explorer davantage", + "description": "Découvrez toutes les capacités et fonctionnalités d'Angry Data Scanner", + "discovery": "PII, PCI DSS, données bancaires, portefeuilles cryptographiques, etc.", + "features": "Classement, planification, exportation et correspondances configurables", + "useCases": "Conformité, audits de sécurité et protection des données", + "download": "Windows, Linux, macOS – Commencez dès maintenant" + }, + "quick_links": { + "title_h2": "Explorer davantage" + }, + "discovery": { + "title_h1": "Découverte de données sensibles", + "description": "Angry Data Scanner peut détecter différents types de données sensibles dans plusieurs catégories", + "filterByCountry": "Filtrer par pays", + "all": "Tous", + "international": "International", + "russia": "Russie", + "unitedStates": "États-Unis", + "china": "Chine" + }, + "fileTypes": { + "title_h2": "Types de fichiers pris en charge" + }, + "file_types": { + "title_h2": "Types de fichiers pris en charge" + }, + "dataSources": { + "title_h2": "Sources de données prises en charge" + }, + "data_sources": { + "title_h2": "Sources de données prises en charge" + }, + "features": { + "title_h1": "Principales caractéristiques", + "description": "Découvrez les puissantes capacités qui font d'Angry Data Scanner la solution idéale pour la découverte de données sensibles" + }, + "use_cases": { + "title_h1": "Cas d'utilisation réels", + "description": "Découvrez comment des organisations de différents secteurs utilisent Angry Data Scanner pour protéger les données sensibles et garantir la conformité.", + "cases": [ + "Une équipe de chasse aux fuites scanne un dossier réseau et s'assure qu'il ne contient pas de code source", + "Un employé trouve et supprime les fichiers contenant des numéros de carte pour se conformer à PCI DSS", + "Un employé bancaire scanne un dossier réseau pour s'assurer qu'il ne contient pas de PII de clients VIP", + "Un patron scanne un dossier partagé de l'équipe commerciale pour qu'ils n'aient pas de contacts clients là-bas", + "Les forces de l'ordre doivent découvrir des traces de cryptomonnaie sur un ordinateur portable", + "Un responsable de la cybersécurité doit valider que la base de données ne contient pas de données personnelles" + ], + "who_should_use_h2": "Qui devrait utiliser Angry Data Scanner ?", + "security_teams_h3": "Équipes de sécurité", + "security_teams_desc": "Réalisez des audits de sécurité, identifiez les fuites de données et assurez-vous que les informations sensibles sont correctement protégées dans votre infrastructure.", + "compliance_officers_h3": "Agents de conformité", + "compliance_officers_desc": "Assurez le respect des réglementations telles que le RGPD, PCI DSS, HIPAA et d'autres normes de protection des données.", + "developers_h3": "Développeurs et DevOps", + "developers_desc": "Analysez les référentiels et l'infrastructure pour éviter l'exposition accidentelle de données sensibles dans le code ou les configurations.", + "forensics_h3": "Médecine légale et application de la loi", + "forensics_desc": "Découvrez des traces de données sensibles, de cryptomonnaies et d'autres preuves lors d'enquêtes numériques." + }, + "download": { + "title_h1": "Télécharger", + "systemRequirements": "Configuration système requise : Windows, Linux, macOS | Disque dur de 400 Mo | 4 Go de RAM | Processeur 1,3 GHz", + "gettingStarted": "Commencer", + "getting_started_h2": "Commencer", + "gettingStartedDesc": "Guide de démarrage rapide pour vous aider à commencer à utiliser Angry Data Scanner", + "step1": { + "title_h3": "Télécharger", + "description": "Téléchargez la version appropriée pour votre système d'exploitation à partir des liens ci-dessus." + }, + "step2": { + "title_h3": "Installer ou extraire", + "description": "Exécutez le programme d'installation (Windows/macOS) ou extrayez la version portable. Aucun droit d'administrateur requis." + }, + "step3": { + "title_h3": "Démarrer la numérisation", + "description": "Lancez l'application, sélectionnez votre source de données et lancez la numérisation. Les résultats apparaissent instantanément." + } + } + }, + "footer": { + "copyright": "© 2025, par admin@angryscan.org" + }, + "categories": { + "personalDataNumbers": "Données personnelles (chiffres)", + "personalDataText": "Données personnelles (texte)", + "pciDss": "PCI DSS", + "bankingSecrecy": "Secret bancaire", + "crypto": "cryptomonnaie", + "itAssets": "Actifs informatiques", + "customSignatures": "Signatures personnalisées", + "customSignaturesDesc": "Il est possible d'ajouter des signatures de recherche de données personnalisées en utilisant du texte brut :", + "customSignaturesOr": "ou tout autre.", + "personal_data_numbers": "données personnelles (chiffres)", + "personal_data_text": "données personnelles (texte)", + "pci_dss": "données PCI DSS", + "banking_secrecy": "secret bancaire", + "it_assets": "actifs informatiques", + "custom_signatures": "signatures personnalisées", + "personal_data_numbers_title_h2": "Recherche de données personnelles (chiffres)", + "personal_data_text_title_h2": "Recherche de données personnelles (texte)", + "pci_dss_title_h2": "Recherche de données PCI DSS", + "banking_secrecy_title_h2": "Recherche de secret bancaire", + "crypto_title_h2": "Recherche de cryptomonnaie", + "it_assets_title_h2": "Recherche d'actifs informatiques", + "custom_signatures_title_h2": "Recherche de signatures personnalisées" + }, + "tableHeaders": { + "dataType": "Type de données", + "localName": "Nom local", + "country": "Pays", + "example": "Exemple", + "fileType": "Type de fichier", + "fileFormat": "Format de fichier", + "connector": "Connecteur", + "description": "Description" + }, + "features": { + "ranking": { + "title_h3": "Classement", + "description": "Le scanner affiche en premier les fichiers de grande valeur, vous aidant ainsi à prioriser les résultats les plus critiques. Le système de classement intelligent analyse la sensibilité des données et présente les résultats par ordre d'importance." + }, + "history": { + "title_h3": "Afficher l'historique des analyses", + "description": "Suivez toutes vos analyses précédentes avec un historique détaillé. Examinez les résultats antérieurs, comparez les analyses au fil du temps et conservez une piste d'audit complète de vos activités de découverte de données." + }, + "export": { + "title_h3": "Exporter les résultats", + "description": "Téléchargez les résultats dans un fichier CSV pour une analyse plus approfondie, des rapports ou une intégration avec d'autres outils. L'exportation inclut tous les types de données, emplacements et métadonnées détectés." + }, + "schedule": { + "title_h3": "Planifier des analyses", + "description": "Automatisez votre processus d'analyse avec des analyses planifiées. Configurez des analyses récurrentes pour surveiller en permanence vos sources de données et garantir une conformité continue." + }, + "matchers": { + "title_h3": "Matcheurs configurables", + "description": "Configurez les PII, PCI DSS et autres comparateurs pour répondre à vos exigences de conformité spécifiques. Activez ou désactivez les modèles de détection en fonction de vos besoins." + }, + "formats": { + "title_h3": "Plusieurs formats de fichiers", + "description": "Configurez les formats de fichiers (pdf, excel, etc.) à numériser. Prise en charge de MS Office, Open Office, Adobe PDF, des archives et des fichiers texte brut. Personnalisez les formats à inclure dans vos numérisations." + } + }, + "common": { + "search": "Recherche" + } + } + }, + "languages": { + "en": { + "name": "English", + "flag": "flag-us.svg", + "locale": "en_US" + }, + "ru": { + "name": "Русский", + "flag": "flag-ru.svg", + "locale": "ru_RU" + }, + "es": { + "name": "Español", + "flag": "flag-es.svg", + "locale": "es_ES" + }, + "de": { + "name": "Deutsch", + "flag": "flag-de.svg", + "locale": "de_DE" + }, + "fr": { + "name": "Français", + "flag": "flag-fr.svg", + "locale": "fr_FR" + } + }, + "pages": { + "index": { + "en": { + "path": "/", + "title_key": "site.title", + "title": "Angry Data Scanner - Free Sensitive Data Discovery Tool", + "description": "Advanced sensitive data discovery tool combining personal data discovery, payment card discovery, and passwords finder in one solution. Comprehensive data detector for compliance and security.", + "keywords": "data scanner, PII discovery, sensitive data, PCI DSS, data protection, free data scanner" + }, + "ru": { + "path": "/ru/", + "title_key": "site.title", + "title": "Angry Data Scanner - Программа поиска конфиденциальных данных для Mac, Windows и Linux", + "description": "Инструмент поиска данных. С его помощью можно найти персональные данные, данные банковских карт и другие конфиденциальные данные в папках, S3, базах данных, веб-страницах.", + "keywords": "сканер данных, поиск PII, конфиденциальные данные, PCI DSS, защита данных, бесплатный сканер данных" + }, + "es": { + "path": "/es/", + "title_key": "site.title", + "title": "Angry Data Scanner: herramienta gratuita de descubrimiento de datos confidenciales", + "description": "Descubra PII, PCI DSS y datos confidenciales en carpetas, S3 y bases de datos. Herramienta gratuita de código abierto para Windows, Linux, macOS. No se requieren derechos de administrador.", + "keywords": "escáner datos, descubrimiento PII, datos sensibles, PCI DSS, protección datos, escáner datos gratuito" + }, + "de": { + "path": "/de/", + "title_key": "site.title", + "title": "Angry Data Scanner – kostenloses Tool zur Erkennung sensibler Daten", + "description": "Entdecken Sie PII, PCI DSS und vertrauliche Daten in Ordnern, S3 und Datenbanken. Kostenloses Open-Source-Tool für Windows, Linux, macOS. Keine Administratorrechte erforderlich.", + "keywords": "Daten-Scanner, PII-Erkennung, sensible Daten, PCI DSS, Datenschutz, kostenloser Daten-Scanner" + }, + "fr": { + "path": "/fr/", + "title_key": "site.title", + "title": "Angry Data Scanner - Outil gratuit de découverte de données sensibles", + "description": "Découvrez les informations PII, PCI DSS et les données sensibles dans les dossiers, S3 et bases de données. Outil open source gratuit pour Windows, Linux, macOS. Aucun droit d'administrateur requis.", + "keywords": "scanner données, découverte PII, données sensibles, PCI DSS, protection données, scanner données gratuit" + } + }, + "discovery": { + "en": { + "path": "/discovery", + "title_key": "sections.discovery.title_h1", + "title": "Free PII & PCI DSS Data Discovery Tool | Angry Data Scanner", + "description": "Discover PII, credit cards, banking data, and crypto wallets. Free open-source scanner for GDPR, PCI DSS, and HIPAA compliance. No admin rights required.", + "keywords": "PII discovery, PCI DSS scanner, sensitive data discovery, credit card scanner, banking data finder, cryptocurrency wallet detection, GDPR compliance tool, HIPAA data scanner, free data discovery, open source data scanner, personal information finder" + }, + "ru": { + "path": "/ru/discovery", + "title_key": "sections.discovery.title_h1", + "title": "Поиск персональных данных | Angry Data Scanner", + "description": "Обнаружение персональных данных, номеров карт и банковской информации. Бесплатный сканер для GDPR, PCI DSS и HIPAA. Работает без прав администратора.", + "keywords": "поиск PII, сканер PCI DSS, обнаружение конфиденциальных данных, поиск номеров карт, поиск банковских данных, обнаружение криптокошельков, инструмент соответствия GDPR, сканер данных HIPAA, бесплатный поиск данных, поиск персональных данных" + }, + "es": { + "path": "/es/discovery", + "title_key": "sections.discovery.title_h1", + "title": "Escáner Gratis de Datos PII y PCI DSS | Angry Data Scanner", + "description": "Descubra PII, tarjetas de crédito, datos bancarios y billeteras cripto. Escáner gratuito open source para cumplimiento GDPR, PCI DSS e HIPAA. Sin derechos de admin.", + "keywords": "descubrimiento PII, escáner PCI DSS, descubrimiento de datos sensibles, escáner de tarjetas de crédito, buscador de datos bancarios, detección de billeteras criptográficas, herramienta cumplimiento GDPR, escáner datos HIPAA, descubrimiento datos gratuito" + }, + "de": { + "path": "/de/discovery", + "title_key": "sections.discovery.title_h1", + "title": "Kostenloses PII & PCI DSS Datenerkennungs-Tool | Angry Data Scanner", + "description": "Erkennen Sie PII, Kreditkarten, Bankdaten und Krypto-Wallets. Kostenloses Open-Source-Tool für GDPR-, PCI DSS- und HIPAA-Compliance. Keine Admin-Rechte nötig.", + "keywords": "PII-Erkennung, PCI DSS-Scanner, Erkennung sensibler Daten, Kreditkarten-Scanner, Bankdaten-Finder, Kryptowährungs-Wallet-Erkennung, GDPR-Compliance-Tool, HIPAA-Daten-Scanner, kostenlose Datenerkennung" + }, + "fr": { + "path": "/fr/discovery", + "title_key": "sections.discovery.title_h1", + "title": "Scanner Gratuit de Données PII & PCI DSS | Angry Data Scanner", + "description": "Découvrez PII, cartes de crédit, données bancaires et portefeuilles crypto. Scanner gratuit open source pour conformité RGPD, PCI DSS et HIPAA. Sans droits admin.", + "keywords": "découverte PII, scanner PCI DSS, découverte données sensibles, scanner cartes de crédit, détecteur données bancaires, détection portefeuilles cryptographiques, outil conformité RGPD, scanner données HIPAA, découverte données gratuite" + } + }, + "features": { + "en": { + "path": "/features", + "title_key": "sections.features.title_h1", + "title": "Data Scanner Features: Ranking & CSV Export | Angry Data Scanner", + "description": "Features: intelligent file ranking, scan history, CSV export, scheduled scans, customizable matchers, and 15+ file formats. Perfect for compliance audits.", + "keywords": "data scanner features, file ranking system, scan history tracking, CSV export data, scheduled data scans, configurable matchers, file format support, PDF scanner, Excel data scanner, automated scanning, data discovery tools" + }, + "ru": { + "path": "/ru/features", + "title_key": "sections.features.title_h1", + "title": "Возможности сканера: ранжирование и экспорт CSV | Angry Data Scanner", + "description": "Функции: интеллектуальное ранжирование, история сканирований, экспорт CSV, запланированные сканирования, настраиваемые матчеры и 15+ форматов. Идеально для аудитов.", + "keywords": "возможности сканера данных, система ранжирования файлов, история сканирований, экспорт CSV, запланированные сканирования, настраиваемые матчеры, поддержка форматов, сканер PDF, сканер Excel, автоматизированное сканирование" + }, + "es": { + "path": "/es/features", + "title_key": "sections.features.title_h1", + "title": "Características del Escáner: Clasificación y Exportación CSV | Angry Data Scanner", + "description": "Funciones potentes: clasificación inteligente, historial de escaneos, exportación CSV, escaneos programados, comparadores configurables y 15+ formatos. Ideal para auditorías.", + "keywords": "características escáner datos, sistema clasificación archivos, historial escaneos, exportación CSV, escaneos programados, comparadores configurables, soporte formatos, escáner PDF, escáner Excel, escaneo automatizado" + }, + "de": { + "path": "/de/features", + "title_key": "sections.features.title_h1", + "title": "Daten-Scanner Funktionen: Ranking & CSV-Export | Angry Data Scanner", + "description": "Leistungsstarke Funktionen: intelligentes Datei-Ranking, Scanverlauf, CSV-Export, geplante Scans, konfigurierbare Matcher und 15+ Dateiformate. Perfekt für Compliance-Prüfungen.", + "keywords": "Daten-Scanner-Funktionen, Datei-Ranking-System, Scanverlauf-Verfolgung, CSV-Export, geplante Scans, konfigurierbare Matcher, Dateiformat-Unterstützung, PDF-Scanner, Excel-Daten-Scanner, automatisierte Scans" + }, + "fr": { + "path": "/fr/features", + "title_key": "sections.features.title_h1", + "title": "Fonctionnalités Scanner: Classement & Export CSV | Angry Data Scanner", + "description": "Fonctions puissantes: classement intelligent, historique des analyses, export CSV, analyses planifiées, matcheurs configurables et 15+ formats. Parfait pour audits de conformité.", + "keywords": "fonctionnalités scanner données, système classement fichiers, historique analyses, export CSV, analyses planifiées, matcheurs configurables, prise en charge formats, scanner PDF, scanner Excel, analyse automatisée" + } + }, + "use-cases": { + "en": { + "path": "/use-cases", + "title_key": "sections.use_cases.title_h1", + "title": "Data Scanner Use Cases: Security & Compliance | Angry Data Scanner", + "description": "How security teams, compliance officers, developers, and forensics experts use our free scanner for GDPR, PCI DSS compliance, data leak prevention, and investigations.", + "keywords": "data scanner use cases, security team tools, GDPR compliance scanner, PCI DSS compliance tool, data leak prevention, source code scanner, digital forensics tools, HIPAA compliance scanner, data protection tools, security audit software" + }, + "ru": { + "path": "/ru/use-cases", + "title_key": "sections.use_cases.title_h1", + "title": "Применение сканера: безопасность и соответствие | Angry Data Scanner", + "description": "Как команды безопасности, специалисты по соответствию, разработчики и эксперты используют наш бесплатный сканер для GDPR, PCI DSS, предотвращения утечек и расследований.", + "keywords": "применение сканера данных, инструменты команды безопасности, сканер соответствия GDPR, инструмент соответствия PCI DSS, предотвращение утечек данных, сканер исходного кода, инструменты цифровой криминалистики" + }, + "es": { + "path": "/es/use-cases", + "title_key": "sections.use_cases.title_h1", + "title": "Casos de Uso: Seguridad y Cumplimiento | Angry Data Scanner", + "description": "Descubra cómo equipos de seguridad, oficiales de cumplimiento, desarrolladores y expertos usan nuestro escáner gratuito para GDPR, PCI DSS, prevención de fugas e investigaciones.", + "keywords": "casos uso escáner datos, herramientas equipos seguridad, escáner cumplimiento GDPR, herramienta cumplimiento PCI DSS, prevención fugas datos, escáner código fuente, herramientas forense digital" + }, + "de": { + "path": "/de/use-cases", + "title_key": "sections.use_cases.title_h1", + "title": "Anwendungsfälle: Sicherheit & Compliance | Angry Data Scanner", + "description": "Erfahren Sie, wie Sicherheitsteams, Compliance-Beauftragte, Entwickler und Forensik-Experten unseren kostenlosen Scanner für GDPR, PCI DSS, Datenleck-Prävention und Ermittlungen nutzen.", + "keywords": "Daten-Scanner-Anwendungsfälle, Sicherheitsteam-Tools, GDPR-Compliance-Scanner, PCI DSS-Compliance-Tool, Datenleck-Prävention, Quellcode-Scanner, digitale Forensik-Tools" + }, + "fr": { + "path": "/fr/use-cases", + "title_key": "sections.use_cases.title_h1", + "title": "Cas d'Utilisation: Sécurité & Conformité | Angry Data Scanner", + "description": "Découvrez comment les équipes de sécurité, agents de conformité, développeurs et experts utilisent notre scanner gratuit pour RGPD, PCI DSS, prévention des fuites et enquêtes.", + "keywords": "cas utilisation scanner données, outils équipes sécurité, scanner conformité RGPD, outil conformité PCI DSS, prévention fuites données, scanner code source, outils forensique numérique" + } + }, + "download": { + "en": { + "path": "/download", + "title_key": "sections.download.title_h1", + "title": "Download Free Data Scanner: Windows, Linux & macOS | Angry Data Scanner", + "description": "Download free Angry Data Scanner for Windows, Linux, and macOS. No admin rights required. Portable version available. Start scanning sensitive data instantly.", + "keywords": "download data scanner free, Windows data scanner download, Linux data scanner, macOS data scanner download, portable data scanner, free PII scanner download, no admin required scanner, open source data scanner download" + }, + "ru": { + "path": "/ru/download", + "title_key": "sections.download.title_h1", + "title": "Скачать бесплатный сканер данных: Windows, Linux, macOS | Angry Data Scanner", + "description": "Скачайте бесплатный Angry Data Scanner для Windows, Linux и macOS. Без прав администратора. Портативная версия доступна. Начните сканирование конфиденциальных данных мгновенно.", + "keywords": "скачать сканер данных бесплатно, скачать сканер данных Windows, сканер данных Linux, скачать сканер данных macOS, портативный сканер данных, скачать бесплатный сканер PII, сканер без прав администратора" + }, + "es": { + "path": "/es/download", + "title_key": "sections.download.title_h1", + "title": "Descargar Escáner Gratis: Windows, Linux y macOS | Angry Data Scanner", + "description": "Descargue Angry Data Scanner gratis para Windows, Linux y macOS. Sin derechos de admin. Versión portátil disponible. 400 MB disco, 4 GB RAM. Comience a escanear datos sensibles ya.", + "keywords": "descargar escáner datos gratis, descargar escáner datos Windows, escáner datos Linux, descargar escáner datos macOS, escáner datos portátil, descargar escáner PII gratis, escáner sin derechos admin" + }, + "de": { + "path": "/de/download", + "title_key": "sections.download.title_h1", + "title": "Kostenlosen Daten-Scanner Download: Windows, Linux & macOS | Angry Data Scanner", + "description": "Laden Sie Angry Data Scanner kostenlos für Windows, Linux und macOS herunter. Keine Admin-Rechte nötig. Portable Version verfügbar. 400 MB Speicher, 4 GB RAM. Scannen Sie sofort.", + "keywords": "Daten-Scanner kostenlos downloaden, Windows Daten-Scanner Download, Linux Daten-Scanner, macOS Daten-Scanner Download, portable Daten-Scanner, kostenloser PII-Scanner Download" + }, + "fr": { + "path": "/fr/download", + "title_key": "sections.download.title_h1", + "title": "Télécharger Scanner Gratuit: Windows, Linux et macOS | Angry Data Scanner", + "description": "Téléchargez Angry Data Scanner gratuit pour Windows, Linux et macOS. Sans droits d'admin. Version portable disponible. 400 Mo disque, 4 Go RAM. Scannez des données sensibles instantanément.", + "keywords": "télécharger scanner données gratuit, télécharger scanner données Windows, scanner données Linux, télécharger scanner données macOS, scanner données portable, télécharger scanner PII gratuit" + } + } + } +} \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..fc19a96 --- /dev/null +++ b/config.py @@ -0,0 +1,196 @@ +""" +Единый конфиг со всеми данными и переводами для сайта Angry Data Scanner +Загружается из config.json +""" + +import json +from pathlib import Path +from typing import Dict, Any, Optional + +# Путь к JSON конфигу +CONFIG_FILE = Path(__file__).parent / 'config.json' + +# Загрузить конфиг из JSON +def _load_config() -> Dict[str, Any]: + """Загрузить конфиг из JSON файла""" + if CONFIG_FILE.exists(): + with open(CONFIG_FILE, 'r', encoding='utf-8') as f: + return json.load(f) + else: + # Fallback на пустой конфиг + return { + 'site_config': {}, + 'data': {}, + 'translations': {}, + 'languages': {}, + 'pages': {} + } + +# Загрузить данные +_config_data = _load_config() + +# Экспортировать данные как константы +SITE_CONFIG = _config_data.get('site_config', {}) +DATA = _config_data.get('data', {}) +TRANSLATIONS = _config_data.get('translations', {}) +LANGUAGES = _config_data.get('languages', {}) +PAGES = _config_data.get('pages', {}) + + +def _camel_to_snake(name: str) -> str: + """Конвертировать camelCase в snake_case""" + import re + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + +def _snake_to_camel(name: str) -> str: + """Конвертировать snake_case в camelCase""" + components = name.split('_') + return components[0] + ''.join(x.capitalize() for x in components[1:]) + +def get_translation(lang: str, key_path: str, default: Optional[str] = None) -> Any: + """Получить перевод по пути ключа (например, 'nav.home' -> 'nav': {'home': ...}) + Поддерживает как snake_case, так и camelCase ключи в JSON""" + keys = key_path.split('.') + translations = TRANSLATIONS.get(lang, TRANSLATIONS.get('en', {})) + + value = translations + for key in keys: + if isinstance(value, dict): + # Сначала пробуем ключ как есть + if key in value: + value = value[key] + else: + # Если не нашли, пробуем конвертировать snake_case -> camelCase + camel_key = _snake_to_camel(key) + if camel_key in value: + value = value[camel_key] + else: + # Или наоборот camelCase -> snake_case + snake_key = _camel_to_snake(key) + if snake_key in value: + value = value[snake_key] + else: + return default if default is not None else key_path + else: + return default if default is not None else key_path + return value + + +def get_page_url(page_name: str, lang: str) -> str: + """Получить URL страницы для языка (всегда со слэшем в конце, кроме корня /)""" + if page_name in PAGES and lang in PAGES[page_name]: + path = PAGES[page_name][lang]['path'] + if path != '/' and not path.endswith('/'): + return path + '/' + return path + return '/' + + +def get_page_meta(page_name: str, lang: str) -> Dict[str, str]: + """Получить мета-данные страницы (title, description, keywords)""" + # Для всех страниц берем из pages[page_name][lang] + page_config = PAGES.get(page_name, {}) + lang_config = page_config.get(lang, page_config.get('en', {})) + + # Если есть явные title, description, keywords - используем их + if 'title' in lang_config or 'description' in lang_config or 'keywords' in lang_config: + return { + 'title': lang_config.get('title', get_translation(lang, lang_config.get('title_key', 'site.title'))), + 'description': lang_config.get('description', get_translation(lang, 'site.description')), + 'keywords': lang_config.get('keywords', 'data scanner, PII discovery, sensitive data, PCI DSS, data protection, free data scanner') + } + + # Fallback - используем title_key и site.description + title_key = lang_config.get('title_key', 'site.title') + return { + 'title': get_translation(lang, title_key), + 'description': get_translation(lang, 'site.description'), + 'keywords': 'data scanner, PII discovery, sensitive data, PCI DSS, data protection, free data scanner' + } + + +def _convert_keys_to_camel_case(obj: Any) -> Any: + """ + Рекурсивно конвертирует все ключи словаря из snake_case в camelCase + """ + if isinstance(obj, dict): + converted = {} + for key, value in obj.items(): + # Конвертировать ключ из snake_case в camelCase + camel_key = _snake_to_camel(key) + # Рекурсивно конвертировать значение + converted[camel_key] = _convert_keys_to_camel_case(value) + return converted + elif isinstance(obj, list): + # Для списков рекурсивно конвертировать каждый элемент + return [_convert_keys_to_camel_case(item) for item in obj] + else: + # Для примитивных типов возвращать как есть + return obj + + +def get_i18n_for_js() -> Dict[str, Any]: + """ + Получить переводы в формате для JavaScript (window.I18N) + Конвертирует ключи из snake_case (config.json) в camelCase (как в старом i18n.js) + для совместимости с существующим JavaScript кодом + """ + import copy + result = {} + + # Пройтись по всем языкам и конвертировать ключи + for lang_code, translations in TRANSLATIONS.items(): + result[lang_code] = _convert_keys_to_camel_case(copy.deepcopy(translations)) + + return result + + +def get_config_for_js() -> Dict[str, Any]: + """ + Получить конфигурацию в формате для JavaScript (window.CONFIG) + Конвертирует данные из config.json (snake_case) в camelCase для совместимости + Рекурсивно конвертирует все ключи, включая вложенные объекты в массивах + """ + import copy + + # Копируем данные из config.json + config_data = copy.deepcopy(DATA) + + # Рекурсивно конвертируем все ключи из snake_case в camelCase + converted_data = _convert_keys_to_camel_case(config_data) + + # Создаем результат с правильными ключами верхнего уровня + result = {} + + # Маппинг ключей верхнего уровня (они уже конвертированы, но нужно убедиться в правильном именовании) + key_mapping = { + 'personalDataNumbers': 'personalDataNumbers', + 'personalDataText': 'personalDataText', + 'pciDss': 'pciDss', + 'bankingSecrecy': 'bankingSecrecy', + 'itAssets': 'itAssets', + 'customSignatures': 'customSignatures', + 'fileTypes': 'fileTypes', + 'dataSources': 'dataSources', + 'useCases': 'useCases', + 'downloads': 'downloads', + 'features': 'features', + 'systemRequirements': 'systemRequirements', + 'crypto': 'crypto' + } + + # Переносим конвертированные данные + for old_key, new_key in key_mapping.items(): + if old_key in converted_data: + result[new_key] = converted_data[old_key] + + # Добавляем site metadata из site_config + result['site'] = { + 'title': 'Angry Data Scanner - Sensitive Data Discovery Tool', + 'description': 'Free open source sensitive data discovery tool', + 'githubUrl': SITE_CONFIG.get('github_url', ''), + 'email': SITE_CONFIG.get('email', '') + } + + return result diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index 77dbf77..0000000 --- a/docs/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -angrydata-core/ -*.md -.pages -download/ -assets/menu-translations.json -BingSiteAuth.xml -robots.txt -favicon.ico -/*.png -/*.html \ No newline at end of file diff --git a/docs/assets/images/apple-touch-icon.png b/docs/assets/images/apple-touch-icon.png deleted file mode 100644 index 60bbc64..0000000 Binary files a/docs/assets/images/apple-touch-icon.png and /dev/null differ diff --git a/docs/assets/images/favicon-96x96.png b/docs/assets/images/favicon-96x96.png deleted file mode 100644 index 0915800..0000000 Binary files a/docs/assets/images/favicon-96x96.png and /dev/null differ diff --git a/docs/assets/images/favicon.ico b/docs/assets/images/favicon.ico deleted file mode 100644 index 0d95364..0000000 Binary files a/docs/assets/images/favicon.ico and /dev/null differ diff --git a/docs/assets/images/favicon.svg b/docs/assets/images/favicon.svg deleted file mode 100644 index 2b324a6..0000000 --- a/docs/assets/images/favicon.svg +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/docs/assets/images/logo.png b/docs/assets/images/logo.png deleted file mode 100644 index 6e2c02a..0000000 Binary files a/docs/assets/images/logo.png and /dev/null differ diff --git a/docs/assets/images/og_image.png b/docs/assets/images/og_image.png deleted file mode 100644 index 096860f..0000000 Binary files a/docs/assets/images/og_image.png and /dev/null differ diff --git a/docs/assets/images/screenshot.png b/docs/assets/images/screenshot.png deleted file mode 100644 index 93d7db0..0000000 Binary files a/docs/assets/images/screenshot.png and /dev/null differ diff --git a/docs/assets/images/screenshot_2.png b/docs/assets/images/screenshot_2.png deleted file mode 100644 index 8b9c2f9..0000000 Binary files a/docs/assets/images/screenshot_2.png and /dev/null differ diff --git a/docs/assets/images/screenshot_2_thumb.png b/docs/assets/images/screenshot_2_thumb.png deleted file mode 100644 index 097c45e..0000000 Binary files a/docs/assets/images/screenshot_2_thumb.png and /dev/null differ diff --git a/docs/assets/images/screenshot_3.png b/docs/assets/images/screenshot_3.png deleted file mode 100644 index 3dd7e40..0000000 Binary files a/docs/assets/images/screenshot_3.png and /dev/null differ diff --git a/docs/assets/images/screenshot_3_thumb.png b/docs/assets/images/screenshot_3_thumb.png deleted file mode 100644 index 0a23da7..0000000 Binary files a/docs/assets/images/screenshot_3_thumb.png and /dev/null differ diff --git a/docs/assets/images/screenshot_4.png b/docs/assets/images/screenshot_4.png deleted file mode 100644 index 0c128cf..0000000 Binary files a/docs/assets/images/screenshot_4.png and /dev/null differ diff --git a/docs/assets/images/screenshot_4_thumb.png b/docs/assets/images/screenshot_4_thumb.png deleted file mode 100644 index d0ed290..0000000 Binary files a/docs/assets/images/screenshot_4_thumb.png and /dev/null differ diff --git a/docs/assets/images/screenshot_thumb.png b/docs/assets/images/screenshot_thumb.png deleted file mode 100644 index df72472..0000000 Binary files a/docs/assets/images/screenshot_thumb.png and /dev/null differ diff --git a/docs/assets/images/site.webmanifest b/docs/assets/images/site.webmanifest deleted file mode 100644 index 2287ef0..0000000 --- a/docs/assets/images/site.webmanifest +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "MyWebSite", - "short_name": "MySite", - "icons": [ - { - "src": "/assets/images/web-app-manifest-192x192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "/assets/images/web-app-manifest-512x512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} \ No newline at end of file diff --git a/docs/assets/images/web-app-manifest-192x192.png b/docs/assets/images/web-app-manifest-192x192.png deleted file mode 100644 index e76ecd8..0000000 Binary files a/docs/assets/images/web-app-manifest-192x192.png and /dev/null differ diff --git a/docs/assets/images/web-app-manifest-512x512.png b/docs/assets/images/web-app-manifest-512x512.png deleted file mode 100644 index 7b00018..0000000 Binary files a/docs/assets/images/web-app-manifest-512x512.png and /dev/null differ diff --git a/docs/assets/javascripts/download-button.js b/docs/assets/javascripts/download-button.js deleted file mode 100644 index 1bb1e5e..0000000 --- a/docs/assets/javascripts/download-button.js +++ /dev/null @@ -1,177 +0,0 @@ -/** - * Download Button Functionality - * Creates download button after screenshots gallery and scrolls to Download section on click - * Supports multiple languages: Download, Скачать, Descargar, Herunterladen, etc. - */ - -(function() { - 'use strict'; - - /** - * Create and insert download button after screenshots gallery - */ - function createDownloadButton() { - // Check if button already exists - if (document.getElementById('download-button')) { - return; - } - - // Find screenshots gallery or lightbox overlay - const gallery = document.querySelector('.screenshots-gallery'); - const lightbox = document.getElementById('screenshot-lightbox'); - - // Determine insertion point: after lightbox if exists, otherwise after gallery - let insertAfter = null; - if (lightbox && lightbox.parentNode) { - insertAfter = lightbox; - } else if (gallery && gallery.parentNode) { - insertAfter = gallery; - } - - if (!insertAfter) { - return; // No gallery found, skip button creation - } - - // Create button container - const container = document.createElement('div'); - container.className = 'download-button-container'; - - // Create button - const button = document.createElement('button'); - button.id = 'download-button'; - button.className = 'download-button'; - button.onclick = scrollToDownload; - - // Create button text span - const textSpan = document.createElement('span'); - textSpan.className = 'download-button-text'; - textSpan.textContent = 'Download'; - - // Create button icon span - const iconSpan = document.createElement('span'); - iconSpan.className = 'download-button-icon'; - iconSpan.textContent = '↓'; - - // Assemble button - button.appendChild(textSpan); - button.appendChild(iconSpan); - container.appendChild(button); - - // Insert after gallery/lightbox - if (insertAfter.nextSibling) { - insertAfter.parentNode.insertBefore(container, insertAfter.nextSibling); - } else { - insertAfter.parentNode.appendChild(container); - } - } - - /** - * Scroll to Download section - * Supports multiple languages: Download, Скачать, Descargar, Herunterladen, etc. - */ - function scrollToDownload() { - // Possible download section headings in different languages - const downloadHeadings = [ - 'Download', - 'Скачать', - 'Descargar', - 'Herunterladen', - 'Télécharger', - 'Scarica', - 'ダウンロード', - '下载', - '다운로드' - ]; - - // Find all h2 headings - const headings = document.querySelectorAll('h2'); - - let targetHeading = null; - - // Search for download section heading - for (const heading of headings) { - const headingText = heading.textContent.trim(); - // Check if heading contains any of the download keywords - for (const keyword of downloadHeadings) { - if (headingText.toLowerCase().includes(keyword.toLowerCase())) { - targetHeading = heading; - break; - } - } - if (targetHeading) { - break; - } - } - - if (targetHeading) { - // Scroll to the heading with smooth behavior - targetHeading.scrollIntoView({ - behavior: 'smooth', - block: 'start' - }); - - // Add a highlight effect - targetHeading.style.transition = 'background-color 0.3s ease'; - const originalBg = targetHeading.style.backgroundColor; - targetHeading.style.backgroundColor = 'var(--md-primary-fg-color--lightest, rgba(64, 81, 181, 0.1))'; - - setTimeout(() => { - targetHeading.style.backgroundColor = originalBg; - setTimeout(() => { - targetHeading.style.transition = ''; - }, 300); - }, 2000); - } else { - // Fallback: try to find by ID or anchor - const downloadAnchor = document.querySelector('#download, [id*="download"], [id*="скачать"]'); - if (downloadAnchor) { - downloadAnchor.scrollIntoView({ - behavior: 'smooth', - block: 'start' - }); - } - } - } - - // Make function available globally - window.scrollToDownload = scrollToDownload; - - /** - * Initialize download button when DOM is ready - * Tries multiple times to find gallery in case it's added dynamically - */ - function initDownloadButton() { - let attempts = 0; - const maxAttempts = 10; // Try for up to 1 second (10 * 100ms) - - function tryCreateButton() { - attempts++; - - // Check if gallery exists - const gallery = document.querySelector('.screenshots-gallery'); - const lightbox = document.getElementById('screenshot-lightbox'); - - if (gallery || lightbox) { - // Gallery found, create button - createDownloadButton(); - } else if (attempts < maxAttempts) { - // Gallery not found yet, try again - setTimeout(tryCreateButton, 100); - } - // If max attempts reached and no gallery found, give up silently - } - - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', function() { - tryCreateButton(); - }); - } else { - // DOM already loaded - tryCreateButton(); - } - } - - // Initialize - initDownloadButton(); -})(); - diff --git a/docs/assets/javascripts/language-selector.js b/docs/assets/javascripts/language-selector.js deleted file mode 100644 index c6c5612..0000000 --- a/docs/assets/javascripts/language-selector.js +++ /dev/null @@ -1,176 +0,0 @@ -// Language selector with flag display -// Version: 1.1 - Replace language button with current language flag -// Compatible with MkDocs Material theme - -(function() { - 'use strict'; - - // Определяем текущий язык по URL - function getCurrentLanguage() { - const path = window.location.pathname; - if (path.startsWith('/ru/')) return 'ru'; - if (path.startsWith('/es/')) return 'es'; - if (path.startsWith('/de/')) return 'de'; - if (path.startsWith('/fr/')) return 'fr'; - return 'en'; // по умолчанию английский - } - - // Маппинг языков на флаги (используем более высокое разрешение для лучшего качества) - const languageFlags = { - 'en': { - flag: 'https://flagcdn.com/w40/us.png', - name: 'English' - }, - 'ru': { - flag: 'https://flagcdn.com/w40/ru.png', - name: 'Русский' - }, - 'es': { - flag: 'https://flagcdn.com/w40/es.png', - name: 'Español' - }, - 'de': { - flag: 'https://flagcdn.com/w40/de.png', - name: 'Deutsch' - }, - 'fr': { - flag: 'https://flagcdn.com/w40/fr.png', - name: 'Français' - } - }; - - let isReplaced = false; - - // Функция для замены кнопки языка на флаг - function replaceLanguageButton() { - if (isReplaced) return; - - const languageButton = document.querySelector('.md-header__option .md-select button'); - if (!languageButton) { - console.log('Language button not found, trying again...'); - return; - } - - console.log('Found language button, replacing with flag...'); - - const currentLang = getCurrentLanguage(); - const currentLangData = languageFlags[currentLang]; - - if (!currentLangData) { - console.warn('Language data not found for:', currentLang); - return; - } - - // Создаем новый элемент с флагом - const flagElement = document.createElement('div'); - flagElement.className = 'md-header__button md-icon language-flag-button'; - flagElement.setAttribute('aria-label', `Select language - Current: ${currentLangData.name}`); - flagElement.setAttribute('title', `Current language: ${currentLangData.name}`); - - // Применяем стили через CSS классы - flagElement.style.cssText = ` - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 21px; - border-radius: 4px; - background-size: cover; - background-repeat: no-repeat; - background-position: center; - background-image: url('${currentLangData.flag}'); - border: 1px solid rgba(0, 0, 0, 0.1); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - transition: opacity 0.4s ease, transform 0.2s ease, box-shadow 0.2s ease; - cursor: pointer; - aspect-ratio: 3/2; - visibility: visible !important; - opacity: 0; - `; - - // Добавляем hover эффект - flagElement.addEventListener('mouseenter', function() { - this.style.transform = 'scale(1.05)'; - this.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.15)'; - }); - - flagElement.addEventListener('mouseleave', function() { - this.style.transform = 'scale(1)'; - this.style.boxShadow = '0 1px 3px rgba(0, 0, 0, 0.1)'; - }); - - // Сохраняем функциональность выпадающего меню - const selectInner = languageButton.parentNode.querySelector('.md-select__inner'); - if (selectInner) { - // Показываем/скрываем меню при клике на флаг - flagElement.addEventListener('click', function(e) { - e.stopPropagation(); - e.preventDefault(); - const isVisible = selectInner.style.display !== 'none' && selectInner.style.display !== ''; - if (isVisible) { - selectInner.style.display = 'none'; - } else { - selectInner.style.display = 'block'; - } - }); - - // Скрываем меню при клике вне его - document.addEventListener('click', function(e) { - if (!languageButton.parentNode.contains(e.target)) { - selectInner.style.display = 'none'; - } - }); - } - - // Заменяем кнопку - languageButton.parentNode.replaceChild(flagElement, languageButton); - isReplaced = true; - - // Плавное появление флага - setTimeout(function() { - flagElement.style.opacity = '1'; - }, 10); - - console.log('Language button replaced with flag for:', currentLang); - } - - // Функция инициализации - function init() { - // Пробуем сразу - replaceLanguageButton(); - - // Если не получилось, ждем загрузки DOM - if (!isReplaced) { - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', replaceLanguageButton); - } else { - // DOM уже загружен - setTimeout(replaceLanguageButton, 100); - } - } - } - - // Запускаем инициализацию - init(); - - // Также пробуем при изменении DOM (на случай динамической загрузки) - const observer = new MutationObserver(function(mutations) { - if (isReplaced) return; - - mutations.forEach(function(mutation) { - if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { - setTimeout(replaceLanguageButton, 50); - } - }); - }); - - observer.observe(document.body, { - childList: true, - subtree: true - }); - - // Очищаем observer через 10 секунд - setTimeout(function() { - observer.disconnect(); - }, 10000); -})(); diff --git a/docs/assets/javascripts/screenshots.js b/docs/assets/javascripts/screenshots.js deleted file mode 100644 index 7fcc745..0000000 --- a/docs/assets/javascripts/screenshots.js +++ /dev/null @@ -1,256 +0,0 @@ -/** - * Screenshots Gallery Lightbox - * Handles opening/closing lightbox and navigation between images - */ - -(function() { - 'use strict'; - - let currentImageIndex = 0; - let images = []; - let lightbox = null; - let lightboxImage = null; - let lightboxClose = null; - let lightboxPrev = null; - let lightboxNext = null; - - /** - * Initialize the lightbox functionality - */ - function initLightbox() { - // Get all screenshot links - const screenshotLinks = document.querySelectorAll('.screenshot-link'); - - if (screenshotLinks.length === 0) { - return; - } - - // Collect all image URLs - images = Array.from(screenshotLinks).map(link => { - const fullImage = link.getAttribute('data-full') || link.getAttribute('href'); - return fullImage; - }); - - // Get lightbox elements - lightbox = document.getElementById('screenshot-lightbox'); - lightboxImage = document.getElementById('lightbox-image'); - lightboxClose = document.querySelector('.lightbox-close'); - lightboxPrev = document.querySelector('.lightbox-prev'); - lightboxNext = document.querySelector('.lightbox-next'); - - if (!lightbox || !lightboxImage) { - return; - } - - // Add click handlers to screenshot links - screenshotLinks.forEach((link, index) => { - link.addEventListener('click', function(e) { - e.preventDefault(); - openLightbox(index); - }); - }); - - // Close lightbox handlers - if (lightboxClose) { - lightboxClose.addEventListener('click', closeLightbox); - } - - // Navigation handlers - if (lightboxPrev) { - lightboxPrev.addEventListener('click', function(e) { - e.stopPropagation(); - showPreviousImage(); - }); - } - - if (lightboxNext) { - lightboxNext.addEventListener('click', function(e) { - e.stopPropagation(); - showNextImage(); - }); - } - - // Close on overlay click - lightbox.addEventListener('click', function(e) { - if (e.target === lightbox) { - closeLightbox(); - } - }); - - // Keyboard navigation - document.addEventListener('keydown', handleKeyPress); - } - - /** - * Open lightbox with specified image - */ - function openLightbox(index) { - if (index < 0 || index >= images.length) { - return; - } - - currentImageIndex = index; - updateLightboxImage(); - - if (lightbox) { - lightbox.classList.add('active'); - document.body.style.overflow = 'hidden'; // Prevent background scrolling - } - } - - /** - * Close lightbox - */ - function closeLightbox() { - if (lightbox) { - lightbox.classList.remove('active'); - document.body.style.overflow = ''; // Restore scrolling - } - } - - /** - * Show previous image - */ - function showPreviousImage() { - currentImageIndex = (currentImageIndex - 1 + images.length) % images.length; - updateLightboxImage(); - } - - /** - * Show next image - */ - function showNextImage() { - currentImageIndex = (currentImageIndex + 1) % images.length; - updateLightboxImage(); - } - - /** - * Update lightbox image source - */ - function updateLightboxImage() { - if (lightboxImage && images[currentImageIndex]) { - lightboxImage.src = images[currentImageIndex]; - lightboxImage.alt = `Screenshot ${currentImageIndex + 1}`; - } - - // Update navigation button visibility - if (lightboxPrev && lightboxNext) { - if (images.length <= 1) { - lightboxPrev.style.display = 'none'; - lightboxNext.style.display = 'none'; - } else { - lightboxPrev.style.display = 'block'; - lightboxNext.style.display = 'block'; - } - } - } - - /** - * Handle keyboard events - */ - function handleKeyPress(e) { - if (!lightbox || !lightbox.classList.contains('active')) { - return; - } - - switch(e.key) { - case 'Escape': - closeLightbox(); - break; - case 'ArrowLeft': - showPreviousImage(); - break; - case 'ArrowRight': - showNextImage(); - break; - } - } - - /** - * Handle touch events for mobile navigation - */ - function initTouchSupport() { - if (!lightbox) { - return; - } - - let touchStartX = 0; - let touchEndX = 0; - - lightbox.addEventListener('touchstart', function(e) { - touchStartX = e.changedTouches[0].screenX; - }, { passive: true }); - - lightbox.addEventListener('touchend', function(e) { - touchEndX = e.changedTouches[0].screenX; - handleSwipe(); - }, { passive: true }); - - function handleSwipe() { - const swipeThreshold = 50; - const diff = touchStartX - touchEndX; - - if (Math.abs(diff) > swipeThreshold) { - if (diff > 0) { - // Swipe left - next image - showNextImage(); - } else { - // Swipe right - previous image - showPreviousImage(); - } - } - } - } - - /** - * Update scroll indicators for gallery - */ - function updateScrollIndicators() { - const gallery = document.querySelector('.screenshots-gallery'); - if (!gallery) { - return; - } - - const scrollLeft = gallery.scrollLeft; - const scrollWidth = gallery.scrollWidth; - const clientWidth = gallery.clientWidth; - const isAtStart = scrollLeft <= 1; - const isAtEnd = scrollLeft + clientWidth >= scrollWidth - 1; - - gallery.classList.toggle('scrolled-start', isAtStart); - gallery.classList.toggle('scrolled-end', isAtEnd); - } - - /** - * Initialize scroll indicators for gallery - */ - function initScrollIndicators() { - const gallery = document.querySelector('.screenshots-gallery'); - if (!gallery) { - return; - } - - // Update on scroll - gallery.addEventListener('scroll', updateScrollIndicators, { passive: true }); - - // Update on resize - window.addEventListener('resize', updateScrollIndicators, { passive: true }); - - // Initial update - updateScrollIndicators(); - } - - // Initialize when DOM is ready - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', function() { - initLightbox(); - initTouchSupport(); - initScrollIndicators(); - }); - } else { - initLightbox(); - initTouchSupport(); - initScrollIndicators(); - } -})(); - diff --git a/docs/assets/javascripts/table-enhanced.js b/docs/assets/javascripts/table-enhanced.js deleted file mode 100644 index 1cba3c1..0000000 --- a/docs/assets/javascripts/table-enhanced.js +++ /dev/null @@ -1,572 +0,0 @@ -// Enhanced table functionality -// Combines country filtering and table collapse functionality -// Automatically collapses tables with more than 5 data rows and adds country filter for tables with Country column -// Compatible with MkDocs Material theme - -(function() { - 'use strict'; - - const COLLAPSED_ICON = '▼'; - const EXPANDED_ICON = '▲'; - const MAX_VISIBLE_ROWS = 5; - const GLOBE_ICON = '🌐'; - - // Language detection - function getCurrentLanguage() { - const path = window.location.pathname; - const langMap = { '/ru/': 'ru', '/es/': 'es', '/de/': 'de', '/fr/': 'fr' }; - return Object.entries(langMap).find(([prefix]) => path.startsWith(prefix))?.[1] || 'en'; - } - - // Configuration - const countryColumnNames = { - 'en': ['Country', 'country'], - 'ru': ['Страна', 'страна'], - 'es': ['País', 'país', 'Country', 'country'], - 'de': ['Land', 'land', 'Country', 'country'], - 'fr': ['Pays', 'pays', 'Country', 'country'] - }; - - const countryFlagMapping = { - 'RU': 'ru', 'US': 'us', 'CN': 'cn', 'GB': 'gb', 'DE': 'de', 'FR': 'fr', 'ES': 'es', - 'IT': 'it', 'JP': 'jp', 'KR': 'kr', 'IN': 'in', 'BR': 'br', 'CA': 'ca', 'AU': 'au', - 'MX': 'mx', 'International': 'un' - }; - - const translations = { - 'en': { allCountries: 'All' }, - 'ru': { allCountries: 'Все' }, - 'es': { allCountries: 'Todos' }, - 'de': { allCountries: 'Alle' }, - 'fr': { allCountries: 'Tous' } - }; - - // Utility functions - function getCountryFlagCode(country) { - return countryFlagMapping[country] || countryFlagMapping[country?.toUpperCase()] || null; - } - - function getFlagUrl(countryCode) { - return countryCode ? `https://flagcdn.com/w40/${countryCode.toLowerCase()}.png` : null; - } - - function getDataRows(table) { - const tbody = table.querySelector('tbody') || table; - return Array.from(tbody.querySelectorAll('tr')).filter( - row => !row.classList.contains('table-collapse-button-row') - ); - } - - function countDataRows(table) { - return getDataRows(table).length; - } - - function getColumnCount(table) { - const headerRow = table.querySelector('thead tr') || table.querySelector('tbody tr') || table.querySelector('tr'); - return headerRow?.querySelectorAll('th, td').length || 1; - } - - function findCountryColumnIndex(table) { - const headerRow = table.querySelector('thead tr'); - if (!headerRow) return -1; - - const headers = headerRow.querySelectorAll('th, td'); - const countryNames = countryColumnNames[getCurrentLanguage()] || countryColumnNames['en']; - - for (let i = 0; i < headers.length; i++) { - const headerText = headers[i].textContent.trim().toLowerCase(); - if (countryNames.some(name => headerText === name.toLowerCase())) { - return i; - } - } - return -1; - } - - function getCountriesFromTable(table, countryColumnIndex) { - const countries = new Set(); - getDataRows(table).forEach(row => { - const cells = row.querySelectorAll('td, th'); - const country = cells[countryColumnIndex]?.textContent.trim(); - if (country) countries.add(country); - }); - return Array.from(countries).sort(); - } - - function findPreviousHeader(table) { - // Ищем заголовок h2, h3 или h4 перед таблицей - // Сначала проверяем прямых соседей таблицы - let element = table.previousElementSibling; - while (element) { - if (/^H[2-4]$/.test(element.tagName)) { - return element; - } - // Если встретили h1 - останавливаемся - if (/^H[1]$/.test(element.tagName)) { - break; - } - element = element.previousElementSibling; - } - - // Если таблица внутри md-typeset__scrollwrap, ищем заголовок вне этого контейнера - const scrollWrap = table.closest('.md-typeset__scrollwrap'); - if (scrollWrap) { - // Ищем заголовок перед scrollWrap контейнером - element = scrollWrap.previousElementSibling; - while (element) { - if (/^H[2-4]$/.test(element.tagName)) { - return element; - } - if (/^H[1]$/.test(element.tagName)) { - break; - } - element = element.previousElementSibling; - } - - // Если не нашли среди соседей scrollWrap, ищем в родительском контейнере - let parent = scrollWrap.parentElement; - while (parent && parent.tagName !== 'BODY' && parent.tagName !== 'HTML') { - const allChildren = Array.from(parent.children); - const scrollWrapIndex = allChildren.indexOf(scrollWrap); - - if (scrollWrapIndex > 0) { - // Ищем заголовок перед scrollWrap в этом родителе - for (let i = scrollWrapIndex - 1; i >= 0; i--) { - const elem = allChildren[i]; - if (/^H[2-4]$/.test(elem.tagName)) { - return elem; - } - if (/^H[1]$/.test(elem.tagName)) { - break; - } - } - } - - parent = parent.parentElement; - } - } else { - // Если таблица не в scrollWrap, ищем в родительском контейнере - let parent = table.parentElement; - while (parent && parent.tagName !== 'BODY' && parent.tagName !== 'HTML') { - const allChildren = Array.from(parent.children); - const tableIndex = allChildren.indexOf(table); - - if (tableIndex > 0) { - // Ищем заголовок перед таблицей в этом родителе - for (let i = tableIndex - 1; i >= 0; i--) { - const elem = allChildren[i]; - if (/^H[2-4]$/.test(elem.tagName)) { - return elem; - } - if (/^H[1]$/.test(elem.tagName)) { - break; - } - } - } - - parent = parent.parentElement; - } - } - - return null; - } - - function rowPassesFilter(row, countryColumnIndex, selectedCountry) { - if (countryColumnIndex === -1 || !selectedCountry) return true; - const cells = row.querySelectorAll('td, th'); - return cells[countryColumnIndex]?.textContent.trim() === selectedCountry; - } - - // UI creation - function createButtonRow(table, isCollapsed) { - const row = document.createElement('tr'); - row.className = 'table-collapse-button-row'; - - const cell = document.createElement('td'); - cell.className = 'table-collapse-button-cell'; - cell.setAttribute('colspan', getColumnCount(table)); - - const button = document.createElement('button'); - button.className = 'table-collapse-button'; - button.setAttribute('type', 'button'); - button.setAttribute('aria-label', isCollapsed ? 'Expand table' : 'Collapse table'); - button.setAttribute('aria-expanded', !isCollapsed); - button.textContent = isCollapsed ? COLLAPSED_ICON : EXPANDED_ICON; - - cell.appendChild(button); - row.appendChild(cell); - return row; - } - - function createCountryOption(value, flagUrlOrIcon, label, isAll) { - const option = document.createElement('div'); - option.className = 'country-filter-option'; - option.dataset.country = value; - - const flagElement = document.createElement('div'); - flagElement.className = 'country-filter-flag'; - if (!isAll && flagUrlOrIcon && !flagUrlOrIcon.startsWith(GLOBE_ICON)) { - flagElement.className += ' has-flag'; - flagElement.style.backgroundImage = `url('${flagUrlOrIcon}')`; - } else { - flagElement.textContent = GLOBE_ICON; - } - - const labelElement = document.createElement('span'); - labelElement.className = 'country-filter-label'; - labelElement.textContent = label; - - option.appendChild(flagElement); - option.appendChild(labelElement); - return option; - } - - function createCountrySelector(countries, currentLang) { - const container = document.createElement('div'); - container.className = 'country-filter-container'; - - const buttonWrapper = document.createElement('div'); - buttonWrapper.className = 'country-filter-button-wrapper'; - - const button = document.createElement('button'); - button.className = 'country-filter-button'; - button.setAttribute('aria-label', 'Select country'); - - const flagContainer = document.createElement('span'); - flagContainer.className = 'country-filter-button-flag'; - flagContainer.textContent = GLOBE_ICON; - - const selectedText = document.createElement('span'); - selectedText.className = 'country-filter-selected-text'; - selectedText.textContent = translations[currentLang]?.allCountries || 'All'; - - button.appendChild(flagContainer); - button.appendChild(selectedText); - - const dropdown = document.createElement('div'); - dropdown.className = 'country-filter-dropdown'; - dropdown.appendChild(createCountryOption('', GLOBE_ICON, selectedText.textContent, true)); - dropdown.appendChild(Object.assign(document.createElement('div'), { className: 'country-filter-divider' })); - - countries.forEach(country => { - const flagUrl = getFlagUrl(getCountryFlagCode(country)); - dropdown.appendChild(createCountryOption(country, flagUrl, country, false)); - }); - - buttonWrapper.appendChild(button); - buttonWrapper.appendChild(dropdown); - container.appendChild(buttonWrapper); - - let hoverTimeout, isClosing = false; - - button.addEventListener('click', e => { - e.stopPropagation(); - clearTimeout(hoverTimeout); - isClosing = false; - dropdown.classList.toggle('show'); - }); - - buttonWrapper.addEventListener('mouseenter', () => { - if (!isClosing) { - clearTimeout(hoverTimeout); - dropdown.classList.add('show'); - } - }); - - buttonWrapper.addEventListener('mouseleave', () => { - if (dropdown.classList.contains('show') && !isClosing) { - hoverTimeout = setTimeout(() => dropdown.classList.remove('show'), 100); - } - }); - - document.addEventListener('click', e => { - if (!buttonWrapper.contains(e.target)) { - clearTimeout(hoverTimeout); - dropdown.classList.remove('show'); - } - }); - - return { container, button, dropdown, selectedText, flagContainer, setClosing: v => isClosing = v }; - } - - function hideCountryColumn(table, countryColumnIndex) { - const hideCell = (cell) => cell && (cell.style.display = 'none'); - const headerRow = table.querySelector('thead tr'); - if (headerRow) hideCell(headerRow.querySelectorAll('th, td')[countryColumnIndex]); - - getDataRows(table).forEach(row => { - const cells = row.querySelectorAll('td, th'); - if (cells[countryColumnIndex]) hideCell(cells[countryColumnIndex]); - }); - } - - function applyColumnWidths(cells, columnWidths) { - cells.forEach((cell, index) => { - if (columnWidths[index]) { - cell.style.width = cell.style.minWidth = columnWidths[index]; - } - }); - } - - function saveColumnWidths(table) { - const thead = table.querySelector('thead'); - if (!thead) return null; - - const headerRow = thead.querySelector('tr'); - if (!headerRow) return null; - - const headerCells = headerRow.querySelectorAll('th, td'); - const columnWidths = []; - - headerCells.forEach((cell, index) => { - const width = window.getComputedStyle(cell).width; - if (width) { - columnWidths[index] = width; - cell.dataset.originalWidth = width; - cell.style.width = cell.style.minWidth = width; - } - }); - - getDataRows(table).forEach(row => { - applyColumnWidths(row.querySelectorAll('td, th'), columnWidths); - }); - - return columnWidths; - } - - function updateTableVisibility(table) { - const tableData = table._tableData; - if (!tableData || tableData.updating) return; - - const tbody = table.querySelector('tbody'); - if (!tbody) return; - - tableData.updating = true; - - const dataRows = getDataRows(table); - const selectedCountry = tableData.selectedCountry || ''; - const isExpanded = !tableData.isCollapsed; - - // Filter rows - const filteredRows = dataRows.filter(row => - rowPassesFilter(row, tableData.countryColumnIndex, selectedCountry) - ); - - // Apply collapse - const visibleRows = []; - const hiddenRows = []; - let visibleCount = 0; - - filteredRows.forEach(row => { - const shouldBeVisible = !tableData.needsCollapse || visibleCount < MAX_VISIBLE_ROWS || isExpanded; - if (shouldBeVisible && visibleCount < MAX_VISIBLE_ROWS) visibleCount++; - - (shouldBeVisible ? visibleRows : hiddenRows).push(row); - }); - - // Hide rows that don't pass filter - dataRows.forEach(row => { - if (!filteredRows.includes(row)) hiddenRows.push(row); - }); - - // Apply visibility changes synchronously - dataRows.forEach(row => { - const shouldBeVisible = visibleRows.includes(row); - row.style.display = shouldBeVisible ? '' : 'none'; - - if (shouldBeVisible) { - row.classList.remove('table-collapse-hidden-row'); - delete row.dataset.collapseHidden; - if (tableData.columnWidths) { - applyColumnWidths(row.querySelectorAll('td, th'), tableData.columnWidths); - } - } else { - row.classList.add('table-collapse-hidden-row'); - row.dataset.collapseHidden = 'true'; - } - }); - - tableData.hiddenRows = hiddenRows; - updateCollapseButton(table); - tableData.updating = false; - } - - function updateCollapseButton(table) { - const tableData = table._tableData; - if (!tableData?.needsCollapse) return; - - const tbody = table.querySelector('tbody'); - if (!tbody) return; - - const filteredRowCount = getDataRows(table).filter(row => - rowPassesFilter(row, tableData.countryColumnIndex, tableData.selectedCountry || '') - ).length; - - if (filteredRowCount <= MAX_VISIBLE_ROWS) { - tableData.buttonRow?.parentNode?.removeChild(tableData.buttonRow); - return; - } - - if (!tableData.buttonRow?.parentNode) { - const buttonRow = createButtonRow(table, tableData.isCollapsed); - tableData.buttonRow = buttonRow; - tableData.button = buttonRow.querySelector('.table-collapse-button'); - tbody.appendChild(buttonRow); - - tableData.button?.addEventListener('click', () => { - tableData.isCollapsed = !tableData.isCollapsed; - updateTableVisibility(table); - }); - } - - if (tableData.button) { - const isCollapsed = tableData.isCollapsed; - tableData.button.textContent = isCollapsed ? COLLAPSED_ICON : EXPANDED_ICON; - tableData.button.setAttribute('aria-expanded', !isCollapsed); - tableData.button.setAttribute('aria-label', isCollapsed ? 'Expand table' : 'Collapse table'); - } - } - - function updateFilterButton(flagContainer, selectedText, selectedCountry, allCountriesText) { - if (selectedCountry) { - const flagUrl = getFlagUrl(getCountryFlagCode(selectedCountry)); - if (flagUrl) { - flagContainer.style.backgroundImage = `url('${flagUrl}')`; - flagContainer.textContent = ''; - flagContainer.classList.add('has-flag'); - selectedText.textContent = selectedCountry; - return; - } - } - flagContainer.style.backgroundImage = 'none'; - flagContainer.textContent = GLOBE_ICON; - flagContainer.classList.remove('has-flag'); - selectedText.textContent = allCountriesText; - } - - function processTable(table) { - if (table.dataset.tableEnhancedProcessed === 'true') return; - - table.dataset.tableEnhancedProcessed = 'true'; - table.dataset.collapseProcessed = 'true'; - - // Ensure tbody exists - let tbody = table.querySelector('tbody'); - if (!tbody) { - tbody = document.createElement('tbody'); - const thead = table.querySelector('thead'); - const allRows = Array.from(table.querySelectorAll('tr')); - - allRows.forEach(row => { - if (!thead?.contains(row) && !row.classList.contains('table-collapse-button-row')) { - tbody.appendChild(row); - } - }); - - (thead ? thead.parentNode : table).insertBefore(tbody, thead?.nextSibling || table.firstChild); - } - - const tableData = { - countryColumnIndex: -1, - selectedCountry: '', - needsCollapse: false, - isCollapsed: true, - hiddenRows: [], - columnWidths: null, - buttonRow: null, - button: null, - updating: false - }; - - table._tableData = tableData; - - // Setup country filter if needed - const countryColumnIndex = findCountryColumnIndex(table); - if (countryColumnIndex !== -1) { - tableData.countryColumnIndex = countryColumnIndex; - const countries = getCountriesFromTable(table, countryColumnIndex); - - if (countries.length > 0) { - const currentLang = getCurrentLanguage(); - const { container, dropdown, selectedText, flagContainer, setClosing } = - createCountrySelector(countries, currentLang); - - // Ищем заголовок h2, h3 или h4 перед таблицей - const header = findPreviousHeader(table); - if (header && header.parentNode) { - // Проверяем, есть ли уже обертка для заголовка - let headerWrapper = header.closest('.table-header-with-filter'); - if (!headerWrapper) { - // Создаем обертку для заголовка и фильтра - headerWrapper = document.createElement('div'); - headerWrapper.className = 'table-header-with-filter'; - // Заменяем заголовок на обертку с заголовком внутри - header.parentNode.insertBefore(headerWrapper, header); - headerWrapper.appendChild(header); - } - // Добавляем фильтр в обертку после заголовка - headerWrapper.appendChild(container); - } else { - // Если заголовок не найден, добавляем фильтр перед таблицей - table.parentNode?.insertBefore(container, table); - } - - hideCountryColumn(table, countryColumnIndex); - - const allCountriesText = translations[currentLang]?.allCountries || 'All'; - dropdown.querySelectorAll('.country-filter-option').forEach(option => { - option.addEventListener('click', function(e) { - e.stopPropagation(); - e.preventDefault(); - - tableData.selectedCountry = this.dataset.country || ''; - - setClosing(true); - dropdown.classList.remove('show'); - setTimeout(() => setClosing(false), 150); - - updateFilterButton(flagContainer, selectedText, tableData.selectedCountry, allCountriesText); - updateTableVisibility(table); - }); - }); - } - } - - // Setup collapse if needed - if (countDataRows(table) > MAX_VISIBLE_ROWS) { - tableData.needsCollapse = true; - tableData.columnWidths = saveColumnWidths(table); - } - - updateTableVisibility(table); - } - - function processAllTables() { - document.querySelectorAll('table').forEach(processTable); - } - - function init() { - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', processAllTables); - } else { - processAllTables(); - } - - new MutationObserver(mutations => { - mutations.forEach(mutation => { - if (mutation.type === 'childList') { - mutation.addedNodes.forEach(node => { - if (node.nodeType === 1) { - if (node.tagName === 'TABLE') { - processTable(node); - } else { - node.querySelectorAll?.('table').forEach(processTable); - } - } - }); - } - }); - }).observe(document.body, { childList: true, subtree: true }); - } - - init(); -})(); diff --git a/docs/assets/stylesheets/badge.css b/docs/assets/stylesheets/badge.css deleted file mode 100644 index a1723c9..0000000 --- a/docs/assets/stylesheets/badge.css +++ /dev/null @@ -1,85 +0,0 @@ -/* Badge container - make links with badges flex containers */ -table a:has(.badge-left), -table a:has(.badge-only) { - display: inline-flex; - align-items: stretch; - width: 100%; -} - -/* Badge styles for download buttons */ -.badge-left, -.badge-right, -.badge-only { - display: inline-block; - padding: 6px 12px; - line-height: 1.2; - text-align: center; - white-space: nowrap; - vertical-align: middle; - transition: all 0.2s ease; - margin: 0.2rem 0rem; - text-transform: uppercase; -} - -/* Left part of the badge - stretches to fill available space */ -.badge-left { - background-color: #555; - color: #fff; - border-radius: 4px 0 0 4px; - border-right: 1px solid rgba(0, 0, 0, 0.1); - flex: 1 1 auto; - min-width: 0; -} - -/* Right part of the badge - stays compact */ -.badge-right { - background-color: #007ec6; - color: #fff; - border-radius: 0 4px 4px 0; - margin-left: -4px; - flex: 0 0 auto; -} - -/* Single badge (no split) */ -.badge-only { - background-color: #6c757d; - color: #fff; - border-radius: 4px; - width: 100%; -} - -/* Remove underline from links containing badges */ -a:has(.badge-left), -a:has(.badge-right), -a:has(.badge-only) { - text-decoration: none !important; -} - -/* Hover effect for entire badge link */ -a:hover .badge-left { - background-color: #444; -} - -a:hover .badge-right { - background-color: #0066a1; -} - -a:hover .badge-only { - background-color: #5a6268; -} - -/* Ensure badges are inline */ -a .badge-left + .badge-right { - display: inline-block; -} - -/* Mobile responsive */ -@media screen and (max-width: 768px) { - .badge-left, - .badge-right, - .badge-only { - font-size: 12px; - padding: 5px 10px; - } -} - diff --git a/docs/assets/stylesheets/country-table-filter.css b/docs/assets/stylesheets/country-table-filter.css deleted file mode 100644 index 28a65ce..0000000 --- a/docs/assets/stylesheets/country-table-filter.css +++ /dev/null @@ -1,207 +0,0 @@ -/* Country table filter styles */ - -/* Container for country filter */ -.country-filter-container { - display: inline-flex; - align-items: center; - gap: 0.5rem; - position: relative; - flex-shrink: 0; - white-space: nowrap; -} - -/* Wrapper for header and filter */ -.table-header-with-filter { - display: flex; - align-items: center; - justify-content: flex-start; - gap: 1rem; - width: auto; - margin-bottom: 0; -} - -/* Ensure header stays on the left */ -.table-header-with-filter > h1, -.table-header-with-filter > h2, -.table-header-with-filter > h3, -.table-header-with-filter > h4, -.table-header-with-filter > h5, -.table-header-with-filter > h6 { - margin: 0; -} - -/* Text showing selected country inside button */ -.country-filter-selected-text { - color: var(--md-default-fg-color, #000); - font-size: 0.9rem; - font-weight: 500; - white-space: nowrap; -} - -/* Compact spacing */ -.country-filter-container + table { - margin-top: 0; -} - -/* Spacing when filter is next to header */ -.table-header-with-filter + table { - margin-top: 1rem; -} - -/* Remove top and bottom margins from table wrapper */ -.md-typeset__scrollwrap { - margin-top: 0.2rem; - margin-bottom: 0.2rem; -} - -/* Wrapper for button and dropdown */ -.country-filter-button-wrapper { - position: relative; - display: inline-block; -} - -/* Button with flag and text */ -.country-filter-button { - display: flex; - align-items: center; - justify-content: flex-start; - gap: 0.5rem; - min-width: auto; - height: auto; - border-radius: 0; - border: none; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - transition: opacity 0.3s ease, transform 0.2s ease, box-shadow 0.2s ease; - cursor: pointer; - background-color: var(--md-default-bg-color, #fff); - padding: 0.4rem 0.6rem; - font-size: 0.9rem; -} - -.country-filter-button:hover { - transform: scale(1.05); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); -} - -/* Flag container inside button */ -.country-filter-button-flag { - display: inline-flex; - align-items: center; - justify-content: center; - width: 20px; - height: 15px; - flex-shrink: 0; - background-size: cover; - background-repeat: no-repeat; - background-position: center; - font-size: 16px; -} - -.country-filter-button-flag.has-flag { - font-size: 0; -} - -/* Dropdown menu - styled like Material theme select */ -.country-filter-dropdown { - position: absolute; - top: 100%; - left: 0; - margin-top: 0.2rem; - background-color: var(--md-default-bg-color, #fff); - border: none; - border-radius: 0; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25); - display: none; - flex-direction: column; - min-width: auto; - max-height: 300px; - overflow-y: auto; - z-index: 1000; - padding: 0.2rem 0; - width: auto; -} - -.country-filter-button-wrapper:hover .country-filter-dropdown, -.country-filter-dropdown.show { - display: flex; -} - -/* Divider in dropdown */ -.country-filter-divider { - height: 1px; - background-color: var(--md-default-fg-color--lighter, rgba(0, 0, 0, 0.12)); - margin: 0.2rem 0; -} - -/* Option in dropdown - styled like md-select__link */ -.country-filter-option { - display: flex; - align-items: center; - justify-content: flex-start; - padding: 0.3rem 0.5rem; - cursor: pointer; - transition: background-color 0.2s ease; - width: 100%; - min-height: 28px; - gap: 0.5rem; -} - -.country-filter-option:hover { - background-color: var(--md-default-fg-color--lightest, rgba(0, 0, 0, 0.05)); -} - -/* Flag element in option - only flag, no text */ -.country-filter-flag { - width: 20px; - height: 15px; - border-radius: 2px; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - font-size: 16px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); -} - -.country-filter-flag.has-flag { - background-size: contain; - background-repeat: no-repeat; - background-position: center; - border: none; -} - -/* Label text in option */ -.country-filter-label { - color: var(--md-default-fg-color, #000); - font-size: 0.8rem; - display: block; -} - -/* Responsive styles for mobile devices */ -@media screen and (max-width: 59.9375em) { - /* На мобильных устройствах селектор не должен переноситься */ - .table-header-with-filter { - flex-wrap: nowrap; - } - - .country-filter-container { - margin-left: 0; - flex-shrink: 0; - white-space: nowrap; - } -} - -/* Для очень маленьких экранов селектор также не должен переноситься */ -@media screen and (max-width: 30em) { - .table-header-with-filter { - flex-wrap: nowrap; - align-items: center; - } - - .country-filter-container { - margin-left: 0; - flex-shrink: 0; - white-space: nowrap; - } -} - diff --git a/docs/assets/stylesheets/download-button.css b/docs/assets/stylesheets/download-button.css deleted file mode 100644 index e86636b..0000000 --- a/docs/assets/stylesheets/download-button.css +++ /dev/null @@ -1,79 +0,0 @@ -/* Download Button Styles */ -.download-button-container { - display: flex; - justify-content: center; - margin: 0.35rem 0 0 0; - padding: 0 0.5rem; -} - -.download-button { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.625rem 1.5rem; - font-size: 1rem; - font-weight: 600; - color: var(--md-primary-bg-color, #fff); - background-color: var(--md-primary-fg-color, #4051b5); - border: none; - border-radius: 8px; - cursor: pointer; - transition: all 0.3s ease; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - text-decoration: none; - font-family: inherit; -} - -.download-button:hover { - background-color: var(--md-primary-fg-color--dark, #303fa0); - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); -} - -.download-button:active { - transform: translateY(0); - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); -} - -.download-button-text { - display: inline-block; -} - -.download-button-icon { - display: inline-block; - font-size: 1.25rem; - transition: transform 0.3s ease; -} - -.download-button:hover .download-button-icon { - transform: translateY(4px); -} - -/* Responsive adjustments for download button */ -@media screen and (max-width: 59.9375em) { - .download-button-container { - margin: 0.35rem 0 0 0; - } - - .download-button { - padding: 0.5rem 1.25rem; - font-size: 0.9375rem; - } -} - -@media screen and (max-width: 30em) { - .download-button-container { - margin: 0.35rem 0 0 0; - } - - .download-button { - padding: 0.5rem 1rem; - font-size: 0.875rem; - gap: 0.5rem; - } - - .download-button-icon { - font-size: 1.125rem; - } -} - diff --git a/docs/assets/stylesheets/extra.css b/docs/assets/stylesheets/extra.css deleted file mode 100644 index 1bdae2f..0000000 --- a/docs/assets/stylesheets/extra.css +++ /dev/null @@ -1,389 +0,0 @@ -:root { - --md-primary-fg-color: #121212; -} - -/* Hide navigation and drawer on mobile devices */ -@media screen and (max-width: 76.1875em) { - /* Hide burger menu button */ - .md-header__button[for="__drawer"] { - display: none !important; - } - - /* Hide drawer (slide-out menu) */ - .md-overlay { - display: none !important; - } - - #__drawer { - display: none !important; - } - - /* Hide both sidebars */ - .md-sidebar--primary, - .md-sidebar--secondary { - display: none !important; - } - - /* Make content full-width */ - .md-content { - max-width: 100%; - margin-left: 0; - margin-right: 0; - } - - .md-main__inner { - margin-left: 0; - margin-right: 0; - } -} - -/* Icon styling in standard menu */ -.md-nav__icon { - font-size: 1.2rem; - margin-right: 0.5rem; - width: 20px; - text-align: center; - display: inline-block; -} - -/* Improve styles for custom menu items */ -.md-nav__item .md-nav__link { - display: flex; - align-items: center; - padding: 0.6rem 1rem; -} - -.md-nav__item .md-nav__link:hover { - background-color: var(--md-default-fg-color--lightest); -} - -/* Smooth appearance of navigation tabs */ -.md-tabs__list { - opacity: 0; - transform: translateY(-10px); - transition: opacity 0.4s ease-out, transform 0.4s ease-out; -} - -/* Show tabs after loading */ -.md-tabs__list.loaded { - opacity: 1; - transform: translateY(0); -} - -/* Smooth appearance of navigation items */ -.md-nav__item { - opacity: 0; - transform: translateX(-20px); - transition: opacity 0.3s ease-out, transform 0.3s ease-out; -} - -/* Show navigation items with delay */ -.md-nav__item.loaded { - opacity: 1; - transform: translateX(0); -} - -/* Delay for each item */ -.md-nav__item:nth-child(1).loaded { transition-delay: 0.1s; } -.md-nav__item:nth-child(2).loaded { transition-delay: 0.2s; } -.md-nav__item:nth-child(3).loaded { transition-delay: 0.3s; } -.md-nav__item:nth-child(4).loaded { transition-delay: 0.4s; } -.md-nav__item:nth-child(5).loaded { transition-delay: 0.5s; } - -/* Styles for page links (not in menu) */ -.md-content a:not(.md-nav__link):not(.md-tabs__link) { - color: var(--md-primary-fg-color); - text-decoration: underline; -} - -.md-content a:not(.md-nav__link):not(.md-tabs__link):hover { - color: var(--md-accent-fg-color); - text-decoration: underline; -} - -/* Ensure link readability in dark theme */ -[data-md-color-scheme="slate"] .md-content a:not(.md-nav__link):not(.md-tabs__link) { - color: #8cc8ff; -} - -[data-md-color-scheme="slate"] .md-content a:not(.md-nav__link):not(.md-tabs__link):hover { - color: #b3d9ff; -} - -/* Fix horizontal scrolling on mobile devices */ -html, body { - overflow-x: hidden; - max-width: 100%; -} - -/* Optimized styles for header on mobile devices */ -/* Use built-in Material theme capabilities with minimal overrides */ -@media screen and (max-width: 76.1875em) { - - /* Ensure proper header element display */ - .md-header__title { - flex: 1; - min-width: 0; - overflow: hidden; - } - - .md-header__button { - flex-shrink: 0; - } -} -.md-header__inner { - width: 100%; - max-width: none; - padding-left: 1rem; - padding-right: 1rem; -} - -/* Additional optimization for navigation.top usage */ -@media screen and (min-width: 76.25em) { - .md-header__inner { - max-width: 100%; - } -} - -.md-header__button.md-logo { - display: contents; -} - -/* Prevent content overflow */ -.md-container { - max-width: 100%; - overflow-x: hidden; -} - -.md-main { - max-width: 100%; - overflow-x: hidden; -} - -.md-content { - max-width: 100%; - overflow-x: hidden; - word-wrap: break-word; - overflow-wrap: break-word; -} - -/* Fix for tables */ -.md-typeset table { - width: 100%; - max-width: 100%; - table-layout: auto; - overflow-x: visible; - display: table; - white-space: normal; -} - -.md-typeset table th, -.md-typeset table td { - word-wrap: break-word; - overflow-wrap: break-word; - max-width: none; - white-space: normal; -} - -/* Fix for code */ -.md-typeset pre { - max-width: 100%; - overflow-x: auto; - word-wrap: break-word; - white-space: pre-wrap; -} - -.md-typeset code { - word-wrap: break-word; - overflow-wrap: break-word; - max-width: 100%; -} - -/* Fix for images */ -.md-typeset img { - max-width: 100%; - height: auto; -} - -/* Fix for navigation */ -.md-nav { - max-width: 100%; - overflow-x: hidden; -} - -.md-nav__list { - max-width: 100%; - overflow-x: hidden; -} -/* Fix for table alignment */ -.md-typeset table td { - align-content: center; -} - -.md-typeset table:not([class]) td, -.md-typeset table:not([class]) th { - padding: 0.3em 0.7em; -} - -.md-typeset table:not([class]) { - font-size: 0.9em; -} - -.md-typeset ol li, .md-typeset ul li { - margin-bottom: 0; -} - - -/* Fix for h1 margin */ -.md-typeset ul { - margin-block-end: 0; - margin-block-start: 0; -} -.md-typeset h1 { - margin-bottom: 1em; - font-weight: 500; -} - -.md-typeset h2 { - margin-top: 0.75em; - margin-block-end: 0; - margin-bottom: 0; - font-weight: 400; -} -.md-typeset h3 { - margin-top: 0.5em; - margin-block-end: 0; - margin-bottom: 0; - font-weight: 300; -} - -.md-typeset p { - margin-block-start: 0; - margin-block-end: 0; -} - -thead:not(:has(th:not(:empty))) { - display: none; -} - -/* Additional fixes for mobile devices */ -@media screen and (max-width: 76.1875em) { - .md-container { - padding-left: 0; - padding-right: 0; - } - - .md-main__inner { - max-width: 100%; - overflow-x: hidden; - } - - .md-content__inner { - max-width: 100%; - overflow-x: hidden; - } - - .md-typeset table th, - .md-typeset table td { - padding: 0.3rem 0.3rem; - } - - .md-typeset table:not([class]) td, - .md-typeset table:not([class]) th { - padding: 0.3em 0.5em; - } -} - -/* Additional responsive styles for mobile devices */ -@media screen and (max-width: 59.9375em) { - /* Reduce padding on very small screens */ - .md-content { - padding: 1rem 0.5rem; - } - - /* Improve table display on mobile */ - .md-typeset table { - font-size: 0.8rem; - table-layout: fixed; - width: 100%; - } - - .md-typeset table th, - .md-typeset table td { - padding: 0.1rem 0.2rem; - word-break: break-word; - } - .md-typeset table:not([class]) td, - .md-typeset table:not([class]) th { - padding: 0.1em 0.5em; - } - - /* Improve code display on mobile */ - .md-typeset pre { - font-size: 0.8rem; - padding: 0.5rem; - } - - /* Improve navigation display */ - .md-nav__item { - padding: 0.2rem 0; - } - - .md-nav__link { - padding: 0.4rem 0.8rem; - } -} - -/* Language flag styles moved to separate file language-flags.css */ - -/* Styles for very small screens (up to 480px) */ -@media screen and (max-width: 30em) { - .md-content { - padding: 0.5rem 0.3rem; - } - - .md-typeset h1 { - font-size: 1.5rem; - } - - .md-typeset h2 { - font-size: 1.3rem; - } - - .md-typeset h3 { - font-size: 1.1rem; - } - - /* Improve button and link display */ - .md-typeset a { - word-break: break-word; - } - - /* Improve list display */ - .md-typeset ul, .md-typeset ol { - padding-left: 1rem; - } - - /* Header optimization for very small screens */ - .md-header__inner { - padding-left: 0.5rem; - padding-right: 0.5rem; - } - - .md-header__button { - padding: 0.4rem; - min-width: 2.4rem; - } - -} - -/* Add left margin to logo to match right margin of GitHub widget */ -.md-header__button.md-logo { - margin-left: 3rem; -} - -.md-icon { - visibility: hidden; - opacity: 0; - transition: opacity 0.3s ease, transform 0.2s ease, box-shadow 0.2s ease; -} \ No newline at end of file diff --git a/docs/assets/stylesheets/language-flags.css b/docs/assets/stylesheets/language-flags.css deleted file mode 100644 index 5657c5a..0000000 --- a/docs/assets/stylesheets/language-flags.css +++ /dev/null @@ -1,160 +0,0 @@ -/* Language flags for MkDocs Material theme */ - -/* Base styles for language selector links */ -.md-select__link { - position: relative; -} - -/* Add flags to language codes using hreflang attribute */ -.md-select__link[hreflang="en"]::before { - content: ""; - display: inline-block; - width: 20px; - height: 15px; - margin-right: 8px; - vertical-align: middle; - background-image: url("https://flagcdn.com/w20/us.png"); - background-size: contain; - background-repeat: no-repeat; - background-position: center; - border-radius: 2px; - box-shadow: 0 1px 3px rgba(0,0,0,0.2); -} - -.md-select__link[hreflang="ru"]::before { - content: ""; - display: inline-block; - width: 20px; - height: 15px; - margin-right: 8px; - vertical-align: middle; - background-image: url("https://flagcdn.com/w20/ru.png"); - background-size: contain; - background-repeat: no-repeat; - background-position: center; - border-radius: 2px; - box-shadow: 0 1px 3px rgba(0,0,0,0.2); -} - -.md-select__link[hreflang="es"]::before { - content: ""; - display: inline-block; - width: 20px; - height: 15px; - margin-right: 8px; - vertical-align: middle; - background-image: url("https://flagcdn.com/w20/es.png"); - background-size: contain; - background-repeat: no-repeat; - background-position: center; - border-radius: 2px; - box-shadow: 0 1px 3px rgba(0,0,0,0.2); -} - -.md-select__link[hreflang="de"]::before { - content: ""; - display: inline-block; - width: 20px; - height: 15px; - margin-right: 8px; - vertical-align: middle; - background-image: url("https://flagcdn.com/w20/de.png"); - background-size: contain; - background-repeat: no-repeat; - background-position: center; - border-radius: 2px; - box-shadow: 0 1px 3px rgba(0,0,0,0.2); -} - -.md-select__link[hreflang="fr"]::before { - content: ""; - display: inline-block; - width: 20px; - height: 15px; - margin-right: 8px; - vertical-align: middle; - background-image: url("https://flagcdn.com/w20/fr.png"); - background-size: contain; - background-repeat: no-repeat; - background-position: center; - border-radius: 2px; - box-shadow: 0 1px 3px rgba(0,0,0,0.2); -} - -/* Styles for the language flag button */ -.language-flag-button { - position: relative; - overflow: hidden; - border-radius: 4px; - aspect-ratio: 3/2; - width: 32px; - height: 21px; - background-size: cover; - background-position: center; - background-repeat: no-repeat; - border: 1px solid rgba(0, 0, 0, 0.1); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - transition: all 0.2s ease; -} - -.language-flag-button::after { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient(45deg, transparent 30%, rgba(255,255,255,0.2) 50%, transparent 70%); - transform: translateX(-100%); - transition: transform 0.4s ease; - pointer-events: none; - border-radius: 4px; -} - -.language-flag-button:hover::after { - transform: translateX(100%); -} - -.language-flag-button:hover { - transform: scale(1.05); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); -} - -/* Ensure the flag button is properly sized */ -.md-header__option .md-select .language-flag-button { - min-width: 32px; - min-height: 21px; - padding: 0; - border: 1px solid rgba(0, 0, 0, 0.1); - background: transparent; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - outline: none; -} - -/* Mobile responsiveness */ -@media screen and (max-width: 30em) { - /* Adapt flags for small screens */ - .md-select__link[hreflang]::before { - width: 16px; - height: 12px; - margin-right: 6px; - } - - /* Smaller flag button on mobile */ - .language-flag-button { - width: 28px; - height: 19px; - min-width: 28px; - min-height: 19px; - } -} - -/* Dark theme adjustments */ -[data-md-color-scheme="slate"] .language-flag-button { - border: 1px solid rgba(255, 255, 255, 0.1); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); -} - -[data-md-color-scheme="slate"] .language-flag-button:hover { - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); -} diff --git a/docs/assets/stylesheets/language-selector.css b/docs/assets/stylesheets/language-selector.css deleted file mode 100644 index bb4e95b..0000000 --- a/docs/assets/stylesheets/language-selector.css +++ /dev/null @@ -1,73 +0,0 @@ -/* Language selector dropdown positioning and styling */ - -/* Language selector dropdown positioning */ -.md-select { - position: relative; -} - -.md-select__inner { - position: absolute; - top: 100%; - left: 0; - right: auto; - transform: none; - margin-top: 0.2rem; - z-index: 1000; -} - -/* Hide the dropdown arrow since positioning is now left-aligned */ -.md-select__inner::before { - display: none !important; -} - -.md-select__inner::after { - display: none !important; -} - -/* Ensure dropdown is positioned correctly on mobile */ -@media screen and (max-width: 76.1875em) { - .md-select__inner { - left: 0; - right: auto; - min-width: 8rem; - max-width: 12rem; - } -} - -/* Additional mobile optimization for language selector */ -@media screen and (max-width: 59.9375em) { - .md-select__inner { - left: 0; - right: auto; - min-width: 7rem; - max-width: 10rem; - } - - .md-select__list { - padding: 0.2rem 0; - } - - .md-select__item { - padding: 0.1rem 0; - } - - .md-select__link { - padding: 0.4rem 0.8rem; - font-size: 0.9rem; - } -} - -/* Very small screens optimization */ -@media screen and (max-width: 30em) { - .md-select__inner { - left: 0; - right: auto; - min-width: 6rem; - max-width: 8rem; - } - - .md-select__link { - padding: 0.3rem 0.6rem; - font-size: 0.8rem; - } -} diff --git a/docs/assets/stylesheets/screenshots.css b/docs/assets/stylesheets/screenshots.css deleted file mode 100644 index 5578bce..0000000 --- a/docs/assets/stylesheets/screenshots.css +++ /dev/null @@ -1,287 +0,0 @@ -/* Screenshots Gallery Styles */ -.screenshots-gallery { - display: flex; - flex-wrap: nowrap; - gap: 1rem; - margin: 0.1rem 0; - padding: 0.35rem; - overflow-x: auto; - overflow-y: hidden; - scrollbar-width: none; /* Firefox */ - -ms-overflow-style: none; /* IE and Edge */ - scroll-behavior: smooth; - border-radius: 8px; - background-color: var(--md-default-bg-color--lightest); - box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); - position: relative; -} - -/* Gradient fade indicators for scrollable content */ -.screenshots-gallery::before, -.screenshots-gallery::after { - content: ''; - position: absolute; - top: 0; - bottom: 0; - width: 40px; - pointer-events: none; - z-index: 1; - transition: opacity 0.3s ease; -} - -.screenshots-gallery::before { - left: 0; - background: linear-gradient(to right, var(--md-default-bg-color--lightest), transparent); -} - -.screenshots-gallery::after { - right: 0; - background: linear-gradient(to left, var(--md-default-bg-color--lightest), transparent); -} - -/* Hide gradients when scrolled to edges */ -.screenshots-gallery.scrolled-start::before { - opacity: 0; -} - -.screenshots-gallery.scrolled-end::after { - opacity: 0; -} - -/* Hide scrollbar for Chrome, Safari and Opera */ -.screenshots-gallery::-webkit-scrollbar { - display: none; -} - -.screenshot-item { - flex: 0 0 auto; - min-width: 300px; - max-width: 300px; - width: 300px; - position: relative; - z-index: 2; -} - -.screenshot-link { - display: block; - cursor: pointer; - transition: transform 0.2s ease, box-shadow 0.2s ease; - border-radius: 8px; - overflow: hidden; - background-color: var(--md-default-bg-color); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.screenshot-link:hover { - transform: translateY(-4px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); -} - -.screenshot-thumb { - width: 100%; - height: auto; - display: block; - object-fit: cover; -} - -/* Lightbox Overlay */ -.lightbox-overlay { - display: none; - position: fixed; - z-index: 9999; - left: 0; - top: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.9); - overflow: auto; - animation: fadeIn 0.3s ease; -} - -.lightbox-overlay.active { - display: flex; - align-items: center; - justify-content: center; -} - -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -.lightbox-content { - position: relative; - max-width: 90%; - max-height: 90%; - margin: auto; - display: flex; - align-items: center; - justify-content: center; -} - -#lightbox-image { - max-width: 100%; - max-height: 90vh; - width: auto; - height: auto; - object-fit: contain; - border-radius: 4px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); - animation: zoomIn 0.3s ease; -} - -@keyframes zoomIn { - from { - transform: scale(0.8); - opacity: 0; - } - to { - transform: scale(1); - opacity: 1; - } -} - -.lightbox-close { - position: absolute; - top: 20px; - right: 35px; - color: #f1f1f1; - font-size: 40px; - font-weight: bold; - cursor: pointer; - z-index: 10000; - transition: color 0.2s ease; - line-height: 1; - user-select: none; -} - -.lightbox-close:hover, -.lightbox-close:focus { - color: #fff; - text-decoration: none; -} - -.lightbox-prev, -.lightbox-next { - position: absolute; - top: 50%; - transform: translateY(-50%); - color: #f1f1f1; - font-size: 40px; - font-weight: bold; - cursor: pointer; - z-index: 10000; - transition: color 0.2s ease, background-color 0.2s ease; - user-select: none; - padding: 16px; - border-radius: 4px; - background-color: rgba(0, 0, 0, 0.3); -} - -.lightbox-prev { - left: 20px; -} - -.lightbox-next { - right: 20px; -} - -.lightbox-prev:hover, -.lightbox-prev:focus, -.lightbox-next:hover, -.lightbox-next:focus { - color: #fff; - background-color: rgba(0, 0, 0, 0.6); - text-decoration: none; -} - -/* Responsive Design */ -@media screen and (max-width: 76.1875em) { - .screenshots-gallery { - gap: 0.75rem; - } - - .screenshot-item { - min-width: 250px; - max-width: 250px; - width: 250px; - } - - .lightbox-close { - top: 15px; - right: 25px; - font-size: 35px; - } - - .lightbox-prev, - .lightbox-next { - font-size: 30px; - padding: 12px; - } - - .lightbox-prev { - left: 10px; - } - - .lightbox-next { - right: 10px; - } -} - -@media screen and (max-width: 59.9375em) { - .screenshots-gallery { - gap: 0.5rem; - } - - .screenshot-item { - min-width: 200px; - max-width: 200px; - width: 200px; - } - - .lightbox-content { - max-width: 95%; - max-height: 95%; - } - - #lightbox-image { - max-height: 85vh; - } -} - -@media screen and (max-width: 30em) { - .screenshots-gallery { - gap: 0.5rem; - } - - .screenshot-item { - min-width: 180px; - max-width: 180px; - width: 180px; - } - - .lightbox-close { - top: 10px; - right: 15px; - font-size: 30px; - } - - .lightbox-prev, - .lightbox-next { - font-size: 24px; - padding: 8px; - } - - .lightbox-prev { - left: 5px; - } - - .lightbox-next { - right: 5px; - } -} - diff --git a/docs/assets/stylesheets/table-collapse.css b/docs/assets/stylesheets/table-collapse.css deleted file mode 100644 index f799c0c..0000000 --- a/docs/assets/stylesheets/table-collapse.css +++ /dev/null @@ -1,145 +0,0 @@ -/* Table collapse styles */ - -/* Row containing the collapse button */ -.table-collapse-button-row { - border-top: 1px solid var(--md-default-fg-color--lightest, rgba(0, 0, 0, 0.12)); -} - -/* Cell containing the collapse button */ -.table-collapse-button-cell { - text-align: center; - padding: 0; - background-color: var(--md-default-bg-color--light, rgba(0, 0, 0, 0.02)); -} - -/* Collapse/expand button */ -.table-collapse-button { - background: none; - border: none; - cursor: pointer; - padding: 0; - font-size: 1rem; - color: var(--md-default-fg-color--light, rgba(0, 0, 0, 0.54)); - transition: opacity 0.2s ease; - line-height: 1; - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 1rem; - max-height: 1rem; -} - -.table-collapse-button:hover { - opacity: 0.8; -} - -.table-collapse-button:active { - opacity: 0.6; -} - -.table-collapse-button:focus { - outline: none; -} - -/* Hidden rows */ -.table-collapse-hidden-row { - display: none; -} - -/* Ensure button row is visible */ -.table-collapse-button-row { - display: table-row; -} - -/* Fix table width to prevent changes when collapsing/expanding */ -table[data-collapse-processed="true"] { - table-layout: fixed; - width: 100%; - max-width: 100%; -} - -/* Preserve column widths from original rendering */ -table[data-collapse-processed="true"] th[data-original-width], -table[data-collapse-processed="true"] td[data-original-width] { - width: var(--original-width, auto); -} - -/* Ensure all cells in the same column have consistent width */ -table[data-collapse-processed="true"] th, -table[data-collapse-processed="true"] td { - min-width: 0; - overflow-wrap: break-word; -} - -/* Prevent vertical scrolling on small screens */ -@media screen and (max-width: 76.1875em) { - /* Remove vertical overflow from table containers */ - .md-typeset__scrollwrap { - overflow-y: visible !important; - overflow-x: auto; - /* max-height: none !important; */ - } - - /* Ensure tables with collapse script don't create vertical scroll */ - table[data-collapse-processed="true"] { - overflow-y: visible !important; - max-height: none !important; - } - - /* Ensure parent containers don't create vertical scroll */ - .md-typeset table[data-collapse-processed="true"], - .md-content table[data-collapse-processed="true"] { - overflow-y: visible !important; - max-height: none !important; - } - - /* Ensure button cell doesn't create extra height */ - .table-collapse-button-cell { - padding: 0.1rem 0 !important; - height: auto; - line-height: 1; - vertical-align: middle; - } - - /* Ensure button doesn't create extra space */ - .table-collapse-button { - margin: 0; - padding: 0; - min-height: auto; - height: 1rem; - max-height: 1rem; - line-height: 1; - } - - /* Ensure button row doesn't create extra height */ - .table-collapse-button-row { - height: auto; - line-height: 1; - } -} - -/* Additional fixes for very small screens */ -@media screen and (max-width: 59.9375em) { - .md-typeset__scrollwrap { - overflow-y: visible !important; - overflow-x: auto; - max-height: none !important; - } - - table[data-collapse-processed="true"] { - overflow-y: visible !important; - max-height: none !important; - } - - .table-collapse-button-cell { - padding: 0.05rem 0 !important; - } - - .table-collapse-button { - padding: 0; - height: 0.9rem; - max-height: 0.9rem; - } -} - diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..291ca38 --- /dev/null +++ b/docs/index.md @@ -0,0 +1 @@ +# Home diff --git a/generate_html.py b/generate_html.py new file mode 100755 index 0000000..66d6efe --- /dev/null +++ b/generate_html.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +""" +Скрипт для генерации HTML страниц из Jinja2 шаблонов +""" + +import os +import json +from pathlib import Path +from jinja2 import Environment, FileSystemLoader, select_autoescape +import config + + +def setup_jinja_environment(templates_dir): + """Настроить окружение Jinja2""" + env = Environment( + loader=FileSystemLoader(templates_dir), + autoescape=select_autoescape(['html', 'xml']) + ) + + # Добавить функции в контекст шаблонов + env.globals.update({ + 'site_config': config.SITE_CONFIG, + 'data': config.DATA, + 'languages': config.LANGUAGES, + 'pages': config.PAGES, + 'get_page_url': config.get_page_url + }) + + return env + + +def get_asset_path(lang): + """Получить путь к ассетам в зависимости от языка""" + if lang == 'en': + return 'assets/' + return '../assets/' + + +def generate_page(env, page_name, lang, output_dir): + """Сгенерировать HTML страницу""" + # Загрузить шаблон страницы + template_path = f'pages/{page_name}.html' + try: + template = env.get_template(template_path) + except Exception as e: + print(f"Warning: Template {template_path} not found, skipping... ({e})") + return + + # Получить переводы для языка + translations = config.TRANSLATIONS.get(lang, config.TRANSLATIONS['en']) + + # Создать функцию перевода для текущего языка + def t(key_path, default=None): + return config.get_translation(lang, key_path, default) + + # Определить путь страницы + page_path = config.get_page_url(page_name, lang) + + # Определить путь к ассетам + asset_path = get_asset_path(lang) + + # Получить переводы для JavaScript (I18N) в виде JSON строки + i18n_for_js = config.get_i18n_for_js() + i18n_json = json.dumps(i18n_for_js, ensure_ascii=False) + + # Получить конфигурацию для JavaScript (CONFIG) в виде JSON строки + config_for_js = config.get_config_for_js() + config_json = json.dumps(config_for_js, ensure_ascii=False) + + # Получить мета-данные страницы + page_meta = config.get_page_meta(page_name, lang) + + # Рендерить шаблон + html = template.render( + lang=lang, + page_name=page_name, + page_path=page_path, + asset_path=asset_path, + t=t, + translations=translations, + site_config=config.SITE_CONFIG, + data=config.DATA, + languages=config.LANGUAGES, + get_page_url=lambda pname, l: config.get_page_url(pname, l), + get_translation=lambda key_path, default=None: config.get_translation(lang, key_path, default), + i18n_json=i18n_json, + config_json=config_json, + page_meta=page_meta, + # Для use-cases - сначала проверяем translations, потом data как fallback + use_cases_list=translations.get('sections', {}).get('use_cases', {}).get('cases', []) or config.DATA.get('use_cases', []) + ) + + # Определить выходной путь + if lang == 'en': + output_file = output_dir / f'{page_name}.html' + else: + lang_dir = output_dir / lang + lang_dir.mkdir(exist_ok=True) + output_file = lang_dir / f'{page_name}.html' + + # Сохранить HTML + output_file.write_text(html, encoding='utf-8') + try: + rel_path = output_file.relative_to(Path.cwd()) + print(f"Generated: {rel_path}") + except ValueError: + # Если относительный путь не работает, используем абсолютный + print(f"Generated: {output_file}") + + +def main(): + """Главная функция""" + # Пути + project_root = Path(__file__).parent + templates_dir = project_root / 'templates' + output_dir = project_root / 'src' + + # Проверить существование директорий + if not templates_dir.exists(): + print(f"Error: Templates directory not found: {templates_dir}") + return + + output_dir.mkdir(exist_ok=True) + + # Настроить Jinja2 + env = setup_jinja_environment(str(templates_dir)) + + # Список страниц для генерации + pages = ['index', 'discovery', 'features', 'use-cases', 'download'] + + # Список языков + languages = list(config.LANGUAGES.keys()) + + # Генерировать страницы для каждого языка + for lang in languages: + print(f"\nGenerating pages for language: {lang}") + for page_name in pages: + try: + generate_page(env, page_name, lang, output_dir) + except Exception as e: + print(f"Error generating {page_name} for {lang}: {e}") + import traceback + traceback.print_exc() + + print("\nHTML generation complete!") + + +if __name__ == '__main__': + main() + diff --git a/hooks.py b/hooks.py index fd7fc17..a1b78df 100644 --- a/hooks.py +++ b/hooks.py @@ -1,9 +1,6 @@ """MkDocs event hooks used by the AngryScan documentation build.""" from __future__ import annotations -import hashlib -import os -import time from mkdocs.structure.nav import Navigation @@ -18,80 +15,167 @@ def on_nav(nav: Navigation, config, files): def on_post_build(config): - """Add cache-busting to CSS files after build.""" - import os - import re + """Replace MkDocs build with static site from src directory.""" + import shutil + import subprocess + from pathlib import Path # Get the site directory - site_dir = config['site_dir'] + site_dir = Path(config['site_dir']) + src_dir = Path(__file__).parent / 'src' + static_dir = Path(__file__).parent / 'static' - # Define CSS files that need cache-busting - css_files = [ - 'assets/stylesheets/extra.css', - 'assets/stylesheets/language-flags.css' - ] + # Check if src directory exists + if not src_dir.exists(): + print(f"Warning: src directory not found at {src_dir}") + return - # Generate a cache-busting hash based on file modification times - cache_hash = generate_cache_hash(site_dir, css_files) + # Get current language being built (from i18n plugin) + # The i18n plugin sets site_dir to include language subdirectory for non-default languages + # For default language (en), site_dir is just 'site', for others it's 'site/ru', 'site/de', etc. + current_lang = None + site_dir_str = str(site_dir) + if '/site/' in site_dir_str or '\\site\\' in site_dir_str: + # Extract language from path like 'site/ru' or 'site/de' + parts = site_dir_str.replace('\\', '/').split('/') + if 'site' in parts: + site_idx = parts.index('site') + if site_idx + 1 < len(parts): + potential_lang = parts[site_idx + 1] + if potential_lang in ['ru', 'de', 'fr', 'es']: + current_lang = potential_lang - # Process all HTML files in the site directory - for root, dirs, files in os.walk(site_dir): - for file in files: - if file.endswith('.html'): - html_path = os.path.join(root, file) - add_cache_busting_to_html(html_path, css_files, cache_hash) - - -def generate_cache_hash(site_dir, css_files): - """Generate a hash based on CSS file modification times for cache-busting.""" - hash_input = "" + # Only run generation and translation scripts once (for the first language build, typically 'en') + # Use a marker file to track if scripts have already run + marker_file = Path(__file__).parent / '.scripts_run_marker' - for css_file in css_files: - css_path = os.path.join(site_dir, css_file) - if os.path.exists(css_path): - # Use file modification time and size for hash - stat = os.stat(css_path) - hash_input += f"{css_file}:{stat.st_mtime}:{stat.st_size}" + if not marker_file.exists(): + # First build - run scripts + marker_file.touch() + + # Generate HTML pages from templates (includes all languages from config.json) + generate_html_script = Path(__file__).parent / 'generate_html.py' + if generate_html_script.exists(): + print("Generating HTML pages from templates...") + try: + subprocess.run(['python3', str(generate_html_script)], check=True, cwd=generate_html_script.parent) + except subprocess.CalledProcessError as e: + print(f"Warning: Failed to generate HTML pages: {e}") + except FileNotFoundError: + print("Warning: python3 not found, skipping HTML generation") + else: + # Scripts already run for this build - skip + print("Skipping generation/translation (already done for this build)") - # Add current timestamp to ensure uniqueness - hash_input += f":{time.time()}" + # Remove all existing files in site_dir (except .git if exists) + print(f"Cleaning site directory: {site_dir}") + for item in site_dir.iterdir(): + if item.name != '.git': + if item.is_dir(): + shutil.rmtree(item) + else: + item.unlink() - # Generate a short hash - return hashlib.md5(hash_input.encode()).hexdigest()[:8] - - -def add_cache_busting_to_html(html_path, css_files, cache_hash): - """Add cache-busting parameters to CSS links in HTML files.""" - try: - with open(html_path, 'r', encoding='utf-8') as f: - content = f.read() - - original_content = content - - # Add cache-busting to each CSS file - for css_file in css_files: - # Pattern to match CSS file references - patterns = [ - f'href="{css_file}"', - f"href='{css_file}'", - f'rel="stylesheet" href="{css_file}"', - f"rel='stylesheet' href='{css_file}'" - ] - - for pattern in patterns: - if '?' in pattern: - # If already has parameters, replace them - new_pattern = pattern.split('?')[0] + f'?v={cache_hash}' - content = content.replace(pattern, new_pattern) - else: - # Add cache-busting parameter - new_pattern = pattern[:-1] + f'?v={cache_hash}"' - content = content.replace(pattern, new_pattern) - - # Write back if content changed - if content != original_content: - with open(html_path, 'w', encoding='utf-8') as f: + # Copy all files from src to site_dir + print(f"Copying files from {src_dir} to {site_dir}") + for item in src_dir.iterdir(): + if item.name != '.git' and item.name != 'README.md': + dest = site_dir / item.name + if item.is_dir(): + shutil.copytree(item, dest, dirs_exist_ok=True) + else: + shutil.copy2(item, dest) + + # Create clean URL structure for GitHub Pages (discovery/, features/, etc.) + # Each page gets its own folder with index.html inside + print("Creating clean URL structure for GitHub Pages...") + clean_url_pages = ['discovery', 'features', 'use-cases', 'download'] + for page in clean_url_pages: + page_dir = site_dir / page + page_dir.mkdir(exist_ok=True) + source_file = site_dir / f"{page}.html" + if source_file.exists(): + dest_file = page_dir / "index.html" + shutil.copy2(source_file, dest_file) + # Update paths in the copied file (css, js, assets should be ../css, ../js, ../assets) + with open(dest_file, 'r', encoding='utf-8') as f: + content = f.read() + content = content.replace('href="css/', 'href="../css/') + content = content.replace('src="js/', 'src="../js/') + content = content.replace('src="assets/', 'src="../assets/') + content = content.replace('href="assets/', 'href="../assets/') + content = content.replace('data-light="assets/', 'data-light="../assets/') + content = content.replace('data-dark="assets/', 'data-dark="../assets/') + # Update navigation links to use clean URLs (trailing slash) + content = content.replace(f'href="{page}.html"', f'href="/{page}/"') + content = content.replace(f'href="/{page}.html"', f'href="/{page}/"') + with open(dest_file, 'w', encoding='utf-8') as f: f.write(content) - - except Exception as e: - print(f"Warning: Could not process {html_path}: {e}") + print(f" Created {page}/index.html") + + # Copy language-specific directories (ru, de, fr, es) to site root + print("Copying language-specific directories...") + for lang_dir in ['ru', 'de', 'fr', 'es']: + lang_src = src_dir / lang_dir + if lang_src.exists() and lang_src.is_dir(): + lang_dest = site_dir / lang_dir + if lang_dest.exists(): + shutil.rmtree(lang_dest) + shutil.copytree(lang_src, lang_dest) + print(f" Copied {lang_dir}/ to site root") + + # Create clean URL structure for language-specific pages (required for GitHub Pages) + # Both /ru/discovery and /ru/discovery/ will work, but canonical points to /ru/discovery + print("Creating clean URL structure for language-specific pages...") + for lang_dir in ['ru', 'de', 'fr', 'es']: + lang_path = site_dir / lang_dir + if lang_path.exists() and lang_path.is_dir(): + for page in clean_url_pages: + page_dir = lang_path / page + page_dir.mkdir(exist_ok=True) + source_file = lang_path / f"{page}.html" + if source_file.exists(): + dest_file = page_dir / "index.html" + shutil.copy2(source_file, dest_file) + # Update paths in the copied file + with open(dest_file, 'r', encoding='utf-8') as f: + content = f.read() + content = content.replace('href="../css/', 'href="../../css/') + content = content.replace('src="../js/', 'src="../../js/') + content = content.replace('src="../assets/', 'src="../../assets/') + content = content.replace('href="../assets/', 'href="../../assets/') + content = content.replace('data-light="../assets/', 'data-light="../../assets/') + content = content.replace('data-dark="../assets/', 'data-dark="../../assets/') + # Update navigation links to use clean URLs with language prefix (trailing slash) + for nav_page in clean_url_pages: + if nav_page == 'index': + content = content.replace(f'href="index.html"', f'href="/{lang_dir}/"') + content = content.replace(f'href="/{lang_dir}/index.html"', f'href="/{lang_dir}/"') + else: + content = content.replace(f'href="{nav_page}.html"', f'href="/{lang_dir}/{nav_page}/"') + content = content.replace(f'href="/{lang_dir}/{nav_page}.html"', f'href="/{lang_dir}/{nav_page}/"') + # Also update root links + content = content.replace('href="/"', f'href="/{lang_dir}/"') + content = content.replace('href="/discovery"', f'href="/{lang_dir}/discovery/"') + content = content.replace('href="/features"', f'href="/{lang_dir}/features/"') + content = content.replace('href="/use-cases"', f'href="/{lang_dir}/use-cases/"') + content = content.replace('href="/download"', f'href="/{lang_dir}/download/"') + with open(dest_file, 'w', encoding='utf-8') as f: + f.write(content) + print(f" Created {lang_dir}/{page}/index.html") + # Remove the .html file to avoid duplicate URLs (GitHub Pages will use the folder structure) + source_file.unlink() + print(f" Removed {lang_dir}/{page}.html (using folder structure instead)") + + # Copy static files from static directory + if static_dir.exists(): + for static_file in static_dir.iterdir(): + if static_file.is_file(): + dest_file = site_dir / static_file.name + shutil.copy2(static_file, dest_file) + print(f"Copied {static_file.name} to site root") + + # Note: Marker file persists during the build process to prevent multiple script runs + # It will be automatically removed when mkdocs build starts fresh next time + + print(f"Successfully replaced site with src content") diff --git a/mkdocs.yml b/mkdocs.yml index b8dfc45..e6d5133 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -9,10 +9,7 @@ hooks: - hooks.py theme: name: material - custom_dir: overrides language: en - logo: assets/images/logo.png - favicon: assets/images/favicon.ico palette: - scheme: slate primary: custom @@ -48,20 +45,8 @@ plugins: fallback_to_default: true - minify: minify_html: true -extra_javascript: -- assets/javascripts/language-selector.js -- assets/javascripts/table-enhanced.js -- assets/javascripts/screenshots.js -- assets/javascripts/download-button.js -extra_css: -- assets/stylesheets/extra.css -- assets/stylesheets/badge.css -- assets/stylesheets/language-flags.css -- assets/stylesheets/language-selector.css -- assets/stylesheets/country-table-filter.css -- assets/stylesheets/screenshots.css -- assets/stylesheets/download-button.css -- assets/stylesheets/table-collapse.css +extra_javascript: [] +extra_css: [] extra: generator: false alternate: diff --git a/overrides/main.html b/overrides/main.html deleted file mode 100644 index ff7f91f..0000000 --- a/overrides/main.html +++ /dev/null @@ -1,126 +0,0 @@ -{% extends "base.html" %} - -{% block site_meta %} - - - - - {% if page.meta and page.meta.description %} - - {% elif config.site_description %} - - {% endif %} - - - {% if page.meta and page.meta.author %} - - {% elif config.site_author %} - - {% endif %} - - - {% if page.canonical_url %} - - {% endif %} - - - {% if page.previous_page %} - - {% endif %} - - - {% if page.next_page %} - - {% endif %} - - - {% if "rss" in config.plugins %} - - - {% endif %} - - - - - - - - - - - {% if page and page.meta and page.meta.title %} - - {% elif page and page.title %} - - {% else %} - - {% endif %} - - - {% if page and page.meta and page.meta.description %} - - {% elif page and page.description %} - - {% else %} - - {% endif %} - - - - - - - - - - - - - - {% include "partials/alternate-links.html" %} -{% endblock %} - -{% block htmltitle %} - {% if page and page.meta and page.meta.title %} - {{ page.meta.title }} - {% elif page and page.title %} - {{ page.title }} - {% else %} - {{ config.site_name }} - {% endif %} - - {% include "partials/integrations/analytics/custom.html" %} - -{% endblock %} \ No newline at end of file diff --git a/overrides/partials/alternate-links.html b/overrides/partials/alternate-links.html deleted file mode 100644 index e1a0093..0000000 --- a/overrides/partials/alternate-links.html +++ /dev/null @@ -1,26 +0,0 @@ -{% if config.extra.alternate and page and page.canonical_url %} - {% set page_url = page.canonical_url | replace(config.site_url, '') %} - - {# Удаляем языковой префикс для получения базового пути #} - {% set ns = namespace(clean_path=page_url) %} - {% for alt in config.extra.alternate %} - {% if alt.lang != 'en' %} - {% set lang_prefix = alt.lang ~ '/' %} - {% if page_url.startswith(lang_prefix) %} - {% set ns.clean_path = page_url[lang_prefix|length:] %} - {% endif %} - {% endif %} - {% endfor %} - - {# Генерируем теги alternate для каждого языка #} - {% for alt in config.extra.alternate %} - {% if alt.lang == 'en' %} - - {% else %} - - {% endif %} - {% endfor %} - {# x-default указывает на английскую версию #} - -{% endif %} - diff --git a/overrides/partials/header.html b/overrides/partials/header.html deleted file mode 100644 index 27b3224..0000000 --- a/overrides/partials/header.html +++ /dev/null @@ -1,99 +0,0 @@ - -{% set class = "md-header" %} -{% if "navigation.tabs.sticky" in features %} - {% set class = class ~ " md-header--shadow md-header--lifted" %} -{% elif "navigation.tabs" not in features %} - {% set class = class ~ " md-header--shadow" %} -{% endif %} - - - - - - -
- -
\ No newline at end of file diff --git a/overrides/partials/integrations/analytics/custom.html b/overrides/partials/integrations/analytics/custom.html deleted file mode 100644 index 95de798..0000000 --- a/overrides/partials/integrations/analytics/custom.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 25668c0..f9842e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,8 +2,8 @@ mkdocs>=1.5 mkdocs-material>=9.5 mkdocs-awesome-pages-plugin>=2.9 mkdocs-minify-plugin>=0.7 -deep-translator>=1.11 mkdocs-static-i18n>=1.3 PyYAML>=6.0 aiofiles>=23.0 tqdm>=4.65 +Jinja2>=3.1 diff --git a/scripts/README_download_config.md b/scripts/README_download_config.md deleted file mode 100644 index 71208e0..0000000 --- a/scripts/README_download_config.md +++ /dev/null @@ -1,159 +0,0 @@ ---- -hide: - - navigation - - toc - - navigation - - toc - - navigation - - toc - - navigation - - toc ---- -# Download Page Configuration - -## Overview - -The download page generation system is split into two files: -- `download_config.yaml` - configuration rules and settings in YAML format -- `generate_downloads_page.py` - main generation script - -## Configuration Structure - -### YAML Structure - -The configuration is organized into the following sections: - -```yaml -# Page settings -page: - title: "Download" - description: "Page description" - subtitle: "Subtitle" - -# Repository settings -repository: - owner: "angryscan" - name: "angrydata-app" - -# Operating systems display order -os_order: - - "Windows" - - "Linux" - - "MacOS" - -# Operating systems configuration -operating_systems: - Windows: - label: "🪟 **Windows**" - icon: "windows" - color_scheme: - primary: "#0078d6" - secondary: "#00bcf2" - -# Asset rules -asset_rules: - - os_name: "Windows" - display_name: "Windows" - description: "Download for Windows" - badge_url: "https://img.shields.io/badge/Setup-x64-0078D6?style=for-the-badge&logo=windows" - alt_text: "Windows setup (x64)" - suffixes: - - ".exe" - preferred_substrings: - - "amd64" - - "x64" -``` - -### Adding New Asset Types - -1. **Add a new rule to `asset_rules`**: -```yaml -asset_rules: - - os_name: "Windows" - display_name: "Windows ARM" - description: "Download for Windows ARM" - badge_url: "https://img.shields.io/badge/ARM-64-0078D6?style=for-the-badge&logo=windows" - alt_text: "Windows ARM build" - suffixes: - - "-arm64.exe" - - "-aarch64.exe" - preferred_substrings: - - "arm64" - - "aarch64" -``` - -2. **Update OS configuration if needed**: -```yaml -operating_systems: - Android: - label: "🤖 **Android**" - placeholder: "N/A" - icon: "android" - color_senses: - primary: "#3DDC84" - secondary: "#2E7D32" -``` - -3. **Add OS to display order**: -```yaml -os_order: - - "Windows" - - "Linux" - - "macOS" - - "Android" -``` - -### Styling Configuration - -Styles are located in the `render_css_styles()` function in the main script. To modify: - -1. **Brand colors** - change CSS variables -2. **Animations** - configure `transition` properties -3. **Responsiveness** - update media queries - -### Page Structure - -The page consists of: -1. **Header** - title and description -2. **Release information** - version and date -3. **Download cards** - one for each OS -4. **Release link** - full release notes -5. **Tip** - auto-update information - -## YAML Configuration Benefits - -### Readability -- Human-readable format -- Easy to edit without Python knowledge -- Comments are supported - -### Easy Editing -- No need to know Python syntax -- Syntax validation in editors -- Autocomplete in modern IDEs - -### Flexibility -- Easy to add new fields -- Support for multi-level structures -- Ability to use variables - -## Extending Functionality - -### Adding New File Types - -1. Define rules in `download_config.yaml` -2. Add corresponding CSS styles to the main script -3. Update file selection logic if needed - -### Customizing Display - -1. Modify `render_download_card()` for new card structure -2. Update `render_css_styles()` for new styles -3. Modify `format_release()` to change layout - -### Supporting New Platforms - -1. Add OS to `os_order` in YAML -2. Create configuration in `operating_systems` -3. Define asset rules for the new platform -4. Add corresponding CSS styles diff --git a/scripts/README_metadata_config.md b/scripts/README_metadata_config.md deleted file mode 100644 index 5ad5f42..0000000 --- a/scripts/README_metadata_config.md +++ /dev/null @@ -1,261 +0,0 @@ ---- -hide: - - navigation - - toc - - navigation - - toc - - navigation - - toc - - navigation - - toc ---- -# Metadata Configuration for .md Files - -This document describes the functionality for configuring custom title and description for .md files in the project. - -## Overview - -The functionality allows configuring metadata (title and description) for markdown files through YAML configuration. Metadata is applied automatically when running `sync_docs.py`. - -## Configuration File - -Configuration is stored in `scripts/metadata_config.yaml`: - -```yaml -# Configuration for custom metadata (title and description) for .md files -metadata: - # Global settings - enabled: true - - # File-specific metadata configuration - files: - # Example configurations for different files - # Path is relative to docs directory - "index.md": - title: "Sensitive data discovery tool with friendly UI for Mac, Win, Linux" - description: "Advanced sensitive data discovery tool combining personal data discovery, payment card discovery, and passwords finder in one solution." - # Language-specific overrides (optional) - # If not specified, the default title/description will be auto-translated - translations: - ru: - title: "Инструмент обнаружения конфиденциальных данных с дружественным интерфейсом для Mac, Win, Linux" - description: "Продвинутый инструмент для обнаружения конфиденциальных данных, объединяющий обнаружение персональных данных, платежных карт и паролей в одном решении." - de: - title: "Tool zur Erkennung sensibler Daten mit benutzerfreundlicher Oberfläche für Mac, Win, Linux" - description: "Fortschrittliches Tool zur Erkennung sensibler Daten, das die Erkennung personenbezogener Daten, Zahlungskarten und Passwörter in einer Lösung vereint." - - "angrydata-core/index.md": - title: "Core Library | Angry Data Scanner" - description: "Library for sensitive data processing" - translations: - ru: - title: "Основная библиотека | Angry Data Scanner" - description: "Библиотека для обработки конфиденциальных данных" - - "CONSOLE.md": - title: "Console Mode | Angry Data Scanner" - description: "Using Angry Data Scanner in console mode" - translations: - ru: - title: "Консольный режим | Angry Data Scanner" - description: "Использование Angry Data Scanner в консольном режиме" - - # Default metadata for files not explicitly configured - defaults: - description: "Advanced sensitive data discovery tool combining personal data discovery, payment card discovery, and passwords finder in one solution." -``` - -## Configuration Structure - -### Main Sections - -- `metadata.enabled` - enable/disable metadata processing (default: true) -- `metadata.files` - exact file paths with their metadata -- `metadata.defaults` - default metadata for files without specific configuration - -### Supported Metadata Fields - -- `title` - full page title -- `description` - page description -- `translations` - language-specific overrides for title and description - -### Language-Specific Overrides - -You can define custom title and description for each language using the `translations` section. This is useful when: - -- Automatic translation doesn't capture the right meaning -- You want to use specific terminology for a language -- You need SEO-optimized titles and descriptions for different markets - -**How it works:** - -1. If a language-specific override exists in the `translations` section, it will be used directly (no automatic translation) -2. If no override is defined for a language, the default title/description will be automatically translated -3. Each language can have its own `title` and `description` - -**Example:** - -```yaml -"index.md": - title: "Sensitive data discovery tool" - description: "Tool for discovering sensitive data" - translations: - ru: - title: "Инструмент обнаружения конфиденциальных данных" - description: "Инструмент для обнаружения конфиденциальных данных" - de: - title: "Tool zur Erkennung sensibler Daten" - # description will be auto-translated if not specified - es: - # Both title and description will be auto-translated -``` - -**Supported language codes:** -- `ru` - Russian -- `de` - German -- `es` - Spanish -- `fr` - French -- And any other language code supported by Google Translate - -### File Patterns - -The following wildcard patterns are supported: -- `*` - any number of characters (except `/`) -- `**` - any number of characters including `/` -- `?` - single character - -Examples: -- `"angrydata-core/*.md"` - all .md files in angrydata-core folder -- `"**/README.md"` - all README.md files in any subfolders - -## Metadata Application Priority - -### For Translation (translate_docs.py) - -When translating documents, metadata is applied in the following priority order: - -1. **Language-specific override** in `translations` section for the target language -2. **Auto-translation** of default title/description if no override exists - -Example: -```yaml -"index.md": - title: "Main Page" - description: "Welcome" - translations: - ru: - title: "Главная страница" # ← Will be used for Russian - # description not specified, will be auto-translated - de: - # Both title and description will be auto-translated -``` - -For Russian translation: -- Title: "Главная страница" (from override) -- Description: Auto-translated "Welcome" - -For German translation: -- Title: Auto-translated "Main Page" -- Description: Auto-translated "Welcome" - -### For Source Files (sync_docs.py) - -Metadata is applied in the following priority order: - -1. **Exact match** in `files` section -2. **Default values** from `defaults` section - -## Usage Examples - -### Example 1: Main Page Configuration with Language Overrides - -```yaml -metadata: - files: - "index.md": - title: "Angry Data Scanner - Main Page" - description: "Welcome to Angry Data Scanner" - translations: - ru: - title: "Angry Data Scanner - Главная страница" - description: "Добро пожаловать в Angry Data Scanner" - de: - title: "Angry Data Scanner - Hauptseite" - description: "Willkommen bei Angry Data Scanner" -``` - -### Example 2: Auto-Translation (No Overrides) - -```yaml -metadata: - files: - "CONSOLE.md": - title: "Console Mode | Angry Data Scanner" - description: "Using Angry Data Scanner in console mode" - # No translations section - all languages will be auto-translated -``` - -### Example 3: Default Configuration - -```yaml -metadata: - defaults: - description: "Angry Data Scanner project documentation" -``` - -## Integration with Existing Front Matter - -The functionality works correctly with existing front matter in markdown files: - -- If front matter already exists, new metadata is added or updates existing ones -- If front matter is missing, it is created automatically -- Existing fields are preserved if they are not overridden in configuration - -## Running - -### Translation with Metadata Overrides - -When translating documentation, language-specific metadata overrides are automatically applied: - -```bash -# Translate to all configured languages (async mode) -python scripts/translate_docs.py - -# Translate to specific languages -python scripts/translate_docs.py --targets ru de es - -# Translate in synchronous mode (slower but more reliable) -python scripts/translate_docs.py --sync -``` - -The script will: -1. Check `metadata_config.yaml` for language-specific overrides -2. Use overrides if available for the target language -3. Auto-translate title/description if no override is defined - -### Applying Metadata to Source Files - -Metadata can also be applied to source files when running: - -```bash -python scripts/sync_docs.py -``` - -Or with repository updates: - -```bash -python scripts/sync_docs.py --update-repos -``` - -## Disabling Functionality - -To disable metadata processing, set: - -```yaml -metadata: - enabled: false -``` - -## Logging - -When applying metadata, files are updated only if content actually changes, which minimizes unnecessary write operations. diff --git a/scripts/README_sync_config.md b/scripts/README_sync_config.md deleted file mode 100644 index 1d95a95..0000000 --- a/scripts/README_sync_config.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -hide: - - navigation - - toc ---- -# Конфигурация синхронизации документации - -Файл `sync_config.yaml` позволяет настраивать процесс синхронизации документации. - -## Структура конфигурации - -```yaml -sync: - # Заголовки для удаления из документации - remove_headers: - - "## Direct Download" - - "## Another Section" - - # Пропускать контент до первого заголовка - skip_until_first_header: true - - # Обрабатывать ссылки на переводы - process_translation_links: true -``` - -## Параметры - -### `remove_headers` -Список заголовков, которые должны быть удалены из документации. Поддерживаются точные совпадения заголовков. - -**Примеры:** -- `"## Direct Download"` - удалит раздел Direct Download -- `"## [Console mode](CONSOLE.md)"` - удалит раздел Console mode -- `"## Another Section"` - удалит любой раздел с таким заголовком - -### `skip_until_first_header` -Если `true`, то весь контент до первого заголовка (начинающегося с `#`) будет пропущен. - -### `process_translation_links` -Если `true`, то ссылки на файлы переводов будут обрабатываться и удаляться. - -## Примеры использования - -### Удалить только Direct Download -```yaml -sync: - remove_headers: - - "## Direct Download" - skip_until_first_header: true - process_translation_links: true -``` - -### Удалить несколько разделов -```yaml -sync: - remove_headers: - - "## Direct Download" - - "## [Console mode](CONSOLE.md)" - - "## Installation" - skip_until_first_header: true - process_translation_links: true -``` - -### Отключить обработку ссылок на переводы -```yaml -sync: - remove_headers: - - "## Direct Download" - skip_until_first_header: true - process_translation_links: false -``` - -### Не пропускать контент до первого заголовка -```yaml -sync: - remove_headers: - - "## Direct Download" - skip_until_first_header: false - process_translation_links: true -``` diff --git a/scripts/README_thumbnails.md b/scripts/README_thumbnails.md deleted file mode 100644 index d12fb4f..0000000 --- a/scripts/README_thumbnails.md +++ /dev/null @@ -1,38 +0,0 @@ -# Создание миниатюр для галереи скриншотов - -Для оптимизации загрузки страницы рекомендуется создать уменьшенные версии (thumbnails) скриншотов. - -## Автоматическое создание миниатюр - -Используйте скрипт `create_thumbnails.py`: - -```bash -python scripts/create_thumbnails.py -``` - -**Требования:** -- Python 3.x -- Pillow библиотека: `pip install Pillow` - -Скрипт создаст миниатюры размером 300x200px в формате JPEG: -- `screenshot_thumb.png` (из `screenshot.png`) -- `screenshot_2_thumb.png` (из `screenshot_2.png`) -- `screenshot_3_thumb.png` (из `screenshot_3.png`) -- `screenshot_4_thumb.png` (из `screenshot_4.png`) - -## Ручное создание миниатюр - -Если автоматический скрипт недоступен, создайте миниатюры вручную: - -1. Откройте каждый скриншот в графическом редакторе -2. Измените размер до 300x200px (или пропорционально, сохраняя соотношение сторон) -3. Сохраните как: - - `docs/assets/images/screenshot_thumb.png` - - `docs/assets/images/screenshot_2_thumb.png` - - `docs/assets/images/screenshot_3_thumb.png` - - `docs/assets/images/screenshot_4_thumb.png` - -## Fallback - -Если миниатюры не созданы, галерея будет использовать полные изображения. Это работает, но увеличивает время загрузки страницы. - diff --git a/scripts/config_utils.py b/scripts/config_utils.py deleted file mode 100644 index 19c8f30..0000000 --- a/scripts/config_utils.py +++ /dev/null @@ -1,318 +0,0 @@ -#!/usr/bin/env python3 -"""Shared helpers for reading MkDocs configuration.""" - -from __future__ import annotations - -from pathlib import Path -from typing import Any, Dict, List - -import yaml - -ROOT = Path(__file__).resolve().parents[1] -MKDOCS_PATH = ROOT / "mkdocs.yml" -SYNC_CONFIG_PATH = Path(__file__).parent / "sync_config.yaml" -TRANSLATION_CONFIG_PATH = Path(__file__).parent / "translation_config.yaml" - - -def load_mkdocs_config() -> Dict[str, Any]: - with MKDOCS_PATH.open("r", encoding="utf-8") as stream: - return yaml.safe_load(stream) or {} - - -def get_i18n_languages(config: Dict[str, Any] | None = None) -> List[Dict[str, Any]]: - config = config or load_mkdocs_config() - plugins = config.get("plugins", []) - for plugin in plugins: - if isinstance(plugin, dict) and "i18n" in plugin: - languages = plugin["i18n"].get("languages", []) - normalized: List[Dict[str, Any]] = [] - for entry in languages: - if isinstance(entry, dict): - normalized.append( - { - "locale": entry.get("locale"), - "name": entry.get("name", entry.get("locale")), - "default": bool(entry.get("default")), - } - ) - else: - normalized.append({"locale": entry, "name": entry, "default": False}) - return [lang for lang in normalized if lang.get("locale")] - return [] - - -def get_translation_locales(config: Dict[str, Any] | None = None) -> List[str]: - locales: List[str] = [] - for lang in get_i18n_languages(config=config): - if not lang.get("default"): - locales.append(str(lang["locale"])) - return locales - - -def get_all_locales(config: Dict[str, Any] | None = None) -> List[str]: - return [str(lang["locale"]) for lang in get_i18n_languages(config=config)] - - -def generate_alternate_menu_items(config: Dict[str, Any] | None = None) -> List[Dict[str, str]]: - """Generate alternate menu items for all languages.""" - languages = get_i18n_languages(config) - alternate_items = [] - - for lang in languages: - locale = lang["locale"] - name = lang["name"] - - # Create link path - if lang.get("default"): - link = "/" - else: - link = f"/{locale}/" - - alternate_items.append({ - "name": name, - "lang": locale, - "link": link - }) - - return alternate_items - - -def update_mkdocs_alternate_menu(config_path: Path | None = None) -> None: - """Update mkdocs.yml with auto-generated alternate menu items.""" - if config_path is None: - config_path = MKDOCS_PATH - - # Load current config - config = load_mkdocs_config() - - # Generate new alternate menu items - alternate_items = generate_alternate_menu_items(config) - - # Update the config - if "extra" not in config: - config["extra"] = {} - - config["extra"]["alternate"] = alternate_items - - # Write back to file - with config_path.open("w", encoding="utf-8") as f: - yaml.dump(config, f, default_flow_style=False, allow_unicode=True, sort_keys=False) - - -def generate_js_language_mappings(config: Dict[str, Any] | None = None) -> Dict[str, Dict[str, str]]: - """Generate JavaScript language mappings for custom-menu.js.""" - languages = get_i18n_languages(config) - js_mappings = {} - - # Language name mappings for JavaScript - language_names = { - 'en': 'English', - 'ru': 'Русский', - 'es': 'Español', - 'de': 'Deutsch', - 'fr': 'Français' - } - - # Menu text translations - menu_translations = { - 'en': {'main': 'Main', 'library': 'Angry Data Core'}, - 'ru': {'main': 'Главная', 'library': 'Angry Data Core'}, - 'es': {'main': 'Principal', 'library': 'Angry Data Core'}, - 'de': {'main': 'Hauptseite', 'library': 'Angry Data Core'}, - 'fr': {'main': 'Principal', 'library': 'Angry Data Core'} - } - - for lang in languages: - locale = lang["locale"] - if locale in menu_translations: - js_mappings[locale] = menu_translations[locale] - - return js_mappings - - -def update_menu_translations_json(config: Dict[str, Any] | None = None, json_path: Path | None = None) -> None: - """Update menu-translations.json with auto-generated language mappings.""" - if json_path is None: - json_path = ROOT / "docs" / "assets" / "menu-translations.json" - - # Generate language mappings - js_mappings = generate_js_language_mappings(config) - - # Write JSON file - import json - with json_path.open("w", encoding="utf-8") as f: - json.dump(js_mappings, f, ensure_ascii=False, indent=2) - - -def load_sync_config() -> Dict[str, Any]: - """Load sync configuration from YAML file.""" - if not SYNC_CONFIG_PATH.exists(): - return {} - - with SYNC_CONFIG_PATH.open("r", encoding="utf-8") as stream: - return yaml.safe_load(stream) or {} - - -def get_remove_headers() -> List[str]: - """Get list of headers to remove from documentation.""" - config = load_sync_config() - sync_config = config.get("sync", {}) - return sync_config.get("remove_headers", ["## Direct Download"]) - - -def should_skip_until_first_header() -> bool: - """Check if content should be skipped until first header.""" - config = load_sync_config() - sync_config = config.get("sync", {}) - return sync_config.get("skip_until_first_header", True) - - -def should_process_translation_links() -> bool: - """Check if translation links should be processed.""" - config = load_sync_config() - sync_config = config.get("sync", {}) - return sync_config.get("process_translation_links", True) - - -def load_translation_config() -> Dict[str, Any]: - """Load translation configuration from YAML file.""" - if not TRANSLATION_CONFIG_PATH.exists(): - return {} - - with TRANSLATION_CONFIG_PATH.open("r", encoding="utf-8") as stream: - return yaml.safe_load(stream) or {} - - -def load_metadata_config() -> Dict[str, Any]: - """Load metadata configuration from YAML file.""" - config = load_translation_config() - return config.get("metadata", {}) - - -def is_metadata_enabled() -> bool: - """Check if metadata processing is enabled.""" - metadata_config = load_metadata_config() - # metadata_config already contains the metadata section - return metadata_config.get("enabled", True) - - -def get_file_metadata(file_path: str) -> Dict[str, str]: - """Get custom metadata for a specific file.""" - if not is_metadata_enabled(): - return {} - - metadata_config = load_metadata_config() - # metadata_config already contains the metadata section - - # Check for exact file match - files_config = metadata_config.get("files", {}) - if file_path in files_config: - return files_config[file_path] - - # Check for pattern matches - patterns_config = metadata_config.get("patterns", {}) - for pattern, pattern_config in patterns_config.items(): - if _match_pattern(file_path, pattern): - return pattern_config - - # Return defaults if no specific configuration found - defaults = metadata_config.get("defaults", {}) - return defaults - - -def _match_pattern(file_path: str, pattern: str) -> bool: - """Check if file path matches a pattern (supports wildcards).""" - import fnmatch - return fnmatch.fnmatch(file_path, pattern) - - -def apply_metadata_to_content(content: str, file_path: str) -> str: - """Apply custom metadata to markdown content.""" - metadata = get_file_metadata(file_path) - if not metadata: - return content - - # Check if front matter already exists - if content.startswith("---"): - # Extract existing front matter - parts = content.split("---", 2) - if len(parts) >= 3: - front_matter = parts[1].strip() - content_after = parts[2].lstrip("\n") - - # Parse existing front matter - existing_metadata = {} - lines = front_matter.split("\n") - i = 0 - while i < len(lines): - line = lines[i].strip() - if ":" in line and not line.startswith(" "): - key, value = line.split(":", 1) - key = key.strip() - value = value.strip() - - # Check if this is a multi-line value (like hide: with list) - if i + 1 < len(lines) and lines[i + 1].startswith(" "): - # Collect all indented lines - multi_line_value = [value] if value else [] - i += 1 - while i < len(lines) and lines[i].startswith(" "): - multi_line_value.append(lines[i].strip()) - i += 1 - existing_metadata[key] = "\n".join(multi_line_value) - i -= 1 # Adjust for the loop increment - else: - existing_metadata[key] = value - i += 1 - - # Apply new metadata (skip 'translations' key) - for key, value in metadata.items(): - if key == "translations": - continue # Skip translations, they are language-specific - if key == "title": - existing_metadata["title"] = value - elif key == "description": - existing_metadata["description"] = value - elif key == "title_prefix": - # Add prefix to existing title or use as new title - if "title" in existing_metadata: - existing_metadata["title"] = value + existing_metadata["title"] - else: - existing_metadata["title"] = value - - # Preserve existing hide values if they exist - don't override them - # This prevents duplication of hide entries - - # Rebuild front matter - new_front_matter = [] - for key, value in existing_metadata.items(): - if "\n" in value: - # Multi-line value (like hide: with list) - new_front_matter.append(f"{key}:") - for line in value.split("\n"): - if line.strip(): - new_front_matter.append(f" {line}") - else: - # Single-line value - new_front_matter.append(f"{key}: {value}") - - return f"---\n" + "\n".join(new_front_matter) + f"\n---\n{content_after}" - else: - # Create new front matter - front_matter_lines = ["---"] - - # Apply metadata (skip 'translations' key) - for key, value in metadata.items(): - if key == "translations": - continue # Skip translations, they are language-specific - if key == "title": - front_matter_lines.append(f"title: {value}") - elif key == "description": - front_matter_lines.append(f"description: {value}") - elif key == "title_prefix": - front_matter_lines.append(f"title: {value}") - - front_matter_lines.append("---") - front_matter_lines.append("") - - return "\n".join(front_matter_lines) + "\n" + content \ No newline at end of file diff --git a/scripts/create_thumbnails.py b/scripts/create_thumbnails.py deleted file mode 100644 index 04f1b7d..0000000 --- a/scripts/create_thumbnails.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python3 -"""Create thumbnail versions of screenshot images.""" - -from pathlib import Path -from PIL import Image -import sys - -ROOT = Path(__file__).resolve().parents[1] -IMAGES_DIR = ROOT / "docs" / "assets" / "images" - -THUMBNAIL_SIZE = (300, 200) # width, height -THUMBNAIL_QUALITY = 85 - -def create_thumbnail(source_path: Path, dest_path: Path) -> bool: - """Create a thumbnail from source image.""" - try: - with Image.open(source_path) as img: - # Convert to RGB if necessary (for PNG with transparency) - if img.mode in ('RGBA', 'LA', 'P'): - # Create a white background - background = Image.new('RGB', img.size, (255, 255, 255)) - if img.mode == 'P': - img = img.convert('RGBA') - background.paste(img, mask=img.split()[-1] if img.mode in ('RGBA', 'LA') else None) - img = background - elif img.mode != 'RGB': - img = img.convert('RGB') - - # Create thumbnail maintaining aspect ratio - img.thumbnail(THUMBNAIL_SIZE, Image.Resampling.LANCZOS) - - # Save thumbnail - img.save(dest_path, 'JPEG', quality=THUMBNAIL_QUALITY, optimize=True) - print(f"Created thumbnail: {dest_path.name}") - return True - except Exception as e: - print(f"Error creating thumbnail for {source_path.name}: {e}", file=sys.stderr) - return False - -def main(): - """Create thumbnails for all screenshot images.""" - if not IMAGES_DIR.exists(): - print(f"Images directory not found: {IMAGES_DIR}", file=sys.stderr) - return 1 - - screenshot_files = [ - "screenshot.png", - "screenshot_2.png", - "screenshot_3.png", - "screenshot_4.png" - ] - - success_count = 0 - total_count = 0 - - for screenshot_file in screenshot_files: - source_path = IMAGES_DIR / screenshot_file - - if not source_path.exists(): - print(f"Warning: {screenshot_file} not found, skipping", file=sys.stderr) - continue - - # Create thumbnail filename - base_name = screenshot_file.replace('.png', '') - thumb_filename = f"{base_name}_thumb.png" - dest_path = IMAGES_DIR / thumb_filename - - total_count += 1 - if create_thumbnail(source_path, dest_path): - success_count += 1 - - print(f"\nThumbnails created: {success_count}/{total_count}") - return 0 if success_count == total_count else 1 - -if __name__ == "__main__": - sys.exit(main()) - diff --git a/scripts/static/favicon.ico b/scripts/static/favicon.ico deleted file mode 100644 index 0d95364..0000000 Binary files a/scripts/static/favicon.ico and /dev/null differ diff --git a/scripts/static/robots.txt b/scripts/static/robots.txt deleted file mode 100644 index dbbd956..0000000 --- a/scripts/static/robots.txt +++ /dev/null @@ -1,23 +0,0 @@ -# robots.txt for angryscan.org -# Generated for Angry Data Scanner documentation site - -User-agent: * -Allow: / -Disallow: /.well-known/ - -# Sitemap -Sitemap: https://angryscan.org/sitemap.xml - -# Allow common bots explicitly -User-agent: Googlebot -Allow: / - -User-agent: Bingbot -Allow: / - -User-agent: Yandex -Allow: / - -# Crawl delay for polite crawling -Crawl-delay: 1 - diff --git a/scripts/sync_config.yaml b/scripts/sync_config.yaml deleted file mode 100644 index 45d9bbb..0000000 --- a/scripts/sync_config.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# Configuration for documentation synchronization -sync: - # Headers to remove from documentation - remove_headers: [] - # - "## Direct Download" # Uncomment to remove Direct Download section - # Add more headers to remove as needed - # - "## Another Section" - # - "## [Console mode](CONSOLE.md)" # Uncomment to remove Console mode section - - # Whether to skip content until first header - skip_until_first_header: true - - # Whether to process translation links - process_translation_links: true diff --git a/scripts/sync_docs.py b/scripts/sync_docs.py deleted file mode 100755 index ee99991..0000000 --- a/scripts/sync_docs.py +++ /dev/null @@ -1,892 +0,0 @@ -#!/usr/bin/env python3 -"""Sync documentation from sibling repositories into the local docs directory.""" - -from __future__ import annotations - -import argparse -import re -import shutil -import subprocess -import sys -import urllib.parse -from pathlib import Path -from typing import Iterable, Tuple - -from config_utils import ( - get_all_locales, - get_translation_locales, - get_remove_headers, - should_skip_until_first_header, - should_process_translation_links, - apply_metadata_to_content -) - -ROOT = Path(__file__).resolve().parents[1] -DOCS_ROOT = ROOT / "docs" -SOURCES_ROOT = ROOT / "sources" - -REPOSITORIES: Tuple[Tuple[str, str, str], ...] = ( - ("angrydata-app", "AngryData App", "https://github.com/angryscan/angrydata-app.git"), - # ("angrydata-core", "AngryData Core", "https://github.com/angryscan/angrydata-core.git"), -) - -ALL_LOCALES = [locale for locale in get_all_locales() if locale] -TRANSLATION_LOCALES = [locale for locale in get_translation_locales() if locale] - -TRANSLATION_SUFFIXES = tuple( - suffix - for locale in ALL_LOCALES - for suffix in (f".{locale}.md", f".{locale}.markdown") -) - -if ALL_LOCALES: - escaped_locales = sorted( - {re.escape(locale) for locale in ALL_LOCALES}, - key=len, - reverse=True, - ) - locale_pattern = "|".join(escaped_locales) -else: - locale_pattern = "ru|es|de" - -TRANSLATION_LINK_PATTERN = re.compile( - rf"\[([^\]]+)\]\(([^)]+\.({locale_pattern})\.(?:md|markdown))\)", re.IGNORECASE -) - - -def is_translation_filename(name: str) -> bool: - lowered = name.lower() - return any(lowered.endswith(suffix) for suffix in TRANSLATION_SUFFIXES) - - -ASSET_PRESERVE = {"assets"} - - -def clone_repository(repo_url: str, repo_dir: Path) -> bool: - """Clone repository from URL.""" - if repo_dir.exists(): - print(f"Repository {repo_dir.name} already exists, skipping clone", file=sys.stderr) - return True - - try: - print(f"Cloning repository: {repo_url}") - result = subprocess.run( - ["git", "clone", repo_url, str(repo_dir)], - capture_output=True, - text=True, - check=True - ) - print(f"Repository {repo_dir.name} cloned successfully") - return True - except subprocess.CalledProcessError as e: - print(f"Error cloning repository {repo_dir.name}: {e}", file=sys.stderr) - if e.stderr: - print(f"Git error: {e.stderr}", file=sys.stderr) - return False - except FileNotFoundError: - print(f"Git not found in system. Make sure git is installed.", file=sys.stderr) - return False - - -def clone_repositories(repos: Iterable[Tuple[str, str, str]]) -> None: - """Clone all repositories.""" - SOURCES_ROOT.mkdir(exist_ok=True) - - success_count = 0 - total_count = 0 - - for slug, title, repo_url in repos: - total_count += 1 - repo_dir = SOURCES_ROOT / slug - if clone_repository(repo_url, repo_dir): - success_count += 1 - - print(f"\nCloning completed: {success_count}/{total_count} repositories cloned successfully") - - -def update_repository(repo_dir: Path) -> bool: - """Update repository using git pull.""" - if not repo_dir.exists(): - print(f"Repository {repo_dir} does not exist, skipping update", file=sys.stderr) - return False - - if not (repo_dir / ".git").exists(): - print(f"Directory {repo_dir} is not a git repository, skipping update", file=sys.stderr) - return False - - try: - print(f"Updating repository: {repo_dir.name}") - result = subprocess.run( - ["git", "pull"], - cwd=repo_dir, - capture_output=True, - text=True, - check=True - ) - print(f"Repository {repo_dir.name} updated successfully") - if result.stdout.strip(): - print(f"Git pull output: {result.stdout.strip()}") - return True - except subprocess.CalledProcessError as e: - print(f"Error updating repository {repo_dir.name}: {e}", file=sys.stderr) - if e.stderr: - print(f"Git error: {e.stderr}", file=sys.stderr) - return False - except FileNotFoundError: - print(f"Git not found in system. Make sure git is installed.", file=sys.stderr) - return False - - -def update_repositories(repos: Iterable[Tuple[str, str, str]]) -> None: - """Update all repositories.""" - if not SOURCES_ROOT.exists(): - print(f"Directory {SOURCES_ROOT} does not exist. Create it and clone repositories.", file=sys.stderr) - return - - success_count = 0 - total_count = 0 - - for slug, title, repo_url in repos: - total_count += 1 - repo_dir = SOURCES_ROOT / slug - if update_repository(repo_dir): - success_count += 1 - - print(f"\nUpdate completed: {success_count}/{total_count} repositories updated successfully") - - -def reset_docs_root() -> None: - """Remove any previously generated documentation from this repository.""" - if not DOCS_ROOT.exists(): - DOCS_ROOT.mkdir(parents=True) - return - - for path in DOCS_ROOT.iterdir(): - if path.name in {".gitignore", *ASSET_PRESERVE}: - continue - if path.is_dir(): - shutil.rmtree(path) - else: - path.unlink() - - -def copytree(src: Path, dest: Path) -> None: - if dest.exists(): - shutil.rmtree(dest) - patterns = ["CONSOLE.md", "CONSOLE.*.md"] - if TRANSLATION_SUFFIXES: - patterns.extend([f"*{suffix}" for suffix in TRANSLATION_SUFFIXES]) - shutil.copytree(src, dest, ignore=shutil.ignore_patterns(*patterns)) - - -def copytree_to_root(src: Path, dest: Path) -> None: - """Copy tree to root directory, preserving existing files like .gitignore and assets.""" - # Copy files from src to dest without removing existing files - for item in src.iterdir(): - # Check if file should be ignored - should_ignore = False - - # Ignore CONSOLE.md and its translations - if item.name == "CONSOLE.md" or (item.name.startswith("CONSOLE.") and item.name.endswith(".md")): - should_ignore = True - - if not should_ignore and TRANSLATION_SUFFIXES: - for suffix in TRANSLATION_SUFFIXES: - if item.name.lower().endswith(suffix): - should_ignore = True - break - - if should_ignore: - continue - - dest_item = dest / item.name - - if item.is_file(): - # Copy file, overwriting existing - shutil.copy2(item, dest_item) - elif item.is_dir(): - # Copy directory - if dest_item.exists(): - shutil.rmtree(dest_item) - patterns = ["CONSOLE.md", "CONSOLE.*.md"] - if TRANSLATION_SUFFIXES: - patterns.extend([f"*{suffix}" for suffix in TRANSLATION_SUFFIXES]) - shutil.copytree(item, dest_item, ignore=shutil.ignore_patterns(*patterns)) - - -def ensure_index(doc_dir: Path) -> None: - """Make sure the directory contains an index file for MkDocs.""" - index_candidates = [doc_dir / f"index.{ext}" for ext in ("md", "markdown")] - if any(candidate.exists() for candidate in index_candidates): - return - - readme_candidates = [doc_dir / f"README.{ext}" for ext in ("md", "markdown")] - for readme in readme_candidates: - if readme.exists(): - readme.rename(doc_dir / "index.md") - return - - # Create a placeholder index to avoid MkDocs build failures. - (doc_dir / "index.md").write_text( - "# Documentation\n\n" - "This documentation set was imported automatically, but no index page was found." - "\n\nPlease add an `index.md` (or `README.md`) file to the upstream repository.\n", - encoding="utf-8", - ) - - -def process_content_lines(content_lines: list[str]) -> list[str]: - """Process content lines to remove configured sections and translation links.""" - processed_lines = [] - - # Get configuration - remove_headers = get_remove_headers() - skip_until_first_header = should_skip_until_first_header() - process_translation_links = should_process_translation_links() - - # Flags for content processing - skip_until_first_header_flag = skip_until_first_header - in_removable_section = False - current_removable_header = None - first_header_found = False - - for line in content_lines: - line_no_bom = line.lstrip("\ufeff") - stripped = line_no_bom.strip() - - # Skip empty lines and translation lines until first header - if skip_until_first_header_flag: - if stripped.startswith("#"): - first_header_found = True - skip_until_first_header_flag = False - processed_lines.append(line_no_bom.rstrip()) - continue - elif stripped == "" or (process_translation_links and TRANSLATION_LINK_PATTERN.fullmatch(stripped)): - continue - else: - # If not a header and not empty line, skip until first header - continue - - # Check for start of any removable section - if stripped in remove_headers: - in_removable_section = True - current_removable_header = stripped - continue - - # If in removable section, skip everything until next header - if in_removable_section: - if stripped.startswith("##") and stripped not in remove_headers: - in_removable_section = False - current_removable_header = None - # Don't add this line as we skip the entire removable section - continue - else: - continue - - # Process translation links (if enabled) - if process_translation_links: - if TRANSLATION_LINK_PATTERN.fullmatch(stripped): - continue - skip_line = False - for prefix in ("- ", "* ", "+ "): - if stripped.startswith(prefix): - candidate = stripped[len(prefix) :].strip() - if TRANSLATION_LINK_PATTERN.fullmatch(candidate): - skip_line = True - break - if skip_line: - continue - if TRANSLATION_LINK_PATTERN.search(line_no_bom): - without_links = TRANSLATION_LINK_PATTERN.sub("", line_no_bom) - trimmed = without_links.strip() - trimmed = trimmed.lstrip("-*+•·—–:| ").strip() - if not trimmed: - continue - if not any(char.isalnum() for char in trimmed): - continue - replaced_line = TRANSLATION_LINK_PATTERN.sub( - lambda match: match.group(1), line_no_bom - ) - processed_lines.append(replaced_line.rstrip()) - else: - processed_lines.append(line_no_bom.rstrip()) - else: - processed_lines.append(line_no_bom.rstrip()) - - return processed_lines - - -def apply_custom_metadata(doc_dir: Path) -> None: - """Apply custom metadata to markdown files based on configuration.""" - for md_file in doc_dir.rglob("*.md"): - if is_translation_filename(md_file.name): - continue - # Skip root index.md only if it's a redirect (contains meta refresh) - if md_file.name == "index.md" and md_file.parent == doc_dir: - content = md_file.read_text(encoding="utf-8") - if "meta http-equiv=\"refresh\"" in content or "window.location.replace" in content: - continue - - # Get relative path for metadata lookup - relative_path = md_file.relative_to(doc_dir) - file_path_str = str(relative_path).replace("\\", "/") - - # Apply custom metadata - original_content = md_file.read_text(encoding="utf-8") - updated_content = apply_metadata_to_content(original_content, file_path_str) - - if updated_content != original_content: - md_file.write_text(updated_content, encoding="utf-8") - - -def clean_hide_duplicates(content: str) -> str: - """Clean up duplicate hide entries in front matter.""" - if not content.startswith("---"): - return content - - parts = content.split("---", 2) - if len(parts) < 3: - return content - - front_matter = parts[1].strip() - content_after = parts[2].lstrip("\n") - - if "hide:" not in front_matter: - return content - - # Check if there are duplicates - nav_count = front_matter.count('- navigation') - toc_count = front_matter.count('- toc') - - if nav_count <= 1 and toc_count <= 1: - return content # No duplicates, return as is - - # Replace the entire hide section with a clean one - lines = front_matter.split('\n') - new_lines = [] - in_hide_section = False - hide_added = False - hide_values = set() # Track unique hide values to avoid duplicates - - for line in lines: - if line.strip().startswith('hide:'): - in_hide_section = True - new_lines.append(line) - if not hide_added: - # Only add if not already present - if '- navigation' not in hide_values: - new_lines.append(' - navigation') - hide_values.add('- navigation') - if '- toc' not in hide_values: - new_lines.append(' - toc') - hide_values.add('- toc') - hide_added = True - elif in_hide_section and line.startswith(' -'): - # Track existing hide entries to avoid duplicates - hide_value = line.strip() - if hide_value not in hide_values: - hide_values.add(hide_value) - new_lines.append(line) - # Skip duplicates - elif in_hide_section and not line.startswith(' '): - in_hide_section = False - new_lines.append(line) - elif not in_hide_section: - new_lines.append(line) - - new_front_matter = '\n'.join(new_lines) - return f"---\n{new_front_matter}\n---\n{content_after}" - - -def parse_badge_url(badge_url: str) -> tuple[str, str] | None: - """ - Parse shields.io badge URL to extract left and right text. - - Example: https://img.shields.io/badge/x64-Portable-0078D6?style=for-the-badge - Returns: ("x64", "Portable") - - Example: https://img.shields.io/badge/Online%20Installer-x64-0078D6 - Returns: ("Online Installer", "x64") - """ - # Extract the badge path (everything between /badge/ and the color or query params) - badge_match = re.search(r'/badge/([^?]+)', badge_url) - if not badge_match: - return None - - badge_text = badge_match.group(1) - - # Split by hyphen, but only the last occurrence before the color code - # Badge format: text1-text2-color where color is hex or color name - parts = badge_text.split('-') - - if len(parts) < 2: - return None - - # The last part is typically the color (hex code or color name) - # Check if last part looks like a color code (3 or 6 hex chars) - last_part_is_hex = ( - (len(parts[-1]) == 6 or len(parts[-1]) == 3) and - all(c in '0123456789ABCDEFabcdef' for c in parts[-1]) - ) - - if last_part_is_hex: - # Last part is color, take the one before as right text - if len(parts) < 3: - return None - right_text = parts[-2] - left_text = '-'.join(parts[:-2]) - else: - # No color in URL, split into two parts - right_text = parts[-1] - left_text = '-'.join(parts[:-1]) - - # Decode URL encoding - left_text = urllib.parse.unquote_plus(left_text.replace('%20', ' ')) - right_text = urllib.parse.unquote_plus(right_text.replace('%20', ' ')) - - return (left_text, right_text) - - -def replace_badge_with_divs(content: str) -> str: - """ - Replace badge elements with div elements containing parsed text. - - Converts: - ... - To: -
x64
Portable
- - Special case for "in progress": - ... - To: -
in progress
- """ - # Pattern to match img tags with shields.io badge URLs (more flexible) - # Matches any img tag that contains a shields.io badge URL in src attribute - img_pattern = r']*src="(https?://img\.shields\.io/badge/[^"]+)"[^>]*/?>' - - def replace_img(match): - # Get URL from group 1 - img_url = match.group(1) - parsed = parse_badge_url(img_url) - - if parsed: - left_text, right_text = parsed - - # Special case: if right text is "in progress", show only that - if right_text.lower() in ['in progress', 'in_progress', 'coming soon', 'coming_soon']: - return f'
{right_text}
' - - return f'
{left_text}
{right_text}
' - - # If parsing failed, return original - return match.group(0) - - return re.sub(img_pattern, replace_img, content) - - -def process_badge_images(doc_dir: Path) -> None: - """Process all markdown files to replace badge images with div elements.""" - for md_file in doc_dir.rglob("*.md"): - if is_translation_filename(md_file.name): - continue - - # Skip root index.md only if it's a redirect (contains meta refresh) - if md_file.name == "index.md" and md_file.parent == doc_dir: - content = md_file.read_text(encoding="utf-8") - if "meta http-equiv=\"refresh\"" in content or "window.location.replace" in content: - continue - - original_content = md_file.read_text(encoding="utf-8") - updated_content = replace_badge_with_divs(original_content) - - if updated_content != original_content: - md_file.write_text(updated_content, encoding="utf-8") - print(f"Processed badges in: {md_file.relative_to(doc_dir)}") - - -def add_screenshots_gallery(doc_dir: Path) -> None: - """Add screenshots gallery to index.md after first h1, before first h2.""" - index_file = doc_dir / "index.md" - - if not index_file.exists(): - return - - content = index_file.read_text(encoding="utf-8") - - # Skip if it's a redirect - if "meta http-equiv=\"refresh\"" in content or "window.location.replace" in content: - return - - # Check if gallery already exists - if 'class="screenshots-gallery"' in content: - return - - # Find the position after first h1, before first h2 - lines = content.split('\n') - insert_position = None - h1_found = False - - for i, line in enumerate(lines): - stripped = line.strip() - if stripped.startswith('# ') and not stripped.startswith('##'): - h1_found = True - elif h1_found and stripped.startswith('##'): - # Found first h2 after h1, insert before it - insert_position = i - break - - if insert_position is None: - # If no h2 found, append at the end - insert_position = len(lines) - - # Check which thumbnails exist, use full images as fallback - images_dir = doc_dir / "assets" / "images" - screenshots = [ - ( - "screenshot.png", - "screenshot_thumb.png", - "Angry Data Scanner - Main interface showing sensitive data discovery tool", - "View main interface screenshot of Angry Data Scanner sensitive data discovery tool" - ), - ( - "screenshot_2.png", - "screenshot_2_thumb.png", - "Angry Data Scanner - Scanning results and data detection interface", - "View scanning results screenshot showing detected sensitive data" - ), - ( - "screenshot_3.png", - "screenshot_3_thumb.png", - "Angry Data Scanner - File scanning and PII detection view", - "View file scanning interface with PII detection capabilities" - ), - ( - "screenshot_4.png", - "screenshot_4_thumb.png", - "Angry Data Scanner - Advanced scanning features and configuration", - "View advanced scanning features and configuration options" - ), - ] - - gallery_items = [] - for full_img, thumb_img, alt_text, title_text in screenshots: - full_path = images_dir / full_img - thumb_path = images_dir / thumb_img - - # Use absolute paths starting with / to work on all language versions - # Use thumbnail if exists, otherwise use full image - if thumb_path.exists(): - img_src = f"/assets/images/{thumb_img}" - elif full_path.exists(): - img_src = f"/assets/images/{full_img}" - else: - continue # Skip if neither exists - - full_img_path = f"/assets/images/{full_img}" - gallery_items.append(f'''
- - {alt_text} - -
''') - - if not gallery_items: - return # No screenshots found, skip gallery - - # Create gallery HTML - gallery_html = f''' - - - - -''' - - # Insert gallery before the h2 - lines.insert(insert_position, gallery_html) - - updated_content = '\n'.join(lines) - index_file.write_text(updated_content, encoding="utf-8") - print(f"Added screenshots gallery to index.md") - - -def sanitize_translation_links(doc_dir: Path) -> None: - """Strip links that point to translation markdown files and remove Direct Download section.""" - for md_file in doc_dir.rglob("*.md"): - if is_translation_filename(md_file.name): - continue - # Skip root index.md only if it's a redirect (contains meta refresh) - if md_file.name == "index.md" and md_file.parent == doc_dir: - content = md_file.read_text(encoding="utf-8") - if "meta http-equiv=\"refresh\"" in content or "window.location.replace" in content: - continue - original = md_file.read_text(encoding="utf-8") - lines: list[str] = [] - modified = False - - # First, clean up any existing duplicates - cleaned_content = clean_hide_duplicates(original) - if cleaned_content != original: - original = cleaned_content - modified = True - - # Check if front matter already exists - has_front_matter = original.startswith("---") - if has_front_matter: - # Extract existing front matter - parts = original.split("---", 2) - if len(parts) >= 3: - front_matter = parts[1].strip() - content = parts[2].lstrip("\n") - - # Add hide: navigation and toc if not present - if "hide:" not in front_matter: - front_matter += "\nhide:\n - navigation\n - toc" - modified = True - else: - # Check if hide: exists but has no values (just "hide:" or "hide: ") - hide_pattern = r'hide:\s*$' - import re - if re.search(hide_pattern, front_matter, re.MULTILINE): - # Replace empty hide with proper values - front_matter = re.sub(hide_pattern, "hide:\n - navigation\n - toc", front_matter, flags=re.MULTILINE) - modified = True - else: - # If hide already exists with values, check if navigation and toc are present - nav_count = front_matter.count('- navigation') - toc_count = front_matter.count('- toc') - - if nav_count == 0 or toc_count == 0: - # Parse hide section and add missing values properly - lines = front_matter.split('\n') - new_lines = [] - in_hide_section = False - hide_added = False - - for line in lines: - if line.strip().startswith('hide:'): - in_hide_section = True - new_lines.append(line) - if not hide_added: - if nav_count == 0: - new_lines.append(' - navigation') - if toc_count == 0: - new_lines.append(' - toc') - hide_added = True - elif in_hide_section and line.startswith(' -'): - # Skip existing hide entries to avoid duplicates - continue - elif in_hide_section and not line.startswith(' '): - in_hide_section = False - new_lines.append(line) - elif not in_hide_section: - new_lines.append(line) - - front_matter = '\n'.join(new_lines) - modified = True - - lines = [f"---\n{front_matter}\n---\n{content}"] - else: - lines = original.splitlines() - else: - # Add front matter with hide: navigation and toc - lines = ["---", "hide:", " - navigation", " - toc", "---", ""] + original.splitlines() - modified = True - - # Process the rest as before - if not has_front_matter or modified: - if not has_front_matter: - # If front matter was added, take content after it - content_lines = lines[5:] # Skip added front matter (including empty line) - else: - # If front matter already existed, take all content - content_lines = lines - - processed_lines = process_content_lines(content_lines) - - if not has_front_matter: - # Combine added front matter with processed content - lines = lines[:5] + processed_lines # Include empty line after front matter - else: - # Replace only content, preserving front matter - lines = lines[:4] + processed_lines - - if modified or not has_front_matter: - final_content = "\n".join(lines).rstrip() + "\n" - # Final cleanup to ensure no duplicates remain - cleaned_final = clean_hide_duplicates(final_content) - md_file.write_text(cleaned_final, encoding="utf-8") - - -def find_doc_root(repo_dir: Path) -> Path | None: - candidates = [ - repo_dir / "docs", - repo_dir / "documentation", - repo_dir / "doc", - ] - for candidate in candidates: - if candidate.exists(): - return candidate - readme = repo_dir / "README.md" - if readme.exists(): - temp_dir = repo_dir / ".aggregated-docs" - temp_dir.mkdir(exist_ok=True) - shutil.copy2(readme, temp_dir / "index.md") - return temp_dir - return None - - -def sync_repo(repo_slug: str, title: str, repo_url: str) -> None: - repo_dir = SOURCES_ROOT / repo_slug - if not repo_dir.exists(): - print(f"Repository {repo_slug} not found, attempting to clone...") - SOURCES_ROOT.mkdir(exist_ok=True) - if not clone_repository(repo_url, repo_dir): - raise FileNotFoundError( - f"Failed to clone repository {repo_slug} from {repo_url}." - ) - - doc_root = find_doc_root(repo_dir) - if doc_root is None: - raise FileNotFoundError( - f"Could not locate documentation inside {repo_slug}." - " Provide a docs/ directory or README.md." - ) - - has_inline_index = any((doc_root / f"index.{ext}").exists() for ext in ("md", "markdown")) - has_inline_readme = any((doc_root / f"README.{ext}").exists() for ext in ("md", "markdown")) - repo_readme = repo_dir / "README.md" - should_inject_repo_readme = not has_inline_index and not has_inline_readme and repo_readme.exists() - - # For angrydata-app copy content to docs root, for others - to subdirectories - if repo_slug == "angrydata-app": - destination = DOCS_ROOT - copytree_to_root(doc_root, destination) - else: - destination = DOCS_ROOT / repo_slug - copytree(doc_root, destination) - - if should_inject_repo_readme: - content = repo_readme.read_text(encoding="utf-8") - doc_prefix = f"{doc_root.name}/" - link_prefix_pattern = re.compile(rf"(\[[^\]]+\]\()({re.escape(doc_prefix)})([^)]+)\)") - content = content.replace("(README.md", "(index.md") - content = link_prefix_pattern.sub(r"\1\3)", content) - destination.joinpath("index.md").write_text(content, encoding="utf-8") - else: - ensure_index(destination) - - sanitize_translation_links(destination) - - # Apply custom metadata - apply_custom_metadata(destination) - - # Process badge images - process_badge_images(destination) - - temp_dir = repo_dir / ".aggregated-docs" - if temp_dir.exists(): - shutil.rmtree(temp_dir) - - -def copy_static_files() -> None: - """Copy static files from scripts/static and scripts/static_html to docs root.""" - # Copy from scripts/static - static_dir = ROOT / "scripts" / "static" - if static_dir.exists(): - for static_file in static_dir.iterdir(): - if static_file.is_file(): - dest_file = DOCS_ROOT / static_file.name - shutil.copy2(static_file, dest_file) - print(f"Copied {static_file.name} to docs root") - else: - print(f"Warning: Static directory {static_dir} does not exist", file=sys.stderr) - - # Copy from scripts/static_html - static_html_dir = ROOT / "scripts" / "static_html" - if static_html_dir.exists(): - for static_file in static_html_dir.iterdir(): - if static_file.is_file(): - dest_file = DOCS_ROOT / static_file.name - shutil.copy2(static_file, dest_file) - print(f"Copied {static_file.name} to docs root") - else: - print(f"Warning: Static HTML directory {static_html_dir} does not exist", file=sys.stderr) - - -def sync(repos: Iterable[Tuple[str, str, str]]) -> None: - reset_docs_root() - for slug, title, repo_url in repos: - sync_repo(slug, title, repo_url) - - # For angrydata-app in root, don't create redirect as content is already in root - # Main content of angrydata-app is already copied to root, so no need to create redirect - - sanitize_translation_links(DOCS_ROOT) - - # Apply custom metadata to all documentation - apply_custom_metadata(DOCS_ROOT) - - # Process badge images in all documentation - process_badge_images(DOCS_ROOT) - - # Add screenshots gallery to index.md - add_screenshots_gallery(DOCS_ROOT) - - # Copy static files (robots.txt, BingSiteAuth.xml, etc.) - copy_static_files() - - -def main() -> None: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--update-repos", - action="store_true", - help="Update repositories before syncing documentation" - ) - parser.add_argument( - "--only-update", - action="store_true", - help="Only update repositories without syncing documentation" - ) - parser.add_argument( - "--clone-repos", - action="store_true", - help="Clone repositories before syncing documentation" - ) - parser.add_argument( - "--only-clone", - action="store_true", - help="Only clone repositories without syncing documentation" - ) - args = parser.parse_args() - - if args.only_clone: - print("Cloning repositories...") - clone_repositories(REPOSITORIES) - elif args.only_update: - print("Updating repositories...") - update_repositories(REPOSITORIES) - elif args.clone_repos: - print("Cloning repositories and syncing documentation...") - clone_repositories(REPOSITORIES) - sync(REPOSITORIES) - elif args.update_repos: - print("Updating repositories and syncing documentation...") - update_repositories(REPOSITORIES) - sync(REPOSITORIES) - else: - print("Syncing documentation...") - sync(REPOSITORIES) - - -if __name__ == "__main__": - main() diff --git a/scripts/translate_docs.py b/scripts/translate_docs.py deleted file mode 100644 index 6b0e610..0000000 --- a/scripts/translate_docs.py +++ /dev/null @@ -1,1691 +0,0 @@ -#!/usr/bin/env python3 -"""Generate machine translations of aggregated documentation.""" - -from __future__ import annotations - -import argparse -import asyncio -import contextlib -import time -from collections import defaultdict -from concurrent.futures import ThreadPoolExecutor -from pathlib import Path -from typing import Iterable, Iterator, List, Tuple -import aiofiles - -from deep_translator import GoogleTranslator -from tqdm.asyncio import tqdm - -from config_utils import get_i18n_languages, get_translation_locales, update_mkdocs_alternate_menu, update_menu_translations_json -import yaml - -ROOT = Path(__file__).resolve().parents[1] -DOCS_ROOT = ROOT / "docs" -TRANSLATION_CONFIG_PATH = ROOT / "scripts" / "translation_config.yaml" - -LANGUAGE_METADATA = get_i18n_languages() -TRANSLATION_LOCALES = get_translation_locales() - -# For folder structure, we don't use suffixes -TRANSLATION_SUFFIXES = {locale: f".{locale}.md" for locale in TRANSLATION_LOCALES} - -IGNORED_NAMES = {".gitignore", ".pages"} -# Add language folders to ignored directories -IGNORED_DIRS = {".git", "__pycache__", "assets"} | set(TRANSLATION_LOCALES) -FENCE_PREFIX = "```" - - -def load_translation_exclusions() -> dict: - """Load translation exclusions from config file.""" - # No translation exclusions config file available - return {} - - -def get_exclusion_config(): - """Get cached exclusion configuration.""" - if not hasattr(get_exclusion_config, '_config'): - get_exclusion_config._config = load_translation_exclusions() - return get_exclusion_config._config - - -def load_translation_config() -> dict: - """Load translation configuration from translation_config.yaml.""" - try: - with open(TRANSLATION_CONFIG_PATH, 'r', encoding='utf-8') as f: - config = yaml.safe_load(f) - return config or {} - except (FileNotFoundError, yaml.YAMLError) as e: - print(f"Warning: Could not load translation config: {e}") - return {} - - -def load_metadata_config() -> dict: - """Load metadata configuration from translation_config.yaml.""" - config = load_translation_config() - return config.get('metadata', {}) - - -def get_metadata_config(): - """Get cached metadata configuration.""" - if not hasattr(get_metadata_config, '_config'): - get_metadata_config._config = load_metadata_config() - return get_metadata_config._config - - -def get_metadata_for_file(file_path: Path, language: str = None) -> dict: - """ - Get metadata (title, description) for a specific file and language. - - Args: - file_path: Path to the markdown file - language: Target language code (e.g., 'ru', 'de'). If None, returns default metadata. - - Returns: - Dictionary with 'title' and 'description' keys, or empty dict if not configured. - """ - metadata_config = get_metadata_config() - - if not metadata_config.get('enabled', False): - return {} - - # Get relative path from docs root - try: - rel_path = file_path.relative_to(DOCS_ROOT) - file_key = str(rel_path).replace('\\', '/') - except ValueError: - # File is not in docs root - return {} - - files_config = metadata_config.get('files', {}) - - if file_key not in files_config: - return {} - - file_metadata = files_config[file_key] - - # If language is specified, check for explicit translation - if language and 'translations' in file_metadata: - lang_metadata = file_metadata.get('translations', {}).get(language, {}) - if lang_metadata: - # Return explicit translation for this language - return { - 'title': lang_metadata.get('title'), - 'description': lang_metadata.get('description') - } - else: - # Language specified but no explicit translation - return empty dict - # This signals that default metadata should be auto-translated - return {} - - # Return default metadata (for original files or when language is None) - return { - 'title': file_metadata.get('title'), - 'description': file_metadata.get('description') - } - - -def get_file_key(file_path: Path) -> str | None: - """Get file key (relative path) for config lookup.""" - try: - rel_path = file_path.relative_to(DOCS_ROOT) - return str(rel_path).replace('\\', '/') - except ValueError: - return None - - -def get_table_config_for_file(file_path: Path) -> List[dict]: - """Get table translation configuration for a specific file.""" - config = load_translation_config() - if not config: - return [] - translation_config = config.get('translation', {}) - if not translation_config: - return [] - tables_config = translation_config.get('tables', {}) - if not tables_config: - return [] - files_config = tables_config.get('files', {}) - if not files_config: - return [] - - file_key = get_file_key(file_path) - if not file_key or file_key not in files_config: - return [] - - result = files_config[file_key] - # Ensure result is a list and filter out None values - if not isinstance(result, list): - return [] - return [cfg for cfg in result if cfg is not None] - - -def get_header_config_for_file(file_path: Path) -> List[dict]: - """Get header translation configuration for a specific file.""" - config = load_translation_config() - if not config: - return [] - translation_config = config.get('translation', {}) - if not translation_config: - return [] - headers_config = translation_config.get('headers', {}) - if not headers_config: - return [] - files_config = headers_config.get('files', {}) - if not files_config: - return [] - - file_key = get_file_key(file_path) - if not file_key or file_key not in files_config: - return [] - - result = files_config[file_key] - # Ensure result is a list and filter out None values - if not isinstance(result, list): - return [] - return [cfg for cfg in result if cfg is not None] - - -def get_text_config_for_file(file_path: Path) -> List[dict]: - """Get static text translation configuration for a specific file.""" - config = load_translation_config() - if not config: - return [] - translation_config = config.get('translation', {}) - if not translation_config: - return [] - texts_config = translation_config.get('texts', {}) - if not texts_config: - return [] - files_config = texts_config.get('files', {}) - if not files_config: - return [] - - file_key = get_file_key(file_path) - if not file_key or file_key not in files_config: - return [] - - result = files_config[file_key] - if not isinstance(result, list): - return [] - return [cfg for cfg in result if cfg is not None] - - -def get_retry_config() -> dict: - """Get retry configuration from translation config.""" - config = load_translation_config() - translation_config = config.get('translation', {}) - retry_config = translation_config.get('retry', {}) - return { - 'max_attempts': retry_config.get('max_attempts', 3), - 'delay_seconds': retry_config.get('delay_seconds', 2) - } - - -def retry_translate(translator: GoogleTranslator, text: str, context: str = "") -> str: - """ - Translate text with retry logic on failure. - - Args: - translator: GoogleTranslator instance - text: Text to translate - context: Optional context string for error messages - - Returns: - Translated text, or original text if all attempts fail - """ - retry_config = get_retry_config() - max_attempts = retry_config['max_attempts'] - delay_seconds = retry_config['delay_seconds'] - - last_exception = None - for attempt in range(1, max_attempts + 1): - try: - result = translator.translate(text) - if result is not None: - return result - except Exception as e: - last_exception = e - if attempt < max_attempts: - context_msg = f" ({context})" if context else "" - print(f"Warning: Translation attempt {attempt}/{max_attempts} failed{context_msg}: {e}") - print(f"Retrying in {delay_seconds} seconds...") - time.sleep(delay_seconds) - else: - context_msg = f" ({context})" if context else "" - print(f"Warning: Translation failed after {max_attempts} attempts{context_msg}: {e}") - - # If all attempts failed, return original text - return text - -# Async configuration -MAX_CONCURRENT_TRANSLATIONS = 10 # Increased for parallel language translation -MAX_CONCURRENT_FILES = 2 # Reduced since each file now processes multiple languages in parallel -TRANSLATION_DELAY = 0.05 # Reduced delay since we have better concurrency control - -# Protected terms that should not be translated -PROTECTED_TERMS = [ - "Angry Data Scanner", - "Angry Data Core", - "AngryScan", - "angryscan.org", - "packetdima", - "datascanner", - "Login", - "Connector" -] - -# CSS classes and HTML attributes that should not be translated -PROTECTED_CSS_CLASSES = [ - "release-info", - "release-date", - "os-header", - "windows", - "linux", - "apple" -] - - -def iter_markdown_files(root: Path) -> Iterator[Path]: - """Iterate over markdown files in the root directory, excluding translations.""" - for path in root.rglob("*.md"): - # Skip files with translation suffixes (legacy) - if any(path.name.endswith(suffix) for suffix in TRANSLATION_SUFFIXES.values()): - continue - if path.name in IGNORED_NAMES: - continue - # Skip files in ignored directories (including language folders) - if any(part in IGNORED_DIRS for part in path.parts): - continue - # Only process files directly in docs root or its subdirectories (not in language folders) - try: - rel_path = path.relative_to(root) - # Check if the first directory component is a language code - if len(rel_path.parts) > 1 and rel_path.parts[0] in TRANSLATION_LOCALES: - continue - except ValueError: - continue - yield path - - -def split_front_matter(content: str) -> tuple[str | None, str]: - if content.startswith("---\n"): - end = content.find("\n---", 4) - if end != -1: - end += len("\n---") - return content[:end], content[end:].lstrip("\n") - return None, content - - -def add_metadata_to_front_matter(front_matter: str, metadata: dict, lang: str = None, - translator: GoogleTranslator = None, file_path: Path = None) -> str: - """ - Add title and description to front matter if they don't exist. - - Args: - front_matter: Existing front matter string - metadata: Metadata dict with title/description - lang: Target language code - translator: Translator instance (for auto-translation if needed) - file_path: Path to file (for language-specific metadata lookup) - - Returns: - Updated front matter with title/description added - """ - if not metadata: - return front_matter - - # Check if title/description already exist - has_title = 'title:' in front_matter - has_description = 'description:' in front_matter - - if has_title and has_description: - return front_matter - - # Get language-specific metadata if available - lang_metadata = {} - if lang and file_path: - lang_metadata = get_metadata_for_file(file_path, lang) - - # Parse front matter lines - lines = front_matter.split('\n') - new_lines = [] - title_added = has_title - description_added = has_description - - # Find where to insert (after first ---, before last ---) - first_dash_idx = -1 - last_dash_idx = -1 - - for i, line in enumerate(lines): - if line.strip() == '---': - if first_dash_idx == -1: - first_dash_idx = i - last_dash_idx = i - - # Build new front matter - for i, line in enumerate(lines): - new_lines.append(line) - - # Add title after first --- if not present - if i == first_dash_idx and not title_added: - title = None - if lang_metadata and lang_metadata.get('title'): - title = lang_metadata['title'] - elif metadata.get('title'): - title = metadata['title'] - # Translate if no language-specific version - if translator and (not lang_metadata or not lang_metadata.get('title')): - title = retry_translate(translator, title, "metadata title") - - if title: - new_lines.append(f"title: {title}") - title_added = True - - # Add description after title (or after first --- if no title) - if (line.strip().startswith('title:') or (i == first_dash_idx and title_added)) and not description_added: - description = None - if lang_metadata and lang_metadata.get('description'): - description = lang_metadata['description'] - elif metadata.get('description'): - description = metadata['description'] - # Translate if no language-specific version - if translator and (not lang_metadata or not lang_metadata.get('description')): - description = retry_translate(translator, description, "metadata description") - - if description: - # Insert after current line (title) or after first --- - insert_idx = len(new_lines) - new_lines.insert(insert_idx, f"description: {description}") - description_added = True - - # If still not added, add before last --- - if not title_added and metadata.get('title'): - title = None - if lang_metadata and lang_metadata.get('title'): - title = lang_metadata['title'] - elif metadata.get('title'): - title = metadata['title'] - if translator and (not lang_metadata or not lang_metadata.get('title')): - title = retry_translate(translator, title, "metadata title") - - if title and last_dash_idx >= 0: - new_lines.insert(last_dash_idx, f"title: {title}") - title_added = True - - if not description_added and metadata.get('description'): - description = None - if lang_metadata and lang_metadata.get('description'): - description = lang_metadata['description'] - elif metadata.get('description'): - description = metadata['description'] - if translator and (not lang_metadata or not lang_metadata.get('description')): - description = retry_translate(translator, description, "metadata description") - - if description and last_dash_idx >= 0: - # Find title to insert after it, or insert before last --- - title_idx = -1 - for i, line in enumerate(new_lines): - if line.strip().startswith('title:'): - title_idx = i - break - - if title_idx >= 0: - new_lines.insert(title_idx + 1, f"description: {description}") - else: - new_lines.insert(last_dash_idx, f"description: {description}") - - return '\n'.join(new_lines) - - -def translate_front_matter(front_matter: str, translator: GoogleTranslator, - file_path: Path = None, target_lang: str = None) -> str: - """ - Translate title and description in front matter YAML. - - Args: - front_matter: The front matter YAML content - translator: GoogleTranslator instance for translation - file_path: Path to the source file (for metadata override lookup) - target_lang: Target language code (for metadata override lookup) - - Returns: - Translated front matter YAML content - """ - if not front_matter: - return front_matter - - import re - - # Get metadata overrides if available - metadata_overrides = {} - if file_path and target_lang: - metadata_overrides = get_metadata_for_file(file_path, target_lang) - - # Parse YAML-like content to find title and description - lines = front_matter.split('\n') - translated_lines = [] - - for line in lines: - # Check if this line contains title or description - if line.strip().startswith('title:') or line.strip().startswith('description:'): - # Extract the key and value - match = re.match(r'^(\s*)(title|description):\s*(.+)$', line) - if match: - indent = match.group(1) - key = match.group(2) - value = match.group(3).strip() - - # Check if we have an override for this field - override_value = metadata_overrides.get(key) - - if override_value: - # Use the override value directly (no translation needed) - translated_value = override_value - else: - # Remove quotes if present - if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")): - value = value[1:-1] - - # Protect terms before translation - protected_value, protected_mapping = protect_terms(value) - translated_value = retry_translate(translator, protected_value, f"front matter {key}") - # Restore protected terms after translation - translated_value = restore_terms(translated_value, protected_mapping) - - # Re-add quotes if the original had them - if (match.group(3).strip().startswith('"') and match.group(3).strip().endswith('"')) or \ - (match.group(3).strip().startswith("'") and match.group(3).strip().endswith("'")): - translated_value = f'"{translated_value}"' - - translated_line = f"{indent}{key}: {translated_value}" - translated_lines.append(translated_line) - else: - translated_lines.append(line) - else: - translated_lines.append(line) - - return '\n'.join(translated_lines) - - -def protect_backticks(text: str) -> tuple[str, dict]: - """Replace content in backticks with placeholders and return mapping.""" - import re - protected_mapping = {} - protected_text = text - - # Find all content in backticks (but not code blocks) - pattern = r'`([^`]*)`' - matches = re.finditer(pattern, protected_text) - - for i, match in enumerate(matches): - backtick_content = match.group(1) - if backtick_content.strip(): # Only protect non-empty content - placeholder = f"__PROTECTED_BACKTICK_{i}__" - protected_mapping[placeholder] = f"`{backtick_content}`" - protected_text = protected_text.replace(f"`{backtick_content}`", placeholder) - - return protected_text, protected_mapping - - -def protect_terms(text: str) -> tuple[str, dict]: - """Replace protected terms with placeholders and return mapping.""" - protected_mapping = {} - protected_text = text - - # Get exclusion configuration - exclusion_config = get_exclusion_config() - - # Protect excluded column placeholders first (before other protections) - import re - # Match placeholders - use non-greedy match to get the shortest possible match - # Pattern: __EXCLUDE_COL_____EXCLUDE_COL___ - # We need to match the content between the markers, which should not contain the closing marker - exclude_col_pattern = r'__EXCLUDE_COL_(\d+)__(.*?)__EXCLUDE_COL_\1__' - exclude_col_matches = list(re.finditer(exclude_col_pattern, protected_text)) - # Process matches in reverse order to preserve indices when replacing - for i, match in enumerate(reversed(exclude_col_matches)): - placeholder = f"__PROTECTED_EXCLUDE_COL_{len(exclude_col_matches) - 1 - i}__" - full_match = match.group(0) - protected_mapping[placeholder] = full_match - # Replace from end to start to preserve positions - start, end = match.span() - protected_text = protected_text[:start] + placeholder + protected_text[end:] - - # Protect content in backticks - protected_text, backtick_mapping = protect_backticks(protected_text) - protected_mapping.update(backtick_mapping) - - # Protect regular terms - for i, term in enumerate(PROTECTED_TERMS): - placeholder = f"__PROTECTED_TERM_{i}__" - if term in protected_text: - protected_mapping[placeholder] = term - protected_text = protected_text.replace(term, placeholder) - - # Protect CSS classes from config - css_classes = exclusion_config.get('css_classes') - if css_classes is None: - css_classes = [] - for i, css_class in enumerate(css_classes): - placeholder = f"__PROTECTED_CSS_{i}__" - if css_class in protected_text: - protected_mapping[placeholder] = css_class - protected_text = protected_text.replace(css_class, placeholder) - - # Protect text patterns from config - text_patterns = exclusion_config.get('text_patterns') - if text_patterns is None: - text_patterns = [] - for i, pattern in enumerate(text_patterns): - placeholder = f"__PROTECTED_PATTERN_{i}__" - if pattern in protected_text: - protected_mapping[placeholder] = pattern - protected_text = protected_text.replace(pattern, placeholder) - - return protected_text, protected_mapping - - -def restore_terms(text: str, protected_mapping: dict) -> str: - """Restore protected terms from placeholders.""" - restored_text = text - # Restore in reverse order to avoid conflicts (exclude_col placeholders first) - # Sort by placeholder name to ensure consistent order - sorted_items = sorted(protected_mapping.items(), key=lambda x: x[0], reverse=True) - for placeholder, original_term in sorted_items: - restored_text = restored_text.replace(placeholder, original_term) - return restored_text - - -def is_table_line(line: str) -> bool: - """Check if a line is part of a markdown table.""" - stripped = line.strip() - # Table line should start and end with | or contain | with proper spacing - return stripped.startswith('|') and stripped.endswith('|') and '|' in stripped[1:-1] - - -def parse_table_line(line: str) -> List[str]: - """Parse a table line into columns.""" - # Split by | and strip whitespace, remove empty first/last if they exist - parts = [part.strip() for part in line.split('|')] - # Remove empty parts at start/end (from leading/trailing |) - if parts and not parts[0]: - parts = parts[1:] - if parts and not parts[-1]: - parts = parts[:-1] - return parts - - -def find_table_ranges(lines: List[str]) -> List[Tuple[int, int]]: - """ - Find all table ranges in lines. - Returns list of (start_index, end_index) tuples. - """ - table_ranges = [] - i = 0 - while i < len(lines): - if is_table_line(lines[i]): - start = i - # Find the end of the table - i += 1 - while i < len(lines) and is_table_line(lines[i]): - i += 1 - end = i - # Only include if we have at least 2 lines (header + separator) - if end - start >= 2: - table_ranges.append((start, end)) - else: - i += 1 - return table_ranges - - -def find_headers_with_numbers(lines: List[str]) -> Tuple[dict, dict, dict]: - """ - Find all h1, h2, and h3 headers and assign numbers. - Returns (h1_dict, h2_dict, h3_dict) where: - - h1_dict: {number: (index, text)} - - h2_dict: {h1_number: {h2_number: (index, text)}} - - h3_dict: {h1_number: {h2_number: {h3_number: (index, text)}}} - """ - h1_dict = {} - h2_dict = {} - h3_dict = {} - h1_counter = 0 - h2_counters = {} # Track h2 counters per h1 - h3_counters = {} # Track h3 counters per (h1, h2) - current_h1 = 0 - current_h2_map = {} # Track current h2 number per h1 - - for i, line in enumerate(lines): - stripped = line.strip() - if stripped.startswith('# '): - h1_counter += 1 - h1_dict[h1_counter] = (i, stripped) - h2_counters[h1_counter] = 0 - h3_counters[h1_counter] = {} - current_h1 = h1_counter - current_h2_map[current_h1] = 0 - elif stripped.startswith('## '): - # Find which h1 this h2 belongs to - if current_h1 > 0: - h2_counters[current_h1] += 1 - h2_number = h2_counters[current_h1] - if current_h1 not in h2_dict: - h2_dict[current_h1] = {} - h2_dict[current_h1][h2_number] = (i, stripped) - if current_h1 not in h3_dict: - h3_dict[current_h1] = {} - h3_dict[current_h1][h2_number] = {} - h3_counters[current_h1][h2_number] = 0 - current_h2_map[current_h1] = h2_number - elif stripped.startswith('### '): - if current_h1 > 0: - current_h2 = current_h2_map.get(current_h1, 0) - if current_h2 > 0: - if current_h1 not in h3_counters: - h3_counters[current_h1] = {} - if current_h2 not in h3_counters[current_h1]: - h3_counters[current_h1][current_h2] = 0 - h3_counters[current_h1][current_h2] += 1 - h3_number = h3_counters[current_h1][current_h2] - if current_h1 not in h3_dict: - h3_dict[current_h1] = {} - if current_h2 not in h3_dict[current_h1]: - h3_dict[current_h1][current_h2] = {} - h3_dict[current_h1][current_h2][h3_number] = (i, stripped) - - return h1_dict, h2_dict, h3_dict - - -def get_table_config_by_header(table_configs: List[dict], header_text: str) -> dict | None: - """Find table config by matching header text.""" - for config in table_configs: - if config.get('match_by_header') == header_text: - return config - return None - - -def get_table_config_by_number(table_configs: List[dict], table_number: int) -> dict | None: - """Find table config by table number.""" - for config in table_configs: - if config.get('table_number') == table_number: - return config - return None - - -def get_header_translation(header_configs: List[dict], header_text: str, header_level: int, - h1_number: int | None = None, h2_number: int | None = None, - h3_number: int | None = None, - language: str = None) -> str | None: - """ - Get manual translation for a header. - - Args: - header_configs: List of header configs from file - header_text: The header text to match - header_level: 1 for h1, 2 for h2, 3 for h3 - h1_number: H1 number (for h2/h3 identification) - h2_number: H2 number within parent h1 (for h3 identification) - h3_number: H3 number within parent h2 (for identification) - language: Target language code - - Returns: - Translated header text or None if not found - """ - if not language: - return None - - for config in header_configs: - if config.get('level') != header_level: - continue - - # Check if this config matches - matched = False - - if header_level == 1: - # H1: match by text or number - if 'text' in config and config['text'] == header_text: - matched = True - elif 'number' in config and h1_number: - if config.get('number') == h1_number: - matched = True - elif header_level == 2: - # H2: match by text or by parent_h1_number + number - if 'text' in config and config['text'] == header_text: - matched = True - elif h1_number and h2_number: - if config.get('parent_h1_number') == h1_number and config.get('number') == h2_number: - matched = True - elif header_level == 3: - if 'text' in config and config['text'] == header_text: - matched = True - else: - number = config.get('number') - if number is not None: - parent_h1 = config.get('parent_h1_number') - parent_h2 = config.get('parent_h2_number') - if ((parent_h1 is None or parent_h1 == h1_number) and - (parent_h2 is None or parent_h2 == h2_number) and - number == h3_number): - matched = True - - if matched and 'translations' in config: - return config['translations'].get(language) - - return None - - -def process_table_line(line: str, exclude_columns: List[int] = None, - exclude_header: bool = False, is_header_row: bool = False) -> str: - """ - Process a table line by excluding specified columns from translation. - - Args: - line: The table line to process - exclude_columns: List of column indices (1-based) to exclude - exclude_header: Whether to exclude the header row from translation - is_header_row: Whether this is the header row - - Returns: - Processed line with excluded columns replaced by placeholders - """ - if not exclude_columns or exclude_columns is None: - return line - - # Always process columns if exclude_columns is specified, regardless of is_header_row - # exclude_header only affects whether the header row gets translated, not column exclusion - - columns = parse_table_line(line) - if not columns: - return line - - # Create placeholders for excluded columns - processed_columns = [] - for i, col in enumerate(columns): - col_index = i + 1 # 1-based index - if exclude_columns and col_index in exclude_columns: - # Replace with placeholder that won't be translated - placeholder = f"__EXCLUDE_COL_{col_index}__{col}__EXCLUDE_COL_{col_index}__" - processed_columns.append(placeholder) - else: - processed_columns.append(col) - - # Reconstruct table line - return '| ' + ' | '.join(processed_columns) + ' |' - - -def restore_table_line(line: str) -> str: - """Restore excluded columns in a table line after translation.""" - import re - # Pattern: __EXCLUDE_COL_N__content__EXCLUDE_COL_N__ - # Use non-greedy match to get the shortest possible match - # The pattern should match: __EXCLUDE_COL_____EXCLUDE_COL___ - pattern = r'__EXCLUDE_COL_(\d+)__(.*?)__EXCLUDE_COL_\1__' - - def replace_placeholder(match): - col_num = match.group(1) - content = match.group(2) - return content - - result = re.sub(pattern, replace_placeholder, line) - return result - - -def translate_blocks(text: str, translator: GoogleTranslator, file_path: Path = None, language: str = None) -> str: - """ - Translate markdown blocks with support for table/column exclusions and manual header translations. - - Args: - text: The text to translate - translator: GoogleTranslator instance - file_path: Path to the source file (for config lookup) - language: Target language code (for header translations) - """ - lines = text.splitlines() - original_lines = lines.copy() - - # Get configurations for tables, headers, and static texts - table_configs = get_table_config_for_file(file_path) if file_path else [] - header_configs = get_header_config_for_file(file_path) if file_path else [] - text_configs = get_text_config_for_file(file_path) if file_path else [] - - # Find headers with numbers for identification - h1_dict, h2_dict, h3_dict = find_headers_with_numbers(lines) - - # Find table ranges - table_ranges = find_table_ranges(lines) - - # Map table numbers and find preceding headers for each table - table_info = {} # {table_index: (table_number, preceding_header, config)} - table_counter = 0 - for start, end in table_ranges: - table_counter += 1 - # Find preceding header (look backwards from table start) - # Look for h2 (##) or h3 (###) headers - preceding_header = None - for i in range(start - 1, -1, -1): - if i < len(lines): - stripped = lines[i].strip() - if stripped.startswith('### '): - preceding_header = stripped - break - elif stripped.startswith('## '): - preceding_header = stripped - break - elif stripped.startswith('# '): - # Stop at h1, don't use it - break - - table_info[table_counter] = (start, end, preceding_header) - - # Process headers and static texts - replace with manual translations - processed_lines = lines.copy() - manual_line_replacements: dict[int, str] = {} - - # Process headers using configuration overrides - for h1_num, (idx, h1_text) in h1_dict.items(): - manual_trans = get_header_translation(header_configs, h1_text, header_level=1, h1_number=h1_num, language=language) - if manual_trans: - processed_lines[idx] = manual_trans - manual_line_replacements[idx] = manual_trans - - for h1_num, h2_dict_inner in h2_dict.items(): - for h2_num, (idx, h2_text) in h2_dict_inner.items(): - manual_trans = get_header_translation( - header_configs, - h2_text, - header_level=2, - h1_number=h1_num, - h2_number=h2_num, - language=language - ) - if manual_trans: - processed_lines[idx] = manual_trans - manual_line_replacements[idx] = manual_trans - - for h1_num, h2_map in h3_dict.items(): - for h2_num, h3_map in h2_map.items(): - for h3_num, (idx, h3_text) in h3_map.items(): - manual_trans = get_header_translation( - header_configs, - h3_text, - header_level=3, - h1_number=h1_num, - h2_number=h2_num, - h3_number=h3_num, - language=language - ) - if manual_trans: - processed_lines[idx] = manual_trans - manual_line_replacements[idx] = manual_trans - - # Process static text replacements - if text_configs and language: - match_counters = defaultdict(int) - for idx, original_line in enumerate(original_lines): - # Skip if line already has manual replacement (e.g., headers) - if idx in manual_line_replacements: - continue - for config_index, config in enumerate(text_configs): - if not isinstance(config, dict): - continue - source_text = config.get('text') - translations = config.get('translations') or {} - if not source_text or not translations: - continue - manual_trans = translations.get(language) - if not manual_trans: - continue - match_mode = config.get('match', 'exact') - strip_match = config.get('strip', False) - preserve_indent = config.get('preserve_indent', True) - occurrence = config.get('occurrence') - - line_to_compare = original_line.strip() if strip_match else original_line - source_to_compare = source_text.strip() if strip_match else source_text - - matched = False - if match_mode == 'exact': - matched = line_to_compare == source_to_compare - elif match_mode == 'startswith': - matched = line_to_compare.startswith(source_to_compare) - elif match_mode == 'endswith': - matched = line_to_compare.endswith(source_to_compare) - elif match_mode == 'contains': - matched = source_to_compare in line_to_compare - - if not matched: - continue - - match_key = (config_index, source_to_compare) - match_counters[match_key] += 1 - if occurrence is not None and match_counters[match_key] != occurrence: - continue - - output_text = manual_trans - if preserve_indent: - # Preserve leading whitespace from original line - leading_whitespace = original_line[:len(original_line) - len(original_line.lstrip(' \t'))] - if leading_whitespace and not output_text.startswith(leading_whitespace): - output_text = f"{leading_whitespace}{output_text.lstrip(' \t')}" - - processed_lines[idx] = output_text - manual_line_replacements[idx] = output_text - break - - # Store original lines for excluded tables restoration - original_lines_for_translation = original_lines.copy() - - # Dictionary to store table header info: {line_index: (is_header, should_translate, exclude_columns)} - table_header_info = {} - - # Process tables - apply exclusions - for table_num, (start, end, preceding_header) in table_info.items(): - # Find matching config - table_config = None - if preceding_header: - table_config = get_table_config_by_header(table_configs, preceding_header) - if not table_config: - table_config = get_table_config_by_number(table_configs, table_num) - - # Default values for tables without config (translate headers by default) - exclude_columns = [] - exclude_header = False - - if table_config: - # Check if entire table should be excluded - if table_config.get('exclude_table'): - # Mark all table lines as excluded - for i in range(start, end): - processed_lines[i] = f"__EXCLUDE_TABLE_LINE_{i}__" - continue - else: - # Get config values - exclude_columns = table_config.get('exclude_columns') - if exclude_columns is None: - exclude_columns = [] - exclude_header = table_config.get('exclude_header', False) - - # Process all table lines (including headers) - for both configured and unconfigured tables - for i in range(start, end): - line = processed_lines[i] - # Check if this is separator row (second row, typically contains dashes) - is_separator = (i == start + 1 and - ('---' in line or all(c in '-:| ' for c in line.strip()))) - is_header_row = i == start - - if is_separator: - # Don't modify separator row - continue - elif is_header_row: - # Store header info: (is_header=True, should_translate=not exclude_header, exclude_columns) - table_header_info[i] = (True, not exclude_header, exclude_columns) - # For header: if exclude_header=False, translate full header (don't apply column exclusions) - # If exclude_header=True, don't translate header at all (no processing needed) - # In both cases, keep header as-is (column exclusions don't apply to headers) - processed_lines[i] = line - else: - # Store data row info: (is_header=False, should_translate=True, exclude_columns) - table_header_info[i] = (False, True, exclude_columns) - # Process data rows: apply column exclusions - processed_lines[i] = process_table_line(line, exclude_columns, exclude_header, False) - - # Now translate the processed lines - lines = processed_lines - translated: List[str] = [] - buffer: List[str] = [] - in_code = False - in_html_tag = False - in_style_block = False - - def flush() -> None: - if not buffer: - return - chunk = "\n".join(buffer) - - # Protect terms before translation - protected_chunk, protected_mapping = protect_terms(chunk) - translated_chunk = retry_translate(translator, protected_chunk, "text block") - - # Ensure translated_chunk is not None - if translated_chunk is None: - translated_chunk = protected_chunk - - # Restore protected terms after translation - translated_chunk = restore_terms(translated_chunk, protected_mapping) - - # Ensure translated_chunk is still not None and is a string - if translated_chunk is None: - translated_chunk = chunk - - # Split and filter out None values - lines = translated_chunk.splitlines() - translated.extend([line for line in lines if line is not None]) - buffer.clear() - - for line_idx, line in enumerate(lines): - stripped = line.strip() - - # Handle manual replacements for headers and texts - if line_idx in manual_line_replacements: - flush() - translated.append(manual_line_replacements[line_idx]) - continue - - # Handle excluded table lines (entire table excluded) - if stripped.startswith("__EXCLUDE_TABLE_LINE_"): - # Restore original line from original lines - # Extract index from placeholder: __EXCLUDE_TABLE_LINE_{idx}__ - try: - idx_str = stripped.replace("__EXCLUDE_TABLE_LINE_", "").replace("__", "") - original_idx = int(idx_str) - if original_idx < len(original_lines_for_translation): - translated.append(original_lines_for_translation[original_idx]) - else: - translated.append(line) - except (ValueError, IndexError): - translated.append(line) - continue - - # Handle code blocks - if stripped.startswith(FENCE_PREFIX): - flush() - translated.append(line) - in_code = not in_code - continue - - # Handle HTML style blocks - if stripped.startswith(""): - flush() - translated.append(line) - in_style_block = False - continue - - # Handle HTML tags - but translate text content inside them - if "<" in line and ">" in line: - # Get exclusion configuration - exclusion_config = get_exclusion_config() - excluded_elements = exclusion_config.get('html_elements') - if excluded_elements is None: - excluded_elements = [] - - # Check if this is a simple HTML tag with text content that should be translated - should_translate = False - for tag in ["([^<]+)<', line) - if text_match: - text_content = text_match.group(1).strip() - if text_content and not any(term in text_content for term in PROTECTED_TERMS): - # Check if text contains excluded patterns - text_patterns = exclusion_config.get('text_patterns') - if text_patterns is None: - text_patterns = [] - if not text_patterns or not any(pattern in text_content for pattern in text_patterns): - # Protect terms before translation - protected_content, protected_mapping = protect_terms(text_content) - translated_content = retry_translate(translator, protected_content, "HTML content") - - # Ensure translated_content is not None - if translated_content is None: - translated_content = protected_content - - # Restore protected terms after translation - translated_content = restore_terms(translated_content, protected_mapping) - - # Ensure final content is not None - if translated_content is not None: - # Replace the text content in the line - translated_line = line.replace(text_content, translated_content) - translated.append(translated_line) - else: - translated.append(line) - continue - flush() - translated.append(line) - continue - - # Handle table lines (with column exclusions) - # Process table lines individually to preserve structure and handle exclusions - if is_table_line(line): - flush() - - # Check if this line is a table header and if it should be translated - header_info = table_header_info.get(line_idx) - is_header = False - should_translate = True - - if header_info: - is_header, should_translate, exclude_columns = header_info - - # If this is a header and should not be translated, skip translation - if is_header and not should_translate: - # Header should not be translated - restore without translation - translated_line = restore_table_line(line) - translated.append(translated_line) - continue - - # For headers with exclude_header: false, translate full header (no column exclusions) - # For data rows, apply column exclusions if any - # Check if line has excluded columns (placeholders) - only for data rows - has_excluded_columns = "__EXCLUDE_COL_" in line - - if has_excluded_columns: - # Line has excluded columns - need to translate while preserving placeholders - # This should only happen for data rows, not headers - # Protect excluded column placeholders and other terms - protected_line, protected_mapping = protect_terms(line) - - # Translate the line (placeholders will be preserved) - translated_line = retry_translate(translator, protected_line, "table line with excluded columns") - - # Ensure translated_line is not None - if translated_line is None: - translated_line = protected_line - - # Restore protected terms (including excluded column placeholders) - translated_line = restore_terms(translated_line, protected_mapping) - # Restore excluded columns (extract content from placeholders) - translated_line = restore_table_line(translated_line) - - translated.append(translated_line) - else: - # Normal table line without excluded columns - translate normally - # This includes headers with exclude_header: false (they have no placeholders) - protected_line, protected_mapping = protect_terms(line) - translated_line = retry_translate(translator, protected_line, "table line") - - # Ensure translated_line is not None - if translated_line is None: - translated_line = protected_line - - translated_line = restore_terms(translated_line, protected_mapping) - translated.append(translated_line) - - continue - - # Skip code blocks, empty lines, and style blocks - if in_code or not stripped or in_style_block or stripped.startswith("```"): - flush() - translated.append(line) - continue - - # Handle blockquotes - if stripped.startswith(">"): - flush() - quote_content = line.lstrip("> ").strip() - # Protect terms before translation - protected_content, protected_mapping = protect_terms(quote_content) - translated_content = retry_translate(translator, protected_content, "blockquote") - - # Ensure translated_content is not None - if translated_content is None: - translated_content = protected_content - - # Restore protected terms after translation - translated_content = restore_terms(translated_content, protected_mapping) - - # Ensure final content is not None - if translated_content is not None: - translated.append("> " + translated_content) - else: - translated.append(line) - continue - - # Handle headers - flush buffer before adding header to ensure proper translation - # Headers that were statically replaced should be added directly without translation - # Headers without static replacement should be translated separately - if stripped.startswith("#"): - flush() - # Check if header has manual replacement (static translation) - if line_idx in manual_line_replacements: - translated.append(manual_line_replacements[line_idx]) - else: - # Header was not statically replaced - translate it separately - protected_line, protected_mapping = protect_terms(line) - translated_line = retry_translate(translator, protected_line, "header") - - # Ensure translated_line is not None - if translated_line is None: - translated_line = protected_line - - # Restore protected terms after translation - translated_line = restore_terms(translated_line, protected_mapping) - translated.append(translated_line) - continue - - buffer.append(line) - - flush() - # Filter out any None values before joining - translated = [line for line in translated if line is not None] - return "\n".join(translated) - - -async def translate_blocks_async(text: str, translator: GoogleTranslator, semaphore: asyncio.Semaphore, - file_path: Path = None, language: str = None) -> str: - """Async version of translate_blocks with rate limiting.""" - async with semaphore: - # Run the synchronous translation in a thread pool - loop = asyncio.get_event_loop() - with ThreadPoolExecutor() as executor: - result = await loop.run_in_executor( - executor, translate_blocks, text, translator, file_path, language - ) - await asyncio.sleep(TRANSLATION_DELAY) # Rate limiting - return result - - -def translate_file(path: Path, targets: Iterable[str]) -> None: - content = path.read_text(encoding="utf-8") - front_matter, body = split_front_matter(content) - - for lang in targets: - suffix = TRANSLATION_SUFFIXES.get(lang, f".{lang}.md") - translator = GoogleTranslator(source="auto", target=lang) - translated_body = translate_blocks(body, translator, file_path=path, language=lang) - - # Translate front matter if present (with metadata override support) - translated_front_matter = front_matter - if front_matter: - # Check if front matter has title or description - has_title = 'title:' in front_matter - has_description = 'description:' in front_matter - - # If front matter exists but lacks title/description, add metadata - if not has_title or not has_description: - metadata = get_metadata_for_file(path, None) # Get default metadata - if metadata: - translated_front_matter = add_metadata_to_front_matter( - front_matter, metadata, lang=lang, translator=translator, file_path=path - ) - else: - # No metadata, just translate existing front matter - translated_front_matter = translate_front_matter( - front_matter, translator, file_path=path, target_lang=lang - ) - else: - # Front matter has title/description, just translate it - translated_front_matter = translate_front_matter( - front_matter, translator, file_path=path, target_lang=lang - ) - else: - # If no front matter exists, create one with metadata if available - metadata = get_metadata_for_file(path, None) # Get default metadata - if metadata and (metadata.get('title') or metadata.get('description')): - front_matter_lines = ["---"] - # Get language-specific metadata if available - lang_metadata = get_metadata_for_file(path, lang) if lang else {} - - if lang_metadata and lang_metadata.get('title'): - front_matter_lines.append(f"title: {lang_metadata['title']}") - elif metadata.get('title'): - # Translate default title if no language-specific version - if not lang_metadata or not lang_metadata.get('title'): - translated_title = retry_translate(translator, metadata['title'], "metadata title") - front_matter_lines.append(f"title: {translated_title}") - else: - front_matter_lines.append(f"title: {metadata['title']}") - - if lang_metadata and lang_metadata.get('description'): - front_matter_lines.append(f"description: {lang_metadata['description']}") - elif metadata.get('description'): - # Translate default description if no language-specific version - if not lang_metadata or not lang_metadata.get('description'): - translated_description = retry_translate(translator, metadata['description'], "metadata description") - front_matter_lines.append(f"description: {translated_description}") - else: - front_matter_lines.append(f"description: {metadata['description']}") - - front_matter_lines.append("---") - translated_front_matter = "\n".join(front_matter_lines) - - pieces = [] - if translated_front_matter: - pieces.append(translated_front_matter) - pieces.append("") - pieces.append(translated_body) - output_path = build_translation_path(path, suffix) - output_path.write_text("\n".join(pieces).strip() + "\n", encoding="utf-8") - - -async def translate_single_language(path: Path, lang: str, body: str, front_matter: str | None, - semaphore: asyncio.Semaphore, progress_bar: tqdm) -> None: - """Translate a single file to a single language.""" - try: - suffix = TRANSLATION_SUFFIXES.get(lang, f".{lang}.md") - translator = GoogleTranslator(source="auto", target=lang) - translated_body = await translate_blocks_async(body, translator, semaphore, file_path=path, language=lang) - - # Translate front matter if present (with metadata override support) - translated_front_matter = front_matter - if front_matter: - # Check if front matter has title or description - has_title = 'title:' in front_matter - has_description = 'description:' in front_matter - - # If front matter exists but lacks title/description, add metadata - if not has_title or not has_description: - metadata = get_metadata_for_file(path, None) # Get default metadata - if metadata: - # Run add_metadata_to_front_matter in thread pool - loop = asyncio.get_event_loop() - with ThreadPoolExecutor() as executor: - translated_front_matter = await loop.run_in_executor( - executor, add_metadata_to_front_matter, - front_matter, metadata, lang, translator, path - ) - else: - # No metadata, just translate existing front matter - loop = asyncio.get_event_loop() - with ThreadPoolExecutor() as executor: - translated_front_matter = await loop.run_in_executor( - executor, translate_front_matter, front_matter, translator, path, lang - ) - else: - # Front matter has title/description, just translate it - loop = asyncio.get_event_loop() - with ThreadPoolExecutor() as executor: - translated_front_matter = await loop.run_in_executor( - executor, translate_front_matter, front_matter, translator, path, lang - ) - else: - # If no front matter exists, create one with metadata if available - metadata = get_metadata_for_file(path, None) # Get default metadata - if metadata and (metadata.get('title') or metadata.get('description')): - front_matter_lines = ["---"] - # Get language-specific metadata if available - lang_metadata = get_metadata_for_file(path, lang) if lang else {} - - if lang_metadata and lang_metadata.get('title'): - front_matter_lines.append(f"title: {lang_metadata['title']}") - elif metadata.get('title'): - # Translate default title if no language-specific version - if not lang_metadata or not lang_metadata.get('title'): - translated_title = retry_translate(translator, metadata['title'], "metadata title") - front_matter_lines.append(f"title: {translated_title}") - else: - front_matter_lines.append(f"title: {metadata['title']}") - - if lang_metadata and lang_metadata.get('description'): - front_matter_lines.append(f"description: {lang_metadata['description']}") - elif metadata.get('description'): - # Translate default description if no language-specific version - if not lang_metadata or not lang_metadata.get('description'): - translated_description = retry_translate(translator, metadata['description'], "metadata description") - front_matter_lines.append(f"description: {translated_description}") - else: - front_matter_lines.append(f"description: {metadata['description']}") - - front_matter_lines.append("---") - translated_front_matter = "\n".join(front_matter_lines) - - pieces = [] - if translated_front_matter: - pieces.append(translated_front_matter) - pieces.append("") - pieces.append(translated_body) - output_path = build_translation_path(path, suffix) - - async with aiofiles.open(output_path, 'w', encoding='utf-8') as f: - await f.write("\n".join(pieces).strip() + "\n") - - progress_bar.set_description(f"Translated {path.name} to {lang}") - progress_bar.update(1) - - except Exception as e: - print(f"Error translating {path.name} to {lang}: {e}") - progress_bar.update(1) - - -async def translate_file_async(path: Path, targets: List[str], semaphore: asyncio.Semaphore, progress_bar: tqdm) -> None: - """Async version of translate_file with parallel language translation.""" - try: - async with aiofiles.open(path, 'r', encoding='utf-8') as f: - content = await f.read() - - front_matter, body = split_front_matter(content) - - # Create tasks for all language translations to run in parallel - tasks = [] - for lang in targets: - task = translate_single_language(path, lang, body, front_matter, semaphore, progress_bar) - tasks.append(task) - - # Execute all language translations concurrently - await asyncio.gather(*tasks, return_exceptions=True) - - except Exception as e: - print(f"Error processing file {path}: {e}") - progress_bar.update(len(targets)) - - -def add_metadata_to_original_files() -> None: - """Add metadata to original markdown files before translation.""" - metadata_config = get_metadata_config() - - if not metadata_config.get('enabled', False): - return - - for md_file in iter_markdown_files(DOCS_ROOT): - content = md_file.read_text(encoding="utf-8") - front_matter, body = split_front_matter(content) - - # Get metadata for this file - metadata = get_metadata_for_file(md_file, None) - if not metadata or (not metadata.get('title') and not metadata.get('description')): - continue - - # Check if front matter has title or description - has_title = front_matter and 'title:' in front_matter if front_matter else False - has_description = front_matter and 'description:' in front_matter if front_matter else False - - # If both exist, skip - if has_title and has_description: - continue - - # Add metadata to front matter - if front_matter: - updated_front_matter = add_metadata_to_front_matter( - front_matter, metadata, lang=None, translator=None, file_path=md_file - ) - else: - # Create new front matter - front_matter_lines = ["---"] - if metadata.get('title'): - front_matter_lines.append(f"title: {metadata['title']}") - if metadata.get('description'): - front_matter_lines.append(f"description: {metadata['description']}") - front_matter_lines.append("---") - updated_front_matter = "\n".join(front_matter_lines) - - # Write updated content - pieces = [] - if updated_front_matter: - pieces.append(updated_front_matter) - pieces.append("") - pieces.append(body) - md_file.write_text("\n".join(pieces).strip() + "\n", encoding="utf-8") - - -def build_translation_path(path: Path, suffix: str) -> Path: - """ - Build path for translated file using folder structure. - - For folder structure: - docs/index.md -> docs/ru/index.md - docs/subdir/page.md -> docs/ru/subdir/page.md - """ - # Extract language code from suffix (e.g., ".ru.md" -> "ru") - lang_code = suffix.split('.')[1] if '.' in suffix else suffix - - # Get relative path from docs root - try: - rel_path = path.relative_to(DOCS_ROOT) - except ValueError: - # If path is not relative to docs root, fall back to old behavior - return path.with_name(f"{path.stem}{suffix}") - - # Build new path: docs/lang_code/original_relative_path - translated_path = DOCS_ROOT / lang_code / rel_path - - # Create parent directory if it doesn't exist - translated_path.parent.mkdir(parents=True, exist_ok=True) - - return translated_path - - -def run(targets: Iterable[str]) -> None: - targets = list(targets) - if not targets: - return - DOCS_ROOT.mkdir(exist_ok=True) - # Add metadata to original files before translation - add_metadata_to_original_files() - for md_file in iter_markdown_files(DOCS_ROOT): - translate_file(md_file, targets) - - -async def run_async(targets: List[str], args: argparse.Namespace) -> None: - """Async version of run with progress tracking and concurrent processing.""" - if not targets: - return - - DOCS_ROOT.mkdir(exist_ok=True) - - # Add metadata to original files before translation - add_metadata_to_original_files() - - # Collect all markdown files - md_files = list(iter_markdown_files(DOCS_ROOT)) - if not md_files: - print("No markdown files found to translate.") - return - - # Calculate total operations (files * languages) - total_operations = len(md_files) * len(targets) - - print(f"Starting translation of {len(md_files)} files to {len(targets)} languages...") - print(f"Total operations: {total_operations}") - print(f"Using {MAX_CONCURRENT_TRANSLATIONS} concurrent translations with {MAX_CONCURRENT_FILES} concurrent files") - print(f"Each file will be translated to all languages in parallel for maximum speed") - - # Create semaphores for rate limiting - translation_semaphore = asyncio.Semaphore(MAX_CONCURRENT_TRANSLATIONS) - file_semaphore = asyncio.Semaphore(MAX_CONCURRENT_FILES) - - # Create progress bar - progress_bar = tqdm(total=total_operations, desc="Translating", unit="ops") - - start_time = time.time() - - try: - # Process files in batches to avoid overwhelming the system - batch_size = MAX_CONCURRENT_FILES - for i in range(0, len(md_files), batch_size): - batch = md_files[i:i + batch_size] - - # Create tasks for this batch - tasks = [] - for md_file in batch: - task = translate_file_async(md_file, targets, translation_semaphore, progress_bar) - tasks.append(task) - - # Wait for this batch to complete - await asyncio.gather(*tasks, return_exceptions=True) - - # Small delay between batches - if i + batch_size < len(md_files): - await asyncio.sleep(0.5) - - except Exception as e: - print(f"Error during translation: {e}") - - finally: - progress_bar.close() - - end_time = time.time() - duration = end_time - start_time - print(f"\nTranslation completed in {duration:.2f} seconds") - print(f"Average time per operation: {duration/total_operations:.2f} seconds") - - # Update menu items automatically (unless disabled) - if not getattr(args, 'no_menu_update', False): - print("Updating menu items...") - try: - update_mkdocs_alternate_menu() - print("Menu items updated successfully!") - except Exception as e: - print(f"Warning: Failed to update menu items: {e}") - - print("Updating menu translations...") - try: - update_menu_translations_json() - print("Menu translations updated successfully!") - except Exception as e: - print(f"Warning: Failed to update menu translations: {e}") - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--targets", - nargs="+", - default=TRANSLATION_LOCALES, - help="Language codes to translate into (default: %(default)s).", - ) - parser.add_argument( - "--max-concurrent-translations", - type=int, - default=MAX_CONCURRENT_TRANSLATIONS, - help=f"Maximum concurrent translations (default: {MAX_CONCURRENT_TRANSLATIONS}).", - ) - parser.add_argument( - "--max-concurrent-files", - type=int, - default=MAX_CONCURRENT_FILES, - help=f"Maximum concurrent files (default: {MAX_CONCURRENT_FILES}).", - ) - parser.add_argument( - "--sync", - action="store_true", - help="Use synchronous processing instead of async (slower but more reliable).", - ) - parser.add_argument( - "--no-menu-update", - action="store_true", - help="Skip automatic menu items update.", - ) - return parser.parse_args() - - -def main() -> None: - args = parse_args() - - # Update global constants based on command line arguments - global MAX_CONCURRENT_TRANSLATIONS, MAX_CONCURRENT_FILES - MAX_CONCURRENT_TRANSLATIONS = args.max_concurrent_translations - MAX_CONCURRENT_FILES = args.max_concurrent_files - - with contextlib.ExitStack(): - if args.sync: - print("Using synchronous processing...") - run(args.targets) - else: - print("Using asynchronous processing...") - asyncio.run(run_async(args.targets, args)) - - -if __name__ == "__main__": - main() diff --git a/scripts/translation_config.yaml b/scripts/translation_config.yaml deleted file mode 100644 index 929dce1..0000000 --- a/scripts/translation_config.yaml +++ /dev/null @@ -1,176 +0,0 @@ -# Configuration for custom metadata (title and description) for .md files -# And translation settings (tables, headers) -metadata: - # Global settings - enabled: true - - # File-specific metadata configuration - files: - # Example configurations for different files - # Path is relative to docs directory - "index.md": - title: "Angry Data Scanner - Sensitive Data Scanner for Mac, Windows and Linux" - description: "Advanced sensitive data discovery tool combining personal data discovery, payment card discovery, and passwords finder in one solution. Comprehensive data detector for compliance and security." - # Language-specific overrides (optional) - # If not specified, the default title/description will be auto-translated - translations: - ru: - title: "Angry Data Scanner - Программа поиска конфиденциальных данных для Mac, Windows и Linux" - description: "Инструмент поиска данных. С его помощью можно найти персональные данные, данные банковских карт и другие конфиденциальные данные в папках, S3, базах данных, веб-страницах." - - - "angrydata-core/index.md": - title: "Core Library | Angry Data Scanner" - description: "Library for sensitive data processing" - - "CONSOLE.md": - title: "Console Mode | Angry Data Scanner" - description: "Using Angry Data Scanner in console mode" - - # Default metadata for files not explicitly configured - defaults: - description: "Advanced sensitive data discovery tool combining personal data discovery, payment card discovery, and passwords finder in one solution. Comprehensive data detector for compliance and security." - -# Translation settings for tables and headers -translation: - # Retry settings for failed translations - retry: - max_attempts: 3 # Number of retry attempts for failed translations - delay_seconds: 2 # Delay between retry attempts in seconds - - # Table translation settings - # Tables can be identified by file + header (e.g., "## Personal Data (numbers)") OR by table number - tables: - files: - "index.md": - - match_by_header: "### Personal Data (numbers)" - exclude_columns: [2, 3, 4] - exclude_header: false - - match_by_header: "### Personal Data (text)" - exclude_columns: [2, 3, 4] - exclude_header: false - - match_by_header: "### PCI DSS" - exclude_columns: [4] - exclude_header: false - - match_by_header: "### Banking Secrecy" - exclude_columns: [3, 4] - exclude_header: false - - match_by_header: "### IT Assets" - exclude_columns: [3] - exclude_header: false - - match_by_header: "### Custom Signatures" - exclude_columns: [3, 4] - exclude_header: false - - match_by_header: "## Supported file types" - exclude_columns: [1, 2] - exclude_header: false - - match_by_header: "## Supported data sources" - exclude_columns: [1, 2] - exclude_header: false - - match_by_header: "## Download" - exclude_table: true - # Example: "index.md": - # - match_by_header: "## Personal Data (numbers)" - # exclude_columns: [2] # Exclude column 2 (Country) from translation - # exclude_header: true # Also exclude table header - # - match_by_header: "## Personal Data (text)" - # exclude_table: true # Exclude entire table from translation - # - table_number: 3 # Identify by table number in file - # exclude_columns: [1, 3] # Exclude columns 1 and 3 - - # Header translation settings - # H1 can be identified by text OR by number - # H2 can be identified by text OR by number within parent H1 - headers: - files: - "index.md": - - level: 1 - number: 1 - translations: - ru: "# Бесплатная программа для быстрого поиска конфиденциальных данных" - - level: 2 - parent_h1_number: 1 - number: 1 - translations: - ru: "## Поиск конфиденциальных данных" - - level: 3 - parent_h1_number: 1 - parent_h2_number: 1 - text: "### Personal Data (numbers)" - translations: - ru: "### Поиск числовых персональных данных: ИНН, телефон, паспортные данные, СНИЛС, ОМС" - - level: 3 - parent_h1_number: 1 - parent_h2_number: 1 - text: "### Personal Data (text)" - translations: - ru: "### Поиск текстовых персональных данных: ФИО, адрес, e-mail, логин/пароль" - - level: 3 - parent_h1_number: 1 - parent_h2_number: 1 - text: "### Banking Secrecy" - translations: - ru: "### Обнаружение банковской тайны: номер счета, номер карты, CVV, номер криптовалютного кошелька" - - level: 3 - parent_h1_number: 1 - parent_h2_number: 1 - text: "### IT Assets" - translations: - ru: "### Поиск ИТ-Активов: файлы с исходным кодом, файлы с паролями, TLS-сертификаты, AI-модели" - - level: 3 - parent_h1_number: 1 - parent_h2_number: 1 - text: "### Network & Infrastructure" - translations: - ru: "### Поиск данных сети и инфраструктуры: IPV4, IPV6, заблокированные домены" - - level: 3 - parent_h1_number: 1 - parent_h2_number: 1 - text: "### Custom Signatures" - translations: - ru: "### Поиск пользовательских сигнатур" - - # Static text (non-header) translation settings - # Each entry specifies the original text and its manual translations. - # Supported match modes: exact (default), startswith, endswith, contains. - # Set occurrence to match only the N-th occurrence (1-based) if needed. - texts: - files: - - text: "**Angry Data Scanner** is a sensitive data discovery tool that uses pattern matching to automatically discover sensitive data stored in folders, web pages, S3, database." - translations: - ru: "Angry Data Scanner — это инструмент с простым UI для поиска конфиденциальных данных, хранящихся в папках, веб-страницах, S3, базе данных." - - text: "It helps organizations by identifying where sensitive data such as personally identifiable information (PII) and intellectual property is stored." - translations: - ru: "Он помогает организациям определять, где хранятся конфиденциальные данные, такие как личная информация (PII) и интеллектуальная собственность." - - text: "The tool provides visibility where your sensitive data is stored." - translations: - ru: "Инструмент обеспечивает видимость того, где хранятся ваши конфиденциальные данные." - # Example: "index.md": - # - text: "Download Angry Data Scanner" - # match: exact # optional, default is exact - # strip: true # optional, compare stripped text - # preserve_indent: true # optional, keep original leading spaces - # translations: - # ru: "Скачать Angry Data Scanner" - # Example: "index.md": - # # H1 headers - # - level: 1 - # text: "# A tool with friendly UI" # Identify by text - # translations: - # ru: "# Инструмент с дружелюбным интерфейсом" - # de: "# Ein Tool mit benutzerfreundlicher Oberfläche" - # - level: 1 - # number: 2 # Identify by H1 number in file - # translations: - # ru: "# Второй заголовок" - # # H2 headers - # - level: 2 - # text: "## Discovered sensitive data" # Identify by text - # translations: - # ru: "## Обнаруженные конфиденциальные данные" - # - level: 2 - # parent_h1_number: 1 # Parent H1 number - # number: 2 # H2 number within that H1 - # translations: - # ru: "## Второй подзаголовок" - diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..d43d8f9 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,20 @@ +# OS files +.DS_Store +Thumbs.db +desktop.ini + +# Editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Logs +*.log +npm-debug.log* + +# Temporary files +*.tmp +*.temp + diff --git a/src/assets/apple-icon.svg b/src/assets/apple-icon.svg new file mode 100644 index 0000000..6afe160 --- /dev/null +++ b/src/assets/apple-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/favicon_dark.ico b/src/assets/favicon_dark.ico new file mode 100644 index 0000000..4a12cdd Binary files /dev/null and b/src/assets/favicon_dark.ico differ diff --git a/src/assets/favicon_light.ico b/src/assets/favicon_light.ico new file mode 100644 index 0000000..b590982 Binary files /dev/null and b/src/assets/favicon_light.ico differ diff --git a/src/assets/favicon_light_tab.ico b/src/assets/favicon_light_tab.ico new file mode 100644 index 0000000..fef1656 Binary files /dev/null and b/src/assets/favicon_light_tab.ico differ diff --git a/src/assets/flag-cn.svg b/src/assets/flag-cn.svg new file mode 100644 index 0000000..b3369c2 --- /dev/null +++ b/src/assets/flag-cn.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/flag-de.svg b/src/assets/flag-de.svg new file mode 100644 index 0000000..4c75505 --- /dev/null +++ b/src/assets/flag-de.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/flag-es.svg b/src/assets/flag-es.svg new file mode 100644 index 0000000..d2b27b5 --- /dev/null +++ b/src/assets/flag-es.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/flag-fr.svg b/src/assets/flag-fr.svg new file mode 100644 index 0000000..13f31d8 --- /dev/null +++ b/src/assets/flag-fr.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/flag-ru.svg b/src/assets/flag-ru.svg new file mode 100644 index 0000000..7878ce2 --- /dev/null +++ b/src/assets/flag-ru.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/flag-us.svg b/src/assets/flag-us.svg new file mode 100644 index 0000000..356ecb1 --- /dev/null +++ b/src/assets/flag-us.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/linux-icon.svg b/src/assets/linux-icon.svg new file mode 100644 index 0000000..cb2f724 --- /dev/null +++ b/src/assets/linux-icon.svg @@ -0,0 +1,2 @@ + + diff --git a/src/assets/screenshot_dark.png b/src/assets/screenshot_dark.png new file mode 100644 index 0000000..828950f Binary files /dev/null and b/src/assets/screenshot_dark.png differ diff --git a/src/assets/screenshot_light.png b/src/assets/screenshot_light.png new file mode 100644 index 0000000..218fa4e Binary files /dev/null and b/src/assets/screenshot_light.png differ diff --git a/src/assets/windows-icon.svg b/src/assets/windows-icon.svg new file mode 100644 index 0000000..ac7bd22 --- /dev/null +++ b/src/assets/windows-icon.svg @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/src/css/base/_reset.css b/src/css/base/_reset.css new file mode 100644 index 0000000..e1d363a --- /dev/null +++ b/src/css/base/_reset.css @@ -0,0 +1,31 @@ +/* ============================================================================ + Reset & Base Styles + ============================================================================ */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + font-family: var(--font-family-base); + background-color: var(--color-bg-primary); + color: var(--color-text-primary); + line-height: 1.6; + transition: background-color var(--transition-base), color var(--transition-base); +} + +code { + background: var(--color-code-bg); + color: var(--color-code-text); + padding: 2px var(--spacing-xs); + border-radius: var(--border-radius-sm); + font-family: var(--font-family-mono); + font-size: var(--font-size-sm); +} + diff --git a/src/css/base/_themes.css b/src/css/base/_themes.css new file mode 100644 index 0000000..7b9b4b9 --- /dev/null +++ b/src/css/base/_themes.css @@ -0,0 +1,31 @@ +/* ============================================================================ + Dark Theme Overrides + ============================================================================ */ + +[data-theme="dark"] { + /* Color Palette - Dark Theme */ + --color-bg-primary: #0f172a; + --color-bg-secondary: #1e293b; + --color-bg-tertiary: #334155; + --color-text-primary: #f1f5f9; + --color-text-secondary: #cbd5e1; + --color-text-tertiary: #94a3b8; + --color-border: #334155; + --color-accent: #8b5cf6; + --color-accent-hover: #a78bfa; + --color-success: #34d399; + --color-warning: #fbbf24; + --color-error: #f87171; + --color-code-bg: #1e293b; + --color-code-text: #e2e8f0; + + /* Shadows - Dark Theme */ + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.3); + + /* Component overrides for dark theme */ + --card-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + --card-hover-shadow: 0 12px 32px rgba(139, 92, 246, 0.2), 0 4px 12px rgba(0, 0, 0, 0.3); +} + diff --git a/src/css/base/_utilities.css b/src/css/base/_utilities.css new file mode 100644 index 0000000..ffa331e --- /dev/null +++ b/src/css/base/_utilities.css @@ -0,0 +1,11 @@ +/* ============================================================================ + Utility Classes + ============================================================================ */ + +/* Container utility */ +.container { + max-width: 1400px; + margin: 0 auto; + padding: 0 var(--spacing-xl); +} + diff --git a/src/css/base/_variables.css b/src/css/base/_variables.css new file mode 100644 index 0000000..b9f3e31 --- /dev/null +++ b/src/css/base/_variables.css @@ -0,0 +1,85 @@ +/* ============================================================================ + CSS Variables & Design Tokens + ============================================================================ */ + +:root { + /* Color Palette - Light Theme */ + --color-bg-primary: #ffffff; + --color-bg-secondary: #f6f8fa; + --color-bg-tertiary: #f0f3f6; + --color-text-primary: #1e293b; + --color-text-secondary: #64748b; + --color-text-tertiary: #94a3b8; + --color-border: #e2e8f0; + --color-accent: #6320ee; + --color-accent-hover: #4c1a9e; + --color-success: #10b981; + --color-warning: #f59e0b; + --color-error: #ef4444; + --color-code-bg: #f1f5f9; + --color-code-text: #0f172a; + + /* Shadows */ + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + + /* Spacing */ + --spacing-xs: 4px; + --spacing-sm: 8px; + --spacing-md: 16px; + --spacing-lg: 24px; + --spacing-xl: 32px; + --spacing-2xl: 48px; + --spacing-3xl: 64px; + --spacing-4xl: 80px; + + /* Typography */ + --font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + --font-family-mono: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + --font-size-xs: 12px; + --font-size-sm: 13px; + --font-size-base: 14px; + --font-size-md: 15px; + --font-size-lg: 18px; + --font-size-xl: 20px; + --font-size-2xl: 24px; + --font-size-3xl: 36px; + --font-size-4xl: 48px; + + /* Layout */ + --container-max-width: 1200px; + --navbar-height: 64px; + --border-radius-sm: 4px; + --border-radius-md: 6px; + --border-radius-lg: 8px; + --border-radius-xl: 12px; + + /* Transitions */ + --transition-fast: 0.2s ease; + --transition-base: 0.3s ease; + --transition-slow: 0.6s ease; + + /* Component-specific variables */ + --card-padding: var(--spacing-xl) var(--spacing-lg); + --card-border-radius: var(--border-radius-xl); + --card-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + --card-hover-shadow: 0 12px 32px rgba(99, 32, 238, 0.15), 0 4px 12px rgba(0, 0, 0, 0.08); + --card-hover-transform: translateY(-6px); + + /* Button variables */ + --button-padding: var(--spacing-md) var(--spacing-xl); + --button-border-radius: var(--border-radius-lg); + --button-font-weight: 600; + + /* Breakpoints */ + --breakpoint-mobile: 480px; + --breakpoint-tablet: 768px; + --breakpoint-desktop: 1024px; + --breakpoint-large: 1200px; + + /* Gradients */ + --gradient-accent: linear-gradient(135deg, var(--color-accent) 0%, var(--color-accent-hover) 100%); + --gradient-card-hover: linear-gradient(135deg, var(--color-bg-tertiary) 0%, var(--color-bg-secondary) 100%); +} + diff --git a/src/css/components/_buttons.css b/src/css/components/_buttons.css new file mode 100644 index 0000000..dc77c0c --- /dev/null +++ b/src/css/components/_buttons.css @@ -0,0 +1,69 @@ +/* ============================================================================ + Button Components + ============================================================================ */ + +.btn { + padding: var(--button-padding); + border-radius: var(--button-border-radius); + font-weight: var(--button-font-weight); + font-size: var(--font-size-base); + text-decoration: none; + display: inline-flex; + align-items: center; + justify-content: center; + transition: all var(--transition-base); + border: none; + cursor: pointer; + position: relative; + overflow: hidden; +} + +.btn-primary { + background: var(--gradient-accent); + color: white; + box-shadow: 0 4px 12px rgba(99, 32, 238, 0.3); + border: none; +} + +[data-theme="dark"] .btn-primary { + box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4); +} + +.btn-primary:hover { + background: linear-gradient(135deg, var(--color-accent-hover) 0%, var(--color-accent) 100%); + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(99, 32, 238, 0.4); +} + +[data-theme="dark"] .btn-primary:hover { + box-shadow: 0 6px 20px rgba(139, 92, 246, 0.5); +} + +.btn-secondary { + background-color: var(--color-bg-primary); + color: var(--color-text-primary); + border: 2px solid var(--color-border); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +[data-theme="dark"] .btn-secondary { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.btn-secondary:hover { + background-color: var(--color-bg-secondary); + border-color: var(--color-accent); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(99, 32, 238, 0.15); +} + +[data-theme="dark"] .btn-secondary:hover { + box-shadow: 0 4px 12px rgba(139, 92, 246, 0.2); +} + +.btn-large { + padding: var(--spacing-md) var(--spacing-2xl); + font-size: var(--font-size-lg); + font-weight: 600; +} + diff --git a/src/css/components/_cards.css b/src/css/components/_cards.css new file mode 100644 index 0000000..2dcfa43 --- /dev/null +++ b/src/css/components/_cards.css @@ -0,0 +1,95 @@ +/* ============================================================================ + Card Base Component + ============================================================================ */ + +/** + * Base card class with common styles + * Use this as a foundation for all card variants + */ +.card-base { + background: var(--color-bg-primary); + padding: var(--card-padding); + border-radius: var(--card-border-radius); + border: 1px solid var(--color-border); + transition: all var(--transition-base); + position: relative; + overflow: hidden; + box-shadow: var(--card-shadow); +} + +[data-theme="dark"] .card-base { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +/* Accent top border animation */ +.card-base[data-variant="accent-top"]::before, +.card-base.card-accent-top::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: var(--gradient-accent); + transform: scaleX(0); + transform-origin: left; + transition: transform var(--transition-base); +} + +.card-base[data-variant="accent-top"]:hover::before, +.card-base.card-accent-top:hover::before { + transform: scaleX(1); +} + +/* Hover lift effect */ +.card-base[data-variant="hover-lift"]:hover, +.card-base.card-hover-lift:hover { + transform: var(--card-hover-transform); + box-shadow: var(--card-hover-shadow); + border-color: var(--color-accent); +} + +[data-theme="dark"] .card-base[data-variant="hover-lift"]:hover, +[data-theme="dark"] .card-base.card-hover-lift:hover { + box-shadow: 0 12px 32px rgba(139, 92, 246, 0.2), 0 4px 12px rgba(0, 0, 0, 0.3); +} + +/* Background change on hover */ +.card-base[data-variant="hover-bg"]:hover, +.card-base.card-hover-bg:hover { + background: var(--color-bg-secondary); +} + +/* Combined variants (most common) */ +.card-base.card-interactive { + cursor: pointer; +} + +.card-base.card-interactive::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: var(--gradient-accent); + transform: scaleX(0); + transform-origin: left; + transition: transform var(--transition-base); +} + +.card-base.card-interactive:hover { + transform: var(--card-hover-transform); + box-shadow: var(--card-hover-shadow); + border-color: var(--color-accent); + background: var(--color-bg-secondary); +} + +.card-base.card-interactive:hover::before { + transform: scaleX(1); +} + +[data-theme="dark"] .card-base.card-interactive:hover { + box-shadow: 0 12px 32px rgba(139, 92, 246, 0.2), 0 4px 12px rgba(0, 0, 0, 0.3); +} + diff --git a/src/css/components/_country-selector.css b/src/css/components/_country-selector.css new file mode 100644 index 0000000..73a5ca2 --- /dev/null +++ b/src/css/components/_country-selector.css @@ -0,0 +1,217 @@ +/* ============================================================================ + Country Selector Component + ============================================================================ */ + +.country-selector-wrapper { + margin-bottom: var(--spacing-3xl); + padding: var(--spacing-xl); + background: var(--color-bg-primary); + border-radius: var(--border-radius-xl); + border: 1px solid var(--color-border); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + transition: all var(--transition-base); + position: relative; + overflow: hidden; +} + +[data-theme="dark"] .country-selector-wrapper { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.country-selector-wrapper::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: var(--gradient-accent); + transform: scaleX(0); + transform-origin: left; + transition: transform var(--transition-base); +} + +.country-selector-wrapper:hover::before { + transform: scaleX(1); +} + +.country-selector-wrapper:hover { + box-shadow: 0 8px 24px rgba(99, 32, 238, 0.12), 0 4px 12px rgba(0, 0, 0, 0.08); + border-color: var(--color-accent); + background: var(--color-bg-secondary); +} + +[data-theme="dark"] .country-selector-wrapper:hover { + box-shadow: 0 8px 24px rgba(139, 92, 246, 0.2), 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.country-selector-header { + display: flex; + align-items: center; + justify-content: center; + gap: var(--spacing-md); + margin-bottom: var(--spacing-lg); +} + +.country-selector-globe-icon { + width: 24px; + height: 24px; + color: var(--color-accent); + animation: rotate 20s linear infinite; +} + +@keyframes rotate { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.country-selector-label { + font-size: var(--font-size-lg); + font-weight: 600; + color: var(--color-text-primary); + letter-spacing: 0.3px; +} + +.country-buttons-group { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-md); + justify-content: center; + align-items: center; +} + +.country-button { + display: flex; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-md) var(--spacing-lg); + background: var(--color-bg-primary); + border: 2px solid var(--color-border); + border-radius: var(--border-radius-lg); + font-size: var(--font-size-base); + font-weight: 500; + color: var(--color-text-secondary); + cursor: pointer; + transition: background-color var(--transition-base), border-color var(--transition-base), color var(--transition-base), box-shadow var(--transition-base), opacity var(--transition-base); + outline: none; + position: relative; + overflow: hidden; + min-width: 120px; + justify-content: center; +} + +.country-button::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(99, 32, 238, 0.1), transparent); + transition: left 0.5s ease; +} + +[data-theme="dark"] .country-button::before { + background: linear-gradient(90deg, transparent, rgba(139, 92, 246, 0.2), transparent); +} + +.country-button:hover::before { + left: 100%; +} + +.country-button:hover:not(.active) { + border-color: var(--color-accent); + background: var(--color-bg-secondary); + color: var(--color-text-primary); + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.country-button.active:hover { + box-shadow: 0 6px 16px rgba(99, 32, 238, 0.4); +} + +[data-theme="dark"] .country-button.active:hover { + box-shadow: 0 6px 16px rgba(139, 92, 246, 0.5); +} + +.country-button:active:not(.active) { + transform: translateY(0); +} + +.country-button.active:active { + opacity: 0.9; +} + +.country-button.active { + background: var(--gradient-accent); + border-color: var(--color-accent); + color: white; + box-shadow: 0 4px 12px rgba(99, 32, 238, 0.3); + font-weight: 500; +} + +[data-theme="dark"] .country-button.active { + box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4); +} + +.country-button.active::before { + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); +} + +.country-button.active .country-button-icon { + color: white; +} + +.country-flag-emoji { + font-size: 24px; + line-height: 1; + display: inline-block; + transition: all var(--transition-base); + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1)); +} + +.country-button:hover:not(.active) .country-flag-emoji { + transform: rotate(5deg); + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2)); +} + +.country-button.active .country-flag-emoji { + filter: drop-shadow(0 2px 6px rgba(99, 32, 238, 0.4)); +} + +[data-theme="dark"] .country-button.active .country-flag-emoji { + filter: drop-shadow(0 2px 6px rgba(139, 92, 246, 0.5)); +} + +.country-flag-icon { + width: 24px; + height: 24px; + display: inline-block; + transition: all var(--transition-base); + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1)); + object-fit: contain; +} + +.country-button:hover:not(.active) .country-flag-icon { + transform: rotate(5deg); + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2)); +} + +.country-button.active .country-flag-icon { + filter: drop-shadow(0 2px 6px rgba(99, 32, 238, 0.4)); +} + +[data-theme="dark"] .country-button.active .country-flag-icon { + filter: drop-shadow(0 2px 6px rgba(139, 92, 246, 0.5)); +} + +.country-button span { + position: relative; + z-index: 1; +} + diff --git a/src/css/components/_modals.css b/src/css/components/_modals.css new file mode 100644 index 0000000..f95482a --- /dev/null +++ b/src/css/components/_modals.css @@ -0,0 +1,80 @@ +/* ============================================================================ + Lightbox Modal + ============================================================================ */ + +.lightbox { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.95); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + padding: var(--spacing-xl); + opacity: 0; + visibility: hidden; + transition: opacity var(--transition-base), visibility var(--transition-base); + backdrop-filter: blur(10px); +} + +[data-theme="dark"] .lightbox { + background: rgba(0, 0, 0, 0.98); +} + +.lightbox.active { + opacity: 1; + visibility: visible; +} + +.lightbox-close { + position: absolute; + top: var(--spacing-xl); + right: var(--spacing-xl); + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: var(--border-radius-md); + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: white; + transition: all var(--transition-fast); + z-index: 10001; +} + +.lightbox-close:hover { + background: rgba(255, 255, 255, 0.2); + border-color: rgba(255, 255, 255, 0.3); + transform: scale(1.1); +} + +.lightbox-close svg { + width: 24px; + height: 24px; +} + +.lightbox-content { + max-width: 80vw; + max-height: 75vh; + display: flex; + align-items: center; + justify-content: center; + position: relative; + width: 100%; +} + +.lightbox-image { + max-width: 100%; + max-height: 75vh; + width: auto; + height: auto; + border-radius: var(--border-radius-lg); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); + object-fit: contain; +} + diff --git a/src/css/components/_navigation.css b/src/css/components/_navigation.css new file mode 100644 index 0000000..f5f0cb3 --- /dev/null +++ b/src/css/components/_navigation.css @@ -0,0 +1,471 @@ +/* ============================================================================ + Navigation Component + ============================================================================ */ + +.navbar { + position: sticky; + top: 0; + z-index: 1000; + background-color: rgba(255, 255, 255, 0.85); + backdrop-filter: blur(20px) saturate(180%); + -webkit-backdrop-filter: blur(20px) saturate(180%); + border-bottom: 1px solid var(--color-border); + transition: all var(--transition-base); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} + +[data-theme="dark"] .navbar { + background-color: rgba(15, 23, 42, 0.85); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); +} + +.nav-container { + max-width: 1400px; + margin: 0 auto; + padding: 0 var(--spacing-xl); + display: flex; + align-items: center; + justify-content: space-between; + height: var(--navbar-height); + position: relative; + flex-wrap: nowrap; + gap: var(--spacing-md); + overflow: visible; +} + +.nav-brand { + display: flex; + align-items: center; + gap: var(--spacing-md); + font-weight: 600; + font-size: var(--font-size-lg); + color: var(--color-text-primary); + text-decoration: none; + flex-shrink: 0; + white-space: nowrap; +} + +.nav-brand a { + display: flex; + align-items: center; + gap: var(--spacing-sm); + text-decoration: none; + color: inherit; +} + +.logo-icon { + width: 80px; + height: 80px; + object-fit: contain; + display: block; +} + +.brand-text { + font-weight: 600; +} + +.nav-menu { + display: flex; + align-items: center; + gap: var(--spacing-lg); + flex-wrap: nowrap; + flex: 0 1 auto; + justify-content: flex-end; + min-width: 0; + max-width: 100%; +} + +/* Smaller gap for French, Spanish, and German (longer text) */ +html[lang="es"] .nav-menu { + gap: var(--spacing-md); +} + +/* Even smaller gap for French (longest text) */ +html[lang="fr"] .nav-menu { + gap: var(--spacing-xs); +} + +/* Slightly larger gap for German */ +html[lang="de"] .nav-menu { + gap: 12px; +} + +.nav-link { + color: var(--color-text-secondary); + text-decoration: none; + font-weight: 500; + font-size: var(--font-size-md); + transition: color var(--transition-fast); + position: relative; + white-space: nowrap; + flex-shrink: 0; + padding: var(--spacing-xs) var(--spacing-sm); + border-radius: var(--border-radius-md); +} + +.nav-link::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%) scaleX(0); + width: 80%; + height: 2px; + background: var(--gradient-accent); + border-radius: 2px; + transition: transform var(--transition-fast); +} + +.nav-link:hover { + color: var(--color-accent); + background-color: var(--color-bg-secondary); +} + +.nav-link:hover::after { + transform: translateX(-50%) scaleX(1); +} + +.nav-link.active { + color: var(--color-accent); +} + +.nav-link.active::after { + transform: translateX(-50%) scaleX(1); +} + +.nav-actions { + display: flex; + align-items: center; + gap: var(--spacing-md); + flex-shrink: 0; +} + +/* ============================================================================ + Burger Menu + ============================================================================ */ + +.burger-menu { + display: none; + flex-direction: column; + justify-content: space-around; + width: 28px; + height: 28px; + background: transparent; + border: none; + cursor: pointer; + padding: 0; + z-index: 1001; + position: relative; + flex-shrink: 0; +} + +.burger-line { + width: 100%; + height: 3px; + background-color: var(--color-text-primary); + border-radius: 2px; + transition: all 0.3s ease; + transform-origin: center; +} + +.burger-menu[aria-expanded="true"] .burger-line:nth-child(1) { + transform: rotate(45deg) translate(8px, 8px); +} + +.burger-menu[aria-expanded="true"] .burger-line:nth-child(2) { + opacity: 0; +} + +.burger-menu[aria-expanded="true"] .burger-line:nth-child(3) { + transform: rotate(-45deg) translate(8px, -8px); +} + +/* ============================================================================ + Language Selector + ============================================================================ */ + +.language-selector-wrapper { + position: relative; + display: inline-block; +} + +.language-selector-hidden { + display: none !important; +} + +.language-selector-button { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacing-md); + background: var(--gradient-card-hover); + border: 2px solid var(--color-border); + border-radius: var(--border-radius-lg); + padding: var(--spacing-sm) var(--spacing-md); + font-size: var(--font-size-sm); + font-weight: 600; + color: var(--color-text-primary); + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + min-width: 160px; + box-shadow: + 0 2px 4px rgba(0, 0, 0, 0.05), + 0 1px 2px rgba(0, 0, 0, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + position: relative; + overflow: hidden; + font-family: var(--font-family-base); +} + +.language-selector-button::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(99, 32, 238, 0.1), transparent); + transition: left 0.5s ease; +} + +[data-theme="dark"] .language-selector-button::before { + background: linear-gradient(90deg, transparent, rgba(139, 92, 246, 0.15), transparent); +} + +.language-selector-button:hover::before { + left: 100%; +} + +.language-selector-button:hover { + border-color: var(--color-accent); + background: linear-gradient(135deg, var(--color-bg-tertiary) 0%, var(--color-bg-secondary) 100%); + box-shadow: + 0 4px 12px rgba(99, 32, 238, 0.15), + 0 2px 4px rgba(0, 0, 0, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.15); + transform: translateY(-2px); +} + +[data-theme="dark"] .language-selector-button:hover { + box-shadow: + 0 4px 12px rgba(139, 92, 246, 0.2), + 0 2px 4px rgba(0, 0, 0, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.05); +} + +.language-selector-button:active { + transform: translateY(0); +} + +.language-selector-button:focus { + outline: none; + border-color: var(--color-accent); + background: linear-gradient(135deg, var(--color-bg-primary) 0%, var(--color-bg-secondary) 100%); + box-shadow: + 0 0 0 4px rgba(99, 32, 238, 0.2), + 0 4px 16px rgba(99, 32, 238, 0.15), + 0 2px 4px rgba(0, 0, 0, 0.1); + transform: translateY(-1px); +} + +[data-theme="dark"] .language-selector-button:focus { + box-shadow: + 0 0 0 4px rgba(139, 92, 246, 0.3), + 0 4px 16px rgba(139, 92, 246, 0.2), + 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.language-selector-button[aria-expanded="true"] { + border-color: var(--color-accent); + box-shadow: + 0 0 0 4px rgba(99, 32, 238, 0.2), + 0 4px 16px rgba(99, 32, 238, 0.15), + 0 2px 4px rgba(0, 0, 0, 0.1); +} + +[data-theme="dark"] .language-selector-button[aria-expanded="true"] { + box-shadow: + 0 0 0 4px rgba(139, 92, 246, 0.3), + 0 4px 16px rgba(139, 92, 246, 0.2), + 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.language-selector-text { + display: flex; + align-items: center; + gap: var(--spacing-sm); + flex: 1; +} + +.language-selector-arrow { + width: 18px; + height: 18px; + color: var(--color-text-secondary); + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); + flex-shrink: 0; +} + +.language-selector-button[aria-expanded="true"] .language-selector-arrow { + transform: rotate(180deg); + color: var(--color-accent); +} + +.language-dropdown { + position: absolute; + top: calc(100% + 8px); + left: 0; + right: 0; + background: var(--color-bg-primary); + border: 2px solid var(--color-border); + border-radius: var(--border-radius-lg); + box-shadow: + 0 10px 25px rgba(0, 0, 0, 0.15), + 0 4px 10px rgba(0, 0, 0, 0.1); + opacity: 0; + visibility: hidden; + transform: translateY(-10px); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + z-index: 1000; + overflow: hidden; + min-width: 160px; + max-height: 300px; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: var(--color-accent) var(--color-bg-secondary); +} + +.language-dropdown::-webkit-scrollbar { + width: 6px; +} + +.language-dropdown::-webkit-scrollbar-track { + background: var(--color-bg-secondary); + border-radius: var(--border-radius-sm); +} + +.language-dropdown::-webkit-scrollbar-thumb { + background: var(--color-accent); + border-radius: var(--border-radius-sm); + transition: background 0.2s ease; +} + +.language-dropdown::-webkit-scrollbar-thumb:hover { + background: var(--color-accent-hover); +} + +[data-theme="dark"] .language-dropdown { + box-shadow: + 0 10px 25px rgba(0, 0, 0, 0.4), + 0 4px 10px rgba(0, 0, 0, 0.3); +} + +.language-dropdown.active { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.language-option { + display: flex; + align-items: center; + gap: var(--spacing-md); + padding: var(--spacing-md) var(--spacing-lg); + cursor: pointer; + transition: all 0.2s ease; + border-bottom: 1px solid var(--color-border); + position: relative; + overflow: hidden; +} + +.language-option:last-child { + border-bottom: none; +} + +.language-option::before { + content: ''; + position: absolute; + left: 0; + top: 0; + width: 4px; + height: 100%; + background: var(--color-accent); + transform: scaleY(0); + transition: transform 0.2s ease; +} + +.language-option:hover { + background: var(--color-bg-secondary); + padding-left: calc(var(--spacing-lg) + 4px); +} + +.language-option:hover::before { + transform: scaleY(1); +} + +.language-option.selected { + background: linear-gradient(90deg, rgba(99, 32, 238, 0.1), transparent); + font-weight: 600; + color: var(--color-accent); +} + +[data-theme="dark"] .language-option.selected { + background: linear-gradient(90deg, rgba(139, 92, 246, 0.15), transparent); +} + +.language-option.selected::before { + transform: scaleY(1); +} + +.language-flag { + font-size: 20px; + line-height: 1; + flex-shrink: 0; +} + +.language-flag-icon { + width: 20px; + height: 20px; + display: inline-block; + flex-shrink: 0; + vertical-align: middle; +} + +.language-name { + flex: 1; + font-size: var(--font-size-sm); + font-weight: 500; + color: var(--color-text-primary); +} + +.language-option.selected .language-name { + color: var(--color-accent); + font-weight: 600; +} + +/* ============================================================================ + Theme Toggle + ============================================================================ */ + +.theme-toggle { + background: none; + border: none; + cursor: pointer; + padding: var(--spacing-sm); + display: flex; + align-items: center; + justify-content: center; + color: var(--color-text-secondary); + transition: color var(--transition-fast), background-color var(--transition-fast); + border-radius: var(--border-radius-md); +} + +.theme-toggle:hover { + color: var(--color-accent); + background-color: var(--color-bg-secondary); +} + +.theme-icon { + width: 20px; + height: 20px; +} + diff --git a/src/css/components/_tables.css b/src/css/components/_tables.css new file mode 100644 index 0000000..fe65cd4 --- /dev/null +++ b/src/css/components/_tables.css @@ -0,0 +1,136 @@ +/* ============================================================================ + Table Components + ============================================================================ */ + +.table-container { + overflow-x: auto; + border-radius: var(--border-radius-xl); + border: 1px solid var(--color-border); + margin-bottom: var(--spacing-xl); + background: var(--color-bg-primary); + -webkit-overflow-scrolling: touch; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); +} + +[data-theme="dark"] .table-container { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.data-table-wrapper { + overflow-x: auto; + margin-bottom: var(--spacing-xl); + -webkit-overflow-scrolling: touch; +} + +/* Reduced margins for table containers in compact sections */ +.section-compact .table-container { + margin-bottom: 0; +} + +.data-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + background: var(--color-bg-primary); + table-layout: fixed; + border-radius: var(--border-radius-xl); + border: 1px solid var(--color-border); + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); +} + +[data-theme="dark"] .data-table { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.data-table thead { + background: linear-gradient(135deg, var(--color-bg-secondary) 0%, var(--color-bg-tertiary) 100%); +} + +.data-table thead tr:first-child th:first-child { + border-top-left-radius: var(--border-radius-xl); +} + +.data-table thead tr:first-child th:last-child { + border-top-right-radius: var(--border-radius-xl); +} + +.data-table tbody tr:last-child td:first-child { + border-bottom-left-radius: var(--border-radius-xl); +} + +.data-table tbody tr:last-child td:last-child { + border-bottom-right-radius: var(--border-radius-xl); +} + +.data-table th { + padding: 16px 12px; + text-align: left; + font-weight: 600; + font-size: var(--font-size-sm); + color: var(--color-text-primary); + border-bottom: 2px solid var(--color-border); + box-sizing: border-box; + letter-spacing: 0.3px; +} + +.data-table td { + padding: 14px 12px; + font-size: var(--font-size-base); + color: var(--color-text-primary); + border-bottom: 1px solid var(--color-border); + vertical-align: middle; + word-wrap: break-word; + overflow-wrap: break-word; + text-align: left; + box-sizing: border-box; +} + +.data-table th:first-child, +.data-table td:first-child { + padding-left: 16px; +} + +.data-table[data-table="personal-data-numbers"] th:nth-child(3), +.data-table[data-table="personal-data-text"] th:nth-child(3), +.data-table[data-table="banking-secrecy"] th:nth-child(2) { + text-align: left !important; +} + +.data-table[data-table="personal-data-numbers"] td:nth-child(3), +.data-table[data-table="personal-data-text"] td:nth-child(3), +.data-table[data-table="banking-secrecy"] td:nth-child(2) { + text-align: left !important; + color: var(--color-text-secondary); +} + +.data-table tbody tr { + transition: all var(--transition-fast); +} + +.data-table tbody tr:hover { + background: var(--color-bg-secondary); + transform: scale(1.001); + box-shadow: 0 2px 8px rgba(99, 32, 238, 0.08); +} + +[data-theme="dark"] .data-table tbody tr:hover { + box-shadow: 0 2px 8px rgba(139, 92, 246, 0.12); +} + +.data-table tbody tr[style*="display: none"] { + display: none !important; +} + +.data-table tbody tr:last-child td { + border-bottom: none; +} + +.table-empty-message { + padding: var(--spacing-2xl); + text-align: center; + color: var(--color-text-secondary); + font-size: var(--font-size-md); + font-style: italic; +} + diff --git a/src/css/layout/_responsive.css b/src/css/layout/_responsive.css new file mode 100644 index 0000000..5e766a9 --- /dev/null +++ b/src/css/layout/_responsive.css @@ -0,0 +1,604 @@ +/* ============================================================================ + Responsive Design - All Media Queries + ============================================================================ */ + +/* Container responsive */ +@media (max-width: 768px) { + .container { + padding: 0 var(--spacing-lg); + } +} + +/* Hero responsive */ +@media (max-width: 968px) { + .hero .container { + grid-template-columns: 1fr; + gap: var(--spacing-3xl); + } + + .hero-content { + padding-right: 0; + text-align: left; + max-width: 100%; + } + + .hero-title { + font-size: clamp(1.75rem, 6vw, 2.5rem); + text-align: left; + } + + .hero-description { + text-align: left; + margin-left: 0; + margin-right: 0; + font-size: var(--font-size-base); + } + + .hero-features { + justify-items: start; + max-width: 100%; + margin-left: 0; + margin-right: 0; + } + + .hero-actions { + justify-content: flex-start; + } +} + +/* Navigation responsive */ +@media (min-width: 1025px) { + .nav-menu { + gap: var(--spacing-xl); + } + + .nav-link { + font-size: var(--font-size-base); + } +} + +@media (max-width: 1200px) { + .nav-menu { + gap: var(--spacing-lg); + } + + .nav-link { + font-size: var(--font-size-sm); + } + + .nav-container { + padding: 0 var(--spacing-md); + } + + .nav-brand { + font-size: var(--font-size-base); + } +} + +@media (max-width: 1024px) { + .burger-menu { + display: flex; + } + + .nav-menu { + position: fixed; + top: var(--navbar-height); + left: 0; + right: 0; + background: var(--color-bg-primary); + border-bottom: 1px solid var(--color-border); + box-shadow: var(--shadow-lg); + flex-direction: column; + align-items: stretch; + gap: 0; + padding: var(--spacing-md) 0; + transform: translateY(-100%); + opacity: 0; + visibility: hidden; + transition: transform var(--transition-base), opacity var(--transition-base), visibility var(--transition-base); + z-index: 1000; + } + + .nav-menu.active { + transform: translateY(0); + opacity: 1; + visibility: visible; + } + + .nav-link { + padding: var(--spacing-md) var(--spacing-lg); + border-bottom: 1px solid var(--color-border); + width: 100%; + display: block; + white-space: normal; + } + + .nav-link::after { + left: var(--spacing-lg); + transform: scaleX(0); + transform-origin: left; + width: calc(100% - calc(var(--spacing-lg) * 2)); + height: 3px; + background: linear-gradient(90deg, var(--color-accent) 0%, var(--color-accent-hover) 50%, transparent 100%); + box-shadow: 0 2px 8px rgba(99, 32, 238, 0.3), 0 0 20px rgba(99, 32, 238, 0.1); + border-radius: 3px; + opacity: 0; + transition: all var(--transition-base); + } + + [data-theme="dark"] .nav-link::after { + box-shadow: 0 2px 8px rgba(139, 92, 246, 0.4), 0 0 20px rgba(139, 92, 246, 0.15); + } + + .nav-link:hover::after, + .nav-link.active::after { + transform: scaleX(1); + opacity: 1; + box-shadow: 0 2px 12px rgba(99, 32, 238, 0.5), 0 0 24px rgba(99, 32, 238, 0.2); + } + + [data-theme="dark"] .nav-link:hover::after, + [data-theme="dark"] .nav-link.active::after { + box-shadow: 0 2px 12px rgba(139, 92, 246, 0.6), 0 0 24px rgba(139, 92, 246, 0.25); + } + + .nav-link:last-child { + border-bottom: none; + } + + .nav-link:hover { + background: var(--color-bg-secondary); + } + + .nav-menu { + gap: var(--spacing-md); + } + + .nav-link { + font-size: var(--font-size-sm); + } + + .nav-actions { + gap: var(--spacing-sm); + } + + .language-selector-button { + min-width: 140px; + padding: var(--spacing-xs) var(--spacing-sm); + font-size: var(--font-size-xs); + } +} + +/* Features responsive */ +@media (max-width: 1024px) { + .features-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + .features-section { + background: var(--color-bg-primary); + } +} + +/* Use Cases responsive */ +@media (max-width: 1024px) { + .use-cases { + grid-template-columns: repeat(2, 1fr); + } +} + +/* Mobile */ +@media (max-width: 768px) { + .logo-icon { + width: 56px; + height: 56px; + } + + .hero { + padding: var(--spacing-2xl) 0; + } + + .hero-content { + padding-right: 0; + } + + .badge { + margin-left: 0; + } + + .section { + padding: var(--spacing-2xl) 0; + } + + .country-selector-wrapper { + padding: var(--spacing-md); + } + + .country-selector-header { + margin-bottom: var(--spacing-md); + } + + .country-buttons-group { + gap: var(--spacing-sm); + } + + .country-button { + flex: 1; + min-width: calc(50% - var(--spacing-sm)); + padding: var(--spacing-sm) var(--spacing-md); + font-size: var(--font-size-sm); + } + + .country-button-icon { + width: 18px; + height: 18px; + } + + .country-selector-globe-icon { + width: 20px; + height: 20px; + } + + .country-selector-label { + font-size: var(--font-size-base); + } + + .hero-title { + font-size: var(--font-size-2xl); + } + + .hero-description { + max-width: 100%; + } + + .section-title { + font-size: var(--font-size-2xl); + } + + .nav-brand { + order: 2; + flex: 1; + justify-content: flex-start; + min-width: 0; + gap: var(--spacing-sm); + } + + .brand-text { + display: block; + font-size: var(--font-size-sm); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .burger-menu { + display: flex; + order: 3; + margin-left: var(--spacing-sm); + margin-right: var(--spacing-md); + } + + .nav-actions { + order: 2; + gap: var(--spacing-sm); + flex-shrink: 0; + } + + /* Smaller gap for French, Spanish, and German versions on mobile */ + html[lang="es"] .nav-actions, + html[lang="fr"] .nav-actions, + html[lang="de"] .nav-actions { + gap: 2px; + } + + /* French mobile - prevent brand text from overlapping theme button */ + html[lang="fr"] .nav-brand { + padding-right: var(--spacing-sm); + max-width: calc(100% - 120px); + } + + html[lang="fr"] .brand-text { + font-size: 11px; + } + + /* Language selector - only flags on mobile */ + .language-selector-button { + min-width: auto; + width: 40px; + height: 40px; + padding: 0; + justify-content: center; + } + + .language-selector-text { + font-size: 0; + gap: 0; + position: relative; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + } + + .language-selector-text::before { + display: none; + } + + .language-selector-text .language-flag-icon { + width: 24px; + height: 24px; + } + + .language-selector-arrow { + display: none; + } + + .language-dropdown { + right: 0; + left: auto; + min-width: 120px; + } + + .language-option { + padding: var(--spacing-sm) var(--spacing-md); + gap: var(--spacing-sm); + } + + .language-name { + font-size: var(--font-size-sm); + } + + .nav-menu { + position: fixed; + top: var(--navbar-height); + left: 0; + right: 0; + background: var(--color-bg-primary); + border-bottom: 1px solid var(--color-border); + box-shadow: var(--shadow-lg); + flex-direction: column; + align-items: stretch; + gap: 0; + padding: var(--spacing-md) 0; + transform: translateY(-100%); + opacity: 0; + visibility: hidden; + transition: transform var(--transition-base), opacity var(--transition-base), visibility var(--transition-base); + z-index: 1000; + } + + .nav-menu.active { + transform: translateY(0); + opacity: 1; + visibility: visible; + } + + .nav-link { + padding: var(--spacing-md) var(--spacing-lg); + border-bottom: 1px solid var(--color-border); + width: 100%; + display: block; + } + + .nav-link:last-child { + border-bottom: none; + } + + .nav-link:hover { + background: var(--color-bg-secondary); + } + + .hero-actions { + flex-direction: column; + } + + .hero-features { + grid-template-columns: 1fr; + gap: var(--spacing-sm); + max-width: 100%; + } + + .feature-item { + font-size: var(--font-size-sm); + } + + .btn { + width: 100%; + } + + .features-grid, + .download-grid, + .use-cases { + grid-template-columns: 1fr !important; + } + + .table-container { + font-size: 10px; + } + + .data-table { + width: 100% !important; + table-layout: fixed !important; + } + + .data-table th, + .data-table td { + padding: 8px 4px; + font-size: 10px; + } + + .data-table th:first-child, + .data-table td:first-child { + padding-left: 8px; + } + + .data-table code { + font-size: 10px; + padding: 1px 3px; + } + + /* Adjust column widths for mobile - make example column wider */ + .data-table[data-table="personal-data-numbers"] colgroup col:nth-child(1) { + width: 18% !important; + } + + .data-table[data-table="personal-data-numbers"] colgroup col:nth-child(2) { + width: 30% !important; + } + + .data-table[data-table="personal-data-numbers"] colgroup col:nth-child(3) { + width: 12% !important; + } + + .data-table[data-table="personal-data-numbers"] colgroup col:nth-child(4) { + width: 40% !important; + } + + .data-table[data-table="personal-data-text"] colgroup col:nth-child(1) { + width: 18% !important; + } + + .data-table[data-table="personal-data-text"] colgroup col:nth-child(2) { + width: 18% !important; + } + + .data-table[data-table="personal-data-text"] colgroup col:nth-child(3) { + width: 14% !important; + } + + .data-table[data-table="personal-data-text"] colgroup col:nth-child(4) { + width: 50% !important; + } + + /* Banking Secrecy table - make example column wider */ + .data-table[data-table="banking-secrecy"] colgroup col:nth-child(1) { + width: 25% !important; + } + + .data-table[data-table="banking-secrecy"] colgroup col:nth-child(2) { + width: 20% !important; + } + + .data-table[data-table="banking-secrecy"] colgroup col:nth-child(3) { + width: 55% !important; + } + + /* PCI DSS and IT Assets - make example column wider */ + .data-table[data-table="pci-dss"] colgroup col:nth-child(1), + .data-table[data-table="it-assets"] colgroup col:nth-child(1) { + width: 40% !important; + } + + .data-table[data-table="pci-dss"] colgroup col:nth-child(2), + .data-table[data-table="it-assets"] colgroup col:nth-child(2) { + width: 60% !important; + } + + /* Screenshot adjustments for mobile */ + .hero-image { + padding: var(--spacing-md); + } + + .screenshot-decoration { + display: none; + } + + .screenshot-container { + padding: 4px; + } + + .screenshot-container:hover { + transform: translateY(-4px); + } + + /* Lightbox mobile adjustments */ + .lightbox { + padding: var(--spacing-md); + } + + .lightbox-close { + top: var(--spacing-md); + right: var(--spacing-md); + width: 40px; + height: 40px; + } + + .lightbox-content { + max-width: 90vw; + max-height: 80vh; + } + + .lightbox-image { + max-height: 80vh; + border-radius: var(--border-radius-md); + } +} + +/* Small Mobile */ +@media (max-width: 480px) { + .container { + padding: 0 var(--spacing-md); + } + + .nav-container { + padding: 0 var(--spacing-md); + } + + .hero-title { + font-size: var(--font-size-2xl); + } + + .section-title { + font-size: var(--font-size-xl); + } + + .country-selector-wrapper { + padding: var(--spacing-md); + } + + .country-buttons-group { + flex-direction: column; + gap: var(--spacing-sm); + } + + .country-button { + width: 100%; + min-width: unset; + justify-content: flex-start; + } +} + +/* Responsive adjustments for new sections */ +@media (max-width: 968px) { + .overview-grid, + .quick-links-grid, + .who-should-use-grid, + .getting-started-steps { + grid-template-columns: 1fr; + } + + .breadcrumbs { + padding: var(--spacing-sm) 0; + } + + .footer-top-row { + flex-direction: column; + align-items: flex-start; + gap: var(--spacing-md); + } + + .footer-links { + flex-direction: column; + align-items: flex-start; + gap: var(--spacing-sm); + } + + .footer-pages { + justify-content: flex-start; + gap: var(--spacing-md); + } +} + diff --git a/src/css/sections/_download.css b/src/css/sections/_download.css new file mode 100644 index 0000000..71fbfea --- /dev/null +++ b/src/css/sections/_download.css @@ -0,0 +1,122 @@ +/* ============================================================================ + Download Section + ============================================================================ */ + +.download-section { + background: var(--color-bg-secondary); +} + +.download-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: var(--spacing-lg); +} + +.download-card { + background: var(--color-bg-primary); + padding: var(--spacing-xl) var(--spacing-lg); + border-radius: var(--border-radius-xl); + border: 1px solid var(--color-border); + transition: all var(--transition-base); + position: relative; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); +} + +[data-theme="dark"] .download-card { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.download-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: var(--gradient-accent); + transform: scaleX(0); + transform-origin: left; + transition: transform var(--transition-base); +} + +.download-card:hover::before { + transform: scaleX(1); +} + +.download-card:hover { + transform: translateY(-6px); + box-shadow: 0 12px 32px rgba(99, 32, 238, 0.15), 0 4px 12px rgba(0, 0, 0, 0.08); + border-color: var(--color-accent); +} + +[data-theme="dark"] .download-card:hover { + box-shadow: 0 12px 32px rgba(139, 92, 246, 0.2), 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.download-platform { + display: flex; + align-items: center; + gap: var(--spacing-md); + margin-bottom: var(--spacing-lg); +} + +.platform-icon { + width: 40px; + height: 40px; + color: var(--color-accent); + transition: all var(--transition-base); +} + +.download-card:hover .platform-icon { + transform: scale(1.1); + filter: drop-shadow(0 4px 8px rgba(99, 32, 238, 0.3)); +} + +[data-theme="dark"] .download-card:hover .platform-icon { + filter: drop-shadow(0 4px 8px rgba(139, 92, 246, 0.4)); +} + +.platform-name { + font-size: var(--font-size-xl); + font-weight: 600; + color: var(--color-text-primary); + transition: color var(--transition-fast); +} + +.download-card:hover .platform-name { + color: var(--color-accent); +} + +.download-links { + display: flex; + flex-direction: column; + gap: var(--spacing-md); +} + +.download-link { + display: flex; + align-items: center; + gap: var(--spacing-md); + padding: var(--spacing-md) var(--spacing-md); + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-lg); + color: var(--color-text-primary); + text-decoration: none; + font-weight: 500; + font-size: var(--font-size-base); + transition: all var(--transition-fast); +} + +.download-link:hover { + background: var(--color-bg-tertiary); + border-color: var(--color-accent); + color: var(--color-accent); +} + +.download-link svg { + width: 18px; + height: 18px; +} + diff --git a/src/css/sections/_features.css b/src/css/sections/_features.css new file mode 100644 index 0000000..bfda446 --- /dev/null +++ b/src/css/sections/_features.css @@ -0,0 +1,113 @@ +/* ============================================================================ + Features Section + ============================================================================ */ + +.features-section { + background: var(--color-bg-secondary); +} + +.features-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: var(--spacing-xl); +} + +.feature-card { + background: var(--color-bg-primary); + padding: var(--spacing-xl) var(--spacing-lg); + border-radius: var(--border-radius-xl); + border: 1px solid var(--color-border); + transition: all var(--transition-base); + position: relative; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); +} + +[data-theme="dark"] .feature-card { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.feature-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: var(--gradient-accent); + transform: scaleX(0); + transform-origin: left; + transition: transform var(--transition-base); +} + +.feature-card:hover::before { + transform: scaleX(1); +} + +.feature-card:hover { + transform: translateY(-6px); + box-shadow: 0 12px 32px rgba(99, 32, 238, 0.15), 0 4px 12px rgba(0, 0, 0, 0.08); + border-color: var(--color-accent); +} + +[data-theme="dark"] .feature-card:hover { + box-shadow: 0 12px 32px rgba(139, 92, 246, 0.2), 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.feature-icon { + width: 52px; + height: 52px; + color: var(--color-accent); + margin-bottom: var(--spacing-lg); + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, rgba(99, 32, 238, 0.1) 0%, rgba(99, 32, 238, 0.05) 100%); + border-radius: var(--border-radius-lg); + padding: var(--spacing-sm); + transition: all var(--transition-base); +} + +[data-theme="dark"] .feature-icon { + background: linear-gradient(135deg, rgba(139, 92, 246, 0.15) 0%, rgba(139, 92, 246, 0.08) 100%); +} + +.feature-card:hover .feature-icon { + transform: scale(1.08) rotate(3deg); + background: linear-gradient(135deg, rgba(99, 32, 238, 0.2) 0%, rgba(99, 32, 238, 0.1) 100%); +} + +[data-theme="dark"] .feature-card:hover .feature-icon { + background: linear-gradient(135deg, rgba(139, 92, 246, 0.25) 0%, rgba(139, 92, 246, 0.15) 100%); +} + +.feature-icon svg { + width: 28px; + height: 28px; + filter: drop-shadow(0 2px 4px rgba(99, 32, 238, 0.2)); +} + +[data-theme="dark"] .feature-icon svg { + filter: drop-shadow(0 2px 4px rgba(139, 92, 246, 0.3)); +} + +.feature-title { + font-size: var(--font-size-xl); + font-weight: 600; + color: var(--color-text-primary); + margin-bottom: var(--spacing-md); + margin-top: 0; + transition: color var(--transition-fast); +} + +.feature-card:hover .feature-title { + color: var(--color-accent); +} + +.feature-description { + color: var(--color-text-secondary); + font-size: var(--font-size-base); + line-height: 1.7; + margin: 0; +} + diff --git a/src/css/sections/_footer.css b/src/css/sections/_footer.css new file mode 100644 index 0000000..14bd685 --- /dev/null +++ b/src/css/sections/_footer.css @@ -0,0 +1,156 @@ +/* ============================================================================ + Footer Component + ============================================================================ */ + +.footer { + padding: var(--spacing-3xl) 0 var(--spacing-xl); + border-top: 1px solid var(--color-border); + background: var(--color-bg-secondary); + margin-top: var(--spacing-4xl); +} + +.footer-content { + display: flex; + flex-direction: column; + gap: var(--spacing-xl); +} + +.footer-content > p:first-child { + display: inline-block; +} + +.footer-top-row { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: var(--spacing-lg); + padding-bottom: var(--spacing-lg); + border-bottom: 1px solid var(--color-border); +} + +.footer-content p { + color: var(--color-text-secondary); + font-size: var(--font-size-sm); + margin: 0; +} + +.footer-links { + display: flex; + gap: var(--spacing-lg); + align-items: center; +} + +.footer-links a { + color: var(--color-text-secondary); + text-decoration: none; + font-size: var(--font-size-sm); + font-weight: 500; + transition: color var(--transition-fast); + padding: var(--spacing-xs) 0; + position: relative; +} + +.footer-links a::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 0; + height: 2px; + background: var(--color-accent); + transition: width var(--transition-fast); +} + +.footer-links a:hover { + color: var(--color-accent); +} + +.footer-links a:hover::after { + width: 100%; +} + +.footer-pages { + display: flex; + gap: var(--spacing-lg); + flex-wrap: wrap; + justify-content: center; + align-items: center; +} + +.footer-pages a { + color: var(--color-text-secondary); + text-decoration: none; + font-size: var(--font-size-sm); + font-weight: 500; + transition: all var(--transition-fast); + padding: var(--spacing-xs) var(--spacing-sm); + border-radius: var(--border-radius-sm); + position: relative; + z-index: 1; +} + +.footer-pages a::before { + content: ''; + position: absolute; + inset: 0; + border-radius: var(--border-radius-sm); + background: var(--color-bg-tertiary); + opacity: 0; + transition: opacity var(--transition-fast); +} + +.footer-pages a:hover { + color: var(--color-accent); +} + +.footer-pages a:hover::before { + opacity: 1; +} + +/* Breadcrumbs */ +.breadcrumbs { + padding: var(--spacing-lg) 0; + background: var(--color-bg-primary); + border-bottom: 1px solid var(--color-border); +} + +.breadcrumbs-list { + display: flex; + align-items: center; + gap: var(--spacing-sm); + list-style: none; + flex-wrap: wrap; +} + +.breadcrumbs-item { + display: flex; + align-items: center; + font-size: var(--font-size-sm); + color: var(--color-text-secondary); +} + +.breadcrumbs-item:not(:last-child)::after { + content: '/'; + margin-left: var(--spacing-sm); + color: var(--color-text-tertiary); +} + +.breadcrumbs-item a { + color: var(--color-text-secondary); + text-decoration: none; + transition: color var(--transition-fast); + padding: var(--spacing-xs) var(--spacing-sm); + border-radius: var(--border-radius-sm); +} + +.breadcrumbs-item a:hover { + color: var(--color-accent); + background-color: var(--color-bg-secondary); +} + +.breadcrumbs-item[aria-current="page"] { + color: var(--color-text-primary); + font-weight: 600; +} + diff --git a/src/css/sections/_hero.css b/src/css/sections/_hero.css new file mode 100644 index 0000000..220fdb4 --- /dev/null +++ b/src/css/sections/_hero.css @@ -0,0 +1,304 @@ +/* ============================================================================ + Hero Section + ============================================================================ */ + +.hero { + padding: var(--spacing-2xl) 0 var(--spacing-4xl); + background: linear-gradient(135deg, var(--color-bg-primary) 0%, var(--color-bg-secondary) 30%, var(--color-bg-primary) 100%); + position: relative; + overflow: hidden; +} + +.hero::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: radial-gradient(circle at 20% 50%, rgba(99, 32, 238, 0.04) 0%, transparent 40%), + radial-gradient(circle at 80% 80%, rgba(99, 32, 238, 0.03) 0%, transparent 40%); + pointer-events: none; + z-index: 0; +} + +[data-theme="dark"] .hero::before { + background: radial-gradient(circle at 20% 50%, rgba(139, 92, 246, 0.06) 0%, transparent 40%), + radial-gradient(circle at 80% 80%, rgba(139, 92, 246, 0.04) 0%, transparent 40%); +} + +.hero .container { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: 1.1fr 0.9fr; + gap: var(--spacing-4xl); + align-items: center; + max-width: 1400px; +} + +.hero-content { + display: flex; + flex-direction: column; + gap: var(--spacing-xl); + max-width: 680px; + padding-right: var(--spacing-lg); +} + +.badge { + display: inline-block; + padding: var(--spacing-xs) var(--spacing-md); + background: var(--gradient-accent); + color: white; + border-radius: var(--border-radius-md); + font-size: var(--font-size-xs); + font-weight: 600; + width: fit-content; + text-transform: uppercase; + letter-spacing: 0.5px; + box-shadow: 0 4px 12px rgba(99, 32, 238, 0.25); + transition: transform var(--transition-fast), box-shadow var(--transition-fast); + margin-bottom: var(--spacing-sm); +} + +[data-theme="dark"] .badge { + box-shadow: 0 4px 12px rgba(139, 92, 246, 0.35); +} + +.badge:hover { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(99, 32, 238, 0.35); +} + +[data-theme="dark"] .badge:hover { + box-shadow: 0 6px 16px rgba(139, 92, 246, 0.45); +} + +.hero-title { + font-size: clamp(2rem, 5vw, 3.5rem); + font-weight: 700; + line-height: 1.1; + color: var(--color-text-primary); + letter-spacing: -0.02em; + margin-bottom: var(--spacing-lg); + margin-top: var(--spacing-sm); + background: linear-gradient(135deg, var(--color-text-primary) 0%, var(--color-text-secondary) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +[data-theme="dark"] .hero-title { + background: linear-gradient(135deg, var(--color-text-primary) 0%, var(--color-text-secondary) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.hero-description { + font-size: var(--font-size-lg); + color: var(--color-text-secondary); + line-height: 1.75; + font-weight: 400; + max-width: 100%; + margin-bottom: var(--spacing-sm); +} + +.hero-features { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: var(--spacing-sm) var(--spacing-lg); + margin-top: var(--spacing-md); + margin-bottom: var(--spacing-sm); +} + +.feature-item { + display: flex; + align-items: flex-start; + gap: var(--spacing-sm); + color: var(--color-text-secondary); + font-size: var(--font-size-sm); + line-height: 1.6; + padding: var(--spacing-xs) var(--spacing-sm); + border-radius: var(--border-radius-md); + transition: all var(--transition-fast); +} + +.feature-item:hover { + background-color: var(--color-bg-secondary); + transform: translateX(4px); + color: var(--color-text-primary); +} + +.check-icon { + width: 20px; + height: 20px; + color: var(--color-success); + flex-shrink: 0; + filter: drop-shadow(0 2px 4px rgba(16, 185, 129, 0.2)); + margin-top: 2px; +} + +[data-theme="dark"] .check-icon { + filter: drop-shadow(0 2px 4px rgba(52, 211, 153, 0.3)); +} + +.hero-actions { + display: flex; + gap: var(--spacing-md); + margin-top: var(--spacing-lg); + flex-wrap: wrap; + align-items: center; +} + +/* Hero Screenshot Component */ +.hero-image { + display: flex; + align-items: center; + justify-content: center; + position: relative; + padding: var(--spacing-xl); +} + +.screenshot-wrapper { + position: relative; + width: 100%; + max-width: 600px; + display: flex; + align-items: center; + justify-content: center; +} + +.screenshot-container { + position: relative; + z-index: 2; + width: 100%; + border-radius: var(--border-radius-xl); + overflow: hidden; + transition: transform var(--transition-base), box-shadow var(--transition-base); + background: var(--color-bg-primary); + padding: 8px; + border: 1px solid var(--color-border); + cursor: pointer; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); +} + +[data-theme="dark"] .screenshot-container { + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); +} + +.screenshot-container:hover { + transform: translateY(-8px) scale(1.02); + box-shadow: + 0 24px 48px -12px rgba(99, 32, 238, 0.2), + 0 8px 24px rgba(0, 0, 0, 0.12), + 0 0 0 1px var(--color-accent); +} + +[data-theme="dark"] .screenshot-container:hover { + box-shadow: + 0 24px 48px -12px rgba(139, 92, 246, 0.3), + 0 8px 24px rgba(0, 0, 0, 0.4), + 0 0 0 1px var(--color-accent); +} + +.screenshot-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0); + display: flex; + align-items: center; + justify-content: center; + transition: background var(--transition-base); + border-radius: var(--border-radius-xl); + pointer-events: none; +} + +.screenshot-container:hover .screenshot-overlay { + background: rgba(0, 0, 0, 0.3); + pointer-events: all; +} + +.expand-icon { + width: 48px; + height: 48px; + color: white; + opacity: 0; + transform: scale(0.8); + transition: opacity var(--transition-base), transform var(--transition-base); +} + +.screenshot-container:hover .expand-icon { + opacity: 1; + transform: scale(1); +} + +.screenshot-image { + width: 100%; + height: auto; + display: block; + border-radius: calc(var(--border-radius-xl) - 6px); + transition: opacity var(--transition-base); + opacity: 1; + position: relative; + z-index: 2; +} + +/* Decorative elements */ +.screenshot-decoration { + position: absolute; + border-radius: 50%; + background: var(--color-accent); + opacity: 0.05; + filter: blur(50px); + z-index: 1; + animation: float 12s ease-in-out infinite; +} + +[data-theme="dark"] .screenshot-decoration { + opacity: 0.08; +} + +.screenshot-decoration-1 { + width: 350px; + height: 350px; + top: -80px; + left: -80px; + animation-delay: 0s; + background: radial-gradient(circle, var(--color-accent) 0%, transparent 70%); +} + +.screenshot-decoration-2 { + width: 250px; + height: 250px; + bottom: -40px; + right: -40px; + animation-delay: 4s; + background: radial-gradient(circle, var(--color-accent) 0%, transparent 70%); +} + +.screenshot-decoration-3 { + width: 180px; + height: 180px; + top: 50%; + right: -30px; + transform: translateY(-50%); + animation-delay: 8s; + background: radial-gradient(circle, var(--color-accent) 0%, transparent 70%); +} + +@keyframes float { + 0%, 100% { + transform: translate(0, 0) scale(1); + } + 33% { + transform: translate(20px, -20px) scale(1.1); + } + 66% { + transform: translate(-15px, 15px) scale(0.95); + } +} + diff --git a/src/css/sections/_other.css b/src/css/sections/_other.css new file mode 100644 index 0000000..3aeb1e5 --- /dev/null +++ b/src/css/sections/_other.css @@ -0,0 +1,376 @@ +/* ============================================================================ + Other Sections (Overview, Quick Links, Download CTA, Preview, Who Should Use, Getting Started) + ============================================================================ */ + +/* Overview Section */ +.overview-section { + background: var(--color-bg-secondary); +} + +.overview-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: var(--spacing-xl); + margin-top: var(--spacing-2xl); +} + +.overview-card { + background: var(--color-bg-primary); + padding: var(--spacing-xl); + border-radius: var(--border-radius-lg); + border: 1px solid var(--color-border); + transition: transform var(--transition-base), box-shadow var(--transition-base); +} + +.overview-card:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-lg); +} + +.overview-icon { + width: 48px; + height: 48px; + color: var(--color-accent); + margin-bottom: var(--spacing-md); +} + +.overview-title { + font-size: var(--font-size-xl); + font-weight: 600; + margin-bottom: var(--spacing-sm); + color: var(--color-text-primary); +} + +.overview-description { + color: var(--color-text-secondary); + line-height: 1.6; + margin-bottom: var(--spacing-md); +} + +.overview-link { + color: var(--color-accent); + text-decoration: none; + font-weight: 500; + display: inline-flex; + align-items: center; + gap: var(--spacing-xs); + transition: color var(--transition-fast); +} + +.overview-link:hover { + color: var(--color-accent-hover); +} + +/* Quick Links Section */ +.quick-links-section { + background: var(--color-bg-primary); + position: relative; + padding-top: var(--spacing-4xl); +} + +.quick-links-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: var(--spacing-xl); + margin-top: var(--spacing-3xl); + max-width: 1400px; + margin-left: auto; + margin-right: auto; +} + +.quick-link-card { + background: var(--color-bg-primary); + padding: var(--spacing-xl) var(--spacing-lg); + border-radius: var(--border-radius-xl); + border: 1px solid var(--color-border); + text-decoration: none; + transition: all var(--transition-base); + display: flex; + flex-direction: column; + position: relative; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + height: 100%; +} + +.quick-link-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: var(--gradient-accent); + transform: scaleX(0); + transform-origin: left; + transition: transform var(--transition-base); +} + +.quick-link-card:hover::before { + transform: scaleX(1); +} + +.quick-link-card:hover { + transform: translateY(-6px); + box-shadow: 0 12px 32px rgba(99, 32, 238, 0.15), 0 4px 12px rgba(0, 0, 0, 0.08); + border-color: var(--color-accent); + background: var(--color-bg-secondary); +} + +[data-theme="dark"] .quick-link-card { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +[data-theme="dark"] .quick-link-card:hover { + box-shadow: 0 12px 32px rgba(139, 92, 246, 0.2), 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.quick-link-card h3 { + font-size: var(--font-size-xl); + font-weight: 600; + margin-bottom: var(--spacing-sm); + margin-top: 0; + color: var(--color-text-primary); + transition: color var(--transition-fast); +} + +.quick-link-card:hover h3 { + color: var(--color-accent); +} + +.quick-link-card p { + color: var(--color-text-secondary); + font-size: var(--font-size-sm); + line-height: 1.6; + margin: 0; + flex-grow: 1; +} + +.quick-link-icon { + width: 52px; + height: 52px; + color: var(--color-accent); + margin-bottom: var(--spacing-md); + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, rgba(99, 32, 238, 0.1) 0%, rgba(99, 32, 238, 0.05) 100%); + border-radius: var(--border-radius-lg); + transition: all var(--transition-base); + position: relative; + flex-shrink: 0; +} + +[data-theme="dark"] .quick-link-icon { + background: linear-gradient(135deg, rgba(139, 92, 246, 0.15) 0%, rgba(139, 92, 246, 0.08) 100%); +} + +.quick-link-card:hover .quick-link-icon { + transform: scale(1.08) rotate(3deg); + background: linear-gradient(135deg, rgba(99, 32, 238, 0.2) 0%, rgba(99, 32, 238, 0.1) 100%); +} + +[data-theme="dark"] .quick-link-card:hover .quick-link-icon { + background: linear-gradient(135deg, rgba(139, 92, 246, 0.25) 0%, rgba(139, 92, 246, 0.15) 100%); +} + +.quick-link-icon svg { + width: 24px; + height: 24px; + filter: drop-shadow(0 2px 4px rgba(99, 32, 238, 0.2)); +} + +[data-theme="dark"] .quick-link-icon svg { + filter: drop-shadow(0 2px 4px rgba(139, 92, 246, 0.3)); +} + +/* Download CTA Section */ +.download-cta-section { + background: var(--color-bg-secondary); + text-align: center; +} + +.download-cta-content { + max-width: 600px; + margin: 0 auto; + padding: var(--spacing-3xl) 0; +} + +.download-cta-content .section-title { + margin-bottom: var(--spacing-md); +} + +.download-cta-content .section-description { + margin-bottom: var(--spacing-xl); + color: var(--color-text-secondary); +} + +/* Preview Section */ +.preview-section { + background: var(--color-bg-secondary); +} + +.preview-content { + text-align: center; + max-width: 800px; + margin: var(--spacing-2xl) auto 0; +} + +.preview-content p { + color: var(--color-text-secondary); + font-size: var(--font-size-lg); + line-height: 1.7; + margin-bottom: var(--spacing-xl); +} + +/* Who Should Use Section */ +.who-should-use-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: var(--spacing-xl); + margin-top: var(--spacing-2xl); +} + +.who-should-use-card { + background: var(--color-bg-primary); + padding: var(--spacing-xl) var(--spacing-lg); + border-radius: var(--border-radius-xl); + border: 1px solid var(--color-border); + transition: all var(--transition-base); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + position: relative; + overflow: hidden; +} + +[data-theme="dark"] .who-should-use-card { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.who-should-use-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: var(--gradient-accent); + transform: scaleX(0); + transform-origin: left; + transition: transform var(--transition-base); +} + +.who-should-use-card:hover::before { + transform: scaleX(1); +} + +.who-should-use-card:hover { + transform: translateY(-4px); + box-shadow: 0 12px 32px rgba(99, 32, 238, 0.15), 0 4px 12px rgba(0, 0, 0, 0.08); + border-color: var(--color-accent); + background: var(--color-bg-secondary); +} + +[data-theme="dark"] .who-should-use-card:hover { + box-shadow: 0 12px 32px rgba(139, 92, 246, 0.2), 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.who-should-use-card h3 { + font-size: var(--font-size-xl); + font-weight: 600; + margin-bottom: var(--spacing-md); + margin-top: 0; + color: var(--color-text-primary); + transition: color var(--transition-fast); +} + +.who-should-use-card:hover h3 { + color: var(--color-accent); +} + +.who-should-use-card p { + color: var(--color-text-secondary); + line-height: 1.7; + font-size: var(--font-size-base); + margin: 0; +} + +.who-should-use-card .use-case-icon { + width: 32px; + height: 32px; + color: var(--color-accent); + flex-shrink: 0; + margin-bottom: var(--spacing-md); + transition: all var(--transition-base); + filter: drop-shadow(0 2px 4px rgba(99, 32, 238, 0.2)); + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, rgba(99, 32, 238, 0.1) 0%, rgba(99, 32, 238, 0.05) 100%); + border-radius: var(--border-radius-md); + padding: var(--spacing-xs); +} + +[data-theme="dark"] .who-should-use-card .use-case-icon { + filter: drop-shadow(0 2px 4px rgba(139, 92, 246, 0.3)); + background: linear-gradient(135deg, rgba(139, 92, 246, 0.15) 0%, rgba(139, 92, 246, 0.08) 100%); +} + +.who-should-use-card:hover .use-case-icon { + transform: scale(1.1) translateY(-2px); + filter: drop-shadow(0 4px 8px rgba(99, 32, 238, 0.3)); + color: var(--color-accent-hover); + background: linear-gradient(135deg, rgba(99, 32, 238, 0.2) 0%, rgba(99, 32, 238, 0.1) 100%); +} + +[data-theme="dark"] .who-should-use-card:hover .use-case-icon { + filter: drop-shadow(0 4px 8px rgba(139, 92, 246, 0.4)); + background: linear-gradient(135deg, rgba(139, 92, 246, 0.25) 0%, rgba(139, 92, 246, 0.15) 100%); +} + +.who-should-use-card .use-case-icon svg { + width: 20px; + height: 20px; +} + +/* Getting Started Section */ +.getting-started-steps { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: var(--spacing-xl); + margin-top: var(--spacing-2xl); +} + +.step-card { + background: var(--color-bg-secondary); + padding: var(--spacing-xl); + border-radius: var(--border-radius-lg); + border: 1px solid var(--color-border); + position: relative; +} + +.step-number { + width: 40px; + height: 40px; + background: var(--color-accent); + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: var(--font-size-lg); + margin-bottom: var(--spacing-md); +} + +.step-card h3 { + font-size: var(--font-size-lg); + font-weight: 600; + margin-bottom: var(--spacing-sm); + color: var(--color-text-primary); +} + +.step-card p { + color: var(--color-text-secondary); + line-height: 1.6; +} + diff --git a/src/css/sections/_sections.css b/src/css/sections/_sections.css new file mode 100644 index 0000000..8f12a0e --- /dev/null +++ b/src/css/sections/_sections.css @@ -0,0 +1,93 @@ +/* ============================================================================ + Section Components (Common Styles) + ============================================================================ */ + +.section { + padding: calc(var(--spacing-4xl) + var(--spacing-lg)) 0; +} + +/* Reduced padding for compact sections */ +.section-compact { + padding: var(--spacing-md) 0; +} + +/* Reduce bottom padding for discovery section when followed by compact section */ +#discovery:has(+ .section-compact) { + padding-bottom: var(--spacing-md); +} + +/* Reduce margin-bottom for last data-category in discovery section when followed by compact section */ +#discovery .data-category:last-child { + margin-bottom: var(--spacing-lg); +} + +/* Ensure consistent spacing between all sections - override padding-top for following sections */ +.section + .section, +.section-compact + .section { + padding-top: var(--spacing-4xl) !important; +} + +/* Reduced spacing when compact section follows regular section */ +.section + .section-compact { + padding-top: var(--spacing-md) !important; +} + +/* Reduced spacing between compact sections */ +.section-compact + .section-compact { + padding-top: var(--spacing-lg) !important; +} + +.section-header { + margin-bottom: var(--spacing-3xl); + text-align: center; + max-width: 800px; + margin-left: auto; + margin-right: auto; + padding: 0 var(--spacing-lg); +} + +/* Reduced margins for compact sections */ +.section-compact .section-header { + margin-bottom: var(--spacing-sm); +} + +.section-title { + font-size: var(--font-size-3xl); + font-weight: 700; + color: var(--color-text-primary); + margin-bottom: var(--spacing-md); + letter-spacing: -0.01em; + line-height: 1.2; +} + +.section-description { + font-size: var(--font-size-lg); + color: var(--color-text-secondary); + max-width: 700px; + margin: 0 auto; + line-height: 1.7; +} + +.section-footer { + text-align: center; + margin-top: var(--spacing-2xl); +} + +/* Data Category Components */ +.data-category { + margin-bottom: var(--spacing-3xl); +} + +.category-title { + font-size: var(--font-size-2xl); + font-weight: 600; + color: var(--color-text-primary); + margin-bottom: var(--spacing-lg); +} + +.category-description { + color: var(--color-text-secondary); + font-size: var(--spacing-md); + margin-bottom: var(--spacing-lg); +} + diff --git a/src/css/sections/_use-cases.css b/src/css/sections/_use-cases.css new file mode 100644 index 0000000..a4e7618 --- /dev/null +++ b/src/css/sections/_use-cases.css @@ -0,0 +1,100 @@ +/* ============================================================================ + Use Cases Section + ============================================================================ */ + +.use-cases { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: var(--spacing-xl); +} + +.use-case-item { + display: flex; + align-items: flex-start; + gap: var(--spacing-lg); + padding: var(--spacing-xl) var(--spacing-lg); + background: var(--color-bg-primary); + border-radius: var(--border-radius-xl); + border: 1px solid var(--color-border); + transition: all var(--transition-base); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + position: relative; + overflow: hidden; +} + +[data-theme="dark"] .use-case-item { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.use-case-item::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: var(--gradient-accent); + transform: scaleX(0); + transform-origin: left; + transition: transform var(--transition-base); +} + +.use-case-item:hover::before { + transform: scaleX(1); +} + +.use-case-item:hover { + transform: translateY(-4px); + box-shadow: 0 12px 32px rgba(99, 32, 238, 0.15), 0 4px 12px rgba(0, 0, 0, 0.08); + border-color: var(--color-accent); + background: var(--color-bg-secondary); +} + +[data-theme="dark"] .use-case-item:hover { + box-shadow: 0 12px 32px rgba(139, 92, 246, 0.2), 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.use-case-icon { + width: 32px; + height: 32px; + color: var(--color-accent); + flex-shrink: 0; + margin-top: 2px; + transition: all var(--transition-base); + filter: drop-shadow(0 2px 4px rgba(99, 32, 238, 0.2)); + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, rgba(99, 32, 238, 0.1) 0%, rgba(99, 32, 238, 0.05) 100%); + border-radius: var(--border-radius-md); + padding: var(--spacing-xs); +} + +[data-theme="dark"] .use-case-icon { + filter: drop-shadow(0 2px 4px rgba(139, 92, 246, 0.3)); + background: linear-gradient(135deg, rgba(139, 92, 246, 0.15) 0%, rgba(139, 92, 246, 0.08) 100%); +} + +.use-case-item:hover .use-case-icon { + transform: scale(1.1) translateY(-2px); + filter: drop-shadow(0 4px 8px rgba(99, 32, 238, 0.3)); + color: var(--color-accent-hover); + background: linear-gradient(135deg, rgba(99, 32, 238, 0.2) 0%, rgba(99, 32, 238, 0.1) 100%); +} + +[data-theme="dark"] .use-case-item:hover .use-case-icon { + filter: drop-shadow(0 4px 8px rgba(139, 92, 246, 0.4)); + background: linear-gradient(135deg, rgba(139, 92, 246, 0.25) 0%, rgba(139, 92, 246, 0.15) 100%); +} + +.use-case-icon svg { + width: 20px; + height: 20px; +} + +.use-case-item p { + color: var(--color-text-secondary); + font-size: var(--font-size-md); + line-height: 1.6; +} + diff --git a/src/css/styles.css b/src/css/styles.css new file mode 100644 index 0000000..d6303ed --- /dev/null +++ b/src/css/styles.css @@ -0,0 +1,37 @@ +/* ============================================================================ + Main Stylesheet - Modular CSS Architecture + ============================================================================ + + This file imports all CSS modules in the correct order. + The structure follows: + 1. Base (variables, themes, reset, utilities) + 2. Components (reusable UI components) + 3. Sections (page-specific sections) + 4. Layout (responsive design) + ============================================================================ */ + +/* Base Layer */ +@import url('base/_variables.css'); +@import url('base/_themes.css'); +@import url('base/_reset.css'); +@import url('base/_utilities.css'); + +/* Components Layer */ +@import url('components/_cards.css'); +@import url('components/_buttons.css'); +@import url('components/_navigation.css'); +@import url('components/_country-selector.css'); +@import url('components/_tables.css'); +@import url('components/_modals.css'); + +/* Sections Layer */ +@import url('sections/_sections.css'); +@import url('sections/_hero.css'); +@import url('sections/_features.css'); +@import url('sections/_use-cases.css'); +@import url('sections/_download.css'); +@import url('sections/_footer.css'); +@import url('sections/_other.css'); + +/* Layout Layer (Responsive) */ +@import url('layout/_responsive.css'); diff --git a/src/de/discovery.html b/src/de/discovery.html new file mode 100644 index 0000000..23be683 --- /dev/null +++ b/src/de/discovery.html @@ -0,0 +1,955 @@ + + + + + + + + Kostenloses PII & PCI DSS Datenerkennungs-Tool | Angry Data Scanner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Sensible Datenerkennung

+

Angry Data Scanner kann verschiedene Arten sensibler Daten in mehreren Kategorien erkennen

+
+ + +
+
+ + + + + Nach Land filtern +
+
+ + + + + +
+
+ + +
+

Suche personenbezogener Daten (Zahlen)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DatentypLokaler NameLandBeispiel
Phone number-RU+7 926 3847291
Phone number-US+1 212 5550198
Taxpayer numberИННRU7707083893
Taxpayer numberSSNUS536-90-4399
Taxpayer numberRINCN110101199003078912
Passport-RU4505 857555
Passport-US847293641
Pension insurance numberСНИЛСRU234-567-890 12
Medical insurance numberОМСRU9876543210987654
Medical insurance numberMedicareUS1A2B3C4D5E
Car insurance numberполис ОСАГОRUААА3847291847
Driver licenseВодительские праваRU77АВ987654
Military IDУдостоверение личности военнослужащегоRU3847291847
Birthday--15.03.1985
VIN--1HGBH41JXMN109186
Employer Identification NumberEINUS12-3456789
Individual Taxpayer Identification NumberITINUS987-65-4321
Driver license-USD1234567
Visa number-USB12345678
Alien Registration NumberA-NumberUSA123456789
USCIS receipt numberUSCISUSEAC2190012345
SEVIS IDSEVISUSN0001234567
Department of Defense IDDOD IDUS1234567890
Military Mail AddressAPO/FPO/DPOUSFPO AP 96677-1234
National Stock NumberNSNUS5330-00-123-4567
Transportation Control NumberTCNUSTCN12345678901234567
National Provider IdentifierNPIUS1234567890
+
+ + +
+

Suche personenbezogener Daten (Text)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DatentypLokaler NameLandBeispiel
Full nameФИОRUИван Иванович Иванов
Full nameFull nameUSJohn Smith
E-mail--captainbull@gmail.com
AddressАдресRUМосква, ул. Ленина, д. 1
AddressAddressUS123 Main St CA 90210
Login--username
Password--password123
+
+ + +
+

Suche PCI DSS-Daten

+ + + + + + + + + + + + + + + + + + + + +
DatentypBeispiel
Payment card number4400 5678 9012 3456
CVV456
+
+ + +
+

Suche Bankgeheimnis

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DatentypLandBeispiel
Bank account (Individual)RU408 028 103 3 5300 5405 83
Bank account (Legal entity)RU407 028 103 3 5300 5405 83
Routing Transit NumberUS123456789
+
+ + +
+

Suche IT-Assets

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DatentypBeispiel
IPv4192.168.1.1
IPv62001:db8::1
Source code filesFinds files with source-code. Source code should be placed in git repository.
TLS certificatesFinds folders with the most amount of TLS certificates
Hash dataSHA-256, MD5, NTLM (NT hash), SHA-1, SHA-512
+
+ + +
+

Suche Kryptowährung

+ + + + + + + + + + + + + + + + + + + + +
DatentypBeispiel
Crypto wallet1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
Crypto seed phraseA sequence of 12 to 24 words from the BIP39 standard wordlist, used for cryptographic wallet recovery and key derivation
+
+ + +
+

Suche benutzerdefinierter Signaturen

+

+ Es ist möglich, benutzerdefinierte Datensuchsignaturen mithilfe von Klartext hinzuzufügen: + + Secret, + + Password, + + Central bank + + oder irgendein anderes. +

+
+ + +
+

Unterstützte Dateitypen

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DateitypDateiformat
MS Office (tables).xlsx .xls
MS Office (text).docx .doc
MS Office (presentation).pptx .potx .ppsx .pptm .ppt .pps .pot
Open Office (tables).ods
Open Office (text).odt
Open Office (presentation).odp .otp
Adobe.pdf
Archives.zip .rar
Plain text.txt .csv .xml .json .log
+
+ + +
+

Unterstützte Datenquellen

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SteckerBeschreibung
Network FolderScans files on remote directory like Windows environment
HDD/SDDScan local hard drive
S3Scan files in S3
HTTP/HTTPSScans web site content
+
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/src/de/download.html b/src/de/download.html new file mode 100644 index 0000000..19b69f0 --- /dev/null +++ b/src/de/download.html @@ -0,0 +1,532 @@ + + + + + + + + Kostenlosen Daten-Scanner Download: Windows, Linux & macOS | Angry Data Scanner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Herunterladen

+

Systemanforderungen: Windows, Linux, macOS | 400 MB Festplatte | 4GB RAM | 1,3-GHz-CPU

+
+ +
+ +
+
+ + + + + + + + +

Windows

+
+ +
+ +
+
+ + + + + +

Linux

+
+ +
+ +
+
+ + + + + +

macOS

+
+ +
+ +
+
+
+ + +
+
+
+

Erste Schritte

+

Kurzanleitung, die Ihnen den Einstieg in die Verwendung von Angry Data Scanner erleichtert

+
+ +
+
+
1
+

Herunterladen

+

Laden Sie über die obigen Links die entsprechende Version für Ihr Betriebssystem herunter.

+
+
+
2
+

Installieren oder extrahieren

+

Führen Sie das Installationsprogramm aus (Windows/macOS) oder extrahieren Sie die portable Version. Keine Administratorrechte erforderlich.

+
+
+
3
+

Starten Sie den Scanvorgang

+

Starten Sie die Anwendung, wählen Sie Ihre Datenquelle aus und beginnen Sie mit dem Scannen. Die Ergebnisse werden sofort angezeigt.

+
+
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/src/de/features.html b/src/de/features.html new file mode 100644 index 0000000..0a1594f --- /dev/null +++ b/src/de/features.html @@ -0,0 +1,488 @@ + + + + + + + + Daten-Scanner Funktionen: Ranking & CSV-Export | Angry Data Scanner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Hauptmerkmale

+

Entdecken Sie die leistungsstarken Funktionen, die Angry Data Scanner zur idealen Lösung für die Erkennung sensibler Daten machen

+
+ +
+ +
+
+ + + + + +
+

Rang

+

Der Scanner zeigt hochwertige Dateien zuerst an und hilft Ihnen, die kritischsten Ergebnisse zu priorisieren. Das intelligente Ranking-System analysiert die Datensensitivität und präsentiert die Ergebnisse in der Reihenfolge ihrer Wichtigkeit.

+
+ +
+
+ + + + + + +
+

Scanverlauf anzeigen

+

Verfolgen Sie alle Ihre vorherigen Scans mit detailliertem Verlauf. Überprüfen Sie frühere Ergebnisse, vergleichen Sie Scans im Laufe der Zeit und führen Sie einen vollständigen Prüfpfad Ihrer Datenerkennungsaktivitäten.

+
+ +
+
+ + + + + + +
+

Ergebnisse exportieren

+

Laden Sie die Ergebnisse zur weiteren Analyse, Berichterstellung oder Integration mit anderen Tools in eine CSV-Datei herunter. Der Export umfasst alle erkannten Datentypen, Speicherorte und Metadaten.

+
+ +
+
+ + + + + + +
+

Planen Sie Scans

+

Automatisieren Sie Ihren Scanvorgang mit geplanten Scans. Richten Sie wiederkehrende Scans ein, um Ihre Datenquellen kontinuierlich zu überwachen und die fortlaufende Compliance sicherzustellen.

+
+ +
+
+ + + + + + +
+

Konfigurierbare Matcher

+

Konfigurieren Sie PII, PCI DSS und andere Matcher entsprechend Ihren spezifischen Compliance-Anforderungen. Aktivieren oder deaktivieren Sie Erkennungsmuster entsprechend Ihren Anforderungen.

+
+ +
+
+ + + + + + +
+

Mehrere Dateiformate

+

Konfigurieren Sie die zu scannenden Dateiformate (PDF, Excel usw.). Unterstützung für MS Office, Open Office, Adobe PDF, Archive und Nur-Text-Dateien. Passen Sie an, welche Formate in Ihre Scans einbezogen werden sollen.

+
+ +
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/src/de/index.html b/src/de/index.html new file mode 100644 index 0000000..3c88668 --- /dev/null +++ b/src/de/index.html @@ -0,0 +1,514 @@ + + + + + + + + Angry Data Scanner – kostenloses Tool zur Erkennung sensibler Daten + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
Kostenlose Open Source
+

Tool zur Erkennung sensibler Daten

+

+ Angry Data Scanner nutzt Mustervergleich, um vertrauliche Daten, die in Ordnern, Webseiten, S3 und Datenbanken gespeichert sind, automatisch zu erkennen. Es hilft Unternehmen dabei, herauszufinden, wo sensible Daten wie personenbezogene Daten (PII) und geistiges Eigentum gespeichert sind. +

+
+
+ + + + Einfache Benutzeroberfläche: Intuitives Design für Geschwindigkeit und Benutzerfreundlichkeit. +
+
+ + + + One-Click Discovery: Erkennen Sie sensible Daten sofort mit nur 2 Klicks. +
+
+ + + + Problemlose Einrichtung: Keine Administratorrechte oder Installation erforderlich. +
+
+ + + + Plattformübergreifend: Funktioniert nahtlos unter Linux, macOS und Windows. +
+
+ + + + Absolute Privatsphäre: Alle Scanvorgänge erfolgen lokal. Ihre Daten verlassen nie Ihren PC. +
+
+ +
+
+
+
+
+
+
+ Angry Data Scanner Interface +
+ + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + diff --git a/src/de/use-cases.html b/src/de/use-cases.html new file mode 100644 index 0000000..013510d --- /dev/null +++ b/src/de/use-cases.html @@ -0,0 +1,507 @@ + + + + + + + + Anwendungsfälle: Sicherheit & Compliance | Angry Data Scanner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Anwendungsfälle aus dem wirklichen Leben

+

Entdecken Sie, wie Unternehmen aus verschiedenen Branchen Angry Data Scanner nutzen, um sensible Daten zu schützen und Compliance sicherzustellen

+
+ +
+ + +
+ + + + + + + + +

Ein Leck-Jagd-Team scannt einen Netzwerkordner und stellt sicher, dass er keinen Quellcode enthält

+
+ +
+ + + + + + + +

Ein Mitarbeiter findet und löscht Dateien mit Kartennummern, um PCI DSS zu entsprechen

+
+ +
+ + + + + + +

Ein Bankmitarbeiter scannt einen Netzwerkordner, um sicherzustellen, dass er keine PII von VIP-Kunden enthält

+
+ +
+ + + + + + +

Ein Chef scannt einen freigegebenen Ordner des Vertriebsteams, damit dort keine Kundenkontakte vorhanden sind

+
+ +
+ + + + + + + + +

Strafverfolgungsbehörden müssen Spuren von Kryptowährung auf einem Laptop entdecken

+
+ +
+ + + + + + + + +

Ein Cybersicherheitsbeauftragter muss überprüfen, dass die Datenbank keine persönlichen Daten enthält

+
+ +
+
+
+ + +
+
+
+

Wer sollte Angry Data Scanner verwenden?

+
+ +
+
+

Sicherheitsteams

+

Führen Sie Sicherheitsüberprüfungen durch, identifizieren Sie Datenlecks und stellen Sie sicher, dass vertrauliche Informationen in Ihrer gesamten Infrastruktur ordnungsgemäß geschützt sind.

+
+
+

Compliance-Beauftragte

+

Stellen Sie die Einhaltung von Vorschriften wie DSGVO, PCI DSS, HIPAA und anderen Datenschutzstandards sicher.

+
+
+

Entwickler und DevOps

+

Scannen Sie Repositorys und Infrastruktur, um zu verhindern, dass vertrauliche Daten in Code oder Konfigurationen versehentlich offengelegt werden.

+
+
+

Forensik und Strafverfolgung

+

Entdecken Sie bei digitalen Ermittlungen Spuren sensibler Daten, Kryptowährungen und anderer Beweise.

+
+
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/src/discovery.html b/src/discovery.html new file mode 100644 index 0000000..361437e --- /dev/null +++ b/src/discovery.html @@ -0,0 +1,955 @@ + + + + + + + + Free PII & PCI DSS Data Discovery Tool | Angry Data Scanner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Sensitive data discovery

+

Angry Data Scanner can detect various types of sensitive data across multiple categories

+
+ + +
+
+ + + + + Filter by country +
+
+ + + + + +
+
+ + +
+

Search personal data (numbers)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Data typeLocal nameCountryExample
Phone number-RU+7 926 3847291
Phone number-US+1 212 5550198
Taxpayer numberИННRU7707083893
Taxpayer numberSSNUS536-90-4399
Taxpayer numberRINCN110101199003078912
Passport-RU4505 857555
Passport-US847293641
Pension insurance numberСНИЛСRU234-567-890 12
Medical insurance numberОМСRU9876543210987654
Medical insurance numberMedicareUS1A2B3C4D5E
Car insurance numberполис ОСАГОRUААА3847291847
Driver licenseВодительские праваRU77АВ987654
Military IDУдостоверение личности военнослужащегоRU3847291847
Birthday--15.03.1985
VIN--1HGBH41JXMN109186
Employer Identification NumberEINUS12-3456789
Individual Taxpayer Identification NumberITINUS987-65-4321
Driver license-USD1234567
Visa number-USB12345678
Alien Registration NumberA-NumberUSA123456789
USCIS receipt numberUSCISUSEAC2190012345
SEVIS IDSEVISUSN0001234567
Department of Defense IDDOD IDUS1234567890
Military Mail AddressAPO/FPO/DPOUSFPO AP 96677-1234
National Stock NumberNSNUS5330-00-123-4567
Transportation Control NumberTCNUSTCN12345678901234567
National Provider IdentifierNPIUS1234567890
+
+ + +
+

Search personal data (text)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Data typeLocal nameCountryExample
Full nameФИОRUИван Иванович Иванов
Full nameFull nameUSJohn Smith
E-mail--captainbull@gmail.com
AddressАдресRUМосква, ул. Ленина, д. 1
AddressAddressUS123 Main St CA 90210
Login--username
Password--password123
+
+ + +
+

Search PCI DSS data

+ + + + + + + + + + + + + + + + + + + + +
Data typeExample
Payment card number4400 5678 9012 3456
CVV456
+
+ + +
+

Search banking secrecy

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Data typeCountryExample
Bank account (Individual)RU408 028 103 3 5300 5405 83
Bank account (Legal entity)RU407 028 103 3 5300 5405 83
Routing Transit NumberUS123456789
+
+ + +
+

Search IT assets

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Data typeExample
IPv4192.168.1.1
IPv62001:db8::1
Source code filesFinds files with source-code. Source code should be placed in git repository.
TLS certificatesFinds folders with the most amount of TLS certificates
Hash dataSHA-256, MD5, NTLM (NT hash), SHA-1, SHA-512
+
+ + +
+

Search cryptocurrency

+ + + + + + + + + + + + + + + + + + + + +
Data typeExample
Crypto wallet1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
Crypto seed phraseA sequence of 12 to 24 words from the BIP39 standard wordlist, used for cryptographic wallet recovery and key derivation
+
+ + +
+

Search custom signatures

+

+ It is possible to add custom data search signatures using plain text: + + Secret, + + Password, + + Central bank + + or any other. +

+
+ + +
+

Supported file types

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
File TypeFile Format
MS Office (tables).xlsx .xls
MS Office (text).docx .doc
MS Office (presentation).pptx .potx .ppsx .pptm .ppt .pps .pot
Open Office (tables).ods
Open Office (text).odt
Open Office (presentation).odp .otp
Adobe.pdf
Archives.zip .rar
Plain text.txt .csv .xml .json .log
+
+ + +
+

Supported data sources

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ConnectorDescription
Network FolderScans files on remote directory like Windows environment
HDD/SDDScan local hard drive
S3Scan files in S3
HTTP/HTTPSScans web site content
+
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/src/download.html b/src/download.html new file mode 100644 index 0000000..0f943db --- /dev/null +++ b/src/download.html @@ -0,0 +1,532 @@ + + + + + + + + Download Free Data Scanner: Windows, Linux & macOS | Angry Data Scanner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Download

+

System Requirements: Windows, Linux, macOS | 400MB HDD | 4GB RAM | 1.3Ghz CPU

+
+ +
+ +
+
+ + + + + + + + +

Windows

+
+ +
+ +
+
+ + + + + +

Linux

+
+ +
+ +
+
+ + + + + +

macOS

+
+ +
+ +
+
+
+ + +
+
+
+

Getting Started

+

Quick start guide to help you begin using Angry Data Scanner

+
+ +
+
+
1
+

Download

+

Download the appropriate version for your operating system from the links above.

+
+
+
2
+

Install or Extract

+

Run the installer (Windows/macOS) or extract the portable version. No admin rights required.

+
+
+
3
+

Start Scanning

+

Launch the application, select your data source, and start scanning. Results appear instantly.

+
+
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/src/es/discovery.html b/src/es/discovery.html new file mode 100644 index 0000000..54f02e6 --- /dev/null +++ b/src/es/discovery.html @@ -0,0 +1,955 @@ + + + + + + + + Escáner Gratis de Datos PII y PCI DSS | Angry Data Scanner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Descubrimiento de datos confidenciales

+

Angry Data Scanner puede detectar varios tipos de datos confidenciales en múltiples categorías

+
+ + +
+
+ + + + + Filtrar por país +
+
+ + + + + +
+
+ + +
+

Búsqueda de datos personales (números)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
tipo de datosNombre localPaísEjemplo
Phone number-RU+7 926 3847291
Phone number-US+1 212 5550198
Taxpayer numberИННRU7707083893
Taxpayer numberSSNUS536-90-4399
Taxpayer numberRINCN110101199003078912
Passport-RU4505 857555
Passport-US847293641
Pension insurance numberСНИЛСRU234-567-890 12
Medical insurance numberОМСRU9876543210987654
Medical insurance numberMedicareUS1A2B3C4D5E
Car insurance numberполис ОСАГОRUААА3847291847
Driver licenseВодительские праваRU77АВ987654
Military IDУдостоверение личности военнослужащегоRU3847291847
Birthday--15.03.1985
VIN--1HGBH41JXMN109186
Employer Identification NumberEINUS12-3456789
Individual Taxpayer Identification NumberITINUS987-65-4321
Driver license-USD1234567
Visa number-USB12345678
Alien Registration NumberA-NumberUSA123456789
USCIS receipt numberUSCISUSEAC2190012345
SEVIS IDSEVISUSN0001234567
Department of Defense IDDOD IDUS1234567890
Military Mail AddressAPO/FPO/DPOUSFPO AP 96677-1234
National Stock NumberNSNUS5330-00-123-4567
Transportation Control NumberTCNUSTCN12345678901234567
National Provider IdentifierNPIUS1234567890
+
+ + +
+

Búsqueda de datos personales (texto)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
tipo de datosNombre localPaísEjemplo
Full nameФИОRUИван Иванович Иванов
Full nameFull nameUSJohn Smith
E-mail--captainbull@gmail.com
AddressАдресRUМосква, ул. Ленина, д. 1
AddressAddressUS123 Main St CA 90210
Login--username
Password--password123
+
+ + +
+

Búsqueda de datos PCI DSS

+ + + + + + + + + + + + + + + + + + + + +
tipo de datosEjemplo
Payment card number4400 5678 9012 3456
CVV456
+
+ + +
+

Búsqueda de secreto bancario

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
tipo de datosPaísEjemplo
Bank account (Individual)RU408 028 103 3 5300 5405 83
Bank account (Legal entity)RU407 028 103 3 5300 5405 83
Routing Transit NumberUS123456789
+
+ + +
+

Búsqueda de activos de TI

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
tipo de datosEjemplo
IPv4192.168.1.1
IPv62001:db8::1
Source code filesFinds files with source-code. Source code should be placed in git repository.
TLS certificatesFinds folders with the most amount of TLS certificates
Hash dataSHA-256, MD5, NTLM (NT hash), SHA-1, SHA-512
+
+ + +
+

Búsqueda de criptomonedas

+ + + + + + + + + + + + + + + + + + + + +
tipo de datosEjemplo
Crypto wallet1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
Crypto seed phraseA sequence of 12 to 24 words from the BIP39 standard wordlist, used for cryptographic wallet recovery and key derivation
+
+ + +
+

Búsqueda de firmas personalizadas

+

+ Es posible agregar firmas de búsqueda de datos personalizadas utilizando texto sin formato: + + Secret, + + Password, + + Central bank + + o cualquier otro. +

+
+ + +
+

Tipos de archivos admitidos

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Tipo de archivoFormato de archivo
MS Office (tables).xlsx .xls
MS Office (text).docx .doc
MS Office (presentation).pptx .potx .ppsx .pptm .ppt .pps .pot
Open Office (tables).ods
Open Office (text).odt
Open Office (presentation).odp .otp
Adobe.pdf
Archives.zip .rar
Plain text.txt .csv .xml .json .log
+
+ + +
+

Fuentes de datos admitidas

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ConectorDescripción
Network FolderScans files on remote directory like Windows environment
HDD/SDDScan local hard drive
S3Scan files in S3
HTTP/HTTPSScans web site content
+
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/src/es/download.html b/src/es/download.html new file mode 100644 index 0000000..77ddbb3 --- /dev/null +++ b/src/es/download.html @@ -0,0 +1,532 @@ + + + + + + + + Descargar Escáner Gratis: Windows, Linux y macOS | Angry Data Scanner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Descargar

+

Requisitos del sistema: Windows, Linux, macOS | Disco duro de 400 MB | 4 GB de RAM | Procesador de 1,3 GHz

+
+ +
+ +
+
+ + + + + + + + +

Windows

+
+ +
+ +
+
+ + + + + +

Linux

+
+ +
+ +
+
+ + + + + +

macOS

+
+ +
+ +
+
+
+ + +
+
+
+

Empezando

+

Guía de inicio rápido para ayudarle a comenzar a utilizar Angry Data Scanner

+
+ +
+
+
1
+

Descargar

+

Descargue la versión adecuada para su sistema operativo desde los enlaces anteriores.

+
+
+
2
+

Instalar o extraer

+

Ejecute el instalador (Windows/macOS) o extraiga la versión portátil. No se requieren derechos de administrador.

+
+
+
3
+

Iniciar escaneo

+

Inicie la aplicación, seleccione su fuente de datos y comience a escanear. Los resultados aparecen al instante.

+
+
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/src/es/features.html b/src/es/features.html new file mode 100644 index 0000000..68134a9 --- /dev/null +++ b/src/es/features.html @@ -0,0 +1,488 @@ + + + + + + + + Características del Escáner: Clasificación y Exportación CSV | Angry Data Scanner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Características clave

+

Descubra las poderosas capacidades que hacen de Angry Data Scanner la solución ideal para el descubrimiento de datos confidenciales

+
+ +
+ +
+
+ + + + + +
+

Categoría

+

El escáner muestra primero los archivos de alto valor, lo que le ayuda a priorizar los hallazgos más críticos. El sistema de clasificación inteligente analiza la sensibilidad de los datos y presenta los resultados en orden de importancia.

+
+ +
+
+ + + + + + +
+

Ver historial de escaneo

+

Realice un seguimiento de todos sus escaneos anteriores con un historial detallado. Revise resultados anteriores, compare escaneos a lo largo del tiempo y mantenga un seguimiento de auditoría completo de sus actividades de descubrimiento de datos.

+
+ +
+
+ + + + + + +
+

Exportar resultados

+

Descargue los resultados en un archivo CSV para realizar más análisis, generar informes o integrarlos con otras herramientas. La exportación incluye todos los tipos de datos, ubicaciones y metadatos detectados.

+
+ +
+
+ + + + + + +
+

Programar escaneos

+

Automatice su proceso de escaneo con escaneos programados. Configure análisis recurrentes para monitorear continuamente sus fuentes de datos y garantizar el cumplimiento continuo.

+
+ +
+
+ + + + + + +
+

Comparadores configurables

+

Configure PII, PCI DSS y otros comparadores para que coincidan con sus requisitos de cumplimiento específicos. Habilite o deshabilite los patrones de detección según sus necesidades.

+
+ +
+
+ + + + + + +
+

Múltiples formatos de archivos

+

Configure los formatos de archivos (pdf, excel, etc.) para escanear. Soporte para MS Office, Open Office, Adobe PDF, archivos y archivos de texto sin formato. Personalice qué formatos incluir en sus escaneos.

+
+ +
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/src/es/index.html b/src/es/index.html new file mode 100644 index 0000000..bfab6cb --- /dev/null +++ b/src/es/index.html @@ -0,0 +1,514 @@ + + + + + + + + Angry Data Scanner: herramienta gratuita de descubrimiento de datos confidenciales + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
Código abierto gratuito
+

Herramienta de descubrimiento de datos confidenciales

+

+ Angry Data Scanner utiliza la coincidencia de patrones para descubrir automáticamente datos confidenciales almacenados en carpetas, páginas web, S3 y bases de datos. Ayuda a las organizaciones a identificar dónde se almacenan datos confidenciales, como información de identificación personal (PII) y propiedad intelectual. +

+
+
+ + + + Interfaz de usuario sencilla: diseño intuitivo pensado para la velocidad y la facilidad de uso. +
+
+ + + + Descubrimiento con un clic: detecte datos confidenciales al instante con solo 2 clics. +
+
+ + + + Configuración sin complicaciones: no se requieren derechos de administrador ni instalación. +
+
+ + + + Multiplataforma: funciona perfectamente en Linux, macOS y Windows. +
+
+ + + + Privacidad total: todo el escaneo se realiza localmente. Tus datos nunca salen de tu PC. +
+
+ +
+
+
+
+
+
+
+ Angry Data Scanner Interface +
+ + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + diff --git a/src/es/use-cases.html b/src/es/use-cases.html new file mode 100644 index 0000000..7094f6d --- /dev/null +++ b/src/es/use-cases.html @@ -0,0 +1,507 @@ + + + + + + + + Casos de Uso: Seguridad y Cumplimiento | Angry Data Scanner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Casos de uso de la vida real

+

Descubra cómo organizaciones de diferentes industrias utilizan Angry Data Scanner para proteger datos confidenciales y garantizar el cumplimiento.

+
+ +
+ + +
+ + + + + + + + +

Un equipo de caza de fugas escanea una carpeta de red y se asegura de que no contenga código fuente

+
+ +
+ + + + + + + +

Un empleado encuentra y elimina archivos que contienen números de tarjeta para cumplir con PCI DSS

+
+ +
+ + + + + + +

Un empleado bancario escanea una carpeta de red para asegurarse de que no contenga PII de clientes VIP

+
+ +
+ + + + + + +

Un jefe escanea una carpeta compartida del equipo de ventas para que no tengan contactos de clientes allí

+
+ +
+ + + + + + + + +

Las fuerzas del orden necesitan descubrir rastros de criptomoneda en una laptop

+
+ +
+ + + + + + + + +

Un oficial de ciberseguridad necesita validar que la base de datos no contenga datos personales

+
+ +
+
+
+ + +
+
+
+

¿Quién debería utilizar Angry Data Scanner?

+
+ +
+
+

Equipos de seguridad

+

Realice auditorías de seguridad, identifique fugas de datos y asegúrese de que la información confidencial esté protegida adecuadamente en toda su infraestructura.

+
+
+

Oficiales de cumplimiento

+

Garantice el cumplimiento de normativas como GDPR, PCI DSS, HIPAA y otros estándares de protección de datos.

+
+
+

Desarrolladores y DevOps

+

Escanee repositorios e infraestructura para evitar la exposición accidental de datos confidenciales en el código o las configuraciones.

+
+
+

Forense y aplicación de la ley

+

Descubra rastros de datos confidenciales, criptomonedas y otras pruebas durante las investigaciones digitales.

+
+
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/src/favicon.ico b/src/favicon.ico new file mode 100644 index 0000000..fef1656 Binary files /dev/null and b/src/favicon.ico differ diff --git a/src/features.html b/src/features.html new file mode 100644 index 0000000..234c3bf --- /dev/null +++ b/src/features.html @@ -0,0 +1,488 @@ + + + + + + + + Data Scanner Features: Ranking & CSV Export | Angry Data Scanner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Key features

+

Discover the powerful capabilities that make Angry Data Scanner the ideal solution for sensitive data discovery

+
+ +
+ +
+
+ + + + + +
+

Ranking

+

Scanner shows high-value files first

+
+ +
+
+ + + + + + +
+

View scanning history

+

Track all your previous scans

+
+ +
+
+ + + + + + +
+

Export results

+

Download results in a CSV file

+
+ +
+
+ + + + + + +
+

Schedule scans

+

Automate your scanning process

+
+ +
+
+ + + + + + +
+

Configurable matchers

+

Configure PII, PCI DSS and other matchers

+
+ +
+
+ + + + + + +
+

Multiple file formats

+

Configure file formats (pdf, excel, etc.)

+
+ +
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/src/fr/discovery.html b/src/fr/discovery.html new file mode 100644 index 0000000..0e1aa10 --- /dev/null +++ b/src/fr/discovery.html @@ -0,0 +1,955 @@ + + + + + + + + Scanner Gratuit de Données PII & PCI DSS | Angry Data Scanner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Découverte de données sensibles

+

Angry Data Scanner peut détecter différents types de données sensibles dans plusieurs catégories

+
+ + +
+
+ + + + + Filtrer par pays +
+
+ + + + + +
+
+ + +
+

Recherche de données personnelles (chiffres)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Type de donnéesNom localPaysExemple
Phone number-RU+7 926 3847291
Phone number-US+1 212 5550198
Taxpayer numberИННRU7707083893
Taxpayer numberSSNUS536-90-4399
Taxpayer numberRINCN110101199003078912
Passport-RU4505 857555
Passport-US847293641
Pension insurance numberСНИЛСRU234-567-890 12
Medical insurance numberОМСRU9876543210987654
Medical insurance numberMedicareUS1A2B3C4D5E
Car insurance numberполис ОСАГОRUААА3847291847
Driver licenseВодительские праваRU77АВ987654
Military IDУдостоверение личности военнослужащегоRU3847291847
Birthday--15.03.1985
VIN--1HGBH41JXMN109186
Employer Identification NumberEINUS12-3456789
Individual Taxpayer Identification NumberITINUS987-65-4321
Driver license-USD1234567
Visa number-USB12345678
Alien Registration NumberA-NumberUSA123456789
USCIS receipt numberUSCISUSEAC2190012345
SEVIS IDSEVISUSN0001234567
Department of Defense IDDOD IDUS1234567890
Military Mail AddressAPO/FPO/DPOUSFPO AP 96677-1234
National Stock NumberNSNUS5330-00-123-4567
Transportation Control NumberTCNUSTCN12345678901234567
National Provider IdentifierNPIUS1234567890
+
+ + +
+

Recherche de données personnelles (texte)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Type de donnéesNom localPaysExemple
Full nameФИОRUИван Иванович Иванов
Full nameFull nameUSJohn Smith
E-mail--captainbull@gmail.com
AddressАдресRUМосква, ул. Ленина, д. 1
AddressAddressUS123 Main St CA 90210
Login--username
Password--password123
+
+ + +
+

Recherche de données PCI DSS

+ + + + + + + + + + + + + + + + + + + + +
Type de donnéesExemple
Payment card number4400 5678 9012 3456
CVV456
+
+ + +
+

Recherche de secret bancaire

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Type de donnéesPaysExemple
Bank account (Individual)RU408 028 103 3 5300 5405 83
Bank account (Legal entity)RU407 028 103 3 5300 5405 83
Routing Transit NumberUS123456789
+
+ + +
+

Recherche d'actifs informatiques

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Type de donnéesExemple
IPv4192.168.1.1
IPv62001:db8::1
Source code filesFinds files with source-code. Source code should be placed in git repository.
TLS certificatesFinds folders with the most amount of TLS certificates
Hash dataSHA-256, MD5, NTLM (NT hash), SHA-1, SHA-512
+
+ + +
+

Recherche de cryptomonnaie

+ + + + + + + + + + + + + + + + + + + + +
Type de donnéesExemple
Crypto wallet1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
Crypto seed phraseA sequence of 12 to 24 words from the BIP39 standard wordlist, used for cryptographic wallet recovery and key derivation
+
+ + +
+

Recherche de signatures personnalisées

+

+ Il est possible d'ajouter des signatures de recherche de données personnalisées en utilisant du texte brut : + + Secret, + + Password, + + Central bank + + ou tout autre. +

+
+ + +
+

Types de fichiers pris en charge

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Type de fichierFormat de fichier
MS Office (tables).xlsx .xls
MS Office (text).docx .doc
MS Office (presentation).pptx .potx .ppsx .pptm .ppt .pps .pot
Open Office (tables).ods
Open Office (text).odt
Open Office (presentation).odp .otp
Adobe.pdf
Archives.zip .rar
Plain text.txt .csv .xml .json .log
+
+ + +
+

Sources de données prises en charge

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ConnecteurDescription
Network FolderScans files on remote directory like Windows environment
HDD/SDDScan local hard drive
S3Scan files in S3
HTTP/HTTPSScans web site content
+
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/src/fr/download.html b/src/fr/download.html new file mode 100644 index 0000000..3db72e9 --- /dev/null +++ b/src/fr/download.html @@ -0,0 +1,532 @@ + + + + + + + + Télécharger Scanner Gratuit: Windows, Linux et macOS | Angry Data Scanner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Télécharger

+

Configuration système requise : Windows, Linux, macOS | Disque dur de 400 Mo | 4 Go de RAM | Processeur 1,3 GHz

+
+ +
+ +
+
+ + + + + + + + +

Windows

+
+ +
+ +
+
+ + + + + +

Linux

+
+ +
+ +
+
+ + + + + +

macOS

+
+ +
+ +
+
+
+ + +
+
+
+

Commencer

+

Guide de démarrage rapide pour vous aider à commencer à utiliser Angry Data Scanner

+
+ +
+
+
1
+

Télécharger

+

Téléchargez la version appropriée pour votre système d'exploitation à partir des liens ci-dessus.

+
+
+
2
+

Installer ou extraire

+

Exécutez le programme d'installation (Windows/macOS) ou extrayez la version portable. Aucun droit d'administrateur requis.

+
+
+
3
+

Démarrer la numérisation

+

Lancez l'application, sélectionnez votre source de données et lancez la numérisation. Les résultats apparaissent instantanément.

+
+
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/src/fr/features.html b/src/fr/features.html new file mode 100644 index 0000000..a0abd77 --- /dev/null +++ b/src/fr/features.html @@ -0,0 +1,488 @@ + + + + + + + + Fonctionnalités Scanner: Classement & Export CSV | Angry Data Scanner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Principales caractéristiques

+

Découvrez les puissantes capacités qui font d'Angry Data Scanner la solution idéale pour la découverte de données sensibles

+
+ +
+ +
+
+ + + + + +
+

Classement

+

Le scanner affiche en premier les fichiers de grande valeur, vous aidant ainsi à prioriser les résultats les plus critiques. Le système de classement intelligent analyse la sensibilité des données et présente les résultats par ordre d'importance.

+
+ +
+
+ + + + + + +
+

Afficher l'historique des analyses

+

Suivez toutes vos analyses précédentes avec un historique détaillé. Examinez les résultats antérieurs, comparez les analyses au fil du temps et conservez une piste d'audit complète de vos activités de découverte de données.

+
+ +
+
+ + + + + + +
+

Exporter les résultats

+

Téléchargez les résultats dans un fichier CSV pour une analyse plus approfondie, des rapports ou une intégration avec d'autres outils. L'exportation inclut tous les types de données, emplacements et métadonnées détectés.

+
+ +
+
+ + + + + + +
+

Planifier des analyses

+

Automatisez votre processus d'analyse avec des analyses planifiées. Configurez des analyses récurrentes pour surveiller en permanence vos sources de données et garantir une conformité continue.

+
+ +
+
+ + + + + + +
+

Matcheurs configurables

+

Configurez les PII, PCI DSS et autres comparateurs pour répondre à vos exigences de conformité spécifiques. Activez ou désactivez les modèles de détection en fonction de vos besoins.

+
+ +
+
+ + + + + + +
+

Plusieurs formats de fichiers

+

Configurez les formats de fichiers (pdf, excel, etc.) à numériser. Prise en charge de MS Office, Open Office, Adobe PDF, des archives et des fichiers texte brut. Personnalisez les formats à inclure dans vos numérisations.

+
+ +
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/src/fr/index.html b/src/fr/index.html new file mode 100644 index 0000000..f75718d --- /dev/null +++ b/src/fr/index.html @@ -0,0 +1,514 @@ + + + + + + + + Angry Data Scanner - Outil gratuit de découverte de données sensibles + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
Source ouverte gratuite
+

Outil de découverte de données sensibles

+

+ Angry Data Scanner utilise la correspondance de modèles pour découvrir automatiquement les données sensibles stockées dans des dossiers, des pages Web, S3 et des bases de données. Il aide les organisations à identifier où sont stockées les données sensibles telles que les informations personnelles identifiables (PII) et la propriété intellectuelle. +

+
+
+ + + + Interface utilisateur simple : conception intuitive conçue pour la rapidité et la facilité d'utilisation. +
+
+ + + + Découverte en un clic : détectez instantanément les données sensibles en seulement 2 clics. +
+
+ + + + Installation sans tracas : aucun droit d'administrateur ni installation requis. +
+
+ + + + Multiplateforme : fonctionne de manière transparente sur Linux macOS et Windows. +
+
+ + + + Confidentialité totale : toutes les analyses s'effectuent localement. Vos données ne quittent jamais votre PC. +
+
+ +
+
+
+
+
+
+
+ Angry Data Scanner Interface +
+ + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + diff --git a/src/fr/use-cases.html b/src/fr/use-cases.html new file mode 100644 index 0000000..2d16f99 --- /dev/null +++ b/src/fr/use-cases.html @@ -0,0 +1,507 @@ + + + + + + + + Cas d'Utilisation: Sécurité & Conformité | Angry Data Scanner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Cas d'utilisation réels

+

Découvrez comment des organisations de différents secteurs utilisent Angry Data Scanner pour protéger les données sensibles et garantir la conformité.

+
+ +
+ + +
+ + + + + + + + +

Une équipe de chasse aux fuites scanne un dossier réseau et s'assure qu'il ne contient pas de code source

+
+ +
+ + + + + + + +

Un employé trouve et supprime les fichiers contenant des numéros de carte pour se conformer à PCI DSS

+
+ +
+ + + + + + +

Un employé bancaire scanne un dossier réseau pour s'assurer qu'il ne contient pas de PII de clients VIP

+
+ +
+ + + + + + +

Un patron scanne un dossier partagé de l'équipe commerciale pour qu'ils n'aient pas de contacts clients là-bas

+
+ +
+ + + + + + + + +

Les forces de l'ordre doivent découvrir des traces de cryptomonnaie sur un ordinateur portable

+
+ +
+ + + + + + + + +

Un responsable de la cybersécurité doit valider que la base de données ne contient pas de données personnelles

+
+ +
+
+
+ + +
+
+
+

Qui devrait utiliser Angry Data Scanner ?

+
+ +
+
+

Équipes de sécurité

+

Réalisez des audits de sécurité, identifiez les fuites de données et assurez-vous que les informations sensibles sont correctement protégées dans votre infrastructure.

+
+
+

Agents de conformité

+

Assurez le respect des réglementations telles que le RGPD, PCI DSS, HIPAA et d'autres normes de protection des données.

+
+
+

Développeurs et DevOps

+

Analysez les référentiels et l'infrastructure pour éviter l'exposition accidentelle de données sensibles dans le code ou les configurations.

+
+
+

Médecine légale et application de la loi

+

Découvrez des traces de données sensibles, de cryptomonnaies et d'autres preuves lors d'enquêtes numériques.

+
+
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..b57eb0e --- /dev/null +++ b/src/index.html @@ -0,0 +1,514 @@ + + + + + + + + Angry Data Scanner - Free Sensitive Data Discovery Tool + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
Free Open Source
+

Sensitive Data Discovery Tool

+

+ Angry Data Scanner is a free sensitive data discovery tool designed to automatically find PII (Personally Identifiable Information), PHI (Protected Health Information) and intellectual property using advanced pattern matching. Perform unified data searches across local folders, web pages, AWS S3 buckets, and databases. +

+
+
+ + + + Intuitive design meant for speed and ease of use. +
+
+ + + + Detect sensitive data instantly with just 2 clicks. +
+
+ + + + No admin rights or installation required. +
+
+ + + + Works seamlessly on Linux macOS and Windows. +
+
+ + + + All scanning happens locally. Your data never leaves your PC. +
+
+ +
+
+
+
+
+
+
+ Angry Data Scanner Interface +
+ + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + diff --git a/src/js/core/app.js b/src/js/core/app.js new file mode 100644 index 0000000..ba279e5 --- /dev/null +++ b/src/js/core/app.js @@ -0,0 +1,187 @@ +/** + * Main Application Initializer + * Coordinates initialization of all modules using Dependency Injection + * + * Architecture: + * - Event Bus: Modules communicate through events (loose coupling) + * - Dependency Container: Manages module dependencies + * - Module Registry: Tracks all initialized modules + */ + +import { eventBus } from './event-bus.js'; +import { container } from './dependency-container.js'; + +// CONFIG and I18N are embedded as inline scripts in HTML from config.json +// They should be loaded before this script runs +if (typeof window === 'undefined' || !window.CONFIG || !window.I18N) { + console.error('CONFIG or I18N not found. Make sure translations and config are embedded in HTML before script.js'); +} + +const CONFIG = window.CONFIG; +const I18N = window.I18N; + +// Import modules +import { ThemeManager } from '../modules/theme.js'; +import { ScrollManager } from '../modules/scroll.js'; +import { AnimationManager } from '../modules/animation.js'; +import { LightboxManager } from '../modules/lightbox.js'; +import { TableEnhancement } from '../modules/table.js'; +import { CountryFilter } from '../modules/country-filter.js'; +import { DataRenderer } from '../modules/data-renderer.js'; +import { LanguageManager } from '../modules/language.js'; + +/** + * Application class - manages module lifecycle + */ +class App { + constructor() { + this.modules = new Map(); + this.initialized = false; + } + + /** + * Register dependencies in the container + */ + registerDependencies() { + // Register CONFIG and I18N as singletons + container.register('config', () => CONFIG, true); + container.register('i18n', () => I18N, true); + container.register('eventBus', () => eventBus, true); + } + + /** + * Initialize all modules + */ + async init() { + if (this.initialized) { + console.warn('App already initialized'); + return; + } + + // Wait for DOM to be fully loaded + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => this.init()); + return; + } + + try { + // Register dependencies first + this.registerDependencies(); + + // Initialize language first (before rendering content) + // Language manager needs to be initialized early + const languageManager = new LanguageManager(); + languageManager.init(); + this.modules.set('language', languageManager); + container.register('languageManager', () => languageManager, true); + + // Initialize data renderer (needs language manager and config) + const dataRenderer = new DataRenderer(); + dataRenderer.init(languageManager, CONFIG); + this.modules.set('dataRenderer', dataRenderer); + languageManager.setDataRenderer(dataRenderer); + + // Initialize theme manager + const themeManager = new ThemeManager(); + themeManager.init(); + this.modules.set('theme', themeManager); + + // Initialize scroll manager + const scrollManager = new ScrollManager(); + scrollManager.init(); + this.modules.set('scroll', scrollManager); + + // Initialize animation manager + const animationManager = new AnimationManager(); + animationManager.init(); + this.modules.set('animation', animationManager); + + // Initialize country filter (needs language manager) + const countryFilter = new CountryFilter(); + countryFilter.init(languageManager); + this.modules.set('countryFilter', countryFilter); + + // Initialize table enhancement + const tableEnhancement = new TableEnhancement(); + tableEnhancement.init(); + this.modules.set('table', tableEnhancement); + + // Initialize lightbox (needs theme manager) + const lightboxManager = new LightboxManager(); + lightboxManager.init(themeManager); + this.modules.set('lightbox', lightboxManager); + + // Emit initialization complete event + eventBus.emit('app:initialized', { + modules: Array.from(this.modules.keys()) + }); + + // Update translations after all content is rendered + requestAnimationFrame(() => { + this.finalizeInitialization(languageManager, dataRenderer); + }); + + this.initialized = true; + console.log('Angry Data Scanner website initialized successfully'); + } catch (error) { + console.error('Error initializing website:', error); + eventBus.emit('app:error', { error }); + } + } + + /** + * Finalize initialization - render translated content + */ + finalizeInitialization(languageManager, dataRenderer) { + const currentLang = languageManager.getCurrentLanguage(); + + // Only re-render for en/ru - de/fr/es pages are pre-translated + if (currentLang === 'en' || currentLang === 'ru') { + dataRenderer.renderUseCases(); + dataRenderer.renderDataSources(); + dataRenderer.renderCustomSignatures(); + dataRenderer.renderItAssets(); + dataRenderer.renderCrypto(); + dataRenderer.renderDownloads(); + } else if (currentLang === 'de' || currentLang === 'fr' || currentLang === 'es') { + // Ensure page is visible for pre-translated pages + document.documentElement.style.visibility = ''; + if (document.documentElement.hasAttribute('data-lang-loading')) { + document.documentElement.removeAttribute('data-lang-loading'); + } + } + } + + /** + * Get a module by name + * @param {string} name - Module name + * @returns {*} Module instance + */ + getModule(name) { + return this.modules.get(name); + } + + /** + * Destroy all modules (useful for testing) + */ + destroy() { + this.modules.clear(); + container.clear(); + eventBus.clear(); + this.initialized = false; + } +} + +// Create and export singleton instance +const app = new App(); + +// Initialize on load +app.init(); + +// Export for programmatic access +export { app, App }; + +// Export init function for backward compatibility +export function init() { + app.init(); +} diff --git a/src/js/core/constants.js b/src/js/core/constants.js new file mode 100644 index 0000000..003ce9c --- /dev/null +++ b/src/js/core/constants.js @@ -0,0 +1,33 @@ +/** + * Application constants + * Centralized configuration values used across modules + */ + +export const CONSTANTS = { + THEME: { + LIGHT: 'light', + DARK: 'dark', + STORAGE_KEY: 'theme' + }, + SCROLL: { + NAVBAR_HEIGHT: 64, + SCROLL_THRESHOLD: 100 + }, + ANIMATION: { + INTERSECTION_THRESHOLD: 0.1, + ROOT_MARGIN: '0px 0px -50px 0px', + DURATION: 600 + }, + BREAKPOINTS: { + MOBILE: 768, + TABLET: 968 + }, + SELECTORS: { + THEME_TOGGLE: '#themeToggle', + ANCHOR_LINKS: 'a[href^="#"]', + NAVBAR: '.navbar', + ANIMATABLE_ELEMENTS: '.section, .feature-card, .download-card, .use-case-item', + TABLE_ROWS: '.data-table tbody tr' + } +}; + diff --git a/src/js/core/dependency-container.js b/src/js/core/dependency-container.js new file mode 100644 index 0000000..d18156d --- /dev/null +++ b/src/js/core/dependency-container.js @@ -0,0 +1,71 @@ +/** + * Dependency Container + * Manages dependencies between modules (Dependency Injection pattern) + * This makes modules more testable and loosely coupled + */ + +class DependencyContainer { + constructor() { + this.dependencies = new Map(); + this.singletons = new Map(); + } + + /** + * Register a dependency + * @param {string} name - Dependency name + * @param {Function|*} factory - Factory function or value + * @param {boolean} singleton - Whether to create a singleton instance + */ + register(name, factory, singleton = false) { + this.dependencies.set(name, { factory, singleton }); + } + + /** + * Resolve a dependency + * @param {string} name - Dependency name + * @returns {*} Resolved dependency + */ + resolve(name) { + const dependency = this.dependencies.get(name); + if (!dependency) { + throw new Error(`Dependency "${name}" not found`); + } + + const { factory, singleton } = dependency; + + // If singleton, return cached instance + if (singleton) { + if (!this.singletons.has(name)) { + this.singletons.set(name, typeof factory === 'function' ? factory() : factory); + } + return this.singletons.get(name); + } + + // Otherwise, create new instance + return typeof factory === 'function' ? factory() : factory; + } + + /** + * Check if a dependency is registered + * @param {string} name - Dependency name + * @returns {boolean} + */ + has(name) { + return this.dependencies.has(name); + } + + /** + * Clear all dependencies (useful for testing) + */ + clear() { + this.dependencies.clear(); + this.singletons.clear(); + } +} + +// Export singleton instance +export const container = new DependencyContainer(); + +// Export class for testing +export { DependencyContainer }; + diff --git a/src/js/core/event-bus.js b/src/js/core/event-bus.js new file mode 100644 index 0000000..7e624b0 --- /dev/null +++ b/src/js/core/event-bus.js @@ -0,0 +1,86 @@ +/** + * Event Bus Module + * Provides a centralized event system for module communication + * This decouples modules and makes the architecture more scalable + */ + +class EventBus { + constructor() { + this.events = new Map(); + } + + /** + * Subscribe to an event + * @param {string} event - Event name + * @param {Function} callback - Callback function + * @returns {Function} Unsubscribe function + */ + on(event, callback) { + if (!this.events.has(event)) { + this.events.set(event, []); + } + this.events.get(event).push(callback); + + // Return unsubscribe function + return () => this.off(event, callback); + } + + /** + * Unsubscribe from an event + * @param {string} event - Event name + * @param {Function} callback - Callback function to remove + */ + off(event, callback) { + if (!this.events.has(event)) return; + + const callbacks = this.events.get(event); + const index = callbacks.indexOf(callback); + if (index > -1) { + callbacks.splice(index, 1); + } + } + + /** + * Emit an event + * @param {string} event - Event name + * @param {*} data - Data to pass to callbacks + */ + emit(event, data = null) { + if (!this.events.has(event)) return; + + this.events.get(event).forEach(callback => { + try { + callback(data); + } catch (error) { + console.error(`Error in event handler for "${event}":`, error); + } + }); + } + + /** + * Subscribe to an event once + * @param {string} event - Event name + * @param {Function} callback - Callback function + */ + once(event, callback) { + const wrapper = (data) => { + callback(data); + this.off(event, wrapper); + }; + this.on(event, wrapper); + } + + /** + * Clear all event listeners + */ + clear() { + this.events.clear(); + } +} + +// Export singleton instance +export const eventBus = new EventBus(); + +// Export class for testing +export { EventBus }; + diff --git a/src/js/modules/animation.js b/src/js/modules/animation.js new file mode 100644 index 0000000..4d23cdb --- /dev/null +++ b/src/js/modules/animation.js @@ -0,0 +1,73 @@ +/** + * Animation Manager Module + * Handles scroll-triggered animations using Intersection Observer + */ + +import { CONSTANTS } from '../core/constants.js'; + +export class AnimationManager { + constructor() { + // Can be extended with dependency injection + } + /** + * Initialize intersection observer for animations + */ + init() { + if (!('IntersectionObserver' in window)) { + // Fallback for browsers without IntersectionObserver + this.fallbackAnimation(); + return; + } + + const observerOptions = { + threshold: CONSTANTS.ANIMATION.INTERSECTION_THRESHOLD, + rootMargin: CONSTANTS.ANIMATION.ROOT_MARGIN + }; + + const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + this.animateIn(entry.target); + } + }); + }, observerOptions); + + // Observe all animatable elements + const elements = document.querySelectorAll(CONSTANTS.SELECTORS.ANIMATABLE_ELEMENTS); + elements.forEach(el => { + this.prepareElement(el); + observer.observe(el); + }); + } + + /** + * Prepare element for animation + * @param {HTMLElement} element - Element to prepare + */ + prepareElement(element) { + element.style.opacity = '0'; + element.style.transform = 'translateY(20px)'; + element.style.transition = `opacity ${CONSTANTS.ANIMATION.DURATION}ms ease, transform ${CONSTANTS.ANIMATION.DURATION}ms ease`; + } + + /** + * Animate element in + * @param {HTMLElement} element - Element to animate + */ + animateIn(element) { + element.style.opacity = '1'; + element.style.transform = 'translateY(0)'; + } + + /** + * Fallback animation for browsers without IntersectionObserver + */ + fallbackAnimation() { + const elements = document.querySelectorAll(CONSTANTS.SELECTORS.ANIMATABLE_ELEMENTS); + elements.forEach(el => { + el.style.opacity = '1'; + el.style.transform = 'translateY(0)'; + }); + } +} + diff --git a/src/js/modules/country-filter.js b/src/js/modules/country-filter.js new file mode 100644 index 0000000..eecde30 --- /dev/null +++ b/src/js/modules/country-filter.js @@ -0,0 +1,145 @@ +/** + * Country Filter Module + * Handles filtering of data tables by country + * + * Note: I18N is loaded as regular script, available via window + */ + +export class CountryFilter { + constructor() { + this.languageManager = null; + } + /** + * Initialize country filter functionality + * @param {Object} languageManager - LanguageManager instance for translations + */ + init(languageManager) { + this.languageManager = languageManager; + const countryButtons = document.querySelectorAll('.country-button'); + if (countryButtons.length === 0) return; + + // Wait for tables to be rendered first + setTimeout(() => { + // Mark all table rows with data attributes for filtering + this.markTableRows(); + + // Add event listeners to buttons + countryButtons.forEach(button => { + button.addEventListener('click', (e) => { + const selectedCountry = button.getAttribute('data-country'); + + // Update active state + countryButtons.forEach(btn => btn.classList.remove('active')); + button.classList.add('active'); + + // Filter tables + this.filterTables(selectedCountry); + }); + }); + }, 100); + } + + /** + * Mark table rows with country data attributes for filtering + * Note: Rows are already marked during rendering, but this ensures compatibility + */ + markTableRows() { + // Rows are already marked with data-country attribute during rendering + // This method is kept for backward compatibility + } + + /** + * Filter tables based on selected country + * @param {string} selectedCountry - Selected country code or 'all' or 'international' + */ + filterTables(selectedCountry) { + // Get all filterable tables + const tables = [ + '[data-table="personal-data-numbers"]', + '[data-table="personal-data-text"]', + '[data-table="banking-secrecy"]' + ]; + + tables.forEach(tableSelector => { + const table = document.querySelector(tableSelector); + if (!table) return; + + const rows = table.querySelectorAll('tbody tr'); + let visibleCount = 0; + + rows.forEach((row, index) => { + const rowCountry = row.getAttribute('data-country'); + let shouldShow = false; + + if (selectedCountry === 'all') { + // Show all rows + shouldShow = true; + } else if (selectedCountry === 'international') { + // Show only international rows + shouldShow = rowCountry === 'international'; + } else { + // Show rows for selected country AND international rows + shouldShow = rowCountry === selectedCountry || rowCountry === 'international'; + } + + // Add smooth transition + if (shouldShow) { + row.style.opacity = '0'; + row.style.transform = 'translateY(-10px)'; + row.style.display = ''; + visibleCount++; + + // Animate in + setTimeout(() => { + row.style.transition = 'opacity 0.3s ease, transform 0.3s ease'; + row.style.opacity = '1'; + row.style.transform = 'translateY(0)'; + }, index * 20); + } else { + // Animate out + row.style.transition = 'opacity 0.2s ease, transform 0.2s ease'; + row.style.opacity = '0'; + row.style.transform = 'translateY(-10px)'; + + setTimeout(() => { + row.style.display = 'none'; + }, 200); + } + }); + + // Show message if no rows visible + this.showEmptyMessage(table, visibleCount === 0); + }); + } + + /** + * Show or hide empty message for table + * @param {HTMLElement} table - Table element + * @param {boolean} show - Whether to show message + */ + showEmptyMessage(table, show) { + let message = table.parentElement.querySelector('.table-empty-message'); + + if (show && !message) { + message = document.createElement('div'); + message.className = 'table-empty-message'; + + // I18N is loaded as regular script, available via window + const I18N = window.I18N || {}; + + if (this.languageManager) { + const currentLang = this.languageManager.getCurrentLanguage(); + const translations = I18N[currentLang] || I18N.en || {}; + message.textContent = translations.emptyMessage || 'No data available for selected country'; + } else { + message.textContent = 'No data available for selected country'; + } + + message.setAttribute('data-i18n', 'emptyMessage'); + table.parentElement.appendChild(message); + } else if (!show && message) { + message.remove(); + } + } +} + diff --git a/src/js/modules/data-renderer.js b/src/js/modules/data-renderer.js new file mode 100644 index 0000000..e7a5817 --- /dev/null +++ b/src/js/modules/data-renderer.js @@ -0,0 +1,489 @@ +/** + * Data Renderer Module + * Renders data tables and content from CONFIG + * + * Note: CONFIG and I18N are loaded as regular scripts, available via window + */ + +export class DataRenderer { + /** + * Initialize data rendering from config + * @param {Object} languageManager - LanguageManager instance for translations + * @param {Object} config - Configuration object (optional, uses imported CONFIG by default) + */ + constructor() { + this.languageManager = null; + // CONFIG is loaded as regular script, available via window + this.config = window.CONFIG; + } + + /** + * Initialize the renderer + * @param {Object} languageManager - LanguageManager instance for translations + * @param {Object} config - Optional config override + */ + init(languageManager, config = null) { + this.languageManager = languageManager; + if (config) { + this.config = config; + } + + if (!this.config) { + console.warn('CONFIG is not defined. Data will not be rendered.'); + return; + } + + this.renderPersonalDataNumbers(); + this.renderPersonalDataText(); + this.renderPciDss(); + this.renderBankingSecrecy(); + this.renderCrypto(); + this.renderItAssets(); + this.renderCustomSignatures(); + this.renderFileTypes(); + this.renderDataSources(); + this.renderUseCases(); + this.renderDownloads(); + } + + /** + * Get translation for a key + * @param {string} key - Translation key + * @returns {string} Translated text + */ + getTranslation(key) { + if (!this.languageManager) { + return key; + } + + // I18N is loaded as regular script, available via window + const I18N = window.I18N || {}; + const lang = this.languageManager.getCurrentLanguage(); + const translations = I18N[lang] || I18N.en || {}; + + const keys = key.split('.'); + let value = translations; + for (const k of keys) { + value = value && value[k]; + } + return value !== undefined ? value : key; + } + + /** + * Render table row + * @param {HTMLElement} tbody - Table body element + * @param {Array} cells - Array of cell content (strings or HTML strings) + * @param {string} country - Optional country code for filtering + */ + renderTableRow(tbody, cells, country = null) { + const row = document.createElement('tr'); + if (country !== null) { + // Normalize country: '-' becomes 'international' + const normalizedCountry = country === '-' ? 'international' : country; + row.setAttribute('data-country', normalizedCountry); + } + cells.forEach(cellContent => { + const cell = document.createElement('td'); + if (typeof cellContent === 'string') { + // Check if it's HTML (contains tags) + if (cellContent.includes('<')) { + cell.innerHTML = cellContent; + } else { + cell.textContent = cellContent; + } + } else { + cell.appendChild(cellContent); + } + row.appendChild(cell); + }); + tbody.appendChild(row); + } + + /** + * Get current translations object + * @returns {Object} Translations object + */ + getTranslations() { + // I18N is loaded as regular script, available via window + const I18N = window.I18N || {}; + + if (!this.languageManager) { + return I18N.en || {}; + } + + const currentLang = this.languageManager.getCurrentLanguage(); + return I18N[currentLang] || I18N.en || {}; + } + + /** + * Render Personal Data (numbers) table + */ + renderPersonalDataNumbers() { + const tbody = document.querySelector('[data-table="personal-data-numbers"] tbody'); + if (!tbody) return; + + // If table already has content (server-rendered), only ensure data-country attributes are set + if (tbody.children.length > 0) { + // Content already exists, just ensure data-country attributes are set for filtering + Array.from(tbody.children).forEach((row, index) => { + if (!row.hasAttribute('data-country') && this.config.personalDataNumbers[index]) { + const item = this.config.personalDataNumbers[index]; + const normalizedCountry = item.country === '-' ? 'international' : item.country; + row.setAttribute('data-country', normalizedCountry); + } + }); + return; + } + + // Table is empty, render from scratch + tbody.innerHTML = ''; + this.config.personalDataNumbers.forEach(item => { + const countryDisplay = item.country === '-' ? '-' : item.country; + this.renderTableRow(tbody, [ + item.type, + item.localName, + countryDisplay, + `${item.example}` + ], item.country); + }); + } + + /** + * Render Personal Data (text) table + */ + renderPersonalDataText() { + const tbody = document.querySelector('[data-table="personal-data-text"] tbody'); + if (!tbody) return; + + // If table already has content (server-rendered), only ensure data-country attributes are set + if (tbody.children.length > 0) { + // Content already exists, just ensure data-country attributes are set for filtering + Array.from(tbody.children).forEach((row, index) => { + if (!row.hasAttribute('data-country') && this.config.personalDataText[index]) { + const item = this.config.personalDataText[index]; + const normalizedCountry = item.country === '-' ? 'international' : item.country; + row.setAttribute('data-country', normalizedCountry); + } + }); + return; + } + + // Table is empty, render from scratch + tbody.innerHTML = ''; + this.config.personalDataText.forEach(item => { + const countryDisplay = item.country === '-' ? '-' : item.country; + this.renderTableRow(tbody, [ + item.type, + item.localName, + countryDisplay, + `${item.example}` + ], item.country); + }); + } + + /** + * Render PCI DSS table + */ + renderPciDss() { + const tbody = document.querySelector('[data-table="pci-dss"] tbody'); + if (!tbody) return; + + // If table already has content (server-rendered), skip rendering + if (tbody.children.length > 0) { + return; + } + + // Table is empty, render from scratch + tbody.innerHTML = ''; + this.config.pciDss.forEach(item => { + this.renderTableRow(tbody, [ + item.type, + `${item.example}` + ]); + }); + } + + /** + * Render Banking Secrecy table + */ + renderBankingSecrecy() { + const tbody = document.querySelector('[data-table="banking-secrecy"] tbody'); + if (!tbody) return; + + // If table already has content (server-rendered), only ensure data-country attributes are set + if (tbody.children.length > 0) { + // Content already exists, just ensure data-country attributes are set for filtering + Array.from(tbody.children).forEach((row, index) => { + if (!row.hasAttribute('data-country') && this.config.bankingSecrecy[index]) { + const item = this.config.bankingSecrecy[index]; + row.setAttribute('data-country', item.country); + } + }); + return; + } + + // Table is empty, render from scratch + tbody.innerHTML = ''; + this.config.bankingSecrecy.forEach(item => { + const countryDisplay = item.country === '-' ? '-' : item.country; + this.renderTableRow(tbody, [ + item.type, + countryDisplay, + `${item.example}` + ], item.country); + }); + } + + /** + * Render Crypto table + */ + renderCrypto() { + const tbody = document.querySelector('[data-table="crypto"] tbody'); + if (!tbody) return; + + // If table already has content (server-rendered), skip rendering + if (tbody.children.length > 0) { + return; + } + + const translations = this.getTranslations(); + + // Use translated crypto data if available, otherwise fall back to CONFIG + const crypto = translations.crypto && Array.isArray(translations.crypto) + ? translations.crypto + : this.config.crypto; + + // Table is empty, render from scratch + tbody.innerHTML = ''; + crypto.forEach(item => { + this.renderTableRow(tbody, [ + item.type, + `${item.example}` + ]); + }); + } + + /** + * Render IT Assets table + */ + renderItAssets() { + const tbody = document.querySelector('[data-table="it-assets"] tbody'); + if (!tbody) return; + + // If table already has content (server-rendered), skip rendering + if (tbody.children.length > 0) { + return; + } + + const translations = this.getTranslations(); + + // Use translated IT assets if available, otherwise fall back to CONFIG + const itAssets = translations.itAssets && Array.isArray(translations.itAssets) + ? translations.itAssets + : this.config.itAssets; + + // Table is empty, render from scratch + tbody.innerHTML = ''; + itAssets.forEach(item => { + // Check if example should be wrapped in code tags + const shouldWrapInCode = !item.example.startsWith('Finds') && + !item.example.startsWith('Находит') && + !item.example.includes('SHA-256'); + const exampleCell = shouldWrapInCode + ? `${item.example}` + : item.example; + + this.renderTableRow(tbody, [ + item.type, + exampleCell + ]); + }); + } + + /** + * Render Custom Signatures section + */ + renderCustomSignatures() { + const container = document.querySelector('[data-section="custom-signatures"]'); + if (!container) return; + + const translations = this.getTranslations(); + + // Use translated examples if available, otherwise fall back to CONFIG + const examples = translations.categories && translations.categories.customSignaturesExamples + ? translations.categories.customSignaturesExamples + : this.config.customSignatures.examples; + + const examplesHTML = examples + .map(ex => `${ex}`) + .join(', '); + + const description = container.querySelector('.category-description'); + if (description) { + const descText = translations.categories && translations.categories.customSignaturesDesc + ? translations.categories.customSignaturesDesc + : this.config.customSignatures.description; + const orText = translations.categories && translations.categories.customSignaturesOr + ? translations.categories.customSignaturesOr + : 'or any other.'; + description.innerHTML = `${descText} ${examplesHTML} ${orText}`; + } + } + + /** + * Render File Types table + */ + renderFileTypes() { + const tbody = document.querySelector('[data-table="file-types"] tbody'); + if (!tbody) return; + + // If table already has content (server-rendered), skip rendering + if (tbody.children.length > 0) { + return; + } + + // Table is empty, render from scratch + tbody.innerHTML = ''; + this.config.fileTypes.forEach(item => { + this.renderTableRow(tbody, [ + item.category, + `${item.formats}` + ]); + }); + } + + /** + * Render Data Sources table + */ + renderDataSources() { + const tbody = document.querySelector('[data-table="data-sources"] tbody'); + if (!tbody) return; + + // If table already has content (server-rendered), skip rendering + if (tbody.children.length > 0) { + return; + } + + const translations = this.getTranslations(); + + // Use translated data sources if available, otherwise fall back to CONFIG + const dataSources = translations.sections && translations.sections.dataSources && translations.sections.dataSources.sources + ? translations.sections.dataSources.sources + : this.config.dataSources; + + // Table is empty, render from scratch + tbody.innerHTML = ''; + dataSources.forEach(item => { + this.renderTableRow(tbody, [ + item.connector, + item.description + ]); + }); + } + + /** + * Render Use Cases + */ + renderUseCases() { + const container = document.querySelector('[data-section="use-cases"]'); + if (!container) return; + + const useCasesContainer = container.querySelector('.use-cases'); + if (!useCasesContainer) return; + + const translations = this.getTranslations(); + + // Use translated use cases if available, otherwise fall back to CONFIG + const useCases = translations.sections && translations.sections.useCases && translations.sections.useCases.cases + ? translations.sections.useCases.cases + : this.config.useCases; + + // Icons for each use case - standard icons + const useCaseIcons = [ + // Leak hunting - folder with search + ` + + + + `, + // PCI DSS - credit card + ` + + + `, + // Banking - shield + ` + + `, + // Sales team - folder + ` + + `, + // Cryptocurrency - laptop + ` + + + + `, + // Database - database + ` + + + + ` + ]; + + useCasesContainer.innerHTML = ''; + useCases.forEach((useCase, index) => { + const item = document.createElement('div'); + item.className = 'use-case-item'; + item.innerHTML = ` + ${useCaseIcons[index] || useCaseIcons[0]} +

${useCase}

+ `; + useCasesContainer.appendChild(item); + }); + } + + /** + * Render Download links + */ + renderDownloads() { + const platforms = { + windows: document.querySelector('[data-platform="windows"]'), + linux: document.querySelector('[data-platform="linux"]'), + macos: document.querySelector('[data-platform="macos"]') + }; + + Object.keys(platforms).forEach(platform => { + const container = platforms[platform]; + if (!container) return; + + const linksContainer = container.querySelector('.download-links'); + if (!linksContainer) return; + + linksContainer.innerHTML = ''; + this.config.downloads[platform].forEach(link => { + const linkEl = document.createElement('a'); + linkEl.href = link.href; + linkEl.className = 'download-link'; + linkEl.innerHTML = ` + + + + + ${link.text} + `; + linksContainer.appendChild(linkEl); + }); + }); + + // Update system requirements + const sysReq = document.querySelector('[data-info="system-requirements"]'); + if (sysReq) { + const systemRequirementsText = this.getTranslation('sections.download.systemRequirements'); + sysReq.textContent = systemRequirementsText || `System Requirements: ${this.config.systemRequirements}`; + } + } +} + diff --git a/src/js/modules/language.js b/src/js/modules/language.js new file mode 100644 index 0000000..7ad55c3 --- /dev/null +++ b/src/js/modules/language.js @@ -0,0 +1,538 @@ +/** + * Language Manager Module + * Handles internationalization, language detection, and translation management + * + * Note: I18N is loaded as regular script, available via window + */ + +export class LanguageManager { + constructor() { + this.currentLanguage = 'en'; + this.dataRenderer = null; + } + + /** + * Set DataRenderer reference for re-rendering on language change + * @param {Object} dataRenderer - DataRenderer instance + */ + setDataRenderer(dataRenderer) { + this.dataRenderer = dataRenderer; + } + + /** + * Initialize language system + */ + init() { + // Check if we need to redirect to language-specific URL + const path = window.location.pathname; + const pathMatch = path.match(/^\/(ru|de|fr|es)\//); + + // If we're on root page (no language prefix) and have saved non-English language, redirect + if (!pathMatch) { + const savedLanguage = localStorage.getItem('language'); + if (savedLanguage && savedLanguage !== 'en' && + (savedLanguage === 'ru' || savedLanguage === 'es' || savedLanguage === 'de' || savedLanguage === 'fr')) { + // Determine current page name + let currentPage = path.split('/').filter(p => p).pop() || ''; + if (currentPage.endsWith('.html')) { + currentPage = currentPage.replace(/\.html$/, ''); + } + if (!currentPage || currentPage === '' || currentPage === 'index') { + currentPage = ''; + } + + // Build redirect URL (trailing slash for canonical URLs) + const redirectUrl = currentPage ? `/${savedLanguage}/${currentPage}/` : `/${savedLanguage}/`; + window.location.href = redirectUrl; + return; // Exit early, page will reload + } + } + + // Detect language from URL first (has priority) + const detectedLang = this.detectLanguage(); + + // Use detected language (which already checks saved language for root pages) + const languageToUse = detectedLang || 'en'; + + // Set language (but don't redirect if we're already on the right page) + this.currentLanguage = languageToUse; + localStorage.setItem('language', languageToUse); + + // Initialize language selector (hidden select for form compatibility) + const languageSelector = document.getElementById('languageSelector'); + if (languageSelector) { + languageSelector.value = this.currentLanguage; + } + + // Initialize custom dropdown + this.initCustomLanguageDropdown(); + + // Update all internal links to preserve language + this.updateInternalLinks(languageToUse); + + // For de/fr/es: pages are pre-translated, just ensure page is visible + if (languageToUse === 'de' || languageToUse === 'fr' || languageToUse === 'es') { + // Don't call applyLanguage for pre-translated pages - it might try to update translations + // Just ensure page is visible and language attributes are set + document.documentElement.setAttribute('lang', languageToUse); + document.documentElement.style.visibility = ''; + if (document.documentElement.hasAttribute('data-lang-loading')) { + document.documentElement.removeAttribute('data-lang-loading'); + } + } else { + // For en/ru: apply translations + this.applyLanguage(languageToUse, false); + + // Show page after translations are applied (if it was hidden) + if (document.documentElement.hasAttribute('data-lang-loading')) { + document.documentElement.style.visibility = ''; + document.documentElement.removeAttribute('data-lang-loading'); + } + } + } + + /** + * Initialize custom language dropdown + */ + initCustomLanguageDropdown() { + const button = document.getElementById('languageSelectorButton'); + const dropdown = document.getElementById('languageDropdown'); + const hiddenSelect = document.getElementById('languageSelector'); + + if (!button || !dropdown || !hiddenSelect) return; + + // Update button text based on current language + this.updateLanguageButton(); + + // Toggle dropdown on button click + button.addEventListener('click', (e) => { + e.stopPropagation(); + const isOpen = dropdown.classList.contains('active'); + this.toggleLanguageDropdown(!isOpen); + }); + + // Handle option clicks + const options = dropdown.querySelectorAll('.language-option'); + options.forEach(option => { + option.addEventListener('click', (e) => { + e.stopPropagation(); + const value = option.getAttribute('data-value'); + if (value) { + this.setLanguage(value); + this.toggleLanguageDropdown(false); + } + }); + }); + + // Close dropdown when clicking outside + document.addEventListener('click', (e) => { + if (!button.contains(e.target) && !dropdown.contains(e.target)) { + this.toggleLanguageDropdown(false); + } + }); + + // Close dropdown on Escape key + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && dropdown.classList.contains('active')) { + this.toggleLanguageDropdown(false); + button.focus(); + } + }); + } + + /** + * Toggle language dropdown + */ + toggleLanguageDropdown(open) { + const button = document.getElementById('languageSelectorButton'); + const dropdown = document.getElementById('languageDropdown'); + + if (!button || !dropdown) return; + + if (open) { + dropdown.classList.add('active'); + button.setAttribute('aria-expanded', 'true'); + } else { + dropdown.classList.remove('active'); + button.setAttribute('aria-expanded', 'false'); + } + } + + /** + * Update language button text + */ + updateLanguageButton() { + const buttonText = document.getElementById('languageSelectorText'); + const hiddenSelect = document.getElementById('languageSelector'); + const options = document.querySelectorAll('.language-option'); + + if (!buttonText || !hiddenSelect) return; + + const currentValue = hiddenSelect.value; + const currentOption = hiddenSelect.querySelector(`option[value="${currentValue}"]`); + + // Get the base path from existing logo icon to maintain correct relative/absolute path + const existingLogoIcon = document.querySelector('.logo-icon'); + let assetsPath = '/assets/'; + + // If we have an existing logo icon, extract the base path from its src attribute + if (existingLogoIcon && existingLogoIcon.getAttribute('src')) { + const logoSrc = existingLogoIcon.getAttribute('src'); + // Extract the directory path (everything before the filename) + const pathMatch = logoSrc.match(/^(.+\/)favicon[^\/]*\.ico$/); + if (pathMatch) { + assetsPath = pathMatch[1]; + } + } + + // Language to flag mapping + const flagMap = { + 'en': { file: 'flag-us.svg', alt: 'US', name: 'English' }, + 'ru': { file: 'flag-ru.svg', alt: 'RU', name: 'Русский' }, + 'es': { file: 'flag-es.svg', alt: 'ES', name: 'Español' }, + 'de': { file: 'flag-de.svg', alt: 'DE', name: 'Deutsch' }, + 'fr': { file: 'flag-fr.svg', alt: 'FR', name: 'Français' } + }; + + if (currentOption && flagMap[currentValue]) { + const flagInfo = flagMap[currentValue]; + buttonText.innerHTML = `${flagInfo.alt} ${flagInfo.name}`; + } + + // Update selected state in dropdown (don't update flag paths - they're already correct in HTML) + options.forEach(option => { + if (option.getAttribute('data-value') === currentValue) { + option.classList.add('selected'); + } else { + option.classList.remove('selected'); + } + }); + } + + /** + * Detect language from URL, localStorage, or URL parameter + */ + detectLanguage() { + // Check URL path for language prefix (/ru/, /de/, /fr/, /es/) + const path = window.location.pathname; + const pathMatch = path.match(/^\/(ru|de|fr|es)\//); + if (pathMatch && pathMatch[1]) { + return pathMatch[1]; + } + + // If we're on root pages (/, /index.html, etc.), check saved language first + // This ensures that when switching from /ru/ to English, we use English + const savedLanguage = localStorage.getItem('language'); + if (savedLanguage && (savedLanguage === 'en' || savedLanguage === 'ru' || savedLanguage === 'es' || savedLanguage === 'de' || savedLanguage === 'fr')) { + // If we're on root page (no language prefix in URL), return saved language + if (!pathMatch) { + return savedLanguage; + } + } + + // Check URL parameter + const urlParams = new URLSearchParams(window.location.search); + const langParam = urlParams.get('lang'); + if (langParam && (langParam === 'en' || langParam === 'ru' || langParam === 'es' || langParam === 'de' || langParam === 'fr')) { + return langParam; + } + + return 'en'; // Default to English + } + + /** + * Update all internal links to include language prefix + * @param {string} lang - Language code + */ + updateInternalLinks(lang) { + // List of internal HTML pages + const internalPages = ['index.html', 'discovery.html', 'features.html', 'use-cases.html', 'download.html']; + + // Get all links on the page + const links = document.querySelectorAll('a[href]'); + + links.forEach(link => { + const href = link.getAttribute('href'); + if (!href) return; + + // Skip external links, anchors, and mailto links + if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('mailto:') || href.startsWith('#')) { + return; + } + + // Check if it's an internal page (exact match or ends with page name) + const isInternalPage = internalPages.some(page => { + return href === page || + href === '/' + page || + href.endsWith('/' + page) || + href === '/' + lang + '/' + page; + }); + + if (isInternalPage) { + // Extract page name (remove any existing language prefix and leading slashes) + let pageName = href; + + // Remove language prefix if present + pageName = pageName.replace(/^\/(ru|de|fr|es)\//, ''); + + // Remove leading slash + pageName = pageName.replace(/^\//, ''); + + // Ensure we have a valid page name + if (!pageName || !internalPages.includes(pageName)) { + // Try to extract from full path + const match = href.match(/\/([^\/]+\.html)$/); + if (match && internalPages.includes(match[1])) { + pageName = match[1]; + } else { + return; // Skip if we can't determine the page + } + } + + // Update href based on language (trailing slash for canonical URLs) + if (lang === 'ru' || lang === 'de' || lang === 'fr' || lang === 'es') { + link.setAttribute('href', pageName ? `/${lang}/${pageName}/` : `/${lang}/`); + } else { + link.setAttribute('href', pageName ? `/${pageName}/` : '/'); + } + } + }); + } + + /** + * Apply language without redirecting + * @param {string} lang - Language code + * @param {boolean} updateStorage - Whether to update localStorage + */ + applyLanguage(lang, updateStorage = true) { + this.currentLanguage = lang; + if (updateStorage) { + localStorage.setItem('language', lang); + } + + // Update language selector (hidden select) + const languageSelector = document.getElementById('languageSelector'); + if (languageSelector) { + languageSelector.value = lang; + } + + // Update custom dropdown button + this.updateLanguageButton(); + + // Update all internal links to preserve language + this.updateInternalLinks(lang); + + // Update HTML lang attribute + if (lang === 'ru') { + document.documentElement.setAttribute('lang', 'ru'); + } else if (lang === 'de') { + document.documentElement.setAttribute('lang', 'de'); + } else if (lang === 'fr') { + document.documentElement.setAttribute('lang', 'fr'); + } else if (lang === 'es') { + document.documentElement.setAttribute('lang', 'es'); + } else { + document.documentElement.setAttribute('lang', 'en'); + } + + // Handle different languages + if (lang === 'en' || lang === 'ru') { + // Use custom translations + // Apply translations immediately without delay to avoid showing English first + this.updateTranslations(lang); + // Note: Page title is already set correctly in HTML template, so we don't override it here + // Re-render use cases, data sources, custom signatures, IT assets, crypto and downloads with new translations + if (this.dataRenderer) { + this.dataRenderer.renderUseCases(); + this.dataRenderer.renderDataSources(); + this.dataRenderer.renderCustomSignatures(); + this.dataRenderer.renderItAssets(); + this.dataRenderer.renderCrypto(); + this.dataRenderer.renderDownloads(); + } + // Show page after translations are applied (if it was hidden) + if (document.documentElement.hasAttribute('data-lang-loading')) { + document.documentElement.style.visibility = ''; + document.documentElement.removeAttribute('data-lang-loading'); + } + } else { + // For es, de, fr: pages are pre-translated via Python API + // No need to update translations - pages are already translated + // Don't update title - it's already translated in HTML + // Show page immediately (it was never hidden for pre-translated pages) + // But ensure it's visible in case something went wrong + document.documentElement.style.visibility = ''; + if (document.documentElement.hasAttribute('data-lang-loading')) { + document.documentElement.removeAttribute('data-lang-loading'); + } + } + } + + /** + * Set language (with redirect if needed) + * @param {string} lang - Language code + */ + setLanguage(lang) { + // Check if we need to redirect to language-specific URL + const currentPath = window.location.pathname; + const currentLangMatch = currentPath.match(/^\/(ru|de|fr|es)\//); + + // Determine current page name (remove .html extension for clean URLs) + let currentPage = currentPath.split('/').filter(p => p).pop() || ''; + + // If we're on a language page, extract the page name + if (currentLangMatch) { + const parts = currentPath.split('/').filter(p => p); + if (parts.length > 1) { + currentPage = parts[parts.length - 1]; + } else { + currentPage = ''; + } + } + + // Remove .html extension if present + if (currentPage.endsWith('.html')) { + currentPage = currentPage.replace(/\.html$/, ''); + } + + // Handle root/index page - use clean URL (empty string or '/') + if (!currentPage || currentPage === '' || currentPage === 'index') { + currentPage = ''; + } + + // If switching to a language that has a dedicated page (ru, de, fr, es) + if (lang === 'ru' || lang === 'de' || lang === 'fr' || lang === 'es') { + // If we're not already on that language's page, redirect + if (!currentLangMatch || currentLangMatch[1] !== lang) { + // Save language to localStorage before redirect + localStorage.setItem('language', lang); + // Build clean URL with trailing slash + const newUrl = currentPage ? `/${lang}/${currentPage}/` : `/${lang}/`; + window.location.href = newUrl; + return; // Exit early, page will reload + } + } else if (lang === 'en') { + // If switching to English and we're on a language page, redirect to root + if (currentLangMatch) { + // Save language to localStorage before redirect + localStorage.setItem('language', lang); + // Build clean URL with trailing slash + const rootPage = currentPage ? `/${currentPage}/` : '/'; + window.location.href = rootPage; + return; // Exit early, page will reload + } + } + + // If we're already on the correct page, apply language without redirect + this.applyLanguage(lang, true); + } + + /** + * Update translations for custom languages (en, ru) + * @param {string} lang - Language code + */ + updateTranslations(lang) { + // Only update translations for en and ru + // de, fr, es pages are pre-translated via Python API + if (lang !== 'en' && lang !== 'ru') { + return; + } + + // I18N is loaded as regular script, available via window + const I18N = window.I18N || {}; + + if (!I18N[lang]) { + console.warn(`Translations for language ${lang} not found`); + return; + } + + const translations = I18N[lang]; + const elements = document.querySelectorAll('[data-i18n]'); + + elements.forEach(element => { + const key = element.getAttribute('data-i18n'); + if (!key) return; + + // Skip if element has child elements with data-i18n (they will be processed separately) + if (element.querySelector('[data-i18n]')) { + return; + } + + const value = this.getNestedValue(translations, key); + + if (value !== undefined && value !== null) { + try { + if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') { + element.value = value; + } else if (element.hasAttribute('placeholder')) { + element.setAttribute('placeholder', value); + } else { + // Simple text replacement - innerHTML will be handled by child elements + element.textContent = value; + } + } catch (error) { + console.warn(`Error updating translation for key "${key}":`, error); + } + } + }); + } + + /** + * Convert snake_case to camelCase + * @param {string} str - String to convert + * @returns {string} Converted string + */ + _snakeToCamel(str) { + return str.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase()); + } + + /** + * Convert camelCase to snake_case + * @param {string} str - String to convert + * @returns {string} Converted string + */ + _camelToSnake(str) { + return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); + } + + /** + * Get nested value from object using dot notation + * Supports both snake_case and camelCase keys for compatibility + * @param {Object} obj - Object to search + * @param {string} path - Dot notation path (can be snake_case or camelCase) + * @returns {*} Value or undefined + */ + getNestedValue(obj, path) { + return path.split('.').reduce((current, key) => { + if (!current) return undefined; + + // First try exact key match + if (current[key] !== undefined) { + return current[key]; + } + + // Try converting snake_case to camelCase + const camelKey = this._snakeToCamel(key); + if (current[camelKey] !== undefined) { + return current[camelKey]; + } + + // Try converting camelCase to snake_case + const snakeKey = this._camelToSnake(key); + if (current[snakeKey] !== undefined) { + return current[snakeKey]; + } + + return undefined; + }, obj); + } + +М /** + * Get current language + * @returns {string} Current language code + */ + getCurrentLanguage() { + return this.currentLanguage; + } +} + diff --git a/src/js/modules/lightbox.js b/src/js/modules/lightbox.js new file mode 100644 index 0000000..c9794a0 --- /dev/null +++ b/src/js/modules/lightbox.js @@ -0,0 +1,139 @@ +/** + * Lightbox Manager Module + * Handles image lightbox functionality + */ + +import { CONSTANTS } from '../core/constants.js'; + +export class LightboxManager { + constructor() { + this.themeManager = null; + } + + /** + * Set theme manager reference + * @param {Object} themeManager - ThemeManager instance + */ + setThemeManager(themeManager) { + this.themeManager = themeManager; + } + /** + * Initialize lightbox functionality + * @param {Object} themeManager - ThemeManager instance (optional) + */ + init(themeManager = null) { + if (themeManager) { + this.setThemeManager(themeManager); + } + const screenshotContainer = document.getElementById('screenshotContainer'); + const lightbox = document.getElementById('lightbox'); + const lightboxClose = document.getElementById('lightboxClose'); + const lightboxImage = document.getElementById('lightboxScreenshot'); + const heroScreenshot = document.getElementById('heroScreenshot'); + + if (!screenshotContainer || !lightbox || !lightboxClose || !lightboxImage) return; + + // Open lightbox on screenshot click + screenshotContainer.addEventListener('click', () => { + this.open(heroScreenshot); + }); + + // Close lightbox + lightboxClose.addEventListener('click', (e) => { + e.stopPropagation(); + this.close(); + }); + + // Close on background click + lightbox.addEventListener('click', (e) => { + if (e.target === lightbox) { + this.close(); + } + }); + + // Close on ESC key + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && lightbox.classList.contains('active')) { + this.close(); + } + }); + + // Prevent body scroll when lightbox is open + this.observeLightbox(); + } + + /** + * Open lightbox with image + * @param {HTMLElement} sourceImage - Source image element + */ + open(sourceImage) { + const lightbox = document.getElementById('lightbox'); + const lightboxImage = document.getElementById('lightboxScreenshot'); + + if (!lightbox || !lightboxImage) return; + + // Set image source from source image + const lightSrc = sourceImage.getAttribute('data-light'); + const darkSrc = sourceImage.getAttribute('data-dark'); + const currentTheme = this.themeManager?.getCurrentTheme() || 'light'; + + if (currentTheme === CONSTANTS.THEME.DARK && darkSrc) { + lightboxImage.src = darkSrc; + } else if (lightSrc) { + lightboxImage.src = lightSrc; + } + + // Show lightbox + lightbox.classList.add('active'); + document.body.style.overflow = 'hidden'; + } + + /** + * Close lightbox + */ + close() { + const lightbox = document.getElementById('lightbox'); + if (!lightbox) return; + + lightbox.classList.remove('active'); + document.body.style.overflow = ''; + } + + /** + * Update lightbox image when theme changes + * @param {string} theme - Current theme + */ + updateLightboxImage(theme) { + const lightbox = document.getElementById('lightbox'); + const lightboxImage = document.getElementById('lightboxScreenshot'); + + if (!lightbox || !lightboxImage || !lightbox.classList.contains('active')) return; + + const lightSrc = lightboxImage.getAttribute('data-light'); + const darkSrc = lightboxImage.getAttribute('data-dark'); + + if (theme === CONSTANTS.THEME.DARK && darkSrc) { + lightboxImage.src = darkSrc; + } else if (lightSrc) { + lightboxImage.src = lightSrc; + } + } + + /** + * Observe lightbox state and update image on theme change + */ + observeLightbox() { + if (!this.themeManager) return; + + // Store original updateScreenshot method + const originalUpdateScreenshot = this.themeManager.updateScreenshot.bind(this.themeManager); + const self = this; + + // Wrap updateScreenshot to also update lightbox + this.themeManager.updateScreenshot = function(theme) { + originalUpdateScreenshot(theme); + self.updateLightboxImage(theme); + }; + } +} + diff --git a/src/js/modules/scroll.js b/src/js/modules/scroll.js new file mode 100644 index 0000000..0ae7eed --- /dev/null +++ b/src/js/modules/scroll.js @@ -0,0 +1,132 @@ +/** + * Scroll Manager Module + * Handles smooth scrolling, navbar effects, and mobile menu + */ + +import { CONSTANTS } from '../core/constants.js'; + +export class ScrollManager { + constructor() { + // Can be extended with dependency injection + } + /** + * Initialize scroll-related functionality + */ + init() { + this.initSmoothScroll(); + this.initNavbarScroll(); + this.initBurgerMenu(); + } + + /** + * Initialize smooth scroll for anchor links + */ + initSmoothScroll() { + const anchorLinks = document.querySelectorAll(CONSTANTS.SELECTORS.ANCHOR_LINKS); + + anchorLinks.forEach(anchor => { + anchor.addEventListener('click', (e) => { + const href = anchor.getAttribute('href'); + if (href === '#' || !href) return; + + e.preventDefault(); + const target = document.querySelector(href); + + if (target) { + const offsetTop = target.offsetTop - CONSTANTS.SCROLL.NAVBAR_HEIGHT; + window.scrollTo({ + top: offsetTop, + behavior: 'smooth' + }); + } + }); + }); + } + + /** + * Initialize navbar scroll effects + */ + initNavbarScroll() { + const navbar = document.querySelector(CONSTANTS.SELECTORS.NAVBAR); + if (!navbar) return; + + let ticking = false; + + const handleScroll = () => { + if (!ticking) { + window.requestAnimationFrame(() => { + const currentScroll = window.pageYOffset; + + if (currentScroll > CONSTANTS.SCROLL.SCROLL_THRESHOLD) { + navbar.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1)'; + } else { + navbar.style.boxShadow = 'none'; + } + + ticking = false; + }); + + ticking = true; + } + }; + + window.addEventListener('scroll', handleScroll, { passive: true }); + } + + /** + * Initialize burger menu for mobile devices + */ + initBurgerMenu() { + const burgerMenu = document.getElementById('burgerMenu'); + const navMenu = document.getElementById('navMenu'); + const navLinks = navMenu?.querySelectorAll('.nav-link'); + + if (!burgerMenu || !navMenu) return; + + // Toggle menu on burger click + burgerMenu.addEventListener('click', () => { + const isExpanded = burgerMenu.getAttribute('aria-expanded') === 'true'; + burgerMenu.setAttribute('aria-expanded', !isExpanded); + navMenu.classList.toggle('active'); + + // Prevent body scroll when menu is open + if (!isExpanded) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = ''; + } + }); + + // Close menu when clicking on a link + if (navLinks) { + navLinks.forEach(link => { + link.addEventListener('click', () => { + burgerMenu.setAttribute('aria-expanded', 'false'); + navMenu.classList.remove('active'); + document.body.style.overflow = ''; + }); + }); + } + + // Close menu when clicking outside + document.addEventListener('click', (e) => { + if (navMenu.classList.contains('active') && + !navMenu.contains(e.target) && + !burgerMenu.contains(e.target)) { + burgerMenu.setAttribute('aria-expanded', 'false'); + navMenu.classList.remove('active'); + document.body.style.overflow = ''; + } + }); + + // Close menu on window resize (if resizing to desktop) + window.addEventListener('resize', () => { + if (window.innerWidth > 768 && navMenu.classList.contains('active')) { + burgerMenu.setAttribute('aria-expanded', 'false'); + navMenu.classList.remove('active'); + document.body.style.overflow = ''; + } + }); + } +} + diff --git a/src/js/modules/table.js b/src/js/modules/table.js new file mode 100644 index 0000000..2ce351a --- /dev/null +++ b/src/js/modules/table.js @@ -0,0 +1,25 @@ +/** + * Table Enhancement Module + * Adds interactive enhancements to data tables + */ + +import { CONSTANTS } from '../core/constants.js'; + +export class TableEnhancement { + constructor() { + // Can be extended with dependency injection + } + /** + * Initialize table enhancements + */ + init() { + const tableRows = document.querySelectorAll(CONSTANTS.SELECTORS.TABLE_ROWS); + + tableRows.forEach(row => { + row.addEventListener('mouseenter', () => { + row.style.transition = 'background-color 0.2s ease'; + }); + }); + } +} + diff --git a/src/js/modules/theme.js b/src/js/modules/theme.js new file mode 100644 index 0000000..be116a6 --- /dev/null +++ b/src/js/modules/theme.js @@ -0,0 +1,160 @@ +/** + * Theme Manager Module + * Handles light/dark theme switching and related UI updates + */ + +import { CONSTANTS } from '../core/constants.js'; + +export class ThemeManager { + constructor() { + // Can be extended with dependency injection + } + /** + * Initialize theme system + */ + init() { + const themeToggle = document.querySelector(CONSTANTS.SELECTORS.THEME_TOGGLE); + if (!themeToggle) return; + + const html = document.documentElement; + // Check for saved theme first, then fall back to system preference + const savedTheme = localStorage.getItem(CONSTANTS.THEME.STORAGE_KEY); + const initialTheme = savedTheme || this.getSystemTheme(); + + this.setTheme(initialTheme); + this.updateIcon(initialTheme); + this.updateScreenshot(initialTheme); + this.updateFavicon(initialTheme); + + themeToggle.addEventListener('click', () => this.toggle()); + } + + /** + * Get system theme preference + * @returns {string} System theme ('light' or 'dark') + */ + getSystemTheme() { + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + return CONSTANTS.THEME.DARK; + } + return CONSTANTS.THEME.LIGHT; + } + + /** + * Set theme + * @param {string} theme - Theme name ('light' or 'dark') + */ + setTheme(theme) { + const html = document.documentElement; + html.setAttribute('data-theme', theme); + localStorage.setItem(CONSTANTS.THEME.STORAGE_KEY, theme); + this.updateScreenshot(theme); + this.updateFavicon(theme); + } + + /** + * Update screenshot image based on theme + * @param {string} theme - Current theme + */ + updateScreenshot(theme) { + const screenshot = document.getElementById('heroScreenshot'); + if (!screenshot) return; + + const lightSrc = screenshot.getAttribute('data-light'); + const darkSrc = screenshot.getAttribute('data-dark'); + + // Set correct src immediately + if (theme === CONSTANTS.THEME.DARK && darkSrc) { + screenshot.src = darkSrc; + } else if (lightSrc) { + screenshot.src = lightSrc; + } + + // Show image after src is set + screenshot.style.opacity = '1'; + } + + /** + * Get current theme + * @returns {string} Current theme + */ + getCurrentTheme() { + return document.documentElement.getAttribute('data-theme') || CONSTANTS.THEME.LIGHT; + } + + /** + * Toggle between light and dark theme + */ + toggle() { + const currentTheme = this.getCurrentTheme(); + const newTheme = currentTheme === CONSTANTS.THEME.DARK + ? CONSTANTS.THEME.LIGHT + : CONSTANTS.THEME.DARK; + + this.setTheme(newTheme); + this.updateIcon(newTheme); + } + + /** + * Update theme icon based on current theme + * @param {string} theme - Current theme + */ + updateIcon(theme) { + const themeToggle = document.querySelector(CONSTANTS.SELECTORS.THEME_TOGGLE); + if (!themeToggle) return; + + const themeIcon = themeToggle.querySelector('.theme-icon'); + if (!themeIcon) return; + + if (theme === CONSTANTS.THEME.DARK) { + // Moon icon for dark theme (to switch to light) + themeIcon.innerHTML = ` + + `; + } else { + // Sun icon for light theme (to switch to dark) + themeIcon.innerHTML = ` + + + `; + } + } + + /** + * Update logo icons in navigation based on theme + * @param {string} theme - Current theme + */ + updateFavicon(theme) { + // Get the base path from existing logo icon to maintain correct relative/absolute path + const existingLogoIcon = document.querySelector('.logo-icon'); + let assetsPath = '/assets/'; + + // If we have an existing logo icon, extract the base path from its src attribute + if (existingLogoIcon && existingLogoIcon.getAttribute('src')) { + const logoSrc = existingLogoIcon.getAttribute('src'); + // Extract the directory path (everything before the filename) + const pathMatch = logoSrc.match(/^(.+\/)favicon[^\/]*\.ico$/); + if (pathMatch) { + assetsPath = pathMatch[1]; + } + } + + // Update logo icons in navigation based on theme + const logoFaviconFile = theme === CONSTANTS.THEME.DARK + ? 'favicon_dark.ico' + : 'favicon_light.ico'; + const logoFaviconPath = assetsPath + logoFaviconFile; + const logoIcons = document.querySelectorAll('.logo-icon'); + logoIcons.forEach(icon => { + icon.src = logoFaviconPath; + }); + } +} + diff --git a/src/js/modules/utils.js b/src/js/modules/utils.js new file mode 100644 index 0000000..e8ccb74 --- /dev/null +++ b/src/js/modules/utils.js @@ -0,0 +1,57 @@ +/** + * Utility Functions Module + * Common helper functions used across the application + */ + +export const Utils = { + /** + * Debounce function + * @param {Function} func - Function to debounce + * @param {number} wait - Wait time in milliseconds + * @returns {Function} Debounced function + */ + debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + }, + + /** + * Throttle function + * @param {Function} func - Function to throttle + * @param {number} limit - Time limit in milliseconds + * @returns {Function} Throttled function + */ + throttle(func, limit) { + let inThrottle; + return function(...args) { + if (!inThrottle) { + func.apply(this, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; + }, + + /** + * Check if element is in viewport + * @param {HTMLElement} element - Element to check + * @returns {boolean} True if element is in viewport + */ + isInViewport(element) { + const rect = element.getBoundingClientRect(); + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); + } +}; + diff --git a/src/js/script.js b/src/js/script.js new file mode 100644 index 0000000..089e365 --- /dev/null +++ b/src/js/script.js @@ -0,0 +1,58 @@ +/** + * Main JavaScript file for Angry Data Scanner website + * + * This file now uses ES6 modules for better organization. + * All functionality has been split into separate modules in: + * - js/core/ - Core functionality (constants, app initialization) + * - js/modules/ - Feature modules (theme, scroll, language, etc.) + * + * For backward compatibility, this file imports and initializes the app. + */ + +/** + * Main entry point for the application + * + * This file imports and initializes the app. + * The app initialization happens automatically in app.js + * + * For backward compatibility, modules are also exported to global scope + */ + +// Import and initialize the application +// Note: CONFIG and I18N are loaded as regular scripts in HTML, available via window +import { app, init } from './core/app.js'; + +// Export modules to global scope for backward compatibility +// (in case any inline scripts or other code references them) +import { ThemeManager } from './modules/theme.js'; +import { ScrollManager } from './modules/scroll.js'; +import { AnimationManager } from './modules/animation.js'; +import { LightboxManager } from './modules/lightbox.js'; +import { TableEnhancement } from './modules/table.js'; +import { CountryFilter } from './modules/country-filter.js'; +import { DataRenderer } from './modules/data-renderer.js'; +import { LanguageManager } from './modules/language.js'; +import { Utils } from './modules/utils.js'; +import { CONSTANTS } from './core/constants.js'; +import { eventBus } from './core/event-bus.js'; +import { container } from './core/dependency-container.js'; + +// Make modules available globally for backward compatibility +if (typeof window !== 'undefined') { + // Export classes + window.ThemeManager = ThemeManager; + window.ScrollManager = ScrollManager; + window.AnimationManager = AnimationManager; + window.LightboxManager = LightboxManager; + window.TableEnhancement = TableEnhancement; + window.CountryFilter = CountryFilter; + window.DataRenderer = DataRenderer; + window.LanguageManager = LanguageManager; + window.Utils = Utils; + window.CONSTANTS = CONSTANTS; + + // Export app instance and utilities + window.app = app; + window.eventBus = eventBus; + window.container = container; +} diff --git a/src/ru/discovery.html b/src/ru/discovery.html new file mode 100644 index 0000000..c92e497 --- /dev/null +++ b/src/ru/discovery.html @@ -0,0 +1,955 @@ + + + + + + + + Поиск персональных данных | Angry Data Scanner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Поиск чувствительных данных

+

Angry Data Scanner может обнаруживать различные типы конфиденциальных данных в нескольких категориях

+
+ + +
+
+ + + + + Фильтр по стране +
+
+ + + + + +
+
+ + +
+

Поиск персональных данных (числа)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Тип данныхЛокальное названиеСтранаПример
Phone number-RU+7 926 3847291
Phone number-US+1 212 5550198
Taxpayer numberИННRU7707083893
Taxpayer numberSSNUS536-90-4399
Taxpayer numberRINCN110101199003078912
Passport-RU4505 857555
Passport-US847293641
Pension insurance numberСНИЛСRU234-567-890 12
Medical insurance numberОМСRU9876543210987654
Medical insurance numberMedicareUS1A2B3C4D5E
Car insurance numberполис ОСАГОRUААА3847291847
Driver licenseВодительские праваRU77АВ987654
Military IDУдостоверение личности военнослужащегоRU3847291847
Birthday--15.03.1985
VIN--1HGBH41JXMN109186
Employer Identification NumberEINUS12-3456789
Individual Taxpayer Identification NumberITINUS987-65-4321
Driver license-USD1234567
Visa number-USB12345678
Alien Registration NumberA-NumberUSA123456789
USCIS receipt numberUSCISUSEAC2190012345
SEVIS IDSEVISUSN0001234567
Department of Defense IDDOD IDUS1234567890
Military Mail AddressAPO/FPO/DPOUSFPO AP 96677-1234
National Stock NumberNSNUS5330-00-123-4567
Transportation Control NumberTCNUSTCN12345678901234567
National Provider IdentifierNPIUS1234567890
+
+ + +
+

Поиск персональных данных (текст)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Тип данныхЛокальное названиеСтранаПример
Full nameФИОRUИван Иванович Иванов
Full nameFull nameUSJohn Smith
E-mail--captainbull@gmail.com
AddressАдресRUМосква, ул. Ленина, д. 1
AddressAddressUS123 Main St CA 90210
Login--username
Password--password123
+
+ + +
+

Поиск данных PCI DSS

+ + + + + + + + + + + + + + + + + + + + +
Тип данныхПример
Payment card number4400 5678 9012 3456
CVV456
+
+ + +
+

Поиск банковской тайны

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Тип данныхСтранаПример
Bank account (Individual)RU408 028 103 3 5300 5405 83
Bank account (Legal entity)RU407 028 103 3 5300 5405 83
Routing Transit NumberUS123456789
+
+ + +
+

Поиск IT-активов

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Тип данныхПример
IPv4192.168.1.1
IPv62001:db8::1
Source code filesFinds files with source-code. Source code should be placed in git repository.
TLS certificatesFinds folders with the most amount of TLS certificates
Hash dataSHA-256, MD5, NTLM (NT hash), SHA-1, SHA-512
+
+ + +
+

Поиск криптовалюты

+ + + + + + + + + + + + + + + + + + + + +
Тип данныхПример
Crypto wallet1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
Crypto seed phraseA sequence of 12 to 24 words from the BIP39 standard wordlist, used for cryptographic wallet recovery and key derivation
+
+ + +
+

Поиск пользовательских сигнатур

+

+ Можно добавить пользовательские сигнатуры поиска данных, используя обычный текст: + + Secret, + + Password, + + Central bank + + или любой другой. +

+
+ + +
+

Поддерживаемые типы файлов

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Тип файлаФормат файла
MS Office (tables).xlsx .xls
MS Office (text).docx .doc
MS Office (presentation).pptx .potx .ppsx .pptm .ppt .pps .pot
Open Office (tables).ods
Open Office (text).odt
Open Office (presentation).odp .otp
Adobe.pdf
Archives.zip .rar
Plain text.txt .csv .xml .json .log
+
+ + +
+

Поддерживаемые источники данных

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
КоннекторОписание
Network FolderScans files on remote directory like Windows environment
HDD/SDDScan local hard drive
S3Scan files in S3
HTTP/HTTPSScans web site content
+
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/src/ru/download.html b/src/ru/download.html new file mode 100644 index 0000000..f44805a --- /dev/null +++ b/src/ru/download.html @@ -0,0 +1,532 @@ + + + + + + + + Скачать бесплатный сканер данных: Windows, Linux, macOS | Angry Data Scanner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Скачать

+

Системные требования: Windows, Linux, macOS | 400 МБ на жестком диске | 4 ГБ ОЗУ | процессор 1.3 ГГц

+
+ +
+ +
+
+ + + + + + + + +

Windows

+
+ +
+ +
+
+ + + + + +

Linux

+
+ +
+ +
+
+ + + + + +

macOS

+
+ +
+ +
+
+
+ + +
+
+
+

Начало работы

+

Краткое руководство, которое поможет вам начать использовать Angry Data Scanner

+
+ +
+
+
1
+

Скачать

+

Скачайте подходящую версию для вашей операционной системы по ссылкам выше.

+
+
+
2
+

Установить или распаковать

+

Запустите установщик (Windows/macOS) или распакуйте портативную версию. Права администратора не требуются.

+
+
+
3
+

Начать сканирование

+

Запустите приложение, выберите источник данных и начните сканирование. Результаты появятся мгновенно.

+
+
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/src/ru/features.html b/src/ru/features.html new file mode 100644 index 0000000..c222518 --- /dev/null +++ b/src/ru/features.html @@ -0,0 +1,488 @@ + + + + + + + + Возможности сканера: ранжирование и экспорт CSV | Angry Data Scanner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Ключевые возможности

+

Откройте для себя мощные возможности, которые делают Angry Data Scanner идеальным решением для обнаружения конфиденциальных данных

+
+ +
+ +
+
+ + + + + +
+

Ранжирование

+

Сканер показывает файлы с высокой ценностью первыми

+
+ +
+
+ + + + + + +
+

Просмотр истории сканирования

+

Отслеживайте все ваши предыдущие сканирования

+
+ +
+
+ + + + + + +
+

Экспорт результатов

+

Скачайте результаты в CSV файл

+
+ +
+
+ + + + + + +
+

Планирование сканирований

+

Автоматизируйте процесс сканирования

+
+ +
+
+ + + + + + +
+

Настраиваемые матчеры

+

Настройте PII, PCI DSS и другие матчеры

+
+ +
+
+ + + + + + +
+

Множество форматов файлов

+

Настройте форматы файлов (pdf, excel и т.д.)

+
+ +
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/src/ru/index.html b/src/ru/index.html new file mode 100644 index 0000000..77c27da --- /dev/null +++ b/src/ru/index.html @@ -0,0 +1,514 @@ + + + + + + + + Angry Data Scanner - Программа поиска конфиденциальных данных для Mac, Windows и Linux + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
Free Open Source
+

Бесплатная программа для быстрого поиска конфиденциальных данных

+

+ Бесплатный инструмент для автоматического поиска PII, PHI и интеллектуальной собственности с помощью расширенного сопоставления шаблонов. Выполняйте поиск данных в локальных папках, веб-страницах, AWS S3 и базах данных. +

+
+
+ + + + Интуитивный дизайн для скорости и удобства. +
+
+ + + + Обнаружение конфиденциальных данных за 2 клика. +
+
+ + + + Не требуются права администратора или установка. +
+
+ + + + Работает на Linux, macOS и Windows. +
+
+ + + + Все сканирование происходит локально. Данные не покидают ваш компьютер. +
+
+ +
+
+
+
+
+
+
+ Angry Data Scanner Interface +
+ + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + diff --git a/src/ru/use-cases.html b/src/ru/use-cases.html new file mode 100644 index 0000000..cb1c365 --- /dev/null +++ b/src/ru/use-cases.html @@ -0,0 +1,507 @@ + + + + + + + + Применение сканера: безопасность и соответствие | Angry Data Scanner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Реальные случаи использования

+

Узнайте, как организации из разных отраслей используют Angry Data Scanner для защиты конфиденциальных данных и обеспечения соответствия требованиям

+
+ +
+ + +
+ + + + + + + + +

Команда по поиску утечек сканирует сетевую папку и убеждается, что она не содержит исходный код

+
+ +
+ + + + + + + +

Сотрудник находит и удаляет файлы, содержащие номера карт, для соответствия PCI DSS

+
+ +
+ + + + + + +

Банковский сотрудник сканирует сетевую папку, чтобы убедиться, что она не содержит PII VIP-клиентов

+
+ +
+ + + + + + +

Руководитель сканирует общую папку отдела продаж, чтобы там не было контактов клиентов

+
+ +
+ + + + + + + + +

Правоохранительным органам нужно обнаружить следы криптовалюты на ноутбуке

+
+ +
+ + + + + + + + +

Специалист по кибербезопасности должен проверить, что база данных не содержит персональных данных

+
+ +
+
+
+ + +
+
+
+

Кому подходит Angry Data Scanner?

+
+ +
+
+

Команды безопасности

+

Проводите аудиты безопасности, выявляйте утечки данных и обеспечивайте надлежащую защиту конфиденциальной информации в вашей инфраструктуре.

+
+
+

Специалисты по соответствию

+

Обеспечивайте соответствие требованиям GDPR, PCI DSS, HIPAA и другим стандартам защиты данных.

+
+
+

Разработчики и DevOps

+

Сканируйте репозитории и инфраструктуру, чтобы предотвратить случайное раскрытие конфиденциальных данных в коде или конфигурациях.

+
+
+

Криминалистика и правоохранительные органы

+

Обнаруживайте следы конфиденциальных данных, криптовалюты и другие доказательства во время цифровых расследований.

+
+
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/src/use-cases.html b/src/use-cases.html new file mode 100644 index 0000000..c53843a --- /dev/null +++ b/src/use-cases.html @@ -0,0 +1,507 @@ + + + + + + + + Data Scanner Use Cases: Security & Compliance | Angry Data Scanner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Real life use cases

+

Discover how organizations across different industries use Angry Data Scanner to protect sensitive data and ensure compliance

+
+ +
+ + +
+ + + + + + + + +

A leak hunting team scans network folder and ensure that it does not contain source code

+
+ +
+ + + + + + + +

An employee finds and deletes files containing card numbers to comply with PCI DSS

+
+ +
+ + + + + + +

A banking employee scans network folder to ensure that it does not contain PII of VIP clients

+
+ +
+ + + + + + +

A boss scans a shared folder of the sales team so they don't have client contacts there

+
+ +
+ + + + + + + + +

Law enforcements need to discover a traces of cryptocurrency on a laptop

+
+ +
+ + + + + + + + +

A cybersecurity officer need to validate that the database does not contain a personal data

+
+ +
+
+
+ + +
+
+
+

Who Should Use Angry Data Scanner?

+
+ +
+
+

Security Teams

+

Conduct security audits, identify data leaks, and ensure sensitive information is properly protected across your infrastructure.

+
+
+

Compliance Officers

+

Ensure compliance with regulations like GDPR, PCI DSS, HIPAA, and other data protection standards.

+
+
+

Developers & DevOps

+

Scan repositories and infrastructure to prevent accidental exposure of sensitive data in code or configurations.

+
+
+

Forensics & Law Enforcement

+

Discover traces of sensitive data, cryptocurrency, and other evidence during digital investigations.

+
+
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/scripts/static/BingSiteAuth.xml b/static/BingSiteAuth.xml similarity index 100% rename from scripts/static/BingSiteAuth.xml rename to static/BingSiteAuth.xml diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..fef1656 Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/robots.txt b/static/robots.txt new file mode 100644 index 0000000..efae8a2 --- /dev/null +++ b/static/robots.txt @@ -0,0 +1,95 @@ +# robots.txt for angryscan.org +# Generated for Angry Data Scanner documentation site + +User-agent: * +Disallow: /.well-known/ +Disallow: /assets/ +Disallow: /js/ +Disallow: /css/ +Allow: / + +# Sitemap +Sitemap: https://angryscan.org/sitemap.xml + +# Allow common bots explicitly +User-agent: Googlebot +Disallow: /.well-known/ +Disallow: /assets/ +Disallow: /js/ +Disallow: /css/ +Allow: / + +User-agent: Bingbot +Disallow: /.well-known/ +Disallow: /assets/ +Disallow: /js/ +Disallow: /css/ +Allow: / + +User-agent: Yandex +Disallow: /.well-known/ +Disallow: /assets/ +Disallow: /js/ +Disallow: /css/ +Allow: / + +# Allow AI bots explicitly +User-agent: anthropic-ai +Disallow: /.well-known/ +Disallow: /assets/ +Disallow: /js/ +Disallow: /css/ +Allow: / + +User-agent: Claude-Web +Disallow: /.well-known/ +Disallow: /assets/ +Disallow: /js/ +Disallow: /css/ +Allow: / + +User-agent: Google-Extended +Disallow: /.well-known/ +Disallow: /assets/ +Disallow: /js/ +Disallow: /css/ +Allow: / + +User-agent: OAI-SearchBot +Disallow: /.well-known/ +Disallow: /assets/ +Disallow: /js/ +Disallow: /css/ +Allow: / + +User-agent: ChatGPT-User +Disallow: /.well-known/ +Disallow: /assets/ +Disallow: /js/ +Disallow: /css/ +Allow: / + +User-agent: GPTBot +Disallow: /.well-known/ +Disallow: /assets/ +Disallow: /js/ +Disallow: /css/ +Allow: / + +User-agent: PerplexityBot +Disallow: /.well-known/ +Disallow: /assets/ +Disallow: /js/ +Disallow: /css/ +Allow: / + +User-agent: Perplexity‑User +Disallow: /.well-known/ +Disallow: /assets/ +Disallow: /js/ +Disallow: /css/ +Allow: / + +# Crawl delay for polite crawling +Crawl-delay: 1 + diff --git a/static/site.webmanifest b/static/site.webmanifest new file mode 100644 index 0000000..c94575a --- /dev/null +++ b/static/site.webmanifest @@ -0,0 +1,32 @@ +{ + "name": "Angry Data Scanner", + "short_name": "AngryScan", + "description": "Free sensitive data discovery tool", + "icons": [ + { + "src": "/favicon.ico", + "sizes": "16x16", + "type": "image/x-icon" + }, + { + "src": "/favicon.ico", + "sizes": "32x32", + "type": "image/x-icon" + }, + { + "src": "/favicon.ico", + "sizes": "192x192", + "type": "image/x-icon" + }, + { + "src": "/favicon.ico", + "sizes": "512x512", + "type": "image/x-icon" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone", + "start_url": "/", + "scope": "/" +} diff --git a/static/sitemap.xml b/static/sitemap.xml new file mode 100644 index 0000000..4cfa04f --- /dev/null +++ b/static/sitemap.xml @@ -0,0 +1,227 @@ + + + + + + + https://angryscan.org/ + 2025-12-30 + daily + 1.0 + + + + https://angryscan.org/discovery/ + 2025-12-30 + daily + 0.8 + + + + https://angryscan.org/features/ + 2025-12-30 + daily + 0.8 + + + + https://angryscan.org/download/ + 2025-12-30 + daily + 0.9 + + + + https://angryscan.org/use-cases/ + 2025-12-30 + daily + 0.8 + + + + + https://angryscan.org/de/ + 2025-12-30 + daily + 0.9 + + + + + + https://angryscan.org/de/discovery/ + 2025-12-30 + daily + 0.8 + + + + + + https://angryscan.org/de/features/ + 2025-12-30 + daily + 0.8 + + + + + + https://angryscan.org/de/download/ + 2025-12-30 + daily + 0.9 + + + + + + https://angryscan.org/de/use-cases/ + 2025-12-30 + daily + 0.8 + + + + + + + https://angryscan.org/es/ + 2025-12-30 + daily + 0.9 + + + + + + https://angryscan.org/es/discovery/ + 2025-12-30 + daily + 0.8 + + + + + + https://angryscan.org/es/features/ + 2025-12-30 + daily + 0.8 + + + + + + https://angryscan.org/es/download/ + 2025-12-30 + daily + 0.9 + + + + + + https://angryscan.org/es/use-cases/ + 2025-12-30 + daily + 0.8 + + + + + + + https://angryscan.org/fr/ + 2025-12-30 + daily + 0.9 + + + + + + https://angryscan.org/fr/discovery/ + 2025-12-30 + daily + 0.8 + + + + + + https://angryscan.org/fr/features/ + 2025-12-30 + daily + 0.8 + + + + + + https://angryscan.org/fr/download/ + 2025-12-30 + daily + 0.9 + + + + + + https://angryscan.org/fr/use-cases/ + 2025-12-30 + daily + 0.8 + + + + + + + https://angryscan.org/ru/ + 2025-12-30 + daily + 0.9 + + + + + + https://angryscan.org/ru/discovery/ + 2025-12-30 + daily + 0.8 + + + + + + https://angryscan.org/ru/features/ + 2025-12-30 + daily + 0.8 + + + + + + https://angryscan.org/ru/download/ + 2025-12-30 + daily + 0.9 + + + + + + https://angryscan.org/ru/use-cases/ + 2025-12-30 + daily + 0.8 + + + + + + diff --git a/static/sitemap.xsl b/static/sitemap.xsl new file mode 100644 index 0000000..27c652b --- /dev/null +++ b/static/sitemap.xsl @@ -0,0 +1,234 @@ + + + + + + + XML Sitemap - Angry Data Scanner + + + + +

XML Sitemap

+
+

This sitemap contains URLs from the Angry Data Scanner website.

+

Last generated:

+
+
+
+ + Total URLs +
+
+ + High Priority +
+
+ + Multilingual Pages +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
URLLanguagePriorityChange FrequencyLast Modified
+ + + + + + + + + + RU + + + DE + + + ES + + + FR + + + EN + + + + + + + + + + + + + + +
+ + +
+
diff --git a/scripts/static_html/yandex_443bb767bb25244a.html b/static/yandex_443bb767bb25244a.html similarity index 100% rename from scripts/static_html/yandex_443bb767bb25244a.html rename to static/yandex_443bb767bb25244a.html diff --git a/templates/base/base.html b/templates/base/base.html new file mode 100644 index 0000000..b4d17cd --- /dev/null +++ b/templates/base/base.html @@ -0,0 +1,262 @@ + + + + + + + + {{ page_meta.title if page_meta else t('site.title') }} + + + + + + + + + + + {% for lcode, linfo in languages.items() %} + {% if lcode != lang %} + + {% endif %} + {% endfor %} + + + + + + + + + + + + + {% for lcode, linfo in languages.items() %} + + {% endfor %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% block structured_data %} + + {% endblock %} + + + + + + + + + + {% include 'components/navigation.html' %} + + {% block content %}{% endblock %} + + {% include 'components/footer.html' %} + + + + + + + + + + + + + + diff --git a/templates/components/footer.html b/templates/components/footer.html new file mode 100644 index 0000000..953aeab --- /dev/null +++ b/templates/components/footer.html @@ -0,0 +1,22 @@ + + + diff --git a/templates/components/navigation.html b/templates/components/navigation.html new file mode 100644 index 0000000..dd9010d --- /dev/null +++ b/templates/components/navigation.html @@ -0,0 +1,56 @@ + + + diff --git a/templates/pages/discovery.html b/templates/pages/discovery.html new file mode 100644 index 0000000..be12c91 --- /dev/null +++ b/templates/pages/discovery.html @@ -0,0 +1,251 @@ +{% extends "base/base.html" %} + +{% block content %} + + + + +
+
+
+

{{ t('sections.discovery.title_h1') }}

+

{{ t('sections.discovery.description') }}

+
+ + +
+
+ + + + + {{ t('sections.discovery.filter_by_country') }} +
+
+ + + + + +
+
+ + +
+

{{ t('categories.personal_data_numbers_title_h2') }}

+ + + + + + + + + + + {% for item in data.personal_data_numbers %} + + + + + + + {% endfor %} + +
{{ t('table_headers.data_type') }}{{ t('table_headers.local_name') }}{{ t('table_headers.country') }}{{ t('table_headers.example') }}
{{ item.type }}{{ item.local_name }}{{ '-' if item.country == '-' else item.country }}{{ item.example }}
+
+ + +
+

{{ t('categories.personal_data_text_title_h2') }}

+ + + + + + + + + + + {% for item in data.personal_data_text %} + + + + + + + {% endfor %} + +
{{ t('table_headers.data_type') }}{{ t('table_headers.local_name') }}{{ t('table_headers.country') }}{{ t('table_headers.example') }}
{{ item.type }}{{ item.local_name }}{{ '-' if item.country == '-' else item.country }}{{ item.example }}
+
+ + +
+

{{ t('categories.pci_dss_title_h2') }}

+ + + + + + + + + {% for item in data.pci_dss %} + + + + + {% endfor %} + +
{{ t('table_headers.data_type') }}{{ t('table_headers.example') }}
{{ item.type }}{{ item.example }}
+
+ + +
+

{{ t('categories.banking_secrecy_title_h2') }}

+ + + + + + + + + + {% for item in data.banking_secrecy %} + + + + + + {% endfor %} + +
{{ t('table_headers.data_type') }}{{ t('table_headers.country') }}{{ t('table_headers.example') }}
{{ item.type }}{{ item.country }}{{ item.example }}
+
+ + +
+

{{ t('categories.it_assets_title_h2') }}

+ + + + + + + + + {% for item in data.it_assets %} + + + + + {% endfor %} + +
{{ t('table_headers.data_type') }}{{ t('table_headers.example') }}
{{ item.type }}{% if item.example.startswith('Finds') or item.example.startswith('Находит') or 'SHA-256' in item.example %}{{ item.example }}{% else %}{{ item.example }}{% endif %}
+
+ + +
+

{{ t('categories.crypto_title_h2') }}

+ + + + + + + + + {% for item in data.crypto %} + + + + + {% endfor %} + +
{{ t('table_headers.data_type') }}{{ t('table_headers.example') }}
{{ item.type }}{{ item.example }}
+
+ + +
+

{{ t('categories.custom_signatures_title_h2') }}

+

+ {{ t('categories.custom_signatures_desc') }} + + Secret, + + Password, + + Central bank + + {{ t('categories.custom_signatures_or') }} +

+
+ + +
+

{{ t('sections.file_types.title_h2') }}

+ + + + + + + + + {% for item in data.file_types %} + + + + + {% endfor %} + +
{{ t('table_headers.file_type') }}{{ t('table_headers.file_format') }}
{{ item.category }}{{ item.formats }}
+
+ + +
+

{{ t('sections.data_sources.title_h2') }}

+ + + + + + + + + {% for item in data.data_sources %} + + + + + {% endfor %} + +
{{ t('table_headers.connector') }}{{ t('table_headers.description') }}
{{ item.connector }}{{ item.description }}
+
+
+
+{% endblock %} + diff --git a/templates/pages/download.html b/templates/pages/download.html new file mode 100644 index 0000000..667e84e --- /dev/null +++ b/templates/pages/download.html @@ -0,0 +1,88 @@ +{% extends "base/base.html" %} + +{% block content %} + + + + +
+
+
+

{{ t('sections.download.title_h1') }}

+

{{ t('sections.download.system_requirements') }}

+
+ +
+ {% for platform_name, platform_links in data.downloads.items() %} +
+
+ {% if platform_name == 'windows' %} + + + + + + + {% elif platform_name == 'linux' %} + + + + {% elif platform_name == 'macos' %} + + + + {% endif %} +

{% if platform_name == 'macos' %}macOS{% else %}{{ platform_name|title }}{% endif %}

+
+ +
+ {% endfor %} +
+
+
+ + +
+
+
+

{{ t('sections.download.getting_started_h2') }}

+

{{ t('sections.download.getting_started_desc') }}

+
+ +
+
+
1
+

{{ t('sections.download.step1.title_h3') }}

+

{{ t('sections.download.step1.description') }}

+
+
+
2
+

{{ t('sections.download.step2.title_h3') }}

+

{{ t('sections.download.step2.description') }}

+
+
+
3
+

{{ t('sections.download.step3.title_h3') }}

+

{{ t('sections.download.step3.description') }}

+
+
+
+
+{% endblock %} diff --git a/templates/pages/features.html b/templates/pages/features.html new file mode 100644 index 0000000..e79c175 --- /dev/null +++ b/templates/pages/features.html @@ -0,0 +1,64 @@ +{% extends "base/base.html" %} + +{% block content %} + + + + +
+
+
+

{{ t('sections.features.title_h1') }}

+

{{ t('sections.features.description') }}

+
+ +
+ {% for feature in data.features %} +
+
+ {% if feature.icon == 'check' %} + + + + {% elif feature.icon == 'history' %} + + + + + {% elif feature.icon == 'download' %} + + + + + {% elif feature.icon == 'clock' %} + + + + + {% elif feature.icon == 'settings' %} + + + + + {% elif feature.icon == 'file' %} + + + + + {% endif %} +
+

{{ t(feature.title_key) }}

+

{{ t(feature.description_key) }}

+
+ {% endfor %} +
+
+
+{% endblock %} diff --git a/templates/pages/index.html b/templates/pages/index.html new file mode 100644 index 0000000..fecd23a --- /dev/null +++ b/templates/pages/index.html @@ -0,0 +1,131 @@ +{% extends "base/base.html" %} + +{% block content %} + +
+
+
+
{{ t('hero.badge') }}
+

{{ t('hero.title_h1') }}

+

+ {{ t('hero.description') }} +

+
+
+ + + + {{ t('hero.features.simple') }} +
+
+ + + + {{ t('hero.features.two_clicks') }} +
+
+ + + + {{ t('hero.features.no_admin') }} +
+
+ + + + {{ t('hero.features.cross_platform') }} +
+
+ + + + {{ t('hero.features.privacy') }} +
+
+ +
+
+
+
+
+
+
+ Angry Data Scanner Interface +
+ + + +
+
+
+
+
+
+ + + +{% endblock %} + diff --git a/templates/pages/use-cases.html b/templates/pages/use-cases.html new file mode 100644 index 0000000..750efb0 --- /dev/null +++ b/templates/pages/use-cases.html @@ -0,0 +1,93 @@ +{% extends "base/base.html" %} + +{% block content %} + + + + +
+
+
+

{{ t('sections.use_cases.title_h1') }}

+

{{ t('sections.use_cases.description') }}

+
+ +
+ {% set use_cases_list = translations.get('sections', {}).get('use_cases', {}).get('cases', []) or data.get('use_cases', []) %} + {% for use_case in use_cases_list %} +
+ {% set icon_index = loop.index0 %} + {% if icon_index == 0 %} + + + + + + {% elif icon_index == 1 %} + + + + + {% elif icon_index == 2 %} + + + + {% elif icon_index == 3 %} + + + + {% elif icon_index == 4 %} + + + + + + {% else %} + + + + + + {% endif %} +

{{ use_case }}

+
+ {% endfor %} +
+
+
+ + +
+
+
+

{{ t('sections.use_cases.who_should_use_h2') }}

+
+ +
+
+

{{ t('sections.use_cases.security_teams_h3') }}

+

{{ t('sections.use_cases.security_teams_desc') }}

+
+
+

{{ t('sections.use_cases.compliance_officers_h3') }}

+

{{ t('sections.use_cases.compliance_officers_desc') }}

+
+
+

{{ t('sections.use_cases.developers_h3') }}

+

{{ t('sections.use_cases.developers_desc') }}

+
+
+

{{ t('sections.use_cases.forensics_h3') }}

+

{{ t('sections.use_cases.forensics_desc') }}

+
+
+
+
+{% endblock %}