From cc118eb008f50c32147e04a6922d90270f2f3f5e Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Sun, 25 Oct 2026 04:51:10 +0100 Subject: [PATCH 1/9] docs: add Spanish README and documentation language navigation --- README.md | 12 +++- README_de.md | 12 +++- README_es.md | 167 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 README_es.md diff --git a/README.md b/README.md index 28a5c74..5014227 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,20 @@ > [!NOTE] > **AI / Agent Context**: Machine-readable repo architecture, file indexes, search phrases, and system boundaries are available in [llms.txt](llms.txt). -**[English](README.md)** | [Deutsch](README_de.md) | [Privacy](PRIVACY_POLICY.md) | [Contributing](CONTRIBUTING.md) | [Changelog](CHANGELOG.md) +**[English](README.md)** | [Deutsch](README_de.md) | [Español](README_es.md) | [Privacy](PRIVACY_POLICY.md) | [Contributing](CONTRIBUTING.md) | [Changelog](CHANGELOG.md) ![MediaBrain main window](README/screenshots/main.png) +## Documentation languages + +| Language | Document | +|---|---| +| English | [README.md](README.md) | +| German | [README_de.md](README_de.md) | +| Spanish | [README_es.md](README_es.md) | + +This list describes the README documentation languages only. + ## Start Here | Need | Go to | diff --git a/README_de.md b/README_de.md index dff87df..0c7ec27 100644 --- a/README_de.md +++ b/README_de.md @@ -17,10 +17,20 @@ > [!NOTE] > **KI / Agenten-Kontext**: Maschinenlesbare Architektur, Dateiverzeichnisse, Suchphrasen und Systemgrenzen sind in [llms.txt](llms.txt) dokumentiert. -[English](README.md) | **[Deutsch](README_de.md)** | [Datenschutz](PRIVACY_POLICY.md) | [Mitwirken](CONTRIBUTING.md) | [Changelog](CHANGELOG.md) +[English](README.md) | **[Deutsch](README_de.md)** | [Español](README_es.md) | [Datenschutz](PRIVACY_POLICY.md) | [Mitwirken](CONTRIBUTING.md) | [Changelog](CHANGELOG.md) ![MediaBrain Hauptfenster](README/screenshots/main.png) +## Dokumentationssprachen + +| Sprache | Dokument | +|---|---| +| Englisch | [README.md](README.md) | +| Deutsch | [README_de.md](README_de.md) | +| Spanisch | [README_es.md](README_es.md) | + +Diese Liste beschreibt ausschließlich die Sprachfassungen der README-Dokumentation. + ## Schnellstart | Bedarf | Einstieg | diff --git a/README_es.md b/README_es.md new file mode 100644 index 0000000..cec1183 --- /dev/null +++ b/README_es.md @@ -0,0 +1,167 @@ +Banner de MediaBrain + +# MediaBrain + +[![Versión: 2.1.0](https://img.shields.io/badge/version-2.1.0-orange.svg)](version.py) +[![Licencia: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) +[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/) +[![Plataforma: Windows](https://img.shields.io/badge/platform-Windows-lightgrey.svg)]() +[![Prioridad sin conexión: sí](https://img.shields.io/badge/offline--first-yes-brightgreen.svg)]() +[![Pruebas: 274 aprobadas](https://img.shields.io/badge/tests-274%20passed-brightgreen.svg)]() +[![doc-bricks](https://img.shields.io/badge/doc--bricks-ecosystem-blue.svg)](https://github.com/doc-bricks) +[![open-bricks](https://img.shields.io/badge/open--bricks-umbrella-purple.svg)](https://github.com/open-bricks) +[![Contexto LLM](https://img.shields.io/badge/LLM--Context-llms.txt-blue.svg)](llms.txt) + +> **MediaBrain es un gestor de bibliotecas multimedia basado en PySide6 que prioriza el almacenamiento local y permite organizar colecciones personales de vídeo y música, listas de reproducción e historial de visualización en servicios de streaming.** + +> [!NOTE] +> **Contexto para IA y agentes**: La arquitectura del repositorio, los índices de archivos, las frases de búsqueda y los límites del sistema están documentados en [llms.txt](llms.txt). + +[English](README.md) | [Deutsch](README_de.md) | **Español** | [Privacidad](PRIVACY_POLICY.md) | [Contribuir](CONTRIBUTING.md) | [Registro de cambios](CHANGELOG.md) + +![Ventana principal de MediaBrain](README/screenshots/main.png) + +## Idiomas de la documentación + +| Idioma | Documento | +|---|---| +| Inglés | [README.md](README.md) | +| Alemán | [README_de.md](README_de.md) | +| Español | [README_es.md](README_es.md) | + +Esta lista se refiere únicamente a las versiones documentales del README. + +## Primeros pasos + +| Necesidad | Ir a | +|---|---| +| Ejecutar la aplicación de escritorio | `python MediaBrain.py` | +| Consultar la versión móvil de Flutter | `flutter_port/README.md` | +| Revisar los límites de privacidad | [`PRIVACY_POLICY.md`](PRIVACY_POLICY.md) | +| Entender la arquitectura | [`ARCH.md`](ARCH.md) | +| Consultar el trabajo planificado | [`ROADMAP.md`](ROADMAP.md) | +| Obtener el contexto legible por máquinas | [`llms.txt`](llms.txt) | + +## Por qué MediaBrain + +MediaBrain está pensado para quienes quieren tener en un mismo lugar local los medios que ven, recopilan, etiquetan, bloquean u organizan entre servicios y archivos locales. No es un servicio de streaming alojado y no sube una biblioteca privada a una cuenta en la nube. + +- Almacenamiento SQLite local para entradas multimedia, favoritos, etiquetas, elementos de la lista de bloqueo y listas de reproducción +- Interfaz de escritorio PySide6 con panel, bibliotecas, búsqueda, estadísticas, ajustes y temas de alto contraste +- Capa de proveedores para Netflix, YouTube, Spotify, Disney+, Prime Video, Apple TV+, Twitch y archivos locales +- Listas de reproducción manuales e inteligentes basadas en reglas de QueryBuilder +- Consulta opcional de metadatos mediante claves de API TMDb/OMDb configuradas por el usuario o datos públicos de MusicBrainz +- Sin telemetría, seguimiento ni sincronización automática con la nube + +## Contexto de búsqueda + +Este repositorio es el hogar canónico en GitHub para: + +- `doc-bricks MediaBrain` +- `MediaBrain media library manager` +- `private SQLite media library manager` +- `local-first PySide6 media manager` +- `PySide6 smart playlist media manager` +- `offline media hub with smart playlists` +- `desktop media history tracker` +- `privacy-first media history desktop app` + +El nombre “MediaBrain” coincide con empresas de análisis de medios, laboratorios de IA aplicada a medios y herramientas genéricas de planificación de medios. Este repositorio corresponde específicamente al gestor local de bibliotecas multimedia de escritorio `doc-bricks/MediaBrain`; no es una plataforma de analítica publicitaria en la nube, un laboratorio de investigación multimedia ni un generador de medios con IA. + +## Arquitectura + +```text +Core Layer Database, MediaManager, BlacklistManager, TagManager, PlaylistManager +Query Layer QueryBuilder for advanced filters and smart playlists +Provider Layer Netflix, YouTube, Spotify, Disney+, Prime, AppleTV+, Twitch, Local +Background Layer FileIndexer, WindowWatcher +GUI Layer Dashboard, Libraries, Favorites, Blacklist, Playlists, Search, Stats, Settings +``` + +Diagrama completo: [`ARCH.md`](ARCH.md) + +## Ecosistema y herramientas relacionadas + +MediaBrain forma parte del conjunto de productividad y documentación de [doc-bricks](https://github.com/doc-bricks), bajo el paraguas de [open-bricks](https://github.com/open-bricks): + +| Herramienta | Enfoque | Repositorio | +|---|---|---| +| **MediaBrain** | Historial multimedia local, listas inteligentes y gestión de bibliotecas | [doc-bricks/MediaBrain](https://github.com/doc-bricks/MediaBrain) | +| **CleanMarkdown** | Normalizador tipográfico, limpiador de Markdown y exportador de PDF | [doc-bricks/CleanMarkdown](https://github.com/doc-bricks/CleanMarkdown) | +| **PDFtoPDFocr** | Flujo de OCR para PDF con búsqueda de texto y extracción de metadatos | [doc-bricks/PDFtoPDFocr](https://github.com/doc-bricks/PDFtoPDFocr) | +| **llm-note** | Cuaderno local SQLite y de texto plano para notas personales o escritas por agentes | [doc-bricks/llm-note](https://github.com/doc-bricks/llm-note) | +| **LaunchBoards** | Lanzador visual, organizador de aplicaciones y panel de trabajo | [file-bricks/LaunchBoards](https://github.com/file-bricks/LaunchBoards) | +| **ProFiler** | Analizador de archivos centrado en la privacidad, inspector y buscador de duplicados | [file-bricks/ProFiler](https://github.com/file-bricks/ProFiler) | +| **KnowledgeDigest** | Base de conocimiento sin conexión y compresor de documentación | [file-bricks/KnowledgeDigest](https://github.com/file-bricks/KnowledgeDigest) | +| **WinStorePackager** | Empaquetado MSIX, preparación de manifiestos de Store y generador de recursos | [file-bricks/WinStorePackager](https://github.com/file-bricks/WinStorePackager) | +| **DevCenter** | Panel para varios repositorios, monitor de CI y gestor de espacios de trabajo | [dev-bricks/DevCenter](https://github.com/dev-bricks/DevCenter) | +| **open-bricks** | Portal general para aplicaciones de escritorio y herramientas CLI | [open-bricks/open-bricks](https://github.com/open-bricks/open-bricks) | + +## Instalación + +Requisitos: + +- Python 3.10 o posterior +- Windows, Linux o macOS con compatibilidad con Qt/PySide6 + +```bash +pip install -r requirements.txt +``` + +Al iniciarse por primera vez, MediaBrain crea `settings.json` localmente. La plantilla pública es [`settings.example.json`](settings.example.json). La configuración personal, las bases de datos, los registros y los artefactos de compilación se excluyen mediante `.gitignore`. + +## Uso + +```bash +python MediaBrain.py +``` + +Acceso de solo lectura a la línea de comandos (nunca escribe en la base de datos): + +```bash +python cli.py list +python cli.py search "title" +python cli.py --db /path/to/media_brain.db types +``` + +La CLI oculta las entradas incluidas en la lista de bloqueo. Establece `MEDIABRAIN_DB` o usa `--db` para seleccionar explícitamente una base de datos. + +Lanzador de Windows con doble clic: + +```bat +START.bat +``` + +Para crear un lanzador ligero de Windows: + +```bat +build_exe.bat +``` + +## Pruebas + +```bash +python -m pytest tests/ -q +``` + +La suite de pruebas cubre los gestores de bases de datos, metadatos, etiquetas, QueryBuilder, listas de reproducción e interfaz de listas de reproducción en modo offscreen. + +La prueba específica de plataforma de origen comprueba rutas Unicode, configuración, intercambio JSON, el abridor de archivos del sistema operativo y el ciclo de vida de la ventana y la bandeja en modo offscreen: + +```bash +python -m pytest tests/source_platform_smoke.py -q +``` + +GitHub Actions ejecuta esta prueba acotada en Ubuntu y macOS. Es un contrato para el código fuente y la interfaz gráfica en modo offscreen; no demuestra el empaquetado nativo, el funcionamiento de la bandeja en una sesión de escritorio ni la aceptación manual en ninguna de las dos plataformas. + +## Privacidad + +MediaBrain guarda los datos de uso localmente en bases de datos SQLite y archivos de configuración. No tiene telemetría ni sincronización automática con la nube. Las solicitudes opcionales de metadatos usan únicamente claves de API TMDb/OMDb configuradas por el usuario o datos públicos de MusicBrainz. Detalles: [`PRIVACY_POLICY.md`](PRIVACY_POLICY.md). + +## Licencia + +MIT; consulta [`LICENSE`](LICENSE). Las licencias de las dependencias directas de ejecución y desarrollo están inventariadas en [`THIRD_PARTY_LICENSES.txt`](THIRD_PARTY_LICENSES.txt). La interfaz usa PySide6 bajo los términos de LGPL. + +## Responsabilidad + +Este proyecto se ofrece gratuitamente y “tal cual”, sin garantía, compromiso de mantenimiento ni garantía de idoneidad para un propósito determinado. Úsalo bajo tu propia responsabilidad. From c581a4f02d71424202b93adbc99d1d5dabcec8b9 Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Sun, 25 Oct 2026 04:55:58 +0100 Subject: [PATCH 2/9] docs: add Chinese Japanese and Russian README translations --- README.md | 5 +- README_de.md | 5 +- README_es.md | 5 +- README_ja.md | 170 +++++++++++++++++++++++++++++++++++++++++++++++++++ README_ru.md | 170 +++++++++++++++++++++++++++++++++++++++++++++++++++ README_zh.md | 170 +++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 522 insertions(+), 3 deletions(-) create mode 100644 README_ja.md create mode 100644 README_ru.md create mode 100644 README_zh.md diff --git a/README.md b/README.md index 5014227..5b2b474 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ > [!NOTE] > **AI / Agent Context**: Machine-readable repo architecture, file indexes, search phrases, and system boundaries are available in [llms.txt](llms.txt). -**[English](README.md)** | [Deutsch](README_de.md) | [Español](README_es.md) | [Privacy](PRIVACY_POLICY.md) | [Contributing](CONTRIBUTING.md) | [Changelog](CHANGELOG.md) +**[English](README.md)** | [Deutsch](README_de.md) | [Español](README_es.md) | [中文](README_zh.md) | [日本語](README_ja.md) | [Русский](README_ru.md) | [Privacy](PRIVACY_POLICY.md) | [Contributing](CONTRIBUTING.md) | [Changelog](CHANGELOG.md) ![MediaBrain main window](README/screenshots/main.png) @@ -28,6 +28,9 @@ | English | [README.md](README.md) | | German | [README_de.md](README_de.md) | | Spanish | [README_es.md](README_es.md) | +| Chinese (Simplified) | [README_zh.md](README_zh.md) | +| Japanese | [README_ja.md](README_ja.md) | +| Russian | [README_ru.md](README_ru.md) | This list describes the README documentation languages only. diff --git a/README_de.md b/README_de.md index 0c7ec27..1d13ff4 100644 --- a/README_de.md +++ b/README_de.md @@ -17,7 +17,7 @@ > [!NOTE] > **KI / Agenten-Kontext**: Maschinenlesbare Architektur, Dateiverzeichnisse, Suchphrasen und Systemgrenzen sind in [llms.txt](llms.txt) dokumentiert. -[English](README.md) | **[Deutsch](README_de.md)** | [Español](README_es.md) | [Datenschutz](PRIVACY_POLICY.md) | [Mitwirken](CONTRIBUTING.md) | [Changelog](CHANGELOG.md) +[English](README.md) | **[Deutsch](README_de.md)** | [Español](README_es.md) | [中文](README_zh.md) | [日本語](README_ja.md) | [Русский](README_ru.md) | [Datenschutz](PRIVACY_POLICY.md) | [Mitwirken](CONTRIBUTING.md) | [Changelog](CHANGELOG.md) ![MediaBrain Hauptfenster](README/screenshots/main.png) @@ -28,6 +28,9 @@ | Englisch | [README.md](README.md) | | Deutsch | [README_de.md](README_de.md) | | Spanisch | [README_es.md](README_es.md) | +| Chinesisch (vereinfacht) | [README_zh.md](README_zh.md) | +| Japanisch | [README_ja.md](README_ja.md) | +| Russisch | [README_ru.md](README_ru.md) | Diese Liste beschreibt ausschließlich die Sprachfassungen der README-Dokumentation. diff --git a/README_es.md b/README_es.md index cec1183..78e731a 100644 --- a/README_es.md +++ b/README_es.md @@ -17,7 +17,7 @@ > [!NOTE] > **Contexto para IA y agentes**: La arquitectura del repositorio, los índices de archivos, las frases de búsqueda y los límites del sistema están documentados en [llms.txt](llms.txt). -[English](README.md) | [Deutsch](README_de.md) | **Español** | [Privacidad](PRIVACY_POLICY.md) | [Contribuir](CONTRIBUTING.md) | [Registro de cambios](CHANGELOG.md) +[English](README.md) | [Deutsch](README_de.md) | **Español** | [中文](README_zh.md) | [日本語](README_ja.md) | [Русский](README_ru.md) | [Privacidad](PRIVACY_POLICY.md) | [Contribuir](CONTRIBUTING.md) | [Registro de cambios](CHANGELOG.md) ![Ventana principal de MediaBrain](README/screenshots/main.png) @@ -28,6 +28,9 @@ | Inglés | [README.md](README.md) | | Alemán | [README_de.md](README_de.md) | | Español | [README_es.md](README_es.md) | +| Chino simplificado | [README_zh.md](README_zh.md) | +| Japonés | [README_ja.md](README_ja.md) | +| Ruso | [README_ru.md](README_ru.md) | Esta lista se refiere únicamente a las versiones documentales del README. diff --git a/README_ja.md b/README_ja.md new file mode 100644 index 0000000..a1da0e5 --- /dev/null +++ b/README_ja.md @@ -0,0 +1,170 @@ +MediaBrain バナー + +# MediaBrain + +[![バージョン: 2.1.0](https://img.shields.io/badge/version-2.1.0-orange.svg)](version.py) +[![ライセンス: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) +[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/) +[![プラットフォーム: Windows](https://img.shields.io/badge/platform-Windows-lightgrey.svg)]() +[![オフライン優先: はい](https://img.shields.io/badge/offline--first-yes-brightgreen.svg)]() +[![テスト: 274 件成功](https://img.shields.io/badge/tests-274%20passed-brightgreen.svg)]() +[![doc-bricks](https://img.shields.io/badge/doc--bricks-ecosystem-blue.svg)](https://github.com/doc-bricks) +[![open-bricks](https://img.shields.io/badge/open--bricks-umbrella-purple.svg)](https://github.com/open-bricks) +[![LLM コンテキスト](https://img.shields.io/badge/LLM--Context-llms.txt-blue.svg)](llms.txt) + +> **MediaBrain は、個人の動画・音楽、プレイリスト、ストリーミング視聴履歴を管理する、PySide6 製のローカル優先メディアライブラリ管理ツールです。** + +> [!NOTE] +> **AI / エージェント向けコンテキスト**: リポジトリのアーキテクチャ、ファイル索引、検索フレーズ、システム境界の機械可読情報は [llms.txt](llms.txt) を参照してください。 + +[English](README.md) | [Deutsch](README_de.md) | **日本語** | [プライバシー](PRIVACY_POLICY.md) | [コントリビューション](CONTRIBUTING.md) | [変更履歴](CHANGELOG.md) + +![MediaBrain のメインウィンドウ](README/screenshots/main.png) + +## ドキュメントの言語 + +| 言語 | ドキュメント | +|---|---| +| English | [README.md](README.md) | +| Deutsch | [README_de.md](README_de.md) | +| Español | [README_es.md](README_es.md) | +| 简体中文 | [README_zh.md](README_zh.md) | +| 日本語 | [README_ja.md](README_ja.md) | +| Русский | [README_ru.md](README_ru.md) | + +この一覧は README の文書版のみを示します。アプリの実行時の言語対応を示すものではありません。 + +## まずはこちら + +| 内容 | 移動先 | +|---|---| +| デスクトップアプリを起動する | `python MediaBrain.py` | +| Flutter モバイル版を確認する | `flutter_port/README.md` | +| プライバシーの境界を確認する | [PRIVACY_POLICY.md](PRIVACY_POLICY.md) | +| アーキテクチャを確認する | [ARCH.md](ARCH.md) | +| 計画中の作業を確認する | [ROADMAP.md](ROADMAP.md) | +| 機械可読コンテキストを取得する | [llms.txt](llms.txt) | + +## MediaBrain を選ぶ理由 + +MediaBrain は、各種サービスやローカルファイルにある、視聴、収集、タグ付け、ブラックリスト登録、整理したメディアを一か所で管理したい人向けです。ホスト型ストリーミングサービスではなく、個人のライブラリをクラウドアカウントへアップロードすることもありません。 + +- メディア項目、お気に入り、タグ、ブラックリスト項目、プレイリストをローカルの SQLite に保存 +- ダッシュボード、ライブラリ、検索、統計、設定、高コントラストテーマを備えた PySide6 デスクトップ UI +- Netflix、YouTube、Spotify、Disney+、Prime Video、Apple TV+、Twitch、ローカルファイル向けのプロバイダー層 +- 手動プレイリストと、QueryBuilder のルールに基づくスマートプレイリスト +- ユーザーが設定した TMDb/OMDb キーまたは MusicBrainz の公開データを使った、任意のメタデータ検索 +- テレメトリー、トラッキング、自動クラウド同期なし + +## 検索コンテキスト + +このリポジトリは、次の名称や検索フレーズに対応する GitHub 上の正式な場所です。 + +- `doc-bricks MediaBrain` +- `MediaBrain media library manager` +- `private SQLite media library manager` +- `local-first PySide6 media manager` +- `PySide6 smart playlist media manager` +- `offline media hub with smart playlists` +- `desktop media history tracker` +- `privacy-first media history desktop app` + +「MediaBrain」という名前は、メディア分析企業、AI メディア研究所、汎用のメディア企画ツールでも使われています。このリポジトリが扱うのは、ローカル優先のデスクトップメディアライブラリ `doc-bricks/MediaBrain` です。クラウド広告分析プラットフォーム、マルチメディア研究所、AI メディア生成ツールではありません。 + +## アーキテクチャ + +```text +Core Layer Database, MediaManager, BlacklistManager, TagManager, PlaylistManager +Query Layer QueryBuilder for advanced filters and smart playlists +Provider Layer Netflix, YouTube, Spotify, Disney+, Prime, AppleTV+, Twitch, Local +Background Layer FileIndexer, WindowWatcher +GUI Layer Dashboard, Libraries, Favorites, Blacklist, Playlists, Search, Stats, Settings +``` + +全体図: [ARCH.md](ARCH.md) + +## エコシステムと関連ツール + +MediaBrain は [`doc-bricks`](https://github.com/doc-bricks) のドキュメント・生産性ツール群の一部であり、[`open-bricks`](https://github.com/open-bricks) の傘下にあります。 + +| ツール | 主な用途 | リポジトリ | +|---|---|---| +| **MediaBrain** | ローカル優先のメディア履歴、スマートプレイリスト、ライブラリ管理 | [doc-bricks/MediaBrain](https://github.com/doc-bricks/MediaBrain) | +| **CleanMarkdown** | タイポグラフィの正規化、Markdown の整理、PDF エクスポート | [doc-bricks/CleanMarkdown](https://github.com/doc-bricks/CleanMarkdown) | +| **PDFtoPDFocr** | 検索可能な PDF を作る OCR パイプラインとメタデータ抽出 | [doc-bricks/PDFtoPDFocr](https://github.com/doc-bricks/PDFtoPDFocr) | +| **llm-note** | 個人またはエージェントが作成したメモ用のローカル SQLite・プレーンテキストノート | [doc-bricks/llm-note](https://github.com/doc-bricks/llm-note) | +| **LaunchBoards** | ビジュアルランチャー、アプリ整理、ワークステーション用ダッシュボード | [file-bricks/LaunchBoards](https://github.com/file-bricks/LaunchBoards) | +| **ProFiler** | プライバシーを重視したファイル分析・検査・重複検出ツール | [file-bricks/ProFiler](https://github.com/file-bricks/ProFiler) | +| **KnowledgeDigest** | オフライン知識ベースとドキュメント圧縮ツール | [file-bricks/KnowledgeDigest](https://github.com/file-bricks/KnowledgeDigest) | +| **WinStorePackager** | MSIX パッケージング、Store マニフェストの準備、アセット生成 | [file-bricks/WinStorePackager](https://github.com/file-bricks/WinStorePackager) | +| **DevCenter** | 複数リポジトリ向け開発者ダッシュボード、CI モニター、ワークスペース管理 | [dev-bricks/DevCenter](https://github.com/dev-bricks/DevCenter) | +| **open-bricks** | デスクトップアプリと CLI ツールをまとめる統合ポータル | [open-bricks/open-bricks](https://github.com/open-bricks/open-bricks) | + +## インストール + +必要条件: + +- Python 3.10 以降 +- Qt/PySide6 に対応した Windows、Linux、macOS + +```bash +pip install -r requirements.txt +``` + +初回起動時に MediaBrain は `settings.json` をローカルに作成します。公開テンプレートは [settings.example.json](settings.example.json) です。個人設定、データベース、ログ、ビルド成果物は `.gitignore` により除外されます。 + +## 使い方 + +```bash +python MediaBrain.py +``` + +読み取り専用のコマンドラインアクセス(データベースには書き込みません): + +```bash +python cli.py list +python cli.py search "title" +python cli.py --db /path/to/media_brain.db types +``` + +CLI はブラックリスト登録済みの項目を表示しません。`MEDIABRAIN_DB` を設定するか、`--db` を使ってデータベースを明示的に選択してください。 + +Windows のダブルクリック用ランチャー: + +```bat +START.bat +``` + +軽量な Windows ランチャーは次のコマンドでビルドできます: + +```bat +build_exe.bat +``` + +## テスト + +```bash +python -m pytest tests/ -q +``` + +テストスイートでは、データベースマネージャー、メタデータ、タグ、QueryBuilder、プレイリスト、および offscreen モードでのプレイリスト GUI の動作を確認します。 + +専用のソースプラットフォーム用スモークテストでは、Unicode パス、設定、JSON 交換、OS のファイルオープナー、offscreen ウィンドウとトレイのライフサイクルを確認します: + +```bash +python -m pytest tests/source_platform_smoke.py -q +``` + +GitHub Actions は、この範囲を絞ったスモークテストを Ubuntu と macOS で実行します。これはソースと offscreen GUI の契約を確認するものであり、ネイティブパッケージ、デスクトップセッションでのトレイ、いずれかのプラットフォームでの手動受け入れを証明するものではありません。 + +## プライバシー + +MediaBrain は利用データをローカルの SQLite データベースと設定ファイルに保存します。テレメトリーはなく、自動クラウド同期もありません。任意のメタデータ要求には、ユーザーが設定した TMDb/OMDb API キーまたは MusicBrainz の公開データのみを使います。詳細: [PRIVACY_POLICY.md](PRIVACY_POLICY.md)。 + +## ライセンス + +MIT ライセンスです。詳細は [LICENSE](LICENSE) を参照してください。直接依存するサードパーティ製ランタイム・開発依存関係のライセンスは [THIRD_PARTY_LICENSES.txt](THIRD_PARTY_LICENSES.txt) に記載されています。GUI が使用する PySide6 には LGPL の条件が適用されます。 + +## 免責事項 + +本プロジェクトは無料で「現状有姿」で提供され、保証、保守の保証、または特定目的への適合性の保証はありません。ご自身の責任でご利用ください。 diff --git a/README_ru.md b/README_ru.md new file mode 100644 index 0000000..c982bcb --- /dev/null +++ b/README_ru.md @@ -0,0 +1,170 @@ +Баннер MediaBrain + +# MediaBrain + +[![Версия: 2.1.0](https://img.shields.io/badge/version-2.1.0-orange.svg)](version.py) +[![Лицензия: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) +[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/) +[![Платформа: Windows](https://img.shields.io/badge/platform-Windows-lightgrey.svg)]() +[![Работа без сети в приоритете: да](https://img.shields.io/badge/offline--first-yes-brightgreen.svg)]() +[![Тесты: 274 пройдены](https://img.shields.io/badge/tests-274%20passed-brightgreen.svg)]() +[![doc-bricks](https://img.shields.io/badge/doc--bricks-ecosystem-blue.svg)](https://github.com/doc-bricks) +[![open-bricks](https://img.shields.io/badge/open--bricks-umbrella-purple.svg)](https://github.com/open-bricks) +[![Контекст для LLM](https://img.shields.io/badge/LLM--Context-llms.txt-blue.svg)](llms.txt) + +> **MediaBrain — менеджер медиатеки на PySide6, который в первую очередь хранит данные локально и помогает управлять личными видео, музыкой, плейлистами и историей просмотров в стриминговых сервисах.** + +> [!NOTE] +> **Контекст для ИИ и агентов**: машиночитаемая архитектура репозитория, индексы файлов, поисковые фразы и границы системы описаны в [llms.txt](llms.txt). + +[English](README.md) | [Deutsch](README_de.md) | **Русский** | [Конфиденциальность](PRIVACY_POLICY.md) | [Участие в проекте](CONTRIBUTING.md) | [История изменений](CHANGELOG.md) + +![Главное окно MediaBrain](README/screenshots/main.png) + +## Языки документации + +| Язык | Документ | +|---|---| +| English | [README.md](README.md) | +| Deutsch | [README_de.md](README_de.md) | +| Español | [README_es.md](README_es.md) | +| 简体中文 | [README_zh.md](README_zh.md) | +| 日本語 | [README_ja.md](README_ja.md) | +| Русский | [README_ru.md](README_ru.md) | + +В таблице перечислены только языковые версии документации README. Она не описывает поддержку языков в приложении во время выполнения. + +## Начните здесь + +| Что нужно | Перейти к | +|---|---| +| Запустить настольное приложение | `python MediaBrain.py` | +| Посмотреть мобильную версию на Flutter | `flutter_port/README.md` | +| Узнать об ограничениях приватности | [PRIVACY_POLICY.md](PRIVACY_POLICY.md) | +| Разобраться в архитектуре | [ARCH.md](ARCH.md) | +| Посмотреть запланированные работы | [ROADMAP.md](ROADMAP.md) | +| Получить машиночитаемый контекст | [llms.txt](llms.txt) | + +## Для чего нужен MediaBrain + +MediaBrain предназначен для тех, кто хочет хранить в одном локальном месте медиа, которые смотрит, собирает, помечает тегами, заносит в чёрный список или организует по разным сервисам и локальным файлам. Это не стриминговый сервис, который размещает медиаконтент для пользователей, и приложение не загружает личную медиатеку в облачную учётную запись. + +- Локальная база SQLite для медиазаписей, избранного, тегов, элементов чёрного списка и плейлистов +- Настольный интерфейс PySide6: панель, библиотеки, поиск, статистика, настройки и темы с высокой контрастностью +- Слой провайдеров для Netflix, YouTube, Spotify, Disney+, Prime Video, Apple TV+, Twitch и локальных файлов +- Обычные плейлисты и умные плейлисты на основе правил QueryBuilder +- Необязательный поиск метаданных по настроенным пользователем ключам TMDb/OMDb или общедоступным данным MusicBrainz +- Нет телеметрии, слежения и автоматической синхронизации с облаком + +## Поисковый контекст + +Этот репозиторий — каноническая страница GitHub для: + +- `doc-bricks MediaBrain` +- `MediaBrain media library manager` +- `private SQLite media library manager` +- `local-first PySide6 media manager` +- `PySide6 smart playlist media manager` +- `offline media hub with smart playlists` +- `desktop media history tracker` +- `privacy-first media history desktop app` + +Название «MediaBrain» совпадает с названиями компаний по аналитике медиа, лабораторий ИИ в области медиа и общих инструментов медиапланирования. Этот репозиторий посвящён именно локальному настольному менеджеру медиатеки `doc-bricks/MediaBrain`, а не облачной платформе рекламной аналитики, лаборатории мультимедийных исследований или генератору медиа на основе ИИ. + +## Архитектура + +```text +Core Layer Database, MediaManager, BlacklistManager, TagManager, PlaylistManager +Query Layer QueryBuilder for advanced filters and smart playlists +Provider Layer Netflix, YouTube, Spotify, Disney+, Prime, AppleTV+, Twitch, Local +Background Layer FileIndexer, WindowWatcher +GUI Layer Dashboard, Libraries, Favorites, Blacklist, Playlists, Search, Stats, Settings +``` + +Полная схема: [ARCH.md](ARCH.md) + +## Экосистема и смежные инструменты + +MediaBrain входит в набор инструментов для документов и продуктивности [`doc-bricks`](https://github.com/doc-bricks), который развивается под зонтичным брендом [`open-bricks`](https://github.com/open-bricks): + +| Инструмент | Назначение | Репозиторий | +|---|---|---| +| **MediaBrain** | Локальная история медиа, умные плейлисты и управление медиатекой | [doc-bricks/MediaBrain](https://github.com/doc-bricks/MediaBrain) | +| **CleanMarkdown** | Нормализация типографики, очистка Markdown и экспорт в PDF | [doc-bricks/CleanMarkdown](https://github.com/doc-bricks/CleanMarkdown) | +| **PDFtoPDFocr** | OCR-процесс для поиска текста в PDF и извлечения метаданных | [doc-bricks/PDFtoPDFocr](https://github.com/doc-bricks/PDFtoPDFocr) | +| **llm-note** | Локальный блокнот на SQLite и обычных текстовых файлах для личных заметок или заметок агентов | [doc-bricks/llm-note](https://github.com/doc-bricks/llm-note) | +| **LaunchBoards** | Визуальный лаунчер, органайзер приложений и рабочая панель | [file-bricks/LaunchBoards](https://github.com/file-bricks/LaunchBoards) | +| **ProFiler** | Анализатор и инспектор файлов с акцентом на приватность и поиском дубликатов | [file-bricks/ProFiler](https://github.com/file-bricks/ProFiler) | +| **KnowledgeDigest** | Офлайн-база знаний и средство сжатия документации | [file-bricks/KnowledgeDigest](https://github.com/file-bricks/KnowledgeDigest) | +| **WinStorePackager** | Упаковка MSIX, подготовка манифестов Store и создание ресурсов | [file-bricks/WinStorePackager](https://github.com/file-bricks/WinStorePackager) | +| **DevCenter** | Панель разработчика для нескольких репозиториев, монитор CI и менеджер рабочих пространств | [dev-bricks/DevCenter](https://github.com/dev-bricks/DevCenter) | +| **open-bricks** | Единый портал для настольных приложений и инструментов CLI | [open-bricks/open-bricks](https://github.com/open-bricks/open-bricks) | + +## Установка + +Требования: + +- Python 3.10 или новее +- Windows, Linux или macOS с поддержкой Qt/PySide6 + +```bash +pip install -r requirements.txt +``` + +При первом запуске MediaBrain локально создаёт файл `settings.json`. Публичный шаблон — [settings.example.json](settings.example.json). Личные настройки, базы данных, журналы и артефакты сборки исключены через `.gitignore`. + +## Использование + +```bash +python MediaBrain.py +``` + +Доступ к командной строке только для чтения (записи в базу данных не выполняются): + +```bash +python cli.py list +python cli.py search "title" +python cli.py --db /path/to/media_brain.db types +``` + +CLI скрывает элементы из чёрного списка. Чтобы явно выбрать базу данных, задайте `MEDIABRAIN_DB` или используйте `--db`. + +Лаунчер Windows для запуска двойным щелчком: + +```bat +START.bat +``` + +Лёгкий лаунчер для Windows можно собрать командой: + +```bat +build_exe.bat +``` + +## Тесты + +```bash +python -m pytest tests/ -q +``` + +Набор тестов охватывает менеджеры баз данных, метаданные, теги, QueryBuilder, плейлисты и работу интерфейса плейлистов в режиме offscreen. + +Специализированный платформенный smoke-тест проверяет пути Unicode, конфигурацию, обмен JSON, системное открытие файлов и жизненный цикл окна и трея в режиме offscreen: + +```bash +python -m pytest tests/source_platform_smoke.py -q +``` + +GitHub Actions запускает этот узконаправленный smoke-тест в Ubuntu и macOS. Он проверяет исходный код и контракт GUI в режиме offscreen, но не подтверждает нативную упаковку, работу трея в настольной сессии или ручную приёмку на любой из этих платформ. + +## Конфиденциальность + +MediaBrain хранит данные об использовании локально в базах SQLite и файлах конфигурации. Телеметрии и автоматической синхронизации с облаком нет. Необязательные запросы метаданных используют только настроенные пользователем ключи API TMDb/OMDb или общедоступные данные MusicBrainz. Подробнее: [PRIVACY_POLICY.md](PRIVACY_POLICY.md). + +## Лицензия + +Лицензия MIT, см. [LICENSE](LICENSE). Лицензии прямых сторонних зависимостей времени выполнения и разработки перечислены в [THIRD_PARTY_LICENSES.txt](THIRD_PARTY_LICENSES.txt). На PySide6, используемый в GUI, распространяются условия LGPL. + +## Ответственность + +Проект предоставляется бесплатно и «как есть», без каких-либо гарантий, обязательств по его сопровождению и гарантий пригодности для конкретной цели. Используйте его на свой риск. diff --git a/README_zh.md b/README_zh.md new file mode 100644 index 0000000..5baba0f --- /dev/null +++ b/README_zh.md @@ -0,0 +1,170 @@ +MediaBrain 横幅 + +# MediaBrain + +[![版本:2.1.0](https://img.shields.io/badge/version-2.1.0-orange.svg)](version.py) +[![许可证:MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) +[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/) +[![平台:Windows](https://img.shields.io/badge/platform-Windows-lightgrey.svg)]() +[![离线优先:是](https://img.shields.io/badge/offline--first-yes-brightgreen.svg)]() +[![测试:274 项通过](https://img.shields.io/badge/tests-274%20passed-brightgreen.svg)]() +[![doc-bricks](https://img.shields.io/badge/doc--bricks-ecosystem-blue.svg)](https://github.com/doc-bricks) +[![open-bricks](https://img.shields.io/badge/open--bricks-umbrella-purple.svg)](https://github.com/open-bricks) +[![LLM 上下文](https://img.shields.io/badge/LLM--Context-llms.txt-blue.svg)](llms.txt) + +> **MediaBrain 是一款基于 PySide6 的本地优先媒体库管理器,用于整理个人视频、音乐、播放列表以及流媒体观看记录。** + +> [!NOTE] +> **AI / 智能体上下文**:仓库架构、文件索引、搜索短语和系统边界的机器可读资料见 [llms.txt](llms.txt)。 + +[English](README.md) | [Deutsch](README_de.md) | **简体中文** | [隐私政策](PRIVACY_POLICY.md) | [贡献指南](CONTRIBUTING.md) | [更新日志](CHANGELOG.md) + +![MediaBrain 主窗口](README/screenshots/main.png) + +## 文档语言 + +| 语言 | 文档 | +|---|---| +| English | [README.md](README.md) | +| Deutsch | [README_de.md](README_de.md) | +| Español | [README_es.md](README_es.md) | +| 简体中文 | [README_zh.md](README_zh.md) | +| 日本語 | [README_ja.md](README_ja.md) | +| Русский | [README_ru.md](README_ru.md) | + +此列表仅说明 README 文档版本,不代表应用运行时支持的语言。 + +## 从这里开始 + +| 需求 | 前往 | +|---|---| +| 运行桌面应用 | `python MediaBrain.py` | +| 查看 Flutter 移动端版本 | `flutter_port/README.md` | +| 了解隐私边界 | [PRIVACY_POLICY.md](PRIVACY_POLICY.md) | +| 了解架构 | [ARCH.md](ARCH.md) | +| 查看计划中的工作 | [ROADMAP.md](ROADMAP.md) | +| 获取机器可读的上下文 | [llms.txt](llms.txt) | + +## 为什么选择 MediaBrain + +MediaBrain 面向希望在一个本地位置管理自己观看、收藏、标记、拉黑或整理的媒体内容的人,内容来自各类服务和本地文件。它不是托管式流媒体服务,也不会把私人媒体库上传到云账户。 + +- 使用本地 SQLite 保存媒体条目、收藏、标签、黑名单项目和播放列表 +- 基于 PySide6 的桌面界面,包含仪表板、媒体库、搜索、统计、设置和高对比度主题 +- 支持 Netflix、YouTube、Spotify、Disney+、Prime Video、Apple TV+、Twitch 和本地文件的服务提供方层 +- 手动播放列表和基于 QueryBuilder 规则生成的智能播放列表 +- 可选的元数据查询:使用用户配置的 TMDb/OMDb 密钥或 MusicBrainz 公共数据 +- 无遥测、无跟踪,也不会自动同步到云端 + +## 搜索上下文 + +此仓库是以下项目和短语对应的规范 GitHub 地址: + +- `doc-bricks MediaBrain` +- `MediaBrain media library manager` +- `private SQLite media library manager` +- `local-first PySide6 media manager` +- `PySide6 smart playlist media manager` +- `offline media hub with smart playlists` +- `desktop media history tracker` +- `privacy-first media history desktop app` + +“MediaBrain”这个名称也被媒体分析公司、AI 媒体实验室和通用媒体规划工具使用。此仓库专指 `doc-bricks/MediaBrain` 这个本地优先桌面媒体库管理器,并非云端广告分析平台、多媒体研究实验室或 AI 媒体生成器。 + +## 架构 + +```text +Core Layer Database, MediaManager, BlacklistManager, TagManager, PlaylistManager +Query Layer QueryBuilder for advanced filters and smart playlists +Provider Layer Netflix, YouTube, Spotify, Disney+, Prime, AppleTV+, Twitch, Local +Background Layer FileIndexer, WindowWatcher +GUI Layer Dashboard, Libraries, Favorites, Blacklist, Playlists, Search, Stats, Settings +``` + +完整架构图:[ARCH.md](ARCH.md) + +## 生态系统与相关工具 + +MediaBrain 是 [`doc-bricks`](https://github.com/doc-bricks) 文档与效率工具套件的一部分,隶属于 [`open-bricks`](https://github.com/open-bricks) 体系: + +| 工具 | 重点方向 | 仓库 | +|---|---|---| +| **MediaBrain** | 本地优先的媒体历史、智能播放列表和媒体库管理 | [doc-bricks/MediaBrain](https://github.com/doc-bricks/MediaBrain) | +| **CleanMarkdown** | 排版规范化、Markdown 清理和 PDF 导出 | [doc-bricks/CleanMarkdown](https://github.com/doc-bricks/CleanMarkdown) | +| **PDFtoPDFocr** | 可搜索 PDF 的 OCR 流程和元数据提取 | [doc-bricks/PDFtoPDFocr](https://github.com/doc-bricks/PDFtoPDFocr) | +| **llm-note** | 用于个人或智能体撰写笔记的本地 SQLite 与纯文本笔记本 | [doc-bricks/llm-note](https://github.com/doc-bricks/llm-note) | +| **LaunchBoards** | 可视化启动器、应用整理器和工作台仪表板 | [file-bricks/LaunchBoards](https://github.com/file-bricks/LaunchBoards) | +| **ProFiler** | 注重隐私的文件分析器、检查工具和重复项查找器 | [file-bricks/ProFiler](https://github.com/file-bricks/ProFiler) | +| **KnowledgeDigest** | 离线知识库和文档压缩工具 | [file-bricks/KnowledgeDigest](https://github.com/file-bricks/KnowledgeDigest) | +| **WinStorePackager** | MSIX 打包、Store 清单暂存和资源生成器 | [file-bricks/WinStorePackager](https://github.com/file-bricks/WinStorePackager) | +| **DevCenter** | 多仓库开发者仪表板、CI 监控器和工作区管理器 | [dev-bricks/DevCenter](https://github.com/dev-bricks/DevCenter) | +| **open-bricks** | 汇集所有桌面应用和 CLI 工具的统一门户 | [open-bricks/open-bricks](https://github.com/open-bricks/open-bricks) | + +## 安装 + +系统要求: + +- Python 3.10 或更高版本 +- 支持 Qt/PySide6 的 Windows、Linux 或 macOS + +```bash +pip install -r requirements.txt +``` + +首次启动时,MediaBrain 会在本地创建 `settings.json`。公开模板位于 [settings.example.json](settings.example.json)。个人设置、数据库、日志和构建产物通过 `.gitignore` 排除。 + +## 使用 + +```bash +python MediaBrain.py +``` + +只读命令行访问(不会写入数据库): + +```bash +python cli.py list +python cli.py search "title" +python cli.py --db /path/to/media_brain.db types +``` + +CLI 会隐藏已列入黑名单的条目。设置 `MEDIABRAIN_DB` 或使用 `--db` 可显式选择数据库。 + +Windows 双击启动器: + +```bat +START.bat +``` + +可通过以下命令构建轻量级 Windows 启动器: + +```bat +build_exe.bat +``` + +## 测试 + +```bash +python -m pytest tests/ -q +``` + +测试套件覆盖数据库管理器、元数据、标签、QueryBuilder、播放列表,以及在 offscreen 模式下运行的播放列表 GUI 行为。 + +专门的源平台冒烟测试会检查 Unicode 路径、配置、JSON 交换、操作系统文件打开器,以及 offscreen 窗口/托盘生命周期: + +```bash +python -m pytest tests/source_platform_smoke.py -q +``` + +GitHub Actions 会在 Ubuntu 和 macOS 上运行这项范围明确的冒烟测试。它验证源码和 offscreen GUI 契约,不代表原生打包、桌面会话中的托盘功能,或在任一平台上的人工验收。 + +## 隐私 + +MediaBrain 将使用数据保存在本地 SQLite 数据库和配置文件中。它没有遥测,也不会自动同步到云端。可选的元数据请求仅使用用户配置的 TMDb/OMDb API 密钥或 MusicBrainz 公共数据。详情见:[PRIVACY_POLICY.md](PRIVACY_POLICY.md)。 + +## 许可证 + +采用 MIT 许可证,见 [LICENSE](LICENSE)。直接的第三方运行时和开发依赖项许可证列于 [THIRD_PARTY_LICENSES.txt](THIRD_PARTY_LICENSES.txt)。GUI 使用的 PySide6 受 LGPL 条款约束。 + +## 责任限制 + +本项目免费提供,并按“现状”提供,不提供任何保证、维护承诺或适用性保证。使用风险由你自行承担。 From 4dd3534b9db0b83744efa008bfe7ec2d0bf89cc8 Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Sun, 25 Oct 2026 05:02:09 +0100 Subject: [PATCH 3/9] docs: complete eleven-language README set and synchronized navigation --- README.md | 17 +++-- README_ar.md | 176 +++++++++++++++++++++++++++++++++++++++++++++++++++ README_bn.md | 176 +++++++++++++++++++++++++++++++++++++++++++++++++++ README_de.md | 17 +++-- README_es.md | 18 ++++-- README_fr.md | 176 +++++++++++++++++++++++++++++++++++++++++++++++++++ README_hi.md | 176 +++++++++++++++++++++++++++++++++++++++++++++++++++ README_ja.md | 8 ++- README_pt.md | 176 +++++++++++++++++++++++++++++++++++++++++++++++++++ README_ru.md | 8 ++- README_zh.md | 8 ++- 11 files changed, 935 insertions(+), 21 deletions(-) create mode 100644 README_ar.md create mode 100644 README_bn.md create mode 100644 README_fr.md create mode 100644 README_hi.md create mode 100644 README_pt.md diff --git a/README.md b/README.md index 5b2b474..174cb0c 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ > [!NOTE] > **AI / Agent Context**: Machine-readable repo architecture, file indexes, search phrases, and system boundaries are available in [llms.txt](llms.txt). -**[English](README.md)** | [Deutsch](README_de.md) | [Español](README_es.md) | [中文](README_zh.md) | [日本語](README_ja.md) | [Русский](README_ru.md) | [Privacy](PRIVACY_POLICY.md) | [Contributing](CONTRIBUTING.md) | [Changelog](CHANGELOG.md) +**English** | [Deutsch](README_de.md) | [Español](README_es.md) | [简体中文](README_zh.md) | [日本語](README_ja.md) | [Русский](README_ru.md) | [Français](README_fr.md) | [हिन्दी](README_hi.md) | [العربية](README_ar.md) | [বাংলা](README_bn.md) | [Português](README_pt.md) | [Privacy](PRIVACY_POLICY.md) | [Contributing](CONTRIBUTING.md) | [Changelog](CHANGELOG.md) ![MediaBrain main window](README/screenshots/main.png) @@ -26,11 +26,16 @@ | Language | Document | |---|---| | English | [README.md](README.md) | -| German | [README_de.md](README_de.md) | -| Spanish | [README_es.md](README_es.md) | -| Chinese (Simplified) | [README_zh.md](README_zh.md) | -| Japanese | [README_ja.md](README_ja.md) | -| Russian | [README_ru.md](README_ru.md) | +| Deutsch | [README_de.md](README_de.md) | +| Español | [README_es.md](README_es.md) | +| 简体中文 | [README_zh.md](README_zh.md) | +| 日本語 | [README_ja.md](README_ja.md) | +| Русский | [README_ru.md](README_ru.md) | +| Français | [README_fr.md](README_fr.md) | +| हिन्दी | [README_hi.md](README_hi.md) | +| العربية | [README_ar.md](README_ar.md) | +| বাংলা | [README_bn.md](README_bn.md) | +| Português | [README_pt.md](README_pt.md) | This list describes the README documentation languages only. diff --git a/README_ar.md b/README_ar.md new file mode 100644 index 0000000..e5c6751 --- /dev/null +++ b/README_ar.md @@ -0,0 +1,176 @@ + +لافتة MediaBrain + +# MediaBrain + +[![الإصدار: 2.1.0](https://img.shields.io/badge/version-2.1.0-orange.svg)](version.py) +[![الترخيص: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) +[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/) +[![المنصة: Windows](https://img.shields.io/badge/platform-Windows-lightgrey.svg)]() +[![الأولوية للعمل دون اتصال: نعم](https://img.shields.io/badge/offline--first-yes-brightgreen.svg)]() +[![الاختبارات: اجتاز 274](https://img.shields.io/badge/tests-274%20passed-brightgreen.svg)]() +[![doc-bricks](https://img.shields.io/badge/doc--bricks-ecosystem-blue.svg)](https://github.com/doc-bricks) +[![open-bricks](https://img.shields.io/badge/open--bricks-umbrella-purple.svg)](https://github.com/open-bricks) +[![سياق LLM](https://img.shields.io/badge/LLM--Context-llms.txt-blue.svg)](llms.txt) + +> **MediaBrain هو مدير مكتبة وسائط مبني على PySide6 يعطي الأولوية للتخزين المحلي، لإدارة الفيديو والموسيقى وقوائم التشغيل وسجل المشاهدة عبر خدمات البث.** + +> [!NOTE] +> **سياق الذكاء الاصطناعي والوكلاء**: تتوفر في [llms.txt](llms.txt) معلومات مقروءة آليًا عن بنية المستودع وفهارس الملفات وعبارات البحث وحدود النظام. + +[English](README.md) | [Deutsch](README_de.md) | [Español](README_es.md) | [简体中文](README_zh.md) | [日本語](README_ja.md) | [Русский](README_ru.md) | [Français](README_fr.md) | [हिन्दी](README_hi.md) | **العربية** | [বাংলা](README_bn.md) | [Português](README_pt.md) | [الخصوصية](PRIVACY_POLICY.md) | [المساهمة](CONTRIBUTING.md) | [سجل التغييرات](CHANGELOG.md) + +![النافذة الرئيسية لـ MediaBrain](README/screenshots/main.png) + +## لغات التوثيق + +| اللغة | المستند | +|---|---| +| English | [README.md](README.md) | +| Deutsch | [README_de.md](README_de.md) | +| Español | [README_es.md](README_es.md) | +| 简体中文 | [README_zh.md](README_zh.md) | +| 日本語 | [README_ja.md](README_ja.md) | +| Русский | [README_ru.md](README_ru.md) | +| Français | [README_fr.md](README_fr.md) | +| हिन्दी | [README_hi.md](README_hi.md) | +| العربية | [README_ar.md](README_ar.md) | +| বাংলা | [README_bn.md](README_bn.md) | +| Português | [README_pt.md](README_pt.md) | + +تعرض هذه القائمة إصدارات README الموثقة فقط، ولا تصف اللغات التي يدعمها التطبيق أثناء التشغيل. + +## ابدأ من هنا + +| ما تحتاج إليه | انتقل إلى | +|---|---| +| تشغيل تطبيق سطح المكتب | `python MediaBrain.py` | +| الاطلاع على إصدار Flutter للأجهزة المحمولة | `flutter_port/README.md` | +| معرفة حدود الخصوصية | [PRIVACY_POLICY.md](PRIVACY_POLICY.md) | +| فهم البنية | [ARCH.md](ARCH.md) | +| مراجعة الأعمال المخطط لها | [ROADMAP.md](ROADMAP.md) | +| الحصول على سياق مقروء آليًا | [llms.txt](llms.txt) | + +## لماذا MediaBrain + +صُمم MediaBrain لمن يريد مكانًا محليًا واحدًا لإدارة الوسائط التي يشاهدها أو يجمعها أو يضع لها وسومًا أو يضيفها إلى القائمة السوداء أو ينظمها عبر الخدمات والملفات المحلية. ليس خدمة بث مستضافة، ولا يرفع مكتبة خاصة إلى حساب سحابي. + +- تخزين SQLite محلي لعناصر الوسائط والمفضلة والوسوم وعناصر القائمة السوداء وقوائم التشغيل +- واجهة سطح مكتب PySide6 تضم لوحة معلومات ومكتبات وبحثًا وإحصاءات وإعدادات وسمات عالية التباين +- طبقة مزودين لـ Netflix وYouTube وSpotify وDisney+ وPrime Video وApple TV+ وTwitch والملفات المحلية +- قوائم تشغيل يدوية وذكية تستند إلى قواعد QueryBuilder +- بحث اختياري عن البيانات الوصفية باستخدام مفاتيح TMDb/OMDb التي يهيئها المستخدم أو بيانات MusicBrainz العامة +- لا قياس عن بُعد ولا تتبّع ولا مزامنة سحابية تلقائية + +## سياق البحث + +هذا المستودع هو الموطن الرسمي على GitHub لما يلي: + +- `doc-bricks MediaBrain` +- `MediaBrain media library manager` +- `private SQLite media library manager` +- `local-first PySide6 media manager` +- `PySide6 smart playlist media manager` +- `offline media hub with smart playlists` +- `desktop media history tracker` +- `privacy-first media history desktop app` + +يتطابق اسم «MediaBrain» مع أسماء شركات لتحليلات الوسائط ومختبرات ذكاء اصطناعي للوسائط وأدوات عامة لتخطيط الوسائط. يخص هذا المستودع تحديدًا مكتبة الوسائط المكتبية ذات الأولوية للتخزين المحلي `doc-bricks/MediaBrain`، وليس منصة تحليلات إعلانية سحابية أو مختبر أبحاث متعددة الوسائط أو مولّدًا للوسائط بالذكاء الاصطناعي. + +## البنية + +```text +Core Layer Database, MediaManager, BlacklistManager, TagManager, PlaylistManager +Query Layer QueryBuilder for advanced filters and smart playlists +Provider Layer Netflix, YouTube, Spotify, Disney+, Prime, AppleTV+, Twitch, Local +Background Layer FileIndexer, WindowWatcher +GUI Layer Dashboard, Libraries, Favorites, Blacklist, Playlists, Search, Stats, Settings +``` + +المخطط الكامل: [ARCH.md](ARCH.md) + +## المنظومة والأدوات المرتبطة + +يندرج MediaBrain ضمن حزمة الوثائق والإنتاجية [`doc-bricks`](https://github.com/doc-bricks) تحت المظلة [`open-bricks`](https://github.com/open-bricks): + +| الأداة | المجال | المستودع | +|---|---|---| +| **MediaBrain** | سجل وسائط محلي وقوائم تشغيل ذكية وإدارة مكتبة الوسائط | [doc-bricks/MediaBrain](https://github.com/doc-bricks/MediaBrain) | +| **CleanMarkdown** | توحيد الطباعة وتنظيف Markdown وتصدير PDF | [doc-bricks/CleanMarkdown](https://github.com/doc-bricks/CleanMarkdown) | +| **PDFtoPDFocr** | مسار OCR لملفات PDF القابلة للبحث واستخراج البيانات الوصفية | [doc-bricks/PDFtoPDFocr](https://github.com/doc-bricks/PDFtoPDFocr) | +| **llm-note** | دفتر ملاحظات محلي باستخدام SQLite والنص العادي لملاحظات شخصية أو مكتوبة بواسطة وكلاء | [doc-bricks/llm-note](https://github.com/doc-bricks/llm-note) | +| **LaunchBoards** | مشغّل مرئي ومنظّم للتطبيقات ولوحة عمل | [file-bricks/LaunchBoards](https://github.com/file-bricks/LaunchBoards) | +| **ProFiler** | محلل وفاحص ملفات يركز على الخصوصية ويكشف الملفات المكررة | [file-bricks/ProFiler](https://github.com/file-bricks/ProFiler) | +| **KnowledgeDigest** | قاعدة معرفة دون اتصال وأداة لضغط التوثيق | [file-bricks/KnowledgeDigest](https://github.com/file-bricks/KnowledgeDigest) | +| **WinStorePackager** | تعبئة MSIX وتجهيز بيانات Store الوصفية وإنشاء الأصول | [file-bricks/WinStorePackager](https://github.com/file-bricks/WinStorePackager) | +| **DevCenter** | لوحة مطورين متعددة المستودعات ومراقب CI ومدير لمساحات العمل | [dev-bricks/DevCenter](https://github.com/dev-bricks/DevCenter) | +| **open-bricks** | بوابة موحدة لتطبيقات سطح المكتب وأدوات CLI | [open-bricks/open-bricks](https://github.com/open-bricks/open-bricks) | + +## التثبيت + +المتطلبات: + +- Python 3.10 أو أحدث +- Windows أو Linux أو macOS مع دعم Qt/PySide6 + +```bash +pip install -r requirements.txt +``` + +عند التشغيل لأول مرة، ينشئ MediaBrain ملف `settings.json` محليًا. القالب العام هو [settings.example.json](settings.example.json). تُستبعد الإعدادات الشخصية وقواعد البيانات والسجلات ومخرجات البناء عبر `.gitignore`. + +## الاستخدام + +```bash +python MediaBrain.py +``` + +استخدام سطر الأوامر للقراءة فقط (لا يكتب مطلقًا في قاعدة البيانات): + +```bash +python cli.py list +python cli.py search "title" +python cli.py --db /path/to/media_brain.db types +``` + +تخفي CLI العناصر المدرجة في القائمة السوداء. عيّن `MEDIABRAIN_DB` أو استخدم `--db` لاختيار قاعدة بيانات صراحةً. + +مشغّل Windows بالنقر المزدوج: + +```bat +START.bat +``` + +يمكن إنشاء مشغّل Windows خفيف باستخدام: + +```bat +build_exe.bat +``` + +## الاختبارات + +```bash +python -m pytest tests/ -q +``` + +تغطي مجموعة الاختبارات مديري قواعد البيانات والبيانات الوصفية والوسوم وQueryBuilder وقوائم التشغيل وسلوك واجهة قوائم التشغيل في وضع offscreen. + +يفحص اختبار smoke مخصص لمنصة المصدر مسارات Unicode والإعدادات وتبادل JSON وفتّاح الملفات في نظام التشغيل ودورة حياة النافذة/علبة النظام في وضع offscreen: + +```bash +python -m pytest tests/source_platform_smoke.py -q +``` + +تشغّل GitHub Actions هذا الاختبار المحدود على Ubuntu وmacOS. وهو يتحقق من عقد الشيفرة المصدرية وواجهة GUI في وضع offscreen؛ ولا يثبت التغليف الأصلي أو عمل علبة النظام ضمن جلسة سطح المكتب أو القبول اليدوي على أي من المنصتين. + +## الخصوصية + +يخزن MediaBrain بيانات الاستخدام محليًا في قواعد SQLite وملفات الإعدادات. لا يحتوي على قياس عن بُعد ولا على مزامنة سحابية تلقائية. لا تستخدم طلبات البيانات الوصفية الاختيارية سوى مفاتيح API لـ TMDb/OMDb يهيئها المستخدم أو بيانات MusicBrainz العامة. التفاصيل: [PRIVACY_POLICY.md](PRIVACY_POLICY.md). + +## الترخيص + +الترخيص MIT، راجع [LICENSE](LICENSE). تُدرج تراخيص تبعيات الطرف الثالث المباشرة لوقت التشغيل والتطوير في [THIRD_PARTY_LICENSES.txt](THIRD_PARTY_LICENSES.txt). وتخضع PySide6 المستخدمة في GUI لشروط LGPL. + +## المسؤولية + +يُقدَّم هذا المشروع مجانًا «كما هو»، من دون أي ضمان أو تعهّد بالصيانة أو ضمان للملاءمة لغرض معيّن. استخدمه على مسؤوليتك الخاصة. diff --git a/README_bn.md b/README_bn.md new file mode 100644 index 0000000..f20b27a --- /dev/null +++ b/README_bn.md @@ -0,0 +1,176 @@ + +MediaBrain ব্যানার + +# MediaBrain + +[![সংস্করণ: 2.1.0](https://img.shields.io/badge/version-2.1.0-orange.svg)](version.py) +[![লাইসেন্স: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) +[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/) +[![প্ল্যাটফর্ম: Windows](https://img.shields.io/badge/platform-Windows-lightgrey.svg)]() +[![অফলাইনে ব্যবহার অগ্রাধিকার: হ্যাঁ](https://img.shields.io/badge/offline--first-yes-brightgreen.svg)]() +[![পরীক্ষা: 274টি সফল](https://img.shields.io/badge/tests-274%20passed-brightgreen.svg)]() +[![doc-bricks](https://img.shields.io/badge/doc--bricks-ecosystem-blue.svg)](https://github.com/doc-bricks) +[![open-bricks](https://img.shields.io/badge/open--bricks-umbrella-purple.svg)](https://github.com/open-bricks) +[![LLM প্রসঙ্গ](https://img.shields.io/badge/LLM--Context-llms.txt-blue.svg)](llms.txt) + +> **MediaBrain হলো PySide6-ভিত্তিক একটি মিডিয়া লাইব্রেরি ম্যানেজার, যা স্থানীয়ভাবে তথ্য রাখাকে অগ্রাধিকার দেয় এবং ব্যক্তিগত ভিডিও, সংগীত, প্লেলিস্ট ও স্ট্রিমিং-সেবায় দেখার ইতিহাস পরিচালনা করে।** + +> [!NOTE] +> **AI / এজেন্ট প্রসঙ্গ**: রিপোজিটরির আর্কিটেকচার, ফাইল সূচি, অনুসন্ধানের বাক্যাংশ এবং সিস্টেমের সীমারেখার মেশিন-পাঠযোগ্য তথ্য [llms.txt](llms.txt)-এ রয়েছে। + +[English](README.md) | [Deutsch](README_de.md) | [Español](README_es.md) | [简体中文](README_zh.md) | [日本語](README_ja.md) | [Русский](README_ru.md) | [Français](README_fr.md) | [हिन्दी](README_hi.md) | [العربية](README_ar.md) | **বাংলা** | [Português](README_pt.md) | [গোপনীয়তা](PRIVACY_POLICY.md) | [অবদান](CONTRIBUTING.md) | [পরিবর্তনের তালিকা](CHANGELOG.md) + +![MediaBrain-এর প্রধান উইন্ডো](README/screenshots/main.png) + +## নথির ভাষা + +| ভাষা | নথি | +|---|---| +| English | [README.md](README.md) | +| Deutsch | [README_de.md](README_de.md) | +| Español | [README_es.md](README_es.md) | +| 简体中文 | [README_zh.md](README_zh.md) | +| 日本語 | [README_ja.md](README_ja.md) | +| Русский | [README_ru.md](README_ru.md) | +| Français | [README_fr.md](README_fr.md) | +| हिन्दी | [README_hi.md](README_hi.md) | +| العربية | [README_ar.md](README_ar.md) | +| বাংলা | [README_bn.md](README_bn.md) | +| Português | [README_pt.md](README_pt.md) | + +এই তালিকায় শুধু README নথির ভাষাগত সংস্করণগুলো আছে। এটি অ্যাপের রানটাইমে কোন ভাষা সমর্থিত, তা জানায় না। + +## এখান থেকে শুরু করুন + +| প্রয়োজন | দেখুন | +|---|---| +| ডেস্কটপ অ্যাপ চালান | `python MediaBrain.py` | +| Flutter মোবাইল সংস্করণ দেখুন | `flutter_port/README.md` | +| গোপনীয়তার সীমারেখা দেখুন | [PRIVACY_POLICY.md](PRIVACY_POLICY.md) | +| আর্কিটেকচার বুঝুন | [ARCH.md](ARCH.md) | +| পরিকল্পিত কাজ দেখুন | [ROADMAP.md](ROADMAP.md) | +| মেশিন-পাঠযোগ্য প্রসঙ্গ নিন | [llms.txt](llms.txt) | + +## MediaBrain কেন + +যাঁরা বিভিন্ন সেবা ও স্থানীয় ফাইলে থাকা দেখা, সংগ্রহ করা, ট্যাগ দেওয়া, ব্ল্যাকলিস্টে রাখা বা গুছিয়ে রাখা মিডিয়া একটিমাত্র স্থানীয় জায়গায় রাখতে চান, তাঁদের জন্য MediaBrain। এটি কোনো হোস্ট করা স্ট্রিমিং সেবা নয় এবং ব্যক্তিগত লাইব্রেরি কোনো ক্লাউড অ্যাকাউন্টে আপলোড করে না। + +- মিডিয়া এন্ট্রি, পছন্দের তালিকা, ট্যাগ, ব্ল্যাকলিস্ট আইটেম ও প্লেলিস্টের জন্য স্থানীয় SQLite স্টোরেজ +- ড্যাশবোর্ড, লাইব্রেরি, অনুসন্ধান, পরিসংখ্যান, সেটিংস ও উচ্চ-কনট্রাস্ট থিমসহ PySide6 ডেস্কটপ UI +- Netflix, YouTube, Spotify, Disney+, Prime Video, Apple TV+, Twitch এবং স্থানীয় ফাইলের জন্য প্রোভাইডার স্তর +- হাতে তৈরি প্লেলিস্ট এবং QueryBuilder নিয়মভিত্তিক স্মার্ট প্লেলিস্ট +- ব্যবহারকারীর কনফিগার করা TMDb/OMDb কী বা MusicBrainz-এর উন্মুক্ত তথ্য দিয়ে ঐচ্ছিক মেটাডেটা অনুসন্ধান +- কোনো টেলিমেট্রি নেই, ট্র্যাকিং নেই, স্বয়ংক্রিয় ক্লাউড সিঙ্ক নেই + +## অনুসন্ধানের প্রসঙ্গ + +এই রিপোজিটরিটি নিচের নাম ও অনুসন্ধান-শব্দগুলোর জন্য নির্দিষ্ট GitHub ঠিকানা: + +- `doc-bricks MediaBrain` +- `MediaBrain media library manager` +- `private SQLite media library manager` +- `local-first PySide6 media manager` +- `PySide6 smart playlist media manager` +- `offline media hub with smart playlists` +- `desktop media history tracker` +- `privacy-first media history desktop app` + +“MediaBrain” নামটি মিডিয়া অ্যানালিটিক্স কোম্পানি, AI মিডিয়া ল্যাব এবং সাধারণ মিডিয়া-পরিকল্পনা টুলের সঙ্গেও মিলে যেতে পারে। এই রিপোজিটরিটি বিশেষভাবে স্থানীয় অগ্রাধিকারভিত্তিক ডেস্কটপ মিডিয়া লাইব্রেরি `doc-bricks/MediaBrain` নিয়ে; এটি ক্লাউড বিজ্ঞাপন-অ্যানালিটিক্স প্ল্যাটফর্ম, মাল্টিমিডিয়া গবেষণাগার বা AI মিডিয়া জেনারেটর নয়। + +## আর্কিটেকচার + +```text +Core Layer Database, MediaManager, BlacklistManager, TagManager, PlaylistManager +Query Layer QueryBuilder for advanced filters and smart playlists +Provider Layer Netflix, YouTube, Spotify, Disney+, Prime, AppleTV+, Twitch, Local +Background Layer FileIndexer, WindowWatcher +GUI Layer Dashboard, Libraries, Favorites, Blacklist, Playlists, Search, Stats, Settings +``` + +সম্পূর্ণ চিত্র: [ARCH.md](ARCH.md) + +## ইকোসিস্টেম ও সংশ্লিষ্ট টুল + +MediaBrain [`doc-bricks`](https://github.com/doc-bricks)-এর নথি ও উৎপাদনশীলতা টুলসমষ্টির অংশ, যা [`open-bricks`](https://github.com/open-bricks)-এর অধীনে রয়েছে: + +| টুল | কাজ | রিপোজিটরি | +|---|---|---| +| **MediaBrain** | স্থানীয় মিডিয়া ইতিহাস, স্মার্ট প্লেলিস্ট ও লাইব্রেরি ব্যবস্থাপনা | [doc-bricks/MediaBrain](https://github.com/doc-bricks/MediaBrain) | +| **CleanMarkdown** | টাইপোগ্রাফি স্বাভাবিকীকরণ, Markdown পরিষ্কার ও PDF রপ্তানি | [doc-bricks/CleanMarkdown](https://github.com/doc-bricks/CleanMarkdown) | +| **PDFtoPDFocr** | অনুসন্ধানযোগ্য PDF-এর OCR প্রক্রিয়া ও মেটাডেটা নিষ্কাশন | [doc-bricks/PDFtoPDFocr](https://github.com/doc-bricks/PDFtoPDFocr) | +| **llm-note** | ব্যক্তিগত বা এজেন্ট-লিখিত নোটের জন্য স্থানীয় SQLite ও সাধারণ-টেক্সট নোটবুক | [doc-bricks/llm-note](https://github.com/doc-bricks/llm-note) | +| **LaunchBoards** | ভিজ্যুয়াল লঞ্চার, অ্যাপ সংগঠক ও ওয়ার্কস্টেশন ড্যাশবোর্ড | [file-bricks/LaunchBoards](https://github.com/file-bricks/LaunchBoards) | +| **ProFiler** | গোপনীয়তা-কেন্দ্রিক ফাইল বিশ্লেষক, পরিদর্শক ও ডুপ্লিকেট শনাক্তকারী | [file-bricks/ProFiler](https://github.com/file-bricks/ProFiler) | +| **KnowledgeDigest** | অফলাইন জ্ঞানভাণ্ডার ও নথি সংকোচক | [file-bricks/KnowledgeDigest](https://github.com/file-bricks/KnowledgeDigest) | +| **WinStorePackager** | MSIX প্যাকেজিং, Store ম্যানিফেস্ট প্রস্তুতি ও অ্যাসেট জেনারেটর | [file-bricks/WinStorePackager](https://github.com/file-bricks/WinStorePackager) | +| **DevCenter** | বহু-রিপোজিটরি ডেভেলপার ড্যাশবোর্ড, CI মনিটর ও ওয়ার্কস্পেস ম্যানেজার | [dev-bricks/DevCenter](https://github.com/dev-bricks/DevCenter) | +| **open-bricks** | সব ডেস্কটপ ও CLI টুলের সমন্বিত পোর্টাল | [open-bricks/open-bricks](https://github.com/open-bricks/open-bricks) | + +## ইনস্টলেশন + +প্রয়োজনীয়তা: + +- Python 3.10 বা নতুন +- Qt/PySide6 সমর্থিত Windows, Linux বা macOS + +```bash +pip install -r requirements.txt +``` + +প্রথমবার চালালে MediaBrain স্থানীয়ভাবে `settings.json` তৈরি করে। সর্বজনীন টেমপ্লেট [settings.example.json](settings.example.json)। ব্যক্তিগত সেটিংস, ডেটাবেস, লগ ও বিল্ড আর্টিফ্যাক্ট `.gitignore` দিয়ে বাদ রাখা হয়। + +## ব্যবহার + +```bash +python MediaBrain.py +``` + +শুধু-পড়ার কমান্ড-লাইন ব্যবহার (ডেটাবেসে কখনও লেখে না): + +```bash +python cli.py list +python cli.py search "title" +python cli.py --db /path/to/media_brain.db types +``` + +CLI ব্ল্যাকলিস্টে থাকা এন্ট্রি আড়াল করে। নির্দিষ্টভাবে ডেটাবেস বেছে নিতে `MEDIABRAIN_DB` সেট করুন বা `--db` ব্যবহার করুন। + +Windows-এ ডাবল-ক্লিক লঞ্চার: + +```bat +START.bat +``` + +হালকা Windows লঞ্চার তৈরি করা যায় এভাবে: + +```bat +build_exe.bat +``` + +## পরীক্ষা + +```bash +python -m pytest tests/ -q +``` + +টেস্ট স্যুটে ডেটাবেস ম্যানেজার, মেটাডেটা, ট্যাগ, QueryBuilder, প্লেলিস্ট এবং offscreen মোডে প্লেলিস্ট GUI-এর আচরণ অন্তর্ভুক্ত। + +নির্দিষ্ট source-platform smoke test Unicode পাথ, কনফিগারেশন, JSON বিনিময়, অপারেটিং সিস্টেমের ফাইল ওপেনার এবং offscreen উইন্ডো/ট্রে জীবনচক্র পরীক্ষা করে: + +```bash +python -m pytest tests/source_platform_smoke.py -q +``` + +GitHub Actions Ubuntu ও macOS-এ এই সীমিত smoke test চালায়। এটি সোর্স ও offscreen GUI চুক্তির পরীক্ষা; এটি নেটিভ প্যাকেজিং, ডেস্কটপ সেশনে ট্রে কাজ করা বা কোনো প্ল্যাটফর্মে হাতে করা গ্রহণযোগ্যতার প্রমাণ নয়। + +## গোপনীয়তা + +MediaBrain ব্যবহারের তথ্য স্থানীয় SQLite ডেটাবেস ও কনফিগারেশন ফাইলে রাখে। এতে টেলিমেট্রি বা স্বয়ংক্রিয় ক্লাউড সিঙ্ক নেই। ঐচ্ছিক মেটাডেটা অনুরোধে কেবল ব্যবহারকারীর কনফিগার করা TMDb/OMDb API কী বা MusicBrainz-এর উন্মুক্ত তথ্য ব্যবহৃত হয়। বিস্তারিত: [PRIVACY_POLICY.md](PRIVACY_POLICY.md)। + +## লাইসেন্স + +MIT লাইসেন্স; [LICENSE](LICENSE) দেখুন। সরাসরি ব্যবহৃত তৃতীয়-পক্ষের রানটাইম ও ডেভেলপমেন্ট নির্ভরতাগুলোর লাইসেন্স [THIRD_PARTY_LICENSES.txt](THIRD_PARTY_LICENSES.txt)-এ তালিকাভুক্ত। GUI-তে ব্যবহৃত PySide6 LGPL শর্তের অধীন। + +## দায়বদ্ধতা + +এই প্রকল্প বিনামূল্যে “যেমন আছে” তেমনভাবেই দেওয়া হয়; কোনো ওয়ারেন্টি, রক্ষণাবেক্ষণের নিশ্চয়তা বা নির্দিষ্ট উদ্দেশ্যে উপযোগিতার নিশ্চয়তা নেই। নিজের ঝুঁকিতে ব্যবহার করুন। diff --git a/README_de.md b/README_de.md index 1d13ff4..38e110a 100644 --- a/README_de.md +++ b/README_de.md @@ -17,7 +17,7 @@ > [!NOTE] > **KI / Agenten-Kontext**: Maschinenlesbare Architektur, Dateiverzeichnisse, Suchphrasen und Systemgrenzen sind in [llms.txt](llms.txt) dokumentiert. -[English](README.md) | **[Deutsch](README_de.md)** | [Español](README_es.md) | [中文](README_zh.md) | [日本語](README_ja.md) | [Русский](README_ru.md) | [Datenschutz](PRIVACY_POLICY.md) | [Mitwirken](CONTRIBUTING.md) | [Changelog](CHANGELOG.md) +[English](README.md) | **Deutsch** | [Español](README_es.md) | [简体中文](README_zh.md) | [日本語](README_ja.md) | [Русский](README_ru.md) | [Français](README_fr.md) | [हिन्दी](README_hi.md) | [العربية](README_ar.md) | [বাংলা](README_bn.md) | [Português](README_pt.md) | [Datenschutz](PRIVACY_POLICY.md) | [Mitwirken](CONTRIBUTING.md) | [Changelog](CHANGELOG.md) ![MediaBrain Hauptfenster](README/screenshots/main.png) @@ -25,12 +25,17 @@ | Sprache | Dokument | |---|---| -| Englisch | [README.md](README.md) | +| English | [README.md](README.md) | | Deutsch | [README_de.md](README_de.md) | -| Spanisch | [README_es.md](README_es.md) | -| Chinesisch (vereinfacht) | [README_zh.md](README_zh.md) | -| Japanisch | [README_ja.md](README_ja.md) | -| Russisch | [README_ru.md](README_ru.md) | +| Español | [README_es.md](README_es.md) | +| 简体中文 | [README_zh.md](README_zh.md) | +| 日本語 | [README_ja.md](README_ja.md) | +| Русский | [README_ru.md](README_ru.md) | +| Français | [README_fr.md](README_fr.md) | +| हिन्दी | [README_hi.md](README_hi.md) | +| العربية | [README_ar.md](README_ar.md) | +| বাংলা | [README_bn.md](README_bn.md) | +| Português | [README_pt.md](README_pt.md) | Diese Liste beschreibt ausschließlich die Sprachfassungen der README-Dokumentation. diff --git a/README_es.md b/README_es.md index 78e731a..4dd50f4 100644 --- a/README_es.md +++ b/README_es.md @@ -1,3 +1,4 @@ + Banner de MediaBrain # MediaBrain @@ -17,7 +18,7 @@ > [!NOTE] > **Contexto para IA y agentes**: La arquitectura del repositorio, los índices de archivos, las frases de búsqueda y los límites del sistema están documentados en [llms.txt](llms.txt). -[English](README.md) | [Deutsch](README_de.md) | **Español** | [中文](README_zh.md) | [日本語](README_ja.md) | [Русский](README_ru.md) | [Privacidad](PRIVACY_POLICY.md) | [Contribuir](CONTRIBUTING.md) | [Registro de cambios](CHANGELOG.md) +[English](README.md) | [Deutsch](README_de.md) | **Español** | [简体中文](README_zh.md) | [日本語](README_ja.md) | [Русский](README_ru.md) | [Français](README_fr.md) | [हिन्दी](README_hi.md) | [العربية](README_ar.md) | [বাংলা](README_bn.md) | [Português](README_pt.md) | [Privacidad](PRIVACY_POLICY.md) | [Contribuir](CONTRIBUTING.md) | [Registro de cambios](CHANGELOG.md) ![Ventana principal de MediaBrain](README/screenshots/main.png) @@ -25,12 +26,17 @@ | Idioma | Documento | |---|---| -| Inglés | [README.md](README.md) | -| Alemán | [README_de.md](README_de.md) | +| English | [README.md](README.md) | +| Deutsch | [README_de.md](README_de.md) | | Español | [README_es.md](README_es.md) | -| Chino simplificado | [README_zh.md](README_zh.md) | -| Japonés | [README_ja.md](README_ja.md) | -| Ruso | [README_ru.md](README_ru.md) | +| 简体中文 | [README_zh.md](README_zh.md) | +| 日本語 | [README_ja.md](README_ja.md) | +| Русский | [README_ru.md](README_ru.md) | +| Français | [README_fr.md](README_fr.md) | +| हिन्दी | [README_hi.md](README_hi.md) | +| العربية | [README_ar.md](README_ar.md) | +| বাংলা | [README_bn.md](README_bn.md) | +| Português | [README_pt.md](README_pt.md) | Esta lista se refiere únicamente a las versiones documentales del README. diff --git a/README_fr.md b/README_fr.md new file mode 100644 index 0000000..de9ee87 --- /dev/null +++ b/README_fr.md @@ -0,0 +1,176 @@ + +Bannière MediaBrain + +# MediaBrain + +[![Version : 2.1.0](https://img.shields.io/badge/version-2.1.0-orange.svg)](version.py) +[![Licence : MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) +[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/) +[![Plateforme : Windows](https://img.shields.io/badge/platform-Windows-lightgrey.svg)]() +[![Priorité au fonctionnement hors ligne : oui](https://img.shields.io/badge/offline--first-yes-brightgreen.svg)]() +[![Tests : 274 réussis](https://img.shields.io/badge/tests-274%20passed-brightgreen.svg)]() +[![doc-bricks](https://img.shields.io/badge/doc--bricks-ecosystem-blue.svg)](https://github.com/doc-bricks) +[![open-bricks](https://img.shields.io/badge/open--bricks-umbrella-purple.svg)](https://github.com/open-bricks) +[![Contexte LLM](https://img.shields.io/badge/LLM--Context-llms.txt-blue.svg)](llms.txt) + +> **MediaBrain est un gestionnaire de médiathèque PySide6 privilégiant le stockage local, conçu pour gérer des vidéos, de la musique, des playlists personnelles et l’historique de visionnage sur les services de streaming.** + +> [!NOTE] +> **Contexte pour les IA et les agents** : l’architecture du dépôt, les index de fichiers, les expressions de recherche et les limites du système sont décrits dans [llms.txt](llms.txt) sous une forme lisible par machine. + +[English](README.md) | [Deutsch](README_de.md) | [Español](README_es.md) | [简体中文](README_zh.md) | [日本語](README_ja.md) | [Русский](README_ru.md) | **Français** | [हिन्दी](README_hi.md) | [العربية](README_ar.md) | [বাংলা](README_bn.md) | [Português](README_pt.md) | [Confidentialité](PRIVACY_POLICY.md) | [Contribuer](CONTRIBUTING.md) | [Historique des modifications](CHANGELOG.md) + +![Fenêtre principale de MediaBrain](README/screenshots/main.png) + +## Langues de la documentation + +| Langue | Document | +|---|---| +| English | [README.md](README.md) | +| Deutsch | [README_de.md](README_de.md) | +| Español | [README_es.md](README_es.md) | +| 简体中文 | [README_zh.md](README_zh.md) | +| 日本語 | [README_ja.md](README_ja.md) | +| Русский | [README_ru.md](README_ru.md) | +| Français | [README_fr.md](README_fr.md) | +| हिन्दी | [README_hi.md](README_hi.md) | +| العربية | [README_ar.md](README_ar.md) | +| বাংলা | [README_bn.md](README_bn.md) | +| Português | [README_pt.md](README_pt.md) | + +Cette liste présente uniquement les versions documentaires du README. Elle ne décrit pas les langues prises en charge par l’application à l’exécution. + +## Pour commencer + +| Besoin | Voir | +|---|---| +| Lancer l’application de bureau | `python MediaBrain.py` | +| Consulter la version mobile Flutter | `flutter_port/README.md` | +| Vérifier les limites en matière de confidentialité | [PRIVACY_POLICY.md](PRIVACY_POLICY.md) | +| Comprendre l’architecture | [ARCH.md](ARCH.md) | +| Consulter les travaux prévus | [ROADMAP.md](ROADMAP.md) | +| Obtenir le contexte lisible par machine | [llms.txt](llms.txt) | + +## Pourquoi MediaBrain + +MediaBrain s’adresse aux personnes qui souhaitent réunir en un seul endroit local les médias qu’elles regardent, collectionnent, étiquettent, mettent sur liste noire ou organisent, à partir de services et de fichiers locaux. Ce n’est pas un service de streaming hébergé et l’application ne téléverse pas de médiathèque privée vers un compte cloud. + +- Stockage SQLite local des médias, favoris, étiquettes, éléments de liste noire et playlists +- Interface de bureau PySide6 avec tableau de bord, bibliothèques, recherche, statistiques, paramètres et thèmes à contraste élevé +- Couche de fournisseurs pour Netflix, YouTube, Spotify, Disney+, Prime Video, Apple TV+, Twitch et les fichiers locaux +- Playlists manuelles et playlists intelligentes fondées sur les règles de QueryBuilder +- Recherche facultative de métadonnées à l’aide de clés TMDb/OMDb configurées par l’utilisateur ou de données publiques MusicBrainz +- Aucune télémétrie, aucun suivi et aucune synchronisation automatique avec le cloud + +## Contexte de recherche + +Ce dépôt est le référentiel GitHub canonique pour : + +- `doc-bricks MediaBrain` +- `MediaBrain media library manager` +- `private SQLite media library manager` +- `local-first PySide6 media manager` +- `PySide6 smart playlist media manager` +- `offline media hub with smart playlists` +- `desktop media history tracker` +- `privacy-first media history desktop app` + +Le nom « MediaBrain » est également utilisé par des entreprises d’analyse média, des laboratoires d’IA appliquée aux médias et des outils génériques de planification média. Ce dépôt concerne précisément la médiathèque de bureau locale `doc-bricks/MediaBrain`, et non une plateforme d’analyse publicitaire dans le cloud, un laboratoire de recherche multimédia ou un générateur de médias par IA. + +## Architecture + +```text +Core Layer Database, MediaManager, BlacklistManager, TagManager, PlaylistManager +Query Layer QueryBuilder for advanced filters and smart playlists +Provider Layer Netflix, YouTube, Spotify, Disney+, Prime, AppleTV+, Twitch, Local +Background Layer FileIndexer, WindowWatcher +GUI Layer Dashboard, Libraries, Favorites, Blacklist, Playlists, Search, Stats, Settings +``` + +Schéma complet : [ARCH.md](ARCH.md) + +## Écosystème et outils associés + +MediaBrain fait partie de la suite documentaire et de productivité [`doc-bricks`](https://github.com/doc-bricks), sous l’égide de [`open-bricks`](https://github.com/open-bricks) : + +| Outil | Domaine | Dépôt | +|---|---|---| +| **MediaBrain** | Historique média local, playlists intelligentes et gestion de médiathèque | [doc-bricks/MediaBrain](https://github.com/doc-bricks/MediaBrain) | +| **CleanMarkdown** | Normalisation typographique, nettoyage Markdown et export PDF | [doc-bricks/CleanMarkdown](https://github.com/doc-bricks/CleanMarkdown) | +| **PDFtoPDFocr** | Pipeline OCR pour PDF consultables et extraction de métadonnées | [doc-bricks/PDFtoPDFocr](https://github.com/doc-bricks/PDFtoPDFocr) | +| **llm-note** | Bloc-notes local SQLite et texte brut pour notes personnelles ou rédigées par des agents | [doc-bricks/llm-note](https://github.com/doc-bricks/llm-note) | +| **LaunchBoards** | Lanceur visuel, organisateur d’applications et tableau de bord de poste de travail | [file-bricks/LaunchBoards](https://github.com/file-bricks/LaunchBoards) | +| **ProFiler** | Analyseur et inspecteur de fichiers axé sur la confidentialité, avec détection des doublons | [file-bricks/ProFiler](https://github.com/file-bricks/ProFiler) | +| **KnowledgeDigest** | Base de connaissances hors ligne et compresseur de documentation | [file-bricks/KnowledgeDigest](https://github.com/file-bricks/KnowledgeDigest) | +| **WinStorePackager** | Empaquetage MSIX, préparation des manifestes Store et générateur de ressources | [file-bricks/WinStorePackager](https://github.com/file-bricks/WinStorePackager) | +| **DevCenter** | Tableau de bord développeur multi-dépôts, moniteur CI et gestionnaire d’espaces de travail | [dev-bricks/DevCenter](https://github.com/dev-bricks/DevCenter) | +| **open-bricks** | Portail unifié pour les applications de bureau et outils CLI | [open-bricks/open-bricks](https://github.com/open-bricks/open-bricks) | + +## Installation + +Prérequis : + +- Python 3.10 ou version ultérieure +- Windows, Linux ou macOS avec prise en charge de Qt/PySide6 + +```bash +pip install -r requirements.txt +``` + +Au premier lancement, MediaBrain crée localement `settings.json`. Le modèle public est [settings.example.json](settings.example.json). Les paramètres personnels, bases de données, journaux et artefacts de compilation sont exclus via `.gitignore`. + +## Utilisation + +```bash +python MediaBrain.py +``` + +Accès en lecture seule en ligne de commande (aucune écriture dans la base de données) : + +```bash +python cli.py list +python cli.py search "title" +python cli.py --db /path/to/media_brain.db types +``` + +La CLI masque les éléments figurant sur la liste noire. Définissez `MEDIABRAIN_DB` ou utilisez `--db` pour sélectionner explicitement une base de données. + +Lanceur Windows à double-clic : + +```bat +START.bat +``` + +Un lanceur Windows léger peut être créé avec : + +```bat +build_exe.bat +``` + +## Tests + +```bash +python -m pytest tests/ -q +``` + +La suite de tests couvre les gestionnaires de base de données, les métadonnées, les étiquettes, QueryBuilder, les playlists et le comportement de l’interface des playlists en mode offscreen. + +Le test smoke dédié à la plateforme source vérifie les chemins Unicode, la configuration, l’échange JSON, l’ouvreur de fichiers du système d’exploitation et le cycle de vie de la fenêtre et de la zone de notification en mode offscreen : + +```bash +python -m pytest tests/source_platform_smoke.py -q +``` + +GitHub Actions exécute ce test smoke ciblé sur Ubuntu et macOS. Il vérifie le contrat du code source et de l’interface en mode offscreen ; il ne prouve ni l’empaquetage natif, ni le fonctionnement de la zone de notification dans une session de bureau, ni l’acceptation manuelle sur l’une ou l’autre plateforme. + +## Confidentialité + +MediaBrain stocke les données d’utilisation localement dans des bases SQLite et des fichiers de configuration. Il n’y a ni télémétrie ni synchronisation automatique avec le cloud. Les requêtes facultatives de métadonnées utilisent uniquement des clés API TMDb/OMDb configurées par l’utilisateur ou des données publiques MusicBrainz. Détails : [PRIVACY_POLICY.md](PRIVACY_POLICY.md). + +## Licence + +Licence MIT, voir [LICENSE](LICENSE). Les licences des dépendances directes tierces d’exécution et de développement sont répertoriées dans [THIRD_PARTY_LICENSES.txt](THIRD_PARTY_LICENSES.txt). L’interface graphique utilise PySide6, soumis aux conditions de la LGPL. + +## Responsabilité + +Ce projet est fourni gratuitement et « en l’état », sans garantie, engagement de maintenance ni garantie d’adéquation à un usage particulier. Utilisez-le à vos risques. diff --git a/README_hi.md b/README_hi.md new file mode 100644 index 0000000..f0a0bf8 --- /dev/null +++ b/README_hi.md @@ -0,0 +1,176 @@ + +MediaBrain बैनर + +# MediaBrain + +[![संस्करण: 2.1.0](https://img.shields.io/badge/version-2.1.0-orange.svg)](version.py) +[![लाइसेंस: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) +[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/) +[![प्लेटफ़ॉर्म: Windows](https://img.shields.io/badge/platform-Windows-lightgrey.svg)]() +[![ऑफ़लाइन उपयोग प्राथमिक: हाँ](https://img.shields.io/badge/offline--first-yes-brightgreen.svg)]() +[![परीक्षण: 274 सफल](https://img.shields.io/badge/tests-274%20passed-brightgreen.svg)]() +[![doc-bricks](https://img.shields.io/badge/doc--bricks-ecosystem-blue.svg)](https://github.com/doc-bricks) +[![open-bricks](https://img.shields.io/badge/open--bricks-umbrella-purple.svg)](https://github.com/open-bricks) +[![LLM संदर्भ](https://img.shields.io/badge/LLM--Context-llms.txt-blue.svg)](llms.txt) + +> **MediaBrain PySide6 पर आधारित मीडिया लाइब्रेरी मैनेजर है, जो डेटा को पहले स्थानीय रूप से रखने को प्राथमिकता देता है और व्यक्तिगत वीडियो, संगीत, प्लेलिस्ट तथा स्ट्रीमिंग सेवाओं के देखने के इतिहास को व्यवस्थित करता है।** + +> [!NOTE] +> **AI / एजेंट संदर्भ**: रिपॉज़िटरी की संरचना, फ़ाइल इंडेक्स, खोज वाक्यांश और सिस्टम की सीमाओं की मशीन-पठनीय जानकारी [llms.txt](llms.txt) में उपलब्ध है। + +[English](README.md) | [Deutsch](README_de.md) | [Español](README_es.md) | [简体中文](README_zh.md) | [日本語](README_ja.md) | [Русский](README_ru.md) | [Français](README_fr.md) | **हिन्दी** | [العربية](README_ar.md) | [বাংলা](README_bn.md) | [Português](README_pt.md) | [गोपनीयता](PRIVACY_POLICY.md) | [योगदान](CONTRIBUTING.md) | [बदलाव सूची](CHANGELOG.md) + +![MediaBrain की मुख्य विंडो](README/screenshots/main.png) + +## दस्तावेज़ की भाषाएँ + +| भाषा | दस्तावेज़ | +|---|---| +| English | [README.md](README.md) | +| Deutsch | [README_de.md](README_de.md) | +| Español | [README_es.md](README_es.md) | +| 简体中文 | [README_zh.md](README_zh.md) | +| 日本語 | [README_ja.md](README_ja.md) | +| Русский | [README_ru.md](README_ru.md) | +| Français | [README_fr.md](README_fr.md) | +| हिन्दी | [README_hi.md](README_hi.md) | +| العربية | [README_ar.md](README_ar.md) | +| বাংলা | [README_bn.md](README_bn.md) | +| Português | [README_pt.md](README_pt.md) | + +यह सूची केवल README के दस्तावेज़ी भाषा संस्करण दिखाती है। यह ऐप के रनटाइम भाषा समर्थन का दावा नहीं करती। + +## यहाँ से शुरू करें + +| आवश्यकता | यहाँ जाएँ | +|---|---| +| डेस्कटॉप ऐप चलाएँ | `python MediaBrain.py` | +| Flutter मोबाइल संस्करण देखें | `flutter_port/README.md` | +| गोपनीयता की सीमाएँ देखें | [PRIVACY_POLICY.md](PRIVACY_POLICY.md) | +| आर्किटेक्चर समझें | [ARCH.md](ARCH.md) | +| नियोजित काम देखें | [ROADMAP.md](ROADMAP.md) | +| मशीन-पठनीय संदर्भ पाएँ | [llms.txt](llms.txt) | + +## MediaBrain क्यों + +MediaBrain उन लोगों के लिए है जो अलग-अलग सेवाओं और स्थानीय फ़ाइलों में मौजूद अपने देखे, संग्रहित, टैग किए, ब्लैकलिस्ट में डाले या व्यवस्थित किए गए मीडिया को एक स्थानीय जगह पर रखना चाहते हैं। यह होस्टेड स्ट्रीमिंग सेवा नहीं है और यह निजी लाइब्रेरी को किसी क्लाउड खाते पर अपलोड नहीं करता। + +- मीडिया प्रविष्टियों, पसंदीदा वस्तुओं, टैग, ब्लैकलिस्ट आइटम और प्लेलिस्ट के लिए स्थानीय SQLite स्टोरेज +- डैशबोर्ड, लाइब्रेरी, खोज, आँकड़े, सेटिंग और उच्च-कंट्रास्ट थीम वाला PySide6 डेस्कटॉप UI +- Netflix, YouTube, Spotify, Disney+, Prime Video, Apple TV+, Twitch और स्थानीय फ़ाइलों के लिए प्रदाता परत +- मैन्युअल प्लेलिस्ट और QueryBuilder नियमों पर आधारित स्मार्ट प्लेलिस्ट +- उपयोगकर्ता द्वारा कॉन्फ़िगर की गई TMDb/OMDb कुंजियों या सार्वजनिक MusicBrainz डेटा से वैकल्पिक मेटाडेटा खोज +- कोई टेलीमेट्री, ट्रैकिंग या स्वचालित क्लाउड सिंक नहीं + +## खोज संदर्भ + +यह रिपॉज़िटरी निम्नलिखित के लिए आधिकारिक GitHub स्थान है: + +- `doc-bricks MediaBrain` +- `MediaBrain media library manager` +- `private SQLite media library manager` +- `local-first PySide6 media manager` +- `PySide6 smart playlist media manager` +- `offline media hub with smart playlists` +- `desktop media history tracker` +- `privacy-first media history desktop app` + +“MediaBrain” नाम मीडिया एनालिटिक्स कंपनियों, AI मीडिया लैब और सामान्य मीडिया-प्लानिंग टूल के नामों से मिलता-जुलता है। यह रिपॉज़िटरी विशेष रूप से स्थानीय प्राथमिकता वाले डेस्कटॉप मीडिया लाइब्रेरी `doc-bricks/MediaBrain` के लिए है; यह क्लाउड विज्ञापन-एनालिटिक्स प्लेटफ़ॉर्म, मल्टीमीडिया शोध लैब या AI मीडिया जनरेटर नहीं है। + +## आर्किटेक्चर + +```text +Core Layer Database, MediaManager, BlacklistManager, TagManager, PlaylistManager +Query Layer QueryBuilder for advanced filters and smart playlists +Provider Layer Netflix, YouTube, Spotify, Disney+, Prime, AppleTV+, Twitch, Local +Background Layer FileIndexer, WindowWatcher +GUI Layer Dashboard, Libraries, Favorites, Blacklist, Playlists, Search, Stats, Settings +``` + +पूरा आरेख: [ARCH.md](ARCH.md) + +## इकोसिस्टम और संबंधित टूल + +MediaBrain [`doc-bricks`](https://github.com/doc-bricks) के दस्तावेज़ और उत्पादकता टूल-संग्रह का हिस्सा है, जो [`open-bricks`](https://github.com/open-bricks) के अंतर्गत आता है: + +| टूल | उद्देश्य | रिपॉज़िटरी | +|---|---|---| +| **MediaBrain** | स्थानीय मीडिया इतिहास, स्मार्ट प्लेलिस्ट और लाइब्रेरी प्रबंधन | [doc-bricks/MediaBrain](https://github.com/doc-bricks/MediaBrain) | +| **CleanMarkdown** | टाइपोग्राफी सामान्यीकरण, Markdown सफ़ाई और PDF निर्यात | [doc-bricks/CleanMarkdown](https://github.com/doc-bricks/CleanMarkdown) | +| **PDFtoPDFocr** | खोज योग्य PDF के लिए OCR पाइपलाइन और मेटाडेटा निष्कर्षण | [doc-bricks/PDFtoPDFocr](https://github.com/doc-bricks/PDFtoPDFocr) | +| **llm-note** | व्यक्तिगत या एजेंट द्वारा लिखे नोट्स के लिए स्थानीय SQLite और साधारण-पाठ नोटबुक | [doc-bricks/llm-note](https://github.com/doc-bricks/llm-note) | +| **LaunchBoards** | दृश्य लॉन्चर, ऐप आयोजक और वर्कस्टेशन डैशबोर्ड | [file-bricks/LaunchBoards](https://github.com/file-bricks/LaunchBoards) | +| **ProFiler** | गोपनीयता-केंद्रित फ़ाइल विश्लेषक, निरीक्षक और डुप्लिकेट खोजक | [file-bricks/ProFiler](https://github.com/file-bricks/ProFiler) | +| **KnowledgeDigest** | ऑफ़लाइन ज्ञान आधार और दस्तावेज़ संपीड़क | [file-bricks/KnowledgeDigest](https://github.com/file-bricks/KnowledgeDigest) | +| **WinStorePackager** | MSIX पैकेजिंग, Store मेनिफ़ेस्ट तैयारी और एसेट जनरेटर | [file-bricks/WinStorePackager](https://github.com/file-bricks/WinStorePackager) | +| **DevCenter** | बहु-रिपॉज़िटरी डेवलपर डैशबोर्ड, CI मॉनिटर और वर्कस्पेस मैनेजर | [dev-bricks/DevCenter](https://github.com/dev-bricks/DevCenter) | +| **open-bricks** | सभी डेस्कटॉप और CLI टूल का एकीकृत पोर्टल | [open-bricks/open-bricks](https://github.com/open-bricks/open-bricks) | + +## इंस्टॉलेशन + +आवश्यकताएँ: + +- Python 3.10 या नया संस्करण +- Qt/PySide6 समर्थन वाला Windows, Linux या macOS + +```bash +pip install -r requirements.txt +``` + +पहली बार चलने पर MediaBrain स्थानीय रूप से `settings.json` बनाता है। सार्वजनिक टेम्पलेट [settings.example.json](settings.example.json) है। व्यक्तिगत सेटिंग, डेटाबेस, लॉग और बिल्ड आर्टिफ़ैक्ट `.gitignore` के ज़रिए बाहर रखे जाते हैं। + +## उपयोग + +```bash +python MediaBrain.py +``` + +केवल पढ़ने के लिए कमांड-लाइन पहुँच (डेटाबेस में कभी लिखता नहीं): + +```bash +python cli.py list +python cli.py search "title" +python cli.py --db /path/to/media_brain.db types +``` + +CLI ब्लैकलिस्ट किए गए आइटम छिपाता है। डेटाबेस स्पष्ट रूप से चुनने के लिए `MEDIABRAIN_DB` सेट करें या `--db` का उपयोग करें। + +Windows पर डबल-क्लिक लॉन्चर: + +```bat +START.bat +``` + +हल्का Windows लॉन्चर इस कमांड से बनाया जा सकता है: + +```bat +build_exe.bat +``` + +## परीक्षण + +```bash +python -m pytest tests/ -q +``` + +टेस्ट सूट डेटाबेस मैनेजर, मेटाडेटा, टैग, QueryBuilder, प्लेलिस्ट और offscreen मोड में प्लेलिस्ट GUI व्यवहार को कवर करता है। + +समर्पित source-platform smoke test Unicode पथ, कॉन्फ़िगरेशन, JSON आदान-प्रदान, ऑपरेटिंग सिस्टम के फ़ाइल ओपनर और offscreen विंडो/ट्रे जीवनचक्र की जाँच करता है: + +```bash +python -m pytest tests/source_platform_smoke.py -q +``` + +GitHub Actions यह सीमित smoke test Ubuntu और macOS पर चलाता है। यह सोर्स और offscreen GUI अनुबंध की जाँच है; यह नेटिव पैकेजिंग, डेस्कटॉप सत्र में ट्रे व्यवहार या किसी भी प्लेटफ़ॉर्म पर मैन्युअल स्वीकृति का प्रमाण नहीं है। + +## गोपनीयता + +MediaBrain उपयोग डेटा को स्थानीय SQLite डेटाबेस और कॉन्फ़िगरेशन फ़ाइलों में रखता है। इसमें टेलीमेट्री या स्वचालित क्लाउड सिंक नहीं है। वैकल्पिक मेटाडेटा अनुरोध केवल उपयोगकर्ता द्वारा कॉन्फ़िगर की गई TMDb/OMDb API कुंजियों या सार्वजनिक MusicBrainz डेटा का उपयोग करते हैं। विवरण: [PRIVACY_POLICY.md](PRIVACY_POLICY.md)। + +## लाइसेंस + +MIT लाइसेंस; [LICENSE](LICENSE) देखें। सीधे उपयोग की जाने वाली तृतीय-पक्ष रनटाइम और डेवलपमेंट निर्भरताओं के लाइसेंस [THIRD_PARTY_LICENSES.txt](THIRD_PARTY_LICENSES.txt) में सूचीबद्ध हैं। GUI का PySide6 LGPL की शर्तों के अधीन है। + +## दायित्व + +यह परियोजना निःशुल्क और “जैसी है” वैसी ही प्रदान की जाती है; इसकी कोई वारंटी, रखरखाव की गारंटी या किसी विशेष उद्देश्य के लिए उपयुक्तता की गारंटी नहीं है। इसका उपयोग अपने जोखिम पर करें। diff --git a/README_ja.md b/README_ja.md index a1da0e5..de83c7e 100644 --- a/README_ja.md +++ b/README_ja.md @@ -1,3 +1,4 @@ + MediaBrain バナー # MediaBrain @@ -17,7 +18,7 @@ > [!NOTE] > **AI / エージェント向けコンテキスト**: リポジトリのアーキテクチャ、ファイル索引、検索フレーズ、システム境界の機械可読情報は [llms.txt](llms.txt) を参照してください。 -[English](README.md) | [Deutsch](README_de.md) | **日本語** | [プライバシー](PRIVACY_POLICY.md) | [コントリビューション](CONTRIBUTING.md) | [変更履歴](CHANGELOG.md) +[English](README.md) | [Deutsch](README_de.md) | [Español](README_es.md) | [简体中文](README_zh.md) | **日本語** | [Русский](README_ru.md) | [Français](README_fr.md) | [हिन्दी](README_hi.md) | [العربية](README_ar.md) | [বাংলা](README_bn.md) | [Português](README_pt.md) | [プライバシー](PRIVACY_POLICY.md) | [コントリビューション](CONTRIBUTING.md) | [変更履歴](CHANGELOG.md) ![MediaBrain のメインウィンドウ](README/screenshots/main.png) @@ -31,6 +32,11 @@ | 简体中文 | [README_zh.md](README_zh.md) | | 日本語 | [README_ja.md](README_ja.md) | | Русский | [README_ru.md](README_ru.md) | +| Français | [README_fr.md](README_fr.md) | +| हिन्दी | [README_hi.md](README_hi.md) | +| العربية | [README_ar.md](README_ar.md) | +| বাংলা | [README_bn.md](README_bn.md) | +| Português | [README_pt.md](README_pt.md) | この一覧は README の文書版のみを示します。アプリの実行時の言語対応を示すものではありません。 diff --git a/README_pt.md b/README_pt.md new file mode 100644 index 0000000..1d51dd5 --- /dev/null +++ b/README_pt.md @@ -0,0 +1,176 @@ + +Faixa do MediaBrain + +# MediaBrain + +[![Versão: 2.1.0](https://img.shields.io/badge/version-2.1.0-orange.svg)](version.py) +[![Licença: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) +[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/) +[![Plataforma: Windows](https://img.shields.io/badge/platform-Windows-lightgrey.svg)]() +[![Prioridade ao funcionamento offline: sim](https://img.shields.io/badge/offline--first-yes-brightgreen.svg)]() +[![Testes: 274 aprovados](https://img.shields.io/badge/tests-274%20passed-brightgreen.svg)]() +[![doc-bricks](https://img.shields.io/badge/doc--bricks-ecosystem-blue.svg)](https://github.com/doc-bricks) +[![open-bricks](https://img.shields.io/badge/open--bricks-umbrella-purple.svg)](https://github.com/open-bricks) +[![Contexto LLM](https://img.shields.io/badge/LLM--Context-llms.txt-blue.svg)](llms.txt) + +> **MediaBrain é um gestor de biblioteca multimédia baseado em PySide6 que privilegia o armazenamento local e ajuda a organizar vídeos, música, listas de reprodução pessoais e o histórico de visualização em serviços de streaming.** + +> [!NOTE] +> **Contexto para IA e agentes**: a arquitetura do repositório, os índices de ficheiros, as expressões de pesquisa e os limites do sistema estão descritos em formato legível por máquina em [llms.txt](llms.txt). + +[English](README.md) | [Deutsch](README_de.md) | [Español](README_es.md) | [简体中文](README_zh.md) | [日本語](README_ja.md) | [Русский](README_ru.md) | [Français](README_fr.md) | [हिन्दी](README_hi.md) | [العربية](README_ar.md) | [বাংলা](README_bn.md) | **Português** | [Privacidade](PRIVACY_POLICY.md) | [Contribuir](CONTRIBUTING.md) | [Registo de alterações](CHANGELOG.md) + +![Janela principal do MediaBrain](README/screenshots/main.png) + +## Idiomas da documentação + +| Idioma | Documento | +|---|---| +| English | [README.md](README.md) | +| Deutsch | [README_de.md](README_de.md) | +| Español | [README_es.md](README_es.md) | +| 简体中文 | [README_zh.md](README_zh.md) | +| 日本語 | [README_ja.md](README_ja.md) | +| Русский | [README_ru.md](README_ru.md) | +| Français | [README_fr.md](README_fr.md) | +| हिन्दी | [README_hi.md](README_hi.md) | +| العربية | [README_ar.md](README_ar.md) | +| বাংলা | [README_bn.md](README_bn.md) | +| Português | [README_pt.md](README_pt.md) | + +Esta lista apresenta apenas as versões documentais do README. Não descreve os idiomas suportados pela aplicação em execução. + +## Comece aqui + +| Necessidade | Consulte | +|---|---| +| Executar a aplicação de ambiente de trabalho | `python MediaBrain.py` | +| Consultar a versão móvel Flutter | `flutter_port/README.md` | +| Verificar os limites de privacidade | [PRIVACY_POLICY.md](PRIVACY_POLICY.md) | +| Compreender a arquitetura | [ARCH.md](ARCH.md) | +| Consultar o trabalho planeado | [ROADMAP.md](ROADMAP.md) | +| Obter contexto legível por máquina | [llms.txt](llms.txt) | + +## Porquê o MediaBrain + +O MediaBrain destina-se a quem pretende manter num único local os conteúdos multimédia que vê, coleciona, etiqueta, coloca na lista de bloqueio ou organiza, entre serviços e ficheiros locais. Não é um serviço de streaming alojado e não carrega uma biblioteca privada para uma conta na nuvem. + +- Armazenamento SQLite local para entradas multimédia, favoritos, etiquetas, itens da lista de bloqueio e listas de reprodução +- Interface de ambiente de trabalho PySide6 com painel, bibliotecas, pesquisa, estatísticas, definições e temas de alto contraste +- Camada de fornecedores para Netflix, YouTube, Spotify, Disney+, Prime Video, Apple TV+, Twitch e ficheiros locais +- Listas de reprodução manuais e inteligentes baseadas em regras do QueryBuilder +- Pesquisa opcional de metadados através de chaves TMDb/OMDb configuradas pelo utilizador ou dados públicos do MusicBrainz +- Sem telemetria, sem rastreio e sem sincronização automática com a nuvem + +## Contexto de pesquisa + +Este repositório é o endereço canónico no GitHub para: + +- `doc-bricks MediaBrain` +- `MediaBrain media library manager` +- `private SQLite media library manager` +- `local-first PySide6 media manager` +- `PySide6 smart playlist media manager` +- `offline media hub with smart playlists` +- `desktop media history tracker` +- `privacy-first media history desktop app` + +O nome “MediaBrain” também é usado por empresas de análise de media, laboratórios de IA para media e ferramentas genéricas de planeamento de media. Este repositório refere-se especificamente à biblioteca multimédia de ambiente de trabalho com prioridade local `doc-bricks/MediaBrain`, e não a uma plataforma de análise publicitária na nuvem, a um laboratório de investigação multimédia ou a um gerador de media com IA. + +## Arquitetura + +```text +Core Layer Database, MediaManager, BlacklistManager, TagManager, PlaylistManager +Query Layer QueryBuilder for advanced filters and smart playlists +Provider Layer Netflix, YouTube, Spotify, Disney+, Prime, AppleTV+, Twitch, Local +Background Layer FileIndexer, WindowWatcher +GUI Layer Dashboard, Libraries, Favorites, Blacklist, Playlists, Search, Stats, Settings +``` + +Diagrama completo: [ARCH.md](ARCH.md) + +## Ecossistema e ferramentas relacionadas + +O MediaBrain faz parte do conjunto de documentação e produtividade [`doc-bricks`](https://github.com/doc-bricks), sob a alçada de [`open-bricks`](https://github.com/open-bricks): + +| Ferramenta | Foco | Repositório | +|---|---|---| +| **MediaBrain** | Histórico multimédia local, listas inteligentes e gestão de biblioteca | [doc-bricks/MediaBrain](https://github.com/doc-bricks/MediaBrain) | +| **CleanMarkdown** | Normalização tipográfica, limpeza de Markdown e exportação para PDF | [doc-bricks/CleanMarkdown](https://github.com/doc-bricks/CleanMarkdown) | +| **PDFtoPDFocr** | Fluxo OCR para PDFs pesquisáveis e extração de metadados | [doc-bricks/PDFtoPDFocr](https://github.com/doc-bricks/PDFtoPDFocr) | +| **llm-note** | Bloco de notas local SQLite e texto simples para notas pessoais ou criadas por agentes | [doc-bricks/llm-note](https://github.com/doc-bricks/llm-note) | +| **LaunchBoards** | Lançador visual, organizador de aplicações e painel de trabalho | [file-bricks/LaunchBoards](https://github.com/file-bricks/LaunchBoards) | +| **ProFiler** | Analisador e inspetor de ficheiros com foco na privacidade e deteção de duplicados | [file-bricks/ProFiler](https://github.com/file-bricks/ProFiler) | +| **KnowledgeDigest** | Base de conhecimento offline e compressor de documentação | [file-bricks/KnowledgeDigest](https://github.com/file-bricks/KnowledgeDigest) | +| **WinStorePackager** | Empacotamento MSIX, preparação de manifestos Store e gerador de recursos | [file-bricks/WinStorePackager](https://github.com/file-bricks/WinStorePackager) | +| **DevCenter** | Painel de desenvolvimento multi-repositório, monitor de CI e gestor de áreas de trabalho | [dev-bricks/DevCenter](https://github.com/dev-bricks/DevCenter) | +| **open-bricks** | Portal unificado para aplicações de ambiente de trabalho e ferramentas CLI | [open-bricks/open-bricks](https://github.com/open-bricks/open-bricks) | + +## Instalação + +Requisitos: + +- Python 3.10 ou superior +- Windows, Linux ou macOS com suporte para Qt/PySide6 + +```bash +pip install -r requirements.txt +``` + +No primeiro arranque, o MediaBrain cria localmente `settings.json`. O modelo público é [settings.example.json](settings.example.json). As definições pessoais, bases de dados, registos e artefactos de compilação são excluídos através de `.gitignore`. + +## Utilização + +```bash +python MediaBrain.py +``` + +Acesso de linha de comandos apenas de leitura (nunca escreve na base de dados): + +```bash +python cli.py list +python cli.py search "title" +python cli.py --db /path/to/media_brain.db types +``` + +A CLI oculta as entradas que estão na lista de bloqueio. Defina `MEDIABRAIN_DB` ou use `--db` para selecionar explicitamente uma base de dados. + +Lançador do Windows por duplo clique: + +```bat +START.bat +``` + +É possível criar um lançador leve para Windows com: + +```bat +build_exe.bat +``` + +## Testes + +```bash +python -m pytest tests/ -q +``` + +A suite de testes abrange os gestores de bases de dados, metadados, etiquetas, QueryBuilder, listas de reprodução e o comportamento da interface gráfica das listas em modo offscreen. + +O teste smoke dedicado à plataforma de origem verifica caminhos Unicode, configuração, troca de JSON, o programa de abertura de ficheiros do sistema operativo e o ciclo de vida da janela e do tabuleiro do sistema em modo offscreen: + +```bash +python -m pytest tests/source_platform_smoke.py -q +``` + +O GitHub Actions executa este teste smoke específico no Ubuntu e no macOS. É um contrato do código-fonte e da interface em modo offscreen; não comprova o empacotamento nativo, o funcionamento do tabuleiro numa sessão de ambiente de trabalho nem a aceitação manual em qualquer uma das plataformas. + +## Privacidade + +O MediaBrain guarda dados de utilização localmente em bases de dados SQLite e ficheiros de configuração. Não tem telemetria nem sincronização automática com a nuvem. Os pedidos opcionais de metadados usam apenas chaves de API TMDb/OMDb configuradas pelo utilizador ou dados públicos do MusicBrainz. Detalhes: [PRIVACY_POLICY.md](PRIVACY_POLICY.md). + +## Licença + +Licença MIT; consulte [LICENSE](LICENSE). As licenças das dependências diretas de terceiros para execução e desenvolvimento estão inventariadas em [THIRD_PARTY_LICENSES.txt](THIRD_PARTY_LICENSES.txt). A interface gráfica usa PySide6 ao abrigo dos termos da LGPL. + +## Responsabilidade + +Este projeto é fornecido gratuitamente e “tal como está”, sem garantia, compromisso de manutenção nem garantia de adequação a uma finalidade específica. Utilize-o por sua conta e risco. diff --git a/README_ru.md b/README_ru.md index c982bcb..9e700e2 100644 --- a/README_ru.md +++ b/README_ru.md @@ -1,3 +1,4 @@ + Баннер MediaBrain # MediaBrain @@ -17,7 +18,7 @@ > [!NOTE] > **Контекст для ИИ и агентов**: машиночитаемая архитектура репозитория, индексы файлов, поисковые фразы и границы системы описаны в [llms.txt](llms.txt). -[English](README.md) | [Deutsch](README_de.md) | **Русский** | [Конфиденциальность](PRIVACY_POLICY.md) | [Участие в проекте](CONTRIBUTING.md) | [История изменений](CHANGELOG.md) +[English](README.md) | [Deutsch](README_de.md) | [Español](README_es.md) | [简体中文](README_zh.md) | [日本語](README_ja.md) | **Русский** | [Français](README_fr.md) | [हिन्दी](README_hi.md) | [العربية](README_ar.md) | [বাংলা](README_bn.md) | [Português](README_pt.md) | [Конфиденциальность](PRIVACY_POLICY.md) | [Участие в проекте](CONTRIBUTING.md) | [История изменений](CHANGELOG.md) ![Главное окно MediaBrain](README/screenshots/main.png) @@ -31,6 +32,11 @@ | 简体中文 | [README_zh.md](README_zh.md) | | 日本語 | [README_ja.md](README_ja.md) | | Русский | [README_ru.md](README_ru.md) | +| Français | [README_fr.md](README_fr.md) | +| हिन्दी | [README_hi.md](README_hi.md) | +| العربية | [README_ar.md](README_ar.md) | +| বাংলা | [README_bn.md](README_bn.md) | +| Português | [README_pt.md](README_pt.md) | В таблице перечислены только языковые версии документации README. Она не описывает поддержку языков в приложении во время выполнения. diff --git a/README_zh.md b/README_zh.md index 5baba0f..6ca230e 100644 --- a/README_zh.md +++ b/README_zh.md @@ -1,3 +1,4 @@ + MediaBrain 横幅 # MediaBrain @@ -17,7 +18,7 @@ > [!NOTE] > **AI / 智能体上下文**:仓库架构、文件索引、搜索短语和系统边界的机器可读资料见 [llms.txt](llms.txt)。 -[English](README.md) | [Deutsch](README_de.md) | **简体中文** | [隐私政策](PRIVACY_POLICY.md) | [贡献指南](CONTRIBUTING.md) | [更新日志](CHANGELOG.md) +[English](README.md) | [Deutsch](README_de.md) | [Español](README_es.md) | **简体中文** | [日本語](README_ja.md) | [Русский](README_ru.md) | [Français](README_fr.md) | [हिन्दी](README_hi.md) | [العربية](README_ar.md) | [বাংলা](README_bn.md) | [Português](README_pt.md) | [隐私政策](PRIVACY_POLICY.md) | [贡献指南](CONTRIBUTING.md) | [更新日志](CHANGELOG.md) ![MediaBrain 主窗口](README/screenshots/main.png) @@ -31,6 +32,11 @@ | 简体中文 | [README_zh.md](README_zh.md) | | 日本語 | [README_ja.md](README_ja.md) | | Русский | [README_ru.md](README_ru.md) | +| Français | [README_fr.md](README_fr.md) | +| हिन्दी | [README_hi.md](README_hi.md) | +| العربية | [README_ar.md](README_ar.md) | +| বাংলা | [README_bn.md](README_bn.md) | +| Português | [README_pt.md](README_pt.md) | 此列表仅说明 README 文档版本,不代表应用运行时支持的语言。 From 87321cf28ad2d318f7897c1f01ff02d998745d48 Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Sun, 25 Oct 2026 05:32:23 +0100 Subject: [PATCH 4/9] feat: enable eleven-language Flutter localization with generated catalogs --- flutter_port/l10n.yaml | 8 + flutter_port/lib/l10n/app_ar.arb | 221 ++++ flutter_port/lib/l10n/app_bn.arb | 221 ++++ flutter_port/lib/l10n/app_de.arb | 138 ++- flutter_port/lib/l10n/app_en.arb | 138 ++- flutter_port/lib/l10n/app_es.arb | 221 ++++ flutter_port/lib/l10n/app_fr.arb | 221 ++++ flutter_port/lib/l10n/app_hi.arb | 221 ++++ flutter_port/lib/l10n/app_ja.arb | 221 ++++ flutter_port/lib/l10n/app_localizations.dart | 1076 ++++++++++++----- .../lib/l10n/app_localizations_ar.dart | 415 +++++++ .../lib/l10n/app_localizations_bn.dart | 413 +++++++ .../lib/l10n/app_localizations_de.dart | 412 +++++++ .../lib/l10n/app_localizations_en.dart | 413 +++++++ .../lib/l10n/app_localizations_es.dart | 413 +++++++ .../lib/l10n/app_localizations_fr.dart | 415 +++++++ .../lib/l10n/app_localizations_hi.dart | 412 +++++++ .../lib/l10n/app_localizations_ja.dart | 405 +++++++ .../lib/l10n/app_localizations_pt.dart | 414 +++++++ .../lib/l10n/app_localizations_ru.dart | 413 +++++++ .../lib/l10n/app_localizations_zh.dart | 404 +++++++ flutter_port/lib/l10n/app_pt.arb | 221 ++++ flutter_port/lib/l10n/app_ru.arb | 221 ++++ flutter_port/lib/l10n/app_zh.arb | 221 ++++ flutter_port/pubspec.yaml | 1 + flutter_port/test/l10n_test.dart | 97 +- 26 files changed, 7635 insertions(+), 341 deletions(-) create mode 100644 flutter_port/l10n.yaml create mode 100644 flutter_port/lib/l10n/app_ar.arb create mode 100644 flutter_port/lib/l10n/app_bn.arb create mode 100644 flutter_port/lib/l10n/app_es.arb create mode 100644 flutter_port/lib/l10n/app_fr.arb create mode 100644 flutter_port/lib/l10n/app_hi.arb create mode 100644 flutter_port/lib/l10n/app_ja.arb create mode 100644 flutter_port/lib/l10n/app_localizations_ar.dart create mode 100644 flutter_port/lib/l10n/app_localizations_bn.dart create mode 100644 flutter_port/lib/l10n/app_localizations_de.dart create mode 100644 flutter_port/lib/l10n/app_localizations_en.dart create mode 100644 flutter_port/lib/l10n/app_localizations_es.dart create mode 100644 flutter_port/lib/l10n/app_localizations_fr.dart create mode 100644 flutter_port/lib/l10n/app_localizations_hi.dart create mode 100644 flutter_port/lib/l10n/app_localizations_ja.dart create mode 100644 flutter_port/lib/l10n/app_localizations_pt.dart create mode 100644 flutter_port/lib/l10n/app_localizations_ru.dart create mode 100644 flutter_port/lib/l10n/app_localizations_zh.dart create mode 100644 flutter_port/lib/l10n/app_pt.arb create mode 100644 flutter_port/lib/l10n/app_ru.arb create mode 100644 flutter_port/lib/l10n/app_zh.arb diff --git a/flutter_port/l10n.yaml b/flutter_port/l10n.yaml new file mode 100644 index 0000000..eee3d5e --- /dev/null +++ b/flutter_port/l10n.yaml @@ -0,0 +1,8 @@ +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart +output-class: AppLocalizations +preferred-supported-locales: + - de +nullable-getter: false +use-named-parameters: false diff --git a/flutter_port/lib/l10n/app_ar.arb b/flutter_port/lib/l10n/app_ar.arb new file mode 100644 index 0000000..c771047 --- /dev/null +++ b/flutter_port/lib/l10n/app_ar.arb @@ -0,0 +1,221 @@ +{ + "@@locale": "ar", + "appTitle": "MediaBrain", + "navLibrary": "المكتبة", + "navScan": "فحص", + "navMore": "المزيد", + "screenLibrary": "المكتبة", + "fabAdd": "إضافة", + "searchHint": "بحث (العنوان، الفنان، القناة …)", + "filterAll": "الكل", + "filterFavorites": "⭐ المفضلة", + "emptyNoMatch": "لا توجد نتائج تطابق عوامل التصفية.", + "emptyNoItems": "لا توجد عناصر بعد.\nاضغط علامة تبويب الفحص لاكتشاف تطبيقات الوسائط تلقائيًا.", + "screenScan": "فحص", + "scanDescription": "يبحث عن تطبيقات الوسائط المثبتة (البث والموسيقى والبودكاست والمستندات) ويضيفها إلى مكتبتك. عند تفعيل الوصول إلى بيانات الاستخدام، ستحصل أيضًا على مدة الاستخدام الفعلية.", + "btnAppScan": "فحص التطبيقات (إجمالي الاستخدام)", + "bgScanTitle": "الفحص في الخلفية", + "bgScanNow": "التشغيل مرة واحدة في الخلفية الآن", + "bgScanNowSubtitle": "يرسل مهمة لمرة واحدة إلى Android. ستظهر النتيجة بعد دقيقة أو دقيقتين.", + "bgScanQueued": "وُضع الفحص في الخلفية في قائمة الانتظار — ستظهر النتيجة خلال دقيقة تقريبًا", + "scanAndroidOnly": "الفحص متاح على Android فقط.", + "scanPermissionMissing": "إذن الوصول إلى بيانات الاستخدام غير متاح. فعّله في إعدادات النظام لتطبيق MediaBrain:\n\nالإعدادات → التطبيقات → الوصول الخاص للتطبيقات → الوصول إلى بيانات الاستخدام → MediaBrain.", + "detectedTitle": "📡 ما الذي يتم اكتشافه؟", + "detectedApps": "Netflix · Disney+ · Prime Video · Apple TV · YouTube · Twitch · Spotify · Apple Music · YouTube Music · Tidal · Amazon Music · Audible · Kindle · Pocket Casts · Google Podcasts · Adobe Acrobat · Google Docs · Notion · Obsidian وغيرها.", + "appScanTitle": "🔍 فحص التطبيقات", + "appScanDescription": "فحص التطبيقات: سجل واحد لكل تطبيق وسائط مع إجمالي الاستخدام خلال الأيام الماضية (وفق إحصاءات استخدام Android).", + "neverText": "أبدًا", + "screenSettings": "الإعدادات", + "sectionDataStatus": "حالة البيانات", + "sectionFileSync": "مزامنة الملفات", + "sectionServerSync": "مزامنة الخادم (Mac Studio)", + "sectionActions": "الإجراءات", + "settingsTotalEntries": "إجمالي السجلات", + "settingsLastScan": "آخر فحص", + "exportTitle": "تصدير عبر المشاركة", + "exportSubtitle": "إرسال JSON إلى نفسك عبر البريد أو OneDrive أو Telegram …", + "importTitle": "اختيار ملف للاستيراد", + "importSubtitle": "يستبدل البيانات المحلية", + "noServerConfigured": "لا يوجد خادم مهيأ", + "actionPush": "إرسال", + "actionPull": "استقبال", + "refreshData": "تحديث البيانات", + "deleteAllData": "حذف جميع بيانات المكتبة", + "deleteAllTitle": "حذف جميع بيانات المكتبة؟", + "deleteAllContent": "يحذف جميع السجلات من MediaBrain المحلي.", + "cancel": "إلغاء", + "delete": "حذف", + "save": "حفظ", + "libraryCleared": "تم إفراغ المكتبة.", + "cancelled": "تم الإلغاء.", + "serverConfigTitle": "تهيئة مزامنة الخادم", + "fieldServerUrl": "عنوان الخادم", + "fieldServerUrlHint": "مثال: macstudvonlukas:8082", + "fieldBearerToken": "رمز Bearer", + "aboutTitle": "MediaBrain Mobile", + "aboutSubtitle": "تطبيق Flutter مستقل. يكتشف تطبيقات الوسائط المثبتة، ويتتبع الإدخالات اليدوية، ويزامن عبر ملف أو خادم.", + "detailDescription": "الوصف", + "detailDetails": "التفاصيل", + "detailTags": "الوسوم", + "fieldArtist": "الفنان", + "fieldAlbum": "الألبوم", + "fieldChannel": "القناة", + "fieldSeasonEpisodeLabel": "الموسم/الحلقة", + "fieldLength": "المدة", + "fieldLastOpened": "آخر فتح", + "fieldUsage": "الاستخدام", + "fieldProviderId": "معرّف المزوّد", + "fieldLocalPath": "المسار المحلي", + "openPlayStore": "فتح في Play Store", + "openBrowser": "فتح في المتصفح", + "linkError": "تعذر فتح الرابط.", + "dialogTitleNew": "إدخال جديد", + "dialogTitleEdit": "تعديل الإدخال", + "fieldTitle": "العنوان", + "categoryLabel": "الفئة", + "fieldArtistOptional": "الفنان / المخرج (اختياري)", + "fieldAlbumOptional": "الألبوم / المسلسل (اختياري)", + "fieldChannelOptional": "القناة / المصدر (اختياري)", + "fieldDescriptionOptional": "الوصف (اختياري)", + "fieldTags": "الوسوم (مفصولة بفواصل)", + "fieldTagsHint": "مثال: وثائقي، عطلة نهاية الأسبوع", + "markAsFavorite": "وضع علامة في المفضلة", + "error": "خطأ: {msg}", + "@error": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "foregroundMinutesLabel": "الإجمالي {minutes} دقيقة", + "@foregroundMinutesLabel": { + "placeholders": { + "minutes": { + "type": "int" + } + } + }, + "bgScanSubtitle": "افحص كل نحو {hours} ساعات، حتى عندما يكون التطبيق مغلقًا.", + "@bgScanSubtitle": { + "placeholders": { + "hours": { + "type": "int" + } + } + }, + "lastRun": "آخر تشغيل: {time}", + "@lastRun": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "scanResult": "فحص التطبيقات: تم العثور على {matched} من تطبيقات الوسائط وإضافة {persisted} من السجلات.", + "@scanResult": { + "placeholders": { + "matched": { + "type": "int" + }, + "persisted": { + "type": "int" + } + } + }, + "exportResult": "تصدير: {msg}", + "@exportResult": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "importResult": "استيراد: {msg}", + "@importResult": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "pushPullStatus": "إرسال: {push} · استقبال: {pull}", + "@pushPullStatus": { + "placeholders": { + "push": { + "type": "String" + }, + "pull": { + "type": "String" + } + } + }, + "seasonEpisode": "الموسم {s} الحلقة {e}", + "@seasonEpisode": { + "placeholders": { + "s": { + "type": "int" + }, + "e": { + "type": "int" + } + } + }, + "usageMinutes": "{n} دقيقة", + "@usageMinutes": { + "placeholders": { + "n": { + "type": "int" + } + } + }, + "navPlaylists": "قوائم التشغيل", + "screenPlaylists": "قوائم التشغيل والمجموعات", + "playlistTypeManual": "يدوية", + "playlistTypeSmart": "ذكية (تصفية ديناميكية)", + "dialogNewPlaylist": "قائمة تشغيل جديدة", + "dialogEditPlaylist": "تعديل قائمة التشغيل", + "fieldPlaylistName": "اسم قائمة التشغيل", + "fieldPlaylistDescription": "الوصف (اختياري)", + "fieldPlaylistType": "نوع قائمة التشغيل", + "smartFilterCriteria": "معايير التصفية الذكية", + "smartFilterCategory": "الفئة", + "smartFilterTag": "وسم / كلمة مفتاحية", + "smartFilterFavoritesOnly": "المفضلة فقط", + "smartFilterMinMinutes": "الحد الأدنى لوقت الاستخدام (دقيقة)", + "smartFilterSearch": "مصطلح البحث", + "emptyPlaylists": "لم تُنشأ قوائم تشغيل بعد.\nأنشئ مجموعات يدوية أو قوائم تشغيل ذكية ديناميكية.", + "emptyPlaylistItems": "لا تحتوي قائمة التشغيل هذه على وسائط بعد.", + "deletePlaylistTitle": "حذف قائمة التشغيل؟", + "deletePlaylistContent": "هل تريد حقًا حذف قائمة التشغيل هذه؟ ستبقى عناصر الوسائط فيها في مكتبتك.", + "addToPlaylist": "إضافة إلى قائمة التشغيل", + "removeFromPlaylist": "إزالة من قائمة التشغيل", + "addedToPlaylist": "تمت الإضافة إلى قائمة التشغيل.", + "removedFromPlaylist": "تمت الإزالة من قائمة التشغيل.", + "filterByTag": "تصفية حسب الوسم", + "allTags": "كل الوسوم", + "addTag": "إضافة وسم", + "removeTag": "إزالة الوسم", + "noPlaylistsFound": "لا توجد قوائم تشغيل متاحة.", + "playlistItemCount": "{count, plural, zero {{count} عنصر} one {{count} عنصر} two {{count} عنصران} few {{count} عناصر} many {{count} عنصرًا} other {{count} عنصر}}", + "tagCount": "#{tag} ({count})", + "@playlistItemCount": { + "description": "Number of items in a playlist.", + "placeholders": { + "count": { + "type": "num" + } + } + }, + "@tagCount": { + "description": "Tag and number of matching entries.", + "placeholders": { + "tag": { + "type": "String" + }, + "count": { + "type": "int" + } + } + } +} diff --git a/flutter_port/lib/l10n/app_bn.arb b/flutter_port/lib/l10n/app_bn.arb new file mode 100644 index 0000000..21d0b24 --- /dev/null +++ b/flutter_port/lib/l10n/app_bn.arb @@ -0,0 +1,221 @@ +{ + "@@locale": "bn", + "appTitle": "MediaBrain", + "navLibrary": "লাইব্রেরি", + "navScan": "স্ক্যান", + "navMore": "আরও", + "screenLibrary": "লাইব্রেরি", + "fabAdd": "যোগ করুন", + "searchHint": "খুঁজুন (শিরোনাম, শিল্পী, চ্যানেল …)", + "filterAll": "সব", + "filterFavorites": "⭐ পছন্দের", + "emptyNoMatch": "আপনার ফিল্টারের সঙ্গে মেলে এমন কিছু পাওয়া যায়নি।", + "emptyNoItems": "এখনও খালি।\nমিডিয়া অ্যাপ স্বয়ংক্রিয়ভাবে শনাক্ত করতে স্ক্যান ট্যাবে ট্যাপ করুন।", + "screenScan": "স্ক্যান", + "scanDescription": "ইনস্টল করা মিডিয়া অ্যাপ (স্ট্রিমিং, সংগীত, পডকাস্ট, নথি) খুঁজে সেগুলো লাইব্রেরিতে যোগ করে। ব্যবহার-ডেটা অ্যাক্সেস চালু থাকলে প্রকৃত ব্যবহারের সময়ও পাওয়া যাবে।", + "btnAppScan": "অ্যাপ স্ক্যান (সমষ্টিগত ব্যবহার)", + "bgScanTitle": "ব্যাকগ্রাউন্ড স্ক্যান", + "bgScanNow": "এখন একবার ব্যাকগ্রাউন্ডে চালান", + "bgScanNowSubtitle": "Android-এ একবারের জন্য একটি কাজ পাঠায়। ১–২ মিনিট পরে ফলাফল দেখা যাবে।", + "bgScanQueued": "ব্যাকগ্রাউন্ড স্ক্যান সারিতে আছে — প্রায় ১ মিনিটে ফলাফল দেখা যাবে", + "scanAndroidOnly": "স্ক্যান শুধু Android-এ উপলভ্য।", + "scanPermissionMissing": "ব্যবহার-ডেটা অ্যাক্সেস নেই। MediaBrain-এর জন্য সিস্টেম সেটিংসে এটি চালু করুন:\n\nসেটিংস → অ্যাপস → বিশেষ অ্যাপ অ্যাক্সেস → ব্যবহার-ডেটা অ্যাক্সেস → MediaBrain।", + "detectedTitle": "📡 কী শনাক্ত হয়?", + "detectedApps": "Netflix · Disney+ · Prime Video · Apple TV · YouTube · Twitch · Spotify · Apple Music · YouTube Music · Tidal · Amazon Music · Audible · Kindle · Pocket Casts · Google Podcasts · Adobe Acrobat · Google Docs · Notion · Obsidian এবং আরও।", + "appScanTitle": "🔍 অ্যাপ স্ক্যান", + "appScanDescription": "অ্যাপ স্ক্যান: প্রতিটি মিডিয়া অ্যাপের জন্য একটি এন্ট্রি, যেখানে সাম্প্রতিক দিনগুলোর মোট ব্যবহার দেখানো হয় (Android ব্যবহারের পরিসংখ্যান থেকে)।", + "neverText": "কখনও নয়", + "screenSettings": "সেটিংস", + "sectionDataStatus": "ডেটার অবস্থা", + "sectionFileSync": "ফাইল সিঙ্ক", + "sectionServerSync": "সার্ভার সিঙ্ক (Mac Studio)", + "sectionActions": "কাজ", + "settingsTotalEntries": "মোট এন্ট্রি", + "settingsLastScan": "শেষ স্ক্যান", + "exportTitle": "শেয়ার দিয়ে রপ্তানি", + "exportSubtitle": "ইমেল, OneDrive বা Telegram দিয়ে নিজের কাছে JSON পাঠান …", + "importTitle": "আমদানি ফাইল বেছে নিন", + "importSubtitle": "স্থানীয় ডেটা মুছে প্রতিস্থাপন করে", + "noServerConfigured": "কোনো সার্ভার কনফিগার করা নেই", + "actionPush": "পুশ", + "actionPull": "পুল", + "refreshData": "ডেটা রিফ্রেশ করুন", + "deleteAllData": "লাইব্রেরির সব ডেটা মুছুন", + "deleteAllTitle": "লাইব্রেরির সব ডেটা মুছবেন?", + "deleteAllContent": "স্থানীয় MediaBrain থেকে সব এন্ট্রি সরিয়ে দেয়।", + "cancel": "বাতিল", + "delete": "মুছুন", + "save": "সংরক্ষণ", + "libraryCleared": "লাইব্রেরি খালি করা হয়েছে।", + "cancelled": "বাতিল হয়েছে।", + "serverConfigTitle": "সার্ভার সিঙ্ক কনফিগার করুন", + "fieldServerUrl": "সার্ভার URL", + "fieldServerUrlHint": "যেমন: macstudvonlukas:8082", + "fieldBearerToken": "Bearer টোকেন", + "aboutTitle": "MediaBrain Mobile", + "aboutSubtitle": "স্বতন্ত্র Flutter অ্যাপ। ইনস্টল করা মিডিয়া অ্যাপ শনাক্ত করে, হাতে যোগ করা এন্ট্রি রাখে এবং ফাইল বা সার্ভারের মাধ্যমে সিঙ্ক করে।", + "detailDescription": "বিবরণ", + "detailDetails": "বিস্তারিত", + "detailTags": "ট্যাগ", + "fieldArtist": "শিল্পী", + "fieldAlbum": "অ্যালবাম", + "fieldChannel": "চ্যানেল", + "fieldSeasonEpisodeLabel": "সিজন/পর্ব", + "fieldLength": "সময়কাল", + "fieldLastOpened": "শেষ খোলা", + "fieldUsage": "ব্যবহার", + "fieldProviderId": "প্রোভাইডার ID", + "fieldLocalPath": "স্থানীয় পথ", + "openPlayStore": "Play Store-এ খুলুন", + "openBrowser": "ব্রাউজারে খুলুন", + "linkError": "লিংক খোলা যায়নি।", + "dialogTitleNew": "নতুন এন্ট্রি", + "dialogTitleEdit": "এন্ট্রি সম্পাদনা করুন", + "fieldTitle": "শিরোনাম", + "categoryLabel": "বিভাগ", + "fieldArtistOptional": "শিল্পী / পরিচালক (ঐচ্ছিক)", + "fieldAlbumOptional": "অ্যালবাম / সিরিজ (ঐচ্ছিক)", + "fieldChannelOptional": "চ্যানেল / উৎস (ঐচ্ছিক)", + "fieldDescriptionOptional": "বিবরণ (ঐচ্ছিক)", + "fieldTags": "ট্যাগ (কমা দিয়ে আলাদা)", + "fieldTagsHint": "যেমন: প্রামাণ্যচিত্র, ছুটির দিন", + "markAsFavorite": "পছন্দের হিসেবে চিহ্নিত করুন", + "error": "ত্রুটি: {msg}", + "@error": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "foregroundMinutesLabel": "মোট {minutes} মিনিট", + "@foregroundMinutesLabel": { + "placeholders": { + "minutes": { + "type": "int" + } + } + }, + "bgScanSubtitle": "অ্যাপ বন্ধ থাকলেও প্রতি ~{hours} ঘণ্টায় স্ক্যান করুন।", + "@bgScanSubtitle": { + "placeholders": { + "hours": { + "type": "int" + } + } + }, + "lastRun": "শেষ চালানো: {time}", + "@lastRun": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "scanResult": "অ্যাপ স্ক্যান: {matched}টি মিডিয়া অ্যাপ পাওয়া গেছে, {persisted}টি এন্ট্রি যোগ হয়েছে।", + "@scanResult": { + "placeholders": { + "matched": { + "type": "int" + }, + "persisted": { + "type": "int" + } + } + }, + "exportResult": "রপ্তানি: {msg}", + "@exportResult": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "importResult": "আমদানি: {msg}", + "@importResult": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "pushPullStatus": "পুশ: {push} · পুল: {pull}", + "@pushPullStatus": { + "placeholders": { + "push": { + "type": "String" + }, + "pull": { + "type": "String" + } + } + }, + "seasonEpisode": "S{s} E{e}", + "@seasonEpisode": { + "placeholders": { + "s": { + "type": "int" + }, + "e": { + "type": "int" + } + } + }, + "usageMinutes": "{n} মিনিট", + "@usageMinutes": { + "placeholders": { + "n": { + "type": "int" + } + } + }, + "navPlaylists": "প্লেলিস্ট", + "screenPlaylists": "প্লেলিস্ট ও সংগ্রহ", + "playlistTypeManual": "ম্যানুয়াল", + "playlistTypeSmart": "স্মার্ট (ডায়নামিক ফিল্টার)", + "dialogNewPlaylist": "নতুন প্লেলিস্ট", + "dialogEditPlaylist": "প্লেলিস্ট সম্পাদনা করুন", + "fieldPlaylistName": "প্লেলিস্টের নাম", + "fieldPlaylistDescription": "বিবরণ (ঐচ্ছিক)", + "fieldPlaylistType": "প্লেলিস্টের ধরন", + "smartFilterCriteria": "স্মার্ট ফিল্টারের মানদণ্ড", + "smartFilterCategory": "বিভাগ", + "smartFilterTag": "ট্যাগ / কীওয়ার্ড", + "smartFilterFavoritesOnly": "শুধু পছন্দের", + "smartFilterMinMinutes": "সর্বনিম্ন ব্যবহারের সময় (মিনিট)", + "smartFilterSearch": "অনুসন্ধানের শব্দ", + "emptyPlaylists": "এখনও কোনো প্লেলিস্ট তৈরি হয়নি।\nম্যানুয়াল সংগ্রহ বা ডায়নামিক স্মার্ট প্লেলিস্ট তৈরি করুন।", + "emptyPlaylistItems": "এই প্লেলিস্টে এখনও কোনো মিডিয়া নেই।", + "deletePlaylistTitle": "প্লেলিস্ট মুছবেন?", + "deletePlaylistContent": "আপনি কি সত্যিই এই প্লেলিস্টটি মুছতে চান? এর মিডিয়া লাইব্রেরিতে থেকে যাবে।", + "addToPlaylist": "প্লেলিস্টে যোগ করুন", + "removeFromPlaylist": "প্লেলিস্ট থেকে সরান", + "addedToPlaylist": "প্লেলিস্টে যোগ করা হয়েছে।", + "removedFromPlaylist": "প্লেলিস্ট থেকে সরানো হয়েছে।", + "filterByTag": "ট্যাগ ফিল্টার", + "allTags": "সব ট্যাগ", + "addTag": "ট্যাগ যোগ করুন", + "removeTag": "ট্যাগ সরান", + "noPlaylistsFound": "কোনো প্লেলিস্ট নেই।", + "playlistItemCount": "{count, plural, one {{count}টি আইটেম} other {{count}টি আইটেম}}", + "tagCount": "#{tag} ({count})", + "@playlistItemCount": { + "description": "Number of items in a playlist.", + "placeholders": { + "count": { + "type": "num" + } + } + }, + "@tagCount": { + "description": "Tag and number of matching entries.", + "placeholders": { + "tag": { + "type": "String" + }, + "count": { + "type": "int" + } + } + } +} diff --git a/flutter_port/lib/l10n/app_de.arb b/flutter_port/lib/l10n/app_de.arb index a20e3f5..213b36c 100644 --- a/flutter_port/lib/l10n/app_de.arb +++ b/flutter_port/lib/l10n/app_de.arb @@ -81,23 +81,141 @@ "fieldTagsHint": "z.B. Dokumentation, Wochenende", "markAsFavorite": "Als Favorit markieren", "error": "Fehler: {msg}", - "@error": { "placeholders": { "msg": { "type": "String" } } }, + "@error": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, "foregroundMinutesLabel": "{minutes} Min insgesamt", - "@foregroundMinutesLabel": { "placeholders": { "minutes": { "type": "int" } } }, + "@foregroundMinutesLabel": { + "placeholders": { + "minutes": { + "type": "int" + } + } + }, "bgScanSubtitle": "Alle ~{hours} h scannen, auch wenn die App geschlossen ist.", - "@bgScanSubtitle": { "placeholders": { "hours": { "type": "int" } } }, + "@bgScanSubtitle": { + "placeholders": { + "hours": { + "type": "int" + } + } + }, "lastRun": "Letzter Lauf: {time}", - "@lastRun": { "placeholders": { "time": { "type": "String" } } }, + "@lastRun": { + "placeholders": { + "time": { + "type": "String" + } + } + }, "scanResult": "Apps-Scan: {matched} Medien-Apps gefunden, {persisted} Einträge übernommen.", - "@scanResult": { "placeholders": { "matched": { "type": "int" }, "persisted": { "type": "int" } } }, + "@scanResult": { + "placeholders": { + "matched": { + "type": "int" + }, + "persisted": { + "type": "int" + } + } + }, "exportResult": "Export: {msg}", - "@exportResult": { "placeholders": { "msg": { "type": "String" } } }, + "@exportResult": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, "importResult": "Import: {msg}", - "@importResult": { "placeholders": { "msg": { "type": "String" } } }, + "@importResult": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, "pushPullStatus": "Push: {push} · Pull: {pull}", - "@pushPullStatus": { "placeholders": { "push": { "type": "String" }, "pull": { "type": "String" } } }, + "@pushPullStatus": { + "placeholders": { + "push": { + "type": "String" + }, + "pull": { + "type": "String" + } + } + }, "seasonEpisode": "S{s} E{e}", - "@seasonEpisode": { "placeholders": { "s": { "type": "int" }, "e": { "type": "int" } } }, + "@seasonEpisode": { + "placeholders": { + "s": { + "type": "int" + }, + "e": { + "type": "int" + } + } + }, "usageMinutes": "{n} Minuten", - "@usageMinutes": { "placeholders": { "n": { "type": "int" } } } + "@usageMinutes": { + "placeholders": { + "n": { + "type": "int" + } + } + }, + "navPlaylists": "Playlists", + "screenPlaylists": "Playlists & Sammlungen", + "playlistTypeManual": "Manuell", + "playlistTypeSmart": "Smart (Dynamischer Filter)", + "dialogNewPlaylist": "Neue Playlist", + "dialogEditPlaylist": "Playlist bearbeiten", + "fieldPlaylistName": "Name der Playlist", + "fieldPlaylistDescription": "Beschreibung (optional)", + "fieldPlaylistType": "Playlist-Typ", + "smartFilterCriteria": "Smarte Filterkriterien", + "smartFilterCategory": "Kategorie", + "smartFilterTag": "Tag / Stichwort", + "smartFilterFavoritesOnly": "Nur Favoriten", + "smartFilterMinMinutes": "Minimale Nutzungszeit (Min.)", + "smartFilterSearch": "Suchbegriff", + "emptyPlaylists": "Noch keine Playlists angelegt.\nErstelle manuelle Sammlungen oder dynamische Smart-Playlists.", + "emptyPlaylistItems": "Diese Playlist enthält noch keine Medien.", + "deletePlaylistTitle": "Playlist löschen?", + "deletePlaylistContent": "Möchtest du diese Playlist wirklich löschen? Die enthaltenen Medien bleiben in der Bibliothek erhalten.", + "addToPlaylist": "Zu Playlist hinzufügen", + "removeFromPlaylist": "Aus Playlist entfernen", + "addedToPlaylist": "Zur Playlist hinzugefügt.", + "removedFromPlaylist": "Aus Playlist entfernt.", + "filterByTag": "Tag-Filter", + "allTags": "Alle Tags", + "addTag": "Tag hinzufügen", + "removeTag": "Tag entfernen", + "noPlaylistsFound": "Keine Playlists vorhanden.", + "playlistItemCount": "{count, plural, one {{count} Eintrag} other {{count} Einträge}}", + "tagCount": "#{tag} ({count})", + "@playlistItemCount": { + "description": "Anzahl der Einträge in einer Playlist.", + "placeholders": { + "count": { + "type": "num" + } + } + }, + "@tagCount": { + "description": "Tag und Anzahl der passenden Einträge.", + "placeholders": { + "tag": { + "type": "String" + }, + "count": { + "type": "int" + } + } + } } diff --git a/flutter_port/lib/l10n/app_en.arb b/flutter_port/lib/l10n/app_en.arb index 0d8893f..1fa6a79 100644 --- a/flutter_port/lib/l10n/app_en.arb +++ b/flutter_port/lib/l10n/app_en.arb @@ -81,23 +81,141 @@ "fieldTagsHint": "e.g. Documentary, Weekend", "markAsFavorite": "Mark as favorite", "error": "Error: {msg}", - "@error": { "placeholders": { "msg": { "type": "String" } } }, + "@error": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, "foregroundMinutesLabel": "{minutes} min total", - "@foregroundMinutesLabel": { "placeholders": { "minutes": { "type": "int" } } }, + "@foregroundMinutesLabel": { + "placeholders": { + "minutes": { + "type": "int" + } + } + }, "bgScanSubtitle": "Scan every ~{hours} h, even when the app is closed.", - "@bgScanSubtitle": { "placeholders": { "hours": { "type": "int" } } }, + "@bgScanSubtitle": { + "placeholders": { + "hours": { + "type": "int" + } + } + }, "lastRun": "Last run: {time}", - "@lastRun": { "placeholders": { "time": { "type": "String" } } }, + "@lastRun": { + "placeholders": { + "time": { + "type": "String" + } + } + }, "scanResult": "App scan: {matched} media apps found, {persisted} entries added.", - "@scanResult": { "placeholders": { "matched": { "type": "int" }, "persisted": { "type": "int" } } }, + "@scanResult": { + "placeholders": { + "matched": { + "type": "int" + }, + "persisted": { + "type": "int" + } + } + }, "exportResult": "Export: {msg}", - "@exportResult": { "placeholders": { "msg": { "type": "String" } } }, + "@exportResult": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, "importResult": "Import: {msg}", - "@importResult": { "placeholders": { "msg": { "type": "String" } } }, + "@importResult": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, "pushPullStatus": "Push: {push} · Pull: {pull}", - "@pushPullStatus": { "placeholders": { "push": { "type": "String" }, "pull": { "type": "String" } } }, + "@pushPullStatus": { + "placeholders": { + "push": { + "type": "String" + }, + "pull": { + "type": "String" + } + } + }, "seasonEpisode": "S{s} E{e}", - "@seasonEpisode": { "placeholders": { "s": { "type": "int" }, "e": { "type": "int" } } }, + "@seasonEpisode": { + "placeholders": { + "s": { + "type": "int" + }, + "e": { + "type": "int" + } + } + }, "usageMinutes": "{n} minutes", - "@usageMinutes": { "placeholders": { "n": { "type": "int" } } } + "@usageMinutes": { + "placeholders": { + "n": { + "type": "int" + } + } + }, + "navPlaylists": "Playlists", + "screenPlaylists": "Playlists & Collections", + "playlistTypeManual": "Manual", + "playlistTypeSmart": "Smart (Dynamic Filter)", + "dialogNewPlaylist": "New Playlist", + "dialogEditPlaylist": "Edit Playlist", + "fieldPlaylistName": "Playlist Name", + "fieldPlaylistDescription": "Description (optional)", + "fieldPlaylistType": "Playlist Type", + "smartFilterCriteria": "Smart Filter Criteria", + "smartFilterCategory": "Category", + "smartFilterTag": "Tag / Keyword", + "smartFilterFavoritesOnly": "Favorites only", + "smartFilterMinMinutes": "Minimum usage time (min)", + "smartFilterSearch": "Search term", + "emptyPlaylists": "No playlists created yet.\nCreate manual collections or dynamic smart playlists.", + "emptyPlaylistItems": "This playlist does not contain any media yet.", + "deletePlaylistTitle": "Delete playlist?", + "deletePlaylistContent": "Do you really want to delete this playlist? The media items will remain in your library.", + "addToPlaylist": "Add to playlist", + "removeFromPlaylist": "Remove from playlist", + "addedToPlaylist": "Added to playlist.", + "removedFromPlaylist": "Removed from playlist.", + "filterByTag": "Tag Filter", + "allTags": "All Tags", + "addTag": "Add tag", + "removeTag": "Remove tag", + "noPlaylistsFound": "No playlists available.", + "playlistItemCount": "{count, plural, one {{count} item} other {{count} items}}", + "tagCount": "#{tag} ({count})", + "@playlistItemCount": { + "description": "Number of items in a playlist.", + "placeholders": { + "count": { + "type": "num" + } + } + }, + "@tagCount": { + "description": "Tag and number of matching entries.", + "placeholders": { + "tag": { + "type": "String" + }, + "count": { + "type": "int" + } + } + } } diff --git a/flutter_port/lib/l10n/app_es.arb b/flutter_port/lib/l10n/app_es.arb new file mode 100644 index 0000000..01f70d3 --- /dev/null +++ b/flutter_port/lib/l10n/app_es.arb @@ -0,0 +1,221 @@ +{ + "@@locale": "es", + "appTitle": "MediaBrain", + "navLibrary": "Biblioteca", + "navScan": "Escanear", + "navMore": "Más", + "screenLibrary": "Biblioteca", + "fabAdd": "Añadir", + "searchHint": "Buscar (título, artista, canal …)", + "filterAll": "Todo", + "filterFavorites": "⭐ Favoritos", + "emptyNoMatch": "No hay resultados para tus filtros.", + "emptyNoItems": "Aún está vacío.\nPulsa la pestaña Escanear para detectar automáticamente tus aplicaciones multimedia.", + "screenScan": "Escanear", + "scanDescription": "Busca aplicaciones multimedia instaladas (streaming, música, pódcasts y documentos) y las añade a tu biblioteca. Si permites el acceso a los datos de uso, también obtendrás el tiempo de uso real.", + "btnAppScan": "Escaneo de aplicaciones (uso agregado)", + "bgScanTitle": "Escaneo en segundo plano", + "bgScanNow": "Ejecutar ahora una vez en segundo plano", + "bgScanNowSubtitle": "Envía una tarea puntual a Android. El resultado aparecerá después de 1–2 minutos.", + "bgScanQueued": "Escaneo en cola — el resultado estará visible en ~1 min", + "scanAndroidOnly": "El escaneo solo está disponible en Android.", + "scanPermissionMissing": "Falta el acceso a los datos de uso. Actívalo en los ajustes del sistema para MediaBrain:\n\nAjustes → Aplicaciones → Acceso especial a aplicaciones → Acceso a datos de uso → MediaBrain.", + "detectedTitle": "📡 ¿Qué se detecta?", + "detectedApps": "Netflix · Disney+ · Prime Video · Apple TV · YouTube · Twitch · Spotify · Apple Music · YouTube Music · Tidal · Amazon Music · Audible · Kindle · Pocket Casts · Google Podcasts · Adobe Acrobat · Google Docs · Notion · Obsidian y más.", + "appScanTitle": "🔍 Escaneo de aplicaciones", + "appScanDescription": "Escaneo de aplicaciones: una entrada por aplicación multimedia con el uso total de los últimos días (mediante las estadísticas de uso de Android).", + "neverText": "Nunca", + "screenSettings": "Ajustes", + "sectionDataStatus": "Estado de datos", + "sectionFileSync": "Sincronización de archivos", + "sectionServerSync": "Sincronización del servidor (Mac Studio)", + "sectionActions": "Acciones", + "settingsTotalEntries": "Total de entradas", + "settingsLastScan": "Último escaneo", + "exportTitle": "Exportar mediante Compartir", + "exportSubtitle": "JSON por correo, OneDrive o Telegram a ti mismo …", + "importTitle": "Elegir archivo de importación", + "importSubtitle": "Sobrescribe los datos locales", + "noServerConfigured": "No hay servidor configurado", + "actionPush": "Enviar", + "actionPull": "Recibir", + "refreshData": "Actualizar datos", + "deleteAllData": "Eliminar todos los datos de la biblioteca", + "deleteAllTitle": "¿Eliminar todos los datos de la biblioteca?", + "deleteAllContent": "Elimina todas las entradas del MediaBrain local.", + "cancel": "Cancelar", + "delete": "Eliminar", + "save": "Guardar", + "libraryCleared": "Biblioteca vaciada.", + "cancelled": "Cancelado.", + "serverConfigTitle": "Configurar sincronización del servidor", + "fieldServerUrl": "URL del servidor", + "fieldServerUrlHint": "p. ej., macstudvonlukas:8082", + "fieldBearerToken": "Token de portador", + "aboutTitle": "MediaBrain Mobile", + "aboutSubtitle": "Aplicación Flutter independiente. Detecta aplicaciones multimedia instaladas, registra entradas manuales y sincroniza mediante archivo o servidor.", + "detailDescription": "Descripción", + "detailDetails": "Detalles", + "detailTags": "Etiquetas", + "fieldArtist": "Artista", + "fieldAlbum": "Álbum", + "fieldChannel": "Canal", + "fieldSeasonEpisodeLabel": "Temporada/Episodio", + "fieldLength": "Duración", + "fieldLastOpened": "Última apertura", + "fieldUsage": "Uso", + "fieldProviderId": "ID del proveedor", + "fieldLocalPath": "Ruta local", + "openPlayStore": "Abrir en Play Store", + "openBrowser": "Abrir en el navegador", + "linkError": "No se pudo abrir el enlace.", + "dialogTitleNew": "Nueva entrada", + "dialogTitleEdit": "Editar entrada", + "fieldTitle": "Título", + "categoryLabel": "Categoría", + "fieldArtistOptional": "Artista / director (opcional)", + "fieldAlbumOptional": "Álbum / serie (opcional)", + "fieldChannelOptional": "Canal / fuente (opcional)", + "fieldDescriptionOptional": "Descripción (opcional)", + "fieldTags": "Etiquetas (separadas por comas)", + "fieldTagsHint": "p. ej., Documental, fin de semana", + "markAsFavorite": "Marcar como favorito", + "error": "Error: {msg}", + "@error": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "foregroundMinutesLabel": "Total: {minutes} min", + "@foregroundMinutesLabel": { + "placeholders": { + "minutes": { + "type": "int" + } + } + }, + "bgScanSubtitle": "Escanear cada ~{hours} h, incluso cuando la aplicación esté cerrada.", + "@bgScanSubtitle": { + "placeholders": { + "hours": { + "type": "int" + } + } + }, + "lastRun": "Última ejecución: {time}", + "@lastRun": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "scanResult": "Escaneo de aplicaciones: se encontraron {matched} aplicaciones multimedia y se añadieron {persisted} entradas.", + "@scanResult": { + "placeholders": { + "matched": { + "type": "int" + }, + "persisted": { + "type": "int" + } + } + }, + "exportResult": "Exportación: {msg}", + "@exportResult": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "importResult": "Importación: {msg}", + "@importResult": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "pushPullStatus": "Enviar: {push} · Recibir: {pull}", + "@pushPullStatus": { + "placeholders": { + "push": { + "type": "String" + }, + "pull": { + "type": "String" + } + } + }, + "seasonEpisode": "T{s} E{e}", + "@seasonEpisode": { + "placeholders": { + "s": { + "type": "int" + }, + "e": { + "type": "int" + } + } + }, + "usageMinutes": "{n} minutos", + "@usageMinutes": { + "placeholders": { + "n": { + "type": "int" + } + } + }, + "navPlaylists": "Listas de reproducción", + "screenPlaylists": "Listas y colecciones", + "playlistTypeManual": "Manual", + "playlistTypeSmart": "Inteligente (filtro dinámico)", + "dialogNewPlaylist": "Nueva lista de reproducción", + "dialogEditPlaylist": "Editar lista de reproducción", + "fieldPlaylistName": "Nombre de la lista de reproducción", + "fieldPlaylistDescription": "Descripción (opcional)", + "fieldPlaylistType": "Tipo de lista de reproducción", + "smartFilterCriteria": "Criterios de filtro inteligente", + "smartFilterCategory": "Categoría", + "smartFilterTag": "Etiqueta / palabra clave", + "smartFilterFavoritesOnly": "Solo favoritos", + "smartFilterMinMinutes": "Tiempo mínimo de uso (min)", + "smartFilterSearch": "Término de búsqueda", + "emptyPlaylists": "Todavía no hay listas de reproducción.\nCrea colecciones manuales o listas inteligentes dinámicas.", + "emptyPlaylistItems": "Esta lista todavía no contiene contenido multimedia.", + "deletePlaylistTitle": "¿Eliminar lista de reproducción?", + "deletePlaylistContent": "¿De verdad quieres eliminar esta lista de reproducción? Los medios incluidos permanecerán en tu biblioteca.", + "addToPlaylist": "Añadir a la lista de reproducción", + "removeFromPlaylist": "Quitar de la lista de reproducción", + "addedToPlaylist": "Añadido a la lista de reproducción.", + "removedFromPlaylist": "Quitado de la lista de reproducción.", + "filterByTag": "Filtro de etiquetas", + "allTags": "Todas las etiquetas", + "addTag": "Añadir etiqueta", + "removeTag": "Quitar etiqueta", + "noPlaylistsFound": "No hay listas de reproducción disponibles.", + "playlistItemCount": "{count, plural, one {{count} elemento} other {{count} elementos}}", + "tagCount": "#{tag} ({count})", + "@playlistItemCount": { + "description": "Number of items in a playlist.", + "placeholders": { + "count": { + "type": "num" + } + } + }, + "@tagCount": { + "description": "Tag and number of matching entries.", + "placeholders": { + "tag": { + "type": "String" + }, + "count": { + "type": "int" + } + } + } +} diff --git a/flutter_port/lib/l10n/app_fr.arb b/flutter_port/lib/l10n/app_fr.arb new file mode 100644 index 0000000..0574f48 --- /dev/null +++ b/flutter_port/lib/l10n/app_fr.arb @@ -0,0 +1,221 @@ +{ + "@@locale": "fr", + "appTitle": "MediaBrain", + "navLibrary": "Bibliothèque", + "navScan": "Scanner", + "navMore": "Plus", + "screenLibrary": "Bibliothèque", + "fabAdd": "Ajouter", + "searchHint": "Rechercher (titre, artiste, chaîne …)", + "filterAll": "Tout", + "filterFavorites": "⭐ Favoris", + "emptyNoMatch": "Aucun résultat ne correspond à vos filtres.", + "emptyNoItems": "Encore vide.\nTouchez l’onglet Scanner pour détecter automatiquement vos applications multimédias.", + "screenScan": "Scanner", + "scanDescription": "Recherche les applications multimédias installées (streaming, musique, podcasts, documents) et les ajoute à votre bibliothèque. Si l’accès aux données d’utilisation est activé, le temps d’utilisation réel est également disponible.", + "btnAppScan": "Analyse des applications (utilisation agrégée)", + "bgScanTitle": "Analyse en arrière-plan", + "bgScanNow": "Lancer une fois en arrière-plan maintenant", + "bgScanNowSubtitle": "Envoie une tâche ponctuelle à Android. Le résultat apparaît après 1 à 2 minutes.", + "bgScanQueued": "Analyse en arrière-plan en attente — résultat visible dans environ 1 min", + "scanAndroidOnly": "L’analyse est uniquement disponible sur Android.", + "scanPermissionMissing": "Accès aux données d’utilisation manquant. Activez-le dans les paramètres système de MediaBrain :\n\nParamètres → Applications → Accès spécial aux applications → Accès aux données d’utilisation → MediaBrain.", + "detectedTitle": "📡 Éléments détectés", + "detectedApps": "Netflix · Disney+ · Prime Video · Apple TV · YouTube · Twitch · Spotify · Apple Music · YouTube Music · Tidal · Amazon Music · Audible · Kindle · Pocket Casts · Google Podcasts · Adobe Acrobat · Google Docs · Notion · Obsidian et plus encore.", + "appScanTitle": "🔍 Analyse des applications", + "appScanDescription": "Analyse des applications : une entrée par application multimédia, avec l’utilisation totale des derniers jours (via les statistiques d’utilisation Android).", + "neverText": "jamais", + "screenSettings": "Paramètres", + "sectionDataStatus": "État des données", + "sectionFileSync": "Synchronisation des fichiers", + "sectionServerSync": "Synchronisation serveur (Mac Studio)", + "sectionActions": "Actions", + "settingsTotalEntries": "Nombre total d’entrées", + "settingsLastScan": "Dernière analyse", + "exportTitle": "Exporter via Partager", + "exportSubtitle": "JSON par e-mail, OneDrive ou Telegram à vous-même …", + "importTitle": "Choisir le fichier à importer", + "importSubtitle": "Remplace les données locales", + "noServerConfigured": "Aucun serveur configuré", + "actionPush": "Envoyer", + "actionPull": "Récupérer", + "refreshData": "Actualiser les données", + "deleteAllData": "Supprimer toutes les données de la bibliothèque", + "deleteAllTitle": "Supprimer toutes les données de la bibliothèque ?", + "deleteAllContent": "Supprime toutes les entrées du MediaBrain local.", + "cancel": "Annuler", + "delete": "Supprimer", + "save": "Enregistrer", + "libraryCleared": "Bibliothèque vidée.", + "cancelled": "Annulé.", + "serverConfigTitle": "Configurer la synchronisation du serveur", + "fieldServerUrl": "URL du serveur", + "fieldServerUrlHint": "p. ex. macstudvonlukas:8082", + "fieldBearerToken": "Jeton Bearer", + "aboutTitle": "MediaBrain Mobile", + "aboutSubtitle": "Application Flutter autonome. Détecte les applications multimédias installées, suit les entrées manuelles et synchronise par fichier ou serveur.", + "detailDescription": "Description", + "detailDetails": "Détails", + "detailTags": "Tags", + "fieldArtist": "Artiste", + "fieldAlbum": "Album", + "fieldChannel": "Chaîne", + "fieldSeasonEpisodeLabel": "Saison/épisode", + "fieldLength": "Durée", + "fieldLastOpened": "Dernière ouverture", + "fieldUsage": "Utilisation", + "fieldProviderId": "ID du fournisseur", + "fieldLocalPath": "Chemin local", + "openPlayStore": "Ouvrir dans le Play Store", + "openBrowser": "Ouvrir dans le navigateur", + "linkError": "Impossible d’ouvrir le lien.", + "dialogTitleNew": "Nouvelle entrée", + "dialogTitleEdit": "Modifier l’entrée", + "fieldTitle": "Titre", + "categoryLabel": "Catégorie", + "fieldArtistOptional": "Artiste / réalisateur (facultatif)", + "fieldAlbumOptional": "Album / série (facultatif)", + "fieldChannelOptional": "Chaîne / source (facultatif)", + "fieldDescriptionOptional": "Description (facultatif)", + "fieldTags": "Tags (séparés par des virgules)", + "fieldTagsHint": "p. ex. documentaire, week-end", + "markAsFavorite": "Ajouter aux favoris", + "error": "Erreur : {msg}", + "@error": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "foregroundMinutesLabel": "{minutes} min au total", + "@foregroundMinutesLabel": { + "placeholders": { + "minutes": { + "type": "int" + } + } + }, + "bgScanSubtitle": "Analyser toutes les ~{hours} h, même lorsque l’application est fermée.", + "@bgScanSubtitle": { + "placeholders": { + "hours": { + "type": "int" + } + } + }, + "lastRun": "Dernière exécution : {time}", + "@lastRun": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "scanResult": "Analyse des applications : {matched} applications multimédias détectées, {persisted} entrées ajoutées.", + "@scanResult": { + "placeholders": { + "matched": { + "type": "int" + }, + "persisted": { + "type": "int" + } + } + }, + "exportResult": "Export : {msg}", + "@exportResult": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "importResult": "Import : {msg}", + "@importResult": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "pushPullStatus": "Envoi : {push} · Récupération : {pull}", + "@pushPullStatus": { + "placeholders": { + "push": { + "type": "String" + }, + "pull": { + "type": "String" + } + } + }, + "seasonEpisode": "S{s} E{e}", + "@seasonEpisode": { + "placeholders": { + "s": { + "type": "int" + }, + "e": { + "type": "int" + } + } + }, + "usageMinutes": "{n} minutes", + "@usageMinutes": { + "placeholders": { + "n": { + "type": "int" + } + } + }, + "navPlaylists": "Playlists", + "screenPlaylists": "Playlists et collections", + "playlistTypeManual": "Manuelle", + "playlistTypeSmart": "Intelligente (filtre dynamique)", + "dialogNewPlaylist": "Nouvelle playlist", + "dialogEditPlaylist": "Modifier la playlist", + "fieldPlaylistName": "Nom de la playlist", + "fieldPlaylistDescription": "Description (facultative)", + "fieldPlaylistType": "Type de playlist", + "smartFilterCriteria": "Critères du filtre intelligent", + "smartFilterCategory": "Catégorie", + "smartFilterTag": "Tag / mot-clé", + "smartFilterFavoritesOnly": "Favoris uniquement", + "smartFilterMinMinutes": "Durée minimale d’utilisation (min)", + "smartFilterSearch": "Terme de recherche", + "emptyPlaylists": "Aucune playlist créée pour le moment.\nCréez des collections manuelles ou des playlists intelligentes dynamiques.", + "emptyPlaylistItems": "Cette playlist ne contient encore aucun média.", + "deletePlaylistTitle": "Supprimer la playlist ?", + "deletePlaylistContent": "Voulez-vous vraiment supprimer cette playlist ? Les médias qu’elle contient resteront dans votre bibliothèque.", + "addToPlaylist": "Ajouter à la playlist", + "removeFromPlaylist": "Retirer de la playlist", + "addedToPlaylist": "Ajouté à la playlist.", + "removedFromPlaylist": "Retiré de la playlist.", + "filterByTag": "Filtrer par tag", + "allTags": "Tous les tags", + "addTag": "Ajouter un tag", + "removeTag": "Supprimer le tag", + "noPlaylistsFound": "Aucune playlist disponible.", + "playlistItemCount": "{count, plural, one {{count} élément} other {{count} éléments}}", + "tagCount": "#{tag} ({count})", + "@playlistItemCount": { + "description": "Number of items in a playlist.", + "placeholders": { + "count": { + "type": "num" + } + } + }, + "@tagCount": { + "description": "Tag and number of matching entries.", + "placeholders": { + "tag": { + "type": "String" + }, + "count": { + "type": "int" + } + } + } +} diff --git a/flutter_port/lib/l10n/app_hi.arb b/flutter_port/lib/l10n/app_hi.arb new file mode 100644 index 0000000..8da4e83 --- /dev/null +++ b/flutter_port/lib/l10n/app_hi.arb @@ -0,0 +1,221 @@ +{ + "@@locale": "hi", + "appTitle": "MediaBrain", + "navLibrary": "लाइब्रेरी", + "navScan": "स्कैन", + "navMore": "और", + "screenLibrary": "लाइब्रेरी", + "fabAdd": "जोड़ें", + "searchHint": "खोजें (शीर्षक, कलाकार, चैनल …)", + "filterAll": "सभी", + "filterFavorites": "⭐ पसंदीदा", + "emptyNoMatch": "आपके फ़िल्टर से कोई परिणाम नहीं मिला।", + "emptyNoItems": "अभी खाली है।\nअपने मीडिया ऐप अपने-आप पहचानने के लिए स्कैन टैब पर टैप करें।", + "screenScan": "स्कैन", + "scanDescription": "इंस्टॉल किए गए मीडिया ऐप (स्ट्रीमिंग, संगीत, पॉडकास्ट, दस्तावेज़) खोजता है और उन्हें आपकी लाइब्रेरी में जोड़ता है। उपयोग डेटा की अनुमति चालू होने पर वास्तविक उपयोग समय भी मिलता है।", + "btnAppScan": "ऐप स्कैन (कुल उपयोग)", + "bgScanTitle": "बैकग्राउंड स्कैन", + "bgScanNow": "अभी एक बार बैकग्राउंड में चलाएँ", + "bgScanNowSubtitle": "Android को एक बार चलने वाला टास्क भेजता है। परिणाम 1–2 मिनट बाद दिखेगा।", + "bgScanQueued": "बैकग्राउंड स्कैन कतार में है — परिणाम लगभग 1 मिनट में दिखेगा", + "scanAndroidOnly": "स्कैन केवल Android पर उपलब्ध है।", + "scanPermissionMissing": "उपयोग डेटा की अनुमति नहीं है। MediaBrain के लिए सिस्टम सेटिंग में इसे चालू करें:\n\nसेटिंग → ऐप्स → विशेष ऐप एक्सेस → उपयोग डेटा एक्सेस → MediaBrain।", + "detectedTitle": "📡 क्या पहचाना जाता है?", + "detectedApps": "Netflix · Disney+ · Prime Video · Apple TV · YouTube · Twitch · Spotify · Apple Music · YouTube Music · Tidal · Amazon Music · Audible · Kindle · Pocket Casts · Google Podcasts · Adobe Acrobat · Google Docs · Notion · Obsidian और अन्य।", + "appScanTitle": "🔍 ऐप स्कैन", + "appScanDescription": "ऐप स्कैन: हर मीडिया ऐप के लिए एक प्रविष्टि, जिसमें पिछले दिनों का कुल उपयोग दिखता है (Android उपयोग आँकड़ों से)।", + "neverText": "कभी नहीं", + "screenSettings": "सेटिंग्स", + "sectionDataStatus": "डेटा स्थिति", + "sectionFileSync": "फ़ाइल सिंक", + "sectionServerSync": "सर्वर सिंक (Mac Studio)", + "sectionActions": "कार्रवाइयाँ", + "settingsTotalEntries": "कुल प्रविष्टियाँ", + "settingsLastScan": "अंतिम स्कैन", + "exportTitle": "शेयर के ज़रिए निर्यात", + "exportSubtitle": "ईमेल, OneDrive या Telegram से खुद को JSON भेजें …", + "importTitle": "आयात फ़ाइल चुनें", + "importSubtitle": "स्थानीय डेटा को बदल देता है", + "noServerConfigured": "कोई सर्वर कॉन्फ़िगर नहीं है", + "actionPush": "पुश", + "actionPull": "पुल", + "refreshData": "डेटा रीफ़्रेश करें", + "deleteAllData": "लाइब्रेरी का सारा डेटा हटाएँ", + "deleteAllTitle": "लाइब्रेरी का सारा डेटा हटाएँ?", + "deleteAllContent": "स्थानीय MediaBrain से सभी प्रविष्टियाँ हटाता है।", + "cancel": "रद्द करें", + "delete": "हटाएँ", + "save": "सहेजें", + "libraryCleared": "लाइब्रेरी साफ़ कर दी गई।", + "cancelled": "रद्द किया गया।", + "serverConfigTitle": "सर्वर सिंक कॉन्फ़िगर करें", + "fieldServerUrl": "सर्वर URL", + "fieldServerUrlHint": "जैसे, macstudvonlukas:8082", + "fieldBearerToken": "Bearer टोकन", + "aboutTitle": "MediaBrain Mobile", + "aboutSubtitle": "स्वतंत्र Flutter ऐप। इंस्टॉल किए गए मीडिया ऐप पहचानता है, मैन्युअल प्रविष्टियाँ रखता है और फ़ाइल या सर्वर से सिंक करता है।", + "detailDescription": "विवरण", + "detailDetails": "जानकारी", + "detailTags": "टैग", + "fieldArtist": "कलाकार", + "fieldAlbum": "एल्बम", + "fieldChannel": "चैनल", + "fieldSeasonEpisodeLabel": "सीज़न/एपिसोड", + "fieldLength": "अवधि", + "fieldLastOpened": "अंतिम बार खोला गया", + "fieldUsage": "उपयोग", + "fieldProviderId": "प्रदाता ID", + "fieldLocalPath": "स्थानीय पथ", + "openPlayStore": "Play Store में खोलें", + "openBrowser": "ब्राउज़र में खोलें", + "linkError": "लिंक नहीं खुल सका।", + "dialogTitleNew": "नई प्रविष्टि", + "dialogTitleEdit": "प्रविष्टि संपादित करें", + "fieldTitle": "शीर्षक", + "categoryLabel": "श्रेणी", + "fieldArtistOptional": "कलाकार / निर्देशक (वैकल्पिक)", + "fieldAlbumOptional": "एल्बम / सीरीज़ (वैकल्पिक)", + "fieldChannelOptional": "चैनल / स्रोत (वैकल्पिक)", + "fieldDescriptionOptional": "विवरण (वैकल्पिक)", + "fieldTags": "टैग (अल्पविराम से अलग)", + "fieldTagsHint": "जैसे, वृत्तचित्र, सप्ताहांत", + "markAsFavorite": "पसंदीदा के रूप में चिह्नित करें", + "error": "त्रुटि: {msg}", + "@error": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "foregroundMinutesLabel": "कुल {minutes} मिनट", + "@foregroundMinutesLabel": { + "placeholders": { + "minutes": { + "type": "int" + } + } + }, + "bgScanSubtitle": "ऐप बंद होने पर भी हर ~{hours} घंटे में स्कैन करें।", + "@bgScanSubtitle": { + "placeholders": { + "hours": { + "type": "int" + } + } + }, + "lastRun": "अंतिम बार चला: {time}", + "@lastRun": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "scanResult": "ऐप स्कैन: {matched} मीडिया ऐप मिले, {persisted} प्रविष्टियाँ जोड़ी गईं।", + "@scanResult": { + "placeholders": { + "matched": { + "type": "int" + }, + "persisted": { + "type": "int" + } + } + }, + "exportResult": "निर्यात: {msg}", + "@exportResult": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "importResult": "आयात: {msg}", + "@importResult": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "pushPullStatus": "पुश: {push} · पुल: {pull}", + "@pushPullStatus": { + "placeholders": { + "push": { + "type": "String" + }, + "pull": { + "type": "String" + } + } + }, + "seasonEpisode": "S{s} E{e}", + "@seasonEpisode": { + "placeholders": { + "s": { + "type": "int" + }, + "e": { + "type": "int" + } + } + }, + "usageMinutes": "{n} मिनट", + "@usageMinutes": { + "placeholders": { + "n": { + "type": "int" + } + } + }, + "navPlaylists": "प्लेलिस्ट", + "screenPlaylists": "प्लेलिस्ट और संग्रह", + "playlistTypeManual": "मैन्युअल", + "playlistTypeSmart": "स्मार्ट (डायनेमिक फ़िल्टर)", + "dialogNewPlaylist": "नई प्लेलिस्ट", + "dialogEditPlaylist": "प्लेलिस्ट संपादित करें", + "fieldPlaylistName": "प्लेलिस्ट का नाम", + "fieldPlaylistDescription": "विवरण (वैकल्पिक)", + "fieldPlaylistType": "प्लेलिस्ट का प्रकार", + "smartFilterCriteria": "स्मार्ट फ़िल्टर मानदंड", + "smartFilterCategory": "श्रेणी", + "smartFilterTag": "टैग / कीवर्ड", + "smartFilterFavoritesOnly": "केवल पसंदीदा", + "smartFilterMinMinutes": "न्यूनतम उपयोग समय (मिनट)", + "smartFilterSearch": "खोज शब्द", + "emptyPlaylists": "अभी कोई प्लेलिस्ट नहीं बनाई गई है।\nमैन्युअल संग्रह या डायनेमिक स्मार्ट प्लेलिस्ट बनाएँ।", + "emptyPlaylistItems": "इस प्लेलिस्ट में अभी कोई मीडिया नहीं है।", + "deletePlaylistTitle": "प्लेलिस्ट हटाएँ?", + "deletePlaylistContent": "क्या आप वाकई यह प्लेलिस्ट हटाना चाहते हैं? इसमें मौजूद मीडिया आपकी लाइब्रेरी में रहेगा।", + "addToPlaylist": "प्लेलिस्ट में जोड़ें", + "removeFromPlaylist": "प्लेलिस्ट से हटाएँ", + "addedToPlaylist": "प्लेलिस्ट में जोड़ा गया।", + "removedFromPlaylist": "प्लेलिस्ट से हटाया गया।", + "filterByTag": "टैग फ़िल्टर", + "allTags": "सभी टैग", + "addTag": "टैग जोड़ें", + "removeTag": "टैग हटाएँ", + "noPlaylistsFound": "कोई प्लेलिस्ट उपलब्ध नहीं है।", + "playlistItemCount": "{count, plural, one {{count} आइटम} other {{count} आइटम}}", + "tagCount": "#{tag} ({count})", + "@playlistItemCount": { + "description": "Number of items in a playlist.", + "placeholders": { + "count": { + "type": "num" + } + } + }, + "@tagCount": { + "description": "Tag and number of matching entries.", + "placeholders": { + "tag": { + "type": "String" + }, + "count": { + "type": "int" + } + } + } +} diff --git a/flutter_port/lib/l10n/app_ja.arb b/flutter_port/lib/l10n/app_ja.arb new file mode 100644 index 0000000..e72d1ee --- /dev/null +++ b/flutter_port/lib/l10n/app_ja.arb @@ -0,0 +1,221 @@ +{ + "@@locale": "ja", + "appTitle": "MediaBrain", + "navLibrary": "ライブラリ", + "navScan": "スキャン", + "navMore": "その他", + "screenLibrary": "ライブラリ", + "fabAdd": "追加", + "searchHint": "検索(タイトル、アーティスト、チャンネル…)", + "filterAll": "すべて", + "filterFavorites": "⭐ お気に入り", + "emptyNoMatch": "指定したフィルターに一致する結果はありません。", + "emptyNoItems": "まだ空です。\n「スキャン」タブをタップすると、メディアアプリを自動検出します。", + "screenScan": "スキャン", + "scanDescription": "インストール済みのメディアアプリ(ストリーミング、音楽、ポッドキャスト、ドキュメント)を検索し、ライブラリに項目として追加します。使用状況データへのアクセスを有効にすると、実際の使用時間も取得できます。", + "btnAppScan": "アプリスキャン(集計使用量)", + "bgScanTitle": "バックグラウンドスキャン", + "bgScanNow": "今すぐバックグラウンドで1回実行", + "bgScanNowSubtitle": "Android に単発タスクを送信します。結果は1~2分後に表示されます。", + "bgScanQueued": "バックグラウンドスキャンをキューに追加しました — 約1分後に結果を確認できます", + "scanAndroidOnly": "スキャンは Android でのみ利用できます。", + "scanPermissionMissing": "使用状況データへのアクセスがありません。MediaBrain のシステム設定で有効にしてください:\n\n設定 → アプリ → 特別なアプリアクセス → 使用状況へのアクセス → MediaBrain。", + "detectedTitle": "📡 検出されるもの", + "detectedApps": "Netflix · Disney+ · Prime Video · Apple TV · YouTube · Twitch · Spotify · Apple Music · YouTube Music · Tidal · Amazon Music · Audible · Kindle · Pocket Casts · Google Podcasts · Adobe Acrobat · Google Docs · Notion · Obsidian など。", + "appScanTitle": "🔍 アプリスキャン", + "appScanDescription": "アプリスキャン:メディアアプリごとに1件作成し、直近数日間の合計使用量を表示します(Android の使用状況統計を利用)。", + "neverText": "一度もない", + "screenSettings": "設定", + "sectionDataStatus": "データの状態", + "sectionFileSync": "ファイル同期", + "sectionServerSync": "サーバー同期(Mac Studio)", + "sectionActions": "操作", + "settingsTotalEntries": "登録数", + "settingsLastScan": "最終スキャン", + "exportTitle": "共有からエクスポート", + "exportSubtitle": "メール、OneDrive、Telegram で自分宛てに JSON を送信 …", + "importTitle": "インポートするファイルを選択", + "importSubtitle": "ローカルデータを上書きします", + "noServerConfigured": "サーバーが設定されていません", + "actionPush": "送信", + "actionPull": "取得", + "refreshData": "データを更新", + "deleteAllData": "ライブラリのデータをすべて削除", + "deleteAllTitle": "ライブラリのデータをすべて削除しますか?", + "deleteAllContent": "ローカル MediaBrain のすべての項目を削除します。", + "cancel": "キャンセル", + "delete": "削除", + "save": "保存", + "libraryCleared": "ライブラリを消去しました。", + "cancelled": "キャンセルしました。", + "serverConfigTitle": "サーバー同期を設定", + "fieldServerUrl": "サーバー URL", + "fieldServerUrlHint": "例: macstudvonlukas:8082", + "fieldBearerToken": "Bearer トークン", + "aboutTitle": "MediaBrain Mobile", + "aboutSubtitle": "単体で動作する Flutter アプリです。インストール済みのメディアアプリを検出し、手動項目を記録して、ファイルまたはサーバー経由で同期します。", + "detailDescription": "説明", + "detailDetails": "詳細", + "detailTags": "タグ", + "fieldArtist": "アーティスト", + "fieldAlbum": "アルバム", + "fieldChannel": "チャンネル", + "fieldSeasonEpisodeLabel": "シーズン/エピソード", + "fieldLength": "長さ", + "fieldLastOpened": "最終オープン", + "fieldUsage": "使用状況", + "fieldProviderId": "プロバイダー ID", + "fieldLocalPath": "ローカルパス", + "openPlayStore": "Play ストアで開く", + "openBrowser": "ブラウザーで開く", + "linkError": "リンクを開けませんでした。", + "dialogTitleNew": "新しい項目", + "dialogTitleEdit": "項目を編集", + "fieldTitle": "タイトル", + "categoryLabel": "カテゴリ", + "fieldArtistOptional": "アーティスト / 監督(任意)", + "fieldAlbumOptional": "アルバム / シリーズ(任意)", + "fieldChannelOptional": "チャンネル / ソース(任意)", + "fieldDescriptionOptional": "説明(任意)", + "fieldTags": "タグ(カンマ区切り)", + "fieldTagsHint": "例: ドキュメンタリー、週末", + "markAsFavorite": "お気に入りにする", + "error": "エラー: {msg}", + "@error": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "foregroundMinutesLabel": "合計 {minutes} 分", + "@foregroundMinutesLabel": { + "placeholders": { + "minutes": { + "type": "int" + } + } + }, + "bgScanSubtitle": "アプリが閉じていても、約 {hours} 時間ごとにスキャンします。", + "@bgScanSubtitle": { + "placeholders": { + "hours": { + "type": "int" + } + } + }, + "lastRun": "最終実行: {time}", + "@lastRun": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "scanResult": "アプリスキャン: メディアアプリを {matched} 件検出し、{persisted} 件の項目を追加しました。", + "@scanResult": { + "placeholders": { + "matched": { + "type": "int" + }, + "persisted": { + "type": "int" + } + } + }, + "exportResult": "エクスポート: {msg}", + "@exportResult": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "importResult": "インポート: {msg}", + "@importResult": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "pushPullStatus": "送信: {push} · 取得: {pull}", + "@pushPullStatus": { + "placeholders": { + "push": { + "type": "String" + }, + "pull": { + "type": "String" + } + } + }, + "seasonEpisode": "シーズン{s} エピソード{e}", + "@seasonEpisode": { + "placeholders": { + "s": { + "type": "int" + }, + "e": { + "type": "int" + } + } + }, + "usageMinutes": "{n} 分", + "@usageMinutes": { + "placeholders": { + "n": { + "type": "int" + } + } + }, + "navPlaylists": "プレイリスト", + "screenPlaylists": "プレイリストとコレクション", + "playlistTypeManual": "手動", + "playlistTypeSmart": "スマート(動的フィルター)", + "dialogNewPlaylist": "新しいプレイリスト", + "dialogEditPlaylist": "プレイリストを編集", + "fieldPlaylistName": "プレイリスト名", + "fieldPlaylistDescription": "説明(任意)", + "fieldPlaylistType": "プレイリストの種類", + "smartFilterCriteria": "スマートフィルターの条件", + "smartFilterCategory": "カテゴリ", + "smartFilterTag": "タグ / キーワード", + "smartFilterFavoritesOnly": "お気に入りのみ", + "smartFilterMinMinutes": "最小使用時間(分)", + "smartFilterSearch": "検索語", + "emptyPlaylists": "プレイリストはまだありません。\n手動コレクションまたは動的なスマートプレイリストを作成してください。", + "emptyPlaylistItems": "このプレイリストにはまだメディアがありません。", + "deletePlaylistTitle": "プレイリストを削除しますか?", + "deletePlaylistContent": "このプレイリストを本当に削除しますか?含まれるメディアはライブラリに残ります。", + "addToPlaylist": "プレイリストに追加", + "removeFromPlaylist": "プレイリストから削除", + "addedToPlaylist": "プレイリストに追加しました。", + "removedFromPlaylist": "プレイリストから削除しました。", + "filterByTag": "タグで絞り込む", + "allTags": "すべてのタグ", + "addTag": "タグを追加", + "removeTag": "タグを削除", + "noPlaylistsFound": "利用できるプレイリストはありません。", + "playlistItemCount": "{count, plural, other {{count} 件}}", + "tagCount": "#{tag} ({count})", + "@playlistItemCount": { + "description": "Number of items in a playlist.", + "placeholders": { + "count": { + "type": "num" + } + } + }, + "@tagCount": { + "description": "Tag and number of matching entries.", + "placeholders": { + "tag": { + "type": "String" + }, + "count": { + "type": "int" + } + } + } +} diff --git a/flutter_port/lib/l10n/app_localizations.dart b/flutter_port/lib/l10n/app_localizations.dart index f88c22a..b15c7b6 100644 --- a/flutter_port/lib/l10n/app_localizations.dart +++ b/flutter_port/lib/l10n/app_localizations.dart @@ -1,8 +1,81 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_ar.dart'; +import 'app_localizations_bn.dart'; +import 'app_localizations_de.dart'; +import 'app_localizations_en.dart'; +import 'app_localizations_es.dart'; +import 'app_localizations_fr.dart'; +import 'app_localizations_hi.dart'; +import 'app_localizations_ja.dart'; +import 'app_localizations_pt.dart'; +import 'app_localizations_ru.dart'; +import 'app_localizations_zh.dart'; + +// ignore_for_file: type=lint +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'l10n/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. abstract class AppLocalizations { + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + static AppLocalizations of(BuildContext context) { return Localizations.of(context, AppLocalizations)!; } @@ -10,138 +83,757 @@ abstract class AppLocalizations { static const LocalizationsDelegate delegate = _AppLocalizationsDelegate(); - static const List> localizationsDelegates = [ - delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ]; + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; - static const List supportedLocales = [ + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ Locale('de'), + Locale('ar'), + Locale('bn'), Locale('en'), + Locale('es'), + Locale('fr'), + Locale('hi'), + Locale('ja'), + Locale('pt'), + Locale('ru'), + Locale('zh'), ]; + /// No description provided for @appTitle. + /// + /// In en, this message translates to: + /// **'MediaBrain'** String get appTitle; + + /// No description provided for @navLibrary. + /// + /// In en, this message translates to: + /// **'Library'** String get navLibrary; + + /// No description provided for @navScan. + /// + /// In en, this message translates to: + /// **'Scan'** String get navScan; + + /// No description provided for @navMore. + /// + /// In en, this message translates to: + /// **'More'** String get navMore; + + /// No description provided for @screenLibrary. + /// + /// In en, this message translates to: + /// **'Library'** String get screenLibrary; + + /// No description provided for @fabAdd. + /// + /// In en, this message translates to: + /// **'Add'** String get fabAdd; + + /// No description provided for @searchHint. + /// + /// In en, this message translates to: + /// **'Search (Title, Artist, Channel …)'** String get searchHint; + + /// No description provided for @filterAll. + /// + /// In en, this message translates to: + /// **'All'** String get filterAll; + + /// No description provided for @filterFavorites. + /// + /// In en, this message translates to: + /// **'⭐ Favorites'** String get filterFavorites; + + /// No description provided for @emptyNoMatch. + /// + /// In en, this message translates to: + /// **'No results for your filters.'** String get emptyNoMatch; + + /// No description provided for @emptyNoItems. + /// + /// In en, this message translates to: + /// **'Empty yet.\nTap the \"Scan\" tab to automatically detect your media apps.'** String get emptyNoItems; + + /// No description provided for @screenScan. + /// + /// In en, this message translates to: + /// **'Scan'** String get screenScan; + + /// No description provided for @scanDescription. + /// + /// In en, this message translates to: + /// **'Searches for installed media apps (streaming, music, podcasts, documents) and adds them as entries to your library. With enabled usage data access you additionally get the actual usage time.'** String get scanDescription; + + /// No description provided for @btnAppScan. + /// + /// In en, this message translates to: + /// **'App Scan (Aggregate Usage)'** String get btnAppScan; + + /// No description provided for @bgScanTitle. + /// + /// In en, this message translates to: + /// **'Background Scan'** String get bgScanTitle; + + /// No description provided for @bgScanNow. + /// + /// In en, this message translates to: + /// **'Run once in background now'** String get bgScanNow; + + /// No description provided for @bgScanNowSubtitle. + /// + /// In en, this message translates to: + /// **'Sends a one-off task to Android. Result appears after 1–2 minutes.'** String get bgScanNowSubtitle; + + /// No description provided for @bgScanQueued. + /// + /// In en, this message translates to: + /// **'Background scan queued — result visible in ~1 min'** String get bgScanQueued; + + /// No description provided for @scanAndroidOnly. + /// + /// In en, this message translates to: + /// **'Scan is only available on Android.'** String get scanAndroidOnly; + + /// No description provided for @scanPermissionMissing. + /// + /// In en, this message translates to: + /// **'Usage data access missing. Please enable in system settings for MediaBrain:\n\nSettings → Apps → Special app access → Usage data access → MediaBrain.'** String get scanPermissionMissing; + + /// No description provided for @detectedTitle. + /// + /// In en, this message translates to: + /// **'📡 What is detected?'** String get detectedTitle; + + /// No description provided for @detectedApps. + /// + /// In en, this message translates to: + /// **'Netflix · Disney+ · Prime Video · Apple TV · YouTube · Twitch · Spotify · Apple Music · YouTube Music · Tidal · Amazon Music · Audible · Kindle · Pocket Casts · Google Podcasts · Adobe Acrobat · Google Docs · Notion · Obsidian and more.'** String get detectedApps; + + /// No description provided for @appScanTitle. + /// + /// In en, this message translates to: + /// **'🔍 App Scan'** String get appScanTitle; + + /// No description provided for @appScanDescription. + /// + /// In en, this message translates to: + /// **'App Scan: one entry per media app with total usage over the last days (via Android usage stats).'** String get appScanDescription; + + /// No description provided for @neverText. + /// + /// In en, this message translates to: + /// **'never'** String get neverText; + + /// No description provided for @screenSettings. + /// + /// In en, this message translates to: + /// **'Settings'** String get screenSettings; + + /// No description provided for @sectionDataStatus. + /// + /// In en, this message translates to: + /// **'Data Status'** String get sectionDataStatus; + + /// No description provided for @sectionFileSync. + /// + /// In en, this message translates to: + /// **'File Sync'** String get sectionFileSync; + + /// No description provided for @sectionServerSync. + /// + /// In en, this message translates to: + /// **'Server Sync (Mac Studio)'** String get sectionServerSync; + + /// No description provided for @sectionActions. + /// + /// In en, this message translates to: + /// **'Actions'** String get sectionActions; + + /// No description provided for @settingsTotalEntries. + /// + /// In en, this message translates to: + /// **'Total Entries'** String get settingsTotalEntries; + + /// No description provided for @settingsLastScan. + /// + /// In en, this message translates to: + /// **'Last Scan'** String get settingsLastScan; + + /// No description provided for @exportTitle. + /// + /// In en, this message translates to: + /// **'Export via Share'** String get exportTitle; + + /// No description provided for @exportSubtitle. + /// + /// In en, this message translates to: + /// **'JSON via Mail, OneDrive, Telegram to yourself …'** String get exportSubtitle; + + /// No description provided for @importTitle. + /// + /// In en, this message translates to: + /// **'Choose Import File'** String get importTitle; + + /// No description provided for @importSubtitle. + /// + /// In en, this message translates to: + /// **'Overwrites local data'** String get importSubtitle; + + /// No description provided for @noServerConfigured. + /// + /// In en, this message translates to: + /// **'No server configured'** String get noServerConfigured; + + /// No description provided for @actionPush. + /// + /// In en, this message translates to: + /// **'Push'** String get actionPush; + + /// No description provided for @actionPull. + /// + /// In en, this message translates to: + /// **'Pull'** String get actionPull; + + /// No description provided for @refreshData. + /// + /// In en, this message translates to: + /// **'Refresh data'** String get refreshData; + + /// No description provided for @deleteAllData. + /// + /// In en, this message translates to: + /// **'Delete all library data'** String get deleteAllData; + + /// No description provided for @deleteAllTitle. + /// + /// In en, this message translates to: + /// **'Delete all library data?'** String get deleteAllTitle; + + /// No description provided for @deleteAllContent. + /// + /// In en, this message translates to: + /// **'Removes all entries from the local MediaBrain.'** String get deleteAllContent; + + /// No description provided for @cancel. + /// + /// In en, this message translates to: + /// **'Cancel'** String get cancel; + + /// No description provided for @delete. + /// + /// In en, this message translates to: + /// **'Delete'** String get delete; + + /// No description provided for @save. + /// + /// In en, this message translates to: + /// **'Save'** String get save; + + /// No description provided for @libraryCleared. + /// + /// In en, this message translates to: + /// **'Library cleared.'** String get libraryCleared; + + /// No description provided for @cancelled. + /// + /// In en, this message translates to: + /// **'Cancelled.'** String get cancelled; + + /// No description provided for @serverConfigTitle. + /// + /// In en, this message translates to: + /// **'Configure Server Sync'** String get serverConfigTitle; + + /// No description provided for @fieldServerUrl. + /// + /// In en, this message translates to: + /// **'Server URL'** String get fieldServerUrl; + + /// No description provided for @fieldServerUrlHint. + /// + /// In en, this message translates to: + /// **'e.g. macstudvonlukas:8082'** String get fieldServerUrlHint; + + /// No description provided for @fieldBearerToken. + /// + /// In en, this message translates to: + /// **'Bearer Token'** String get fieldBearerToken; + + /// No description provided for @aboutTitle. + /// + /// In en, this message translates to: + /// **'MediaBrain Mobile'** String get aboutTitle; + + /// No description provided for @aboutSubtitle. + /// + /// In en, this message translates to: + /// **'Standalone Flutter app. Detects installed media apps, tracks manual entries and syncs via file or server.'** String get aboutSubtitle; + + /// No description provided for @detailDescription. + /// + /// In en, this message translates to: + /// **'Description'** String get detailDescription; + + /// No description provided for @detailDetails. + /// + /// In en, this message translates to: + /// **'Details'** String get detailDetails; + + /// No description provided for @detailTags. + /// + /// In en, this message translates to: + /// **'Tags'** String get detailTags; + + /// No description provided for @fieldArtist. + /// + /// In en, this message translates to: + /// **'Artist'** String get fieldArtist; + + /// No description provided for @fieldAlbum. + /// + /// In en, this message translates to: + /// **'Album'** String get fieldAlbum; + + /// No description provided for @fieldChannel. + /// + /// In en, this message translates to: + /// **'Channel'** String get fieldChannel; + + /// No description provided for @fieldSeasonEpisodeLabel. + /// + /// In en, this message translates to: + /// **'Season/Episode'** String get fieldSeasonEpisodeLabel; + + /// No description provided for @fieldLength. + /// + /// In en, this message translates to: + /// **'Length'** String get fieldLength; + + /// No description provided for @fieldLastOpened. + /// + /// In en, this message translates to: + /// **'Last opened'** String get fieldLastOpened; + + /// No description provided for @fieldUsage. + /// + /// In en, this message translates to: + /// **'Usage'** String get fieldUsage; + + /// No description provided for @fieldProviderId. + /// + /// In en, this message translates to: + /// **'Provider ID'** String get fieldProviderId; + + /// No description provided for @fieldLocalPath. + /// + /// In en, this message translates to: + /// **'Local Path'** String get fieldLocalPath; + + /// No description provided for @openPlayStore. + /// + /// In en, this message translates to: + /// **'Open in Play Store'** String get openPlayStore; + + /// No description provided for @openBrowser. + /// + /// In en, this message translates to: + /// **'Open in Browser'** String get openBrowser; + + /// No description provided for @linkError. + /// + /// In en, this message translates to: + /// **'Link could not be opened.'** String get linkError; + + /// No description provided for @dialogTitleNew. + /// + /// In en, this message translates to: + /// **'New Entry'** String get dialogTitleNew; + + /// No description provided for @dialogTitleEdit. + /// + /// In en, this message translates to: + /// **'Edit Entry'** String get dialogTitleEdit; + + /// No description provided for @fieldTitle. + /// + /// In en, this message translates to: + /// **'Title'** String get fieldTitle; + + /// No description provided for @categoryLabel. + /// + /// In en, this message translates to: + /// **'Category'** String get categoryLabel; + + /// No description provided for @fieldArtistOptional. + /// + /// In en, this message translates to: + /// **'Artist / Director (optional)'** String get fieldArtistOptional; + + /// No description provided for @fieldAlbumOptional. + /// + /// In en, this message translates to: + /// **'Album / Series (optional)'** String get fieldAlbumOptional; + + /// No description provided for @fieldChannelOptional. + /// + /// In en, this message translates to: + /// **'Channel / Source (optional)'** String get fieldChannelOptional; + + /// No description provided for @fieldDescriptionOptional. + /// + /// In en, this message translates to: + /// **'Description (optional)'** String get fieldDescriptionOptional; + + /// No description provided for @fieldTags. + /// + /// In en, this message translates to: + /// **'Tags (comma-separated)'** String get fieldTags; + + /// No description provided for @fieldTagsHint. + /// + /// In en, this message translates to: + /// **'e.g. Documentary, Weekend'** String get fieldTagsHint; + + /// No description provided for @markAsFavorite. + /// + /// In en, this message translates to: + /// **'Mark as favorite'** String get markAsFavorite; + + /// No description provided for @error. + /// + /// In en, this message translates to: + /// **'Error: {msg}'** + String error(String msg); + + /// No description provided for @foregroundMinutesLabel. + /// + /// In en, this message translates to: + /// **'{minutes} min total'** + String foregroundMinutesLabel(int minutes); + + /// No description provided for @bgScanSubtitle. + /// + /// In en, this message translates to: + /// **'Scan every ~{hours} h, even when the app is closed.'** + String bgScanSubtitle(int hours); + + /// No description provided for @lastRun. + /// + /// In en, this message translates to: + /// **'Last run: {time}'** + String lastRun(String time); + + /// No description provided for @scanResult. + /// + /// In en, this message translates to: + /// **'App scan: {matched} media apps found, {persisted} entries added.'** + String scanResult(int matched, int persisted); + + /// No description provided for @exportResult. + /// + /// In en, this message translates to: + /// **'Export: {msg}'** + String exportResult(String msg); + + /// No description provided for @importResult. + /// + /// In en, this message translates to: + /// **'Import: {msg}'** + String importResult(String msg); + + /// No description provided for @pushPullStatus. + /// + /// In en, this message translates to: + /// **'Push: {push} · Pull: {pull}'** + String pushPullStatus(String push, String pull); + + /// No description provided for @seasonEpisode. + /// + /// In en, this message translates to: + /// **'S{s} E{e}'** + String seasonEpisode(int s, int e); + + /// No description provided for @usageMinutes. + /// + /// In en, this message translates to: + /// **'{n} minutes'** + String usageMinutes(int n); + + /// No description provided for @navPlaylists. + /// + /// In en, this message translates to: + /// **'Playlists'** String get navPlaylists; + + /// No description provided for @screenPlaylists. + /// + /// In en, this message translates to: + /// **'Playlists & Collections'** String get screenPlaylists; + + /// No description provided for @playlistTypeManual. + /// + /// In en, this message translates to: + /// **'Manual'** String get playlistTypeManual; + + /// No description provided for @playlistTypeSmart. + /// + /// In en, this message translates to: + /// **'Smart (Dynamic Filter)'** String get playlistTypeSmart; + + /// No description provided for @dialogNewPlaylist. + /// + /// In en, this message translates to: + /// **'New Playlist'** String get dialogNewPlaylist; + + /// No description provided for @dialogEditPlaylist. + /// + /// In en, this message translates to: + /// **'Edit Playlist'** String get dialogEditPlaylist; + + /// No description provided for @fieldPlaylistName. + /// + /// In en, this message translates to: + /// **'Playlist Name'** String get fieldPlaylistName; + + /// No description provided for @fieldPlaylistDescription. + /// + /// In en, this message translates to: + /// **'Description (optional)'** String get fieldPlaylistDescription; + + /// No description provided for @fieldPlaylistType. + /// + /// In en, this message translates to: + /// **'Playlist Type'** String get fieldPlaylistType; + + /// No description provided for @smartFilterCriteria. + /// + /// In en, this message translates to: + /// **'Smart Filter Criteria'** String get smartFilterCriteria; + + /// No description provided for @smartFilterCategory. + /// + /// In en, this message translates to: + /// **'Category'** String get smartFilterCategory; + + /// No description provided for @smartFilterTag. + /// + /// In en, this message translates to: + /// **'Tag / Keyword'** String get smartFilterTag; + + /// No description provided for @smartFilterFavoritesOnly. + /// + /// In en, this message translates to: + /// **'Favorites only'** String get smartFilterFavoritesOnly; + + /// No description provided for @smartFilterMinMinutes. + /// + /// In en, this message translates to: + /// **'Minimum usage time (min)'** String get smartFilterMinMinutes; + + /// No description provided for @smartFilterSearch. + /// + /// In en, this message translates to: + /// **'Search term'** String get smartFilterSearch; + + /// No description provided for @emptyPlaylists. + /// + /// In en, this message translates to: + /// **'No playlists created yet.\nCreate manual collections or dynamic smart playlists.'** String get emptyPlaylists; + + /// No description provided for @emptyPlaylistItems. + /// + /// In en, this message translates to: + /// **'This playlist does not contain any media yet.'** String get emptyPlaylistItems; + + /// No description provided for @deletePlaylistTitle. + /// + /// In en, this message translates to: + /// **'Delete playlist?'** String get deletePlaylistTitle; + + /// No description provided for @deletePlaylistContent. + /// + /// In en, this message translates to: + /// **'Do you really want to delete this playlist? The media items will remain in your library.'** String get deletePlaylistContent; + + /// No description provided for @addToPlaylist. + /// + /// In en, this message translates to: + /// **'Add to playlist'** String get addToPlaylist; + + /// No description provided for @removeFromPlaylist. + /// + /// In en, this message translates to: + /// **'Remove from playlist'** String get removeFromPlaylist; + + /// No description provided for @addedToPlaylist. + /// + /// In en, this message translates to: + /// **'Added to playlist.'** String get addedToPlaylist; + + /// No description provided for @removedFromPlaylist. + /// + /// In en, this message translates to: + /// **'Removed from playlist.'** String get removedFromPlaylist; + + /// No description provided for @filterByTag. + /// + /// In en, this message translates to: + /// **'Tag Filter'** String get filterByTag; + + /// No description provided for @allTags. + /// + /// In en, this message translates to: + /// **'All Tags'** String get allTags; + + /// No description provided for @addTag. + /// + /// In en, this message translates to: + /// **'Add tag'** String get addTag; + + /// No description provided for @removeTag. + /// + /// In en, this message translates to: + /// **'Remove tag'** String get removeTag; + + /// No description provided for @noPlaylistsFound. + /// + /// In en, this message translates to: + /// **'No playlists available.'** String get noPlaylistsFound; - String error(String msg); - String foregroundMinutesLabel(int minutes); - String bgScanSubtitle(int hours); - String lastRun(String time); - String scanResult(int matched, int persisted); - String exportResult(String msg); - String importResult(String msg); - String pushPullStatus(String push, String pull); - String seasonEpisode(int s, int e); - String usageMinutes(int n); - String playlistItemCount(int count); + /// Number of items in a playlist. + /// + /// In en, this message translates to: + /// **'{count, plural, one {{count} item} other {{count} items}}'** + String playlistItemCount(num count); + + /// Tag and number of matching entries. + /// + /// In en, this message translates to: + /// **'#{tag} ({count})'** String tagCount(String tag, int count); } @@ -149,313 +841,61 @@ class _AppLocalizationsDelegate extends LocalizationsDelegate { const _AppLocalizationsDelegate(); - @override - bool isSupported(Locale locale) => - ['de', 'en'].contains(locale.languageCode); - @override Future load(Locale locale) { - final AppLocalizations loc = locale.languageCode == 'en' - ? _AppLocalizationsEn() - : _AppLocalizationsDe(); - return SynchronousFuture(loc); + return SynchronousFuture(lookupAppLocalizations(locale)); } + @override + bool isSupported(Locale locale) => [ + 'ar', + 'bn', + 'de', + 'en', + 'es', + 'fr', + 'hi', + 'ja', + 'pt', + 'ru', + 'zh', + ].contains(locale.languageCode); + @override bool shouldReload(_AppLocalizationsDelegate old) => false; } -class _AppLocalizationsDe extends AppLocalizations { - @override String get appTitle => 'MediaBrain'; - @override String get navLibrary => 'Bibliothek'; - @override String get navScan => 'Scan'; - @override String get navMore => 'Mehr'; - @override String get screenLibrary => 'Bibliothek'; - @override String get fabAdd => 'Eintrag'; - @override String get searchHint => 'Suchen (Titel, Künstler, Kanal …)'; - @override String get filterAll => 'Alle'; - @override String get filterFavorites => '⭐ Favoriten'; - @override String get emptyNoMatch => 'Keine Treffer für deine Filter.'; - @override String get emptyNoItems => - 'Noch leer.\nTippe auf den "Scan"-Tab, um deine Medien-Apps automatisch zu erfassen.'; - @override String get screenScan => 'Scan'; - @override String get scanDescription => - 'Sucht installierte Medien-Apps (Streaming, Musik, Podcasts, ' - 'Dokumente) und schreibt sie als Einträge in deine Bibliothek. ' - 'Mit aktiviertem Nutzungsdatenzugriff bekommst du zusätzlich die ' - 'tatsächliche Nutzungszeit.'; - @override String get btnAppScan => 'App-Scan (Aggregat-Nutzung)'; - @override String get bgScanTitle => 'Hintergrund-Scan'; - @override String get bgScanNow => 'Jetzt einmal im Hintergrund laufen lassen'; - @override String get bgScanNowSubtitle => - 'Sendet einen One-Off-Task an Android. Resultat erscheint nach 1–2 Minuten.'; - @override String get bgScanQueued => - 'Hintergrund-Scan in der Warteschlange — Resultat in ~1 min sichtbar'; - @override String get scanAndroidOnly => 'Scan ist nur unter Android verfügbar.'; - @override String get scanPermissionMissing => - 'Nutzungsdatenzugriff fehlt. Bitte in den System-Einstellungen für MediaBrain aktivieren:\n\n' - 'Einstellungen → Apps → Spezieller App-Zugriff → Nutzungsdatenzugriff → MediaBrain.'; - @override String get detectedTitle => '📡 Was wird erkannt?'; - @override String get detectedApps => - 'Netflix · Disney+ · Prime Video · Apple TV · YouTube · Twitch · ' - 'Spotify · Apple Music · YouTube Music · Tidal · Amazon Music · ' - 'Audible · Kindle · Pocket Casts · Google Podcasts · ' - 'Adobe Acrobat · Google Docs · Notion · Obsidian und weitere.'; - @override String get appScanTitle => '🔍 App-Scan'; - @override String get appScanDescription => - 'App-Scan: ein Eintrag pro Medien-App mit Gesamt-Nutzung ' - 'der letzten Tage (über Android-Nutzungsdaten).'; - @override String get neverText => 'nie'; - @override String get screenSettings => 'Einstellungen'; - @override String get sectionDataStatus => 'Datenstand'; - @override String get sectionFileSync => 'Datei-Sync'; - @override String get sectionServerSync => 'Server-Sync (Mac Studio)'; - @override String get sectionActions => 'Aktionen'; - @override String get settingsTotalEntries => 'Gesamt-Einträge'; - @override String get settingsLastScan => 'Letzter Scan'; - @override String get exportTitle => 'Export per Share'; - @override String get exportSubtitle => - 'JSON über Mail, OneDrive, Telegram an dich selbst …'; - @override String get importTitle => 'Import-Datei wählen'; - @override String get importSubtitle => 'Überschreibt den lokalen Stand'; - @override String get noServerConfigured => 'Kein Server konfiguriert'; - @override String get actionPush => 'Push'; - @override String get actionPull => 'Pull'; - @override String get refreshData => 'Datenstand aktualisieren'; - @override String get deleteAllData => 'Alle Bibliotheks-Daten löschen'; - @override String get deleteAllTitle => 'Alle Bibliotheks-Daten löschen?'; - @override String get deleteAllContent => - 'Entfernt alle Einträge aus dem lokalen MediaBrain.'; - @override String get cancel => 'Abbrechen'; - @override String get delete => 'Löschen'; - @override String get save => 'Speichern'; - @override String get libraryCleared => 'Bibliothek gelöscht.'; - @override String get cancelled => 'Abgebrochen.'; - @override String get serverConfigTitle => 'Server-Sync konfigurieren'; - @override String get fieldServerUrl => 'Server-URL'; - @override String get fieldServerUrlHint => 'z.B. macstudvonlukas:8082'; - @override String get fieldBearerToken => 'Bearer-Token'; - @override String get aboutTitle => 'MediaBrain Mobile'; - @override String get aboutSubtitle => - 'Standalone Flutter-App. Erkennt installierte Medien-Apps, ' - 'führt manuelle Einträge und synchronisiert per Datei oder Server.'; - @override String get detailDescription => 'Beschreibung'; - @override String get detailDetails => 'Details'; - @override String get detailTags => 'Tags'; - @override String get fieldArtist => 'Künstler'; - @override String get fieldAlbum => 'Album'; - @override String get fieldChannel => 'Kanal'; - @override String get fieldSeasonEpisodeLabel => 'Staffel/Folge'; - @override String get fieldLength => 'Länge'; - @override String get fieldLastOpened => 'Zuletzt geöffnet'; - @override String get fieldUsage => 'Nutzung'; - @override String get fieldProviderId => 'Provider-ID'; - @override String get fieldLocalPath => 'Lokaler Pfad'; - @override String get openPlayStore => 'Im Play Store öffnen'; - @override String get openBrowser => 'Im Browser öffnen'; - @override String get linkError => 'Link konnte nicht geöffnet werden.'; - @override String get dialogTitleNew => 'Neuer Eintrag'; - @override String get dialogTitleEdit => 'Eintrag bearbeiten'; - @override String get fieldTitle => 'Titel'; - @override String get categoryLabel => 'Kategorie'; - @override String get fieldArtistOptional => 'Künstler / Regie (optional)'; - @override String get fieldAlbumOptional => 'Album / Serie (optional)'; - @override String get fieldChannelOptional => 'Kanal / Quelle (optional)'; - @override String get fieldDescriptionOptional => 'Beschreibung (optional)'; - @override String get fieldTags => 'Tags (Komma-getrennt)'; - @override String get fieldTagsHint => 'z.B. Dokumentation, Wochenende'; - @override String get markAsFavorite => 'Als Favorit markieren'; - @override String get navPlaylists => 'Playlists'; - @override String get screenPlaylists => 'Playlists & Sammlungen'; - @override String get playlistTypeManual => 'Manuell'; - @override String get playlistTypeSmart => 'Smart (Dynamischer Filter)'; - @override String get dialogNewPlaylist => 'Neue Playlist'; - @override String get dialogEditPlaylist => 'Playlist bearbeiten'; - @override String get fieldPlaylistName => 'Name der Playlist'; - @override String get fieldPlaylistDescription => 'Beschreibung (optional)'; - @override String get fieldPlaylistType => 'Playlist-Typ'; - @override String get smartFilterCriteria => 'Smarte Filterkriterien'; - @override String get smartFilterCategory => 'Kategorie'; - @override String get smartFilterTag => 'Tag / Stichwort'; - @override String get smartFilterFavoritesOnly => 'Nur Favoriten'; - @override String get smartFilterMinMinutes => 'Minimale Nutzungszeit (Min.)'; - @override String get smartFilterSearch => 'Suchbegriff'; - @override String get emptyPlaylists => - 'Noch keine Playlists angelegt.\nErstelle manuelle Sammlungen oder dynamische Smart-Playlists.'; - @override String get emptyPlaylistItems => 'Diese Playlist enthält noch keine Medien.'; - @override String get deletePlaylistTitle => 'Playlist löschen?'; - @override String get deletePlaylistContent => - 'Möchtest du diese Playlist wirklich löschen? Die enthaltenen Medien bleiben in der Bibliothek erhalten.'; - @override String get addToPlaylist => 'Zu Playlist hinzufügen'; - @override String get removeFromPlaylist => 'Aus Playlist entfernen'; - @override String get addedToPlaylist => 'Zur Playlist hinzugefügt.'; - @override String get removedFromPlaylist => 'Aus Playlist entfernt.'; - @override String get filterByTag => 'Tag-Filter'; - @override String get allTags => 'Alle Tags'; - @override String get addTag => 'Tag hinzufügen'; - @override String get removeTag => 'Tag entfernen'; - @override String get noPlaylistsFound => 'Keine Playlists vorhanden.'; - - @override String error(String msg) => 'Fehler: $msg'; - @override String foregroundMinutesLabel(int minutes) => '$minutes Min insgesamt'; - @override String bgScanSubtitle(int hours) => - 'Alle ~$hours h scannen, auch wenn die App geschlossen ist.'; - @override String lastRun(String time) => 'Letzter Lauf: $time'; - @override String scanResult(int matched, int persisted) => - 'Apps-Scan: $matched Medien-Apps gefunden, $persisted Einträge übernommen.'; - @override String exportResult(String msg) => 'Export: $msg'; - @override String importResult(String msg) => 'Import: $msg'; - @override String pushPullStatus(String push, String pull) => - 'Push: $push · Pull: $pull'; - @override String seasonEpisode(int s, int e) => 'S$s E$e'; - @override String usageMinutes(int n) => '$n ${n == 1 ? 'Minute' : 'Minuten'}'; - @override String playlistItemCount(int count) => '$count ${count == 1 ? 'Eintrag' : 'Einträge'}'; - @override String tagCount(String tag, int count) => '#$tag ($count)'; -} +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'ar': + return AppLocalizationsAr(); + case 'bn': + return AppLocalizationsBn(); + case 'de': + return AppLocalizationsDe(); + case 'en': + return AppLocalizationsEn(); + case 'es': + return AppLocalizationsEs(); + case 'fr': + return AppLocalizationsFr(); + case 'hi': + return AppLocalizationsHi(); + case 'ja': + return AppLocalizationsJa(); + case 'pt': + return AppLocalizationsPt(); + case 'ru': + return AppLocalizationsRu(); + case 'zh': + return AppLocalizationsZh(); + } -class _AppLocalizationsEn extends AppLocalizations { - @override String get appTitle => 'MediaBrain'; - @override String get navLibrary => 'Library'; - @override String get navScan => 'Scan'; - @override String get navMore => 'More'; - @override String get screenLibrary => 'Library'; - @override String get fabAdd => 'Add'; - @override String get searchHint => 'Search (Title, Artist, Channel …)'; - @override String get filterAll => 'All'; - @override String get filterFavorites => '⭐ Favorites'; - @override String get emptyNoMatch => 'No results for your filters.'; - @override String get emptyNoItems => - 'Empty yet.\nTap the "Scan" tab to automatically detect your media apps.'; - @override String get screenScan => 'Scan'; - @override String get scanDescription => - 'Searches for installed media apps (streaming, music, podcasts, ' - 'documents) and adds them as entries to your library. ' - 'With enabled usage data access you additionally get the actual usage time.'; - @override String get btnAppScan => 'App Scan (Aggregate Usage)'; - @override String get bgScanTitle => 'Background Scan'; - @override String get bgScanNow => 'Run once in background now'; - @override String get bgScanNowSubtitle => - 'Sends a one-off task to Android. Result appears after 1–2 minutes.'; - @override String get bgScanQueued => - 'Background scan queued — result visible in ~1 min'; - @override String get scanAndroidOnly => 'Scan is only available on Android.'; - @override String get scanPermissionMissing => - 'Usage data access missing. Please enable in system settings for MediaBrain:\n\n' - 'Settings → Apps → Special app access → Usage data access → MediaBrain.'; - @override String get detectedTitle => '📡 What is detected?'; - @override String get detectedApps => - 'Netflix · Disney+ · Prime Video · Apple TV · YouTube · Twitch · ' - 'Spotify · Apple Music · YouTube Music · Tidal · Amazon Music · ' - 'Audible · Kindle · Pocket Casts · Google Podcasts · ' - 'Adobe Acrobat · Google Docs · Notion · Obsidian and more.'; - @override String get appScanTitle => '🔍 App Scan'; - @override String get appScanDescription => - 'App Scan: one entry per media app with total usage ' - 'over the last days (via Android usage stats).'; - @override String get neverText => 'never'; - @override String get screenSettings => 'Settings'; - @override String get sectionDataStatus => 'Data Status'; - @override String get sectionFileSync => 'File Sync'; - @override String get sectionServerSync => 'Server Sync (Mac Studio)'; - @override String get sectionActions => 'Actions'; - @override String get settingsTotalEntries => 'Total Entries'; - @override String get settingsLastScan => 'Last Scan'; - @override String get exportTitle => 'Export via Share'; - @override String get exportSubtitle => - 'JSON via Mail, OneDrive, Telegram to yourself …'; - @override String get importTitle => 'Choose Import File'; - @override String get importSubtitle => 'Overwrites local data'; - @override String get noServerConfigured => 'No server configured'; - @override String get actionPush => 'Push'; - @override String get actionPull => 'Pull'; - @override String get refreshData => 'Refresh data'; - @override String get deleteAllData => 'Delete all library data'; - @override String get deleteAllTitle => 'Delete all library data?'; - @override String get deleteAllContent => - 'Removes all entries from the local MediaBrain.'; - @override String get cancel => 'Cancel'; - @override String get delete => 'Delete'; - @override String get save => 'Save'; - @override String get libraryCleared => 'Library cleared.'; - @override String get cancelled => 'Cancelled.'; - @override String get serverConfigTitle => 'Configure Server Sync'; - @override String get fieldServerUrl => 'Server URL'; - @override String get fieldServerUrlHint => 'e.g. macstudvonlukas:8082'; - @override String get fieldBearerToken => 'Bearer Token'; - @override String get aboutTitle => 'MediaBrain Mobile'; - @override String get aboutSubtitle => - 'Standalone Flutter app. Detects installed media apps, ' - 'tracks manual entries and syncs via file or server.'; - @override String get detailDescription => 'Description'; - @override String get detailDetails => 'Details'; - @override String get detailTags => 'Tags'; - @override String get fieldArtist => 'Artist'; - @override String get fieldAlbum => 'Album'; - @override String get fieldChannel => 'Channel'; - @override String get fieldSeasonEpisodeLabel => 'Season/Episode'; - @override String get fieldLength => 'Length'; - @override String get fieldLastOpened => 'Last opened'; - @override String get fieldUsage => 'Usage'; - @override String get fieldProviderId => 'Provider ID'; - @override String get fieldLocalPath => 'Local Path'; - @override String get openPlayStore => 'Open in Play Store'; - @override String get openBrowser => 'Open in Browser'; - @override String get linkError => 'Link could not be opened.'; - @override String get dialogTitleNew => 'New Entry'; - @override String get dialogTitleEdit => 'Edit Entry'; - @override String get fieldTitle => 'Title'; - @override String get categoryLabel => 'Category'; - @override String get fieldArtistOptional => 'Artist / Director (optional)'; - @override String get fieldAlbumOptional => 'Album / Series (optional)'; - @override String get fieldChannelOptional => 'Channel / Source (optional)'; - @override String get fieldDescriptionOptional => 'Description (optional)'; - @override String get fieldTags => 'Tags (comma-separated)'; - @override String get fieldTagsHint => 'e.g. Documentary, Weekend'; - @override String get markAsFavorite => 'Mark as favorite'; - @override String get navPlaylists => 'Playlists'; - @override String get screenPlaylists => 'Playlists & Collections'; - @override String get playlistTypeManual => 'Manual'; - @override String get playlistTypeSmart => 'Smart (Dynamic Filter)'; - @override String get dialogNewPlaylist => 'New Playlist'; - @override String get dialogEditPlaylist => 'Edit Playlist'; - @override String get fieldPlaylistName => 'Playlist Name'; - @override String get fieldPlaylistDescription => 'Description (optional)'; - @override String get fieldPlaylistType => 'Playlist Type'; - @override String get smartFilterCriteria => 'Smart Filter Criteria'; - @override String get smartFilterCategory => 'Category'; - @override String get smartFilterTag => 'Tag / Keyword'; - @override String get smartFilterFavoritesOnly => 'Favorites only'; - @override String get smartFilterMinMinutes => 'Minimum usage time (min)'; - @override String get smartFilterSearch => 'Search term'; - @override String get emptyPlaylists => - 'No playlists created yet.\nCreate manual collections or dynamic smart playlists.'; - @override String get emptyPlaylistItems => 'This playlist does not contain any media yet.'; - @override String get deletePlaylistTitle => 'Delete playlist?'; - @override String get deletePlaylistContent => - 'Do you really want to delete this playlist? The media items will remain in your library.'; - @override String get addToPlaylist => 'Add to playlist'; - @override String get removeFromPlaylist => 'Remove from playlist'; - @override String get addedToPlaylist => 'Added to playlist.'; - @override String get removedFromPlaylist => 'Removed from playlist.'; - @override String get filterByTag => 'Tag Filter'; - @override String get allTags => 'All Tags'; - @override String get addTag => 'Add tag'; - @override String get removeTag => 'Remove tag'; - @override String get noPlaylistsFound => 'No playlists available.'; - - @override String error(String msg) => 'Error: $msg'; - @override String foregroundMinutesLabel(int minutes) => '$minutes min total'; - @override String bgScanSubtitle(int hours) => - 'Scan every ~$hours h, even when the app is closed.'; - @override String lastRun(String time) => 'Last run: $time'; - @override String scanResult(int matched, int persisted) => - 'App scan: $matched media apps found, $persisted entries added.'; - @override String exportResult(String msg) => 'Export: $msg'; - @override String importResult(String msg) => 'Import: $msg'; - @override String pushPullStatus(String push, String pull) => - 'Push: $push · Pull: $pull'; - @override String seasonEpisode(int s, int e) => 'S$s E$e'; - @override String usageMinutes(int n) => '$n ${n == 1 ? 'minute' : 'minutes'}'; - @override String playlistItemCount(int count) => '$count ${count == 1 ? 'item' : 'items'}'; - @override String tagCount(String tag, int count) => '#$tag ($count)'; + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); } diff --git a/flutter_port/lib/l10n/app_localizations_ar.dart b/flutter_port/lib/l10n/app_localizations_ar.dart new file mode 100644 index 0000000..cb74812 --- /dev/null +++ b/flutter_port/lib/l10n/app_localizations_ar.dart @@ -0,0 +1,415 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Arabic (`ar`). +class AppLocalizationsAr extends AppLocalizations { + AppLocalizationsAr([String locale = 'ar']) : super(locale); + + @override + String get appTitle => 'MediaBrain'; + + @override + String get navLibrary => 'المكتبة'; + + @override + String get navScan => 'فحص'; + + @override + String get navMore => 'المزيد'; + + @override + String get screenLibrary => 'المكتبة'; + + @override + String get fabAdd => 'إضافة'; + + @override + String get searchHint => 'بحث (العنوان، الفنان، القناة …)'; + + @override + String get filterAll => 'الكل'; + + @override + String get filterFavorites => '⭐ المفضلة'; + + @override + String get emptyNoMatch => 'لا توجد نتائج تطابق عوامل التصفية.'; + + @override + String get emptyNoItems => + 'لا توجد عناصر بعد.\nاضغط علامة تبويب الفحص لاكتشاف تطبيقات الوسائط تلقائيًا.'; + + @override + String get screenScan => 'فحص'; + + @override + String get scanDescription => + 'يبحث عن تطبيقات الوسائط المثبتة (البث والموسيقى والبودكاست والمستندات) ويضيفها إلى مكتبتك. عند تفعيل الوصول إلى بيانات الاستخدام، ستحصل أيضًا على مدة الاستخدام الفعلية.'; + + @override + String get btnAppScan => 'فحص التطبيقات (إجمالي الاستخدام)'; + + @override + String get bgScanTitle => 'الفحص في الخلفية'; + + @override + String get bgScanNow => 'التشغيل مرة واحدة في الخلفية الآن'; + + @override + String get bgScanNowSubtitle => + 'يرسل مهمة لمرة واحدة إلى Android. ستظهر النتيجة بعد دقيقة أو دقيقتين.'; + + @override + String get bgScanQueued => + 'وُضع الفحص في الخلفية في قائمة الانتظار — ستظهر النتيجة خلال دقيقة تقريبًا'; + + @override + String get scanAndroidOnly => 'الفحص متاح على Android فقط.'; + + @override + String get scanPermissionMissing => + 'إذن الوصول إلى بيانات الاستخدام غير متاح. فعّله في إعدادات النظام لتطبيق MediaBrain:\n\nالإعدادات → التطبيقات → الوصول الخاص للتطبيقات → الوصول إلى بيانات الاستخدام → MediaBrain.'; + + @override + String get detectedTitle => '📡 ما الذي يتم اكتشافه؟'; + + @override + String get detectedApps => + 'Netflix · Disney+ · Prime Video · Apple TV · YouTube · Twitch · Spotify · Apple Music · YouTube Music · Tidal · Amazon Music · Audible · Kindle · Pocket Casts · Google Podcasts · Adobe Acrobat · Google Docs · Notion · Obsidian وغيرها.'; + + @override + String get appScanTitle => '🔍 فحص التطبيقات'; + + @override + String get appScanDescription => + 'فحص التطبيقات: سجل واحد لكل تطبيق وسائط مع إجمالي الاستخدام خلال الأيام الماضية (وفق إحصاءات استخدام Android).'; + + @override + String get neverText => 'أبدًا'; + + @override + String get screenSettings => 'الإعدادات'; + + @override + String get sectionDataStatus => 'حالة البيانات'; + + @override + String get sectionFileSync => 'مزامنة الملفات'; + + @override + String get sectionServerSync => 'مزامنة الخادم (Mac Studio)'; + + @override + String get sectionActions => 'الإجراءات'; + + @override + String get settingsTotalEntries => 'إجمالي السجلات'; + + @override + String get settingsLastScan => 'آخر فحص'; + + @override + String get exportTitle => 'تصدير عبر المشاركة'; + + @override + String get exportSubtitle => + 'إرسال JSON إلى نفسك عبر البريد أو OneDrive أو Telegram …'; + + @override + String get importTitle => 'اختيار ملف للاستيراد'; + + @override + String get importSubtitle => 'يستبدل البيانات المحلية'; + + @override + String get noServerConfigured => 'لا يوجد خادم مهيأ'; + + @override + String get actionPush => 'إرسال'; + + @override + String get actionPull => 'استقبال'; + + @override + String get refreshData => 'تحديث البيانات'; + + @override + String get deleteAllData => 'حذف جميع بيانات المكتبة'; + + @override + String get deleteAllTitle => 'حذف جميع بيانات المكتبة؟'; + + @override + String get deleteAllContent => 'يحذف جميع السجلات من MediaBrain المحلي.'; + + @override + String get cancel => 'إلغاء'; + + @override + String get delete => 'حذف'; + + @override + String get save => 'حفظ'; + + @override + String get libraryCleared => 'تم إفراغ المكتبة.'; + + @override + String get cancelled => 'تم الإلغاء.'; + + @override + String get serverConfigTitle => 'تهيئة مزامنة الخادم'; + + @override + String get fieldServerUrl => 'عنوان الخادم'; + + @override + String get fieldServerUrlHint => 'مثال: macstudvonlukas:8082'; + + @override + String get fieldBearerToken => 'رمز Bearer'; + + @override + String get aboutTitle => 'MediaBrain Mobile'; + + @override + String get aboutSubtitle => + 'تطبيق Flutter مستقل. يكتشف تطبيقات الوسائط المثبتة، ويتتبع الإدخالات اليدوية، ويزامن عبر ملف أو خادم.'; + + @override + String get detailDescription => 'الوصف'; + + @override + String get detailDetails => 'التفاصيل'; + + @override + String get detailTags => 'الوسوم'; + + @override + String get fieldArtist => 'الفنان'; + + @override + String get fieldAlbum => 'الألبوم'; + + @override + String get fieldChannel => 'القناة'; + + @override + String get fieldSeasonEpisodeLabel => 'الموسم/الحلقة'; + + @override + String get fieldLength => 'المدة'; + + @override + String get fieldLastOpened => 'آخر فتح'; + + @override + String get fieldUsage => 'الاستخدام'; + + @override + String get fieldProviderId => 'معرّف المزوّد'; + + @override + String get fieldLocalPath => 'المسار المحلي'; + + @override + String get openPlayStore => 'فتح في Play Store'; + + @override + String get openBrowser => 'فتح في المتصفح'; + + @override + String get linkError => 'تعذر فتح الرابط.'; + + @override + String get dialogTitleNew => 'إدخال جديد'; + + @override + String get dialogTitleEdit => 'تعديل الإدخال'; + + @override + String get fieldTitle => 'العنوان'; + + @override + String get categoryLabel => 'الفئة'; + + @override + String get fieldArtistOptional => 'الفنان / المخرج (اختياري)'; + + @override + String get fieldAlbumOptional => 'الألبوم / المسلسل (اختياري)'; + + @override + String get fieldChannelOptional => 'القناة / المصدر (اختياري)'; + + @override + String get fieldDescriptionOptional => 'الوصف (اختياري)'; + + @override + String get fieldTags => 'الوسوم (مفصولة بفواصل)'; + + @override + String get fieldTagsHint => 'مثال: وثائقي، عطلة نهاية الأسبوع'; + + @override + String get markAsFavorite => 'وضع علامة في المفضلة'; + + @override + String error(String msg) { + return 'خطأ: $msg'; + } + + @override + String foregroundMinutesLabel(int minutes) { + return 'الإجمالي $minutes دقيقة'; + } + + @override + String bgScanSubtitle(int hours) { + return 'افحص كل نحو $hours ساعات، حتى عندما يكون التطبيق مغلقًا.'; + } + + @override + String lastRun(String time) { + return 'آخر تشغيل: $time'; + } + + @override + String scanResult(int matched, int persisted) { + return 'فحص التطبيقات: تم العثور على $matched من تطبيقات الوسائط وإضافة $persisted من السجلات.'; + } + + @override + String exportResult(String msg) { + return 'تصدير: $msg'; + } + + @override + String importResult(String msg) { + return 'استيراد: $msg'; + } + + @override + String pushPullStatus(String push, String pull) { + return 'إرسال: $push · استقبال: $pull'; + } + + @override + String seasonEpisode(int s, int e) { + return 'الموسم $s الحلقة $e'; + } + + @override + String usageMinutes(int n) { + return '$n دقيقة'; + } + + @override + String get navPlaylists => 'قوائم التشغيل'; + + @override + String get screenPlaylists => 'قوائم التشغيل والمجموعات'; + + @override + String get playlistTypeManual => 'يدوية'; + + @override + String get playlistTypeSmart => 'ذكية (تصفية ديناميكية)'; + + @override + String get dialogNewPlaylist => 'قائمة تشغيل جديدة'; + + @override + String get dialogEditPlaylist => 'تعديل قائمة التشغيل'; + + @override + String get fieldPlaylistName => 'اسم قائمة التشغيل'; + + @override + String get fieldPlaylistDescription => 'الوصف (اختياري)'; + + @override + String get fieldPlaylistType => 'نوع قائمة التشغيل'; + + @override + String get smartFilterCriteria => 'معايير التصفية الذكية'; + + @override + String get smartFilterCategory => 'الفئة'; + + @override + String get smartFilterTag => 'وسم / كلمة مفتاحية'; + + @override + String get smartFilterFavoritesOnly => 'المفضلة فقط'; + + @override + String get smartFilterMinMinutes => 'الحد الأدنى لوقت الاستخدام (دقيقة)'; + + @override + String get smartFilterSearch => 'مصطلح البحث'; + + @override + String get emptyPlaylists => + 'لم تُنشأ قوائم تشغيل بعد.\nأنشئ مجموعات يدوية أو قوائم تشغيل ذكية ديناميكية.'; + + @override + String get emptyPlaylistItems => 'لا تحتوي قائمة التشغيل هذه على وسائط بعد.'; + + @override + String get deletePlaylistTitle => 'حذف قائمة التشغيل؟'; + + @override + String get deletePlaylistContent => + 'هل تريد حقًا حذف قائمة التشغيل هذه؟ ستبقى عناصر الوسائط فيها في مكتبتك.'; + + @override + String get addToPlaylist => 'إضافة إلى قائمة التشغيل'; + + @override + String get removeFromPlaylist => 'إزالة من قائمة التشغيل'; + + @override + String get addedToPlaylist => 'تمت الإضافة إلى قائمة التشغيل.'; + + @override + String get removedFromPlaylist => 'تمت الإزالة من قائمة التشغيل.'; + + @override + String get filterByTag => 'تصفية حسب الوسم'; + + @override + String get allTags => 'كل الوسوم'; + + @override + String get addTag => 'إضافة وسم'; + + @override + String get removeTag => 'إزالة الوسم'; + + @override + String get noPlaylistsFound => 'لا توجد قوائم تشغيل متاحة.'; + + @override + String playlistItemCount(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count عنصر', + many: '$count عنصرًا', + few: '$count عناصر', + two: '$count عنصران', + one: '$count عنصر', + zero: '$count عنصر', + ); + return '$_temp0'; + } + + @override + String tagCount(String tag, int count) { + return '#$tag ($count)'; + } +} diff --git a/flutter_port/lib/l10n/app_localizations_bn.dart b/flutter_port/lib/l10n/app_localizations_bn.dart new file mode 100644 index 0000000..1368de8 --- /dev/null +++ b/flutter_port/lib/l10n/app_localizations_bn.dart @@ -0,0 +1,413 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Bengali Bangla (`bn`). +class AppLocalizationsBn extends AppLocalizations { + AppLocalizationsBn([String locale = 'bn']) : super(locale); + + @override + String get appTitle => 'MediaBrain'; + + @override + String get navLibrary => 'লাইব্রেরি'; + + @override + String get navScan => 'স্ক্যান'; + + @override + String get navMore => 'আরও'; + + @override + String get screenLibrary => 'লাইব্রেরি'; + + @override + String get fabAdd => 'যোগ করুন'; + + @override + String get searchHint => 'খুঁজুন (শিরোনাম, শিল্পী, চ্যানেল …)'; + + @override + String get filterAll => 'সব'; + + @override + String get filterFavorites => '⭐ পছন্দের'; + + @override + String get emptyNoMatch => + 'আপনার ফিল্টারের সঙ্গে মেলে এমন কিছু পাওয়া যায়নি।'; + + @override + String get emptyNoItems => + 'এখনও খালি।\nমিডিয়া অ্যাপ স্বয়ংক্রিয়ভাবে শনাক্ত করতে স্ক্যান ট্যাবে ট্যাপ করুন।'; + + @override + String get screenScan => 'স্ক্যান'; + + @override + String get scanDescription => + 'ইনস্টল করা মিডিয়া অ্যাপ (স্ট্রিমিং, সংগীত, পডকাস্ট, নথি) খুঁজে সেগুলো লাইব্রেরিতে যোগ করে। ব্যবহার-ডেটা অ্যাক্সেস চালু থাকলে প্রকৃত ব্যবহারের সময়ও পাওয়া যাবে।'; + + @override + String get btnAppScan => 'অ্যাপ স্ক্যান (সমষ্টিগত ব্যবহার)'; + + @override + String get bgScanTitle => 'ব্যাকগ্রাউন্ড স্ক্যান'; + + @override + String get bgScanNow => 'এখন একবার ব্যাকগ্রাউন্ডে চালান'; + + @override + String get bgScanNowSubtitle => + 'Android-এ একবারের জন্য একটি কাজ পাঠায়। ১–২ মিনিট পরে ফলাফল দেখা যাবে।'; + + @override + String get bgScanQueued => + 'ব্যাকগ্রাউন্ড স্ক্যান সারিতে আছে — প্রায় ১ মিনিটে ফলাফল দেখা যাবে'; + + @override + String get scanAndroidOnly => 'স্ক্যান শুধু Android-এ উপলভ্য।'; + + @override + String get scanPermissionMissing => + 'ব্যবহার-ডেটা অ্যাক্সেস নেই। MediaBrain-এর জন্য সিস্টেম সেটিংসে এটি চালু করুন:\n\nসেটিংস → অ্যাপস → বিশেষ অ্যাপ অ্যাক্সেস → ব্যবহার-ডেটা অ্যাক্সেস → MediaBrain।'; + + @override + String get detectedTitle => '📡 কী শনাক্ত হয়?'; + + @override + String get detectedApps => + 'Netflix · Disney+ · Prime Video · Apple TV · YouTube · Twitch · Spotify · Apple Music · YouTube Music · Tidal · Amazon Music · Audible · Kindle · Pocket Casts · Google Podcasts · Adobe Acrobat · Google Docs · Notion · Obsidian এবং আরও।'; + + @override + String get appScanTitle => '🔍 অ্যাপ স্ক্যান'; + + @override + String get appScanDescription => + 'অ্যাপ স্ক্যান: প্রতিটি মিডিয়া অ্যাপের জন্য একটি এন্ট্রি, যেখানে সাম্প্রতিক দিনগুলোর মোট ব্যবহার দেখানো হয় (Android ব্যবহারের পরিসংখ্যান থেকে)।'; + + @override + String get neverText => 'কখনও নয়'; + + @override + String get screenSettings => 'সেটিংস'; + + @override + String get sectionDataStatus => 'ডেটার অবস্থা'; + + @override + String get sectionFileSync => 'ফাইল সিঙ্ক'; + + @override + String get sectionServerSync => 'সার্ভার সিঙ্ক (Mac Studio)'; + + @override + String get sectionActions => 'কাজ'; + + @override + String get settingsTotalEntries => 'মোট এন্ট্রি'; + + @override + String get settingsLastScan => 'শেষ স্ক্যান'; + + @override + String get exportTitle => 'শেয়ার দিয়ে রপ্তানি'; + + @override + String get exportSubtitle => + 'ইমেল, OneDrive বা Telegram দিয়ে নিজের কাছে JSON পাঠান …'; + + @override + String get importTitle => 'আমদানি ফাইল বেছে নিন'; + + @override + String get importSubtitle => 'স্থানীয় ডেটা মুছে প্রতিস্থাপন করে'; + + @override + String get noServerConfigured => 'কোনো সার্ভার কনফিগার করা নেই'; + + @override + String get actionPush => 'পুশ'; + + @override + String get actionPull => 'পুল'; + + @override + String get refreshData => 'ডেটা রিফ্রেশ করুন'; + + @override + String get deleteAllData => 'লাইব্রেরির সব ডেটা মুছুন'; + + @override + String get deleteAllTitle => 'লাইব্রেরির সব ডেটা মুছবেন?'; + + @override + String get deleteAllContent => + 'স্থানীয় MediaBrain থেকে সব এন্ট্রি সরিয়ে দেয়।'; + + @override + String get cancel => 'বাতিল'; + + @override + String get delete => 'মুছুন'; + + @override + String get save => 'সংরক্ষণ'; + + @override + String get libraryCleared => 'লাইব্রেরি খালি করা হয়েছে।'; + + @override + String get cancelled => 'বাতিল হয়েছে।'; + + @override + String get serverConfigTitle => 'সার্ভার সিঙ্ক কনফিগার করুন'; + + @override + String get fieldServerUrl => 'সার্ভার URL'; + + @override + String get fieldServerUrlHint => 'যেমন: macstudvonlukas:8082'; + + @override + String get fieldBearerToken => 'Bearer টোকেন'; + + @override + String get aboutTitle => 'MediaBrain Mobile'; + + @override + String get aboutSubtitle => + 'স্বতন্ত্র Flutter অ্যাপ। ইনস্টল করা মিডিয়া অ্যাপ শনাক্ত করে, হাতে যোগ করা এন্ট্রি রাখে এবং ফাইল বা সার্ভারের মাধ্যমে সিঙ্ক করে।'; + + @override + String get detailDescription => 'বিবরণ'; + + @override + String get detailDetails => 'বিস্তারিত'; + + @override + String get detailTags => 'ট্যাগ'; + + @override + String get fieldArtist => 'শিল্পী'; + + @override + String get fieldAlbum => 'অ্যালবাম'; + + @override + String get fieldChannel => 'চ্যানেল'; + + @override + String get fieldSeasonEpisodeLabel => 'সিজন/পর্ব'; + + @override + String get fieldLength => 'সময়কাল'; + + @override + String get fieldLastOpened => 'শেষ খোলা'; + + @override + String get fieldUsage => 'ব্যবহার'; + + @override + String get fieldProviderId => 'প্রোভাইডার ID'; + + @override + String get fieldLocalPath => 'স্থানীয় পথ'; + + @override + String get openPlayStore => 'Play Store-এ খুলুন'; + + @override + String get openBrowser => 'ব্রাউজারে খুলুন'; + + @override + String get linkError => 'লিংক খোলা যায়নি।'; + + @override + String get dialogTitleNew => 'নতুন এন্ট্রি'; + + @override + String get dialogTitleEdit => 'এন্ট্রি সম্পাদনা করুন'; + + @override + String get fieldTitle => 'শিরোনাম'; + + @override + String get categoryLabel => 'বিভাগ'; + + @override + String get fieldArtistOptional => 'শিল্পী / পরিচালক (ঐচ্ছিক)'; + + @override + String get fieldAlbumOptional => 'অ্যালবাম / সিরিজ (ঐচ্ছিক)'; + + @override + String get fieldChannelOptional => 'চ্যানেল / উৎস (ঐচ্ছিক)'; + + @override + String get fieldDescriptionOptional => 'বিবরণ (ঐচ্ছিক)'; + + @override + String get fieldTags => 'ট্যাগ (কমা দিয়ে আলাদা)'; + + @override + String get fieldTagsHint => 'যেমন: প্রামাণ্যচিত্র, ছুটির দিন'; + + @override + String get markAsFavorite => 'পছন্দের হিসেবে চিহ্নিত করুন'; + + @override + String error(String msg) { + return 'ত্রুটি: $msg'; + } + + @override + String foregroundMinutesLabel(int minutes) { + return 'মোট $minutes মিনিট'; + } + + @override + String bgScanSubtitle(int hours) { + return 'অ্যাপ বন্ধ থাকলেও প্রতি ~$hours ঘণ্টায় স্ক্যান করুন।'; + } + + @override + String lastRun(String time) { + return 'শেষ চালানো: $time'; + } + + @override + String scanResult(int matched, int persisted) { + return 'অ্যাপ স্ক্যান: $matchedটি মিডিয়া অ্যাপ পাওয়া গেছে, $persistedটি এন্ট্রি যোগ হয়েছে।'; + } + + @override + String exportResult(String msg) { + return 'রপ্তানি: $msg'; + } + + @override + String importResult(String msg) { + return 'আমদানি: $msg'; + } + + @override + String pushPullStatus(String push, String pull) { + return 'পুশ: $push · পুল: $pull'; + } + + @override + String seasonEpisode(int s, int e) { + return 'S$s E$e'; + } + + @override + String usageMinutes(int n) { + return '$n মিনিট'; + } + + @override + String get navPlaylists => 'প্লেলিস্ট'; + + @override + String get screenPlaylists => 'প্লেলিস্ট ও সংগ্রহ'; + + @override + String get playlistTypeManual => 'ম্যানুয়াল'; + + @override + String get playlistTypeSmart => 'স্মার্ট (ডায়নামিক ফিল্টার)'; + + @override + String get dialogNewPlaylist => 'নতুন প্লেলিস্ট'; + + @override + String get dialogEditPlaylist => 'প্লেলিস্ট সম্পাদনা করুন'; + + @override + String get fieldPlaylistName => 'প্লেলিস্টের নাম'; + + @override + String get fieldPlaylistDescription => 'বিবরণ (ঐচ্ছিক)'; + + @override + String get fieldPlaylistType => 'প্লেলিস্টের ধরন'; + + @override + String get smartFilterCriteria => 'স্মার্ট ফিল্টারের মানদণ্ড'; + + @override + String get smartFilterCategory => 'বিভাগ'; + + @override + String get smartFilterTag => 'ট্যাগ / কীওয়ার্ড'; + + @override + String get smartFilterFavoritesOnly => 'শুধু পছন্দের'; + + @override + String get smartFilterMinMinutes => 'সর্বনিম্ন ব্যবহারের সময় (মিনিট)'; + + @override + String get smartFilterSearch => 'অনুসন্ধানের শব্দ'; + + @override + String get emptyPlaylists => + 'এখনও কোনো প্লেলিস্ট তৈরি হয়নি।\nম্যানুয়াল সংগ্রহ বা ডায়নামিক স্মার্ট প্লেলিস্ট তৈরি করুন।'; + + @override + String get emptyPlaylistItems => 'এই প্লেলিস্টে এখনও কোনো মিডিয়া নেই।'; + + @override + String get deletePlaylistTitle => 'প্লেলিস্ট মুছবেন?'; + + @override + String get deletePlaylistContent => + 'আপনি কি সত্যিই এই প্লেলিস্টটি মুছতে চান? এর মিডিয়া লাইব্রেরিতে থেকে যাবে।'; + + @override + String get addToPlaylist => 'প্লেলিস্টে যোগ করুন'; + + @override + String get removeFromPlaylist => 'প্লেলিস্ট থেকে সরান'; + + @override + String get addedToPlaylist => 'প্লেলিস্টে যোগ করা হয়েছে।'; + + @override + String get removedFromPlaylist => 'প্লেলিস্ট থেকে সরানো হয়েছে।'; + + @override + String get filterByTag => 'ট্যাগ ফিল্টার'; + + @override + String get allTags => 'সব ট্যাগ'; + + @override + String get addTag => 'ট্যাগ যোগ করুন'; + + @override + String get removeTag => 'ট্যাগ সরান'; + + @override + String get noPlaylistsFound => 'কোনো প্লেলিস্ট নেই।'; + + @override + String playlistItemCount(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$countটি আইটেম', + one: '$countটি আইটেম', + ); + return '$_temp0'; + } + + @override + String tagCount(String tag, int count) { + return '#$tag ($count)'; + } +} diff --git a/flutter_port/lib/l10n/app_localizations_de.dart b/flutter_port/lib/l10n/app_localizations_de.dart new file mode 100644 index 0000000..55dde6d --- /dev/null +++ b/flutter_port/lib/l10n/app_localizations_de.dart @@ -0,0 +1,412 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for German (`de`). +class AppLocalizationsDe extends AppLocalizations { + AppLocalizationsDe([String locale = 'de']) : super(locale); + + @override + String get appTitle => 'MediaBrain'; + + @override + String get navLibrary => 'Bibliothek'; + + @override + String get navScan => 'Scan'; + + @override + String get navMore => 'Mehr'; + + @override + String get screenLibrary => 'Bibliothek'; + + @override + String get fabAdd => 'Eintrag'; + + @override + String get searchHint => 'Suchen (Titel, Künstler, Kanal …)'; + + @override + String get filterAll => 'Alle'; + + @override + String get filterFavorites => '⭐ Favoriten'; + + @override + String get emptyNoMatch => 'Keine Treffer für deine Filter.'; + + @override + String get emptyNoItems => + 'Noch leer.\nTippe auf den \"Scan\"-Tab, um deine Medien-Apps automatisch zu erfassen.'; + + @override + String get screenScan => 'Scan'; + + @override + String get scanDescription => + 'Sucht installierte Medien-Apps (Streaming, Musik, Podcasts, Dokumente) und schreibt sie als Einträge in deine Bibliothek. Mit aktiviertem Nutzungsdatenzugriff bekommst du zusätzlich die tatsächliche Nutzungszeit.'; + + @override + String get btnAppScan => 'App-Scan (Aggregat-Nutzung)'; + + @override + String get bgScanTitle => 'Hintergrund-Scan'; + + @override + String get bgScanNow => 'Jetzt einmal im Hintergrund laufen lassen'; + + @override + String get bgScanNowSubtitle => + 'Sendet einen One-Off-Task an Android. Resultat erscheint nach 1–2 Minuten.'; + + @override + String get bgScanQueued => + 'Hintergrund-Scan in der Warteschlange — Resultat in ~1 min sichtbar'; + + @override + String get scanAndroidOnly => 'Scan ist nur unter Android verfügbar.'; + + @override + String get scanPermissionMissing => + 'Nutzungsdatenzugriff fehlt. Bitte in den System-Einstellungen für MediaBrain aktivieren:\n\nEinstellungen → Apps → Spezieller App-Zugriff → Nutzungsdatenzugriff → MediaBrain.'; + + @override + String get detectedTitle => '📡 Was wird erkannt?'; + + @override + String get detectedApps => + 'Netflix · Disney+ · Prime Video · Apple TV · YouTube · Twitch · Spotify · Apple Music · YouTube Music · Tidal · Amazon Music · Audible · Kindle · Pocket Casts · Google Podcasts · Adobe Acrobat · Google Docs · Notion · Obsidian und weitere.'; + + @override + String get appScanTitle => '🔍 App-Scan'; + + @override + String get appScanDescription => + 'App-Scan: ein Eintrag pro Medien-App mit Gesamt-Nutzung der letzten Tage (über Android-Nutzungsdaten).'; + + @override + String get neverText => 'nie'; + + @override + String get screenSettings => 'Einstellungen'; + + @override + String get sectionDataStatus => 'Datenstand'; + + @override + String get sectionFileSync => 'Datei-Sync'; + + @override + String get sectionServerSync => 'Server-Sync (Mac Studio)'; + + @override + String get sectionActions => 'Aktionen'; + + @override + String get settingsTotalEntries => 'Gesamt-Einträge'; + + @override + String get settingsLastScan => 'Letzter Scan'; + + @override + String get exportTitle => 'Export per Share'; + + @override + String get exportSubtitle => + 'JSON über Mail, OneDrive, Telegram an dich selbst …'; + + @override + String get importTitle => 'Import-Datei wählen'; + + @override + String get importSubtitle => 'Überschreibt den lokalen Stand'; + + @override + String get noServerConfigured => 'Kein Server konfiguriert'; + + @override + String get actionPush => 'Push'; + + @override + String get actionPull => 'Pull'; + + @override + String get refreshData => 'Datenstand aktualisieren'; + + @override + String get deleteAllData => 'Alle Bibliotheks-Daten löschen'; + + @override + String get deleteAllTitle => 'Alle Bibliotheks-Daten löschen?'; + + @override + String get deleteAllContent => + 'Entfernt alle Einträge aus dem lokalen MediaBrain.'; + + @override + String get cancel => 'Abbrechen'; + + @override + String get delete => 'Löschen'; + + @override + String get save => 'Speichern'; + + @override + String get libraryCleared => 'Bibliothek gelöscht.'; + + @override + String get cancelled => 'Abgebrochen.'; + + @override + String get serverConfigTitle => 'Server-Sync konfigurieren'; + + @override + String get fieldServerUrl => 'Server-URL'; + + @override + String get fieldServerUrlHint => 'z.B. macstudvonlukas:8082'; + + @override + String get fieldBearerToken => 'Bearer-Token'; + + @override + String get aboutTitle => 'MediaBrain Mobile'; + + @override + String get aboutSubtitle => + 'Standalone Flutter-App. Erkennt installierte Medien-Apps, führt manuelle Einträge und synchronisiert per Datei oder Server.'; + + @override + String get detailDescription => 'Beschreibung'; + + @override + String get detailDetails => 'Details'; + + @override + String get detailTags => 'Tags'; + + @override + String get fieldArtist => 'Künstler'; + + @override + String get fieldAlbum => 'Album'; + + @override + String get fieldChannel => 'Kanal'; + + @override + String get fieldSeasonEpisodeLabel => 'Staffel/Folge'; + + @override + String get fieldLength => 'Länge'; + + @override + String get fieldLastOpened => 'Zuletzt geöffnet'; + + @override + String get fieldUsage => 'Nutzung'; + + @override + String get fieldProviderId => 'Provider-ID'; + + @override + String get fieldLocalPath => 'Lokaler Pfad'; + + @override + String get openPlayStore => 'Im Play Store öffnen'; + + @override + String get openBrowser => 'Im Browser öffnen'; + + @override + String get linkError => 'Link konnte nicht geöffnet werden.'; + + @override + String get dialogTitleNew => 'Neuer Eintrag'; + + @override + String get dialogTitleEdit => 'Eintrag bearbeiten'; + + @override + String get fieldTitle => 'Titel'; + + @override + String get categoryLabel => 'Kategorie'; + + @override + String get fieldArtistOptional => 'Künstler / Regie (optional)'; + + @override + String get fieldAlbumOptional => 'Album / Serie (optional)'; + + @override + String get fieldChannelOptional => 'Kanal / Quelle (optional)'; + + @override + String get fieldDescriptionOptional => 'Beschreibung (optional)'; + + @override + String get fieldTags => 'Tags (Komma-getrennt)'; + + @override + String get fieldTagsHint => 'z.B. Dokumentation, Wochenende'; + + @override + String get markAsFavorite => 'Als Favorit markieren'; + + @override + String error(String msg) { + return 'Fehler: $msg'; + } + + @override + String foregroundMinutesLabel(int minutes) { + return '$minutes Min insgesamt'; + } + + @override + String bgScanSubtitle(int hours) { + return 'Alle ~$hours h scannen, auch wenn die App geschlossen ist.'; + } + + @override + String lastRun(String time) { + return 'Letzter Lauf: $time'; + } + + @override + String scanResult(int matched, int persisted) { + return 'Apps-Scan: $matched Medien-Apps gefunden, $persisted Einträge übernommen.'; + } + + @override + String exportResult(String msg) { + return 'Export: $msg'; + } + + @override + String importResult(String msg) { + return 'Import: $msg'; + } + + @override + String pushPullStatus(String push, String pull) { + return 'Push: $push · Pull: $pull'; + } + + @override + String seasonEpisode(int s, int e) { + return 'S$s E$e'; + } + + @override + String usageMinutes(int n) { + return '$n Minuten'; + } + + @override + String get navPlaylists => 'Playlists'; + + @override + String get screenPlaylists => 'Playlists & Sammlungen'; + + @override + String get playlistTypeManual => 'Manuell'; + + @override + String get playlistTypeSmart => 'Smart (Dynamischer Filter)'; + + @override + String get dialogNewPlaylist => 'Neue Playlist'; + + @override + String get dialogEditPlaylist => 'Playlist bearbeiten'; + + @override + String get fieldPlaylistName => 'Name der Playlist'; + + @override + String get fieldPlaylistDescription => 'Beschreibung (optional)'; + + @override + String get fieldPlaylistType => 'Playlist-Typ'; + + @override + String get smartFilterCriteria => 'Smarte Filterkriterien'; + + @override + String get smartFilterCategory => 'Kategorie'; + + @override + String get smartFilterTag => 'Tag / Stichwort'; + + @override + String get smartFilterFavoritesOnly => 'Nur Favoriten'; + + @override + String get smartFilterMinMinutes => 'Minimale Nutzungszeit (Min.)'; + + @override + String get smartFilterSearch => 'Suchbegriff'; + + @override + String get emptyPlaylists => + 'Noch keine Playlists angelegt.\nErstelle manuelle Sammlungen oder dynamische Smart-Playlists.'; + + @override + String get emptyPlaylistItems => 'Diese Playlist enthält noch keine Medien.'; + + @override + String get deletePlaylistTitle => 'Playlist löschen?'; + + @override + String get deletePlaylistContent => + 'Möchtest du diese Playlist wirklich löschen? Die enthaltenen Medien bleiben in der Bibliothek erhalten.'; + + @override + String get addToPlaylist => 'Zu Playlist hinzufügen'; + + @override + String get removeFromPlaylist => 'Aus Playlist entfernen'; + + @override + String get addedToPlaylist => 'Zur Playlist hinzugefügt.'; + + @override + String get removedFromPlaylist => 'Aus Playlist entfernt.'; + + @override + String get filterByTag => 'Tag-Filter'; + + @override + String get allTags => 'Alle Tags'; + + @override + String get addTag => 'Tag hinzufügen'; + + @override + String get removeTag => 'Tag entfernen'; + + @override + String get noPlaylistsFound => 'Keine Playlists vorhanden.'; + + @override + String playlistItemCount(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Einträge', + one: '$count Eintrag', + ); + return '$_temp0'; + } + + @override + String tagCount(String tag, int count) { + return '#$tag ($count)'; + } +} diff --git a/flutter_port/lib/l10n/app_localizations_en.dart b/flutter_port/lib/l10n/app_localizations_en.dart new file mode 100644 index 0000000..8cc3d9a --- /dev/null +++ b/flutter_port/lib/l10n/app_localizations_en.dart @@ -0,0 +1,413 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get appTitle => 'MediaBrain'; + + @override + String get navLibrary => 'Library'; + + @override + String get navScan => 'Scan'; + + @override + String get navMore => 'More'; + + @override + String get screenLibrary => 'Library'; + + @override + String get fabAdd => 'Add'; + + @override + String get searchHint => 'Search (Title, Artist, Channel …)'; + + @override + String get filterAll => 'All'; + + @override + String get filterFavorites => '⭐ Favorites'; + + @override + String get emptyNoMatch => 'No results for your filters.'; + + @override + String get emptyNoItems => + 'Empty yet.\nTap the \"Scan\" tab to automatically detect your media apps.'; + + @override + String get screenScan => 'Scan'; + + @override + String get scanDescription => + 'Searches for installed media apps (streaming, music, podcasts, documents) and adds them as entries to your library. With enabled usage data access you additionally get the actual usage time.'; + + @override + String get btnAppScan => 'App Scan (Aggregate Usage)'; + + @override + String get bgScanTitle => 'Background Scan'; + + @override + String get bgScanNow => 'Run once in background now'; + + @override + String get bgScanNowSubtitle => + 'Sends a one-off task to Android. Result appears after 1–2 minutes.'; + + @override + String get bgScanQueued => + 'Background scan queued — result visible in ~1 min'; + + @override + String get scanAndroidOnly => 'Scan is only available on Android.'; + + @override + String get scanPermissionMissing => + 'Usage data access missing. Please enable in system settings for MediaBrain:\n\nSettings → Apps → Special app access → Usage data access → MediaBrain.'; + + @override + String get detectedTitle => '📡 What is detected?'; + + @override + String get detectedApps => + 'Netflix · Disney+ · Prime Video · Apple TV · YouTube · Twitch · Spotify · Apple Music · YouTube Music · Tidal · Amazon Music · Audible · Kindle · Pocket Casts · Google Podcasts · Adobe Acrobat · Google Docs · Notion · Obsidian and more.'; + + @override + String get appScanTitle => '🔍 App Scan'; + + @override + String get appScanDescription => + 'App Scan: one entry per media app with total usage over the last days (via Android usage stats).'; + + @override + String get neverText => 'never'; + + @override + String get screenSettings => 'Settings'; + + @override + String get sectionDataStatus => 'Data Status'; + + @override + String get sectionFileSync => 'File Sync'; + + @override + String get sectionServerSync => 'Server Sync (Mac Studio)'; + + @override + String get sectionActions => 'Actions'; + + @override + String get settingsTotalEntries => 'Total Entries'; + + @override + String get settingsLastScan => 'Last Scan'; + + @override + String get exportTitle => 'Export via Share'; + + @override + String get exportSubtitle => + 'JSON via Mail, OneDrive, Telegram to yourself …'; + + @override + String get importTitle => 'Choose Import File'; + + @override + String get importSubtitle => 'Overwrites local data'; + + @override + String get noServerConfigured => 'No server configured'; + + @override + String get actionPush => 'Push'; + + @override + String get actionPull => 'Pull'; + + @override + String get refreshData => 'Refresh data'; + + @override + String get deleteAllData => 'Delete all library data'; + + @override + String get deleteAllTitle => 'Delete all library data?'; + + @override + String get deleteAllContent => + 'Removes all entries from the local MediaBrain.'; + + @override + String get cancel => 'Cancel'; + + @override + String get delete => 'Delete'; + + @override + String get save => 'Save'; + + @override + String get libraryCleared => 'Library cleared.'; + + @override + String get cancelled => 'Cancelled.'; + + @override + String get serverConfigTitle => 'Configure Server Sync'; + + @override + String get fieldServerUrl => 'Server URL'; + + @override + String get fieldServerUrlHint => 'e.g. macstudvonlukas:8082'; + + @override + String get fieldBearerToken => 'Bearer Token'; + + @override + String get aboutTitle => 'MediaBrain Mobile'; + + @override + String get aboutSubtitle => + 'Standalone Flutter app. Detects installed media apps, tracks manual entries and syncs via file or server.'; + + @override + String get detailDescription => 'Description'; + + @override + String get detailDetails => 'Details'; + + @override + String get detailTags => 'Tags'; + + @override + String get fieldArtist => 'Artist'; + + @override + String get fieldAlbum => 'Album'; + + @override + String get fieldChannel => 'Channel'; + + @override + String get fieldSeasonEpisodeLabel => 'Season/Episode'; + + @override + String get fieldLength => 'Length'; + + @override + String get fieldLastOpened => 'Last opened'; + + @override + String get fieldUsage => 'Usage'; + + @override + String get fieldProviderId => 'Provider ID'; + + @override + String get fieldLocalPath => 'Local Path'; + + @override + String get openPlayStore => 'Open in Play Store'; + + @override + String get openBrowser => 'Open in Browser'; + + @override + String get linkError => 'Link could not be opened.'; + + @override + String get dialogTitleNew => 'New Entry'; + + @override + String get dialogTitleEdit => 'Edit Entry'; + + @override + String get fieldTitle => 'Title'; + + @override + String get categoryLabel => 'Category'; + + @override + String get fieldArtistOptional => 'Artist / Director (optional)'; + + @override + String get fieldAlbumOptional => 'Album / Series (optional)'; + + @override + String get fieldChannelOptional => 'Channel / Source (optional)'; + + @override + String get fieldDescriptionOptional => 'Description (optional)'; + + @override + String get fieldTags => 'Tags (comma-separated)'; + + @override + String get fieldTagsHint => 'e.g. Documentary, Weekend'; + + @override + String get markAsFavorite => 'Mark as favorite'; + + @override + String error(String msg) { + return 'Error: $msg'; + } + + @override + String foregroundMinutesLabel(int minutes) { + return '$minutes min total'; + } + + @override + String bgScanSubtitle(int hours) { + return 'Scan every ~$hours h, even when the app is closed.'; + } + + @override + String lastRun(String time) { + return 'Last run: $time'; + } + + @override + String scanResult(int matched, int persisted) { + return 'App scan: $matched media apps found, $persisted entries added.'; + } + + @override + String exportResult(String msg) { + return 'Export: $msg'; + } + + @override + String importResult(String msg) { + return 'Import: $msg'; + } + + @override + String pushPullStatus(String push, String pull) { + return 'Push: $push · Pull: $pull'; + } + + @override + String seasonEpisode(int s, int e) { + return 'S$s E$e'; + } + + @override + String usageMinutes(int n) { + return '$n minutes'; + } + + @override + String get navPlaylists => 'Playlists'; + + @override + String get screenPlaylists => 'Playlists & Collections'; + + @override + String get playlistTypeManual => 'Manual'; + + @override + String get playlistTypeSmart => 'Smart (Dynamic Filter)'; + + @override + String get dialogNewPlaylist => 'New Playlist'; + + @override + String get dialogEditPlaylist => 'Edit Playlist'; + + @override + String get fieldPlaylistName => 'Playlist Name'; + + @override + String get fieldPlaylistDescription => 'Description (optional)'; + + @override + String get fieldPlaylistType => 'Playlist Type'; + + @override + String get smartFilterCriteria => 'Smart Filter Criteria'; + + @override + String get smartFilterCategory => 'Category'; + + @override + String get smartFilterTag => 'Tag / Keyword'; + + @override + String get smartFilterFavoritesOnly => 'Favorites only'; + + @override + String get smartFilterMinMinutes => 'Minimum usage time (min)'; + + @override + String get smartFilterSearch => 'Search term'; + + @override + String get emptyPlaylists => + 'No playlists created yet.\nCreate manual collections or dynamic smart playlists.'; + + @override + String get emptyPlaylistItems => + 'This playlist does not contain any media yet.'; + + @override + String get deletePlaylistTitle => 'Delete playlist?'; + + @override + String get deletePlaylistContent => + 'Do you really want to delete this playlist? The media items will remain in your library.'; + + @override + String get addToPlaylist => 'Add to playlist'; + + @override + String get removeFromPlaylist => 'Remove from playlist'; + + @override + String get addedToPlaylist => 'Added to playlist.'; + + @override + String get removedFromPlaylist => 'Removed from playlist.'; + + @override + String get filterByTag => 'Tag Filter'; + + @override + String get allTags => 'All Tags'; + + @override + String get addTag => 'Add tag'; + + @override + String get removeTag => 'Remove tag'; + + @override + String get noPlaylistsFound => 'No playlists available.'; + + @override + String playlistItemCount(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items', + one: '$count item', + ); + return '$_temp0'; + } + + @override + String tagCount(String tag, int count) { + return '#$tag ($count)'; + } +} diff --git a/flutter_port/lib/l10n/app_localizations_es.dart b/flutter_port/lib/l10n/app_localizations_es.dart new file mode 100644 index 0000000..89d599a --- /dev/null +++ b/flutter_port/lib/l10n/app_localizations_es.dart @@ -0,0 +1,413 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Spanish Castilian (`es`). +class AppLocalizationsEs extends AppLocalizations { + AppLocalizationsEs([String locale = 'es']) : super(locale); + + @override + String get appTitle => 'MediaBrain'; + + @override + String get navLibrary => 'Biblioteca'; + + @override + String get navScan => 'Escanear'; + + @override + String get navMore => 'Más'; + + @override + String get screenLibrary => 'Biblioteca'; + + @override + String get fabAdd => 'Añadir'; + + @override + String get searchHint => 'Buscar (título, artista, canal …)'; + + @override + String get filterAll => 'Todo'; + + @override + String get filterFavorites => '⭐ Favoritos'; + + @override + String get emptyNoMatch => 'No hay resultados para tus filtros.'; + + @override + String get emptyNoItems => + 'Aún está vacío.\nPulsa la pestaña Escanear para detectar automáticamente tus aplicaciones multimedia.'; + + @override + String get screenScan => 'Escanear'; + + @override + String get scanDescription => + 'Busca aplicaciones multimedia instaladas (streaming, música, pódcasts y documentos) y las añade a tu biblioteca. Si permites el acceso a los datos de uso, también obtendrás el tiempo de uso real.'; + + @override + String get btnAppScan => 'Escaneo de aplicaciones (uso agregado)'; + + @override + String get bgScanTitle => 'Escaneo en segundo plano'; + + @override + String get bgScanNow => 'Ejecutar ahora una vez en segundo plano'; + + @override + String get bgScanNowSubtitle => + 'Envía una tarea puntual a Android. El resultado aparecerá después de 1–2 minutos.'; + + @override + String get bgScanQueued => + 'Escaneo en cola — el resultado estará visible en ~1 min'; + + @override + String get scanAndroidOnly => 'El escaneo solo está disponible en Android.'; + + @override + String get scanPermissionMissing => + 'Falta el acceso a los datos de uso. Actívalo en los ajustes del sistema para MediaBrain:\n\nAjustes → Aplicaciones → Acceso especial a aplicaciones → Acceso a datos de uso → MediaBrain.'; + + @override + String get detectedTitle => '📡 ¿Qué se detecta?'; + + @override + String get detectedApps => + 'Netflix · Disney+ · Prime Video · Apple TV · YouTube · Twitch · Spotify · Apple Music · YouTube Music · Tidal · Amazon Music · Audible · Kindle · Pocket Casts · Google Podcasts · Adobe Acrobat · Google Docs · Notion · Obsidian y más.'; + + @override + String get appScanTitle => '🔍 Escaneo de aplicaciones'; + + @override + String get appScanDescription => + 'Escaneo de aplicaciones: una entrada por aplicación multimedia con el uso total de los últimos días (mediante las estadísticas de uso de Android).'; + + @override + String get neverText => 'Nunca'; + + @override + String get screenSettings => 'Ajustes'; + + @override + String get sectionDataStatus => 'Estado de datos'; + + @override + String get sectionFileSync => 'Sincronización de archivos'; + + @override + String get sectionServerSync => 'Sincronización del servidor (Mac Studio)'; + + @override + String get sectionActions => 'Acciones'; + + @override + String get settingsTotalEntries => 'Total de entradas'; + + @override + String get settingsLastScan => 'Último escaneo'; + + @override + String get exportTitle => 'Exportar mediante Compartir'; + + @override + String get exportSubtitle => + 'JSON por correo, OneDrive o Telegram a ti mismo …'; + + @override + String get importTitle => 'Elegir archivo de importación'; + + @override + String get importSubtitle => 'Sobrescribe los datos locales'; + + @override + String get noServerConfigured => 'No hay servidor configurado'; + + @override + String get actionPush => 'Enviar'; + + @override + String get actionPull => 'Recibir'; + + @override + String get refreshData => 'Actualizar datos'; + + @override + String get deleteAllData => 'Eliminar todos los datos de la biblioteca'; + + @override + String get deleteAllTitle => '¿Eliminar todos los datos de la biblioteca?'; + + @override + String get deleteAllContent => + 'Elimina todas las entradas del MediaBrain local.'; + + @override + String get cancel => 'Cancelar'; + + @override + String get delete => 'Eliminar'; + + @override + String get save => 'Guardar'; + + @override + String get libraryCleared => 'Biblioteca vaciada.'; + + @override + String get cancelled => 'Cancelado.'; + + @override + String get serverConfigTitle => 'Configurar sincronización del servidor'; + + @override + String get fieldServerUrl => 'URL del servidor'; + + @override + String get fieldServerUrlHint => 'p. ej., macstudvonlukas:8082'; + + @override + String get fieldBearerToken => 'Token de portador'; + + @override + String get aboutTitle => 'MediaBrain Mobile'; + + @override + String get aboutSubtitle => + 'Aplicación Flutter independiente. Detecta aplicaciones multimedia instaladas, registra entradas manuales y sincroniza mediante archivo o servidor.'; + + @override + String get detailDescription => 'Descripción'; + + @override + String get detailDetails => 'Detalles'; + + @override + String get detailTags => 'Etiquetas'; + + @override + String get fieldArtist => 'Artista'; + + @override + String get fieldAlbum => 'Álbum'; + + @override + String get fieldChannel => 'Canal'; + + @override + String get fieldSeasonEpisodeLabel => 'Temporada/Episodio'; + + @override + String get fieldLength => 'Duración'; + + @override + String get fieldLastOpened => 'Última apertura'; + + @override + String get fieldUsage => 'Uso'; + + @override + String get fieldProviderId => 'ID del proveedor'; + + @override + String get fieldLocalPath => 'Ruta local'; + + @override + String get openPlayStore => 'Abrir en Play Store'; + + @override + String get openBrowser => 'Abrir en el navegador'; + + @override + String get linkError => 'No se pudo abrir el enlace.'; + + @override + String get dialogTitleNew => 'Nueva entrada'; + + @override + String get dialogTitleEdit => 'Editar entrada'; + + @override + String get fieldTitle => 'Título'; + + @override + String get categoryLabel => 'Categoría'; + + @override + String get fieldArtistOptional => 'Artista / director (opcional)'; + + @override + String get fieldAlbumOptional => 'Álbum / serie (opcional)'; + + @override + String get fieldChannelOptional => 'Canal / fuente (opcional)'; + + @override + String get fieldDescriptionOptional => 'Descripción (opcional)'; + + @override + String get fieldTags => 'Etiquetas (separadas por comas)'; + + @override + String get fieldTagsHint => 'p. ej., Documental, fin de semana'; + + @override + String get markAsFavorite => 'Marcar como favorito'; + + @override + String error(String msg) { + return 'Error: $msg'; + } + + @override + String foregroundMinutesLabel(int minutes) { + return 'Total: $minutes min'; + } + + @override + String bgScanSubtitle(int hours) { + return 'Escanear cada ~$hours h, incluso cuando la aplicación esté cerrada.'; + } + + @override + String lastRun(String time) { + return 'Última ejecución: $time'; + } + + @override + String scanResult(int matched, int persisted) { + return 'Escaneo de aplicaciones: se encontraron $matched aplicaciones multimedia y se añadieron $persisted entradas.'; + } + + @override + String exportResult(String msg) { + return 'Exportación: $msg'; + } + + @override + String importResult(String msg) { + return 'Importación: $msg'; + } + + @override + String pushPullStatus(String push, String pull) { + return 'Enviar: $push · Recibir: $pull'; + } + + @override + String seasonEpisode(int s, int e) { + return 'T$s E$e'; + } + + @override + String usageMinutes(int n) { + return '$n minutos'; + } + + @override + String get navPlaylists => 'Listas de reproducción'; + + @override + String get screenPlaylists => 'Listas y colecciones'; + + @override + String get playlistTypeManual => 'Manual'; + + @override + String get playlistTypeSmart => 'Inteligente (filtro dinámico)'; + + @override + String get dialogNewPlaylist => 'Nueva lista de reproducción'; + + @override + String get dialogEditPlaylist => 'Editar lista de reproducción'; + + @override + String get fieldPlaylistName => 'Nombre de la lista de reproducción'; + + @override + String get fieldPlaylistDescription => 'Descripción (opcional)'; + + @override + String get fieldPlaylistType => 'Tipo de lista de reproducción'; + + @override + String get smartFilterCriteria => 'Criterios de filtro inteligente'; + + @override + String get smartFilterCategory => 'Categoría'; + + @override + String get smartFilterTag => 'Etiqueta / palabra clave'; + + @override + String get smartFilterFavoritesOnly => 'Solo favoritos'; + + @override + String get smartFilterMinMinutes => 'Tiempo mínimo de uso (min)'; + + @override + String get smartFilterSearch => 'Término de búsqueda'; + + @override + String get emptyPlaylists => + 'Todavía no hay listas de reproducción.\nCrea colecciones manuales o listas inteligentes dinámicas.'; + + @override + String get emptyPlaylistItems => + 'Esta lista todavía no contiene contenido multimedia.'; + + @override + String get deletePlaylistTitle => '¿Eliminar lista de reproducción?'; + + @override + String get deletePlaylistContent => + '¿De verdad quieres eliminar esta lista de reproducción? Los medios incluidos permanecerán en tu biblioteca.'; + + @override + String get addToPlaylist => 'Añadir a la lista de reproducción'; + + @override + String get removeFromPlaylist => 'Quitar de la lista de reproducción'; + + @override + String get addedToPlaylist => 'Añadido a la lista de reproducción.'; + + @override + String get removedFromPlaylist => 'Quitado de la lista de reproducción.'; + + @override + String get filterByTag => 'Filtro de etiquetas'; + + @override + String get allTags => 'Todas las etiquetas'; + + @override + String get addTag => 'Añadir etiqueta'; + + @override + String get removeTag => 'Quitar etiqueta'; + + @override + String get noPlaylistsFound => 'No hay listas de reproducción disponibles.'; + + @override + String playlistItemCount(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count elementos', + one: '$count elemento', + ); + return '$_temp0'; + } + + @override + String tagCount(String tag, int count) { + return '#$tag ($count)'; + } +} diff --git a/flutter_port/lib/l10n/app_localizations_fr.dart b/flutter_port/lib/l10n/app_localizations_fr.dart new file mode 100644 index 0000000..9263569 --- /dev/null +++ b/flutter_port/lib/l10n/app_localizations_fr.dart @@ -0,0 +1,415 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for French (`fr`). +class AppLocalizationsFr extends AppLocalizations { + AppLocalizationsFr([String locale = 'fr']) : super(locale); + + @override + String get appTitle => 'MediaBrain'; + + @override + String get navLibrary => 'Bibliothèque'; + + @override + String get navScan => 'Scanner'; + + @override + String get navMore => 'Plus'; + + @override + String get screenLibrary => 'Bibliothèque'; + + @override + String get fabAdd => 'Ajouter'; + + @override + String get searchHint => 'Rechercher (titre, artiste, chaîne …)'; + + @override + String get filterAll => 'Tout'; + + @override + String get filterFavorites => '⭐ Favoris'; + + @override + String get emptyNoMatch => 'Aucun résultat ne correspond à vos filtres.'; + + @override + String get emptyNoItems => + 'Encore vide.\nTouchez l’onglet Scanner pour détecter automatiquement vos applications multimédias.'; + + @override + String get screenScan => 'Scanner'; + + @override + String get scanDescription => + 'Recherche les applications multimédias installées (streaming, musique, podcasts, documents) et les ajoute à votre bibliothèque. Si l’accès aux données d’utilisation est activé, le temps d’utilisation réel est également disponible.'; + + @override + String get btnAppScan => 'Analyse des applications (utilisation agrégée)'; + + @override + String get bgScanTitle => 'Analyse en arrière-plan'; + + @override + String get bgScanNow => 'Lancer une fois en arrière-plan maintenant'; + + @override + String get bgScanNowSubtitle => + 'Envoie une tâche ponctuelle à Android. Le résultat apparaît après 1 à 2 minutes.'; + + @override + String get bgScanQueued => + 'Analyse en arrière-plan en attente — résultat visible dans environ 1 min'; + + @override + String get scanAndroidOnly => + 'L’analyse est uniquement disponible sur Android.'; + + @override + String get scanPermissionMissing => + 'Accès aux données d’utilisation manquant. Activez-le dans les paramètres système de MediaBrain :\n\nParamètres → Applications → Accès spécial aux applications → Accès aux données d’utilisation → MediaBrain.'; + + @override + String get detectedTitle => '📡 Éléments détectés'; + + @override + String get detectedApps => + 'Netflix · Disney+ · Prime Video · Apple TV · YouTube · Twitch · Spotify · Apple Music · YouTube Music · Tidal · Amazon Music · Audible · Kindle · Pocket Casts · Google Podcasts · Adobe Acrobat · Google Docs · Notion · Obsidian et plus encore.'; + + @override + String get appScanTitle => '🔍 Analyse des applications'; + + @override + String get appScanDescription => + 'Analyse des applications : une entrée par application multimédia, avec l’utilisation totale des derniers jours (via les statistiques d’utilisation Android).'; + + @override + String get neverText => 'jamais'; + + @override + String get screenSettings => 'Paramètres'; + + @override + String get sectionDataStatus => 'État des données'; + + @override + String get sectionFileSync => 'Synchronisation des fichiers'; + + @override + String get sectionServerSync => 'Synchronisation serveur (Mac Studio)'; + + @override + String get sectionActions => 'Actions'; + + @override + String get settingsTotalEntries => 'Nombre total d’entrées'; + + @override + String get settingsLastScan => 'Dernière analyse'; + + @override + String get exportTitle => 'Exporter via Partager'; + + @override + String get exportSubtitle => + 'JSON par e-mail, OneDrive ou Telegram à vous-même …'; + + @override + String get importTitle => 'Choisir le fichier à importer'; + + @override + String get importSubtitle => 'Remplace les données locales'; + + @override + String get noServerConfigured => 'Aucun serveur configuré'; + + @override + String get actionPush => 'Envoyer'; + + @override + String get actionPull => 'Récupérer'; + + @override + String get refreshData => 'Actualiser les données'; + + @override + String get deleteAllData => 'Supprimer toutes les données de la bibliothèque'; + + @override + String get deleteAllTitle => + 'Supprimer toutes les données de la bibliothèque ?'; + + @override + String get deleteAllContent => + 'Supprime toutes les entrées du MediaBrain local.'; + + @override + String get cancel => 'Annuler'; + + @override + String get delete => 'Supprimer'; + + @override + String get save => 'Enregistrer'; + + @override + String get libraryCleared => 'Bibliothèque vidée.'; + + @override + String get cancelled => 'Annulé.'; + + @override + String get serverConfigTitle => 'Configurer la synchronisation du serveur'; + + @override + String get fieldServerUrl => 'URL du serveur'; + + @override + String get fieldServerUrlHint => 'p. ex. macstudvonlukas:8082'; + + @override + String get fieldBearerToken => 'Jeton Bearer'; + + @override + String get aboutTitle => 'MediaBrain Mobile'; + + @override + String get aboutSubtitle => + 'Application Flutter autonome. Détecte les applications multimédias installées, suit les entrées manuelles et synchronise par fichier ou serveur.'; + + @override + String get detailDescription => 'Description'; + + @override + String get detailDetails => 'Détails'; + + @override + String get detailTags => 'Tags'; + + @override + String get fieldArtist => 'Artiste'; + + @override + String get fieldAlbum => 'Album'; + + @override + String get fieldChannel => 'Chaîne'; + + @override + String get fieldSeasonEpisodeLabel => 'Saison/épisode'; + + @override + String get fieldLength => 'Durée'; + + @override + String get fieldLastOpened => 'Dernière ouverture'; + + @override + String get fieldUsage => 'Utilisation'; + + @override + String get fieldProviderId => 'ID du fournisseur'; + + @override + String get fieldLocalPath => 'Chemin local'; + + @override + String get openPlayStore => 'Ouvrir dans le Play Store'; + + @override + String get openBrowser => 'Ouvrir dans le navigateur'; + + @override + String get linkError => 'Impossible d’ouvrir le lien.'; + + @override + String get dialogTitleNew => 'Nouvelle entrée'; + + @override + String get dialogTitleEdit => 'Modifier l’entrée'; + + @override + String get fieldTitle => 'Titre'; + + @override + String get categoryLabel => 'Catégorie'; + + @override + String get fieldArtistOptional => 'Artiste / réalisateur (facultatif)'; + + @override + String get fieldAlbumOptional => 'Album / série (facultatif)'; + + @override + String get fieldChannelOptional => 'Chaîne / source (facultatif)'; + + @override + String get fieldDescriptionOptional => 'Description (facultatif)'; + + @override + String get fieldTags => 'Tags (séparés par des virgules)'; + + @override + String get fieldTagsHint => 'p. ex. documentaire, week-end'; + + @override + String get markAsFavorite => 'Ajouter aux favoris'; + + @override + String error(String msg) { + return 'Erreur : $msg'; + } + + @override + String foregroundMinutesLabel(int minutes) { + return '$minutes min au total'; + } + + @override + String bgScanSubtitle(int hours) { + return 'Analyser toutes les ~$hours h, même lorsque l’application est fermée.'; + } + + @override + String lastRun(String time) { + return 'Dernière exécution : $time'; + } + + @override + String scanResult(int matched, int persisted) { + return 'Analyse des applications : $matched applications multimédias détectées, $persisted entrées ajoutées.'; + } + + @override + String exportResult(String msg) { + return 'Export : $msg'; + } + + @override + String importResult(String msg) { + return 'Import : $msg'; + } + + @override + String pushPullStatus(String push, String pull) { + return 'Envoi : $push · Récupération : $pull'; + } + + @override + String seasonEpisode(int s, int e) { + return 'S$s E$e'; + } + + @override + String usageMinutes(int n) { + return '$n minutes'; + } + + @override + String get navPlaylists => 'Playlists'; + + @override + String get screenPlaylists => 'Playlists et collections'; + + @override + String get playlistTypeManual => 'Manuelle'; + + @override + String get playlistTypeSmart => 'Intelligente (filtre dynamique)'; + + @override + String get dialogNewPlaylist => 'Nouvelle playlist'; + + @override + String get dialogEditPlaylist => 'Modifier la playlist'; + + @override + String get fieldPlaylistName => 'Nom de la playlist'; + + @override + String get fieldPlaylistDescription => 'Description (facultative)'; + + @override + String get fieldPlaylistType => 'Type de playlist'; + + @override + String get smartFilterCriteria => 'Critères du filtre intelligent'; + + @override + String get smartFilterCategory => 'Catégorie'; + + @override + String get smartFilterTag => 'Tag / mot-clé'; + + @override + String get smartFilterFavoritesOnly => 'Favoris uniquement'; + + @override + String get smartFilterMinMinutes => 'Durée minimale d’utilisation (min)'; + + @override + String get smartFilterSearch => 'Terme de recherche'; + + @override + String get emptyPlaylists => + 'Aucune playlist créée pour le moment.\nCréez des collections manuelles ou des playlists intelligentes dynamiques.'; + + @override + String get emptyPlaylistItems => + 'Cette playlist ne contient encore aucun média.'; + + @override + String get deletePlaylistTitle => 'Supprimer la playlist ?'; + + @override + String get deletePlaylistContent => + 'Voulez-vous vraiment supprimer cette playlist ? Les médias qu’elle contient resteront dans votre bibliothèque.'; + + @override + String get addToPlaylist => 'Ajouter à la playlist'; + + @override + String get removeFromPlaylist => 'Retirer de la playlist'; + + @override + String get addedToPlaylist => 'Ajouté à la playlist.'; + + @override + String get removedFromPlaylist => 'Retiré de la playlist.'; + + @override + String get filterByTag => 'Filtrer par tag'; + + @override + String get allTags => 'Tous les tags'; + + @override + String get addTag => 'Ajouter un tag'; + + @override + String get removeTag => 'Supprimer le tag'; + + @override + String get noPlaylistsFound => 'Aucune playlist disponible.'; + + @override + String playlistItemCount(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count éléments', + one: '$count élément', + ); + return '$_temp0'; + } + + @override + String tagCount(String tag, int count) { + return '#$tag ($count)'; + } +} diff --git a/flutter_port/lib/l10n/app_localizations_hi.dart b/flutter_port/lib/l10n/app_localizations_hi.dart new file mode 100644 index 0000000..73f5415 --- /dev/null +++ b/flutter_port/lib/l10n/app_localizations_hi.dart @@ -0,0 +1,412 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Hindi (`hi`). +class AppLocalizationsHi extends AppLocalizations { + AppLocalizationsHi([String locale = 'hi']) : super(locale); + + @override + String get appTitle => 'MediaBrain'; + + @override + String get navLibrary => 'लाइब्रेरी'; + + @override + String get navScan => 'स्कैन'; + + @override + String get navMore => 'और'; + + @override + String get screenLibrary => 'लाइब्रेरी'; + + @override + String get fabAdd => 'जोड़ें'; + + @override + String get searchHint => 'खोजें (शीर्षक, कलाकार, चैनल …)'; + + @override + String get filterAll => 'सभी'; + + @override + String get filterFavorites => '⭐ पसंदीदा'; + + @override + String get emptyNoMatch => 'आपके फ़िल्टर से कोई परिणाम नहीं मिला।'; + + @override + String get emptyNoItems => + 'अभी खाली है।\nअपने मीडिया ऐप अपने-आप पहचानने के लिए स्कैन टैब पर टैप करें।'; + + @override + String get screenScan => 'स्कैन'; + + @override + String get scanDescription => + 'इंस्टॉल किए गए मीडिया ऐप (स्ट्रीमिंग, संगीत, पॉडकास्ट, दस्तावेज़) खोजता है और उन्हें आपकी लाइब्रेरी में जोड़ता है। उपयोग डेटा की अनुमति चालू होने पर वास्तविक उपयोग समय भी मिलता है।'; + + @override + String get btnAppScan => 'ऐप स्कैन (कुल उपयोग)'; + + @override + String get bgScanTitle => 'बैकग्राउंड स्कैन'; + + @override + String get bgScanNow => 'अभी एक बार बैकग्राउंड में चलाएँ'; + + @override + String get bgScanNowSubtitle => + 'Android को एक बार चलने वाला टास्क भेजता है। परिणाम 1–2 मिनट बाद दिखेगा।'; + + @override + String get bgScanQueued => + 'बैकग्राउंड स्कैन कतार में है — परिणाम लगभग 1 मिनट में दिखेगा'; + + @override + String get scanAndroidOnly => 'स्कैन केवल Android पर उपलब्ध है।'; + + @override + String get scanPermissionMissing => + 'उपयोग डेटा की अनुमति नहीं है। MediaBrain के लिए सिस्टम सेटिंग में इसे चालू करें:\n\nसेटिंग → ऐप्स → विशेष ऐप एक्सेस → उपयोग डेटा एक्सेस → MediaBrain।'; + + @override + String get detectedTitle => '📡 क्या पहचाना जाता है?'; + + @override + String get detectedApps => + 'Netflix · Disney+ · Prime Video · Apple TV · YouTube · Twitch · Spotify · Apple Music · YouTube Music · Tidal · Amazon Music · Audible · Kindle · Pocket Casts · Google Podcasts · Adobe Acrobat · Google Docs · Notion · Obsidian और अन्य।'; + + @override + String get appScanTitle => '🔍 ऐप स्कैन'; + + @override + String get appScanDescription => + 'ऐप स्कैन: हर मीडिया ऐप के लिए एक प्रविष्टि, जिसमें पिछले दिनों का कुल उपयोग दिखता है (Android उपयोग आँकड़ों से)।'; + + @override + String get neverText => 'कभी नहीं'; + + @override + String get screenSettings => 'सेटिंग्स'; + + @override + String get sectionDataStatus => 'डेटा स्थिति'; + + @override + String get sectionFileSync => 'फ़ाइल सिंक'; + + @override + String get sectionServerSync => 'सर्वर सिंक (Mac Studio)'; + + @override + String get sectionActions => 'कार्रवाइयाँ'; + + @override + String get settingsTotalEntries => 'कुल प्रविष्टियाँ'; + + @override + String get settingsLastScan => 'अंतिम स्कैन'; + + @override + String get exportTitle => 'शेयर के ज़रिए निर्यात'; + + @override + String get exportSubtitle => + 'ईमेल, OneDrive या Telegram से खुद को JSON भेजें …'; + + @override + String get importTitle => 'आयात फ़ाइल चुनें'; + + @override + String get importSubtitle => 'स्थानीय डेटा को बदल देता है'; + + @override + String get noServerConfigured => 'कोई सर्वर कॉन्फ़िगर नहीं है'; + + @override + String get actionPush => 'पुश'; + + @override + String get actionPull => 'पुल'; + + @override + String get refreshData => 'डेटा रीफ़्रेश करें'; + + @override + String get deleteAllData => 'लाइब्रेरी का सारा डेटा हटाएँ'; + + @override + String get deleteAllTitle => 'लाइब्रेरी का सारा डेटा हटाएँ?'; + + @override + String get deleteAllContent => + 'स्थानीय MediaBrain से सभी प्रविष्टियाँ हटाता है।'; + + @override + String get cancel => 'रद्द करें'; + + @override + String get delete => 'हटाएँ'; + + @override + String get save => 'सहेजें'; + + @override + String get libraryCleared => 'लाइब्रेरी साफ़ कर दी गई।'; + + @override + String get cancelled => 'रद्द किया गया।'; + + @override + String get serverConfigTitle => 'सर्वर सिंक कॉन्फ़िगर करें'; + + @override + String get fieldServerUrl => 'सर्वर URL'; + + @override + String get fieldServerUrlHint => 'जैसे, macstudvonlukas:8082'; + + @override + String get fieldBearerToken => 'Bearer टोकन'; + + @override + String get aboutTitle => 'MediaBrain Mobile'; + + @override + String get aboutSubtitle => + 'स्वतंत्र Flutter ऐप। इंस्टॉल किए गए मीडिया ऐप पहचानता है, मैन्युअल प्रविष्टियाँ रखता है और फ़ाइल या सर्वर से सिंक करता है।'; + + @override + String get detailDescription => 'विवरण'; + + @override + String get detailDetails => 'जानकारी'; + + @override + String get detailTags => 'टैग'; + + @override + String get fieldArtist => 'कलाकार'; + + @override + String get fieldAlbum => 'एल्बम'; + + @override + String get fieldChannel => 'चैनल'; + + @override + String get fieldSeasonEpisodeLabel => 'सीज़न/एपिसोड'; + + @override + String get fieldLength => 'अवधि'; + + @override + String get fieldLastOpened => 'अंतिम बार खोला गया'; + + @override + String get fieldUsage => 'उपयोग'; + + @override + String get fieldProviderId => 'प्रदाता ID'; + + @override + String get fieldLocalPath => 'स्थानीय पथ'; + + @override + String get openPlayStore => 'Play Store में खोलें'; + + @override + String get openBrowser => 'ब्राउज़र में खोलें'; + + @override + String get linkError => 'लिंक नहीं खुल सका।'; + + @override + String get dialogTitleNew => 'नई प्रविष्टि'; + + @override + String get dialogTitleEdit => 'प्रविष्टि संपादित करें'; + + @override + String get fieldTitle => 'शीर्षक'; + + @override + String get categoryLabel => 'श्रेणी'; + + @override + String get fieldArtistOptional => 'कलाकार / निर्देशक (वैकल्पिक)'; + + @override + String get fieldAlbumOptional => 'एल्बम / सीरीज़ (वैकल्पिक)'; + + @override + String get fieldChannelOptional => 'चैनल / स्रोत (वैकल्पिक)'; + + @override + String get fieldDescriptionOptional => 'विवरण (वैकल्पिक)'; + + @override + String get fieldTags => 'टैग (अल्पविराम से अलग)'; + + @override + String get fieldTagsHint => 'जैसे, वृत्तचित्र, सप्ताहांत'; + + @override + String get markAsFavorite => 'पसंदीदा के रूप में चिह्नित करें'; + + @override + String error(String msg) { + return 'त्रुटि: $msg'; + } + + @override + String foregroundMinutesLabel(int minutes) { + return 'कुल $minutes मिनट'; + } + + @override + String bgScanSubtitle(int hours) { + return 'ऐप बंद होने पर भी हर ~$hours घंटे में स्कैन करें।'; + } + + @override + String lastRun(String time) { + return 'अंतिम बार चला: $time'; + } + + @override + String scanResult(int matched, int persisted) { + return 'ऐप स्कैन: $matched मीडिया ऐप मिले, $persisted प्रविष्टियाँ जोड़ी गईं।'; + } + + @override + String exportResult(String msg) { + return 'निर्यात: $msg'; + } + + @override + String importResult(String msg) { + return 'आयात: $msg'; + } + + @override + String pushPullStatus(String push, String pull) { + return 'पुश: $push · पुल: $pull'; + } + + @override + String seasonEpisode(int s, int e) { + return 'S$s E$e'; + } + + @override + String usageMinutes(int n) { + return '$n मिनट'; + } + + @override + String get navPlaylists => 'प्लेलिस्ट'; + + @override + String get screenPlaylists => 'प्लेलिस्ट और संग्रह'; + + @override + String get playlistTypeManual => 'मैन्युअल'; + + @override + String get playlistTypeSmart => 'स्मार्ट (डायनेमिक फ़िल्टर)'; + + @override + String get dialogNewPlaylist => 'नई प्लेलिस्ट'; + + @override + String get dialogEditPlaylist => 'प्लेलिस्ट संपादित करें'; + + @override + String get fieldPlaylistName => 'प्लेलिस्ट का नाम'; + + @override + String get fieldPlaylistDescription => 'विवरण (वैकल्पिक)'; + + @override + String get fieldPlaylistType => 'प्लेलिस्ट का प्रकार'; + + @override + String get smartFilterCriteria => 'स्मार्ट फ़िल्टर मानदंड'; + + @override + String get smartFilterCategory => 'श्रेणी'; + + @override + String get smartFilterTag => 'टैग / कीवर्ड'; + + @override + String get smartFilterFavoritesOnly => 'केवल पसंदीदा'; + + @override + String get smartFilterMinMinutes => 'न्यूनतम उपयोग समय (मिनट)'; + + @override + String get smartFilterSearch => 'खोज शब्द'; + + @override + String get emptyPlaylists => + 'अभी कोई प्लेलिस्ट नहीं बनाई गई है।\nमैन्युअल संग्रह या डायनेमिक स्मार्ट प्लेलिस्ट बनाएँ।'; + + @override + String get emptyPlaylistItems => 'इस प्लेलिस्ट में अभी कोई मीडिया नहीं है।'; + + @override + String get deletePlaylistTitle => 'प्लेलिस्ट हटाएँ?'; + + @override + String get deletePlaylistContent => + 'क्या आप वाकई यह प्लेलिस्ट हटाना चाहते हैं? इसमें मौजूद मीडिया आपकी लाइब्रेरी में रहेगा।'; + + @override + String get addToPlaylist => 'प्लेलिस्ट में जोड़ें'; + + @override + String get removeFromPlaylist => 'प्लेलिस्ट से हटाएँ'; + + @override + String get addedToPlaylist => 'प्लेलिस्ट में जोड़ा गया।'; + + @override + String get removedFromPlaylist => 'प्लेलिस्ट से हटाया गया।'; + + @override + String get filterByTag => 'टैग फ़िल्टर'; + + @override + String get allTags => 'सभी टैग'; + + @override + String get addTag => 'टैग जोड़ें'; + + @override + String get removeTag => 'टैग हटाएँ'; + + @override + String get noPlaylistsFound => 'कोई प्लेलिस्ट उपलब्ध नहीं है।'; + + @override + String playlistItemCount(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count आइटम', + one: '$count आइटम', + ); + return '$_temp0'; + } + + @override + String tagCount(String tag, int count) { + return '#$tag ($count)'; + } +} diff --git a/flutter_port/lib/l10n/app_localizations_ja.dart b/flutter_port/lib/l10n/app_localizations_ja.dart new file mode 100644 index 0000000..85dc783 --- /dev/null +++ b/flutter_port/lib/l10n/app_localizations_ja.dart @@ -0,0 +1,405 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Japanese (`ja`). +class AppLocalizationsJa extends AppLocalizations { + AppLocalizationsJa([String locale = 'ja']) : super(locale); + + @override + String get appTitle => 'MediaBrain'; + + @override + String get navLibrary => 'ライブラリ'; + + @override + String get navScan => 'スキャン'; + + @override + String get navMore => 'その他'; + + @override + String get screenLibrary => 'ライブラリ'; + + @override + String get fabAdd => '追加'; + + @override + String get searchHint => '検索(タイトル、アーティスト、チャンネル…)'; + + @override + String get filterAll => 'すべて'; + + @override + String get filterFavorites => '⭐ お気に入り'; + + @override + String get emptyNoMatch => '指定したフィルターに一致する結果はありません。'; + + @override + String get emptyNoItems => 'まだ空です。\n「スキャン」タブをタップすると、メディアアプリを自動検出します。'; + + @override + String get screenScan => 'スキャン'; + + @override + String get scanDescription => + 'インストール済みのメディアアプリ(ストリーミング、音楽、ポッドキャスト、ドキュメント)を検索し、ライブラリに項目として追加します。使用状況データへのアクセスを有効にすると、実際の使用時間も取得できます。'; + + @override + String get btnAppScan => 'アプリスキャン(集計使用量)'; + + @override + String get bgScanTitle => 'バックグラウンドスキャン'; + + @override + String get bgScanNow => '今すぐバックグラウンドで1回実行'; + + @override + String get bgScanNowSubtitle => 'Android に単発タスクを送信します。結果は1~2分後に表示されます。'; + + @override + String get bgScanQueued => 'バックグラウンドスキャンをキューに追加しました — 約1分後に結果を確認できます'; + + @override + String get scanAndroidOnly => 'スキャンは Android でのみ利用できます。'; + + @override + String get scanPermissionMissing => + '使用状況データへのアクセスがありません。MediaBrain のシステム設定で有効にしてください:\n\n設定 → アプリ → 特別なアプリアクセス → 使用状況へのアクセス → MediaBrain。'; + + @override + String get detectedTitle => '📡 検出されるもの'; + + @override + String get detectedApps => + 'Netflix · Disney+ · Prime Video · Apple TV · YouTube · Twitch · Spotify · Apple Music · YouTube Music · Tidal · Amazon Music · Audible · Kindle · Pocket Casts · Google Podcasts · Adobe Acrobat · Google Docs · Notion · Obsidian など。'; + + @override + String get appScanTitle => '🔍 アプリスキャン'; + + @override + String get appScanDescription => + 'アプリスキャン:メディアアプリごとに1件作成し、直近数日間の合計使用量を表示します(Android の使用状況統計を利用)。'; + + @override + String get neverText => '一度もない'; + + @override + String get screenSettings => '設定'; + + @override + String get sectionDataStatus => 'データの状態'; + + @override + String get sectionFileSync => 'ファイル同期'; + + @override + String get sectionServerSync => 'サーバー同期(Mac Studio)'; + + @override + String get sectionActions => '操作'; + + @override + String get settingsTotalEntries => '登録数'; + + @override + String get settingsLastScan => '最終スキャン'; + + @override + String get exportTitle => '共有からエクスポート'; + + @override + String get exportSubtitle => 'メール、OneDrive、Telegram で自分宛てに JSON を送信 …'; + + @override + String get importTitle => 'インポートするファイルを選択'; + + @override + String get importSubtitle => 'ローカルデータを上書きします'; + + @override + String get noServerConfigured => 'サーバーが設定されていません'; + + @override + String get actionPush => '送信'; + + @override + String get actionPull => '取得'; + + @override + String get refreshData => 'データを更新'; + + @override + String get deleteAllData => 'ライブラリのデータをすべて削除'; + + @override + String get deleteAllTitle => 'ライブラリのデータをすべて削除しますか?'; + + @override + String get deleteAllContent => 'ローカル MediaBrain のすべての項目を削除します。'; + + @override + String get cancel => 'キャンセル'; + + @override + String get delete => '削除'; + + @override + String get save => '保存'; + + @override + String get libraryCleared => 'ライブラリを消去しました。'; + + @override + String get cancelled => 'キャンセルしました。'; + + @override + String get serverConfigTitle => 'サーバー同期を設定'; + + @override + String get fieldServerUrl => 'サーバー URL'; + + @override + String get fieldServerUrlHint => '例: macstudvonlukas:8082'; + + @override + String get fieldBearerToken => 'Bearer トークン'; + + @override + String get aboutTitle => 'MediaBrain Mobile'; + + @override + String get aboutSubtitle => + '単体で動作する Flutter アプリです。インストール済みのメディアアプリを検出し、手動項目を記録して、ファイルまたはサーバー経由で同期します。'; + + @override + String get detailDescription => '説明'; + + @override + String get detailDetails => '詳細'; + + @override + String get detailTags => 'タグ'; + + @override + String get fieldArtist => 'アーティスト'; + + @override + String get fieldAlbum => 'アルバム'; + + @override + String get fieldChannel => 'チャンネル'; + + @override + String get fieldSeasonEpisodeLabel => 'シーズン/エピソード'; + + @override + String get fieldLength => '長さ'; + + @override + String get fieldLastOpened => '最終オープン'; + + @override + String get fieldUsage => '使用状況'; + + @override + String get fieldProviderId => 'プロバイダー ID'; + + @override + String get fieldLocalPath => 'ローカルパス'; + + @override + String get openPlayStore => 'Play ストアで開く'; + + @override + String get openBrowser => 'ブラウザーで開く'; + + @override + String get linkError => 'リンクを開けませんでした。'; + + @override + String get dialogTitleNew => '新しい項目'; + + @override + String get dialogTitleEdit => '項目を編集'; + + @override + String get fieldTitle => 'タイトル'; + + @override + String get categoryLabel => 'カテゴリ'; + + @override + String get fieldArtistOptional => 'アーティスト / 監督(任意)'; + + @override + String get fieldAlbumOptional => 'アルバム / シリーズ(任意)'; + + @override + String get fieldChannelOptional => 'チャンネル / ソース(任意)'; + + @override + String get fieldDescriptionOptional => '説明(任意)'; + + @override + String get fieldTags => 'タグ(カンマ区切り)'; + + @override + String get fieldTagsHint => '例: ドキュメンタリー、週末'; + + @override + String get markAsFavorite => 'お気に入りにする'; + + @override + String error(String msg) { + return 'エラー: $msg'; + } + + @override + String foregroundMinutesLabel(int minutes) { + return '合計 $minutes 分'; + } + + @override + String bgScanSubtitle(int hours) { + return 'アプリが閉じていても、約 $hours 時間ごとにスキャンします。'; + } + + @override + String lastRun(String time) { + return '最終実行: $time'; + } + + @override + String scanResult(int matched, int persisted) { + return 'アプリスキャン: メディアアプリを $matched 件検出し、$persisted 件の項目を追加しました。'; + } + + @override + String exportResult(String msg) { + return 'エクスポート: $msg'; + } + + @override + String importResult(String msg) { + return 'インポート: $msg'; + } + + @override + String pushPullStatus(String push, String pull) { + return '送信: $push · 取得: $pull'; + } + + @override + String seasonEpisode(int s, int e) { + return 'シーズン$s エピソード$e'; + } + + @override + String usageMinutes(int n) { + return '$n 分'; + } + + @override + String get navPlaylists => 'プレイリスト'; + + @override + String get screenPlaylists => 'プレイリストとコレクション'; + + @override + String get playlistTypeManual => '手動'; + + @override + String get playlistTypeSmart => 'スマート(動的フィルター)'; + + @override + String get dialogNewPlaylist => '新しいプレイリスト'; + + @override + String get dialogEditPlaylist => 'プレイリストを編集'; + + @override + String get fieldPlaylistName => 'プレイリスト名'; + + @override + String get fieldPlaylistDescription => '説明(任意)'; + + @override + String get fieldPlaylistType => 'プレイリストの種類'; + + @override + String get smartFilterCriteria => 'スマートフィルターの条件'; + + @override + String get smartFilterCategory => 'カテゴリ'; + + @override + String get smartFilterTag => 'タグ / キーワード'; + + @override + String get smartFilterFavoritesOnly => 'お気に入りのみ'; + + @override + String get smartFilterMinMinutes => '最小使用時間(分)'; + + @override + String get smartFilterSearch => '検索語'; + + @override + String get emptyPlaylists => + 'プレイリストはまだありません。\n手動コレクションまたは動的なスマートプレイリストを作成してください。'; + + @override + String get emptyPlaylistItems => 'このプレイリストにはまだメディアがありません。'; + + @override + String get deletePlaylistTitle => 'プレイリストを削除しますか?'; + + @override + String get deletePlaylistContent => 'このプレイリストを本当に削除しますか?含まれるメディアはライブラリに残ります。'; + + @override + String get addToPlaylist => 'プレイリストに追加'; + + @override + String get removeFromPlaylist => 'プレイリストから削除'; + + @override + String get addedToPlaylist => 'プレイリストに追加しました。'; + + @override + String get removedFromPlaylist => 'プレイリストから削除しました。'; + + @override + String get filterByTag => 'タグで絞り込む'; + + @override + String get allTags => 'すべてのタグ'; + + @override + String get addTag => 'タグを追加'; + + @override + String get removeTag => 'タグを削除'; + + @override + String get noPlaylistsFound => '利用できるプレイリストはありません。'; + + @override + String playlistItemCount(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 件', + ); + return '$_temp0'; + } + + @override + String tagCount(String tag, int count) { + return '#$tag ($count)'; + } +} diff --git a/flutter_port/lib/l10n/app_localizations_pt.dart b/flutter_port/lib/l10n/app_localizations_pt.dart new file mode 100644 index 0000000..5c46a8e --- /dev/null +++ b/flutter_port/lib/l10n/app_localizations_pt.dart @@ -0,0 +1,414 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Portuguese (`pt`). +class AppLocalizationsPt extends AppLocalizations { + AppLocalizationsPt([String locale = 'pt']) : super(locale); + + @override + String get appTitle => 'MediaBrain'; + + @override + String get navLibrary => 'Biblioteca'; + + @override + String get navScan => 'Analisar'; + + @override + String get navMore => 'Mais'; + + @override + String get screenLibrary => 'Biblioteca'; + + @override + String get fabAdd => 'Adicionar'; + + @override + String get searchHint => 'Pesquisar (título, artista, canal …)'; + + @override + String get filterAll => 'Todos'; + + @override + String get filterFavorites => '⭐ Favoritos'; + + @override + String get emptyNoMatch => 'Não há resultados para os filtros selecionados.'; + + @override + String get emptyNoItems => + 'Ainda está vazio.\nToque no separador Analisar para detetar automaticamente as aplicações multimédia.'; + + @override + String get screenScan => 'Analisar'; + + @override + String get scanDescription => + 'Procura aplicações multimédia instaladas (streaming, música, podcasts, documentos) e adiciona-as à biblioteca. Com o acesso aos dados de utilização ativado, também obtém o tempo real de utilização.'; + + @override + String get btnAppScan => 'Análise de aplicações (utilização agregada)'; + + @override + String get bgScanTitle => 'Análise em segundo plano'; + + @override + String get bgScanNow => 'Executar uma vez em segundo plano agora'; + + @override + String get bgScanNowSubtitle => + 'Envia uma tarefa única para o Android. O resultado aparece após 1–2 minutos.'; + + @override + String get bgScanQueued => + 'Análise em segundo plano na fila — resultado visível em cerca de 1 min'; + + @override + String get scanAndroidOnly => 'A análise só está disponível no Android.'; + + @override + String get scanPermissionMissing => + 'Falta o acesso aos dados de utilização. Ative-o nas definições do sistema para o MediaBrain:\n\nDefinições → Aplicações → Acesso especial a aplicações → Acesso aos dados de utilização → MediaBrain.'; + + @override + String get detectedTitle => '📡 O que é detetado?'; + + @override + String get detectedApps => + 'Netflix · Disney+ · Prime Video · Apple TV · YouTube · Twitch · Spotify · Apple Music · YouTube Music · Tidal · Amazon Music · Audible · Kindle · Pocket Casts · Google Podcasts · Adobe Acrobat · Google Docs · Notion · Obsidian e mais.'; + + @override + String get appScanTitle => '🔍 Análise de aplicações'; + + @override + String get appScanDescription => + 'Análise de aplicações: uma entrada por aplicação multimédia com a utilização total dos últimos dias (através das estatísticas de utilização do Android).'; + + @override + String get neverText => 'nunca'; + + @override + String get screenSettings => 'Definições'; + + @override + String get sectionDataStatus => 'Estado dos dados'; + + @override + String get sectionFileSync => 'Sincronização de ficheiros'; + + @override + String get sectionServerSync => 'Sincronização do servidor (Mac Studio)'; + + @override + String get sectionActions => 'Ações'; + + @override + String get settingsTotalEntries => 'Total de entradas'; + + @override + String get settingsLastScan => 'Última análise'; + + @override + String get exportTitle => 'Exportar através de Partilhar'; + + @override + String get exportSubtitle => + 'JSON por e-mail, OneDrive ou Telegram para si …'; + + @override + String get importTitle => 'Escolher ficheiro de importação'; + + @override + String get importSubtitle => 'Substitui os dados locais'; + + @override + String get noServerConfigured => 'Nenhum servidor configurado'; + + @override + String get actionPush => 'Enviar'; + + @override + String get actionPull => 'Obter'; + + @override + String get refreshData => 'Atualizar dados'; + + @override + String get deleteAllData => 'Eliminar todos os dados da biblioteca'; + + @override + String get deleteAllTitle => 'Eliminar todos os dados da biblioteca?'; + + @override + String get deleteAllContent => + 'Remove todas as entradas do MediaBrain local.'; + + @override + String get cancel => 'Cancelar'; + + @override + String get delete => 'Eliminar'; + + @override + String get save => 'Guardar'; + + @override + String get libraryCleared => 'Biblioteca limpa.'; + + @override + String get cancelled => 'Cancelado.'; + + @override + String get serverConfigTitle => 'Configurar sincronização do servidor'; + + @override + String get fieldServerUrl => 'URL do servidor'; + + @override + String get fieldServerUrlHint => 'por exemplo, macstudvonlukas:8082'; + + @override + String get fieldBearerToken => 'Token Bearer'; + + @override + String get aboutTitle => 'MediaBrain Mobile'; + + @override + String get aboutSubtitle => + 'Aplicação Flutter autónoma. Deteta aplicações multimédia instaladas, regista entradas manuais e sincroniza através de ficheiro ou servidor.'; + + @override + String get detailDescription => 'Descrição'; + + @override + String get detailDetails => 'Detalhes'; + + @override + String get detailTags => 'Etiquetas'; + + @override + String get fieldArtist => 'Artista'; + + @override + String get fieldAlbum => 'Álbum'; + + @override + String get fieldChannel => 'Canal'; + + @override + String get fieldSeasonEpisodeLabel => 'Temporada/episódio'; + + @override + String get fieldLength => 'Duração'; + + @override + String get fieldLastOpened => 'Última abertura'; + + @override + String get fieldUsage => 'Utilização'; + + @override + String get fieldProviderId => 'ID do fornecedor'; + + @override + String get fieldLocalPath => 'Caminho local'; + + @override + String get openPlayStore => 'Abrir na Play Store'; + + @override + String get openBrowser => 'Abrir no navegador'; + + @override + String get linkError => 'Não foi possível abrir a ligação.'; + + @override + String get dialogTitleNew => 'Nova entrada'; + + @override + String get dialogTitleEdit => 'Editar entrada'; + + @override + String get fieldTitle => 'Título'; + + @override + String get categoryLabel => 'Categoria'; + + @override + String get fieldArtistOptional => 'Artista / realizador (opcional)'; + + @override + String get fieldAlbumOptional => 'Álbum / série (opcional)'; + + @override + String get fieldChannelOptional => 'Canal / origem (opcional)'; + + @override + String get fieldDescriptionOptional => 'Descrição (opcional)'; + + @override + String get fieldTags => 'Etiquetas (separadas por vírgulas)'; + + @override + String get fieldTagsHint => 'por exemplo, documentário, fim de semana'; + + @override + String get markAsFavorite => 'Marcar como favorito'; + + @override + String error(String msg) { + return 'Erro: $msg'; + } + + @override + String foregroundMinutesLabel(int minutes) { + return 'Total: $minutes min'; + } + + @override + String bgScanSubtitle(int hours) { + return 'Analisar a cada ~$hours h, mesmo quando a aplicação está fechada.'; + } + + @override + String lastRun(String time) { + return 'Última execução: $time'; + } + + @override + String scanResult(int matched, int persisted) { + return 'Análise de aplicações: $matched aplicações multimédia encontradas, $persisted entradas adicionadas.'; + } + + @override + String exportResult(String msg) { + return 'Exportação: $msg'; + } + + @override + String importResult(String msg) { + return 'Importação: $msg'; + } + + @override + String pushPullStatus(String push, String pull) { + return 'Envio: $push · Obtenção: $pull'; + } + + @override + String seasonEpisode(int s, int e) { + return 'T$s E$e'; + } + + @override + String usageMinutes(int n) { + return '$n minutos'; + } + + @override + String get navPlaylists => 'Listas de reprodução'; + + @override + String get screenPlaylists => 'Listas de reprodução e coleções'; + + @override + String get playlistTypeManual => 'Manual'; + + @override + String get playlistTypeSmart => 'Inteligente (filtro dinâmico)'; + + @override + String get dialogNewPlaylist => 'Nova lista de reprodução'; + + @override + String get dialogEditPlaylist => 'Editar lista de reprodução'; + + @override + String get fieldPlaylistName => 'Nome da lista de reprodução'; + + @override + String get fieldPlaylistDescription => 'Descrição (opcional)'; + + @override + String get fieldPlaylistType => 'Tipo de lista de reprodução'; + + @override + String get smartFilterCriteria => 'Critérios do filtro inteligente'; + + @override + String get smartFilterCategory => 'Categoria'; + + @override + String get smartFilterTag => 'Etiqueta / palavra-chave'; + + @override + String get smartFilterFavoritesOnly => 'Apenas favoritos'; + + @override + String get smartFilterMinMinutes => 'Tempo mínimo de utilização (min)'; + + @override + String get smartFilterSearch => 'Termo de pesquisa'; + + @override + String get emptyPlaylists => + 'Ainda não foram criadas listas de reprodução.\nCrie coleções manuais ou listas inteligentes dinâmicas.'; + + @override + String get emptyPlaylistItems => + 'Esta lista de reprodução ainda não contém conteúdos multimédia.'; + + @override + String get deletePlaylistTitle => 'Eliminar lista de reprodução?'; + + @override + String get deletePlaylistContent => + 'Tem a certeza de que pretende eliminar esta lista de reprodução? Os conteúdos nela incluídos permanecerão na biblioteca.'; + + @override + String get addToPlaylist => 'Adicionar à lista de reprodução'; + + @override + String get removeFromPlaylist => 'Remover da lista de reprodução'; + + @override + String get addedToPlaylist => 'Adicionado à lista de reprodução.'; + + @override + String get removedFromPlaylist => 'Removido da lista de reprodução.'; + + @override + String get filterByTag => 'Filtrar por etiqueta'; + + @override + String get allTags => 'Todas as etiquetas'; + + @override + String get addTag => 'Adicionar etiqueta'; + + @override + String get removeTag => 'Remover etiqueta'; + + @override + String get noPlaylistsFound => + 'Não existem listas de reprodução disponíveis.'; + + @override + String playlistItemCount(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count itens', + one: '$count item', + ); + return '$_temp0'; + } + + @override + String tagCount(String tag, int count) { + return '#$tag ($count)'; + } +} diff --git a/flutter_port/lib/l10n/app_localizations_ru.dart b/flutter_port/lib/l10n/app_localizations_ru.dart new file mode 100644 index 0000000..34f708e --- /dev/null +++ b/flutter_port/lib/l10n/app_localizations_ru.dart @@ -0,0 +1,413 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Russian (`ru`). +class AppLocalizationsRu extends AppLocalizations { + AppLocalizationsRu([String locale = 'ru']) : super(locale); + + @override + String get appTitle => 'MediaBrain'; + + @override + String get navLibrary => 'Библиотека'; + + @override + String get navScan => 'Сканирование'; + + @override + String get navMore => 'Ещё'; + + @override + String get screenLibrary => 'Библиотека'; + + @override + String get fabAdd => 'Добавить'; + + @override + String get searchHint => 'Поиск (название, исполнитель, канал …)'; + + @override + String get filterAll => 'Все'; + + @override + String get filterFavorites => '⭐ Избранное'; + + @override + String get emptyNoMatch => 'По заданным фильтрам ничего не найдено.'; + + @override + String get emptyNoItems => + 'Пока пусто.\nНажмите вкладку «Сканирование», чтобы автоматически обнаружить медиаприложения.'; + + @override + String get screenScan => 'Сканирование'; + + @override + String get scanDescription => + 'Ищет установленные медиаприложения (стриминг, музыка, подкасты, документы) и добавляет их в библиотеку. При включённом доступе к данным об использовании также будет доступно фактическое время использования.'; + + @override + String get btnAppScan => 'Сканирование приложений (сводное использование)'; + + @override + String get bgScanTitle => 'Фоновое сканирование'; + + @override + String get bgScanNow => 'Запустить один раз в фоне'; + + @override + String get bgScanNowSubtitle => + 'Отправляет разовое задание в Android. Результат появится через 1–2 минуты.'; + + @override + String get bgScanQueued => + 'Фоновое сканирование поставлено в очередь — результат будет примерно через минуту'; + + @override + String get scanAndroidOnly => 'Сканирование доступно только в Android.'; + + @override + String get scanPermissionMissing => + 'Нет доступа к данным об использовании. Включите его в системных настройках MediaBrain:\n\nНастройки → Приложения → Специальный доступ приложений → Доступ к данным об использовании → MediaBrain.'; + + @override + String get detectedTitle => '📡 Что обнаруживается?'; + + @override + String get detectedApps => + 'Netflix · Disney+ · Prime Video · Apple TV · YouTube · Twitch · Spotify · Apple Music · YouTube Music · Tidal · Amazon Music · Audible · Kindle · Pocket Casts · Google Podcasts · Adobe Acrobat · Google Docs · Notion · Obsidian и другие.'; + + @override + String get appScanTitle => '🔍 Сканирование приложений'; + + @override + String get appScanDescription => + 'Сканирование приложений: отдельная запись для каждого медиаприложения с общим временем использования за последние дни (по статистике использования Android).'; + + @override + String get neverText => 'никогда'; + + @override + String get screenSettings => 'Настройки'; + + @override + String get sectionDataStatus => 'Состояние данных'; + + @override + String get sectionFileSync => 'Синхронизация файлов'; + + @override + String get sectionServerSync => 'Синхронизация с сервером (Mac Studio)'; + + @override + String get sectionActions => 'Действия'; + + @override + String get settingsTotalEntries => 'Всего записей'; + + @override + String get settingsLastScan => 'Последнее сканирование'; + + @override + String get exportTitle => 'Экспорт через «Поделиться»'; + + @override + String get exportSubtitle => + 'Отправить JSON себе по почте, через OneDrive или Telegram …'; + + @override + String get importTitle => 'Выбрать файл для импорта'; + + @override + String get importSubtitle => 'Перезаписывает локальные данные'; + + @override + String get noServerConfigured => 'Сервер не настроен'; + + @override + String get actionPush => 'Отправить'; + + @override + String get actionPull => 'Получить'; + + @override + String get refreshData => 'Обновить данные'; + + @override + String get deleteAllData => 'Удалить все данные библиотеки'; + + @override + String get deleteAllTitle => 'Удалить все данные библиотеки?'; + + @override + String get deleteAllContent => 'Удаляет все записи из локального MediaBrain.'; + + @override + String get cancel => 'Отмена'; + + @override + String get delete => 'Удалить'; + + @override + String get save => 'Сохранить'; + + @override + String get libraryCleared => 'Библиотека очищена.'; + + @override + String get cancelled => 'Отменено.'; + + @override + String get serverConfigTitle => 'Настроить синхронизацию с сервером'; + + @override + String get fieldServerUrl => 'URL сервера'; + + @override + String get fieldServerUrlHint => 'например, macstudvonlukas:8082'; + + @override + String get fieldBearerToken => 'Токен Bearer'; + + @override + String get aboutTitle => 'MediaBrain Mobile'; + + @override + String get aboutSubtitle => + 'Автономное приложение Flutter. Обнаруживает установленные медиаприложения, учитывает записи, добавленные вручную, и синхронизирует данные через файл или сервер.'; + + @override + String get detailDescription => 'Описание'; + + @override + String get detailDetails => 'Сведения'; + + @override + String get detailTags => 'Теги'; + + @override + String get fieldArtist => 'Исполнитель'; + + @override + String get fieldAlbum => 'Альбом'; + + @override + String get fieldChannel => 'Канал'; + + @override + String get fieldSeasonEpisodeLabel => 'Сезон/серия'; + + @override + String get fieldLength => 'Длительность'; + + @override + String get fieldLastOpened => 'Последнее открытие'; + + @override + String get fieldUsage => 'Использование'; + + @override + String get fieldProviderId => 'ID провайдера'; + + @override + String get fieldLocalPath => 'Локальный путь'; + + @override + String get openPlayStore => 'Открыть в Play Store'; + + @override + String get openBrowser => 'Открыть в браузере'; + + @override + String get linkError => 'Не удалось открыть ссылку.'; + + @override + String get dialogTitleNew => 'Новая запись'; + + @override + String get dialogTitleEdit => 'Изменить запись'; + + @override + String get fieldTitle => 'Название'; + + @override + String get categoryLabel => 'Категория'; + + @override + String get fieldArtistOptional => 'Исполнитель / режиссёр (необязательно)'; + + @override + String get fieldAlbumOptional => 'Альбом / сериал (необязательно)'; + + @override + String get fieldChannelOptional => 'Канал / источник (необязательно)'; + + @override + String get fieldDescriptionOptional => 'Описание (необязательно)'; + + @override + String get fieldTags => 'Теги (через запятую)'; + + @override + String get fieldTagsHint => 'например, документальный фильм, выходные'; + + @override + String get markAsFavorite => 'Добавить в избранное'; + + @override + String error(String msg) { + return 'Ошибка: $msg'; + } + + @override + String foregroundMinutesLabel(int minutes) { + return 'Всего: $minutes мин'; + } + + @override + String bgScanSubtitle(int hours) { + return 'Сканировать каждые ~$hours ч, даже если приложение закрыто.'; + } + + @override + String lastRun(String time) { + return 'Последний запуск: $time'; + } + + @override + String scanResult(int matched, int persisted) { + return 'Сканирование приложений: найдено медиаприложений — $matched, добавлено записей — $persisted.'; + } + + @override + String exportResult(String msg) { + return 'Экспорт: $msg'; + } + + @override + String importResult(String msg) { + return 'Импорт: $msg'; + } + + @override + String pushPullStatus(String push, String pull) { + return 'Отправка: $push · Получение: $pull'; + } + + @override + String seasonEpisode(int s, int e) { + return 'S$s E$e'; + } + + @override + String usageMinutes(int n) { + return '$n мин'; + } + + @override + String get navPlaylists => 'Плейлисты'; + + @override + String get screenPlaylists => 'Плейлисты и коллекции'; + + @override + String get playlistTypeManual => 'Вручную'; + + @override + String get playlistTypeSmart => 'Умный (динамический фильтр)'; + + @override + String get dialogNewPlaylist => 'Новый плейлист'; + + @override + String get dialogEditPlaylist => 'Изменить плейлист'; + + @override + String get fieldPlaylistName => 'Название плейлиста'; + + @override + String get fieldPlaylistDescription => 'Описание (необязательно)'; + + @override + String get fieldPlaylistType => 'Тип плейлиста'; + + @override + String get smartFilterCriteria => 'Условия умного фильтра'; + + @override + String get smartFilterCategory => 'Категория'; + + @override + String get smartFilterTag => 'Тег / ключевое слово'; + + @override + String get smartFilterFavoritesOnly => 'Только избранное'; + + @override + String get smartFilterMinMinutes => 'Минимальное время использования (мин)'; + + @override + String get smartFilterSearch => 'Поисковый запрос'; + + @override + String get emptyPlaylists => + 'Плейлисты ещё не созданы.\nСоздайте ручные коллекции или динамические умные плейлисты.'; + + @override + String get emptyPlaylistItems => 'В этом плейлисте пока нет медиа.'; + + @override + String get deletePlaylistTitle => 'Удалить плейлист?'; + + @override + String get deletePlaylistContent => + 'Действительно удалить этот плейлист? Медиа останутся в библиотеке.'; + + @override + String get addToPlaylist => 'Добавить в плейлист'; + + @override + String get removeFromPlaylist => 'Удалить из плейлиста'; + + @override + String get addedToPlaylist => 'Добавлено в плейлист.'; + + @override + String get removedFromPlaylist => 'Удалено из плейлиста.'; + + @override + String get filterByTag => 'Фильтр по тегам'; + + @override + String get allTags => 'Все теги'; + + @override + String get addTag => 'Добавить тег'; + + @override + String get removeTag => 'Удалить тег'; + + @override + String get noPlaylistsFound => 'Нет доступных плейлистов.'; + + @override + String playlistItemCount(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count элемента', + many: '$count элементов', + few: '$count элемента', + one: '$count элемент', + ); + return '$_temp0'; + } + + @override + String tagCount(String tag, int count) { + return '#$tag ($count)'; + } +} diff --git a/flutter_port/lib/l10n/app_localizations_zh.dart b/flutter_port/lib/l10n/app_localizations_zh.dart new file mode 100644 index 0000000..1d1df44 --- /dev/null +++ b/flutter_port/lib/l10n/app_localizations_zh.dart @@ -0,0 +1,404 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Chinese (`zh`). +class AppLocalizationsZh extends AppLocalizations { + AppLocalizationsZh([String locale = 'zh']) : super(locale); + + @override + String get appTitle => 'MediaBrain'; + + @override + String get navLibrary => '媒体库'; + + @override + String get navScan => '扫描'; + + @override + String get navMore => '更多'; + + @override + String get screenLibrary => '媒体库'; + + @override + String get fabAdd => '添加'; + + @override + String get searchHint => '搜索(标题、艺术家、频道……)'; + + @override + String get filterAll => '全部'; + + @override + String get filterFavorites => '⭐ 收藏'; + + @override + String get emptyNoMatch => '没有符合筛选条件的结果。'; + + @override + String get emptyNoItems => '内容为空。\n点击“扫描”标签页,自动检测媒体应用。'; + + @override + String get screenScan => '扫描'; + + @override + String get scanDescription => + '搜索已安装的媒体应用(流媒体、音乐、播客、文档),并将其添加到媒体库。启用使用数据访问权限后,还可以获取实际使用时长。'; + + @override + String get btnAppScan => '应用扫描(汇总使用情况)'; + + @override + String get bgScanTitle => '后台扫描'; + + @override + String get bgScanNow => '立即在后台运行一次'; + + @override + String get bgScanNowSubtitle => '向 Android 发送一次性任务。结果将在 1–2 分钟后显示。'; + + @override + String get bgScanQueued => '后台扫描已排队 — 约 1 分钟后可查看结果'; + + @override + String get scanAndroidOnly => '扫描仅适用于 Android。'; + + @override + String get scanPermissionMissing => + '缺少使用数据访问权限。请在 MediaBrain 的系统设置中启用:\n\n设置 → 应用 → 特殊应用访问权限 → 使用情况访问权限 → MediaBrain。'; + + @override + String get detectedTitle => '📡 可以检测什么?'; + + @override + String get detectedApps => + 'Netflix · Disney+ · Prime Video · Apple TV · YouTube · Twitch · Spotify · Apple Music · YouTube Music · Tidal · Amazon Music · Audible · Kindle · Pocket Casts · Google Podcasts · Adobe Acrobat · Google Docs · Notion · Obsidian 等。'; + + @override + String get appScanTitle => '🔍 应用扫描'; + + @override + String get appScanDescription => + '应用扫描:每个媒体应用生成一个条目,显示最近几天的总使用情况(通过 Android 使用情况统计)。'; + + @override + String get neverText => '从不'; + + @override + String get screenSettings => '设置'; + + @override + String get sectionDataStatus => '数据状态'; + + @override + String get sectionFileSync => '文件同步'; + + @override + String get sectionServerSync => '服务器同步(Mac Studio)'; + + @override + String get sectionActions => '操作'; + + @override + String get settingsTotalEntries => '条目总数'; + + @override + String get settingsLastScan => '上次扫描'; + + @override + String get exportTitle => '通过分享导出'; + + @override + String get exportSubtitle => '通过邮件、OneDrive、Telegram 将 JSON 发给自己 …'; + + @override + String get importTitle => '选择导入文件'; + + @override + String get importSubtitle => '覆盖本地数据'; + + @override + String get noServerConfigured => '未配置服务器'; + + @override + String get actionPush => '推送'; + + @override + String get actionPull => '拉取'; + + @override + String get refreshData => '刷新数据'; + + @override + String get deleteAllData => '删除媒体库中的所有数据'; + + @override + String get deleteAllTitle => '删除媒体库中的所有数据?'; + + @override + String get deleteAllContent => '从本地 MediaBrain 中删除所有条目。'; + + @override + String get cancel => '取消'; + + @override + String get delete => '删除'; + + @override + String get save => '保存'; + + @override + String get libraryCleared => '媒体库已清空。'; + + @override + String get cancelled => '已取消。'; + + @override + String get serverConfigTitle => '配置服务器同步'; + + @override + String get fieldServerUrl => '服务器 URL'; + + @override + String get fieldServerUrlHint => '例如:macstudvonlukas:8082'; + + @override + String get fieldBearerToken => 'Bearer 令牌'; + + @override + String get aboutTitle => 'MediaBrain Mobile'; + + @override + String get aboutSubtitle => + '独立的 Flutter 应用。检测已安装的媒体应用、记录手动添加的条目,并通过文件或服务器同步。'; + + @override + String get detailDescription => '描述'; + + @override + String get detailDetails => '详情'; + + @override + String get detailTags => '标签'; + + @override + String get fieldArtist => '艺术家'; + + @override + String get fieldAlbum => '专辑'; + + @override + String get fieldChannel => '频道'; + + @override + String get fieldSeasonEpisodeLabel => '季/集'; + + @override + String get fieldLength => '时长'; + + @override + String get fieldLastOpened => '上次打开'; + + @override + String get fieldUsage => '使用情况'; + + @override + String get fieldProviderId => '提供方 ID'; + + @override + String get fieldLocalPath => '本地路径'; + + @override + String get openPlayStore => '在 Play 商店中打开'; + + @override + String get openBrowser => '在浏览器中打开'; + + @override + String get linkError => '无法打开链接。'; + + @override + String get dialogTitleNew => '新建条目'; + + @override + String get dialogTitleEdit => '编辑条目'; + + @override + String get fieldTitle => '标题'; + + @override + String get categoryLabel => '类别'; + + @override + String get fieldArtistOptional => '艺术家 / 导演(可选)'; + + @override + String get fieldAlbumOptional => '专辑 / 系列(可选)'; + + @override + String get fieldChannelOptional => '频道 / 来源(可选)'; + + @override + String get fieldDescriptionOptional => '描述(可选)'; + + @override + String get fieldTags => '标签(用逗号分隔)'; + + @override + String get fieldTagsHint => '例如:纪录片、周末'; + + @override + String get markAsFavorite => '标记为收藏'; + + @override + String error(String msg) { + return '错误:$msg'; + } + + @override + String foregroundMinutesLabel(int minutes) { + return '共 $minutes 分钟'; + } + + @override + String bgScanSubtitle(int hours) { + return '每隔约 $hours 小时扫描一次,即使应用已关闭也会扫描。'; + } + + @override + String lastRun(String time) { + return '上次运行:$time'; + } + + @override + String scanResult(int matched, int persisted) { + return '应用扫描:发现 $matched 个媒体应用,添加了 $persisted 个条目。'; + } + + @override + String exportResult(String msg) { + return '导出:$msg'; + } + + @override + String importResult(String msg) { + return '导入:$msg'; + } + + @override + String pushPullStatus(String push, String pull) { + return '推送:$push · 拉取:$pull'; + } + + @override + String seasonEpisode(int s, int e) { + return '第 $s 季 第 $e 集'; + } + + @override + String usageMinutes(int n) { + return '$n 分钟'; + } + + @override + String get navPlaylists => '播放列表'; + + @override + String get screenPlaylists => '播放列表与收藏集'; + + @override + String get playlistTypeManual => '手动'; + + @override + String get playlistTypeSmart => '智能(动态筛选)'; + + @override + String get dialogNewPlaylist => '新建播放列表'; + + @override + String get dialogEditPlaylist => '编辑播放列表'; + + @override + String get fieldPlaylistName => '播放列表名称'; + + @override + String get fieldPlaylistDescription => '描述(可选)'; + + @override + String get fieldPlaylistType => '播放列表类型'; + + @override + String get smartFilterCriteria => '智能筛选条件'; + + @override + String get smartFilterCategory => '类别'; + + @override + String get smartFilterTag => '标签 / 关键词'; + + @override + String get smartFilterFavoritesOnly => '仅收藏'; + + @override + String get smartFilterMinMinutes => '最短使用时长(分钟)'; + + @override + String get smartFilterSearch => '搜索词'; + + @override + String get emptyPlaylists => '尚未创建播放列表。\n创建手动收藏集或动态智能播放列表。'; + + @override + String get emptyPlaylistItems => '此播放列表中还没有媒体内容。'; + + @override + String get deletePlaylistTitle => '删除播放列表?'; + + @override + String get deletePlaylistContent => '确定要删除此播放列表吗?其中的媒体项目仍会保留在媒体库中。'; + + @override + String get addToPlaylist => '添加到播放列表'; + + @override + String get removeFromPlaylist => '从播放列表移除'; + + @override + String get addedToPlaylist => '已添加到播放列表。'; + + @override + String get removedFromPlaylist => '已从播放列表移除。'; + + @override + String get filterByTag => '按标签筛选'; + + @override + String get allTags => '所有标签'; + + @override + String get addTag => '添加标签'; + + @override + String get removeTag => '移除标签'; + + @override + String get noPlaylistsFound => '没有可用的播放列表。'; + + @override + String playlistItemCount(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 个项目', + ); + return '$_temp0'; + } + + @override + String tagCount(String tag, int count) { + return '#$tag ($count)'; + } +} diff --git a/flutter_port/lib/l10n/app_pt.arb b/flutter_port/lib/l10n/app_pt.arb new file mode 100644 index 0000000..b46ed21 --- /dev/null +++ b/flutter_port/lib/l10n/app_pt.arb @@ -0,0 +1,221 @@ +{ + "@@locale": "pt", + "appTitle": "MediaBrain", + "navLibrary": "Biblioteca", + "navScan": "Analisar", + "navMore": "Mais", + "screenLibrary": "Biblioteca", + "fabAdd": "Adicionar", + "searchHint": "Pesquisar (título, artista, canal …)", + "filterAll": "Todos", + "filterFavorites": "⭐ Favoritos", + "emptyNoMatch": "Não há resultados para os filtros selecionados.", + "emptyNoItems": "Ainda está vazio.\nToque no separador Analisar para detetar automaticamente as aplicações multimédia.", + "screenScan": "Analisar", + "scanDescription": "Procura aplicações multimédia instaladas (streaming, música, podcasts, documentos) e adiciona-as à biblioteca. Com o acesso aos dados de utilização ativado, também obtém o tempo real de utilização.", + "btnAppScan": "Análise de aplicações (utilização agregada)", + "bgScanTitle": "Análise em segundo plano", + "bgScanNow": "Executar uma vez em segundo plano agora", + "bgScanNowSubtitle": "Envia uma tarefa única para o Android. O resultado aparece após 1–2 minutos.", + "bgScanQueued": "Análise em segundo plano na fila — resultado visível em cerca de 1 min", + "scanAndroidOnly": "A análise só está disponível no Android.", + "scanPermissionMissing": "Falta o acesso aos dados de utilização. Ative-o nas definições do sistema para o MediaBrain:\n\nDefinições → Aplicações → Acesso especial a aplicações → Acesso aos dados de utilização → MediaBrain.", + "detectedTitle": "📡 O que é detetado?", + "detectedApps": "Netflix · Disney+ · Prime Video · Apple TV · YouTube · Twitch · Spotify · Apple Music · YouTube Music · Tidal · Amazon Music · Audible · Kindle · Pocket Casts · Google Podcasts · Adobe Acrobat · Google Docs · Notion · Obsidian e mais.", + "appScanTitle": "🔍 Análise de aplicações", + "appScanDescription": "Análise de aplicações: uma entrada por aplicação multimédia com a utilização total dos últimos dias (através das estatísticas de utilização do Android).", + "neverText": "nunca", + "screenSettings": "Definições", + "sectionDataStatus": "Estado dos dados", + "sectionFileSync": "Sincronização de ficheiros", + "sectionServerSync": "Sincronização do servidor (Mac Studio)", + "sectionActions": "Ações", + "settingsTotalEntries": "Total de entradas", + "settingsLastScan": "Última análise", + "exportTitle": "Exportar através de Partilhar", + "exportSubtitle": "JSON por e-mail, OneDrive ou Telegram para si …", + "importTitle": "Escolher ficheiro de importação", + "importSubtitle": "Substitui os dados locais", + "noServerConfigured": "Nenhum servidor configurado", + "actionPush": "Enviar", + "actionPull": "Obter", + "refreshData": "Atualizar dados", + "deleteAllData": "Eliminar todos os dados da biblioteca", + "deleteAllTitle": "Eliminar todos os dados da biblioteca?", + "deleteAllContent": "Remove todas as entradas do MediaBrain local.", + "cancel": "Cancelar", + "delete": "Eliminar", + "save": "Guardar", + "libraryCleared": "Biblioteca limpa.", + "cancelled": "Cancelado.", + "serverConfigTitle": "Configurar sincronização do servidor", + "fieldServerUrl": "URL do servidor", + "fieldServerUrlHint": "por exemplo, macstudvonlukas:8082", + "fieldBearerToken": "Token Bearer", + "aboutTitle": "MediaBrain Mobile", + "aboutSubtitle": "Aplicação Flutter autónoma. Deteta aplicações multimédia instaladas, regista entradas manuais e sincroniza através de ficheiro ou servidor.", + "detailDescription": "Descrição", + "detailDetails": "Detalhes", + "detailTags": "Etiquetas", + "fieldArtist": "Artista", + "fieldAlbum": "Álbum", + "fieldChannel": "Canal", + "fieldSeasonEpisodeLabel": "Temporada/episódio", + "fieldLength": "Duração", + "fieldLastOpened": "Última abertura", + "fieldUsage": "Utilização", + "fieldProviderId": "ID do fornecedor", + "fieldLocalPath": "Caminho local", + "openPlayStore": "Abrir na Play Store", + "openBrowser": "Abrir no navegador", + "linkError": "Não foi possível abrir a ligação.", + "dialogTitleNew": "Nova entrada", + "dialogTitleEdit": "Editar entrada", + "fieldTitle": "Título", + "categoryLabel": "Categoria", + "fieldArtistOptional": "Artista / realizador (opcional)", + "fieldAlbumOptional": "Álbum / série (opcional)", + "fieldChannelOptional": "Canal / origem (opcional)", + "fieldDescriptionOptional": "Descrição (opcional)", + "fieldTags": "Etiquetas (separadas por vírgulas)", + "fieldTagsHint": "por exemplo, documentário, fim de semana", + "markAsFavorite": "Marcar como favorito", + "error": "Erro: {msg}", + "@error": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "foregroundMinutesLabel": "Total: {minutes} min", + "@foregroundMinutesLabel": { + "placeholders": { + "minutes": { + "type": "int" + } + } + }, + "bgScanSubtitle": "Analisar a cada ~{hours} h, mesmo quando a aplicação está fechada.", + "@bgScanSubtitle": { + "placeholders": { + "hours": { + "type": "int" + } + } + }, + "lastRun": "Última execução: {time}", + "@lastRun": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "scanResult": "Análise de aplicações: {matched} aplicações multimédia encontradas, {persisted} entradas adicionadas.", + "@scanResult": { + "placeholders": { + "matched": { + "type": "int" + }, + "persisted": { + "type": "int" + } + } + }, + "exportResult": "Exportação: {msg}", + "@exportResult": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "importResult": "Importação: {msg}", + "@importResult": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "pushPullStatus": "Envio: {push} · Obtenção: {pull}", + "@pushPullStatus": { + "placeholders": { + "push": { + "type": "String" + }, + "pull": { + "type": "String" + } + } + }, + "seasonEpisode": "T{s} E{e}", + "@seasonEpisode": { + "placeholders": { + "s": { + "type": "int" + }, + "e": { + "type": "int" + } + } + }, + "usageMinutes": "{n} minutos", + "@usageMinutes": { + "placeholders": { + "n": { + "type": "int" + } + } + }, + "navPlaylists": "Listas de reprodução", + "screenPlaylists": "Listas de reprodução e coleções", + "playlistTypeManual": "Manual", + "playlistTypeSmart": "Inteligente (filtro dinâmico)", + "dialogNewPlaylist": "Nova lista de reprodução", + "dialogEditPlaylist": "Editar lista de reprodução", + "fieldPlaylistName": "Nome da lista de reprodução", + "fieldPlaylistDescription": "Descrição (opcional)", + "fieldPlaylistType": "Tipo de lista de reprodução", + "smartFilterCriteria": "Critérios do filtro inteligente", + "smartFilterCategory": "Categoria", + "smartFilterTag": "Etiqueta / palavra-chave", + "smartFilterFavoritesOnly": "Apenas favoritos", + "smartFilterMinMinutes": "Tempo mínimo de utilização (min)", + "smartFilterSearch": "Termo de pesquisa", + "emptyPlaylists": "Ainda não foram criadas listas de reprodução.\nCrie coleções manuais ou listas inteligentes dinâmicas.", + "emptyPlaylistItems": "Esta lista de reprodução ainda não contém conteúdos multimédia.", + "deletePlaylistTitle": "Eliminar lista de reprodução?", + "deletePlaylistContent": "Tem a certeza de que pretende eliminar esta lista de reprodução? Os conteúdos nela incluídos permanecerão na biblioteca.", + "addToPlaylist": "Adicionar à lista de reprodução", + "removeFromPlaylist": "Remover da lista de reprodução", + "addedToPlaylist": "Adicionado à lista de reprodução.", + "removedFromPlaylist": "Removido da lista de reprodução.", + "filterByTag": "Filtrar por etiqueta", + "allTags": "Todas as etiquetas", + "addTag": "Adicionar etiqueta", + "removeTag": "Remover etiqueta", + "noPlaylistsFound": "Não existem listas de reprodução disponíveis.", + "playlistItemCount": "{count, plural, one {{count} item} other {{count} itens}}", + "tagCount": "#{tag} ({count})", + "@playlistItemCount": { + "description": "Number of items in a playlist.", + "placeholders": { + "count": { + "type": "num" + } + } + }, + "@tagCount": { + "description": "Tag and number of matching entries.", + "placeholders": { + "tag": { + "type": "String" + }, + "count": { + "type": "int" + } + } + } +} diff --git a/flutter_port/lib/l10n/app_ru.arb b/flutter_port/lib/l10n/app_ru.arb new file mode 100644 index 0000000..b80a1e3 --- /dev/null +++ b/flutter_port/lib/l10n/app_ru.arb @@ -0,0 +1,221 @@ +{ + "@@locale": "ru", + "appTitle": "MediaBrain", + "navLibrary": "Библиотека", + "navScan": "Сканирование", + "navMore": "Ещё", + "screenLibrary": "Библиотека", + "fabAdd": "Добавить", + "searchHint": "Поиск (название, исполнитель, канал …)", + "filterAll": "Все", + "filterFavorites": "⭐ Избранное", + "emptyNoMatch": "По заданным фильтрам ничего не найдено.", + "emptyNoItems": "Пока пусто.\nНажмите вкладку «Сканирование», чтобы автоматически обнаружить медиаприложения.", + "screenScan": "Сканирование", + "scanDescription": "Ищет установленные медиаприложения (стриминг, музыка, подкасты, документы) и добавляет их в библиотеку. При включённом доступе к данным об использовании также будет доступно фактическое время использования.", + "btnAppScan": "Сканирование приложений (сводное использование)", + "bgScanTitle": "Фоновое сканирование", + "bgScanNow": "Запустить один раз в фоне", + "bgScanNowSubtitle": "Отправляет разовое задание в Android. Результат появится через 1–2 минуты.", + "bgScanQueued": "Фоновое сканирование поставлено в очередь — результат будет примерно через минуту", + "scanAndroidOnly": "Сканирование доступно только в Android.", + "scanPermissionMissing": "Нет доступа к данным об использовании. Включите его в системных настройках MediaBrain:\n\nНастройки → Приложения → Специальный доступ приложений → Доступ к данным об использовании → MediaBrain.", + "detectedTitle": "📡 Что обнаруживается?", + "detectedApps": "Netflix · Disney+ · Prime Video · Apple TV · YouTube · Twitch · Spotify · Apple Music · YouTube Music · Tidal · Amazon Music · Audible · Kindle · Pocket Casts · Google Podcasts · Adobe Acrobat · Google Docs · Notion · Obsidian и другие.", + "appScanTitle": "🔍 Сканирование приложений", + "appScanDescription": "Сканирование приложений: отдельная запись для каждого медиаприложения с общим временем использования за последние дни (по статистике использования Android).", + "neverText": "никогда", + "screenSettings": "Настройки", + "sectionDataStatus": "Состояние данных", + "sectionFileSync": "Синхронизация файлов", + "sectionServerSync": "Синхронизация с сервером (Mac Studio)", + "sectionActions": "Действия", + "settingsTotalEntries": "Всего записей", + "settingsLastScan": "Последнее сканирование", + "exportTitle": "Экспорт через «Поделиться»", + "exportSubtitle": "Отправить JSON себе по почте, через OneDrive или Telegram …", + "importTitle": "Выбрать файл для импорта", + "importSubtitle": "Перезаписывает локальные данные", + "noServerConfigured": "Сервер не настроен", + "actionPush": "Отправить", + "actionPull": "Получить", + "refreshData": "Обновить данные", + "deleteAllData": "Удалить все данные библиотеки", + "deleteAllTitle": "Удалить все данные библиотеки?", + "deleteAllContent": "Удаляет все записи из локального MediaBrain.", + "cancel": "Отмена", + "delete": "Удалить", + "save": "Сохранить", + "libraryCleared": "Библиотека очищена.", + "cancelled": "Отменено.", + "serverConfigTitle": "Настроить синхронизацию с сервером", + "fieldServerUrl": "URL сервера", + "fieldServerUrlHint": "например, macstudvonlukas:8082", + "fieldBearerToken": "Токен Bearer", + "aboutTitle": "MediaBrain Mobile", + "aboutSubtitle": "Автономное приложение Flutter. Обнаруживает установленные медиаприложения, учитывает записи, добавленные вручную, и синхронизирует данные через файл или сервер.", + "detailDescription": "Описание", + "detailDetails": "Сведения", + "detailTags": "Теги", + "fieldArtist": "Исполнитель", + "fieldAlbum": "Альбом", + "fieldChannel": "Канал", + "fieldSeasonEpisodeLabel": "Сезон/серия", + "fieldLength": "Длительность", + "fieldLastOpened": "Последнее открытие", + "fieldUsage": "Использование", + "fieldProviderId": "ID провайдера", + "fieldLocalPath": "Локальный путь", + "openPlayStore": "Открыть в Play Store", + "openBrowser": "Открыть в браузере", + "linkError": "Не удалось открыть ссылку.", + "dialogTitleNew": "Новая запись", + "dialogTitleEdit": "Изменить запись", + "fieldTitle": "Название", + "categoryLabel": "Категория", + "fieldArtistOptional": "Исполнитель / режиссёр (необязательно)", + "fieldAlbumOptional": "Альбом / сериал (необязательно)", + "fieldChannelOptional": "Канал / источник (необязательно)", + "fieldDescriptionOptional": "Описание (необязательно)", + "fieldTags": "Теги (через запятую)", + "fieldTagsHint": "например, документальный фильм, выходные", + "markAsFavorite": "Добавить в избранное", + "error": "Ошибка: {msg}", + "@error": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "foregroundMinutesLabel": "Всего: {minutes} мин", + "@foregroundMinutesLabel": { + "placeholders": { + "minutes": { + "type": "int" + } + } + }, + "bgScanSubtitle": "Сканировать каждые ~{hours} ч, даже если приложение закрыто.", + "@bgScanSubtitle": { + "placeholders": { + "hours": { + "type": "int" + } + } + }, + "lastRun": "Последний запуск: {time}", + "@lastRun": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "scanResult": "Сканирование приложений: найдено медиаприложений — {matched}, добавлено записей — {persisted}.", + "@scanResult": { + "placeholders": { + "matched": { + "type": "int" + }, + "persisted": { + "type": "int" + } + } + }, + "exportResult": "Экспорт: {msg}", + "@exportResult": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "importResult": "Импорт: {msg}", + "@importResult": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "pushPullStatus": "Отправка: {push} · Получение: {pull}", + "@pushPullStatus": { + "placeholders": { + "push": { + "type": "String" + }, + "pull": { + "type": "String" + } + } + }, + "seasonEpisode": "S{s} E{e}", + "@seasonEpisode": { + "placeholders": { + "s": { + "type": "int" + }, + "e": { + "type": "int" + } + } + }, + "usageMinutes": "{n} мин", + "@usageMinutes": { + "placeholders": { + "n": { + "type": "int" + } + } + }, + "navPlaylists": "Плейлисты", + "screenPlaylists": "Плейлисты и коллекции", + "playlistTypeManual": "Вручную", + "playlistTypeSmart": "Умный (динамический фильтр)", + "dialogNewPlaylist": "Новый плейлист", + "dialogEditPlaylist": "Изменить плейлист", + "fieldPlaylistName": "Название плейлиста", + "fieldPlaylistDescription": "Описание (необязательно)", + "fieldPlaylistType": "Тип плейлиста", + "smartFilterCriteria": "Условия умного фильтра", + "smartFilterCategory": "Категория", + "smartFilterTag": "Тег / ключевое слово", + "smartFilterFavoritesOnly": "Только избранное", + "smartFilterMinMinutes": "Минимальное время использования (мин)", + "smartFilterSearch": "Поисковый запрос", + "emptyPlaylists": "Плейлисты ещё не созданы.\nСоздайте ручные коллекции или динамические умные плейлисты.", + "emptyPlaylistItems": "В этом плейлисте пока нет медиа.", + "deletePlaylistTitle": "Удалить плейлист?", + "deletePlaylistContent": "Действительно удалить этот плейлист? Медиа останутся в библиотеке.", + "addToPlaylist": "Добавить в плейлист", + "removeFromPlaylist": "Удалить из плейлиста", + "addedToPlaylist": "Добавлено в плейлист.", + "removedFromPlaylist": "Удалено из плейлиста.", + "filterByTag": "Фильтр по тегам", + "allTags": "Все теги", + "addTag": "Добавить тег", + "removeTag": "Удалить тег", + "noPlaylistsFound": "Нет доступных плейлистов.", + "playlistItemCount": "{count, plural, one {{count} элемент} few {{count} элемента} many {{count} элементов} other {{count} элемента}}", + "tagCount": "#{tag} ({count})", + "@playlistItemCount": { + "description": "Number of items in a playlist.", + "placeholders": { + "count": { + "type": "num" + } + } + }, + "@tagCount": { + "description": "Tag and number of matching entries.", + "placeholders": { + "tag": { + "type": "String" + }, + "count": { + "type": "int" + } + } + } +} diff --git a/flutter_port/lib/l10n/app_zh.arb b/flutter_port/lib/l10n/app_zh.arb new file mode 100644 index 0000000..b48e78f --- /dev/null +++ b/flutter_port/lib/l10n/app_zh.arb @@ -0,0 +1,221 @@ +{ + "@@locale": "zh", + "appTitle": "MediaBrain", + "navLibrary": "媒体库", + "navScan": "扫描", + "navMore": "更多", + "screenLibrary": "媒体库", + "fabAdd": "添加", + "searchHint": "搜索(标题、艺术家、频道……)", + "filterAll": "全部", + "filterFavorites": "⭐ 收藏", + "emptyNoMatch": "没有符合筛选条件的结果。", + "emptyNoItems": "内容为空。\n点击“扫描”标签页,自动检测媒体应用。", + "screenScan": "扫描", + "scanDescription": "搜索已安装的媒体应用(流媒体、音乐、播客、文档),并将其添加到媒体库。启用使用数据访问权限后,还可以获取实际使用时长。", + "btnAppScan": "应用扫描(汇总使用情况)", + "bgScanTitle": "后台扫描", + "bgScanNow": "立即在后台运行一次", + "bgScanNowSubtitle": "向 Android 发送一次性任务。结果将在 1–2 分钟后显示。", + "bgScanQueued": "后台扫描已排队 — 约 1 分钟后可查看结果", + "scanAndroidOnly": "扫描仅适用于 Android。", + "scanPermissionMissing": "缺少使用数据访问权限。请在 MediaBrain 的系统设置中启用:\n\n设置 → 应用 → 特殊应用访问权限 → 使用情况访问权限 → MediaBrain。", + "detectedTitle": "📡 可以检测什么?", + "detectedApps": "Netflix · Disney+ · Prime Video · Apple TV · YouTube · Twitch · Spotify · Apple Music · YouTube Music · Tidal · Amazon Music · Audible · Kindle · Pocket Casts · Google Podcasts · Adobe Acrobat · Google Docs · Notion · Obsidian 等。", + "appScanTitle": "🔍 应用扫描", + "appScanDescription": "应用扫描:每个媒体应用生成一个条目,显示最近几天的总使用情况(通过 Android 使用情况统计)。", + "neverText": "从不", + "screenSettings": "设置", + "sectionDataStatus": "数据状态", + "sectionFileSync": "文件同步", + "sectionServerSync": "服务器同步(Mac Studio)", + "sectionActions": "操作", + "settingsTotalEntries": "条目总数", + "settingsLastScan": "上次扫描", + "exportTitle": "通过分享导出", + "exportSubtitle": "通过邮件、OneDrive、Telegram 将 JSON 发给自己 …", + "importTitle": "选择导入文件", + "importSubtitle": "覆盖本地数据", + "noServerConfigured": "未配置服务器", + "actionPush": "推送", + "actionPull": "拉取", + "refreshData": "刷新数据", + "deleteAllData": "删除媒体库中的所有数据", + "deleteAllTitle": "删除媒体库中的所有数据?", + "deleteAllContent": "从本地 MediaBrain 中删除所有条目。", + "cancel": "取消", + "delete": "删除", + "save": "保存", + "libraryCleared": "媒体库已清空。", + "cancelled": "已取消。", + "serverConfigTitle": "配置服务器同步", + "fieldServerUrl": "服务器 URL", + "fieldServerUrlHint": "例如:macstudvonlukas:8082", + "fieldBearerToken": "Bearer 令牌", + "aboutTitle": "MediaBrain Mobile", + "aboutSubtitle": "独立的 Flutter 应用。检测已安装的媒体应用、记录手动添加的条目,并通过文件或服务器同步。", + "detailDescription": "描述", + "detailDetails": "详情", + "detailTags": "标签", + "fieldArtist": "艺术家", + "fieldAlbum": "专辑", + "fieldChannel": "频道", + "fieldSeasonEpisodeLabel": "季/集", + "fieldLength": "时长", + "fieldLastOpened": "上次打开", + "fieldUsage": "使用情况", + "fieldProviderId": "提供方 ID", + "fieldLocalPath": "本地路径", + "openPlayStore": "在 Play 商店中打开", + "openBrowser": "在浏览器中打开", + "linkError": "无法打开链接。", + "dialogTitleNew": "新建条目", + "dialogTitleEdit": "编辑条目", + "fieldTitle": "标题", + "categoryLabel": "类别", + "fieldArtistOptional": "艺术家 / 导演(可选)", + "fieldAlbumOptional": "专辑 / 系列(可选)", + "fieldChannelOptional": "频道 / 来源(可选)", + "fieldDescriptionOptional": "描述(可选)", + "fieldTags": "标签(用逗号分隔)", + "fieldTagsHint": "例如:纪录片、周末", + "markAsFavorite": "标记为收藏", + "error": "错误:{msg}", + "@error": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "foregroundMinutesLabel": "共 {minutes} 分钟", + "@foregroundMinutesLabel": { + "placeholders": { + "minutes": { + "type": "int" + } + } + }, + "bgScanSubtitle": "每隔约 {hours} 小时扫描一次,即使应用已关闭也会扫描。", + "@bgScanSubtitle": { + "placeholders": { + "hours": { + "type": "int" + } + } + }, + "lastRun": "上次运行:{time}", + "@lastRun": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "scanResult": "应用扫描:发现 {matched} 个媒体应用,添加了 {persisted} 个条目。", + "@scanResult": { + "placeholders": { + "matched": { + "type": "int" + }, + "persisted": { + "type": "int" + } + } + }, + "exportResult": "导出:{msg}", + "@exportResult": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "importResult": "导入:{msg}", + "@importResult": { + "placeholders": { + "msg": { + "type": "String" + } + } + }, + "pushPullStatus": "推送:{push} · 拉取:{pull}", + "@pushPullStatus": { + "placeholders": { + "push": { + "type": "String" + }, + "pull": { + "type": "String" + } + } + }, + "seasonEpisode": "第 {s} 季 第 {e} 集", + "@seasonEpisode": { + "placeholders": { + "s": { + "type": "int" + }, + "e": { + "type": "int" + } + } + }, + "usageMinutes": "{n} 分钟", + "@usageMinutes": { + "placeholders": { + "n": { + "type": "int" + } + } + }, + "navPlaylists": "播放列表", + "screenPlaylists": "播放列表与收藏集", + "playlistTypeManual": "手动", + "playlistTypeSmart": "智能(动态筛选)", + "dialogNewPlaylist": "新建播放列表", + "dialogEditPlaylist": "编辑播放列表", + "fieldPlaylistName": "播放列表名称", + "fieldPlaylistDescription": "描述(可选)", + "fieldPlaylistType": "播放列表类型", + "smartFilterCriteria": "智能筛选条件", + "smartFilterCategory": "类别", + "smartFilterTag": "标签 / 关键词", + "smartFilterFavoritesOnly": "仅收藏", + "smartFilterMinMinutes": "最短使用时长(分钟)", + "smartFilterSearch": "搜索词", + "emptyPlaylists": "尚未创建播放列表。\n创建手动收藏集或动态智能播放列表。", + "emptyPlaylistItems": "此播放列表中还没有媒体内容。", + "deletePlaylistTitle": "删除播放列表?", + "deletePlaylistContent": "确定要删除此播放列表吗?其中的媒体项目仍会保留在媒体库中。", + "addToPlaylist": "添加到播放列表", + "removeFromPlaylist": "从播放列表移除", + "addedToPlaylist": "已添加到播放列表。", + "removedFromPlaylist": "已从播放列表移除。", + "filterByTag": "按标签筛选", + "allTags": "所有标签", + "addTag": "添加标签", + "removeTag": "移除标签", + "noPlaylistsFound": "没有可用的播放列表。", + "playlistItemCount": "{count, plural, other {{count} 个项目}}", + "tagCount": "#{tag} ({count})", + "@playlistItemCount": { + "description": "Number of items in a playlist.", + "placeholders": { + "count": { + "type": "num" + } + } + }, + "@tagCount": { + "description": "Tag and number of matching entries.", + "placeholders": { + "tag": { + "type": "String" + }, + "count": { + "type": "int" + } + } + } +} diff --git a/flutter_port/pubspec.yaml b/flutter_port/pubspec.yaml index 50f4466..981de80 100644 --- a/flutter_port/pubspec.yaml +++ b/flutter_port/pubspec.yaml @@ -65,6 +65,7 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: + generate: true # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in diff --git a/flutter_port/test/l10n_test.dart b/flutter_port/test/l10n_test.dart index cfc3121..8045830 100644 --- a/flutter_port/test/l10n_test.dart +++ b/flutter_port/test/l10n_test.dart @@ -10,9 +10,7 @@ Widget _app({Locale locale = const Locale('de')}) { locale: locale, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, - home: Builder( - builder: (ctx) => Text(AppLocalizations.of(ctx).appTitle), - ), + home: Builder(builder: (ctx) => Text(AppLocalizations.of(ctx).appTitle)), ); } @@ -89,4 +87,97 @@ void main() { final loc = _loc(tester); expect(loc.seasonEpisode(2, 5), 'S2 E5'); }); + + testWidgets('all 11 generated locales resolve into their own bundle', ( + tester, + ) async { + const locales = [ + Locale('de'), + Locale('en'), + Locale('es'), + Locale('zh'), + Locale('ja'), + Locale('ru'), + Locale('fr'), + Locale('hi'), + Locale('ar'), + Locale('bn'), + Locale('pt'), + ]; + for (final locale in locales) { + await tester.pumpWidget(_app(locale: locale)); + await tester.pump(); + final context = tester.element(find.byType(Text).first); + expect( + Localizations.localeOf(context).languageCode, + locale.languageCode, + reason: 'The requested ${locale.languageCode} ARB should resolve.', + ); + final label = _loc(tester).navLibrary; + expect(label, isNotEmpty); + final countLabel = _loc(tester).playlistItemCount(7); + expect(countLabel, contains('7')); + expect(countLabel, isNot(contains('#'))); + if (locale.languageCode != 'en') { + expect( + label, + isNot('Library'), + reason: '${locale.languageCode} must not silently use English.', + ); + } + } + }); + + testWidgets( + 'regional locales match language bundles and unknown locale falls back to German', + (tester) async { + await tester.pumpWidget(_app(locale: const Locale('es', 'MX'))); + await tester.pump(); + var context = tester.element(find.byType(Text).first); + expect(Localizations.localeOf(context), const Locale('es')); + expect(_loc(tester).navLibrary, 'Biblioteca'); + + await tester.pumpWidget(_app(locale: const Locale('zz'))); + await tester.pump(); + context = tester.element(find.byType(Text).first); + expect(Localizations.localeOf(context), const Locale('de')); + expect(_loc(tester).navLibrary, 'Bibliothek'); + }, + ); + + testWidgets('generated placeholders and ICU plurals preserve call behavior', ( + tester, + ) async { + await tester.pumpWidget(_app(locale: const Locale('es'))); + await tester.pump(); + var loc = _loc(tester); + expect(loc.error('offline'), 'Error: offline'); + expect(loc.tagCount('sci-fi', 3), '#sci-fi (3)'); + expect(loc.playlistItemCount(1), contains('elemento')); + expect(loc.playlistItemCount(2), contains('elementos')); + + await tester.pumpWidget(_app(locale: const Locale('ru'))); + await tester.pump(); + loc = _loc(tester); + expect(loc.playlistItemCount(1), contains('элемент')); + expect(loc.playlistItemCount(2), contains('элемента')); + expect(loc.playlistItemCount(5), contains('элементов')); + + await tester.pumpWidget(_app(locale: const Locale('ar'))); + await tester.pump(); + loc = _loc(tester); + expect(loc.playlistItemCount(0), contains('عنصر')); + expect(loc.playlistItemCount(2), contains('عنصران')); + expect(loc.playlistItemCount(3), contains('عناصر')); + expect(loc.playlistItemCount(11), contains('عنصرًا')); + }); + + testWidgets('Arabic locale supplies right-to-left text direction', ( + tester, + ) async { + await tester.pumpWidget(_app(locale: const Locale('ar'))); + await tester.pump(); + final context = tester.element(find.byType(Text).first); + expect(Directionality.of(context), TextDirection.rtl); + }); } From be6de7540d14e7763ac086fb4fb65dec14f4ae29 Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Sun, 25 Oct 2026 05:38:44 +0100 Subject: [PATCH 5/9] docs: state tested Flutter language support across all READMEs --- README.md | 2 ++ README_ar.md | 4 +++- README_bn.md | 4 +++- README_de.md | 2 ++ README_es.md | 4 +++- README_fr.md | 4 +++- README_hi.md | 4 +++- README_ja.md | 4 +++- README_pt.md | 4 +++- README_ru.md | 4 +++- README_zh.md | 4 +++- 11 files changed, 31 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 174cb0c..302b35b 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ This list describes the README documentation languages only. +The Flutter app provides localized UI messages in German, English, Spanish, Chinese (Simplified), Japanese, Russian, French, Hindi, Arabic, Bengali, and Portuguese. Desktop/CLI localization remains separate; visual layout acceptance on target devices is still pending. + ## Start Here | Need | Go to | diff --git a/README_ar.md b/README_ar.md index e5c6751..4423861 100644 --- a/README_ar.md +++ b/README_ar.md @@ -1,4 +1,4 @@ - + لافتة MediaBrain # MediaBrain @@ -40,6 +40,8 @@ تعرض هذه القائمة إصدارات README الموثقة فقط، ولا تصف اللغات التي يدعمها التطبيق أثناء التشغيل. +يوفّر تطبيق Flutter رسائل واجهة مستخدم مترجمة إلى الألمانية والإنجليزية والإسبانية والصينية المبسطة واليابانية والروسية والفرنسية والهندية والعربية والبنغالية والبرتغالية. يظل توطين تطبيق سطح المكتب وواجهة CLI عملاً منفصلاً؛ وما زال الاعتماد المرئي للتخطيط على الأجهزة المستهدفة معلقًا. + ## ابدأ من هنا | ما تحتاج إليه | انتقل إلى | diff --git a/README_bn.md b/README_bn.md index f20b27a..525375f 100644 --- a/README_bn.md +++ b/README_bn.md @@ -1,4 +1,4 @@ - + MediaBrain ব্যানার # MediaBrain @@ -40,6 +40,8 @@ এই তালিকায় শুধু README নথির ভাষাগত সংস্করণগুলো আছে। এটি অ্যাপের রানটাইমে কোন ভাষা সমর্থিত, তা জানায় না। +Flutter অ্যাপটি জার্মান, ইংরেজি, স্প্যানিশ, সরলীকৃত চীনা, জাপানি, রুশ, ফরাসি, হিন্দি, আরবি, বাংলা এবং পর্তুগিজ ভাষায় স্থানীয়কৃত UI বার্তা সরবরাহ করে। ডেস্কটপ অ্যাপ ও CLI-এর স্থানীয়করণ আলাদা কাজ; লক্ষ্য ডিভাইসে দৃশ্যমান বিন্যাসের গ্রহণযোগ্যতা এখনও বাকি। + ## এখান থেকে শুরু করুন | প্রয়োজন | দেখুন | diff --git a/README_de.md b/README_de.md index 38e110a..8b5f3a1 100644 --- a/README_de.md +++ b/README_de.md @@ -39,6 +39,8 @@ Diese Liste beschreibt ausschließlich die Sprachfassungen der README-Dokumentation. +Die Flutter-App stellt lokalisierte Meldungen der Benutzeroberfläche auf Deutsch, Englisch, Spanisch, Chinesisch (vereinfacht), Japanisch, Russisch, Französisch, Hindi, Arabisch, Bengalisch und Portugiesisch bereit. Die Lokalisierung von Desktop-App und CLI bleibt davon getrennt; die visuelle Abnahme auf Zielgeräten steht noch aus. + ## Schnellstart | Bedarf | Einstieg | diff --git a/README_es.md b/README_es.md index 4dd50f4..5376d5a 100644 --- a/README_es.md +++ b/README_es.md @@ -1,4 +1,4 @@ - + Banner de MediaBrain # MediaBrain @@ -40,6 +40,8 @@ Esta lista se refiere únicamente a las versiones documentales del README. +La aplicación Flutter ofrece mensajes de interfaz localizados en alemán, inglés, español, chino simplificado, japonés, ruso, francés, hindi, árabe, bengalí y portugués. La localización de la aplicación de escritorio y de la CLI sigue siendo independiente; la aceptación visual en los dispositivos de destino aún está pendiente. + ## Primeros pasos | Necesidad | Ir a | diff --git a/README_fr.md b/README_fr.md index de9ee87..6c022b7 100644 --- a/README_fr.md +++ b/README_fr.md @@ -1,4 +1,4 @@ - + Bannière MediaBrain # MediaBrain @@ -40,6 +40,8 @@ Cette liste présente uniquement les versions documentaires du README. Elle ne décrit pas les langues prises en charge par l’application à l’exécution. +L’application Flutter propose des messages d’interface localisés en allemand, anglais, espagnol, chinois simplifié, japonais, russe, français, hindi, arabe, bengali et portugais. La localisation de la version de bureau et de la CLI reste séparée ; la validation visuelle sur les appareils cibles est encore en attente. + ## Pour commencer | Besoin | Voir | diff --git a/README_hi.md b/README_hi.md index f0a0bf8..f0e3f80 100644 --- a/README_hi.md +++ b/README_hi.md @@ -1,4 +1,4 @@ - + MediaBrain बैनर # MediaBrain @@ -40,6 +40,8 @@ यह सूची केवल README के दस्तावेज़ी भाषा संस्करण दिखाती है। यह ऐप के रनटाइम भाषा समर्थन का दावा नहीं करती। +Flutter ऐप जर्मन, अंग्रेज़ी, स्पेनिश, सरलीकृत चीनी, जापानी, रूसी, फ़्रेंच, हिंदी, अरबी, बंगाली और पुर्तगाली में स्थानीयकृत यूज़र-इंटरफ़ेस संदेश प्रदान करता है। डेस्कटॉप ऐप और CLI का स्थानीयकरण अलग कार्य है; लक्षित उपकरणों पर दृश्य लेआउट की स्वीकृति अभी बाकी है। + ## यहाँ से शुरू करें | आवश्यकता | यहाँ जाएँ | diff --git a/README_ja.md b/README_ja.md index de83c7e..ee68bab 100644 --- a/README_ja.md +++ b/README_ja.md @@ -1,4 +1,4 @@ - + MediaBrain バナー # MediaBrain @@ -40,6 +40,8 @@ この一覧は README の文書版のみを示します。アプリの実行時の言語対応を示すものではありません。 +Flutter アプリは、ドイツ語、英語、スペイン語、簡体字中国語、日本語、ロシア語、フランス語、ヒンディー語、アラビア語、ベンガル語、ポルトガル語のUIメッセージを提供します。デスクトップ版とCLIのローカライズは別途対応が必要で、対象デバイスでの視覚的なレイアウト確認は未完了です。 + ## まずはこちら | 内容 | 移動先 | diff --git a/README_pt.md b/README_pt.md index 1d51dd5..64a3eed 100644 --- a/README_pt.md +++ b/README_pt.md @@ -1,4 +1,4 @@ - + Faixa do MediaBrain # MediaBrain @@ -40,6 +40,8 @@ Esta lista apresenta apenas as versões documentais do README. Não descreve os idiomas suportados pela aplicação em execução. +O aplicativo Flutter oferece mensagens de interface localizadas em alemão, inglês, espanhol, chinês simplificado, japonês, russo, francês, hindi, árabe, bengali e português. A localização da versão para desktop e da CLI continua separada; a validação visual nos dispositivos de destino ainda está pendente. + ## Comece aqui | Necessidade | Consulte | diff --git a/README_ru.md b/README_ru.md index 9e700e2..728bd59 100644 --- a/README_ru.md +++ b/README_ru.md @@ -1,4 +1,4 @@ - + Баннер MediaBrain # MediaBrain @@ -40,6 +40,8 @@ В таблице перечислены только языковые версии документации README. Она не описывает поддержку языков в приложении во время выполнения. +Приложение Flutter предоставляет локализованные сообщения интерфейса на немецком, английском, испанском, упрощённом китайском, японском, русском, французском, хинди, арабском, бенгальском и португальском языках. Локализация настольной версии и CLI остаётся отдельной задачей; визуальная проверка на целевых устройствах ещё не завершена. + ## Начните здесь | Что нужно | Перейти к | diff --git a/README_zh.md b/README_zh.md index 6ca230e..46f8883 100644 --- a/README_zh.md +++ b/README_zh.md @@ -1,4 +1,4 @@ - + MediaBrain 横幅 # MediaBrain @@ -40,6 +40,8 @@ 此列表仅说明 README 文档版本,不代表应用运行时支持的语言。 +Flutter 应用提供德语、英语、西班牙语、简体中文、日语、俄语、法语、印地语、阿拉伯语、孟加拉语和葡萄牙语的本地化界面消息。桌面版和 CLI 的本地化仍需单独完成;目标设备上的视觉布局验收尚未完成。 + ## 从这里开始 | 需求 | 前往 | From 35b1536b4442fc0a8032834e645f2002859e4e3d Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Sun, 25 Oct 2026 06:00:07 +0100 Subject: [PATCH 6/9] feat: localize readonly CLI and translation catalogs in eleven languages --- cli.py | 283 ++++++++---- locales/translations.json | 881 +++++++++++++++++++++++++++++++++++-- manage_translations.py | 3 +- tests/test_cli.py | 87 +++- tests/test_translations.py | 14 +- translator.py | 7 +- 6 files changed, 1153 insertions(+), 122 deletions(-) diff --git a/cli.py b/cli.py index c1781ed..8f0a045 100644 --- a/cli.py +++ b/cli.py @@ -8,18 +8,20 @@ wegen config/logger-Abhaengigkeiten). Reiner Lesezugriff. Verwendung: - python cli.py list [--type TYPE] [--limit N] - python cli.py search [--type TYPE] - python cli.py favs [--type TYPE] - python cli.py recent [--limit N] - python cli.py show - python cli.py types - python cli.py --db /pfad/media_brain.db list + python cli.py --lang de list [--type TYPE] [--limit N] + python cli.py --lang fr search [--type TYPE] + python cli.py --lang ar favs [--type TYPE] + python cli.py --lang pt recent [--limit N] + python cli.py --lang hi show + python cli.py --lang bn types + python cli.py --db /pfad/media_brain.db --lang es list -Typen entsprechen den gespeicherten Daten; kanonisch sind movie, series, music,\nclip, podcast, audiobook und document. Legacy-Daten können weitere Werte enthalten. +Typen entsprechen den gespeicherten Daten; kanonisch sind movie, series, music, +clip, podcast, audiobook und document. Legacy-Daten können weitere Werte enthalten. Umgebungsvariablen: - MEDIABRAIN_DB Pfad zur media_brain.db + MEDIABRAIN_DB Pfad zur media_brain.db + MEDIABRAIN_LANG Sprache für die CLI (de, en, es, zh, ja, ru, fr, hi, ar, bn, pt) """ from __future__ import annotations @@ -27,13 +29,18 @@ import os import sqlite3 import sys +from contextlib import contextmanager from pathlib import Path +from typing import Iterator, Sequence + +from translator import SUPPORTED_LANGUAGES, TranslationSystem # --------------------------------------------------------------------------- # DB-Suche # --------------------------------------------------------------------------- _APP_DIR = Path(__file__).resolve().parent +_DEFAULT_TRANSLATOR = TranslationSystem('de', app_dir=_APP_DIR, auto_register_missing=False) def _find_db(override: str | None = None) -> Path: @@ -53,9 +60,14 @@ def _find_db(override: str | None = None) -> Path: return default -def _open(db_path: Path) -> sqlite3.Connection: +def _tr(translator: TranslationSystem | None) -> TranslationSystem: + return translator if translator is not None else _DEFAULT_TRANSLATOR + + +def _open(db_path: Path, translator: TranslationSystem | None = None) -> sqlite3.Connection: + tr = _tr(translator) if not db_path.exists(): - print(f"[FEHLER] Datenbank nicht gefunden: {db_path}", file=sys.stderr) + print(tr.t("[FEHLER] Datenbank nicht gefunden: {db_path}", db_path=db_path), file=sys.stderr) sys.exit(1) uri = db_path.expanduser().resolve().as_uri() + "?mode=ro" conn = sqlite3.connect(uri, uri=True) @@ -67,15 +79,20 @@ def _open(db_path: Path) -> sqlite3.Connection: # Hilfsfunktionen # --------------------------------------------------------------------------- -def _fmt_item(r: sqlite3.Row, verbose: bool = False) -> str: +def _fmt_item( + r: sqlite3.Row, + verbose: bool = False, + translator: TranslationSystem | None = None, +) -> str: + tr = _tr(translator) fav = " ★" if r["is_favorite"] else "" typ = r["type"] or "" src = r["source"] or "" - title = r["title"] or "(kein Titel)" + title = r["title"] or tr.t("(kein Titel)") line = f"[{r['id']}] {title}{fav} [{typ}/{src}]" if verbose: if r["artist"]: - line += f"\n Künstler: {r['artist']}" + line += f"\n {tr.t('Künstler:')} {r['artist']}" if r["description"]: desc = r["description"][:120].replace("\n", " ") line += f"\n {desc}" @@ -96,7 +113,12 @@ def _seconds_to_hms(s: int | None) -> str: # Befehle # --------------------------------------------------------------------------- -def cmd_list(conn: sqlite3.Connection, args: argparse.Namespace) -> int: +def cmd_list( + conn: sqlite3.Connection, + args: argparse.Namespace, + translator: TranslationSystem | None = None, +) -> int: + tr = _tr(translator) query = "SELECT * FROM media_items WHERE blacklist_flag = 0" params: list = [] if args.type: @@ -107,15 +129,20 @@ def cmd_list(conn: sqlite3.Connection, args: argparse.Namespace) -> int: query += f" LIMIT {int(limit)}" rows = conn.execute(query, params).fetchall() if not rows: - print("Keine Einträge gefunden.") + print(tr.t("Keine Einträge gefunden.")) return 0 for r in rows: - print(_fmt_item(r)) - print(f"\n{len(rows)} Einträge (max. {limit})") + print(_fmt_item(r, translator=tr)) + print(f"\n{tr.t('{count} Einträge (max. {limit})', count=len(rows), limit=limit)}") return 0 -def cmd_search(conn: sqlite3.Connection, args: argparse.Namespace) -> int: +def cmd_search( + conn: sqlite3.Connection, + args: argparse.Namespace, + translator: TranslationSystem | None = None, +) -> int: + tr = _tr(translator) term = args.query query = """SELECT * FROM media_items WHERE blacklist_flag = 0 @@ -127,15 +154,20 @@ def cmd_search(conn: sqlite3.Connection, args: argparse.Namespace) -> int: query += " ORDER BY title LIMIT 100" rows = conn.execute(query, params).fetchall() if not rows: - print(f"Keine Treffer für '{term}'.") + print(tr.t("Keine Treffer für '{term}'.", term=term)) return 0 for r in rows: - print(_fmt_item(r)) - print(f"\n{len(rows)} Treffer") + print(_fmt_item(r, translator=tr)) + print(f"\n{tr.t('{count} Treffer', count=len(rows))}") return 0 -def cmd_favs(conn: sqlite3.Connection, args: argparse.Namespace) -> int: +def cmd_favs( + conn: sqlite3.Connection, + args: argparse.Namespace, + translator: TranslationSystem | None = None, +) -> int: + tr = _tr(translator) query = "SELECT * FROM media_items WHERE is_favorite = 1 AND blacklist_flag = 0" params: list = [] if args.type: @@ -144,15 +176,20 @@ def cmd_favs(conn: sqlite3.Connection, args: argparse.Namespace) -> int: query += " ORDER BY title" rows = conn.execute(query, params).fetchall() if not rows: - print("Keine Favoriten.") + print(tr.t("Keine Favoriten.")) return 0 for r in rows: - print(_fmt_item(r)) - print(f"\n{len(rows)} Favoriten") + print(_fmt_item(r, translator=tr)) + print(f"\n{tr.t('{count} Favoriten', count=len(rows))}") return 0 -def cmd_recent(conn: sqlite3.Connection, args: argparse.Namespace) -> int: +def cmd_recent( + conn: sqlite3.Connection, + args: argparse.Namespace, + translator: TranslationSystem | None = None, +) -> int: + tr = _tr(translator) limit = getattr(args, "limit", 20) or 20 rows = conn.execute( """SELECT * FROM media_items @@ -162,20 +199,25 @@ def cmd_recent(conn: sqlite3.Connection, args: argparse.Namespace) -> int: (int(limit),), ).fetchall() if not rows: - print("Noch keine geöffneten Medien.") + print(tr.t("Noch keine geöffneten Medien.")) return 0 for r in rows: opened = (r["last_opened_at"] or "")[:16] dur = _seconds_to_hms(r["length_seconds"]) - line = f" {opened} {_fmt_item(r)}" + line = f" {opened} {_fmt_item(r, translator=tr)}" if dur: line += f" [{dur}]" print(line) - print(f"\n{len(rows)} zuletzt geöffnet") + print(f"\n{tr.t('{count} zuletzt geöffnet', count=len(rows))}") return 0 -def cmd_show(conn: sqlite3.Connection, args: argparse.Namespace) -> int: +def cmd_show( + conn: sqlite3.Connection, + args: argparse.Namespace, + translator: TranslationSystem | None = None, +) -> int: + tr = _tr(translator) term = args.id_or_title if term.isdigit(): rows = conn.execute( @@ -187,37 +229,38 @@ def cmd_show(conn: sqlite3.Connection, args: argparse.Namespace) -> int: (f"%{term}%",), ).fetchall() if not rows: - print(f"Kein Eintrag für '{term}'.") + print(tr.t("Kein Eintrag für '{term}'.", term=term)) return 1 for r in rows: - print(f"ID: {r['id']}") - print(f"Titel: {r['title']}") - print(f"Typ: {r['type']}") - print(f"Quelle: {r['source']}") + print(f"{tr.t('ID:'):<14}{r['id']}") + print(f"{tr.t('Titel:'):<14}{r['title']}") + print(f"{tr.t('Typ:'):<14}{r['type']}") + print(f"{tr.t('Quelle:'):<14}{r['source']}") dur = _seconds_to_hms(r["length_seconds"]) if dur: - print(f"Dauer: {dur}") - print(f"Favorit: {'ja' if r['is_favorite'] else 'nein'}") - print(f"Zuletzt offen: {(r['last_opened_at'] or '—')[:16]}") + print(f"{tr.t('Dauer:'):<14}{dur}") + print(f"{tr.t('Favorit:'):<14}{tr.t('ja') if r['is_favorite'] else tr.t('nein')}") + print(f"{tr.t('Zuletzt offen:')} {(r['last_opened_at'] or '—')[:16]}") if r["artist"]: - print(f"Künstler: {r['artist']}") + print(f"{tr.t('Künstler:'):<14}{r['artist']}") if r["channel"]: - print(f"Kanal: {r['channel']}") + print(f"{tr.t('Kanal:'):<14}{r['channel']}") if r["description"]: - print(f"Beschreibung: {r['description'][:200]}") + print(f"{tr.t('Beschreibung:'):<14}{r['description'][:200]}") print() return 0 -def cmd_types(conn: sqlite3.Connection) -> int: +def cmd_types(conn: sqlite3.Connection, translator: TranslationSystem | None = None) -> int: + tr = _tr(translator) rows = conn.execute( "SELECT type, COUNT(*) as cnt FROM media_items WHERE blacklist_flag = 0 GROUP BY type ORDER BY cnt DESC" ).fetchall() if not rows: - print("Keine Medientypen gefunden.") + print(tr.t("Keine Medientypen gefunden.")) return 0 for r in rows: - print(f" {r['type']:<20} {r['cnt']} Einträge") + print(f" {r['type']:<20} {tr.t('{count} Einträge', count=r['cnt'])}") return 0 @@ -225,59 +268,131 @@ def cmd_types(conn: sqlite3.Connection) -> int: # Argument-Parser # --------------------------------------------------------------------------- -def _build_parser() -> argparse.ArgumentParser: - p = argparse.ArgumentParser( - prog="mediabrain-cli", - description="MediaBrain — rein lesende Kommandozeile", - ) - p.add_argument("--db", metavar="PFAD", help="Pfad zur media_brain.db") - - sub = p.add_subparsers(dest="cmd", required=True) - - ls = sub.add_parser("list", help="Medien auflisten") - ls.add_argument("--type", help="Filter: Medientyp") - ls.add_argument("--limit", type=int, default=50, metavar="N") - - se = sub.add_parser("search", help="Medien suchen") - se.add_argument("query", help="Suchbegriff") - se.add_argument("--type", help="Filter: Medientyp") - - fv = sub.add_parser("favs", help="Favoriten") - fv.add_argument("--type", help="Filter: Medientyp") - - rc = sub.add_parser("recent", help="Zuletzt geoeffnet") - rc.add_argument("--limit", type=int, default=20, metavar="N") - - sh = sub.add_parser("show", help="Details zu einem Medium") - sh.add_argument("id_or_title", help="ID oder Titel (Teilstring)") - - sub.add_parser("types", help="Alle Medientypen mit Anzahl") - - return p +@contextmanager +def _argparse_locale(translator: TranslationSystem) -> Iterator[None]: + original = argparse._ + argparse._ = translator.t + try: + yield + finally: + argparse._ = original + + +def _build_parser( + translator: TranslationSystem | None = None, + default_lang: str | None = None, +) -> argparse.ArgumentParser: + tr = _tr(translator) + lang = default_lang or tr.get_language() + with _argparse_locale(tr): + p = argparse.ArgumentParser( + prog="mediabrain-cli", + description=tr.t("MediaBrain — rein lesende Kommandozeile"), + ) + p.add_argument("--db", metavar=tr.t("PFAD"), help=tr.t("Pfad zur media_brain.db")) + p.add_argument( + "--lang", + choices=SUPPORTED_LANGUAGES, + default=lang, + metavar="LANG", + help=tr.t("Sprache für die CLI"), + ) + + sub = p.add_subparsers(dest="cmd", required=True) + + def add_subcommand_language(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "--lang", + choices=SUPPORTED_LANGUAGES, + default=argparse.SUPPRESS, + metavar="LANG", + help=tr.t("Sprache für die CLI"), + ) + + ls = sub.add_parser("list", help=tr.t("Medien auflisten")) + add_subcommand_language(ls) + ls.add_argument("--type", help=tr.t("Filter: Medientyp")) + ls.add_argument("--limit", type=int, default=50, metavar="N") + + se = sub.add_parser("search", help=tr.t("Medien suchen")) + add_subcommand_language(se) + se.add_argument("query", help=tr.t("Suchbegriff")) + se.add_argument("--type", help=tr.t("Filter: Medientyp")) + + fv = sub.add_parser("favs", help=tr.t("Favoriten")) + add_subcommand_language(fv) + fv.add_argument("--type", help=tr.t("Filter: Medientyp")) + + rc = sub.add_parser("recent", help=tr.t("Zuletzt geoeffnet")) + add_subcommand_language(rc) + rc.add_argument("--limit", type=int, default=20, metavar="N") + + sh = sub.add_parser("show", help=tr.t("Details zu einem Medium")) + add_subcommand_language(sh) + sh.add_argument("id_or_title", help=tr.t("ID oder Titel (Teilstring)")) + + ty = sub.add_parser("types", help=tr.t("Alle Medientypen mit Anzahl")) + add_subcommand_language(ty) + + return p + + +def _normalize_language(raw: str | None) -> str: + if not raw: + return "de" + normalized = raw.strip().replace("_", "-").split("-", maxsplit=1)[0].lower() + return normalized if normalized in SUPPORTED_LANGUAGES else "de" + + +def _requested_language(argv: Sequence[str]) -> str: + explicit: str | None = None + for index, arg in enumerate(argv): + if arg == "--lang" and index + 1 < len(argv): + explicit = argv[index + 1] + elif arg.startswith("--lang="): + explicit = arg.split("=", maxsplit=1)[1] + if explicit is None: + explicit = os.environ.get("MEDIABRAIN_LANG") + return _normalize_language(explicit) + + +def _configure_console_utf8() -> None: + for stream in (sys.stdout, sys.stderr): + reconfigure = getattr(stream, "reconfigure", None) + if callable(reconfigure): + try: + reconfigure(encoding="utf-8") + except (OSError, ValueError): + pass # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- -def main() -> int: - parser = _build_parser() - args = parser.parse_args() +def main(argv: Sequence[str] | None = None) -> int: + _configure_console_utf8() + args_in = list(sys.argv[1:] if argv is None else argv) + lang = _requested_language(args_in) + tr = TranslationSystem(lang, app_dir=_APP_DIR, auto_register_missing=False) + with _argparse_locale(tr): + parser = _build_parser(tr, default_lang=lang) + args = parser.parse_args(args_in) db_path = _find_db(args.db) - conn = _open(db_path) + conn = _open(db_path, tr) try: if args.cmd == "list": - return cmd_list(conn, args) + return cmd_list(conn, args, tr) elif args.cmd == "search": - return cmd_search(conn, args) + return cmd_search(conn, args, tr) elif args.cmd == "favs": - return cmd_favs(conn, args) + return cmd_favs(conn, args, tr) elif args.cmd == "recent": - return cmd_recent(conn, args) + return cmd_recent(conn, args, tr) elif args.cmd == "show": - return cmd_show(conn, args) + return cmd_show(conn, args, tr) elif args.cmd == "types": - return cmd_types(conn) + return cmd_types(conn, tr) finally: conn.close() return 0 diff --git a/locales/translations.json b/locales/translations.json index 33d7bc0..a623377 100644 --- a/locales/translations.json +++ b/locales/translations.json @@ -5,7 +5,12 @@ "es": "Mostrar lista negra", "zh": "显示黑名单", "ja": "ブラックリストを表示", - "ru": "Показать черный список" + "ru": "Показать черный список", + "fr": "Afficher la liste noire", + "hi": "ब्लैकलिस्ट दिखाएँ", + "ar": "عرض القائمة السوداء", + "bn": "ব্ল্যাকলিস্ট দেখান", + "pt": "Mostrar lista de bloqueio" }, "Dauer ändern": { "de": "Dauer ändern", @@ -13,7 +18,12 @@ "es": "Cambiar duración", "zh": "更改时长", "ja": "時間を変更", - "ru": "Изменить длительность" + "ru": "Изменить длительность", + "fr": "Modifier la durée", + "hi": "अवधि बदलें", + "ar": "تغيير المدة", + "bn": "সময়কাল পরিবর্তন করুন", + "pt": "Alterar duração" }, "Einstellungen": { "de": "Einstellungen", @@ -21,7 +31,12 @@ "es": "Configuración", "zh": "设置", "ja": "設定", - "ru": "Настройки" + "ru": "Настройки", + "fr": "Paramètres", + "hi": "सेटिंग्स", + "ar": "الإعدادات", + "bn": "সেটিংস", + "pt": "Configurações" }, "Keine Details verfügbar": { "de": "Keine Details verfügbar", @@ -29,7 +44,12 @@ "es": "No hay detalles disponibles", "zh": "无可用详情", "ja": "詳細はありません", - "ru": "Нет доступных деталей" + "ru": "Нет доступных деталей", + "fr": "Aucun détail disponible", + "hi": "कोई विवरण उपलब्ध नहीं", + "ar": "لا توجد تفاصيل متاحة", + "bn": "কোনো বিবরণ পাওয়া যায় না", + "pt": "Nenhum detalhe disponível" }, "Lokale Daten:": { "de": "Lokale Daten:", @@ -37,7 +57,12 @@ "es": "Datos locales:", "zh": "本地数据:", "ja": "ローカルデータ:", - "ru": "Локальные данные:" + "ru": "Локальные данные:", + "fr": "Données locales :", + "hi": "स्थानीय डेटा:", + "ar": "البيانات المحلية:", + "bn": "স্থানীয় ডেটা:", + "pt": "Dados locais:" }, "Suche speichern": { "de": "Suche speichern", @@ -45,7 +70,12 @@ "es": "Guardar búsqueda", "zh": "保存搜索", "ja": "検索を保存", - "ru": "Сохранить поиск" + "ru": "Сохранить поиск", + "fr": "Enregistrer la recherche", + "hi": "खोज सहेजें", + "ar": "حفظ البحث", + "bn": "অনুসন্ধান সংরক্ষণ করুন", + "pt": "Salvar pesquisa" }, "Zurück": { "de": "Zurück", @@ -53,7 +83,12 @@ "es": "Volver", "zh": "返回", "ja": "戻る", - "ru": "Назад" + "ru": "Назад", + "fr": "Retour", + "hi": "वापस", + "ar": "رجوع", + "bn": "ফিরে যান", + "pt": "Voltar" }, "Öffnen": { "de": "Öffnen", @@ -61,7 +96,12 @@ "es": "Abrir", "zh": "打开", "ja": "開く", - "ru": "Открыть" + "ru": "Открыть", + "fr": "Ouvrir", + "hi": "खोलें", + "ar": "فتح", + "bn": "খুলুন", + "pt": "Abrir" }, "Übersicht": { "de": "Übersicht", @@ -69,7 +109,12 @@ "es": "Resumen", "zh": "概览", "ja": "概要", - "ru": "Обзор" + "ru": "Обзор", + "fr": "Vue d’ensemble", + "hi": "अवलोकन", + "ar": "نظرة عامة", + "bn": "সারসংক্ষেপ", + "pt": "Visão geral" }, "🌐 Metadaten aktualisieren": { "de": "🌐 Metadaten aktualisieren", @@ -77,7 +122,12 @@ "es": "🌐 Actualizar metadatos", "zh": "🌐 更新元数据", "ja": "🌐 メタデータを更新", - "ru": "🌐 Obновить метаданные" + "ru": "🌐 Obновить метаданные", + "fr": "🌐 Actualiser les métadonnées", + "hi": "🌐 मेटाडेटा अपडेट करें", + "ar": "🌐 تحديث البيانات الوصفية", + "bn": "🌐 মেটাডেটা হালনাগাদ করুন", + "pt": "🌐 Atualizar metadados" }, "+ Bedingung hinzufügen": { "de": "+ Bedingung hinzufügen", @@ -85,7 +135,12 @@ "es": "+ Añadir condición", "zh": "+ 添加条件", "ja": "+ 条件を追加", - "ru": "+ Добавить условие" + "ru": "+ Добавить условие", + "fr": "+ Ajouter une condition", + "hi": "+ शर्त जोड़ें", + "ar": "+ إضافة شرط", + "bn": "+ শর্ত যোগ করুন", + "pt": "+ Adicionar condição" }, "Zuletzt hinzugefügt (Top 5):": { "de": "Zuletzt hinzugefügt (Top 5):", @@ -93,7 +148,12 @@ "es": "Añadido recientemente (Top 5):", "zh": "最近添加 (前 5 个):", "ja": "最近追加された項目 (トップ 5):", - "ru": "Недавно добавленные (лучшие 5):" + "ru": "Недавно добавленные (лучшие 5):", + "fr": "Les 5 ajouts les plus récents :", + "hi": "हाल के 5 जोड़े गए आइटम:", + "ar": "أحدث 5 إضافات:", + "bn": "সর্বশেষ যোগ করা ৫টি:", + "pt": "5 adições mais recentes:" }, "Bearbeiten": { "de": "Bearbeiten", @@ -101,7 +161,12 @@ "es": "Editar", "zh": "编辑", "ja": "編集", - "ru": "Редактировать" + "ru": "Редактировать", + "fr": "Modifier", + "hi": "संपादित करें", + "ar": "تحرير", + "bn": "সম্পাদনা", + "pt": "Editar" }, "Export (JSON)": { "de": "Export (JSON)", @@ -109,7 +174,12 @@ "es": "Exportar (JSON)", "zh": "导出 (JSON)", "ja": "エクスポート (JSON)", - "ru": "Экспорт (JSON)" + "ru": "Экспорт (JSON)", + "fr": "Exporter (JSON)", + "hi": "निर्यात (JSON)", + "ar": "تصدير (JSON)", + "bn": "এক্সপোর্ট (JSON)", + "pt": "Exportar (JSON)" }, "Filter \\u25BC": { "de": "Filter \\u25BC", @@ -117,7 +187,12 @@ "es": "Filtrar \\u25BC", "zh": "筛选 \\u25BC", "ja": "フィルター \\u25BC", - "ru": "Фильтр \\u25BC" + "ru": "Фильтр \\u25BC", + "fr": "Filtrer \\u25BC", + "hi": "फ़िल्टर \\u25BC", + "ar": "تصفية \\u25BC", + "bn": "ফিল্টার \\u25BC", + "pt": "Filtrar \\u25BC" }, "Geschätzter Speicher (lokal): Keine lokalen Dateien erfasst": { "de": "Geschätzter Speicher (lokal): Keine lokalen Dateien erfasst", @@ -125,7 +200,12 @@ "es": "Espacio estimado (local): No se detectaron archivos locales", "zh": "估计存储空间 (本地): 未检测到本地文件", "ja": "推定容量 (ローカル): ローカルファイルが検出されませんでした", - "ru": "Оценочный объем (локальный): локальные файлы не обнаружены" + "ru": "Оценочный объем (локальный): локальные файлы не обнаружены", + "fr": "Stockage estimé (local) : Aucun fichier local enregistré", + "hi": "अनुमानित संग्रहण (स्थानीय): कोई स्थानीय फ़ाइल दर्ज नहीं", + "ar": "التخزين المقدّر (محلي): لم تُسجّل ملفات محلية", + "bn": "আনুমানিক স্টোরেজ (স্থানীয়): কোনো স্থানীয় ফাইল নথিভুক্ত নেই", + "pt": "Armazenamento estimado (local): Nenhum arquivo local registrado" }, "Geschätzter Speicher (lokal): –": { "de": "Geschätzter Speicher (lokal): –", @@ -133,7 +213,12 @@ "es": "Espacio estimado (local): –", "zh": "估计存储空间 (本地): –", "ja": "推定容量 (ローカル): –", - "ru": "Оценочный объем (локальный): –" + "ru": "Оценочный объем (локальный): –", + "fr": "Stockage estimé (local) : –", + "hi": "अनुमानित संग्रहण (स्थानीय): –", + "ar": "التخزين المقدّر (محلي): –", + "bn": "আনুমানিক স্টোরেজ (স্থানীয়): –", + "pt": "Armazenamento estimado (local): –" }, "Import (JSON)": { "de": "Import (JSON)", @@ -141,7 +226,12 @@ "es": "Importar (JSON)", "zh": "导入 (JSON)", "ja": "インポート (JSON)", - "ru": "Импорт (JSON)" + "ru": "Импорт (JSON)", + "fr": "Importer (JSON)", + "hi": "आयात (JSON)", + "ar": "استيراد (JSON)", + "bn": "ইম্পোর্ট (JSON)", + "pt": "Importar (JSON)" }, "Keine Playlist ausgewählt.": { "de": "Keine Playlist ausgewählt.", @@ -149,7 +239,12 @@ "es": "No se ha seleccionado ninguna lista de reproducción.", "zh": "未选择播放列表。", "ja": "プレイリストが選択されていません。", - "ru": "Плейлист не выбран." + "ru": "Плейлист не выбран.", + "fr": "Aucune playlist sélectionnée.", + "hi": "कोई प्लेलिस्ट नहीं चुनी गई है।", + "ar": "لم يتم اختيار قائمة تشغيل.", + "bn": "কোনো প্লেলিস্ট নির্বাচন করা হয়নি।", + "pt": "Nenhuma lista de reprodução selecionada." }, "Löschen": { "de": "Löschen", @@ -157,7 +252,12 @@ "es": "Eliminar", "zh": "删除", "ja": "削除", - "ru": "Удалить" + "ru": "Удалить", + "fr": "Supprimer", + "hi": "हटाएँ", + "ar": "حذف", + "bn": "মুছে ফেলুন", + "pt": "Excluir" }, "Metadaten neu laden": { "de": "Metadaten neu laden", @@ -165,7 +265,12 @@ "es": "Recargar metadatos", "zh": "重新加载元数据", "ja": "メタデータを再読み込み", - "ru": "Перезагрузить метаданные" + "ru": "Перезагрузить метаданные", + "fr": "Recharger les métadonnées", + "hi": "मेटाडेटा फिर से लोड करें", + "ar": "إعادة تحميل البيانات الوصفية", + "bn": "মেটাডেটা পুনরায় লোড করুন", + "pt": "Recarregar metadados" }, "Noch keine Aktivitäten.": { "de": "Noch keine Aktivitäten.", @@ -173,7 +278,12 @@ "es": "Aún no hay actividad.", "zh": "尚无活动。", "ja": "アクティビティはまだありません。", - "ru": "Активностей пока нет." + "ru": "Активностей пока нет.", + "fr": "Aucune activité pour le moment.", + "hi": "अभी तक कोई गतिविधि नहीं।", + "ar": "لا توجد أنشطة بعد.", + "bn": "এখনও কোনো কার্যকলাপ নেই।", + "pt": "Ainda não há atividades." }, "Online-Metadaten gefunden, aber keine Felder zum Anzeigen.": { "de": "Online-Metadaten gefunden, aber keine Felder zum Anzeigen.", @@ -181,7 +291,12 @@ "es": "Se encontraron metadatos en línea, pero no hay campos para mostrar.", "zh": "找到在线元数据,但没有可显示的字段。", "ja": "オンラインメタデータが見つかりましたが、表示する項目がありません。", - "ru": "Найдены онлайн-метаданные, но нет полей для отображения." + "ru": "Найдены онлайн-метаданные, но нет полей для отображения.", + "fr": "Métadonnées en ligne trouvées, mais aucun champ à afficher.", + "hi": "ऑनलाइन मेटाडेटा मिला, लेकिन दिखाने के लिए कोई फ़ील्ड नहीं है।", + "ar": "عُثر على بيانات وصفية عبر الإنترنت، لكن لا توجد حقول لعرضها.", + "bn": "অনলাইন মেটাডেটা পাওয়া গেছে, কিন্তু দেখানোর মতো কোনো ক্ষেত্র নেই।", + "pt": "Metadados online encontrados, mas não há campos para exibir." }, "Tag-Filter:": { "de": "Tag-Filter:", @@ -189,7 +304,12 @@ "es": "Filtro de etiquetas:", "zh": "标签筛选:", "ja": "タグフィルター:", - "ru": "Фильтр тегов:" + "ru": "Фильтр тегов:", + "fr": "Filtre de tags :", + "hi": "टैग फ़िल्टर:", + "ar": "تصفية الوسوم:", + "bn": "ট্যাগ ফিল্টার:", + "pt": "Filtro de tags:" }, "Zuletzt geöffnet": { "de": "Zuletzt geöffnet", @@ -197,7 +317,12 @@ "es": "Abierto recientemente", "zh": "最近打开", "ja": "最近開いた項目", - "ru": "Недавно открытые" + "ru": "Недавно открытые", + "fr": "Ouverts récemment", + "hi": "हाल ही में खोले गए", + "ar": "العناصر المفتوحة مؤخرًا", + "bn": "সম্প্রতি খোলা", + "pt": "Abertos recentemente" }, "Zurücksetzen": { "de": "Zurücksetzen", @@ -205,7 +330,12 @@ "es": "Restablecer", "zh": "重置", "ja": "リセット", - "ru": "Сбросить" + "ru": "Сбросить", + "fr": "Réinitialiser", + "hi": "रीसेट करें", + "ar": "إعادة ضبط", + "bn": "পুনরায় সেট করুন", + "pt": "Redefinir" }, "\\u25B6 öffnen": { "de": "\\u25B6 öffnen", @@ -213,7 +343,12 @@ "es": "\\u25B6 abrir", "zh": "\\u25B6 打开", "ja": "\\u25B6 開く", - "ru": "\\u25B6 открыть" + "ru": "\\u25B6 открыть", + "fr": "\\u25B6 ouvrir", + "hi": "\\u25B6 खोलें", + "ar": "\\u25B6 فتح", + "bn": "\\u25B6 খুলুন", + "pt": "\\u25B6 abrir" }, "öffnen": { "de": "öffnen", @@ -221,7 +356,12 @@ "es": "abrir", "zh": "打开", "ja": "開く", - "ru": "открыть" + "ru": "открыть", + "fr": "ouvrir", + "hi": "खोलें", + "ar": "فتح", + "bn": "খুলুন", + "pt": "abrir" }, "🔄 Aktualisieren": { "de": "🔄 Aktualisieren", @@ -229,6 +369,687 @@ "es": "🔄 Actualizar", "zh": "🔄 更新", "ja": "🔄 更新", - "ru": "🔄 Обновить" + "ru": "🔄 Обновить", + "fr": "🔄 Actualiser", + "hi": "🔄 अपडेट करें", + "ar": "🔄 تحديث", + "bn": "🔄 হালনাগাদ করুন", + "pt": "🔄 Atualizar" + }, + "[FEHLER] Datenbank nicht gefunden: {db_path}": { + "de": "[FEHLER] Datenbank nicht gefunden: {db_path}", + "en": "[ERROR] Database not found: {db_path}", + "es": "[ERROR] No se encontró la base de datos: {db_path}", + "zh": "[错误] 找不到数据库:{db_path}", + "ja": "[エラー] データベースが見つかりません: {db_path}", + "ru": "[ОШИБКА] База данных не найдена: {db_path}", + "fr": "[ERREUR] Base de données introuvable : {db_path}", + "hi": "[त्रुटि] डेटाबेस नहीं मिला: {db_path}", + "ar": "[خطأ] لم يُعثر على قاعدة البيانات: {db_path}", + "bn": "[ত্রুটি] ডেটাবেস পাওয়া যায়নি: {db_path}", + "pt": "[ERRO] Banco de dados não encontrado: {db_path}" + }, + "(kein Titel)": { + "de": "(kein Titel)", + "en": "(untitled)", + "es": "(sin título)", + "zh": "(无标题)", + "ja": "(無題)", + "ru": "(без названия)", + "fr": "(sans titre)", + "hi": "(बिना शीर्षक)", + "ar": "(بلا عنوان)", + "bn": "(শিরোনামহীন)", + "pt": "(sem título)" + }, + "Künstler:": { + "de": "Künstler:", + "en": "Artist:", + "es": "Artista:", + "zh": "艺术家:", + "ja": "アーティスト:", + "ru": "Исполнитель:", + "fr": "Artiste :", + "hi": "कलाकार:", + "ar": "الفنان:", + "bn": "শিল্পী:", + "pt": "Artista:" + }, + "Keine Einträge gefunden.": { + "de": "Keine Einträge gefunden.", + "en": "No entries found.", + "es": "No se encontraron entradas.", + "zh": "未找到条目。", + "ja": "項目が見つかりません。", + "ru": "Записи не найдены.", + "fr": "Aucune entrée trouvée.", + "hi": "कोई प्रविष्टि नहीं मिली।", + "ar": "لم يتم العثور على عناصر.", + "bn": "কোনো এন্ট্রি পাওয়া যায়নি।", + "pt": "Nenhuma entrada encontrada." + }, + "{count} Einträge (max. {limit})": { + "de": "Einträge: {count} (max. {limit})", + "en": "Entries: {count} (max. {limit})", + "es": "Entradas: {count} (máx. {limit})", + "zh": "条目数:{count}(最多 {limit})", + "ja": "項目数:{count}(上限 {limit})", + "ru": "Записей: {count} (макс. {limit})", + "fr": "Entrées : {count} (max. {limit})", + "hi": "प्रविष्टियाँ: {count} (अधिकतम {limit})", + "ar": "عدد العناصر: {count} (الحد الأقصى {limit})", + "bn": "এন্ট্রি: {count} (সর্বোচ্চ {limit})", + "pt": "Entradas: {count} (máx. {limit})" + }, + "Keine Treffer für '{term}'.": { + "de": "Keine Treffer für '{term}'.", + "en": "No matches for '{term}'.", + "es": "No hay coincidencias para '{term}'.", + "zh": "没有匹配项:'{term}'。", + "ja": "'{term}' に一致する項目はありません。", + "ru": "Совпадений для «{term}» не найдено.", + "fr": "Aucun résultat pour « {term} ».", + "hi": "'{term}' के लिए कोई मिलान नहीं मिला।", + "ar": "لا توجد نتائج مطابقة لـ '{term}'.", + "bn": "'{term}'-এর সঙ্গে কোনো মিল নেই।", + "pt": "Nenhum resultado para '{term}'." + }, + "{count} Treffer": { + "de": "Treffer: {count}", + "en": "Matches: {count}", + "es": "Coincidencias: {count}", + "zh": "匹配项:{count}", + "ja": "一致件数:{count}", + "ru": "Совпадений: {count}", + "fr": "Résultats : {count}", + "hi": "मिलान: {count}", + "ar": "النتائج المطابقة: {count}", + "bn": "মিল: {count}", + "pt": "Correspondências: {count}" + }, + "Keine Favoriten.": { + "de": "Keine Favoriten.", + "en": "No favorites.", + "es": "No hay favoritos.", + "zh": "没有收藏项。", + "ja": "お気に入りはありません。", + "ru": "Избранного нет.", + "fr": "Aucun favori.", + "hi": "कोई पसंदीदा नहीं है।", + "ar": "لا توجد عناصر مفضلة.", + "bn": "কোনো পছন্দের আইটেম নেই।", + "pt": "Não há favoritos." + }, + "Noch keine geöffneten Medien.": { + "de": "Noch keine geöffneten Medien.", + "en": "No media opened yet.", + "es": "Aún no se ha abierto ningún medio.", + "zh": "尚未打开任何媒体。", + "ja": "まだメディアを開いていません。", + "ru": "Медиа ещё не открывались.", + "fr": "Aucun média ouvert pour le moment.", + "hi": "अभी तक कोई मीडिया नहीं खोला गया है।", + "ar": "لم يتم فتح أي وسائط بعد.", + "bn": "এখনও কোনো মিডিয়া খোলা হয়নি।", + "pt": "Nenhuma mídia foi aberta ainda." + }, + "{count} zuletzt geöffnet": { + "de": "Zuletzt geöffnet: {count}", + "en": "Recently opened: {count}", + "es": "Abiertos recientemente: {count}", + "zh": "最近打开:{count}", + "ja": "最近開いた項目:{count}", + "ru": "Недавно открыто: {count}", + "fr": "Ouverts récemment : {count}", + "hi": "हाल ही में खोले गए: {count}", + "ar": "مفتوح مؤخرًا: {count}", + "bn": "সম্প্রতি খোলা: {count}", + "pt": "Abertos recentemente: {count}" + }, + "Kein Eintrag für '{term}'.": { + "de": "Kein Eintrag für '{term}'.", + "en": "No item for '{term}'.", + "es": "No hay ninguna entrada para '{term}'.", + "zh": "没有找到条目:'{term}'。", + "ja": "'{term}' の項目は見つかりません。", + "ru": "Запись «{term}» не найдена.", + "fr": "Aucune entrée pour « {term} ».", + "hi": "'{term}' के लिए कोई प्रविष्टि नहीं मिली।", + "ar": "لا يوجد عنصر باسم '{term}'.", + "bn": "'{term}'-এর জন্য কোনো এন্ট্রি নেই。", + "pt": "Nenhuma entrada para '{term}'." + }, + "ID:": { + "de": "ID:", + "en": "ID:", + "es": "ID:", + "zh": "ID:", + "ja": "ID:", + "ru": "ID:", + "fr": "ID :", + "hi": "ID:", + "ar": "المعرّف:", + "bn": "আইডি:", + "pt": "ID:" + }, + "Titel:": { + "de": "Titel:", + "en": "Title:", + "es": "Título:", + "zh": "标题:", + "ja": "タイトル:", + "ru": "Название:", + "fr": "Titre :", + "hi": "शीर्षक:", + "ar": "العنوان:", + "bn": "শিরোনাম:", + "pt": "Título:" + }, + "Typ:": { + "de": "Typ:", + "en": "Type:", + "es": "Tipo:", + "zh": "类型:", + "ja": "種類:", + "ru": "Тип:", + "fr": "Type :", + "hi": "प्रकार:", + "ar": "النوع:", + "bn": "ধরন:", + "pt": "Tipo:" + }, + "Quelle:": { + "de": "Quelle:", + "en": "Source:", + "es": "Fuente:", + "zh": "来源:", + "ja": "ソース:", + "ru": "Источник:", + "fr": "Source :", + "hi": "स्रोत:", + "ar": "المصدر:", + "bn": "উৎস:", + "pt": "Fonte:" + }, + "Dauer:": { + "de": "Dauer:", + "en": "Duration:", + "es": "Duración:", + "zh": "时长:", + "ja": "長さ:", + "ru": "Длительность:", + "fr": "Durée :", + "hi": "अवधि:", + "ar": "المدة:", + "bn": "সময়কাল:", + "pt": "Duração:" + }, + "Favorit:": { + "de": "Favorit:", + "en": "Favorite:", + "es": "Favorito:", + "zh": "收藏:", + "ja": "お気に入り:", + "ru": "Избранное:", + "fr": "Favori :", + "hi": "पसंदीदा:", + "ar": "المفضلة:", + "bn": "পছন্দের:", + "pt": "Favorito:" + }, + "ja": { + "de": "ja", + "en": "yes", + "es": "sí", + "zh": "是", + "ja": "はい", + "ru": "да", + "fr": "oui", + "hi": "हाँ", + "ar": "نعم", + "bn": "হ্যাঁ", + "pt": "sim" + }, + "nein": { + "de": "nein", + "en": "no", + "es": "no", + "zh": "否", + "ja": "いいえ", + "ru": "нет", + "fr": "non", + "hi": "नहीं", + "ar": "لا", + "bn": "না", + "pt": "não" + }, + "Zuletzt offen:": { + "de": "Zuletzt offen:", + "en": "Last opened:", + "es": "Última apertura:", + "zh": "上次打开:", + "ja": "最終閲覧:", + "ru": "Последнее открытие:", + "fr": "Dernière ouverture :", + "hi": "अंतिम बार खोला गया:", + "ar": "آخر فتح:", + "bn": "সর্বশেষ খোলা:", + "pt": "Última abertura:" + }, + "Kanal:": { + "de": "Kanal:", + "en": "Channel:", + "es": "Canal:", + "zh": "频道:", + "ja": "チャンネル:", + "ru": "Канал:", + "fr": "Chaîne :", + "hi": "चैनल:", + "ar": "القناة:", + "bn": "চ্যানেল:", + "pt": "Canal:" + }, + "Beschreibung:": { + "de": "Beschreibung:", + "en": "Description:", + "es": "Descripción:", + "zh": "描述:", + "ja": "説明:", + "ru": "Описание:", + "fr": "Description :", + "hi": "विवरण:", + "ar": "الوصف:", + "bn": "বিবরণ:", + "pt": "Descrição:" + }, + "Keine Medientypen gefunden.": { + "de": "Keine Medientypen gefunden.", + "en": "No media types found.", + "es": "No se encontraron tipos de medios.", + "zh": "未找到媒体类型。", + "ja": "メディアの種類が見つかりません。", + "ru": "Типы медиа не найдены.", + "fr": "Aucun type de média trouvé.", + "hi": "कोई मीडिया प्रकार नहीं मिला।", + "ar": "لم يتم العثور على أنواع وسائط.", + "bn": "কোনো মিডিয়ার ধরন পাওয়া যায়নি।", + "pt": "Nenhum tipo de mídia encontrado." + }, + "{count} Einträge": { + "de": "Einträge: {count}", + "en": "Entries: {count}", + "es": "Entradas: {count}", + "zh": "条目:{count}", + "ja": "項目:{count}", + "ru": "Записей: {count}", + "fr": "Entrées : {count}", + "hi": "प्रविष्टियाँ: {count}", + "ar": "العناصر: {count}", + "bn": "এন্ট্রি: {count}", + "pt": "Entradas: {count}" + }, + "MediaBrain — rein lesende Kommandozeile": { + "de": "MediaBrain — schreibgeschützte Kommandozeile", + "en": "MediaBrain — read-only command line", + "es": "MediaBrain — línea de comandos de solo lectura", + "zh": "MediaBrain — 只读命令行", + "ja": "MediaBrain — 読み取り専用コマンドライン", + "ru": "MediaBrain — командная строка только для чтения", + "fr": "MediaBrain — ligne de commande en lecture seule", + "hi": "MediaBrain — केवल-पठन कमांड लाइन", + "ar": "MediaBrain — سطر أوامر للقراءة فقط", + "bn": "MediaBrain — শুধুমাত্র-পঠন কমান্ড লাইন", + "pt": "MediaBrain — linha de comando somente leitura" + }, + "Pfad zur media_brain.db": { + "de": "Pfad zur media_brain.db", + "en": "Path to media_brain.db", + "es": "Ruta a media_brain.db", + "zh": "media_brain.db 的路径", + "ja": "media_brain.db へのパス", + "ru": "Путь к media_brain.db", + "fr": "Chemin vers media_brain.db", + "hi": "media_brain.db का पथ", + "ar": "مسار media_brain.db", + "bn": "media_brain.db-এর পথ", + "pt": "Caminho para media_brain.db" + }, + "Medien auflisten": { + "de": "Medien auflisten", + "en": "List media", + "es": "Listar medios", + "zh": "列出媒体", + "ja": "メディアを一覧表示", + "ru": "Список медиа", + "fr": "Lister les médias", + "hi": "मीडिया सूची दिखाएँ", + "ar": "عرض الوسائط", + "bn": "মিডিয়া তালিকাভুক্ত করুন", + "pt": "Listar mídias" + }, + "Filter: Medientyp": { + "de": "Filter: Medientyp", + "en": "Filter: media type", + "es": "Filtrar por tipo de medio", + "zh": "筛选:媒体类型", + "ja": "フィルター: メディアの種類", + "ru": "Фильтр: тип медиа", + "fr": "Filtrer par type de média", + "hi": "फ़िल्टर: मीडिया प्रकार", + "ar": "تصفية حسب نوع الوسائط", + "bn": "ফিল্টার: মিডিয়ার ধরন", + "pt": "Filtrar por tipo de mídia" + }, + "Medien suchen": { + "de": "Medien suchen", + "en": "Search media", + "es": "Buscar medios", + "zh": "搜索媒体", + "ja": "メディアを検索", + "ru": "Поиск медиа", + "fr": "Rechercher des médias", + "hi": "मीडिया खोजें", + "ar": "البحث عن وسائط", + "bn": "মিডিয়া খুঁজুন", + "pt": "Buscar mídias" + }, + "Suchbegriff": { + "de": "Suchbegriff", + "en": "Search term", + "es": "Término de búsqueda", + "zh": "搜索词", + "ja": "検索語句", + "ru": "Поисковый запрос", + "fr": "Terme de recherche", + "hi": "खोज शब्द", + "ar": "عبارة البحث", + "bn": "অনুসন্ধানের শব্দ", + "pt": "Termo de busca" + }, + "Favoriten": { + "de": "Favoriten", + "en": "Favorites", + "es": "Favoritos", + "zh": "收藏项", + "ja": "お気に入り", + "ru": "Избранное", + "fr": "Favoris", + "hi": "पसंदीदा", + "ar": "المفضلة", + "bn": "পছন্দের তালিকা", + "pt": "Favoritos" + }, + "Zuletzt geoeffnet": { + "de": "Zuletzt geöffnet", + "en": "Recently opened", + "es": "Abiertos recientemente", + "zh": "最近打开", + "ja": "最近開いた項目", + "ru": "Недавно открытые", + "fr": "Ouverts récemment", + "hi": "हाल ही में खोले गए", + "ar": "مفتوح مؤخرًا", + "bn": "সম্প্রতি খোলা", + "pt": "Abertos recentemente" + }, + "Details zu einem Medium": { + "de": "Details zu einem Medium", + "en": "Details for a media item", + "es": "Detalles de un medio", + "zh": "媒体详情", + "ja": "メディアの詳細", + "ru": "Сведения о медиа", + "fr": "Détails d’un média", + "hi": "मीडिया का विवरण", + "ar": "تفاصيل الوسائط", + "bn": "মিডিয়ার বিবরণ", + "pt": "Detalhes de uma mídia" + }, + "ID oder Titel (Teilstring)": { + "de": "ID oder Titel (Teilstring)", + "en": "ID or title (substring)", + "es": "ID o título (coincidencia parcial)", + "zh": "ID 或标题(部分匹配)", + "ja": "ID またはタイトル(一部一致)", + "ru": "ID или название (частичное совпадение)", + "fr": "ID ou titre (correspondance partielle)", + "hi": "ID या शीर्षक (आंशिक मिलान)", + "ar": "المعرّف أو العنوان (مطابقة جزئية)", + "bn": "আইডি বা শিরোনাম (আংশিক মিল)", + "pt": "ID ou título (correspondência parcial)" + }, + "Alle Medientypen mit Anzahl": { + "de": "Alle Medientypen mit Anzahl", + "en": "All media types with counts", + "es": "Todos los tipos de medios con recuento", + "zh": "所有媒体类型及数量", + "ja": "すべてのメディア種類と件数", + "ru": "Все типы медиа с количеством", + "fr": "Tous les types de média avec leur nombre", + "hi": "सभी मीडिया प्रकार और उनकी संख्या", + "ar": "جميع أنواع الوسائط مع الأعداد", + "bn": "সব মিডিয়ার ধরন ও সংখ্যা", + "pt": "Todos os tipos de mídia com contagens" + }, + "PFAD": { + "de": "PFAD", + "en": "PATH", + "es": "RUTA", + "zh": "路径", + "ja": "パス", + "ru": "ПУТЬ", + "fr": "CHEMIN", + "hi": "पथ", + "ar": "المسار", + "bn": "পথ", + "pt": "CAMINHO" + }, + "usage: ": { + "de": "Verwendung: ", + "en": "usage: ", + "es": "uso: ", + "zh": "用法:", + "ja": "使用法: ", + "ru": "использование: ", + "fr": "utilisation : ", + "hi": "उपयोग: ", + "ar": "الاستخدام: ", + "bn": "ব্যবহার: ", + "pt": "uso: " + }, + "positional arguments": { + "de": "Positionsargumente", + "en": "positional arguments", + "es": "argumentos posicionales", + "zh": "位置参数", + "ja": "位置引数", + "ru": "позиционные аргументы", + "fr": "arguments positionnels", + "hi": "स्थितिगत तर्क", + "ar": "وسائط موضعية", + "bn": "অবস্থানগত আর্গুমেন্ট", + "pt": "argumentos posicionais" + }, + "options": { + "de": "Optionen", + "en": "options", + "es": "opciones", + "zh": "选项", + "ja": "オプション", + "ru": "параметры", + "fr": "options", + "hi": "विकल्प", + "ar": "خيارات", + "bn": "বিকল্পসমূহ", + "pt": "opções" + }, + "optional arguments": { + "de": "Optionale Argumente", + "en": "optional arguments", + "es": "argumentos opcionales", + "zh": "可选参数", + "ja": "省略可能な引数", + "ru": "необязательные аргументы", + "fr": "arguments facultatifs", + "hi": "वैकल्पिक तर्क", + "ar": "وسائط اختيارية", + "bn": "ঐচ্ছিক আর্গুমেন্ট", + "pt": "argumentos opcionais" + }, + "subcommands": { + "de": "Unterbefehle", + "en": "subcommands", + "es": "subcomandos", + "zh": "子命令", + "ja": "サブコマンド", + "ru": "подкоманды", + "fr": "sous-commandes", + "hi": "उपकमांड", + "ar": "أوامر فرعية", + "bn": "সাবকমান্ড", + "pt": "subcomandos" + }, + "%(heading)s:": { + "de": "%(heading)s:", + "en": "%(heading)s:", + "es": "%(heading)s:", + "zh": "%(heading)s:", + "ja": "%(heading)s:", + "ru": "%(heading)s:", + "fr": "%(heading)s :", + "hi": "%(heading)s:", + "ar": "%(heading)s:", + "bn": "%(heading)s:", + "pt": "%(heading)s:" + }, + "show this help message and exit": { + "de": "Diese Hilfe anzeigen und beenden", + "en": "show this help message and exit", + "es": "mostrar esta ayuda y salir", + "zh": "显示此帮助信息并退出", + "ja": "このヘルプを表示して終了", + "ru": "показать эту справку и выйти", + "fr": "afficher cette aide puis quitter", + "hi": "यह सहायता दिखाएँ और बाहर निकलें", + "ar": "عرض هذه المساعدة ثم الخروج", + "bn": "এই সহায়তা দেখিয়ে প্রস্থান করুন", + "pt": "mostrar esta ajuda e sair" + }, + "unrecognized arguments: %s": { + "de": "Unbekannte Argumente: %s", + "en": "unrecognized arguments: %s", + "es": "argumentos no reconocidos: %s", + "zh": "无法识别的参数:%s", + "ja": "認識できない引数: %s", + "ru": "неизвестные аргументы: %s", + "fr": "arguments non reconnus : %s", + "hi": "अज्ञात तर्क: %s", + "ar": "وسائط غير معروفة: %s", + "bn": "অচেনা আর্গুমেন্ট: %s", + "pt": "argumentos não reconhecidos: %s" + }, + "the following arguments are required: %s": { + "de": "Folgende Argumente sind erforderlich: %s", + "en": "the following arguments are required: %s", + "es": "se requieren los siguientes argumentos: %s", + "zh": "以下参数为必填项:%s", + "ja": "次の引数が必要です: %s", + "ru": "требуются следующие аргументы: %s", + "fr": "les arguments suivants sont requis : %s", + "hi": "निम्नलिखित तर्क आवश्यक हैं: %s", + "ar": "الوسائط التالية مطلوبة: %s", + "bn": "নিচের আর্গুমেন্টগুলো আবশ্যক: %s", + "pt": "os seguintes argumentos são obrigatórios: %s" + }, + "expected one argument": { + "de": "Ein Argument erwartet", + "en": "expected one argument", + "es": "se esperaba un argumento", + "zh": "需要一个参数", + "ja": "引数を1つ指定してください", + "ru": "ожидается один аргумент", + "fr": "un argument attendu", + "hi": "एक तर्क अपेक्षित था", + "ar": "يُتوقع وسيط واحد", + "bn": "একটি আর্গুমেন্ট প্রত্যাশিত", + "pt": "esperado um argumento" + }, + "invalid %(type)s value: %(value)r": { + "de": "Ungültiger %(type)s-Wert: %(value)r", + "en": "invalid %(type)s value: %(value)r", + "es": "valor %(type)s no válido: %(value)r", + "zh": "无效的 %(type)s 值:%(value)r", + "ja": "無効な%(type)s値: %(value)r", + "ru": "недопустимое значение %(type)s: %(value)r", + "fr": "valeur %(type)s invalide : %(value)r", + "hi": "अमान्य %(type)s मान: %(value)r", + "ar": "قيمة %(type)s غير صالحة: %(value)r", + "bn": "অবৈধ %(type)s মান: %(value)r", + "pt": "valor %(type)s inválido: %(value)r" + }, + "invalid choice: %(value)r (choose from %(choices)s)": { + "de": "Ungültige Auswahl: %(value)r (wähle aus %(choices)s)", + "en": "invalid choice: %(value)r (choose from %(choices)s)", + "es": "opción no válida: %(value)r (elige entre %(choices)s)", + "zh": "无效选项:%(value)r(可选值:%(choices)s)", + "ja": "無効な選択: %(value)r(選択肢: %(choices)s)", + "ru": "недопустимый выбор: %(value)r (выберите из %(choices)s)", + "fr": "choix invalide : %(value)r (choisissez parmi %(choices)s)", + "hi": "अमान्य विकल्प: %(value)r (इनमें से चुनें: %(choices)s)", + "ar": "اختيار غير صالح: %(value)r (اختر من %(choices)s)", + "bn": "অবৈধ পছন্দ: %(value)r (এখান থেকে বেছে নিন: %(choices)s)", + "pt": "opção inválida: %(value)r (escolha entre %(choices)s)" + }, + "argument %(argument_name)s: %(message)s": { + "de": "Argument %(argument_name)s: %(message)s", + "en": "argument %(argument_name)s: %(message)s", + "es": "argumento %(argument_name)s: %(message)s", + "zh": "参数 %(argument_name)s:%(message)s", + "ja": "引数 %(argument_name)s: %(message)s", + "ru": "аргумент %(argument_name)s: %(message)s", + "fr": "argument %(argument_name)s : %(message)s", + "hi": "तर्क %(argument_name)s: %(message)s", + "ar": "الوسيط %(argument_name)s: %(message)s", + "bn": "আর্গুমেন্ট %(argument_name)s: %(message)s", + "pt": "argumento %(argument_name)s: %(message)s" + }, + "%(prog)s: error: %(message)s\n": { + "de": "%(prog)s: Fehler: %(message)s\n", + "en": "%(prog)s: error: %(message)s\n", + "es": "%(prog)s: error: %(message)s\n", + "zh": "%(prog)s:错误:%(message)s\n", + "ja": "%(prog)s: エラー: %(message)s\n", + "ru": "%(prog)s: ошибка: %(message)s\n", + "fr": "%(prog)s : erreur : %(message)s\n", + "hi": "%(prog)s: त्रुटि: %(message)s\n", + "ar": "%(prog)s: خطأ: %(message)s\n", + "bn": "%(prog)s: ত্রুটি: %(message)s\n", + "pt": "%(prog)s: erro: %(message)s\n" + }, + "{count} Favoriten": { + "de": "Favoriten: {count}", + "en": "Favorites: {count}", + "es": "Favoritos: {count}", + "zh": "收藏项:{count}", + "ja": "お気に入り:{count}", + "ru": "Избранное: {count}", + "fr": "Favoris : {count}", + "hi": "पसंदीदा: {count}", + "ar": "المفضلة: {count}", + "bn": "পছন্দের: {count}", + "pt": "Favoritos: {count}" + }, + "Sprache für die CLI": { + "de": "Sprache für die CLI", + "en": "CLI language", + "es": "Idioma de la CLI", + "zh": "CLI 语言", + "ja": "CLI の言語", + "ru": "Язык CLI", + "fr": "Langue de la CLI", + "hi": "CLI की भाषा", + "ar": "لغة سطر الأوامر", + "bn": "CLI-এর ভাষা", + "pt": "Idioma da CLI" } -} \ No newline at end of file +} diff --git a/manage_translations.py b/manage_translations.py index 046d250..6891ffa 100644 --- a/manage_translations.py +++ b/manage_translations.py @@ -12,6 +12,8 @@ import os import sys +from translator import SUPPORTED_LANGUAGES + if hasattr(sys.stdout, "reconfigure"): try: sys.stdout.reconfigure(encoding="utf-8") @@ -19,7 +21,6 @@ pass TRANSLATION_FILE = "locales/translations.json" -SUPPORTED_LANGUAGES = ['de', 'en', 'es', 'zh', 'ja', 'ru'] STRING_PATTERNS = [ re.compile(r'text\s*=\s*"([^"]+)"'), diff --git a/tests/test_cli.py b/tests/test_cli.py index 9b3f6bd..edd56b2 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2,11 +2,14 @@ import argparse import io +import json +import os import sqlite3 import tempfile import unittest -from contextlib import redirect_stdout +from contextlib import redirect_stderr, redirect_stdout from pathlib import Path +from unittest.mock import patch import cli @@ -100,5 +103,87 @@ def test_list_and_types_show_only_visible_entries(self): self.assertNotIn("movie", text) + def test_cli_selects_each_worldset_locale_and_keeps_database_read_only(self): + catalog_path = Path(cli.__file__).resolve().parent / 'locales' / 'translations.json' + catalog = json.loads(catalog_path.read_text(encoding='utf-8')) + before = self.db_path.read_bytes() + for lang in ['de', 'en', 'es', 'zh', 'ja', 'ru', 'fr', 'hi', 'ar', 'bn', 'pt']: + with self.subTest(lang=lang): + output = io.StringIO() + with redirect_stdout(output): + result = cli.main([ + '--db', str(self.db_path), '--lang', lang, 'show', '1' + ]) + self.assertEqual(result, 0) + self.assertIn(catalog['Titel:'][lang], output.getvalue()) + self.assertIn('Visible', output.getvalue()) + self.assertEqual(before, self.db_path.read_bytes()) + + def test_cli_language_option_after_subcommand_is_supported(self): + output = io.StringIO() + with redirect_stdout(output): + result = cli.main([ + '--db', str(self.db_path), 'show', '1', '--lang', 'fr' + ]) + self.assertEqual(result, 0) + self.assertIn('Titre :', output.getvalue()) + + + def test_all_cli_commands_translate_result_messages(self): + expected = { + 'list': 'Entrées : 1 (max. 50)', + 'search': 'Résultats : 1', + 'favs': 'Favoris : 1', + 'recent': 'Ouverts récemment : 1', + 'types': 'Entrées : 1', + } + for command, phrase in expected.items(): + with self.subTest(command=command): + output = io.StringIO() + argv = ['--lang', 'fr', '--db', str(self.db_path), command] + if command == 'search': + argv.append('Visible') + with redirect_stdout(output): + result = cli.main(argv) + self.assertEqual(result, 0) + self.assertIn(phrase, output.getvalue()) + + def test_help_uses_selected_language_for_argparse_and_application_text(self): + output = io.StringIO() + with redirect_stdout(output): + with self.assertRaises(SystemExit) as raised: + cli.main(['--lang', 'es', '--help']) + self.assertEqual(raised.exception.code, 0) + self.assertIn('uso: ', output.getvalue()) + self.assertIn('opciones', output.getvalue()) + self.assertIn('solo lectura', output.getvalue()) + self.assertIn('Idioma de la CLI', output.getvalue()) + + def test_environment_language_accepts_regional_code(self): + output = io.StringIO() + with patch.dict(os.environ, {'MEDIABRAIN_LANG': 'pt_BR'}): + with redirect_stdout(output): + result = cli.main(['--db', str(self.db_path), 'show', '1']) + self.assertEqual(result, 0) + self.assertIn('Título:', output.getvalue()) + + def test_missing_database_keeps_exit_code_and_localizes_error(self): + missing = Path(self.temp.name) / 'missing.db' + error = io.StringIO() + with redirect_stderr(error): + with self.assertRaises(SystemExit) as raised: + cli.main(['--lang', 'fr', '--db', str(missing), 'list']) + self.assertEqual(raised.exception.code, 1) + self.assertIn('Base de données introuvable', error.getvalue()) + + def test_argparse_invalid_locale_keeps_usage_error_exit_code(self): + error = io.StringIO() + with redirect_stderr(error): + with self.assertRaises(SystemExit) as raised: + cli.main(['--lang', 'xx', 'list']) + self.assertEqual(raised.exception.code, 2) + self.assertIn('Ungültige Auswahl', error.getvalue()) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_translations.py b/tests/test_translations.py index 47f2eb2..9819ab2 100644 --- a/tests/test_translations.py +++ b/tests/test_translations.py @@ -47,8 +47,8 @@ def test_desktop_string_scan_has_no_unregistered_german_keys(self): ) -class TestMultiLanguageTier2Catalog(unittest.TestCase): - """Tier-2 multi-language contract tests (DE, EN, ES, ZH, JA, RU).""" +class TestWorldSetCatalog(unittest.TestCase): + """World-set catalog contract tests for all eleven supported locales.""" def setUp(self): self.catalog = json.loads(CATALOG.read_text(encoding="utf-8")) @@ -57,7 +57,7 @@ def setUp(self): def test_supported_languages_list(self): self.assertEqual( self.translator.get_supported_languages(), - ['de', 'en', 'es', 'zh', 'ja', 'ru'], + ['de', 'en', 'es', 'zh', 'ja', 'ru', 'fr', 'hi', 'ar', 'bn', 'pt'], ) def test_every_committed_key_has_a_nonempty_value_in_all_languages(self): @@ -167,6 +167,14 @@ def test_interpolation_resilient_to_missing_kwargs(self): res = self.translator.t("TemplateKey") self.assertEqual(res, "Encontrado: {count} elementos") + def test_cli_can_disable_unknown_german_key_autoregistration(self): + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + translator = TranslationSystem('fr', app_dir=root, auto_register_missing=False) + + self.assertEqual(translator.t('Unbekannter Fehler'), 'Unbekannter Fehler') + self.assertFalse((root / 'locales' / 'translations.json').exists()) + if __name__ == "__main__": unittest.main() diff --git a/translator.py b/translator.py index 2c429cf..add714f 100644 --- a/translator.py +++ b/translator.py @@ -19,13 +19,13 @@ from typing import Dict, List, Set -SUPPORTED_LANGUAGES = ['de', 'en', 'es', 'zh', 'ja', 'ru'] +SUPPORTED_LANGUAGES = ['de', 'en', 'es', 'zh', 'ja', 'ru', 'fr', 'hi', 'ar', 'bn', 'pt'] class TranslationSystem: """Multi-Language Support System v1.1""" - def __init__(self, default_lang: str = 'de', app_dir: Path = None): + def __init__(self, default_lang: str = 'de', app_dir: Path = None, *, auto_register_missing: bool = True): """ Initialisiert Translation-System. @@ -34,6 +34,7 @@ def __init__(self, default_lang: str = 'de', app_dir: Path = None): app_dir: Verzeichnis der Anwendung (default: aktuelles Verzeichnis) """ self.current_lang = default_lang + self.auto_register_missing = auto_register_missing if app_dir is None: app_dir = Path.cwd() @@ -113,7 +114,7 @@ def t(self, key: str, **kwargs) -> str: text = val_de if text is None: - if key not in self.translations and self._is_german(key): + if self.auto_register_missing and key not in self.translations and self._is_german(key): entry = {"de": key} for lang in SUPPORTED_LANGUAGES: if lang != "de": From 24171b573977d3970353f9cce511855f2eb6d402 Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Sun, 25 Oct 2026 07:01:24 +0100 Subject: [PATCH 7/9] feat: localize desktop GUI in eleven languages --- MediaBrain.py | 42 +- config.py | 3 +- gui.py | 588 +++-- gui_playlists.py | 233 +- locales/translations.json | 3679 +++++++++++++++++++++++++++ mediabrain_launcher.py | 32 +- search_advanced.py | 76 +- tests/test_gui_localization_i18n.py | 125 + translator.py | 33 + 9 files changed, 4450 insertions(+), 361 deletions(-) create mode 100644 tests/test_gui_localization_i18n.py diff --git a/MediaBrain.py b/MediaBrain.py index 8e42c85..91563f5 100644 --- a/MediaBrain.py +++ b/MediaBrain.py @@ -7,8 +7,9 @@ import os import socket as _socket import sys +from pathlib import Path from PySide6.QtWidgets import QApplication, QMessageBox -from PySide6.QtCore import QTimer # Import nach oben verschoben +from PySide6.QtCore import QTimer, Qt # Import nach oben verschoben from PySide6.QtGui import QIcon # Projektordner zum Pfad hinzufügen @@ -20,6 +21,21 @@ import background import config from logger_system import logger +from translator import TranslationSystem, resolve_language, set_default_translation_system, translate as _t + + +def _create_translation_system(): + language = resolve_language( + os.environ.get("MEDIABRAIN_LANG"), + config.config.get("ui.language", "de"), + ) + translator = TranslationSystem( + language, + app_dir=Path(__file__).resolve().parent, + auto_register_missing=False, + ) + set_default_translation_system(translator) + return translator APP_ICON_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "MediaBrain.ico") @@ -59,11 +75,15 @@ def __init__(self): # 3. GUI starten existing_app = QApplication.instance() self.app = existing_app if existing_app is not None else QApplication(sys.argv) + self.translator = _create_translation_system() + direction = Qt.LayoutDirection.RightToLeft if self.translator.get_language() == "ar" else Qt.LayoutDirection.LeftToRight + self.app.setLayoutDirection(direction) self.app_icon = QIcon(APP_ICON_PATH) if os.path.exists(APP_ICON_PATH) else None if self.app_icon is not None: self.app.setWindowIcon(self.app_icon) self.window = MainWindow(self.media_manager, self.blacklist_manager, - self.tag_manager, self.playlist_manager) + self.tag_manager, self.playlist_manager, + translator=self.translator) if self.app_icon is not None: self.window.setWindowIcon(self.app_icon) @@ -148,12 +168,22 @@ def run(self): if __name__ == "__main__": if not _acquire_single_instance(): _app = QApplication(sys.argv) - QMessageBox.information( - None, + _translator = _create_translation_system() + _app.setLayoutDirection( + Qt.LayoutDirection.RightToLeft + if _translator.get_language() == "ar" + else Qt.LayoutDirection.LeftToRight + ) + _box = QMessageBox( + QMessageBox.Icon.Information, "MediaBrain", - "MediaBrain läuft bereits im Hintergrund.\n" - "Tray-Icon doppelklicken um das Fenster zu öffnen.", + _t("MediaBrain läuft bereits im Hintergrund.\nTray-Icon doppelklicken um das Fenster zu öffnen."), + QMessageBox.StandardButton.Ok, ) + _ok = _box.button(QMessageBox.StandardButton.Ok) + if _ok is not None: + _ok.setText(_t("OK")) + _box.exec() sys.exit(0) controller = AppController() controller.run() diff --git a/config.py b/config.py index a3b0dde..a4a9df9 100644 --- a/config.py +++ b/config.py @@ -52,7 +52,8 @@ "theme": "light", "window_width": 1200, "window_height": 800, - "system_tray": False + "system_tray": False, + "language": "de" }, "providers": { "netflix": {"preferred_open_method": "browser"}, diff --git a/gui.py b/gui.py index 36c7795..2049c29 100644 --- a/gui.py +++ b/gui.py @@ -25,6 +25,7 @@ from playlists import PlaylistManager from export_import import MediaExporter, MediaImporter import config +from translator import translate as _t, set_default_translation_system, resolve_language from theme_engine import ThemeEngine # Erweiterte Suche @@ -40,6 +41,66 @@ _tag_manager: TagManager = None +LANGUAGE_CHOICES = [ + ("de", "Deutsch"), ("en", "English"), ("es", "Español"), + ("zh", "简体中文"), ("ja", "日本語"), ("ru", "Русский"), + ("fr", "Français"), ("hi", "हिन्दी"), ("ar", "العربية"), + ("bn", "বাংলা"), ("pt", "Português"), +] + + +def _set_standard_message_buttons(box): + for standard, label in ( + (QMessageBox.StandardButton.Ok, "OK"), + (QMessageBox.StandardButton.Cancel, "Abbrechen"), + (QMessageBox.StandardButton.Yes, "Ja"), + (QMessageBox.StandardButton.No, "Nein"), + (QMessageBox.StandardButton.Save, "Speichern"), + (QMessageBox.StandardButton.Close, "Schließen"), + ): + button = box.button(standard) + if button is not None: + button.setText(_t(label)) + + +def _show_message(icon, parent, title, text, *args): + buttons = args[0] if args else ( + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + if icon == QMessageBox.Icon.Question else QMessageBox.StandardButton.Ok + ) + box = QMessageBox(icon, title, text, buttons, parent) + _set_standard_message_buttons(box) + if len(args) > 1 and args[1] != QMessageBox.StandardButton.NoButton: + box.setDefaultButton(args[1]) + elif icon == QMessageBox.Icon.Question: + box.setDefaultButton(QMessageBox.StandardButton.No) + return box.exec() + + +def _input_text(parent, title, prompt): + from PySide6.QtWidgets import QInputDialog + dialog = QInputDialog(parent) + dialog.setWindowTitle(title) + dialog.setLabelText(prompt) + dialog.setInputMode(QInputDialog.InputMode.TextInput) + dialog.setOkButtonText(_t("OK")) + dialog.setCancelButtonText(_t("Abbrechen")) + accepted = dialog.exec() == QInputDialog.DialogCode.Accepted + return dialog.textValue(), accepted + + +def _media_type_display(media_type): + labels = { + "movie": "Film", "music": "Musik", "series": "Serie", + "clip": "Clip", "video": "Video", "short": "Kurzvideo", + "youtube": "YouTube-Clip", "podcast": "Podcast", + "audiobook": "Hörbuch", "document": "Dokument", + } + code = str(media_type or "").lower() + label = labels.get(code) + return _t(label) if label else str(media_type or _t("Medium")) + + def get_tag_manager() -> TagManager: """Returns the global TagManager instance.""" return _tag_manager @@ -84,52 +145,52 @@ def build_online_metadata_fetch_kwargs(item): return kwargs -def format_online_metadata(result, current_description=None): +def format_online_metadata(result, current_description=None, translator=None): """Formatiert Metadaten für die Anzeige im Detail-Panel.""" if not result: - return [], None, "Keine Online-Metadaten gefunden." + return [], None, _t("Keine Online-Metadaten gefunden.", translator) if "error" in result: - return [], None, f"Fehler: {result['error']}" + return [], None, _t("Fehler: {error}", translator, error=result["error"]) lines = [] title = result.get("title") if title: - lines.append(f"Titel: {title}") + lines.append(_t("Titel: {title}", translator, title=title)) year = result.get("year") if year: - lines.append(f"Jahr: {year}") + lines.append(_t("Jahr: {year}", translator, year=year)) rating = result.get("rating") if rating not in (None, ""): - lines.append(f"Bewertung: {rating}/10") + lines.append(_t("Bewertung: {rating}/10", translator, rating=rating)) genres = result.get("genres") if genres: if isinstance(genres, list): genres = ", ".join(str(genre) for genre in genres if genre) - lines.append(f"Genres: {genres}") + lines.append(_t("Genres: {genres}", translator, genres=genres)) runtime = result.get("runtime") if runtime: - lines.append(f"Laufzeit: {runtime} Min") + lines.append(_t("Laufzeit: {runtime} Min", translator, runtime=runtime)) director = result.get("director") if director: - lines.append(f"Regie: {director}") + lines.append(_t("Regie: {director}", translator, director=director)) channel = result.get("channel") or result.get("author_name") if channel: - lines.append(f"Kanal: {channel}") + lines.append(_t("Kanal: {channel}", translator, channel=channel)) source = result.get("source") if source: - lines.append(f"Quelle: {source}") + lines.append(_t("Quelle: {source}", translator, source=source)) if result.get("thumbnail_url"): - lines.append("Vorschaubild: verfügbar") + lines.append(_t("Vorschaubild: verfügbar", translator)) description = result.get("description") if description and description != current_description: @@ -140,7 +201,7 @@ def format_online_metadata(result, current_description=None): description = None if not lines and not description: - return [], None, "Online-Metadaten gefunden, aber keine Felder zum Anzeigen." + return [], None, _t("Online-Metadaten gefunden, aber keine Felder zum Anzeigen.", translator) return lines, description, None @@ -150,15 +211,18 @@ def format_online_metadata(result, current_description=None): # ============================================================ class SearchBar(QWidget): - def __init__(self, placeholder="Suchen...", on_search=None): + def __init__(self, placeholder="Suchen...", on_search=None, translator=None): super().__init__() + self.translator = translator + if translator is not None: + set_default_translation_system(translator) self.on_search = on_search layout = QHBoxLayout() self.setLayout(layout) self.input = QLineEdit() - self.input.setPlaceholderText(placeholder) + self.input.setPlaceholderText(_t(placeholder)) self.input.textChanged.connect(self._trigger_search) layout.addWidget(self.input) @@ -173,22 +237,25 @@ def _trigger_search(self): # ============================================================ class CollapsiblePanel(QWidget): - def __init__(self, title="Details", content_widget=None): + def __init__(self, title="Details", content_widget=None, translator=None): super().__init__() + self.translator = translator + if translator is not None: + set_default_translation_system(translator) self.is_open = False - self._title = title + self._title = _t(title) self.main_layout = QVBoxLayout() self.setLayout(self.main_layout) - self.header = QPushButton(f"\u25B6 {title}") + self.header = QPushButton(f"▶ {_t(title)}") self.header.setCheckable(True) self.header.setStyleSheet("text-align: left; font-size: 14px; padding: 6px 12px;") self.header.clicked.connect(self.toggle) self.main_layout.addWidget(self.header) - self.content = content_widget or QLabel("Keine Details verfügbar") + self.content = content_widget or QLabel(_t("Keine Details verfügbar")) self.content.setVisible(False) self.main_layout.addWidget(self.content) @@ -210,8 +277,11 @@ class MediaItemWidget(QFrame): ICON_SIZE = QSize(44, 44) # war 36×36 — Emoji-Glyphen können 36px überschreiten ACTION_BUTTON_MIN_SIZE = QSize(96, 40) # war QSize(88, 36) — Höhe für komfortables Touch-Target - def __init__(self, item: MediaItem, media_manager: MediaManager, blacklist_manager: BlacklistManager): + def __init__(self, item: MediaItem, media_manager: MediaManager, blacklist_manager: BlacklistManager, translator=None): super().__init__() + self.translator = translator + if translator is not None: + set_default_translation_system(translator) self.item = item self.media_manager = media_manager self.blacklist_manager = blacklist_manager @@ -235,9 +305,9 @@ def __init__(self, item: MediaItem, media_manager: MediaManager, blacklist_manag icon.setStyleSheet("font-size: 22px;") # war 20px — passt besser zu 44px-Container media_type_label = self._media_type_label() icon.setToolTip(media_type_label) - icon.setAccessibleName(f"Medientyp: {media_type_label}") + icon.setAccessibleName(_t('Medientyp: {media_type}', media_type=(media_type_label))) icon.setAccessibleDescription( - f"Zeigt den Medientyp für {self.item.title} an." + _t('Zeigt den Medientyp für {title} an.', title=(self.item.title)) ) layout.addWidget(icon) self.type_icon_label = icon @@ -253,23 +323,23 @@ def __init__(self, item: MediaItem, media_manager: MediaManager, blacklist_manag # Buttons kompakt self.fav_btn = QPushButton("\u2605" if item.is_favorite else "\u2606") self.fav_btn.setFixedSize(QSize(44, 44)) # war 40×40 — 24px-Schrift braucht mehr Spielraum - self.fav_btn.setToolTip("Favorit entfernen" if item.is_favorite else "Als Favorit markieren") + self.fav_btn.setToolTip(_t("Favorit entfernen") if item.is_favorite else _t("Als Favorit markieren")) action_name = self._favorite_action_name() self.fav_btn.setAccessibleName(action_name) self.fav_btn.setAccessibleDescription( - f"Ändert den Favoritenstatus für {self.item.title}." + _t('Ändert den Favoritenstatus für {title}.', title=(self.item.title)) ) fav_color = "#FFC107" if item.is_favorite else "#888888" self.fav_btn.setStyleSheet(f"font-size: 24px; color: {fav_color}; border: none; background: transparent; padding: 0px;") self.fav_btn.clicked.connect(self.toggle_favorite) layout.addWidget(self.fav_btn) - self.open_btn = QPushButton("\u25B6 öffnen") + self.open_btn = QPushButton("▶ " + _t("öffnen")) self.open_btn.setMinimumSize(self.ACTION_BUTTON_MIN_SIZE) self.open_btn.clicked.connect(self.open_item) layout.addWidget(self.open_btn) - self.details_btn = QPushButton("\u2139 Details") + self.details_btn = QPushButton("ℹ " + _t("Details anzeigen")) self.details_btn.setMinimumSize(self.ACTION_BUTTON_MIN_SIZE) self.details_btn.clicked.connect(self.open_detail_page) layout.addWidget(self.details_btn) @@ -280,8 +350,8 @@ def __init__(self, item: MediaItem, media_manager: MediaManager, blacklist_manag def _favorite_action_name(self) -> str: if self.item.is_favorite: - return f"{self.item.title} aus Favoriten entfernen" - return f"{self.item.title} als Favorit markieren" + return _t("{title} aus Favoriten entfernen", title=self.item.title) + return _t("{title} als Favorit markieren", title=self.item.title) def _media_type_label(self) -> str: labels = { @@ -296,7 +366,8 @@ def _media_type_label(self) -> str: "audiobook": "Hörbuch", "document": "Dokument", } - return labels.get(str(getattr(self.item, "type", "") or "").lower(), "Medium") + label = labels.get(str(getattr(self.item, "type", "") or "").lower()) + return _t(label) if label else _t("Medium") def open_item(self): try: @@ -305,7 +376,7 @@ def open_item(self): handler.open_item(self.item) except Exception as e: from PySide6.QtWidgets import QMessageBox - QMessageBox.warning(self, "Fehler", f"Konnte Medium nicht öffnen: {e}") + _show_message(QMessageBox.Icon.Warning, self, _t("Fehler"), _t('Konnte Medium nicht öffnen: {error}', error=(e))) def open_detail_page(self): try: @@ -314,7 +385,7 @@ def open_detail_page(self): mw.open_detail(self.item) except Exception as e: from PySide6.QtWidgets import QMessageBox - QMessageBox.warning(self, "Fehler", f"Detailseite konnte nicht geöffnet werden: {e}") + _show_message(QMessageBox.Icon.Warning, self, _t("Fehler"), _t('Detailseite konnte nicht geöffnet werden: {error}', error=(e))) def toggle_favorite(self): try: @@ -326,7 +397,7 @@ def toggle_favorite(self): notify_gui_refresh() except Exception as e: from PySide6.QtWidgets import QMessageBox - QMessageBox.warning(self, "Fehler", f"Favorit konnte nicht geändert werden: {e}") + _show_message(QMessageBox.Icon.Warning, self, _t("Fehler"), _t('Favorit konnte nicht geändert werden: {error}', error=(e))) def toggle_details(self): @@ -348,16 +419,16 @@ def show_in_explorer(self): subprocess.Popen(["xdg-open", folder]) except Exception as e: from PySide6.QtWidgets import QMessageBox - QMessageBox.warning(self, "Fehler", f"Konnte Datei nicht im Explorer anzeigen: {e}") + _show_message(QMessageBox.Icon.Warning, self, _t("Fehler"), _t('Konnte Datei nicht im Explorer anzeigen: {error}', error=(e))) def delete_file(self): try: from PySide6.QtWidgets import QMessageBox import os - reply = QMessageBox.question( - self, "Datei löschen", - f"Soll die Datei wirklich unwiderruflich gelöscht werden?\n\n{self.item.local_path}", + reply = _show_message(QMessageBox.Icon.Question, + self, _t("Datei löschen"), + _t('Soll die Datei wirklich unwiderruflich gelöscht werden?\n\n{path}', path=(self.item.local_path)), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No ) @@ -374,7 +445,7 @@ def delete_file(self): notify_gui_refresh() except Exception as e: from PySide6.QtWidgets import QMessageBox - QMessageBox.warning(self, "Fehler", f"Datei konnte nicht gelöscht werden: {e}") + _show_message(QMessageBox.Icon.Warning, self, _t("Fehler"), _t('Datei konnte nicht gelöscht werden: {error}', error=(e))) def refresh_metadata(self): # Später: ID3/MP4 neu einlesen @@ -387,7 +458,7 @@ def open_context_menu(self, pos): fav_action.triggered.connect(self.toggle_favorite) menu.addAction(fav_action) - temp_delete_action = QAction("Temporär ausblenden") + temp_delete_action = QAction(_t("Temporär ausblenden")) temp_delete_action.triggered.connect(self.temp_delete) menu.addAction(temp_delete_action) @@ -405,7 +476,7 @@ def open_context_menu(self, pos): blacklist_menu.addAction(act) if self.item.blacklist_flag == 1: - unblack = QAction("Blacklist entfernen") + unblack = QAction(_t("Blacklist entfernen")) unblack.triggered.connect(lambda: (self.blacklist_manager.set_blacklist(self.item.id, False), notify_gui_refresh())) menu.addAction(unblack) @@ -413,21 +484,21 @@ def open_context_menu(self, pos): # Datei-Aktionen (nur für lokale Dateien) if self.item.is_local_file: - show_action = QAction("Im Explorer anzeigen") + show_action = QAction(_t("Im Explorer anzeigen")) show_action.triggered.connect(self.show_in_explorer) menu.addAction(show_action) if config.config.get("allow_file_deletion"): - delete_action = QAction("Datei löschen") + delete_action = QAction(_t("Datei löschen")) delete_action.triggered.connect(self.delete_file) menu.addAction(delete_action) - meta_action = QAction("Lokale Metadaten aktualisieren") + meta_action = QAction(_t("Lokale Metadaten aktualisieren")) meta_action.triggered.connect(self.refresh_metadata) menu.addAction(meta_action) # Online-Metadaten abrufen (NEU!) - online_meta_action = QAction("🌐 Online-Metadaten abrufen") + online_meta_action = QAction(_t("🌐 Online-Metadaten abrufen")) online_meta_action.triggered.connect(self.fetch_online_metadata) menu.addAction(online_meta_action) @@ -443,7 +514,7 @@ def temp_delete(self): notify_gui_refresh() except Exception as e: from PySide6.QtWidgets import QMessageBox - QMessageBox.warning(self, "Fehler", f"Temporäres Ausblenden fehlgeschlagen: {e}") + _show_message(QMessageBox.Icon.Warning, self, _t("Fehler"), _t('Temporäres Ausblenden fehlgeschlagen: {error}', error=(e))) def blacklist(self, code): try: @@ -451,7 +522,7 @@ def blacklist(self, code): notify_gui_refresh() except Exception as e: from PySide6.QtWidgets import QMessageBox - QMessageBox.warning(self, "Fehler", f"Blacklist-Aktion fehlgeschlagen: {e}") + _show_message(QMessageBox.Icon.Warning, self, _t("Fehler"), _t('Blacklist-Aktion fehlgeschlagen: {error}', error=(e))) def fetch_online_metadata(self): """Holt Online-Metadaten von TMDb/OMDb/MusicBrainz und aktualisiert den DB-Eintrag.""" @@ -466,8 +537,8 @@ def fetch_online_metadata(self): needs_api_keys = self.item.type in {"movie", "series"} has_required_api = status.get("tmdb") or status.get("omdb") if needs_api_keys and not has_required_api: - QMessageBox.warning(self, "API nicht verfügbar", - "Keine API-Keys konfiguriert. Bitte in settings.json eintragen.") + _show_message(QMessageBox.Icon.Warning, self, _t("API nicht verfügbar"), + _t("Keine API-Keys konfiguriert. Bitte in settings.json eintragen.")) return # Metadaten basierend auf Typ abrufen @@ -482,8 +553,8 @@ def fetch_online_metadata(self): ) if not result: - QMessageBox.information(self, "Keine Ergebnisse", - f"Keine Online-Metadaten gefunden für '{self.item.title}'") + _show_message(QMessageBox.Icon.Information, self, _t("Keine Ergebnisse"), + _t("Keine Online-Metadaten gefunden für '{title}'", title=(self.item.title))) return # Datenbank aktualisieren @@ -507,18 +578,17 @@ def fetch_online_metadata(self): query = f"UPDATE media_items SET {', '.join(updates)} WHERE id = ?" self.media_manager.db.execute(query, tuple(params)) - QMessageBox.information(self, "Metadaten aktualisiert", - f"Metadaten für '{result.get('title', self.item.title)}' wurden aktualisiert.\n" - f"Quelle: {result.get('source', 'unbekannt')}") + _show_message(QMessageBox.Icon.Information, self, _t("Metadaten aktualisiert"), + _t("Metadaten für '{title}' wurden aktualisiert.\nQuelle: {source}", title=(result.get('title', self.item.title)), source=(result.get('source', 'unbekannt')))) # GUI aktualisieren notify_gui_refresh() else: - QMessageBox.information(self, "Keine neuen Daten", - "Die gefundenen Metadaten sind bereits identisch.") + _show_message(QMessageBox.Icon.Information, self, _t("Keine neuen Daten"), + _t("Die gefundenen Metadaten sind bereits identisch.")) except Exception as e: - QMessageBox.critical(self, "Fehler", - f"Metadaten konnten nicht abgerufen werden: {e}") + _show_message(QMessageBox.Icon.Critical, self, _t("Fehler"), + _t('Metadaten konnten nicht abgerufen werden: {error}', error=(e))) # Duplikat blacklist() entfernt - existiert bereits in Zeile 285 mit Error-Handling @@ -556,8 +626,8 @@ def data(self, index, role): if tm: tags = tm.get_tags_for_media(item.id) if tags: - tags_str = "\nTags: " + ", ".join(t["name"] for t in tags) - return f"{desc}\nQuelle: {item.source}{tags_str}" + tags_str = "\n" + _t("Tags: {tags}", tags=", ".join(t["name"] for t in tags)) + return f"{desc}\n" + _t("Quelle: {source}", source=item.source) + tags_str if role == Qt.ItemDataRole.UserRole: return item @@ -709,21 +779,23 @@ def paint(self, painter: QPainter, option, index): # --- Helper: Shared context menu builder for QListView-based views --- -def _build_item_context_menu(parent_widget, item: MediaItem, media_manager, blacklist_manager, refresh_callback): +def _build_item_context_menu(parent_widget, item: MediaItem, media_manager, blacklist_manager, refresh_callback, translator=None): """Builds a standard context menu for a media item. Used by LibraryView, FavoritesView, GlobalSearchView to avoid code duplication. Returns the QMenu (caller must exec it). """ + if translator is not None: + set_default_translation_system(translator) menu = QMenu(parent_widget) # Open - act_open = QAction("öffnen", parent_widget) + act_open = QAction(_t("öffnen"), parent_widget) act_open.triggered.connect(lambda: _open_media_item(item, media_manager)) menu.addAction(act_open) # Details - act_details = QAction("Details anzeigen", parent_widget) + act_details = QAction(_t("Details anzeigen"), parent_widget) act_details.triggered.connect(lambda: _show_detail(item)) menu.addAction(act_details) @@ -744,7 +816,7 @@ def _build_item_context_menu(parent_widget, item: MediaItem, media_manager, blac bl_menu.addAction(act) if item.blacklist_flag == 1: - act_unbl = QAction("Blacklist entfernen", parent_widget) + act_unbl = QAction(_t("Blacklist entfernen"), parent_widget) act_unbl.triggered.connect(lambda: (blacklist_manager.set_blacklist(item.id, False), refresh_callback())) menu.addAction(act_unbl) @@ -773,14 +845,14 @@ def _build_item_context_menu(parent_widget, item: MediaItem, media_manager, blac if all_tags: tag_menu.addSeparator() - act_new_tag = QAction("+ Neuer Tag...", parent_widget) - act_new_tag.triggered.connect(lambda: _create_and_assign_tag(parent_widget, item.id, refresh_callback)) + act_new_tag = QAction(_t("+ Neuer Tag..."), parent_widget) + act_new_tag.triggered.connect(lambda: _create_and_assign_tag(parent_widget, item.id, refresh_callback, translator)) tag_menu.addAction(act_new_tag) # Local file actions if item.is_local_file: menu.addSeparator() - act_explorer = QAction("Im Explorer anzeigen", parent_widget) + act_explorer = QAction(_t("Im Explorer anzeigen"), parent_widget) act_explorer.triggered.connect(lambda: _show_in_explorer(item)) menu.addAction(act_explorer) @@ -828,13 +900,13 @@ def _show_in_explorer(item): logger.error("Could not show in explorer: %s", e) -def _create_and_assign_tag(parent_widget, media_id, refresh_callback): +def _create_and_assign_tag(parent_widget, media_id, refresh_callback, translator=None): """Shows an input dialog to create a new tag and assign it to a media item.""" from PySide6.QtWidgets import QInputDialog tm = get_tag_manager() if not tm: return - name, ok = QInputDialog.getText(parent_widget, "Neuer Tag", "Tag-Name:") + name, ok = _input_text(parent_widget, _t("Neuer Tag", translator), _t("Tag-Name:", translator)) if ok and name.strip(): try: tag_id = tm.create_tag(name.strip()) @@ -842,7 +914,7 @@ def _create_and_assign_tag(parent_widget, media_id, refresh_callback): refresh_callback() except ValueError as e: from PySide6.QtWidgets import QMessageBox - QMessageBox.warning(parent_widget, "Fehler", str(e)) + _show_message(QMessageBox.Icon.Warning, parent_widget, _t("Fehler"), str(e)) # --- 2. Die optimierte View (Zeigt die Liste an) --- @@ -854,8 +926,11 @@ class LibraryView(QWidget): and context menu actions. """ - def __init__(self, media_type, media_manager: MediaManager, blacklist_manager: BlacklistManager): + def __init__(self, media_type, media_manager: MediaManager, blacklist_manager: BlacklistManager, translator=None): super().__init__() + self.translator = translator + if translator is not None: + set_default_translation_system(translator) self.media_type = media_type self.media_manager = media_manager self.blacklist_manager = blacklist_manager @@ -865,15 +940,15 @@ def __init__(self, media_type, media_manager: MediaManager, blacklist_manager: B self.setLayout(layout) # Search bar - self.search_bar = AdvancedSearchBar() + self.search_bar = AdvancedSearchBar(translator=getattr(self, "translator", None)) self.search_bar.search_triggered.connect(self.apply_search) layout.addWidget(self.search_bar) # Tag filter row tag_row = QHBoxLayout() - tag_row.addWidget(QLabel("Tag-Filter:")) + tag_row.addWidget(QLabel(_t("Tag-Filter:"))) self.tag_combo = QComboBox() - self.tag_combo.addItem("Alle", None) + self.tag_combo.addItem(_t("Alle"), None) self.tag_combo.currentIndexChanged.connect(lambda: self.refresh()) tag_row.addWidget(self.tag_combo, 1) layout.addLayout(tag_row) @@ -900,7 +975,7 @@ def _refresh_tag_combo(self): current_data = self.tag_combo.currentData() self.tag_combo.blockSignals(True) self.tag_combo.clear() - self.tag_combo.addItem("Alle", None) + self.tag_combo.addItem(_t("Alle"), None) for tag in tm.list_tags(): self.tag_combo.addItem(f"{tag['name']} ({tag['count']})", tag["id"]) # Restore selection @@ -938,7 +1013,7 @@ def _on_context_menu(self, pos): item = self.model.data(index, Qt.ItemDataRole.UserRole) if not item: return - menu = _build_item_context_menu(self, item, self.media_manager, self.blacklist_manager, self.refresh) + menu = _build_item_context_menu(self, item, self.media_manager, self.blacklist_manager, self.refresh, self.translator) menu.exec(self.list_view.mapToGlobal(pos)) # ============================================================ # 5. FavoritenView @@ -947,8 +1022,11 @@ def _on_context_menu(self, pos): class FavoritesView(QWidget): """Virtualized favorites view using QListView + Model/Delegate pattern.""" - def __init__(self, media_manager: MediaManager, blacklist_manager: BlacklistManager): + def __init__(self, media_manager: MediaManager, blacklist_manager: BlacklistManager, translator=None): super().__init__() + self.translator = translator + if translator is not None: + set_default_translation_system(translator) self.media_manager = media_manager self.blacklist_manager = blacklist_manager self._needs_refresh = True @@ -956,7 +1034,7 @@ def __init__(self, media_manager: MediaManager, blacklist_manager: BlacklistMana layout = QVBoxLayout() self.setLayout(layout) - layout.addWidget(QLabel("Favoriten")) + layout.addWidget(QLabel(_t("Favoriten"))) self.list_view = QListView() self.model = MediaListModel() @@ -993,7 +1071,7 @@ def _on_context_menu(self, pos): item = self.model.data(index, Qt.ItemDataRole.UserRole) if not item: return - menu = _build_item_context_menu(self, item, self.media_manager, self.blacklist_manager, self.refresh) + menu = _build_item_context_menu(self, item, self.media_manager, self.blacklist_manager, self.refresh, self.translator) menu.exec(self.list_view.mapToGlobal(pos)) @@ -1004,8 +1082,11 @@ def _on_context_menu(self, pos): class GlobalSearchView(QWidget): """Virtualized global search view using QListView + Model/Delegate pattern.""" - def __init__(self, media_manager: MediaManager, blacklist_manager: BlacklistManager): + def __init__(self, media_manager: MediaManager, blacklist_manager: BlacklistManager, translator=None): super().__init__() + self.translator = translator + if translator is not None: + set_default_translation_system(translator) self.media_manager = media_manager self.blacklist_manager = blacklist_manager self._needs_refresh = False @@ -1013,7 +1094,7 @@ def __init__(self, media_manager: MediaManager, blacklist_manager: BlacklistMana layout = QVBoxLayout() self.setLayout(layout) - self.search_bar = AdvancedSearchBar() + self.search_bar = AdvancedSearchBar(translator=getattr(self, "translator", None)) self.search_bar.search_triggered.connect(self.apply_search) layout.addWidget(self.search_bar) @@ -1043,10 +1124,10 @@ def apply_search(self, criteria: SearchCriteria): engine = SearchEngine(self.media_manager.db) results = engine.search(criteria) self.model.update_data(results) - self.result_label.setText(f"{len(results)} Ergebnisse gefunden") + self.result_label.setText(_t('{count} Ergebnisse gefunden', count=(len(results)))) except Exception as e: logger.error("GlobalSearchView.apply_search() error: %s", e) - self.result_label.setText(f"Suchfehler: {e}") + self.result_label.setText(_t('Suchfehler: {error}', error=(e))) def refresh(self): """No-op: search results are not auto-refreshed.""" @@ -1067,7 +1148,7 @@ def _on_context_menu(self, pos): if not item: return menu = _build_item_context_menu(self, item, self.media_manager, self.blacklist_manager, - lambda: self.search_bar._trigger_search()) + lambda: self.search_bar._trigger_search(), self.translator) menu.exec(self.list_view.mapToGlobal(pos)) @@ -1076,9 +1157,12 @@ def _on_context_menu(self, pos): # ============================================================ class SettingsWindow(QWidget): - def __init__(self): + def __init__(self, translator=None): super().__init__() - self.setWindowTitle("Einstellungen") + self.translator = translator + if translator is not None: + set_default_translation_system(translator) + self.setWindowTitle(_t("Einstellungen")) self.resize(600, 400) layout = QVBoxLayout() @@ -1092,10 +1176,23 @@ def __init__(self): g_layout = QVBoxLayout() general.setLayout(g_layout) - theme_label = QLabel("Theme:") + language_label = QLabel(_t("Sprache der Oberfläche:")) + self.language_select = QComboBox() + for code, native_name in LANGUAGE_CHOICES: + self.language_select.addItem(native_name, code) + saved_language = resolve_language( + getattr(translator, "get_language", lambda: None)(), + config.config.get("ui.language", "de"), + ) + self.language_select.setCurrentIndex(self.language_select.findData(saved_language)) + self.language_select.currentIndexChanged.connect(self._on_language_change) + g_layout.addWidget(language_label) + g_layout.addWidget(self.language_select) + + theme_label = QLabel(_t("Theme:")) theme_select = QComboBox() for key, name in _theme_engine.list_themes(): - theme_select.addItem(name, key) + theme_select.addItem(_t(name), key) # Set current current_theme = config.config.get("ui.theme", "light") for i in range(theme_select.count()): @@ -1115,7 +1212,7 @@ def on_theme_change(index): g_layout.addWidget(theme_select) # System Tray - tray_checkbox = QCheckBox("System Tray aktivieren (Minimieren statt Beenden)") + tray_checkbox = QCheckBox(_t("System Tray aktivieren (Minimieren statt Beenden)")) tray_checkbox.setChecked(config.config.get("ui.system_tray", False)) def on_tray_change(state): enabled = bool(state) @@ -1127,18 +1224,18 @@ def on_tray_change(state): tray_checkbox.stateChanged.connect(on_tray_change) g_layout.addWidget(tray_checkbox) - media_types_label = QLabel("Sichtbare Medientypen:") + media_types_label = QLabel(_t("Sichtbare Medientypen:")) g_layout.addWidget(media_types_label) enabled_media_types = config.config.get_enabled_media_types() self.media_type_checkboxes = {} for entry in config.MEDIA_TYPE_DEFINITIONS: media_type = entry["key"] - checkbox = QCheckBox(entry["label"]) + checkbox = QCheckBox(_t(entry["label"])) checkbox.setObjectName(f"mediaType_{media_type}") checkbox.setChecked(enabled_media_types.get(media_type, True)) checkbox.setToolTip( - f"{entry['label']} in der MediaBrain-Bibliotheks-Navigation anzeigen" + _t("{media_type} in der MediaBrain-Bibliotheks-Navigation anzeigen", media_type=_t(entry["label"])) ) checkbox.stateChanged.connect( lambda state, media_type=media_type: self._on_media_type_change(media_type, state) @@ -1148,7 +1245,7 @@ def on_tray_change(state): g_layout.addStretch() - tabs.addTab(general, "Allgemein") + tabs.addTab(general, _t("Allgemein")) # Provider provider = QWidget() @@ -1156,25 +1253,29 @@ def on_tray_change(state): provider.setLayout(p_layout) for name in config.config.get("providers", {}).keys(): - label = QLabel(f"{name} Öffnen mit:") + label = QLabel(_t('{name} Öffnen mit:', name=(name))) combo = QComboBox() - combo.addItems(["browser", "app", "local", "auto"]) - combo.setCurrentText(config.config.get(f"providers.{name}.preferred_open_method", "auto")) - combo.currentTextChanged.connect( - lambda v, n=name: config.config.set(f"providers.{n}.preferred_open_method", v) + for method in ("browser", "app", "local", "auto"): + combo.addItem(_t(method), method) + saved_method = config.config.get(f"providers.{name}.preferred_open_method", "auto") + combo.setCurrentIndex(combo.findData(saved_method)) + combo.currentIndexChanged.connect( + lambda index, n=name, c=combo: config.config.set( + f"providers.{n}.preferred_open_method", c.itemData(index) + ) ) p_layout.addWidget(label) p_layout.addWidget(combo) p_layout.addStretch() - tabs.addTab(provider, "Provider") + tabs.addTab(provider, _t("Provider")) # Sicherheit security = QWidget() s_layout = QVBoxLayout() security.setLayout(s_layout) - delete_checkbox = QCheckBox("Löschen lokaler Dateien erlauben") + delete_checkbox = QCheckBox(_t("Löschen lokaler Dateien erlauben")) delete_checkbox.setChecked(config.config.get("allow_file_deletion", False)) delete_checkbox.stateChanged.connect( lambda v: config.config.set("allow_file_deletion", bool(v)) @@ -1183,7 +1284,19 @@ def on_tray_change(state): s_layout.addWidget(delete_checkbox) s_layout.addStretch() - tabs.addTab(security, "Sicherheit") + tabs.addTab(security, _t("Sicherheit")) + + def _on_language_change(self, index): + language = self.language_select.itemData(index) + if language not in {code for code, _name in LANGUAGE_CHOICES}: + return + config.config.set("ui.language", language) + _show_message( + QMessageBox.Icon.Information, + self, + _t("Anzeigesprache gespeichert"), + _t("Die Änderung der Anzeigesprache wird nach einem Neustart wirksam."), + ) def _on_media_type_change(self, media_type, state): checked = state == Qt.CheckState.Checked or state == Qt.CheckState.Checked.value @@ -1201,8 +1314,11 @@ def _on_media_type_change(self, media_type, state): # In gui.py class DashboardView(QWidget): - def __init__(self, media_manager: MediaManager, blacklist_manager: BlacklistManager, open_settings_callback=None, open_blacklist_callback=None): + def __init__(self, media_manager: MediaManager, blacklist_manager: BlacklistManager, open_settings_callback=None, open_blacklist_callback=None, translator=None): super().__init__() + self.translator = translator + if translator is not None: + set_default_translation_system(translator) self.media_manager = media_manager self.blacklist_manager = blacklist_manager @@ -1218,23 +1334,23 @@ def __init__(self, media_manager: MediaManager, blacklist_manager: BlacklistMana self.setLayout(layout) # 1. Erweiterte Suche (ersetzt alte SearchBar) - self.search_bar = AdvancedSearchBar() + self.search_bar = AdvancedSearchBar(translator=getattr(self, "translator", None)) self.search_bar.search_triggered.connect(self.apply_search) layout.addWidget(self.search_bar) # 2. Quick Actions (Statisch - wird nicht neu geladen -> KEIN FLACKERN MEHR) actions_layout = QHBoxLayout() - btn_scan = QPushButton("Bibliothek scannen") + btn_scan = QPushButton(_t("Bibliothek scannen")) # btn_scan.clicked.connect(...) # Später Funktion einbauen actions_layout.addWidget(btn_scan) - btn_settings = QPushButton("Einstellungen") + btn_settings = QPushButton(_t("Einstellungen")) if self.open_settings_callback: btn_settings.clicked.connect(self.open_settings_callback) actions_layout.addWidget(btn_settings) - btn_bl = QPushButton("Blacklist anzeigen") + btn_bl = QPushButton(_t("Blacklist anzeigen")) if self.open_blacklist_callback: btn_bl.clicked.connect(self.open_blacklist_callback) actions_layout.addWidget(btn_bl) @@ -1270,7 +1386,7 @@ def refresh(self): self._init_container() # --- A. Favoriten --- - fav_label = QLabel("Favoriten") + fav_label = QLabel(_t("Favoriten")) fav_label.setObjectName("sectionHeader") self.container_layout.addWidget(fav_label) @@ -1279,13 +1395,13 @@ def refresh(self): if fav_items: for item in fav_items: - widget = MediaItemWidget(item, self.media_manager, self.blacklist_manager) + widget = MediaItemWidget(item, self.media_manager, self.blacklist_manager, translator=self.translator) self.container_layout.addWidget(widget) else: - self.container_layout.addWidget(QLabel("Keine Favoriten markiert.")) + self.container_layout.addWidget(QLabel(_t("Keine Favoriten markiert."))) # --- B. Zuletzt geöffnet --- - recent_label = QLabel("Zuletzt geöffnet") + recent_label = QLabel(_t("Zuletzt geöffnet")) recent_label.setObjectName("sectionHeader") self.container_layout.addWidget(recent_label) @@ -1293,14 +1409,14 @@ def refresh(self): if recent_items: for item in recent_items: - widget = MediaItemWidget(item, self.media_manager, self.blacklist_manager) + widget = MediaItemWidget(item, self.media_manager, self.blacklist_manager, translator=self.translator) self.container_layout.addWidget(widget) else: - self.container_layout.addWidget(QLabel("Noch keine Aktivitäten.")) + self.container_layout.addWidget(QLabel(_t("Noch keine Aktivitäten."))) self.container_layout.addStretch() except Exception as e: - self.container_layout.addWidget(QLabel(f"Fehler beim Laden des Dashboards: {e}")) + self.container_layout.addWidget(QLabel(_t('Fehler beim Laden des Dashboards: {error}', error=(e)))) def apply_search(self, criteria: SearchCriteria): """Führt erweiterte Suche basierend auf SearchCriteria aus.""" @@ -1329,16 +1445,16 @@ def apply_search(self, criteria: SearchCriteria): if criteria.text: filter_parts.append(f'"{criteria.text}"') if criteria.media_type: - filter_parts.append(f"Typ: {criteria.media_type}") + filter_parts.append(_t("Typ: {type}", type=_media_type_display(criteria.media_type))) if criteria.provider: - filter_parts.append(f"Provider: {criteria.provider}") + filter_parts.append(_t("Provider: {provider}", provider=_t(criteria.provider))) if criteria.favorites_only: filter_parts.append("⭐ Favoriten") if criteria.time_filter_days: - filter_parts.append(f"Letzte {criteria.time_filter_days} Tage") + filter_parts.append(_t("Letzte {days} Tage", days=criteria.time_filter_days)) search_info = " | ".join(filter_parts) if filter_parts else "Alle" - search_label = QLabel(f"Suche: {search_info}") + search_label = QLabel(_t('Suche: {filters}', filters=(search_info))) search_label.setObjectName("sectionHeader") self.container_layout.addWidget(search_label) @@ -1346,15 +1462,15 @@ def apply_search(self, criteria: SearchCriteria): results = self.search_engine.search(criteria) if results: - count_label = QLabel(f"{len(results)} Ergebnisse gefunden") + count_label = QLabel(_t('{count} Ergebnisse gefunden', count=(len(results)))) count_label.setObjectName("mutedLabel") self.container_layout.addWidget(count_label) for media_item in results: - widget = MediaItemWidget(media_item, self.media_manager, self.blacklist_manager) + widget = MediaItemWidget(media_item, self.media_manager, self.blacklist_manager, translator=self.translator) self.container_layout.addWidget(widget) else: - no_results = QLabel("Keine Treffer gefunden.") + no_results = QLabel(_t("Keine Treffer gefunden.")) no_results.setObjectName("mutedLabel") self.container_layout.addWidget(no_results) @@ -1369,8 +1485,11 @@ def apply_search(self, criteria: SearchCriteria): ) class BlacklistView(QWidget): - def __init__(self, media_manager: MediaManager, blacklist_manager: BlacklistManager): + def __init__(self, media_manager: MediaManager, blacklist_manager: BlacklistManager, translator=None): super().__init__() + self.translator = translator + if translator is not None: + set_default_translation_system(translator) self.media_manager = media_manager self.blacklist_manager = blacklist_manager @@ -1378,7 +1497,7 @@ def __init__(self, media_manager: MediaManager, blacklist_manager: BlacklistMana layout = QVBoxLayout() self.setLayout(layout) - title = QLabel("Blacklist-Verwaltung") + title = QLabel(_t("Blacklist-Verwaltung")) title.setObjectName("pageTitle") layout.addWidget(title) @@ -1388,25 +1507,25 @@ def __init__(self, media_manager: MediaManager, blacklist_manager: BlacklistMana filter_row = QHBoxLayout() self.provider_filter = QComboBox() - self.provider_filter.addItem("Alle Provider") - for p in ["netflix", "youtube", "spotify", "local"]: - self.provider_filter.addItem(p) - self.provider_filter.currentTextChanged.connect(self.refresh) + self.provider_filter.addItem(_t("Alle Provider"), None) + for provider in ["netflix", "youtube", "spotify", "local"]: + self.provider_filter.addItem(_t(provider), provider) + self.provider_filter.currentIndexChanged.connect(self.refresh) filter_row.addWidget(self.provider_filter) self.duration_filter = QComboBox() - self.duration_filter.addItem("Alle Dauern") - self.duration_filter.addItems([ - "1 Tag", "1 Woche", "1 Monat", "3 Monate", "1 Jahr", "Für immer" - ]) - self.duration_filter.currentTextChanged.connect(self.refresh) + self.duration_filter.addItem(_t("Alle Dauern"), None) + for label, code in (("1 Tag", 1), ("1 Woche", 2), ("1 Monat", 3), + ("3 Monate", 4), ("1 Jahr", 5), ("Für immer", 6)): + self.duration_filter.addItem(_t(label), code) + self.duration_filter.currentIndexChanged.connect(self.refresh) filter_row.addWidget(self.duration_filter) self.expiry_filter = QComboBox() - self.expiry_filter.addItem("Alle") - self.expiry_filter.addItem("Nur abgelaufen") - self.expiry_filter.addItem("Nur aktiv") - self.expiry_filter.currentTextChanged.connect(self.refresh) + self.expiry_filter.addItem(_t("Alle"), None) + self.expiry_filter.addItem(_t("Nur abgelaufen"), "expired") + self.expiry_filter.addItem(_t("Nur aktiv"), "active") + self.expiry_filter.currentIndexChanged.connect(self.refresh) filter_row.addWidget(self.expiry_filter) layout.addLayout(filter_row) @@ -1416,11 +1535,11 @@ def __init__(self, media_manager: MediaManager, blacklist_manager: BlacklistMana # ------------------------------------------------------- btn_row = QHBoxLayout() - btn_clear_expired = QPushButton("Abgelaufene entfernen") + btn_clear_expired = QPushButton(_t("Abgelaufene entfernen")) btn_clear_expired.clicked.connect(self._remove_expired) btn_row.addWidget(btn_clear_expired) - btn_clear_all = QPushButton("Alle entfernen") + btn_clear_all = QPushButton(_t("Alle entfernen")) btn_clear_all.clicked.connect(self._remove_all) btn_row.addWidget(btn_clear_all) @@ -1477,18 +1596,9 @@ def refresh(self): self._init_container() # Filter-Werte ermitteln - provider_filter = self.provider_filter.currentText() - duration_filter = self.duration_filter.currentText() - expiry_filter = self.expiry_filter.currentText() - - # SQL-Filter für Provider und Dauer (statt Python-seitiger Filterung) - source = None if provider_filter == "Alle Provider" else provider_filter - - code_map = { - "1 Tag": 1, "1 Woche": 2, "1 Monat": 3, - "3 Monate": 4, "1 Jahr": 5, "Für immer": 6 - } - procedure_code = code_map.get(duration_filter) + source = self.provider_filter.currentData() + procedure_code = self.duration_filter.currentData() + expiry_filter = self.expiry_filter.currentData() # Optimiert: Filter in SQL statt Python-Iteration über alle Items items = self.media_manager.list_blacklisted( @@ -1507,9 +1617,9 @@ def refresh(self): expired = False expiry = None - if expiry_filter == "Nur abgelaufen" and not expired: + if expiry_filter == "expired" and not expired: continue - if expiry_filter == "Nur aktiv" and expired: + if expiry_filter == "active" and expired: continue # Widget erzeugen @@ -1531,29 +1641,29 @@ def _create_blacklist_widget(self, item: MediaItem, expired: bool, expiry): title.setObjectName("sectionHeader") layout.addWidget(title) - layout.addWidget(QLabel(f"Sperrcode: {item.procedure_code}")) - layout.addWidget(QLabel(f"Seit: {item.blacklisted_at}")) + layout.addWidget(QLabel(_t('Sperrcode: {code}', code=(item.procedure_code)))) + layout.addWidget(QLabel(_t('Seit: {date}', date=(item.blacklisted_at)))) if expiry: - layout.addWidget(QLabel(f"Ablaufdatum: {expiry}")) + layout.addWidget(QLabel(_t('Ablaufdatum: {expiry}', expiry=(expiry)))) else: - layout.addWidget(QLabel("Ablaufdatum: Nie")) + layout.addWidget(QLabel(_t("Ablaufdatum: Nie"))) if expired: - expired_label = QLabel("Status: Abgelaufen") + expired_label = QLabel(_t("Status: Abgelaufen")) expired_label.setObjectName("dangerLabel") layout.addWidget(expired_label) else: - layout.addWidget(QLabel("Status: Aktiv")) + layout.addWidget(QLabel(_t("Status: Aktiv"))) # Buttons btn_row = QHBoxLayout() - btn_remove = QPushButton("Entfernen") + btn_remove = QPushButton(_t("Entfernen")) btn_remove.clicked.connect(lambda: self._remove_single(item.id)) btn_row.addWidget(btn_remove) - btn_change = QPushButton("Dauer ändern") + btn_change = QPushButton(_t("Dauer ändern")) btn_change.clicked.connect(lambda: self._change_duration(item.id)) btn_row.addWidget(btn_change) @@ -1571,7 +1681,7 @@ def _remove_single(self, item_id): self.refresh() except Exception as e: from PySide6.QtWidgets import QMessageBox - QMessageBox.warning(self, "Fehler", f"Blacklist-Eintrag konnte nicht entfernt werden: {e}") + _show_message(QMessageBox.Icon.Warning, self, _t("Fehler"), _t('Blacklist-Eintrag konnte nicht entfernt werden: {error}', error=(e))) def _change_duration(self, item_id): try: @@ -1579,7 +1689,7 @@ def _change_duration(self, item_id): self.refresh() except Exception as e: from PySide6.QtWidgets import QMessageBox - QMessageBox.warning(self, "Fehler", f"Dauer konnte nicht geändert werden: {e}") + _show_message(QMessageBox.Icon.Warning, self, _t("Fehler"), _t('Dauer konnte nicht geändert werden: {error}', error=(e))) def _remove_expired(self): """Entfernt alle abgelaufenen Blacklist-Einträge in einem Batch-UPDATE.""" @@ -1611,7 +1721,7 @@ def _remove_expired(self): self.refresh() except Exception as e: from PySide6.QtWidgets import QMessageBox - QMessageBox.warning(self, "Fehler", f"Abgelaufene konnten nicht entfernt werden: {e}") + _show_message(QMessageBox.Icon.Warning, self, _t("Fehler"), _t('Abgelaufene konnten nicht entfernt werden: {error}', error=(e))) def _remove_all(self): try: @@ -1625,13 +1735,16 @@ def _remove_all(self): self.refresh() except Exception as e: from PySide6.QtWidgets import QMessageBox - QMessageBox.warning(self, "Fehler", f"Blacklist konnte nicht geleert werden: {e}") + _show_message(QMessageBox.Icon.Warning, self, _t("Fehler"), _t('Blacklist konnte nicht geleert werden: {error}', error=(e))) class MediaDetailView(QWidget): online_metadata_loaded = Signal(object) - def __init__(self, item: MediaItem, media_manager: MediaManager, blacklist_manager: BlacklistManager, back_callback): + def __init__(self, item: MediaItem, media_manager: MediaManager, blacklist_manager: BlacklistManager, back_callback, translator=None): super().__init__() + self.translator = translator + if translator is not None: + set_default_translation_system(translator) self.item = item self.media_manager = media_manager @@ -1648,33 +1761,33 @@ def __init__(self, item: MediaItem, media_manager: MediaManager, blacklist_manag # Thumbnail if item.thumbnail_url: - thumb = QLabel(f"[Thumbnail: {item.thumbnail_url}]") + thumb = QLabel(_t('[Thumbnail: {url}]', url=(item.thumbnail_url))) layout.addWidget(thumb) # Beschreibung - desc = QLabel(item.description or "Keine Beschreibung verfügbar.") + desc = QLabel(item.description or _t("Keine Beschreibung verfügbar.")) desc.setWordWrap(True) layout.addWidget(desc) # Metadaten meta = QLabel( - f"Typ: {item.type}\n" - f"Provider: {item.source}\n" - f"Provider-ID: {item.provider_id}\n" - f"Erstellt am: {item.created_at}\n" - f"Zuletzt geöffnet: {item.last_opened_at}\n" - f"Öffnungsmethode: {item.open_method or '-'}" + _t("Typ: {type}", type=_media_type_display(item.type)) + "\n" + + _t("Provider: {provider}", provider=item.source) + "\n" + + _t("Provider-ID: {provider_id}", provider_id=item.provider_id) + "\n" + + _t("Erstellt am: {created_at}", created_at=item.created_at) + "\n" + + _t("Zuletzt geöffnet: {last_opened_at}", last_opened_at=item.last_opened_at) + "\n" + + _t("Öffnungsmethode: {open_method}", open_method=_t(item.open_method or "-")) ) layout.addWidget(meta) # Online-Metadaten - online_label = QLabel("Online-Metadaten:") + online_label = QLabel(_t("Online-Metadaten:")) online_label.setObjectName("sectionHeader") layout.addWidget(online_label) self.online_meta_container = QVBoxLayout() self.online_meta_container.setContentsMargins(0, 0, 0, 0) - self.online_meta_status = QLabel("Lade Online-Metadaten...") + self.online_meta_status = QLabel(_t("Lade Online-Metadaten...")) self.online_meta_status.setWordWrap(True) self.online_meta_status.setStyleSheet("color: #888; padding-left: 10px;") self.online_meta_container.addWidget(self.online_meta_status) @@ -1683,7 +1796,7 @@ def __init__(self, item: MediaItem, media_manager: MediaManager, blacklist_manag # Buttons btn_row = QHBoxLayout() - open_btn = QPushButton("Öffnen") + open_btn = QPushButton(_t("Öffnen")) open_btn.clicked.connect(self.open_item) btn_row.addWidget(open_btn) @@ -1691,11 +1804,11 @@ def __init__(self, item: MediaItem, media_manager: MediaManager, blacklist_manag fav_btn.clicked.connect(self.toggle_favorite) btn_row.addWidget(fav_btn) - refresh_btn = QPushButton("Metadaten neu laden") + refresh_btn = QPushButton(_t("Metadaten neu laden")) refresh_btn.clicked.connect(self._load_online_metadata) btn_row.addWidget(refresh_btn) - back_btn = QPushButton("Zurück") + back_btn = QPushButton(_t("Zurück")) back_btn.clicked.connect(self.back_callback) btn_row.addWidget(back_btn) @@ -1731,7 +1844,7 @@ def fetch(): pass # widget destroyed while fetch was in flight self._clear_online_metadata() - self.online_meta_status = QLabel("Lade Online-Metadaten...") + self.online_meta_status = QLabel(_t("Lade Online-Metadaten...")) self.online_meta_status.setWordWrap(True) self.online_meta_status.setStyleSheet("color: #888; padding-left: 10px;") self.online_meta_container.addWidget(self.online_meta_status) @@ -1741,7 +1854,7 @@ def _update_online_metadata(self, result): """Aktualisiert die Online-Metadaten-Anzeige.""" self._clear_online_metadata() - lines, description, message = format_online_metadata(result, self.item.description) + lines, description, message = format_online_metadata(result, self.item.description, self.translator) if message: label = QLabel(message) @@ -1758,7 +1871,7 @@ def _update_online_metadata(self, result): self.online_meta_container.addWidget(label) if description: - desc_label = QLabel("Online-Beschreibung:") + desc_label = QLabel(_t("Online-Beschreibung:")) desc_label.setStyleSheet("font-weight: bold; margin-top: 10px; padding-left: 10px;") self.online_meta_container.addWidget(desc_label) @@ -1768,7 +1881,7 @@ def _update_online_metadata(self, result): self.online_meta_container.addWidget(desc) if not lines and not description: - label = QLabel("Online-Metadaten gefunden, aber keine Felder zum Anzeigen.") + label = QLabel(_t("Online-Metadaten gefunden, aber keine Felder zum Anzeigen.")) label.setWordWrap(True) label.setStyleSheet("padding-left: 10px; color: #888;") self.online_meta_container.addWidget(label) @@ -1780,7 +1893,7 @@ def open_item(self): handler.open_item(self.item) except Exception as e: from PySide6.QtWidgets import QMessageBox - QMessageBox.warning(self, "Fehler", f"Konnte Medium nicht öffnen: {e}") + _show_message(QMessageBox.Icon.Warning, self, _t("Fehler"), _t('Konnte Medium nicht öffnen: {error}', error=(e))) def toggle_favorite(self): try: @@ -1792,7 +1905,7 @@ def toggle_favorite(self): notify_gui_refresh() except Exception as e: from PySide6.QtWidgets import QMessageBox - QMessageBox.warning(self, "Fehler", f"Favorit konnte nicht geändert werden: {e}") + _show_message(QMessageBox.Icon.Warning, self, _t("Fehler"), _t('Favorit konnte nicht geändert werden: {error}', error=(e))) # ============================================================ # 8. Statistik-View # ============================================================ @@ -1807,8 +1920,11 @@ class StatsView(QWidget): - Top 5 zuletzt hinzugefügte Medien """ - def __init__(self, media_manager: MediaManager, parent=None): + def __init__(self, media_manager: MediaManager, parent=None, translator=None): super().__init__(parent) + self.translator = translator + if translator is not None: + set_default_translation_system(translator) self.media_manager = media_manager self._needs_refresh = True self._build_ui() @@ -1819,30 +1935,30 @@ def _build_ui(self): layout.setContentsMargins(20, 20, 20, 20) layout.setSpacing(16) - header = QLabel("

📊 Statistiken & Dashboard

") + header = QLabel(_t("

📊 Statistiken & Dashboard

")) layout.addWidget(header) # --- Kennzahlen-Grid --- - self.lbl_total = QLabel("Gesamtanzahl Medien: --") + self.lbl_total = QLabel(_t("Gesamtanzahl Medien: --")) self.lbl_total.setObjectName("sectionHeader") layout.addWidget(self.lbl_total) - self.lbl_storage = QLabel("Geschätzter Speicher (lokal): –") + self.lbl_storage = QLabel(_t("Geschätzter Speicher (lokal): –")) layout.addWidget(self.lbl_storage) # --- Medien pro Typ --- - layout.addWidget(QLabel("Medien nach Typ:")) + layout.addWidget(QLabel(_t("Medien nach Typ:"))) self.type_list = QListWidget() self.type_list.setMaximumHeight(160) layout.addWidget(self.type_list) # --- Zuletzt hinzugefuegt --- - layout.addWidget(QLabel("Zuletzt hinzugefügt (Top 5):")) + layout.addWidget(QLabel(_t("Zuletzt hinzugefügt (Top 5):"))) self.recent_list = QListWidget() self.recent_list.setMaximumHeight(150) layout.addWidget(self.recent_list) - btn_refresh = QPushButton("🔄 Aktualisieren") + btn_refresh = QPushButton(_t("🔄 Aktualisieren")) btn_refresh.clicked.connect(self.refresh) layout.addWidget(btn_refresh) @@ -1859,7 +1975,7 @@ def refresh(self): "SELECT COUNT(*) AS cnt FROM media_items WHERE blacklist_flag = 0" ).fetchone() total = total_row["cnt"] if total_row else 0 - self.lbl_total.setText(f"Gesamtanzahl Medien: {total}") + self.lbl_total.setText(_t('Gesamtanzahl Medien: {count}', count=(total))) # 2. Medien pro Typ type_rows = db.conn.execute( @@ -1872,7 +1988,7 @@ def refresh(self): } for row in type_rows: icon = type_icons.get(row["type"], "▪") - self.type_list.addItem(f" {icon} {row['type'].capitalize()}: {row['cnt']}") + self.type_list.addItem(_t(' {icon} {type}: {count}', icon=(icon), type=(_media_type_display(row["type"])), count=(row['cnt']))) # 3. Speicherverbrauch lokaler Dateien local_rows = db.conn.execute( @@ -1887,9 +2003,9 @@ def refresh(self): pass if total_bytes > 0: total_gb = total_bytes / (1024 ** 3) - self.lbl_storage.setText(f"Geschätzter Speicher (lokal): {total_gb:.2f} GB ({len(local_rows)} Dateien)") + self.lbl_storage.setText(_t("Geschätzter Speicher (lokal): {size_gb} GB ({file_count} Dateien)", size_gb=f"{total_gb:.2f}", file_count=len(local_rows))) else: - self.lbl_storage.setText("Geschätzter Speicher (lokal): Keine lokalen Dateien erfasst") + self.lbl_storage.setText(_t("Geschätzter Speicher (lokal): Keine lokalen Dateien erfasst")) # 4. Zuletzt hinzugefuegt (Top 5) recent_rows = db.conn.execute( @@ -1898,7 +2014,7 @@ def refresh(self): self.recent_list.clear() for r in recent_rows: date_str = (r["created_at"] or "")[:10] - self.recent_list.addItem(f" {date_str} [{r['type']}] {r['title']}") + self.recent_list.addItem(_t(' {date} [{type}] {title}', date=(date_str), type=(_media_type_display(r["type"])), title=(r['title']))) except Exception as e: logger.error(f"StatsView.refresh() Fehler: {e}") @@ -1913,8 +2029,11 @@ def refresh(self): class MainWindow(QMainWindow): def __init__(self, media_manager: MediaManager, blacklist_manager: BlacklistManager, tag_manager: TagManager = None, - playlist_manager: PlaylistManager = None): + playlist_manager: PlaylistManager = None, translator=None): super().__init__() + self.translator = translator + if translator is not None: + set_default_translation_system(translator) self.media_manager = media_manager self.blacklist_manager = blacklist_manager @@ -1943,45 +2062,45 @@ def __init__(self, media_manager: MediaManager, blacklist_manager: BlacklistMana sidebar.setLayout(sidebar_layout) sidebar.setObjectName("Sidebar") # Für CSS Styling - btn_dash = QPushButton("Übersicht") + btn_dash = QPushButton(_t("Übersicht")) btn_dash.clicked.connect(lambda: self._switch_view(self.dashboard)) sidebar_layout.addWidget(btn_dash) for entry in config.MEDIA_TYPE_DEFINITIONS: media_type = entry["key"] - button = QPushButton(entry["label"]) + button = QPushButton(_t(entry["label"])) button.clicked.connect( lambda checked=False, media_type=media_type: self._switch_library_type(media_type) ) self.library_buttons[media_type] = button sidebar_layout.addWidget(button) - btn_favs = QPushButton("Favoriten") + btn_favs = QPushButton(_t("Favoriten")) btn_favs.clicked.connect(lambda: self._switch_view(self.favorites)) sidebar_layout.addWidget(btn_favs) - btn_blacklist = QPushButton("Blacklist") + btn_blacklist = QPushButton(_t("Blacklist")) btn_blacklist.clicked.connect(lambda: self._switch_view(self.blacklist_view)) sidebar_layout.addWidget(btn_blacklist) - btn_stats = QPushButton("📊 Statistiken") + btn_stats = QPushButton(_t("📊 Statistiken")) btn_stats.clicked.connect(lambda: self._switch_view(self.stats_view)) sidebar_layout.addWidget(btn_stats) if self.playlist_manager is not None: - btn_playlists = QPushButton("Playlists") + btn_playlists = QPushButton(_t("Playlists")) btn_playlists.clicked.connect(lambda: self._switch_view(self.playlists_view)) sidebar_layout.addWidget(btn_playlists) - btn_export = QPushButton("Export (JSON)") + btn_export = QPushButton(_t("Export (JSON)")) btn_export.clicked.connect(self._export_library_json) sidebar_layout.addWidget(btn_export) - btn_import = QPushButton("Import (JSON)") + btn_import = QPushButton(_t("Import (JSON)")) btn_import.clicked.connect(self._import_library_json) sidebar_layout.addWidget(btn_import) - btn_settings = QPushButton("Einstellungen") + btn_settings = QPushButton(_t("Einstellungen")) btn_settings.clicked.connect(self.open_settings) sidebar_layout.addWidget(btn_settings) @@ -2000,14 +2119,15 @@ def __init__(self, media_manager: MediaManager, blacklist_manager: BlacklistMana media_manager, blacklist_manager, open_settings_callback=self.open_settings, - open_blacklist_callback=lambda: self.stack.setCurrentWidget(self.blacklist_view) + open_blacklist_callback=lambda: self.stack.setCurrentWidget(self.blacklist_view), + translator=self.translator, ) self.stack.addWidget(self.dashboard) # 2. Libraries for entry in config.MEDIA_TYPE_DEFINITIONS: media_type = entry["key"] - view = LibraryView(media_type, media_manager, blacklist_manager) + view = LibraryView(media_type, media_manager, blacklist_manager, translator=self.translator) self.library_views[media_type] = view self.stack.addWidget(view) @@ -2017,26 +2137,27 @@ def __init__(self, media_manager: MediaManager, blacklist_manager: BlacklistMana self.library_clips = self.library_views["clip"] # 3. Favoriten - self.favorites = FavoritesView(media_manager, blacklist_manager) + self.favorites = FavoritesView(media_manager, blacklist_manager, translator=self.translator) self.stack.addWidget(self.favorites) # 4. Blacklist (Korrigiert: Echte View statt QLabel) - self.blacklist_view = BlacklistView(media_manager, blacklist_manager) + self.blacklist_view = BlacklistView(media_manager, blacklist_manager, translator=self.translator) self.stack.addWidget(self.blacklist_view) # 5. Statistiken - self.stats_view = StatsView(media_manager) + self.stats_view = StatsView(media_manager, translator=self.translator) self.stack.addWidget(self.stats_view) # 6. Playlists (optional - only when a PlaylistManager was injected) self.playlists_view = None if self.playlist_manager is not None: self.playlists_view = PlaylistsView(self.playlist_manager, - media_manager=media_manager) + media_manager=media_manager, + translator=self.translator) self.stack.addWidget(self.playlists_view) # 7. Suche - self.global_search = GlobalSearchView(media_manager, blacklist_manager) + self.global_search = GlobalSearchView(media_manager, blacklist_manager, translator=self.translator) self.stack.addWidget(self.global_search) # Theme anwenden @@ -2061,7 +2182,8 @@ def open_detail(self, item): item, self.media_manager, self.blacklist_manager, - back_callback=lambda: self.stack.setCurrentWidget(self.dashboard) + back_callback=lambda: self.stack.setCurrentWidget(self.dashboard), + translator=self.translator, ) self.stack.addWidget(self.detail_view) self.stack.setCurrentWidget(self.detail_view) @@ -2092,7 +2214,7 @@ def refresh_all_views(self): current._needs_refresh = False except Exception as e: from PySide6.QtWidgets import QMessageBox - QMessageBox.warning(self, "Fehler", f"GUI konnte nicht vollständig aktualisiert werden: {e}") + _show_message(QMessageBox.Icon.Warning, self, _t("Fehler"), _t('GUI konnte nicht vollständig aktualisiert werden: {error}', error=(e))) def _switch_view(self, view): """Wechselt zur gewünschten View und refreshed sie, falls sie dirty ist.""" @@ -2119,9 +2241,9 @@ def apply_media_type_visibility(self): def _export_library_json(self): path, _ = QFileDialog.getSaveFileName( self, - "MediaBrain Export speichern", + _t("MediaBrain Export speichern"), "", - "JSON-Datei (*.json)" + _t("JSON-Datei (*.json)") ) if not path: return @@ -2130,30 +2252,30 @@ def _export_library_json(self): try: exporter = MediaExporter(self.media_manager.db.conn) count = exporter.export_json(path, include_tags=True, include_playlists=True) - QMessageBox.information( + _show_message(QMessageBox.Icon.Information, self, - "Export abgeschlossen", - f"{count} Medien exportiert nach:\n{path}" + _t("Export abgeschlossen"), + _t('{count} Medien exportiert nach:\n{path}', count=(count), path=(path)) ) except Exception as e: - QMessageBox.warning(self, "Export fehlgeschlagen", str(e)) + _show_message(QMessageBox.Icon.Warning, self, _t("Export fehlgeschlagen"), str(e)) def _import_library_json(self): path, _ = QFileDialog.getOpenFileName( self, - "MediaBrain Export laden", + _t("MediaBrain Export laden"), "", - "JSON-Datei (*.json)" + _t("JSON-Datei (*.json)") ) if not path: return - reply = QMessageBox.question( + reply = _show_message(QMessageBox.Icon.Question, self, - "Import-Modus", + _t("Import-Modus"), ( - "Bestehende Einträge mit gleicher Quelle + Provider-ID überspringen?\n" - "Nein = vorhandene Datensätze ersetzen." + _t("Bestehende Einträge mit gleicher Quelle + Provider-ID überspringen?\n" + "Nein = vorhandene Datensätze ersetzen.") ), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.Yes @@ -2164,20 +2286,18 @@ def _import_library_json(self): importer = MediaImporter(self.media_manager.db.conn) stats = importer.import_json(path, merge=merge) self.refresh_all_views() - QMessageBox.information( + _show_message(QMessageBox.Icon.Information, self, - "Import abgeschlossen", + _t("Import abgeschlossen"), ( - f"Importiert: {stats.get('imported', 0)}\n" - f"Übersprungen: {stats.get('skipped', 0)}\n" - f"Fehler: {stats.get('errors', 0)}" + _t('Importiert: {imported}\nÜbersprungen: {skipped}\nFehler: {errors}', imported=(stats.get('imported', 0)), skipped=(stats.get('skipped', 0)), errors=(stats.get('errors', 0))) ) ) except Exception as e: - QMessageBox.warning(self, "Import fehlgeschlagen", str(e)) + _show_message(QMessageBox.Icon.Warning, self, _t("Import fehlgeschlagen"), str(e)) def open_settings(self): - self.settings_window = SettingsWindow() + self.settings_window = SettingsWindow(self.translator) self.settings_window.show() def _setup_system_tray(self): @@ -2204,13 +2324,13 @@ def _setup_system_tray(self): # Kontextmenu tray_menu = QMenu(self) - act_show = QAction("MediaBrain öffnen", self) + act_show = QAction(_t("MediaBrain öffnen"), self) act_show.triggered.connect(self._restore_from_tray) tray_menu.addAction(act_show) tray_menu.addSeparator() - act_quit = QAction("Beenden", self) + act_quit = QAction(_t("Beenden"), self) act_quit.triggered.connect(self._quit_app) tray_menu.addAction(act_quit) @@ -2255,7 +2375,7 @@ def closeEvent(self, event): self.hide() self.tray_icon.showMessage( "MediaBrain", - "MediaBrain läuft im Hintergrund weiter.\nDoppelklick auf das Tray-Icon zum öffnen.", + _t("MediaBrain läuft im Hintergrund weiter.\nDoppelklick auf das Tray-Icon zum öffnen."), QSystemTrayIcon.MessageIcon.Information, 2000 ) diff --git a/gui_playlists.py b/gui_playlists.py index 5c45ffe..f67f81b 100644 --- a/gui_playlists.py +++ b/gui_playlists.py @@ -26,10 +26,49 @@ from playlists import Playlist, PlaylistManager from query_builder import FilterCondition, QueryBuilder +from translator import translate as _t, set_default_translation_system logger = logging.getLogger("MediaBrain.GUIPlaylists") +def _set_dialog_buttons(box): + for standard, label in ( + (QDialogButtonBox.StandardButton.Ok, "OK"), + (QDialogButtonBox.StandardButton.Cancel, "Abbrechen"), + (QDialogButtonBox.StandardButton.Save, "Speichern"), + ): + button = box.button(standard) + if button is not None: + button.setText(_t(label)) + + +def _show_message(icon, parent, title, text, *args): + buttons = args[0] if args else ( + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + if icon == QMessageBox.Icon.Question else QMessageBox.StandardButton.Ok + ) + box = QMessageBox(icon, title, text, buttons, parent) + for standard, label in ((QMessageBox.StandardButton.Ok, "OK"), + (QMessageBox.StandardButton.Cancel, "Abbrechen"), + (QMessageBox.StandardButton.Yes, "Ja"), + (QMessageBox.StandardButton.No, "Nein")): + button = box.button(standard) + if button is not None: + button.setText(_t(label)) + if len(args) > 1 and args[1] != QMessageBox.StandardButton.NoButton: + box.setDefaultButton(args[1]) + elif icon == QMessageBox.Icon.Question: + box.setDefaultButton(QMessageBox.StandardButton.No) + return box.exec() + + +def _display_field_value(value): + labels = {"movie": "Filme", "series": "Serien", "music": "Musik", + "clip": "Clips", "podcast": "Podcasts", + "audiobook": "Hörbücher", "document": "Dokumente"} + return _t(labels[value]) if value in labels else str(value) + + # ============================================================ # Field metadata used by the dialog UI # ============================================================ @@ -87,8 +126,11 @@ class ConditionRow(QFrame): """A single editable filter condition.""" - def __init__(self, on_remove=None, parent=None): + def __init__(self, on_remove=None, parent=None, translator=None): super().__init__(parent) + self.translator = translator + if translator is not None: + set_default_translation_system(translator) self.setObjectName("ConditionRow") self.setFrameShape(QFrame.Shape.NoFrame) self._on_remove = on_remove @@ -99,18 +141,19 @@ def __init__(self, on_remove=None, parent=None): self.setLayout(layout) self.conjunction_combo = QComboBox() - self.conjunction_combo.addItems(["AND", "OR"]) + self.conjunction_combo.addItem(_t("AND"), "AND") + self.conjunction_combo.addItem(_t("OR"), "OR") self.conjunction_combo.setFixedWidth(60) layout.addWidget(self.conjunction_combo) self.field_combo = QComboBox() for key, label, *_ in FIELD_DEFS: - self.field_combo.addItem(label, key) + self.field_combo.addItem(_t(label), key) self.field_combo.currentIndexChanged.connect(self._on_field_changed) layout.addWidget(self.field_combo, stretch=2) self.operator_combo = QComboBox() - self.operator_combo.currentTextChanged.connect(self._on_operator_changed) + self.operator_combo.currentIndexChanged.connect(lambda _index: self._on_operator_changed(self.operator_combo.currentData())) layout.addWidget(self.operator_combo, stretch=2) # Value placeholder, replaced when field/operator changes. @@ -118,10 +161,10 @@ def __init__(self, on_remove=None, parent=None): layout.addWidget(self.value_widget, stretch=3) self.remove_btn = QPushButton("✕") - self.remove_btn.setToolTip("Bedingung entfernen") - self.remove_btn.setAccessibleName("Bedingung entfernen") + self.remove_btn.setToolTip(_t("Bedingung entfernen")) + self.remove_btn.setAccessibleName(_t("Bedingung entfernen")) self.remove_btn.setAccessibleDescription( - "Entfernt diese Filterbedingung aus der Smart-Playlist." + _t("Entfernt diese Filterbedingung aus der Smart-Playlist.") ) self.remove_btn.setFixedWidth(28) self.remove_btn.clicked.connect(self._handle_remove) @@ -141,16 +184,15 @@ def _on_field_changed(self, _index): return _, _, ops, _kind, _presets = defn - prev = self.operator_combo.currentText() + prev = self.operator_combo.currentData() self.operator_combo.blockSignals(True) self.operator_combo.clear() - self.operator_combo.addItems(ops) - if prev in ops: - self.operator_combo.setCurrentText(prev) - else: - self.operator_combo.setCurrentIndex(0) + for operator in ops: + self.operator_combo.addItem(_t(operator), operator) + previous_index = self.operator_combo.findData(prev) + self.operator_combo.setCurrentIndex(previous_index if previous_index >= 0 else 0) self.operator_combo.blockSignals(False) - self._on_operator_changed(self.operator_combo.currentText()) + self._on_operator_changed(self.operator_combo.currentData()) def _on_operator_changed(self, op): self._rebuild_value_widget(op) @@ -169,12 +211,12 @@ def _rebuild_value_widget(self, op): presets = defn[4] if defn else None if op in ("is_empty", "is_not_empty"): - widget = QLabel("(kein Wert)") + widget = QLabel(_t("(kein Wert)")) widget.setEnabled(False) elif kind == "bool": widget = QComboBox() - widget.addItem("Ja", True) - widget.addItem("Nein", False) + widget.addItem(_t("Ja"), True) + widget.addItem(_t("Nein"), False) elif kind == "number": widget = QSpinBox() widget.setRange(-1_000_000, 1_000_000_000) @@ -182,7 +224,14 @@ def _rebuild_value_widget(self, op): elif kind == "combo" and presets: widget = QComboBox() widget.setEditable(True) - widget.addItems(presets) + media_labels = { + "movie": "Filme", "series": "Serien", "music": "Musik", + "clip": "Clips", "podcast": "Podcasts", + "audiobook": "Hörbücher", "document": "Dokumente", + } + for preset in presets: + label = _t(media_labels[preset]) if preset in media_labels else _t(preset) + widget.addItem(label, preset) else: widget = QLineEdit() @@ -198,8 +247,8 @@ def _rebuild_value_widget(self, op): def to_condition(self) -> FilterCondition: field = self.field_combo.currentData() - op = self.operator_combo.currentText() - conjunction = self.conjunction_combo.currentText() + op = self.operator_combo.currentData() + conjunction = self.conjunction_combo.currentData() value = self._value() return FilterCondition(field=field, operator=op, value=value, conjunction=conjunction) @@ -211,6 +260,11 @@ def _value(self): if isinstance(widget, QSpinBox): return widget.value() if isinstance(widget, QComboBox): + if widget.isEditable(): + index = widget.currentIndex() + if index >= 0 and widget.currentText() == widget.itemText(index): + return widget.itemData(index) + return widget.currentText() data = widget.currentData() if data is not None: return data @@ -219,7 +273,7 @@ def _value(self): def load_condition(self, condition: FilterCondition): # Conjunction - idx = self.conjunction_combo.findText(condition.conjunction or "AND") + idx = self.conjunction_combo.findData(condition.conjunction or "AND") if idx >= 0: self.conjunction_combo.setCurrentIndex(idx) @@ -233,12 +287,12 @@ def load_condition(self, condition: FilterCondition): self._on_field_changed(idx) # Operator - idx = self.operator_combo.findText(condition.operator) + idx = self.operator_combo.findData(condition.operator) if idx >= 0: self.operator_combo.blockSignals(True) self.operator_combo.setCurrentIndex(idx) self.operator_combo.blockSignals(False) - self._rebuild_value_widget(condition.operator) + self._rebuild_value_widget(self.operator_combo.currentData()) # Value widget = self.value_widget @@ -254,7 +308,7 @@ def load_condition(self, condition: FilterCondition): if isinstance(value, bool): widget.setCurrentIndex(0 if value else 1) else: - idx = widget.findText(str(value)) + idx = widget.findData(value) if idx >= 0: widget.setCurrentIndex(idx) elif widget.isEditable(): @@ -275,9 +329,12 @@ class SmartPlaylistDialog(QDialog): """ def __init__(self, parent=None, *, name: str = "", description: str = "", - smart_query_json: str = ""): + smart_query_json: str = "", translator=None): super().__init__(parent) - self.setWindowTitle("Smart-Playlist") + self.translator = translator + if translator is not None: + set_default_translation_system(translator) + self.setWindowTitle(_t("Smart-Playlist")) self.resize(720, 480) outer = QVBoxLayout() @@ -285,16 +342,16 @@ def __init__(self, parent=None, *, name: str = "", description: str = "", form = QFormLayout() self.name_edit = QLineEdit(name) - self.name_edit.setPlaceholderText("Name der Playlist") - form.addRow("Name", self.name_edit) + self.name_edit.setPlaceholderText(_t("Name der Playlist")) + form.addRow(_t("Name"), self.name_edit) self.desc_edit = QLineEdit(description) - self.desc_edit.setPlaceholderText("Optionale Beschreibung") - form.addRow("Beschreibung", self.desc_edit) + self.desc_edit.setPlaceholderText(_t("Optionale Beschreibung")) + form.addRow(_t("Beschreibung"), self.desc_edit) outer.addLayout(form) # Conditions list inside a scroll area. - outer.addWidget(QLabel("Bedingungen:")) + outer.addWidget(QLabel(_t("Bedingungen:"))) self.conditions_container = QWidget() self.conditions_layout = QVBoxLayout() self.conditions_layout.setContentsMargins(0, 0, 0, 0) @@ -306,28 +363,28 @@ def __init__(self, parent=None, *, name: str = "", description: str = "", scroll.setWidget(self.conditions_container) outer.addWidget(scroll, stretch=1) - add_row_btn = QPushButton("+ Bedingung hinzufügen") + add_row_btn = QPushButton(_t("+ Bedingung hinzufügen")) add_row_btn.clicked.connect(self._add_condition_row) outer.addWidget(add_row_btn, alignment=Qt.AlignmentFlag.AlignLeft) # Order + limit row. order_form = QFormLayout() self.order_combo = QComboBox() - self.order_combo.addItem("(keine Sortierung)", "") + self.order_combo.addItem(_t("(keine Sortierung)"), "") for key, label in _ORDER_FIELDS: - self.order_combo.addItem(label, key) - order_form.addRow("Sortieren nach", self.order_combo) + self.order_combo.addItem(_t(label), key) + order_form.addRow(_t("Sortieren nach"), self.order_combo) self.order_dir_combo = QComboBox() - self.order_dir_combo.addItem("Aufsteigend", "ASC") - self.order_dir_combo.addItem("Absteigend", "DESC") - order_form.addRow("Richtung", self.order_dir_combo) + self.order_dir_combo.addItem(_t("Aufsteigend"), "ASC") + self.order_dir_combo.addItem(_t("Absteigend"), "DESC") + order_form.addRow(_t("Richtung"), self.order_dir_combo) self.limit_spin = QSpinBox() self.limit_spin.setRange(0, 100_000) self.limit_spin.setSpecialValueText("(unbegrenzt)") self.limit_spin.setValue(0) - order_form.addRow("Limit", self.limit_spin) + order_form.addRow(_t("Limit"), self.limit_spin) outer.addLayout(order_form) # OK / Cancel buttons. @@ -335,6 +392,7 @@ def __init__(self, parent=None, *, name: str = "", description: str = "", QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) + _set_dialog_buttons(buttons) buttons.accepted.connect(self._on_accept) buttons.rejected.connect(self.reject) outer.addWidget(buttons) @@ -383,7 +441,7 @@ def _row_widgets(self): ConditionRow)] def _add_condition_row(self, condition: Optional[FilterCondition] = None): - row = ConditionRow(on_remove=self._remove_condition_row) + row = ConditionRow(on_remove=self._remove_condition_row, translator=self.translator) if condition: row.load_condition(condition) # First row never starts with "OR" - hide the conjunction combo. @@ -446,22 +504,22 @@ def _load_query(self, smart_query_json: str): def _on_accept(self): if not self.name(): - QMessageBox.warning(self, "Name fehlt", - "Bitte einen Namen für die Playlist angeben.") + _show_message(QMessageBox.Icon.Warning, self, _t("Name fehlt"), + _t("Bitte einen Namen für die Playlist angeben.")) return # Validate by serializing - if no conditions are valid the JSON is # still well formed but we warn the user explicitly. try: payload = json.loads(self.smart_query_json()) except json.JSONDecodeError: - QMessageBox.warning(self, "Ungültige Bedingungen", - "Die Smart-Query konnte nicht erzeugt werden.") + _show_message(QMessageBox.Icon.Warning, self, _t("Ungültige Bedingungen"), + _t("Die Smart-Query konnte nicht erzeugt werden.")) return if not payload.get("conditions"): - answer = QMessageBox.question( - self, "Keine Bedingungen", - "Diese Smart-Playlist enthält keine Bedingungen und liefert " - "alle Medien zurück. Trotzdem speichern?", + answer = _show_message( + QMessageBox.Icon.Question, self, _t("Keine Bedingungen"), + _t("Diese Smart-Playlist enthält keine Bedingungen und liefert " + "alle Medien zurück. Trotzdem speichern?"), ) if answer != QMessageBox.StandardButton.Yes: return @@ -475,26 +533,30 @@ def _on_accept(self): class ManualPlaylistDialog(QDialog): """Tiny dialog that captures name + description for a manual playlist.""" - def __init__(self, parent=None, *, name: str = "", description: str = ""): + def __init__(self, parent=None, *, name: str = "", description: str = "", translator=None): super().__init__(parent) - self.setWindowTitle("Playlist") + self.translator = translator + if translator is not None: + set_default_translation_system(translator) + self.setWindowTitle(_t("Playlist")) layout = QVBoxLayout() self.setLayout(layout) form = QFormLayout() self.name_edit = QLineEdit(name) - self.name_edit.setPlaceholderText("Name der Playlist") - form.addRow("Name", self.name_edit) + self.name_edit.setPlaceholderText(_t("Name der Playlist")) + form.addRow(_t("Name"), self.name_edit) self.desc_edit = QTextEdit(description) self.desc_edit.setFixedHeight(80) - form.addRow("Beschreibung", self.desc_edit) + form.addRow(_t("Beschreibung"), self.desc_edit) layout.addLayout(form) buttons = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) + _set_dialog_buttons(buttons) buttons.accepted.connect(self._on_accept) buttons.rejected.connect(self.reject) layout.addWidget(buttons) @@ -507,8 +569,8 @@ def description(self) -> str: def _on_accept(self): if not self.name(): - QMessageBox.warning(self, "Name fehlt", - "Bitte einen Namen für die Playlist angeben.") + _show_message(QMessageBox.Icon.Warning, self, _t("Name fehlt"), + _t("Bitte einen Namen für die Playlist angeben.")) return self.accept() @@ -537,8 +599,9 @@ def data(self, index, role=Qt.ItemDataRole.DisplayRole): return None playlist: Playlist = self._playlists[index.row()] if role == Qt.ItemDataRole.DisplayRole: - kind = "Smart" if playlist.playlist_type == "smart" else "Manuell" - return f"{playlist.name} ({kind}, {playlist.item_count})" + kind = _t("Smart") if playlist.playlist_type == "smart" else _t("Manuell") + return _t("{name} ({kind}, {count})", name=playlist.name, + kind=kind, count=playlist.item_count) if role == self.NameRole: return playlist.name if role == self.PlaylistRole: @@ -571,8 +634,11 @@ class PlaylistsView(QWidget): """ def __init__(self, playlist_manager: PlaylistManager, - media_manager=None, parent=None): + media_manager=None, parent=None, translator=None): super().__init__(parent) + self.translator = translator + if translator is not None: + set_default_translation_system(translator) self.playlist_manager = playlist_manager self.media_manager = media_manager self._needs_refresh = False @@ -581,26 +647,26 @@ def __init__(self, playlist_manager: PlaylistManager, outer = QVBoxLayout() self.setLayout(outer) - title = QLabel("Playlists") + title = QLabel(_t("Playlists")) title.setObjectName("pageTitle") outer.addWidget(title) # Toolbar toolbar = QHBoxLayout() - new_manual_btn = QPushButton("Neue Playlist") + new_manual_btn = QPushButton(_t("Neue Playlist")) new_manual_btn.clicked.connect(self._on_new_manual) toolbar.addWidget(new_manual_btn) - new_smart_btn = QPushButton("Neue Smart-Playlist") + new_smart_btn = QPushButton(_t("Neue Smart-Playlist")) new_smart_btn.clicked.connect(self._on_new_smart) toolbar.addWidget(new_smart_btn) - edit_btn = QPushButton("Bearbeiten") + edit_btn = QPushButton(_t("Bearbeiten")) edit_btn.clicked.connect(self._on_edit) toolbar.addWidget(edit_btn) self._edit_btn = edit_btn - delete_btn = QPushButton("Löschen") + delete_btn = QPushButton(_t("Löschen")) delete_btn.clicked.connect(self._on_delete) toolbar.addWidget(delete_btn) self._delete_btn = delete_btn @@ -626,7 +692,7 @@ def __init__(self, playlist_manager: PlaylistManager, right = QWidget() right_layout = QVBoxLayout() right.setLayout(right_layout) - self.detail_label = QLabel("Keine Playlist ausgewählt.") + self.detail_label = QLabel(_t("Keine Playlist ausgewählt.")) right_layout.addWidget(self.detail_label) self.items_list = QListWidget() right_layout.addWidget(self.items_list, stretch=1) @@ -673,29 +739,30 @@ def _on_selection_changed(self, current, _previous): def _populate_items(self, playlist: Optional[Playlist]): self.items_list.clear() if playlist is None: - self.detail_label.setText("Keine Playlist ausgewählt.") + self.detail_label.setText(_t("Keine Playlist ausgewählt.")) return - kind = "Smart" if playlist.playlist_type == "smart" else "Manuell" + kind = _t("Smart") if playlist.playlist_type == "smart" else _t("Manuell") desc = playlist.description or "(keine Beschreibung)" self.detail_label.setText( - f"{playlist.name} | {kind} | {playlist.item_count} Einträge\n{desc}" + _t('{name} | {kind} | {count} Einträge\n{description}', name=(playlist.name), kind=(kind), count=(playlist.item_count), description=(desc)) ) try: rows = self.playlist_manager.get_media_rows(playlist.id) except Exception as exc: logger.error("Konnte Playlist-Items nicht laden: %s", exc) - self.items_list.addItem(f"Fehler: {exc}") + self.items_list.addItem(_t('Fehler: {error}', error=(exc))) return if not rows: - self.items_list.addItem("(keine Treffer)") + self.items_list.addItem(_t("(keine Treffer)")) return for row in rows: title = self._row_value(row, "title", default="(ohne Titel)") mtype = self._row_value(row, "type", default="?") source = self._row_value(row, "source", default="?") - self.items_list.addItem(f"{title} [{mtype} | {source}]") + self.items_list.addItem(_t(" {title} [{mtype} | {source}]", title=title, + mtype=_display_field_value(mtype), source=source)) @staticmethod def _row_value(row, key, default=None): @@ -708,19 +775,19 @@ def _row_value(row, key, default=None): # --- Toolbar actions ----------------------------------------------- def _on_new_manual(self): - dialog = ManualPlaylistDialog(self) + dialog = ManualPlaylistDialog(self, translator=self.translator) if dialog.exec() == QDialog.DialogCode.Accepted: try: self.playlist_manager.create_playlist( dialog.name(), dialog.description(), playlist_type="manual" ) except Exception as exc: - QMessageBox.warning(self, "Fehler", str(exc)) + _show_message(QMessageBox.Icon.Warning, self, _t("Fehler"), str(exc)) return self.refresh() def _on_new_smart(self): - dialog = SmartPlaylistDialog(self) + dialog = SmartPlaylistDialog(self, translator=self.translator) if dialog.exec() == QDialog.DialogCode.Accepted: try: self.playlist_manager.create_smart_playlist( @@ -729,7 +796,7 @@ def _on_new_smart(self): description=dialog.description(), ) except Exception as exc: - QMessageBox.warning(self, "Fehler", str(exc)) + _show_message(QMessageBox.Icon.Warning, self, _t("Fehler"), str(exc)) return self.refresh() @@ -744,7 +811,8 @@ def _on_edit(self): def _edit_manual(self, playlist: Playlist): dialog = ManualPlaylistDialog( - self, name=playlist.name, description=playlist.description + self, name=playlist.name, description=playlist.description, + translator=self.translator ) if dialog.exec() != QDialog.DialogCode.Accepted: return @@ -769,6 +837,7 @@ def _edit_smart(self, playlist: Playlist): name=playlist.name, description=playlist.description, smart_query_json=playlist.smart_query, + translator=self.translator, ) if dialog.exec() != QDialog.DialogCode.Accepted: return @@ -791,18 +860,16 @@ def _on_delete(self): playlist = self._selected_playlist() if not playlist: return - answer = QMessageBox.question( - self, "Playlist löschen", - f"Playlist '{playlist.name}' wirklich löschen?\n" - "Manuelle Einträge werden ebenfalls entfernt; die Mediendateien " - "bleiben unangetastet." + answer = _show_message( + QMessageBox.Icon.Question, self, _t("Playlist löschen"), + _t("Playlist '{name}' wirklich löschen?\nManuelle Einträge werden ebenfalls entfernt; die Mediendateien bleiben unangetastet.", name=(playlist.name)) ) if answer != QMessageBox.StandardButton.Yes: return try: self.playlist_manager.delete_playlist(playlist.id) except Exception as exc: - QMessageBox.warning(self, "Fehler", str(exc)) + _show_message(QMessageBox.Icon.Warning, self, _t("Fehler"), str(exc)) return self._current_playlist_id = None self.refresh() @@ -816,8 +883,8 @@ def _show_playlist_context_menu(self, pos): if not playlist: return menu = QMenu(self) - menu.addAction(QAction("Bearbeiten", self, + menu.addAction(QAction(_t("Bearbeiten"), self, triggered=lambda: self._on_edit())) - menu.addAction(QAction("Löschen", self, + menu.addAction(QAction(_t("Löschen"), self, triggered=lambda: self._on_delete())) menu.exec(self.playlists_list.viewport().mapToGlobal(pos)) diff --git a/locales/translations.json b/locales/translations.json index a623377..085e5f6 100644 --- a/locales/translations.json +++ b/locales/translations.json @@ -1051,5 +1051,3684 @@ "ar": "لغة سطر الأوامر", "bn": "CLI-এর ভাষা", "pt": "Idioma da CLI" + }, + "MediaBrain läuft bereits im Hintergrund.\nTray-Icon doppelklicken um das Fenster zu öffnen.": { + "de": "MediaBrain läuft bereits im Hintergrund.\nTray-Icon doppelklicken, um das Fenster zu öffnen.", + "en": "MediaBrain is already running in the background.\nDouble-click the tray icon to open the window.", + "es": "MediaBrain ya se está ejecutando en segundo plano.\nHaz doble clic en el icono de la bandeja para abrir la ventana.", + "zh": "MediaBrain 已在后台运行。\n双击托盘图标即可打开窗口。", + "ja": "MediaBrain はすでにバックグラウンドで実行中です。\nトレイアイコンをダブルクリックするとウィンドウが開きます。", + "ru": "MediaBrain уже работает в фоновом режиме.\nДважды щёлкните значок в области уведомлений, чтобы открыть окно.", + "fr": "MediaBrain s’exécute déjà en arrière-plan.\nDouble-cliquez sur l’icône de la zone de notification pour ouvrir la fenêtre.", + "hi": "MediaBrain पहले से बैकग्राउंड में चल रहा है।\nविंडो खोलने के लिए ट्रे आइकन पर डबल-क्लिक करें।", + "ar": "يعمل MediaBrain بالفعل في الخلفية.\nانقر نقرًا مزدوجًا على أيقونة علبة النظام لفتح النافذة.", + "bn": "MediaBrain ইতিমধ্যে ব্যাকগ্রাউন্ডে চলছে।\nউইন্ডো খুলতে ট্রে আইকনে দুবার ক্লিক করুন।", + "pt": "O MediaBrain já está em execução em segundo plano.\nClique duas vezes no ícone da área de notificação para abrir a janela." + }, + "{script} wurde nicht gefunden:\n{path}": { + "de": "{script} wurde nicht gefunden:\n{path}", + "en": "{script} was not found:\n{path}", + "es": "No se encontró {script}:\n{path}", + "zh": "找不到 {script}:\n{path}", + "ja": "{script} が見つかりません:\n{path}", + "ru": "{script} не найден:\n{path}", + "fr": "{script} est introuvable :\n{path}", + "hi": "{script} नहीं मिला:\n{path}", + "ar": "لم يتم العثور على {script}:\n{path}", + "bn": "{script} পাওয়া যায়নি:\n{path}", + "pt": "{script} não foi encontrado:\n{path}" + }, + "Python wurde nicht gefunden.\nBitte Python 3.10+ installieren und die Abhängigkeiten aus requirements.txt einrichten.": { + "de": "Python wurde nicht gefunden.\nBitte Python 3.10 oder neuer installieren und die Abhängigkeiten aus requirements.txt einrichten.", + "en": "Python was not found.\nPlease install Python 3.10 or later and set up the dependencies from requirements.txt.", + "es": "No se encontró Python.\nInstala Python 3.10 o posterior y configura las dependencias de requirements.txt.", + "zh": "未找到 Python。\n请安装 Python 3.10 或更高版本,并按 requirements.txt 配置依赖项。", + "ja": "Python が見つかりません。\nPython 3.10 以降をインストールし、requirements.txt の依存関係を設定してください。", + "ru": "Python не найден.\nУстановите Python 3.10 или новее и настройте зависимости из requirements.txt.", + "fr": "Python est introuvable.\nVeuillez installer Python 3.10 ou une version ultérieure et configurer les dépendances de requirements.txt.", + "hi": "Python नहीं मिला।\nPython 3.10 या नया संस्करण इंस्टॉल करें और requirements.txt की निर्भरताएँ सेट अप करें।", + "ar": "لم يتم العثور على Python.\nيُرجى تثبيت Python 3.10 أو إصدار أحدث وإعداد التبعيات من requirements.txt.", + "bn": "Python পাওয়া যায়নি।\nPython 3.10 বা পরবর্তী সংস্করণ ইনস্টল করুন এবং requirements.txt-এর নির্ভরতা সেট আপ করুন।", + "pt": "Python não foi encontrado.\nInstale o Python 3.10 ou posterior e configure as dependências de requirements.txt." + }, + "Start fehlgeschlagen:\n{error}": { + "de": "Start fehlgeschlagen:\n{error}", + "en": "Launch failed:\n{error}", + "es": "Falló el inicio:\n{error}", + "zh": "启动失败:\n{error}", + "ja": "起動に失敗しました:\n{error}", + "ru": "Не удалось запустить:\n{error}", + "fr": "Échec du lancement :\n{error}", + "hi": "शुरू नहीं हो सका:\n{error}", + "ar": "تعذر بدء التشغيل:\n{error}", + "bn": "চালু করা যায়নি:\n{error}", + "pt": "Falha ao iniciar:\n{error}" + }, + "Suchen...": { + "de": "Suchen...", + "en": "Search...", + "es": "Buscar...", + "zh": "搜索...", + "ja": "検索...", + "ru": "Поиск...", + "fr": "Rechercher...", + "hi": "खोजें...", + "ar": "بحث...", + "bn": "খুঁজুন...", + "pt": "Pesquisar..." + }, + "Details": { + "de": "Details", + "en": "Details", + "es": "Detalles", + "zh": "详情", + "ja": "詳細", + "ru": "Подробности", + "fr": "Détails", + "hi": "विवरण", + "ar": "التفاصيل", + "bn": "বিস্তারিত", + "pt": "Detalhes" + }, + "Details anzeigen": { + "de": "Details anzeigen", + "en": "Show details", + "es": "Mostrar detalles", + "zh": "显示详情", + "ja": "詳細を表示", + "ru": "Показать сведения", + "fr": "Afficher les détails", + "hi": "विवरण दिखाएँ", + "ar": "عرض التفاصيل", + "bn": "বিস্তারিত দেখান", + "pt": "Mostrar detalhes" + }, + "Neuer Tag": { + "de": "Neuer Tag", + "en": "New tag", + "es": "Nueva etiqueta", + "zh": "新标签", + "ja": "新しいタグ", + "ru": "Новый тег", + "fr": "Nouveau tag", + "hi": "नया टैग", + "ar": "وسم جديد", + "bn": "নতুন ট্যাগ", + "pt": "Nova tag" + }, + "Tag-Name:": { + "de": "Tag-Name:", + "en": "Tag name:", + "es": "Nombre de la etiqueta:", + "zh": "标签名称:", + "ja": "タグ名:", + "ru": "Название тега:", + "fr": "Nom du tag :", + "hi": "टैग का नाम:", + "ar": "اسم الوسم:", + "bn": "ট্যাগের নাম:", + "pt": "Nome da tag:" + }, + "Medientyp: {media_type}": { + "de": "Medientyp: {media_type}", + "en": "Media type: {media_type}", + "es": "Tipo de medio: {media_type}", + "zh": "媒体类型:{media_type}", + "ja": "メディアの種類: {media_type}", + "ru": "Тип медиа: {media_type}", + "fr": "Type de média : {media_type}", + "hi": "मीडिया प्रकार: {media_type}", + "ar": "نوع الوسائط: {media_type}", + "bn": "মিডিয়ার ধরন: {media_type}", + "pt": "Tipo de mídia: {media_type}" + }, + "Zeigt den Medientyp für {title} an.": { + "de": "Zeigt den Medientyp für {title} an.", + "en": "Shows the media type for {title}.", + "es": "Muestra el tipo de medio de {title}.", + "zh": "显示 {title} 的媒体类型。", + "ja": "{title} のメディアの種類を表示します。", + "ru": "Показывает тип медиа для «{title}».", + "fr": "Affiche le type de média de {title}.", + "hi": "{title} का मीडिया प्रकार दिखाता है।", + "ar": "يعرض نوع الوسائط للعنصر {title}.", + "bn": "{title}-এর মিডিয়ার ধরন দেখায়।", + "pt": "Mostra o tipo de mídia de {title}." + }, + "Ändert den Favoritenstatus für {title}.": { + "de": "Ändert den Favoritenstatus für {title}.", + "en": "Changes the favorite status for {title}.", + "es": "Cambia el estado de favorito de {title}.", + "zh": "更改 {title} 的收藏状态。", + "ja": "{title} のお気に入り状態を変更します。", + "ru": "Modifie le statut de favori de « {title} ».", + "fr": "Modifie le statut de favori de {title}.", + "hi": "{title} की पसंदीदा स्थिति बदलता है।", + "ar": "يغيّر حالة التفضيل للعنصر {title}.", + "bn": "{title}-এর পছন্দের অবস্থা বদলায়।", + "pt": "Altera o status de favorito de {title}." + }, + "▶ öffnen": { + "de": "▶ öffnen", + "en": "▶ Open", + "es": "▶ Abrir", + "zh": "▶ 打开", + "ja": "▶ 開く", + "ru": "▶ Открыть", + "fr": "▶ Ouvrir", + "hi": "▶ खोलें", + "ar": "▶ فتح", + "bn": "▶ খুলুন", + "pt": "▶ Abrir" + }, + "ℹ Details": { + "de": "ℹ Details", + "en": "ℹ Details", + "es": "ℹ Detalles", + "zh": "ℹ 详情", + "ja": "ℹ 詳細", + "ru": "ℹ Подробности", + "fr": "ℹ Détails", + "hi": "ℹ विवरण", + "ar": "ℹ التفاصيل", + "bn": "ℹ বিস্তারিত", + "pt": "ℹ Detalhes" + }, + "Favorit entfernen": { + "de": "Favorit entfernen", + "en": "Remove favorite", + "es": "Quitar de favoritos", + "zh": "取消收藏", + "ja": "お気に入りから削除", + "ru": "Убрать из избранного", + "fr": "Retirer des favoris", + "hi": "पसंदीदा से हटाएँ", + "ar": "إزالة من المفضلة", + "bn": "পছন্দের তালিকা থেকে সরান", + "pt": "Remover dos favoritos" + }, + "Als Favorit markieren": { + "de": "Als Favorit markieren", + "en": "Mark as favorite", + "es": "Marcar como favorito", + "zh": "标记为收藏", + "ja": "お気に入りに追加", + "ru": "Добавить в избранное", + "fr": "Ajouter aux favoris", + "hi": "पसंदीदा के रूप में चिह्नित करें", + "ar": "وضع علامة كمفضّل", + "bn": "পছন্দের হিসেবে চিহ্নিত করুন", + "pt": "Marcar como favorito" + }, + "{title} aus Favoriten entfernen": { + "de": "{title} aus Favoriten entfernen", + "en": "Remove {title} from favorites", + "es": "Quitar {title} de favoritos", + "zh": "将 {title} 从收藏中移除", + "ja": "{title} をお気に入りから削除", + "ru": "Убрать «{title}» из избранного", + "fr": "Retirer {title} des favoris", + "hi": "{title} को पसंदीदा से हटाएँ", + "ar": "إزالة {title} من المفضلة", + "bn": "{title}-কে পছন্দের তালিকা থেকে সরান", + "pt": "Remover {title} dos favoritos" + }, + "{title} als Favorit markieren": { + "de": "{title} als Favorit markieren", + "en": "Mark {title} as a favorite", + "es": "Marcar {title} como favorito", + "zh": "将 {title} 标记为收藏", + "ja": "{title} をお気に入りに追加", + "ru": "Добавить «{title}» в избранное", + "fr": "Ajouter {title} aux favoris", + "hi": "{title} को पसंदीदा के रूप में चिह्नित करें", + "ar": "وضع علامة على {title} كمفضّل", + "bn": "{title}-কে পছন্দের হিসেবে চিহ্নিত করুন", + "pt": "Marcar {title} como favorito" + }, + "Temporär ausblenden": { + "de": "Temporär ausblenden", + "en": "Hide temporarily", + "es": "Ocultar temporalmente", + "zh": "暂时隐藏", + "ja": "一時的に非表示", + "ru": "Временно скрыть", + "fr": "Masquer temporairement", + "hi": "अस्थायी रूप से छिपाएँ", + "ar": "إخفاء مؤقتًا", + "bn": "সাময়িকভাবে আড়াল করুন", + "pt": "Ocultar temporariamente" + }, + "Blacklist…": { + "de": "Sperrliste…", + "en": "Blacklist…", + "es": "Lista de bloqueo…", + "zh": "黑名单…", + "ja": "ブロックリスト…", + "ru": "Чёрный список…", + "fr": "Liste de blocage…", + "hi": "ब्लॉकलिस्ट…", + "ar": "قائمة الحظر…", + "bn": "ব্ল্যাকলিস্ট…", + "pt": "Lista de bloqueio…" + }, + "Im Explorer anzeigen": { + "de": "Im Explorer anzeigen", + "en": "Show in File Explorer", + "es": "Mostrar en el Explorador de archivos", + "zh": "在文件资源管理器中显示", + "ja": "エクスプローラーで表示", + "ru": "Показать в проводнике", + "fr": "Afficher dans l’Explorateur de fichiers", + "hi": "फ़ाइल एक्सप्लोरर में दिखाएँ", + "ar": "عرض في مستكشف الملفات", + "bn": "ফাইল এক্সপ্লোরারে দেখান", + "pt": "Mostrar no Explorador de Arquivos" + }, + "Datei löschen": { + "de": "Datei löschen", + "en": "Delete file", + "es": "Eliminar archivo", + "zh": "删除文件", + "ja": "ファイルを削除", + "ru": "Удалить файл", + "fr": "Supprimer le fichier", + "hi": "फ़ाइल हटाएँ", + "ar": "حذف الملف", + "bn": "ফাইল মুছুন", + "pt": "Excluir arquivo" + }, + "Lokale Metadaten aktualisieren": { + "de": "Lokale Metadaten aktualisieren", + "en": "Refresh local metadata", + "es": "Actualizar metadatos locales", + "zh": "刷新本地元数据", + "ja": "ローカルメタデータを更新", + "ru": "Обновить локальные метаданные", + "fr": "Actualiser les métadonnées locales", + "hi": "स्थानीय मेटाडेटा ताज़ा करें", + "ar": "تحديث البيانات الوصفية المحلية", + "bn": "স্থানীয় মেটাডেটা হালনাগাদ করুন", + "pt": "Atualizar metadados locais" + }, + "🌐 Online-Metadaten abrufen": { + "de": "🌐 Online-Metadaten abrufen", + "en": "🌐 Fetch online metadata", + "es": "🌐 Obtener metadatos en línea", + "zh": "🌐 获取在线元数据", + "ja": "🌐 オンラインメタデータを取得", + "ru": "🌐 Получить метаданные из интернета", + "fr": "🌐 Récupérer les métadonnées en ligne", + "hi": "🌐 ऑनलाइन मेटाडेटा प्राप्त करें", + "ar": "🌐 جلب البيانات الوصفية عبر الإنترنت", + "bn": "🌐 অনলাইন মেটাডেটা আনুন", + "pt": "🌐 Buscar metadados online" + }, + "Blacklist entfernen": { + "de": "Sperrliste entfernen", + "en": "Remove from blacklist", + "es": "Quitar de la lista de bloqueo", + "zh": "从黑名单移除", + "ja": "ブロックリストから削除", + "ru": "Убрать из чёрного списка", + "fr": "Retirer de la liste de blocage", + "hi": "ब्लॉकलिस्ट से हटाएँ", + "ar": "إزالة من قائمة الحظر", + "bn": "ব্ল্যাকলিস্ট থেকে সরান", + "pt": "Remover da lista de bloqueio" + }, + "Fehler": { + "de": "Fehler", + "en": "Error", + "es": "Error", + "zh": "错误", + "ja": "エラー", + "ru": "Ошибка", + "fr": "Erreur", + "hi": "त्रुटि", + "ar": "خطأ", + "bn": "ত্রুটি", + "pt": "Erro" + }, + "Konnte Medium nicht öffnen: {error}": { + "de": "Konnte Medium nicht öffnen: {error}", + "en": "Could not open media item: {error}", + "es": "No se pudo abrir el elemento multimedia: {error}", + "zh": "无法打开媒体项:{error}", + "ja": "メディア項目を開けませんでした: {error}", + "ru": "Не удалось открыть медиа: {error}", + "fr": "Impossible d’ouvrir le média : {error}", + "hi": "मीडिया आइटम नहीं खोला जा सका: {error}", + "ar": "تعذر فتح الوسائط: {error}", + "bn": "মিডিয়া আইটেম খোলা যায়নি: {error}", + "pt": "Não foi possível abrir a mídia: {error}" + }, + "Detailseite konnte nicht geöffnet werden: {error}": { + "de": "Detailseite konnte nicht geöffnet werden: {error}", + "en": "Could not open details page: {error}", + "es": "No se pudo abrir la página de detalles: {error}", + "zh": "无法打开详情页:{error}", + "ja": "詳細ページを開けませんでした: {error}", + "ru": "Не удалось открыть страницу сведений: {error}", + "fr": "Impossible d’ouvrir la page de détails : {error}", + "hi": "विवरण पृष्ठ नहीं खोला जा सका: {error}", + "ar": "تعذر فتح صفحة التفاصيل: {error}", + "bn": "বিস্তারিত পৃষ্ঠা খোলা যায়নি: {error}", + "pt": "Não foi possível abrir a página de detalhes: {error}" + }, + "Favorit konnte nicht geändert werden: {error}": { + "de": "Favorit konnte nicht geändert werden: {error}", + "en": "Could not change favorite: {error}", + "es": "No se pudo cambiar el favorito: {error}", + "zh": "无法更改收藏状态:{error}", + "ja": "お気に入りを変更できませんでした: {error}", + "ru": "Не удалось изменить избранное: {error}", + "fr": "Impossible de modifier le statut de favori : {error}", + "hi": "पसंदीदा स्थिति नहीं बदली जा सकी: {error}", + "ar": "تعذر تغيير حالة التفضيل: {error}", + "bn": "পছন্দের অবস্থা বদলানো যায়নি: {error}", + "pt": "Não foi possível alterar o favorito: {error}" + }, + "Konnte Datei nicht im Explorer anzeigen: {error}": { + "de": "Konnte Datei nicht im Explorer anzeigen: {error}", + "en": "Could not show file in File Explorer: {error}", + "es": "No se pudo mostrar el archivo en el Explorador: {error}", + "zh": "无法在文件资源管理器中显示文件:{error}", + "ja": "エクスプローラーにファイルを表示できませんでした: {error}", + "ru": "Не удалось показать файл в проводнике: {error}", + "fr": "Impossible d’afficher le fichier dans l’Explorateur : {error}", + "hi": "फ़ाइल एक्सप्लोरर में फ़ाइल नहीं दिखाई जा सकी: {error}", + "ar": "تعذر عرض الملف في مستكشف الملفات: {error}", + "bn": "ফাইল এক্সপ্লোরারে ফাইল দেখানো যায়নি: {error}", + "pt": "Não foi possível mostrar o arquivo no Explorador: {error}" + }, + "Soll die Datei wirklich unwiderruflich gelöscht werden?\n\n{path}": { + "de": "Soll die Datei wirklich unwiderruflich gelöscht werden?\n\n{path}", + "en": "Do you really want to permanently delete this file?\n\n{path}", + "es": "¿Seguro que quieres eliminar este archivo de forma permanente?\n\n{path}", + "zh": "确定要永久删除此文件吗?\n\n{path}", + "ja": "このファイルを完全に削除しますか?\n\n{path}", + "ru": "Вы действительно хотите удалить этот файл без возможности восстановления?\n\n{path}", + "fr": "Voulez-vous vraiment supprimer définitivement ce fichier ?\n\n{path}", + "hi": "क्या आप वाकई इस फ़ाइल को स्थायी रूप से हटाना चाहते हैं?\n\n{path}", + "ar": "هل تريد حقًا حذف هذا الملف نهائيًا؟\n\n{path}", + "bn": "আপনি কি সত্যিই এই ফাইলটি স্থায়ীভাবে মুছতে চান?\n\n{path}", + "pt": "Tem certeza de que deseja excluir este arquivo permanentemente?\n\n{path}" + }, + "Datei konnte nicht gelöscht werden: {error}": { + "de": "Datei konnte nicht gelöscht werden: {error}", + "en": "Could not delete file: {error}", + "es": "No se pudo eliminar el archivo: {error}", + "zh": "无法删除文件:{error}", + "ja": "ファイルを削除できませんでした: {error}", + "ru": "Не удалось удалить файл: {error}", + "fr": "Impossible de supprimer le fichier : {error}", + "hi": "फ़ाइल हटाई नहीं जा सकी: {error}", + "ar": "تعذر حذف الملف: {error}", + "bn": "ফাইল মুছতে পারেনি: {error}", + "pt": "Não foi possível excluir o arquivo: {error}" + }, + "Temporäres Ausblenden fehlgeschlagen: {error}": { + "de": "Temporäres Ausblenden fehlgeschlagen: {error}", + "en": "Could not hide temporarily: {error}", + "es": "No se pudo ocultar temporalmente: {error}", + "zh": "暂时隐藏失败:{error}", + "ja": "一時的に非表示にできませんでした: {error}", + "ru": "Не удалось временно скрыть: {error}", + "fr": "Échec du masquage temporaire : {error}", + "hi": "अस्थायी रूप से छिपाया नहीं जा सका: {error}", + "ar": "تعذر الإخفاء مؤقتًا: {error}", + "bn": "সাময়িকভাবে আড়াল করা যায়নি: {error}", + "pt": "Falha ao ocultar temporariamente: {error}" + }, + "Blacklist-Aktion fehlgeschlagen: {error}": { + "de": "Sperrlistenaktion fehlgeschlagen: {error}", + "en": "Blacklist action failed: {error}", + "es": "Falló la acción de la lista de bloqueo: {error}", + "zh": "黑名单操作失败:{error}", + "ja": "ブロックリストの操作に失敗しました: {error}", + "ru": "Не удалось выполнить действие со списком блокировки: {error}", + "fr": "Échec de l’action sur la liste de blocage : {error}", + "hi": "ब्लॉकलिस्ट कार्रवाई विफल रही: {error}", + "ar": "فشل إجراء قائمة الحظر: {error}", + "bn": "ব্ল্যাকলিস্টের কাজ ব্যর্থ হয়েছে: {error}", + "pt": "Falha na ação da lista de bloqueio: {error}" + }, + "API nicht verfügbar": { + "de": "API nicht verfügbar", + "en": "API unavailable", + "es": "API no disponible", + "zh": "API 不可用", + "ja": "API を利用できません", + "ru": "API недоступен", + "fr": "API indisponible", + "hi": "API उपलब्ध नहीं है", + "ar": "واجهة API غير متاحة", + "bn": "API উপলভ্য নয়", + "pt": "API indisponível" + }, + "Keine API-Keys konfiguriert. Bitte in settings.json eintragen.": { + "de": "Keine API-Schlüssel konfiguriert. Bitte in settings.json eintragen.", + "en": "No API keys are configured. Add them to settings.json.", + "es": "No hay claves de API configuradas. Añádelas a settings.json.", + "zh": "未配置 API 密钥。请将密钥填入 settings.json。", + "ja": "API キーが設定されていません。settings.json に記入してください。", + "ru": "Ключи API не настроены. Добавьте их в settings.json.", + "fr": "Aucune clé API n’est configurée. Ajoutez-en dans settings.json.", + "hi": "कोई API कुंजी कॉन्फ़िगर नहीं है। उन्हें settings.json में जोड़ें।", + "ar": "لم يتم إعداد مفاتيح API. أضفها إلى settings.json.", + "bn": "কোনো API কী কনফিগার করা নেই। settings.json-এ যোগ করুন।", + "pt": "Nenhuma chave de API configurada. Adicione-as a settings.json." + }, + "Keine Ergebnisse": { + "de": "Keine Ergebnisse", + "en": "No results", + "es": "Sin resultados", + "zh": "没有结果", + "ja": "結果がありません", + "ru": "Нет результатов", + "fr": "Aucun résultat", + "hi": "कोई परिणाम नहीं", + "ar": "لا توجد نتائج", + "bn": "কোনো ফলাফল নেই", + "pt": "Nenhum resultado" + }, + "Keine Online-Metadaten gefunden für '{title}'": { + "de": "Keine Online-Metadaten für '{title}' gefunden", + "en": "No online metadata found for '{title}'", + "es": "No se encontraron metadatos en línea para '{title}'", + "zh": "未找到“{title}”的在线元数据", + "ja": "'{title}' のオンラインメタデータが見つかりません", + "ru": "Для «{title}» не найдены метаданные в интернете", + "fr": "Aucune métadonnée en ligne trouvée pour « {title} »", + "hi": "'{title}' के लिए ऑनलाइन मेटाडेटा नहीं मिला", + "ar": "لم يتم العثور على بيانات وصفية عبر الإنترنت للعنصر '{title}'", + "bn": "'{title}'-এর জন্য অনলাইন মেটাডেটা পাওয়া যায়নি", + "pt": "Nenhum metadado online encontrado para '{title}'" + }, + "Metadaten aktualisiert": { + "de": "Metadaten aktualisiert", + "en": "Metadata updated", + "es": "Metadatos actualizados", + "zh": "元数据已更新", + "ja": "メタデータを更新しました", + "ru": "Метаданные обновлены", + "fr": "Métadonnées mises à jour", + "hi": "मेटाडेटा अपडेट किया गया", + "ar": "تم تحديث البيانات الوصفية", + "bn": "মেটাডেটা হালনাগাদ হয়েছে", + "pt": "Metadados atualizados" + }, + "Metadaten für '{title}' wurden aktualisiert.\nQuelle: {source}": { + "de": "Metadaten für '{title}' wurden aktualisiert.\nQuelle: {source}", + "en": "Metadata for '{title}' were updated.\nSource: {source}", + "es": "Se actualizaron los metadatos de '{title}'.\nFuente: {source}", + "zh": "已更新“{title}”的元数据。\n来源:{source}", + "ja": "'{title}' のメタデータを更新しました。\nソース: {source}", + "ru": "Метаданные для «{title}» обновлены.\nИсточник: {source}", + "fr": "Les métadonnées de « {title} » ont été mises à jour.\nSource : {source}", + "hi": "'{title}' के मेटाडेटा अपडेट किए गए।\nस्रोत: {source}", + "ar": "تم تحديث البيانات الوصفية للعنصر '{title}'.\nالمصدر: {source}", + "bn": "'{title}'-এর মেটাডেটা হালনাগাদ হয়েছে।\nউৎস: {source}", + "pt": "Os metadados de '{title}' foram atualizados.\nFonte: {source}" + }, + "Keine neuen Daten": { + "de": "Keine neuen Daten", + "en": "No new data", + "es": "No hay datos nuevos", + "zh": "没有新数据", + "ja": "新しいデータはありません", + "ru": "Новых данных нет", + "fr": "Aucune nouvelle donnée", + "hi": "कोई नया डेटा नहीं", + "ar": "لا توجد بيانات جديدة", + "bn": "নতুন কোনো ডেটা নেই", + "pt": "Nenhum dado novo" + }, + "Die gefundenen Metadaten sind bereits identisch.": { + "de": "Die gefundenen Metadaten sind bereits identisch.", + "en": "The retrieved metadata is already identical.", + "es": "Los metadatos encontrados ya son idénticos.", + "zh": "找到的元数据已完全相同。", + "ja": "取得したメタデータはすでに同じです。", + "ru": "Найденные метаданные уже совпадают.", + "fr": "Les métadonnées trouvées sont déjà identiques.", + "hi": "मिला हुआ मेटाडेटा पहले से समान है।", + "ar": "البيانات الوصفية التي عُثر عليها مطابقة بالفعل.", + "bn": "পাওয়া মেটাডেটা ইতিমধ্যেই একই।", + "pt": "Os metadados encontrados já são idênticos." + }, + "Metadaten konnten nicht abgerufen werden: {error}": { + "de": "Metadaten konnten nicht abgerufen werden: {error}", + "en": "Could not fetch metadata: {error}", + "es": "No se pudieron obtener los metadatos: {error}", + "zh": "无法获取元数据:{error}", + "ja": "メタデータを取得できませんでした: {error}", + "ru": "Не удалось получить метаданные: {error}", + "fr": "Impossible de récupérer les métadonnées : {error}", + "hi": "मेटाडेटा प्राप्त नहीं किया जा सका: {error}", + "ar": "تعذر جلب البيانات الوصفية: {error}", + "bn": "মেটাডেটা আনা যায়নি: {error}", + "pt": "Não foi possível buscar os metadados: {error}" + }, + "Suchfehler: {error}": { + "de": "Suchfehler: {error}", + "en": "Search error: {error}", + "es": "Error de búsqueda: {error}", + "zh": "搜索出错:{error}", + "ja": "検索エラー: {error}", + "ru": "Ошибка поиска: {error}", + "fr": "Erreur de recherche : {error}", + "hi": "खोज में त्रुटि: {error}", + "ar": "خطأ في البحث: {error}", + "bn": "অনুসন্ধানে ত্রুটি: {error}", + "pt": "Erro na busca: {error}" + }, + "Suche: {filters}": { + "de": "Suche: {filters}", + "en": "Search: {filters}", + "es": "Búsqueda: {filters}", + "zh": "搜索:{filters}", + "ja": "検索: {filters}", + "ru": "Поиск: {filters}", + "fr": "Recherche : {filters}", + "hi": "खोज: {filters}", + "ar": "بحث: {filters}", + "bn": "অনুসন্ধান: {filters}", + "pt": "Busca: {filters}" + }, + "{count} Ergebnisse gefunden": { + "de": "{count} Ergebnisse gefunden", + "en": "{count} results found", + "es": "Se encontraron {count} resultados", + "zh": "找到 {count} 个结果", + "ja": "{count} 件の結果が見つかりました", + "ru": "Найдено: {count}", + "fr": "{count} résultats trouvés", + "hi": "{count} परिणाम मिले", + "ar": "تم العثور على {count} نتيجة", + "bn": "{count}টি ফলাফল পাওয়া গেছে", + "pt": "{count} resultados encontrados" + }, + "Keine Treffer gefunden.": { + "de": "Keine Treffer gefunden.", + "en": "No matches found.", + "es": "No se encontraron coincidencias.", + "zh": "未找到匹配项。", + "ja": "一致する項目が見つかりません。", + "ru": "Совпадений не найдено.", + "fr": "Aucun résultat trouvé.", + "hi": "कोई मिलान नहीं मिला।", + "ar": "لم يتم العثور على نتائج مطابقة.", + "bn": "কোনো মিল পাওয়া যায়নি।", + "pt": "Nenhuma correspondência encontrada." + }, + "Provider: {provider}": { + "de": "Anbieter: {provider}", + "en": "Provider: {provider}", + "es": "Proveedor: {provider}", + "zh": "提供方:{provider}", + "ja": "プロバイダー: {provider}", + "ru": "Провайдер: {provider}", + "fr": "Fournisseur : {provider}", + "hi": "प्रदाता: {provider}", + "ar": "المزوّد: {provider}", + "bn": "প্রদানকারী: {provider}", + "pt": "Provedor: {provider}" + }, + "⭐ Favoriten": { + "de": "⭐ Favoriten", + "en": "⭐ Favorites", + "es": "⭐ Favoritos", + "zh": "⭐ 收藏", + "ja": "⭐ お気に入り", + "ru": "⭐ Избранное", + "fr": "⭐ Favoris", + "hi": "⭐ पसंदीदा", + "ar": "⭐ المفضلة", + "bn": "⭐ পছন্দের", + "pt": "⭐ Favoritos" + }, + "Letzte {days} Tage": { + "de": "Letzte {days} Tage", + "en": "Last {days} days", + "es": "Últimos {days} días", + "zh": "最近 {days} 天", + "ja": "過去 {days} 日間", + "ru": "Последние {days} дн.", + "fr": "Ces {days} derniers jours", + "hi": "पिछले {days} दिन", + "ar": "آخر {days} يومًا", + "bn": "গত {days} দিন", + "pt": "Últimos {days} dias" + }, + "Blacklist-Verwaltung": { + "de": "Sperrlistenverwaltung", + "en": "Blacklist management", + "es": "Gestión de la lista de bloqueo", + "zh": "黑名单管理", + "ja": "ブロックリスト管理", + "ru": "Управление чёрным списком", + "fr": "Gestion de la liste de blocage", + "hi": "ब्लॉकलिस्ट प्रबंधन", + "ar": "إدارة قائمة الحظر", + "bn": "ব্ল্যাকলিস্ট ব্যবস্থাপনা", + "pt": "Gerenciamento da lista de bloqueio" + }, + "Theme:": { + "de": "Design:", + "en": "Theme:", + "es": "Tema:", + "zh": "主题:", + "ja": "テーマ:", + "ru": "Тема:", + "fr": "Thème :", + "hi": "थीम:", + "ar": "السمة:", + "bn": "থিম:", + "pt": "Tema:" + }, + "System Tray aktivieren (Minimieren statt Beenden)": { + "de": "System-Tray aktivieren (Minimieren statt Beenden)", + "en": "Enable system tray (minimize instead of quitting)", + "es": "Activar la bandeja del sistema (minimizar en lugar de salir)", + "zh": "启用系统托盘(最小化而非退出)", + "ja": "システムトレイを有効にする(終了せず最小化)", + "ru": "Включить область уведомлений (сворачивать вместо выхода)", + "fr": "Activer la zone de notification (réduire au lieu de quitter)", + "hi": "सिस्टम ट्रे चालू करें (बंद करने के बजाय छोटा करें)", + "ar": "تفعيل علبة النظام (تصغير بدلًا من الإنهاء)", + "bn": "সিস্টেম ট্রে চালু করুন (বন্ধ না করে মিনিমাইজ করুন)", + "pt": "Ativar a área de notificação (minimizar em vez de sair)" + }, + "Sichtbare Medientypen:": { + "de": "Sichtbare Medientypen:", + "en": "Visible media types:", + "es": "Tipos de medios visibles:", + "zh": "可见媒体类型:", + "ja": "表示するメディアの種類:", + "ru": "Видимые типы медиа:", + "fr": "Types de média visibles :", + "hi": "दिखने वाले मीडिया प्रकार:", + "ar": "أنواع الوسائط الظاهرة:", + "bn": "দৃশ্যমান মিডিয়ার ধরন:", + "pt": "Tipos de mídia visíveis:" + }, + "{media_type} in der MediaBrain-Bibliotheks-Navigation anzeigen": { + "de": "{media_type} in der MediaBrain-Bibliotheksnavigation anzeigen", + "en": "Show {media_type} in the MediaBrain library navigation", + "es": "Mostrar {media_type} en la navegación de la biblioteca de MediaBrain", + "zh": "在 MediaBrain 媒体库导航中显示{media_type}", + "ja": "MediaBrain ライブラリのナビゲーションに{media_type}を表示", + "ru": "Показывать «{media_type}» в навигации библиотеки MediaBrain", + "fr": "Afficher {media_type} dans la navigation de la bibliothèque MediaBrain", + "hi": "MediaBrain लाइब्रेरी नेविगेशन में {media_type} दिखाएँ", + "ar": "إظهار {media_type} في قائمة مكتبة MediaBrain", + "bn": "MediaBrain লাইব্রেরি নেভিগেশনে {media_type} দেখান", + "pt": "Mostrar {media_type} na navegação da biblioteca do MediaBrain" + }, + "Allgemein": { + "de": "Allgemein", + "en": "General", + "es": "General", + "zh": "常规", + "ja": "一般", + "ru": "Общие", + "fr": "Général", + "hi": "सामान्य", + "ar": "عام", + "bn": "সাধারণ", + "pt": "Geral" + }, + "Provider": { + "de": "Anbieter", + "en": "Provider", + "es": "Proveedor", + "zh": "提供方", + "ja": "プロバイダー", + "ru": "Провайдер", + "fr": "Fournisseur", + "hi": "प्रदाता", + "ar": "المزوّد", + "bn": "প্রদানকারী", + "pt": "Provedor" + }, + "Löschen lokaler Dateien erlauben": { + "de": "Löschen lokaler Dateien erlauben", + "en": "Allow deletion of local files", + "es": "Permitir eliminar archivos locales", + "zh": "允许删除本地文件", + "ja": "ローカルファイルの削除を許可", + "ru": "Разрешить удаление локальных файлов", + "fr": "Autoriser la suppression des fichiers locaux", + "hi": "स्थानीय फ़ाइलें हटाने की अनुमति दें", + "ar": "السماح بحذف الملفات المحلية", + "bn": "স্থানীয় ফাইল মুছতে অনুমতি দিন", + "pt": "Permitir a exclusão de arquivos locais" + }, + "Sicherheit": { + "de": "Sicherheit", + "en": "Security", + "es": "Seguridad", + "zh": "安全", + "ja": "セキュリティ", + "ru": "Безопасность", + "fr": "Sécurité", + "hi": "सुरक्षा", + "ar": "الأمان", + "bn": "নিরাপত্তা", + "pt": "Segurança" + }, + "Alle": { + "de": "Alle", + "en": "All", + "es": "Todos", + "zh": "全部", + "ja": "すべて", + "ru": "Все", + "fr": "Tout", + "hi": "सभी", + "ar": "الكل", + "bn": "সব", + "pt": "Todos" + }, + "Alle Provider": { + "de": "Alle Anbieter", + "en": "All providers", + "es": "Todos los proveedores", + "zh": "所有提供方", + "ja": "すべてのプロバイダー", + "ru": "Все провайдеры", + "fr": "Tous les fournisseurs", + "hi": "सभी प्रदाता", + "ar": "كل المزوّدين", + "bn": "সব প্রদানকারী", + "pt": "Todos os provedores" + }, + "Alle Dauern": { + "de": "Alle Zeiträume", + "en": "All durations", + "es": "Todas las duraciones", + "zh": "所有时长", + "ja": "すべての期間", + "ru": "Все сроки", + "fr": "Toutes les durées", + "hi": "सभी अवधि", + "ar": "كل المدد", + "bn": "সব সময়কাল", + "pt": "Todas as durações" + }, + "1 Tag": { + "de": "1 Tag", + "en": "1 day", + "es": "1 día", + "zh": "1 天", + "ja": "1日", + "ru": "1 день", + "fr": "1 jour", + "hi": "1 दिन", + "ar": "يوم واحد", + "bn": "১ দিন", + "pt": "1 dia" + }, + "1 Woche": { + "de": "1 Woche", + "en": "1 week", + "es": "1 semana", + "zh": "1 周", + "ja": "1週間", + "ru": "1 неделя", + "fr": "1 semaine", + "hi": "1 सप्ताह", + "ar": "أسبوع واحد", + "bn": "১ সপ্তাহ", + "pt": "1 semana" + }, + "1 Monat": { + "de": "1 Monat", + "en": "1 month", + "es": "1 mes", + "zh": "1 个月", + "ja": "1か月", + "ru": "1 месяц", + "fr": "1 mois", + "hi": "1 महीना", + "ar": "شهر واحد", + "bn": "১ মাস", + "pt": "1 mês" + }, + "3 Monate": { + "de": "3 Monate", + "en": "3 months", + "es": "3 meses", + "zh": "3 个月", + "ja": "3か月", + "ru": "3 месяца", + "fr": "3 mois", + "hi": "3 महीने", + "ar": "3 أشهر", + "bn": "৩ মাস", + "pt": "3 meses" + }, + "1 Jahr": { + "de": "1 Jahr", + "en": "1 year", + "es": "1 año", + "zh": "1 年", + "ja": "1年", + "ru": "1 год", + "fr": "1 an", + "hi": "1 वर्ष", + "ar": "سنة واحدة", + "bn": "১ বছর", + "pt": "1 ano" + }, + "Für immer": { + "de": "Für immer", + "en": "Forever", + "es": "Para siempre", + "zh": "永久", + "ja": "無期限", + "ru": "Бессрочно", + "fr": "Pour toujours", + "hi": "हमेशा के लिए", + "ar": "إلى الأبد", + "bn": "চিরদিনের জন্য", + "pt": "Para sempre" + }, + "Nur abgelaufen": { + "de": "Nur abgelaufene", + "en": "Expired only", + "es": "Solo caducados", + "zh": "仅显示已过期", + "ja": "期限切れのみ", + "ru": "Только истёкшие", + "fr": "Expirés uniquement", + "hi": "केवल समाप्त हो चुके", + "ar": "المنتهية فقط", + "bn": "শুধু মেয়াদোত্তীর্ণ", + "pt": "Somente expirados" + }, + "Nur aktiv": { + "de": "Nur aktive", + "en": "Active only", + "es": "Solo activos", + "zh": "仅显示有效", + "ja": "有効のみ", + "ru": "Только активные", + "fr": "Actifs uniquement", + "hi": "केवल सक्रिय", + "ar": "النشطة فقط", + "bn": "শুধু সক্রিয়", + "pt": "Somente ativos" + }, + "Abgelaufene entfernen": { + "de": "Abgelaufene entfernen", + "en": "Remove expired", + "es": "Eliminar los caducados", + "zh": "移除已过期项", + "ja": "期限切れを削除", + "ru": "Удалить истёкшие", + "fr": "Supprimer les éléments expirés", + "hi": "समाप्त हो चुके हटाएँ", + "ar": "إزالة المنتهية", + "bn": "মেয়াদোত্তীর্ণ সরান", + "pt": "Remover expirados" + }, + "Alle entfernen": { + "de": "Alle entfernen", + "en": "Remove all", + "es": "Eliminar todos", + "zh": "全部移除", + "ja": "すべて削除", + "ru": "Удалить все", + "fr": "Tout supprimer", + "hi": "सभी हटाएँ", + "ar": "إزالة الكل", + "bn": "সব সরান", + "pt": "Remover todos" + }, + "Entfernen": { + "de": "Entfernen", + "en": "Remove", + "es": "Quitar", + "zh": "移除", + "ja": "削除", + "ru": "Удалить", + "fr": "Retirer", + "hi": "हटाएँ", + "ar": "إزالة", + "bn": "সরান", + "pt": "Remover" + }, + "Sperrcode: {code}": { + "de": "Sperrcode: {code}", + "en": "Block code: {code}", + "es": "Código de bloqueo: {code}", + "zh": "封锁代码:{code}", + "ja": "ブロックコード: {code}", + "ru": "Код блокировки: {code}", + "fr": "Code de blocage : {code}", + "hi": "ब्लॉक कोड: {code}", + "ar": "رمز الحظر: {code}", + "bn": "ব্লক কোড: {code}", + "pt": "Código de bloqueio: {code}" + }, + "Seit: {date}": { + "de": "Seit: {date}", + "en": "Since: {date}", + "es": "Desde: {date}", + "zh": "开始时间:{date}", + "ja": "開始日: {date}", + "ru": "С: {date}", + "fr": "Depuis le : {date}", + "hi": "तारीख से: {date}", + "ar": "منذ: {date}", + "bn": "যখন থেকে: {date}", + "pt": "Desde: {date}" + }, + "Status: Abgelaufen": { + "de": "Status: Abgelaufen", + "en": "Status: Expired", + "es": "Estado: Caducado", + "zh": "状态:已过期", + "ja": "状態: 期限切れ", + "ru": "Статус: срок истёк", + "fr": "Statut : expiré", + "hi": "स्थिति: समाप्त", + "ar": "الحالة: منتهية", + "bn": "অবস্থা: মেয়াদ শেষ", + "pt": "Status: expirado" + }, + "Ablaufdatum: {expiry}": { + "de": "Ablaufdatum: {expiry}", + "en": "Expiration date: {expiry}", + "es": "Fecha de caducidad: {expiry}", + "zh": "到期日期:{expiry}", + "ja": "有効期限: {expiry}", + "ru": "Дата окончания: {expiry}", + "fr": "Date d’expiration : {expiry}", + "hi": "समाप्ति तिथि: {expiry}", + "ar": "تاريخ الانتهاء: {expiry}", + "bn": "মেয়াদ শেষের তারিখ: {expiry}", + "pt": "Data de expiração: {expiry}" + }, + "Ablaufdatum: Nie": { + "de": "Ablaufdatum: Nie", + "en": "Expiration date: Never", + "es": "Fecha de caducidad: Nunca", + "zh": "到期日期:永不过期", + "ja": "有効期限: なし", + "ru": "Дата окончания: никогда", + "fr": "Date d’expiration : jamais", + "hi": "समाप्ति तिथि: कभी नहीं", + "ar": "تاريخ الانتهاء: أبدًا", + "bn": "মেয়াদ শেষের তারিখ: কখনো নয়", + "pt": "Data de expiração: nunca" + }, + "Status: Aktiv": { + "de": "Status: Aktiv", + "en": "Status: Active", + "es": "Estado: Activo", + "zh": "状态:有效", + "ja": "状態: 有効", + "ru": "Статус: активно", + "fr": "Statut : actif", + "hi": "स्थिति: सक्रिय", + "ar": "الحالة: نشطة", + "bn": "অবস্থা: সক্রিয়", + "pt": "Status: ativo" + }, + "Keine Beschreibung verfügbar.": { + "de": "Keine Beschreibung verfügbar.", + "en": "No description available.", + "es": "No hay descripción disponible.", + "zh": "没有可用的描述。", + "ja": "説明はありません。", + "ru": "Описание недоступно.", + "fr": "Aucune description disponible.", + "hi": "कोई विवरण उपलब्ध नहीं है।", + "ar": "لا يوجد وصف متاح.", + "bn": "কোনো বিবরণ পাওয়া যায়নি।", + "pt": "Nenhuma descrição disponível." + }, + "[Thumbnail: {url}]": { + "de": "[Vorschaubild: {url}]", + "en": "[Thumbnail: {url}]", + "es": "[Miniatura: {url}]", + "zh": "[缩略图:{url}]", + "ja": "[サムネイル: {url}]", + "ru": "[Миниатюра: {url}]", + "fr": "[Vignette : {url}]", + "hi": "[थंबनेल: {url}]", + "ar": "[صورة مصغرة: {url}]", + "bn": "[থাম্বনেইল: {url}]", + "pt": "[Miniatura: {url}]" + }, + "Typ: {type}\nProvider: {provider}\nProvider-ID: {provider_id}\nErstellt am: {created_at}\nZuletzt geöffnet: {last_opened_at}\nÖffnungsmethode: {open_method}": { + "de": "Typ: {type}\nAnbieter: {provider}\nAnbieter-ID: {provider_id}\nErstellt am: {created_at}\nZuletzt geöffnet: {last_opened_at}\nÖffnungsmethode: {open_method}", + "en": "Type: {type}\nProvider: {provider}\nProvider ID: {provider_id}\nCreated: {created_at}\nLast opened: {last_opened_at}\nOpen method: {open_method}", + "es": "Tipo: {type}\nProveedor: {provider}\nID del proveedor: {provider_id}\nFecha de creación: {created_at}\nÚltima apertura: {last_opened_at}\nMétodo de apertura: {open_method}", + "zh": "类型:{type}\n提供方:{provider}\n提供方 ID:{provider_id}\n创建时间:{created_at}\n上次打开:{last_opened_at}\n打开方式:{open_method}", + "ja": "種類: {type}\nプロバイダー: {provider}\nプロバイダーID: {provider_id}\n作成日時: {created_at}\n最終表示: {last_opened_at}\n起動方法: {open_method}", + "ru": "Тип: {type}\nПровайдер: {provider}\nID провайдера: {provider_id}\nСоздано: {created_at}\nПоследнее открытие: {last_opened_at}\nСпособ открытия: {open_method}", + "fr": "Type : {type}\nFournisseur : {provider}\nID fournisseur : {provider_id}\nCréé le : {created_at}\nDernière ouverture : {last_opened_at}\nMéthode d’ouverture : {open_method}", + "hi": "प्रकार: {type}\nप्रदाता: {provider}\nप्रदाता ID: {provider_id}\nबनाया गया: {created_at}\nअंतिम बार खोला गया: {last_opened_at}\nखोलने का तरीका: {open_method}", + "ar": "النوع: {type}\nالمزوّد: {provider}\nمعرّف المزوّد: {provider_id}\nتاريخ الإنشاء: {created_at}\nآخر فتح: {last_opened_at}\nطريقة الفتح: {open_method}", + "bn": "ধরন: {type}\nপ্রদানকারী: {provider}\nপ্রদানকারীর আইডি: {provider_id}\nতৈরি হয়েছে: {created_at}\nসর্বশেষ খোলা: {last_opened_at}\nখোলার পদ্ধতি: {open_method}", + "pt": "Tipo: {type}\nProvedor: {provider}\nID do provedor: {provider_id}\nCriado em: {created_at}\nÚltima abertura: {last_opened_at}\nMétodo de abertura: {open_method}" + }, + "Online-Metadaten:": { + "de": "Online-Metadaten:", + "en": "Online metadata:", + "es": "Metadatos en línea:", + "zh": "在线元数据:", + "ja": "オンラインメタデータ:", + "ru": "Метаданные из интернета:", + "fr": "Métadonnées en ligne :", + "hi": "ऑनलाइन मेटाडेटा:", + "ar": "البيانات الوصفية عبر الإنترنت:", + "bn": "অনলাইন মেটাডেটা:", + "pt": "Metadados online:" + }, + "Lade Online-Metadaten...": { + "de": "Lade Online-Metadaten...", + "en": "Loading online metadata...", + "es": "Cargando metadatos en línea...", + "zh": "正在加载在线元数据...", + "ja": "オンラインメタデータを読み込み中...", + "ru": "Загрузка метаданных из интернета...", + "fr": "Chargement des métadonnées en ligne...", + "hi": "ऑनलाइन मेटाडेटा लोड हो रहा है...", + "ar": "جارٍ تحميل البيانات الوصفية عبر الإنترنت...", + "bn": "অনলাইন মেটাডেটা লোড হচ্ছে...", + "pt": "Carregando metadados online..." + }, + "Favorit": { + "de": "Favorit", + "en": "Favorite", + "es": "Favorito", + "zh": "收藏", + "ja": "お気に入り", + "ru": "Избранное", + "fr": "Favori", + "hi": "पसंदीदा", + "ar": "المفضلة", + "bn": "পছন্দের", + "pt": "Favorito" + }, + "Online-Beschreibung:": { + "de": "Online-Beschreibung:", + "en": "Online description:", + "es": "Descripción en línea:", + "zh": "在线描述:", + "ja": "オンラインの説明:", + "ru": "Описание из интернета:", + "fr": "Description en ligne :", + "hi": "ऑनलाइन विवरण:", + "ar": "الوصف عبر الإنترنت:", + "bn": "অনলাইন বিবরণ:", + "pt": "Descrição online:" + }, + "Keine Online-Metadaten gefunden.": { + "de": "Keine Online-Metadaten gefunden.", + "en": "No online metadata found.", + "es": "No se encontraron metadatos en línea.", + "zh": "未找到在线元数据。", + "ja": "オンラインメタデータが見つかりません。", + "ru": "Метаданные из интернета не найдены.", + "fr": "Aucune métadonnée en ligne trouvée.", + "hi": "ऑनलाइन मेटाडेटा नहीं मिला।", + "ar": "لم يتم العثور على بيانات وصفية عبر الإنترنت.", + "bn": "অনলাইন মেটাডেটা পাওয়া যায়নি।", + "pt": "Nenhum metadado online encontrado." + }, + "Fehler: {error}": { + "de": "Fehler: {error}", + "en": "Error: {error}", + "es": "Error: {error}", + "zh": "错误:{error}", + "ja": "エラー: {error}", + "ru": "Ошибка: {error}", + "fr": "Erreur : {error}", + "hi": "त्रुटि: {error}", + "ar": "خطأ: {error}", + "bn": "ত্রুটি: {error}", + "pt": "Erro: {error}" + }, + "Jahr: {year}": { + "de": "Jahr: {year}", + "en": "Year: {year}", + "es": "Año: {year}", + "zh": "年份:{year}", + "ja": "年: {year}", + "ru": "Год: {year}", + "fr": "Année : {year}", + "hi": "वर्ष: {year}", + "ar": "السنة: {year}", + "bn": "বছর: {year}", + "pt": "Ano: {year}" + }, + "Bewertung: {rating}/10": { + "de": "Bewertung: {rating}/10", + "en": "Rating: {rating}/10", + "es": "Calificación: {rating}/10", + "zh": "评分:{rating}/10", + "ja": "評価: {rating}/10", + "ru": "Оценка: {rating}/10", + "fr": "Note : {rating}/10", + "hi": "रेटिंग: {rating}/10", + "ar": "التقييم: {rating}/10", + "bn": "রেটিং: {rating}/10", + "pt": "Avaliação: {rating}/10" + }, + "Genres: {genres}": { + "de": "Genres: {genres}", + "en": "Genres: {genres}", + "es": "Géneros: {genres}", + "zh": "类型:{genres}", + "ja": "ジャンル: {genres}", + "ru": "Жанры: {genres}", + "fr": "Genres : {genres}", + "hi": "शैलियाँ: {genres}", + "ar": "الأنواع: {genres}", + "bn": "ধরন: {genres}", + "pt": "Gêneros: {genres}" + }, + "Laufzeit: {minutes} Min": { + "de": "Laufzeit: {minutes} Min", + "en": "Runtime: {minutes} min", + "es": "Duración: {minutes} min", + "zh": "时长:{minutes} 分钟", + "ja": "再生時間: {minutes} 分", + "ru": "Длительность: {minutes} мин", + "fr": "Durée : {minutes} min", + "hi": "अवधि: {minutes} मिनट", + "ar": "المدة: {minutes} دقيقة", + "bn": "সময়কাল: {minutes} মিনিট", + "pt": "Duração: {minutes} min" + }, + "Regie: {director}": { + "de": "Regie: {director}", + "en": "Director: {director}", + "es": "Dirección: {director}", + "zh": "导演:{director}", + "ja": "監督: {director}", + "ru": "Режиссёр: {director}", + "fr": "Réalisation : {director}", + "hi": "निर्देशक: {director}", + "ar": "الإخراج: {director}", + "bn": "পরিচালক: {director}", + "pt": "Direção: {director}" + }, + "Vorschaubild: verfügbar": { + "de": "Vorschaubild: verfügbar", + "en": "Thumbnail: available", + "es": "Miniatura: disponible", + "zh": "缩略图:可用", + "ja": "サムネイル: 利用可能", + "ru": "Миниатюра: доступна", + "fr": "Vignette : disponible", + "hi": "थंबनेल: उपलब्ध", + "ar": "الصورة المصغرة: متاحة", + "bn": "থাম্বনেইল: উপলভ্য", + "pt": "Miniatura: disponível" + }, + "Gesamtanzahl Medien: {count}": { + "de": "Gesamtanzahl Medien: {count}", + "en": "Total media: {count}", + "es": "Total de medios: {count}", + "zh": "媒体总数:{count}", + "ja": "メディア総数: {count}", + "ru": "Всего медиа: {count}", + "fr": "Nombre total de médias : {count}", + "hi": "कुल मीडिया: {count}", + "ar": "إجمالي الوسائط: {count}", + "bn": "মোট মিডিয়া: {count}", + "pt": "Total de mídias: {count}" + }, + "

📊 Statistiken & Dashboard

": { + "de": "

📊 Statistiken & Dashboard

", + "en": "

📊 Statistics & Dashboard

", + "es": "

📊 Estadísticas y panel

", + "zh": "

📊 统计与仪表板

", + "ja": "

📊 統計とダッシュボード

", + "ru": "

📊 Статистика и панель управления

", + "fr": "

📊 Statistiques et tableau de bord

", + "hi": "

📊 आँकड़े और डैशबोर्ड

", + "ar": "

📊 الإحصاءات ولوحة المعلومات

", + "bn": "

📊 পরিসংখ্যান ও ড্যাশবোর্ড

", + "pt": "

📊 Estatísticas e painel

" + }, + "Medien nach Typ:": { + "de": "Medien nach Typ:", + "en": "Media by type:", + "es": "Medios por tipo:", + "zh": "按类型统计媒体:", + "ja": "種類別メディア:", + "ru": "Медиа по типам:", + "fr": "Médias par type :", + "hi": "प्रकार के अनुसार मीडिया:", + "ar": "الوسائط حسب النوع:", + "bn": "ধরন অনুযায়ী মিডিয়া:", + "pt": "Mídias por tipo:" + }, + "Keine Favoriten markiert.": { + "de": "Keine Favoriten markiert.", + "en": "No favorites marked.", + "es": "No hay favoritos marcados.", + "zh": "没有标记的收藏项。", + "ja": "お気に入りはありません。", + "ru": "Избранное не отмечено.", + "fr": "Aucun favori marqué.", + "hi": "कोई पसंदीदा चिह्नित नहीं है।", + "ar": "لا توجد عناصر مفضلة محددة.", + "bn": "কোনো পছন্দের আইটেম চিহ্নিত নেই।", + "pt": "Nenhum favorito marcado." + }, + "Fehler beim Laden des Dashboards: {error}": { + "de": "Fehler beim Laden des Dashboards: {error}", + "en": "Could not load dashboard: {error}", + "es": "Error al cargar el panel: {error}", + "zh": "加载仪表板时出错:{error}", + "ja": "ダッシュボードを読み込めませんでした: {error}", + "ru": "Не удалось загрузить панель управления: {error}", + "fr": "Erreur lors du chargement du tableau de bord : {error}", + "hi": "डैशबोर्ड लोड करने में त्रुटि: {error}", + "ar": "تعذر تحميل لوحة المعلومات: {error}", + "bn": "ড্যাশবোর্ড লোড করতে ত্রুটি: {error}", + "pt": "Falha ao carregar o painel: {error}" + }, + "Geschätzter Speicher (lokal): {size_gb} GB ({file_count} Dateien)": { + "de": "Geschätzter Speicher (lokal): {size_gb} GB ({file_count} Dateien)", + "en": "Estimated storage (local): {size_gb} GB ({file_count} files)", + "es": "Almacenamiento estimado (local): {size_gb} GB ({file_count} archivos)", + "zh": "预计本地存储:{size_gb} GB({file_count} 个文件)", + "ja": "推定ローカル容量: {size_gb} GB({file_count} ファイル)", + "ru": "Примерный локальный объём: {size_gb} ГБ ({file_count} файлов)", + "fr": "Stockage local estimé : {size_gb} Go ({file_count} fichiers)", + "hi": "अनुमानित स्थानीय संग्रहण: {size_gb} GB ({file_count} फ़ाइलें)", + "ar": "التخزين المحلي المقدّر: {size_gb} غيغابايت ({file_count} ملفًا)", + "bn": "আনুমানিক স্থানীয় স্টোরেজ: {size_gb} GB ({file_count}টি ফাইল)", + "pt": "Armazenamento local estimado: {size_gb} GB ({file_count} arquivos)" + }, + "Blacklist": { + "de": "Sperrliste", + "en": "Blacklist", + "es": "Lista de bloqueo", + "zh": "黑名单", + "ja": "ブロックリスト", + "ru": "Чёрный список", + "fr": "Liste de blocage", + "hi": "ब्लॉकलिस्ट", + "ar": "قائمة الحظر", + "bn": "ব্ল্যাকলিস্ট", + "pt": "Lista de bloqueio" + }, + "📊 Statistiken": { + "de": "📊 Statistiken", + "en": "📊 Statistics", + "es": "📊 Estadísticas", + "zh": "📊 统计", + "ja": "📊 統計", + "ru": "📊 Статистика", + "fr": "📊 Statistiques", + "hi": "📊 आँकड़े", + "ar": "📊 الإحصاءات", + "bn": "📊 পরিসংখ্যান", + "pt": "📊 Estatísticas" + }, + "Playlists": { + "de": "Playlists", + "en": "Playlists", + "es": "Listas de reproducción", + "zh": "播放列表", + "ja": "プレイリスト", + "ru": "Плейлисты", + "fr": "Listes de lecture", + "hi": "प्लेलिस्ट", + "ar": "قوائم التشغيل", + "bn": "প্লেলিস্ট", + "pt": "Listas de reprodução" + }, + "MediaBrain Export speichern": { + "de": "MediaBrain-Export speichern", + "en": "Save MediaBrain export", + "es": "Guardar exportación de MediaBrain", + "zh": "保存 MediaBrain 导出文件", + "ja": "MediaBrain のエクスポートを保存", + "ru": "Сохранить экспорт MediaBrain", + "fr": "Enregistrer l’export MediaBrain", + "hi": "MediaBrain निर्यात सहेजें", + "ar": "حفظ تصدير MediaBrain", + "bn": "MediaBrain এক্সপোর্ট সংরক্ষণ করুন", + "pt": "Salvar exportação do MediaBrain" + }, + "JSON-Datei (*.json)": { + "de": "JSON-Datei (*.json)", + "en": "JSON file (*.json)", + "es": "Archivo JSON (*.json)", + "zh": "JSON 文件 (*.json)", + "ja": "JSON ファイル (*.json)", + "ru": "Файл JSON (*.json)", + "fr": "Fichier JSON (*.json)", + "hi": "JSON फ़ाइल (*.json)", + "ar": "ملف JSON (*.json)", + "bn": "JSON ফাইল (*.json)", + "pt": "Arquivo JSON (*.json)" + }, + "MediaBrain Export laden": { + "de": "MediaBrain-Export laden", + "en": "Load MediaBrain export", + "es": "Cargar exportación de MediaBrain", + "zh": "加载 MediaBrain 导出文件", + "ja": "MediaBrain のエクスポートを読み込む", + "ru": "Загрузить экспорт MediaBrain", + "fr": "Charger l’export MediaBrain", + "hi": "MediaBrain निर्यात लोड करें", + "ar": "تحميل تصدير MediaBrain", + "bn": "MediaBrain এক্সপোর্ট লোড করুন", + "pt": "Carregar exportação do MediaBrain" + }, + "Import-Modus": { + "de": "Importmodus", + "en": "Import mode", + "es": "Modo de importación", + "zh": "导入模式", + "ja": "インポートモード", + "ru": "Режим импорта", + "fr": "Mode d’importation", + "hi": "आयात मोड", + "ar": "وضع الاستيراد", + "bn": "ইম্পোর্ট মোড", + "pt": "Modo de importação" + }, + "Bestehende Einträge mit gleicher Quelle + Provider-ID überspringen?\nNein = vorhandene Datensätze ersetzen.": { + "de": "Einträge mit gleicher Quelle und Anbieter-ID überspringen?\nNein = vorhandene Datensätze ersetzen.", + "en": "Skip existing entries with the same source and provider ID?\nNo = replace existing records.", + "es": "¿Omitir las entradas existentes con la misma fuente e ID de proveedor?\nNo = reemplazar los registros existentes.", + "zh": "跳过来源和提供方 ID 相同的现有条目?\n选择“否”将替换现有记录。", + "ja": "ソースとプロバイダーIDが同じ既存項目をスキップしますか?\n「いいえ」を選ぶと既存の記録を置き換えます。", + "ru": "Пропустить записи с тем же источником и ID провайдера?\n«Нет» — заменить существующие записи.", + "fr": "Ignorer les entrées existantes ayant la même source et le même ID fournisseur ?\nNon = remplacer les enregistrements existants.", + "hi": "क्या समान स्रोत और प्रदाता ID वाली मौजूदा प्रविष्टियाँ छोड़ें?\n“नहीं” चुनने पर मौजूदा रिकॉर्ड बदल दिए जाएँगे।", + "ar": "هل تريد تخطي السجلات الموجودة ذات المصدر ومعرّف المزوّد نفسيهما؟\n«لا» يعني استبدال السجلات الحالية.", + "bn": "একই উৎস ও প্রদানকারী আইডির বিদ্যমান এন্ট্রি বাদ দেবেন?\n“না” মানে বিদ্যমান রেকর্ড প্রতিস্থাপন করা।", + "pt": "Ignorar entradas existentes com a mesma fonte e ID do provedor?\nNão = substituir os registros existentes." + }, + "Export abgeschlossen": { + "de": "Export abgeschlossen", + "en": "Export complete", + "es": "Exportación completada", + "zh": "导出完成", + "ja": "エクスポート完了", + "ru": "Экспорт завершён", + "fr": "Exportation terminée", + "hi": "निर्यात पूरा हुआ", + "ar": "اكتمل التصدير", + "bn": "এক্সপোর্ট সম্পন্ন", + "pt": "Exportação concluída" + }, + "{count} Medien exportiert nach:\n{path}": { + "de": "{count} Medien exportiert nach:\n{path}", + "en": "Exported {count} media items to:\n{path}", + "es": "Se exportaron {count} elementos multimedia a:\n{path}", + "zh": "已将 {count} 个媒体项导出到:\n{path}", + "ja": "{count} 件のメディアを次の場所にエクスポートしました:\n{path}", + "ru": "Экспортировано медиа: {count}\nПуть: {path}", + "fr": "{count} médias exportés vers :\n{path}", + "hi": "{count} मीडिया आइटम यहाँ निर्यात किए गए:\n{path}", + "ar": "تم تصدير {count} عنصر وسائط إلى:\n{path}", + "bn": "{count}টি মিডিয়া আইটেম এক্সপোর্ট করা হয়েছে:\n{path}", + "pt": "{count} mídias exportadas para:\n{path}" + }, + "Import abgeschlossen": { + "de": "Import abgeschlossen", + "en": "Import complete", + "es": "Importación completada", + "zh": "导入完成", + "ja": "インポート完了", + "ru": "Импорт завершён", + "fr": "Importation terminée", + "hi": "आयात पूरा हुआ", + "ar": "اكتمل الاستيراد", + "bn": "ইম্পোর্ট সম্পন্ন", + "pt": "Importação concluída" + }, + "Importiert: {imported}\nÜbersprungen: {skipped}\nFehler: {errors}": { + "de": "Importiert: {imported}\nÜbersprungen: {skipped}\nFehler: {errors}", + "en": "Imported: {imported}\nSkipped: {skipped}\nErrors: {errors}", + "es": "Importados: {imported}\nOmitidos: {skipped}\nErrores: {errors}", + "zh": "已导入:{imported}\n已跳过:{skipped}\n错误:{errors}", + "ja": "インポート: {imported}\nスキップ: {skipped}\nエラー: {errors}", + "ru": "Импортировано: {imported}\nПропущено: {skipped}\nОшибки: {errors}", + "fr": "Importés : {imported}\nIgnorés : {skipped}\nErreurs : {errors}", + "hi": "आयात किए गए: {imported}\nछोड़े गए: {skipped}\nत्रुटियाँ: {errors}", + "ar": "تم الاستيراد: {imported}\nتم التخطي: {skipped}\nالأخطاء: {errors}", + "bn": "ইম্পোর্ট হয়েছে: {imported}\nবাদ গেছে: {skipped}\nত্রুটি: {errors}", + "pt": "Importados: {imported}\nIgnorados: {skipped}\nErros: {errors}" + }, + "Export fehlgeschlagen": { + "de": "Export fehlgeschlagen", + "en": "Export failed", + "es": "Falló la exportación", + "zh": "导出失败", + "ja": "エクスポートに失敗しました", + "ru": "Не удалось выполнить экспорт", + "fr": "Échec de l’exportation", + "hi": "निर्यात विफल रहा", + "ar": "فشل التصدير", + "bn": "এক্সপোর্ট ব্যর্থ হয়েছে", + "pt": "Falha na exportação" + }, + "Import fehlgeschlagen": { + "de": "Import fehlgeschlagen", + "en": "Import failed", + "es": "Falló la importación", + "zh": "导入失败", + "ja": "インポートに失敗しました", + "ru": "Не удалось выполнить импорт", + "fr": "Échec de l’importation", + "hi": "आयात विफल रहा", + "ar": "فشل الاستيراد", + "bn": "ইম্পোর্ট ব্যর্থ হয়েছে", + "pt": "Falha na importação" + }, + "MediaBrain öffnen": { + "de": "MediaBrain öffnen", + "en": "Open MediaBrain", + "es": "Abrir MediaBrain", + "zh": "打开 MediaBrain", + "ja": "MediaBrain を開く", + "ru": "Открыть MediaBrain", + "fr": "Ouvrir MediaBrain", + "hi": "MediaBrain खोलें", + "ar": "فتح MediaBrain", + "bn": "MediaBrain খুলুন", + "pt": "Abrir o MediaBrain" + }, + "Beenden": { + "de": "Beenden", + "en": "Quit", + "es": "Salir", + "zh": "退出", + "ja": "終了", + "ru": "Выход", + "fr": "Quitter", + "hi": "बंद करें", + "ar": "إنهاء", + "bn": "প্রস্থান", + "pt": "Sair" + }, + "MediaBrain läuft im Hintergrund weiter.\nDoppelklick auf das Tray-Icon zum öffnen.": { + "de": "MediaBrain läuft im Hintergrund weiter.\nDoppelklick auf das Tray-Icon öffnet es wieder.", + "en": "MediaBrain will keep running in the background.\nDouble-click the tray icon to reopen it.", + "es": "MediaBrain seguirá ejecutándose en segundo plano.\nHaz doble clic en el icono de la bandeja para volver a abrirlo.", + "zh": "MediaBrain 将继续在后台运行。\n双击托盘图标可重新打开。", + "ja": "MediaBrain はバックグラウンドで動作し続けます。\nトレイアイコンをダブルクリックすると再び開きます。", + "ru": "MediaBrain продолжит работать в фоновом режиме.\nДважды щёлкните значок, чтобы снова открыть приложение.", + "fr": "MediaBrain continuera de fonctionner en arrière-plan.\nDouble-cliquez sur l’icône pour le rouvrir.", + "hi": "MediaBrain बैकग्राउंड में चलता रहेगा।\nइसे फिर खोलने के लिए ट्रे आइकन पर डबल-क्लिक करें।", + "ar": "سيواصل MediaBrain العمل في الخلفية.\nانقر نقرًا مزدوجًا على الأيقونة لفتحه مجددًا.", + "bn": "MediaBrain ব্যাকগ্রাউন্ডে চলতে থাকবে।\nআবার খুলতে ট্রে আইকনে দুবার ক্লিক করুন।", + "pt": "O MediaBrain continuará em execução em segundo plano.\nClique duas vezes no ícone para abri-lo novamente." + }, + "Light": { + "de": "Hell", + "en": "Light", + "es": "Claro", + "zh": "浅色", + "ja": "ライト", + "ru": "Светлая", + "fr": "Clair", + "hi": "हल्का", + "ar": "فاتح", + "bn": "হালকা", + "pt": "Claro" + }, + "Dark": { + "de": "Dunkel", + "en": "Dark", + "es": "Oscuro", + "zh": "深色", + "ja": "ダーク", + "ru": "Тёмная", + "fr": "Sombre", + "hi": "गहरा", + "ar": "داكن", + "bn": "গাঢ়", + "pt": "Escuro" + }, + "High Contrast": { + "de": "Hoher Kontrast", + "en": "High contrast", + "es": "Alto contraste", + "zh": "高对比度", + "ja": "ハイコントラスト", + "ru": "Высокая контрастность", + "fr": "Contraste élevé", + "hi": "उच्च कंट्रास्ट", + "ar": "تباين عالٍ", + "bn": "উচ্চ কনট্রাস্ট", + "pt": "Alto contraste" + }, + "browser": { + "de": "Browser", + "en": "Browser", + "es": "Navegador", + "zh": "浏览器", + "ja": "ブラウザー", + "ru": "Браузер", + "fr": "Navigateur", + "hi": "ब्राउज़र", + "ar": "المتصفح", + "bn": "ব্রাউজার", + "pt": "Navegador" + }, + "app": { + "de": "App", + "en": "App", + "es": "Aplicación", + "zh": "应用", + "ja": "アプリ", + "ru": "Приложение", + "fr": "Application", + "hi": "ऐप", + "ar": "تطبيق", + "bn": "অ্যাপ", + "pt": "Aplicativo" + }, + "local": { + "de": "Lokal", + "en": "Local", + "es": "Local", + "zh": "本地", + "ja": "ローカル", + "ru": "Локально", + "fr": "Local", + "hi": "स्थानीय", + "ar": "محلي", + "bn": "স্থানীয়", + "pt": "Local" + }, + "auto": { + "de": "Automatisch", + "en": "Automatic", + "es": "Automático", + "zh": "自动", + "ja": "自動", + "ru": "Автоматически", + "fr": "Automatique", + "hi": "स्वचालित", + "ar": "تلقائي", + "bn": "স্বয়ংক্রিয়", + "pt": "Automático" + }, + "Film": { + "de": "Film", + "en": "Movie", + "es": "Película", + "zh": "电影", + "ja": "映画", + "ru": "Фильм", + "fr": "Film", + "hi": "फ़िल्म", + "ar": "فيلم", + "bn": "চলচ্চিত্র", + "pt": "Filme" + }, + "Musik": { + "de": "Musik", + "en": "Music", + "es": "Música", + "zh": "音乐", + "ja": "音楽", + "ru": "Музыка", + "fr": "Musique", + "hi": "संगीत", + "ar": "موسيقى", + "bn": "সংগীত", + "pt": "Música" + }, + "Serie": { + "de": "Serie", + "en": "Series", + "es": "Serie", + "zh": "剧集", + "ja": "シリーズ", + "ru": "Сериал", + "fr": "Série", + "hi": "श्रृंखला", + "ar": "مسلسل", + "bn": "সিরিজ", + "pt": "Série" + }, + "Clip": { + "de": "Clip", + "en": "Clip", + "es": "Clip", + "zh": "短片", + "ja": "クリップ", + "ru": "Клип", + "fr": "Clip", + "hi": "क्लिप", + "ar": "مقطع", + "bn": "ক্লিপ", + "pt": "Clipe" + }, + "Video": { + "de": "Video", + "en": "Video", + "es": "Vídeo", + "zh": "视频", + "ja": "動画", + "ru": "Видео", + "fr": "Vidéo", + "hi": "वीडियो", + "ar": "فيديو", + "bn": "ভিডিও", + "pt": "Vídeo" + }, + "Kurzvideo": { + "de": "Kurzvideo", + "en": "Short video", + "es": "Vídeo corto", + "zh": "短视频", + "ja": "ショート動画", + "ru": "Короткое видео", + "fr": "Vidéo court", + "hi": "लघु वीडियो", + "ar": "فيديو قصير", + "bn": "ছোট ভিডিও", + "pt": "Vídeo curto" + }, + "YouTube-Clip": { + "de": "YouTube-Clip", + "en": "YouTube clip", + "es": "Clip de YouTube", + "zh": "YouTube 短片", + "ja": "YouTube クリップ", + "ru": "Клип YouTube", + "fr": "Clip YouTube", + "hi": "YouTube क्लिप", + "ar": "مقطع YouTube", + "bn": "YouTube ক্লিপ", + "pt": "Clipe do YouTube" + }, + "Podcast": { + "de": "Podcast", + "en": "Podcast", + "es": "Pódcast", + "zh": "播客", + "ja": "ポッドキャスト", + "ru": "Подкаст", + "fr": "Podcast", + "hi": "पॉडकास्ट", + "ar": "بودكاست", + "bn": "পডকাস্ট", + "pt": "Podcast" + }, + "Hörbuch": { + "de": "Hörbuch", + "en": "Audiobook", + "es": "Audiolibro", + "zh": "有声书", + "ja": "オーディオブック", + "ru": "Аудиокнига", + "fr": "Livre audio", + "hi": "ऑडियोबुक", + "ar": "كتاب صوتي", + "bn": "অডিওবুক", + "pt": "Audiolivro" + }, + "Dokument": { + "de": "Dokument", + "en": "Document", + "es": "Documento", + "zh": "文档", + "ja": "ドキュメント", + "ru": "Документ", + "fr": "Document", + "hi": "दस्तावेज़", + "ar": "مستند", + "bn": "নথি", + "pt": "Documento" + }, + "Medium": { + "de": "Medium", + "en": "Media item", + "es": "Elemento multimedia", + "zh": "媒体项", + "ja": "メディア項目", + "ru": "Медиа", + "fr": "Média", + "hi": "मीडिया आइटम", + "ar": "عنصر وسائط", + "bn": "মিডিয়া আইটেম", + "pt": "Mídia" + }, + "Filme": { + "de": "Filme", + "en": "Movies", + "es": "Películas", + "zh": "电影", + "ja": "映画", + "ru": "Фильмы", + "fr": "Films", + "hi": "फ़िल्में", + "ar": "أفلام", + "bn": "চলচ্চিত্র", + "pt": "Filmes" + }, + "Serien": { + "de": "Serien", + "en": "Series", + "es": "Series", + "zh": "剧集", + "ja": "シリーズ", + "ru": "Сериалы", + "fr": "Séries", + "hi": "श्रृंखलाएँ", + "ar": "مسلسلات", + "bn": "সিরিজ", + "pt": "Séries" + }, + "Clips": { + "de": "Clips", + "en": "Clips", + "es": "Clips", + "zh": "短片", + "ja": "クリップ", + "ru": "Клипы", + "fr": "Clips", + "hi": "क्लिप", + "ar": "مقاطع", + "bn": "ক্লিপ", + "pt": "Clipes" + }, + "Podcasts": { + "de": "Podcasts", + "en": "Podcasts", + "es": "Pódcast", + "zh": "播客", + "ja": "ポッドキャスト", + "ru": "Подкасты", + "fr": "Podcasts", + "hi": "पॉडकास्ट", + "ar": "بودكاست", + "bn": "পডকাস্ট", + "pt": "Podcasts" + }, + "Hörbücher": { + "de": "Hörbücher", + "en": "Audiobooks", + "es": "Audiolibros", + "zh": "有声书", + "ja": "オーディオブック", + "ru": "Аудиокниги", + "fr": "Livres audio", + "hi": "ऑडियोबुक", + "ar": "كتب صوتية", + "bn": "অডিওবুক", + "pt": "Audiolivros" + }, + "Dokumente": { + "de": "Dokumente", + "en": "Documents", + "es": "Documentos", + "zh": "文档", + "ja": "ドキュメント", + "ru": "Документы", + "fr": "Documents", + "hi": "दस्तावेज़", + "ar": "مستندات", + "bn": "নথি", + "pt": "Documentos" + }, + "Bedingung entfernen": { + "de": "Bedingung entfernen", + "en": "Remove condition", + "es": "Eliminar condición", + "zh": "移除条件", + "ja": "条件を削除", + "ru": "Удалить условие", + "fr": "Supprimer la condition", + "hi": "शर्त हटाएँ", + "ar": "إزالة الشرط", + "bn": "শর্ত সরান", + "pt": "Remover condição" + }, + "Entfernt diese Filterbedingung aus der Smart-Playlist.": { + "de": "Entfernt diese Filterbedingung aus der Smart-Playlist.", + "en": "Removes this filter condition from the smart playlist.", + "es": "Elimina esta condición de filtro de la lista inteligente.", + "zh": "从智能播放列表中移除此筛选条件。", + "ja": "この条件をスマートプレイリストから削除します。", + "ru": "Удаляет это условие из смарт-плейлиста.", + "fr": "Supprime cette condition de filtre de la playlist intelligente.", + "hi": "स्मार्ट प्लेलिस्ट से यह फ़िल्टर शर्त हटाता है।", + "ar": "يزيل شرط التصفية هذا من قائمة التشغيل الذكية.", + "bn": "স্মার্ট প্লেলিস্ট থেকে এই ফিল্টার শর্ত সরায়।", + "pt": "Remove esta condição de filtro da playlist inteligente." + }, + "Smart-Playlist": { + "de": "Intelligente Playlist", + "en": "Smart playlist", + "es": "Lista inteligente", + "zh": "智能播放列表", + "ja": "スマートプレイリスト", + "ru": "Смарт-плейлист", + "fr": "Playlist intelligente", + "hi": "स्मार्ट प्लेलिस्ट", + "ar": "قائمة تشغيل ذكية", + "bn": "স্মার্ট প্লেলিস্ট", + "pt": "Playlist inteligente" + }, + "Name der Playlist": { + "de": "Name der Playlist", + "en": "Playlist name", + "es": "Nombre de la lista", + "zh": "播放列表名称", + "ja": "プレイリスト名", + "ru": "Название плейлиста", + "fr": "Nom de la playlist", + "hi": "प्लेलिस्ट का नाम", + "ar": "اسم قائمة التشغيل", + "bn": "প্লেলিস্টের নাম", + "pt": "Nome da playlist" + }, + "Name": { + "de": "Name", + "en": "Name", + "es": "Nombre", + "zh": "名称", + "ja": "名前", + "ru": "Название", + "fr": "Nom", + "hi": "नाम", + "ar": "الاسم", + "bn": "নাম", + "pt": "Nome" + }, + "Optionale Beschreibung": { + "de": "Optionale Beschreibung", + "en": "Optional description", + "es": "Descripción opcional", + "zh": "可选描述", + "ja": "説明(任意)", + "ru": "Необязательное описание", + "fr": "Description facultative", + "hi": "वैकल्पिक विवरण", + "ar": "وصف اختياري", + "bn": "ঐচ্ছিক বিবরণ", + "pt": "Descrição opcional" + }, + "Beschreibung": { + "de": "Beschreibung", + "en": "Description", + "es": "Descripción", + "zh": "描述", + "ja": "説明", + "ru": "Описание", + "fr": "Description", + "hi": "विवरण", + "ar": "الوصف", + "bn": "বিবরণ", + "pt": "Descrição" + }, + "Bedingungen:": { + "de": "Bedingungen:", + "en": "Conditions:", + "es": "Condiciones:", + "zh": "条件:", + "ja": "条件:", + "ru": "Условия:", + "fr": "Conditions :", + "hi": "शर्तें:", + "ar": "الشروط:", + "bn": "শর্তাবলি:", + "pt": "Condições:" + }, + "(keine Sortierung)": { + "de": "(keine Sortierung)", + "en": "(no sorting)", + "es": "(sin ordenar)", + "zh": "(不排序)", + "ja": "(並べ替えなし)", + "ru": "(без сортировки)", + "fr": "(aucun tri)", + "hi": "(कोई क्रम नहीं)", + "ar": "(بدون ترتيب)", + "bn": "(কোনো সাজানো নয়)", + "pt": "(sem ordenação)" + }, + "Sortieren nach": { + "de": "Sortieren nach", + "en": "Sort by", + "es": "Ordenar por", + "zh": "排序依据", + "ja": "並べ替え基準", + "ru": "Сортировать по", + "fr": "Trier par", + "hi": "इसके अनुसार क्रमबद्ध करें", + "ar": "ترتيب حسب", + "bn": "যে অনুযায়ী সাজান", + "pt": "Ordenar por" + }, + "Aufsteigend": { + "de": "Aufsteigend", + "en": "Ascending", + "es": "Ascendente", + "zh": "升序", + "ja": "昇順", + "ru": "По возрастанию", + "fr": "Croissant", + "hi": "आरोही", + "ar": "تصاعدي", + "bn": "ঊর্ধ্বক্রম", + "pt": "Crescente" + }, + "Absteigend": { + "de": "Absteigend", + "en": "Descending", + "es": "Descendente", + "zh": "降序", + "ja": "降順", + "ru": "По убыванию", + "fr": "Décroissant", + "hi": "अवरोही", + "ar": "تنازلي", + "bn": "নিম্নক্রম", + "pt": "Decrescente" + }, + "Richtung": { + "de": "Richtung", + "en": "Direction", + "es": "Dirección", + "zh": "方向", + "ja": "順序", + "ru": "Направление", + "fr": "Ordre", + "hi": "दिशा", + "ar": "الاتجاه", + "bn": "দিক", + "pt": "Direção" + }, + "(unbegrenzt)": { + "de": "(unbegrenzt)", + "en": "(unlimited)", + "es": "(sin límite)", + "zh": "(不限)", + "ja": "(無制限)", + "ru": "(без ограничений)", + "fr": "(illimité)", + "hi": "(असीमित)", + "ar": "(غير محدود)", + "bn": "(সীমাহীন)", + "pt": "(ilimitado)" + }, + "Limit": { + "de": "Limit", + "en": "Limit", + "es": "Límite", + "zh": "上限", + "ja": "上限", + "ru": "Лимит", + "fr": "Limite", + "hi": "सीमा", + "ar": "الحد", + "bn": "সীমা", + "pt": "Limite" + }, + "Playlist": { + "de": "Playlist", + "en": "Playlist", + "es": "Lista de reproducción", + "zh": "播放列表", + "ja": "プレイリスト", + "ru": "Плейлист", + "fr": "Playlist", + "hi": "प्लेलिस्ट", + "ar": "قائمة تشغيل", + "bn": "প্লেলিস্ট", + "pt": "Playlist" + }, + "Neue Playlist": { + "de": "Neue Playlist", + "en": "New playlist", + "es": "Nueva lista de reproducción", + "zh": "新建播放列表", + "ja": "新しいプレイリスト", + "ru": "Новый плейлист", + "fr": "Nouvelle playlist", + "hi": "नई प्लेलिस्ट", + "ar": "قائمة تشغيل جديدة", + "bn": "নতুন প্লেলিস্ট", + "pt": "Nova playlist" + }, + "Neue Smart-Playlist": { + "de": "Neue intelligente Playlist", + "en": "New smart playlist", + "es": "Nueva lista inteligente", + "zh": "新建智能播放列表", + "ja": "新しいスマートプレイリスト", + "ru": "Новый смарт-плейлист", + "fr": "Nouvelle playlist intelligente", + "hi": "नई स्मार्ट प्लेलिस्ट", + "ar": "قائمة تشغيل ذكية جديدة", + "bn": "নতুন স্মার্ট প্লেলিস্ট", + "pt": "Nova playlist inteligente" + }, + "{name} ({kind}, {count})": { + "de": "{name} ({kind}, {count})", + "en": "{name} ({kind}, {count})", + "es": "{name} ({kind}, {count})", + "zh": "{name}({kind},{count})", + "ja": "{name}({kind}、{count})", + "ru": "{name} ({kind}, {count})", + "fr": "{name} ({kind}, {count})", + "hi": "{name} ({kind}, {count})", + "ar": "{name} ({kind}, {count})", + "bn": "{name} ({kind}, {count})", + "pt": "{name} ({kind}, {count})" + }, + "{name} | {kind} | {count} Einträge\n{description}": { + "de": "{name} | {kind} | {count} Einträge\n{description}", + "en": "{name} | {kind} | {count} items\n{description}", + "es": "{name} | {kind} | {count} elementos\n{description}", + "zh": "{name} | {kind} | {count} 个条目\n{description}", + "ja": "{name} | {kind} | {count} 件\n{description}", + "ru": "{name} | {kind} | {count} элементов\n{description}", + "fr": "{name} | {kind} | {count} éléments\n{description}", + "hi": "{name} | {kind} | {count} प्रविष्टियाँ\n{description}", + "ar": "{name} | {kind} | {count} عنصر\n{description}", + "bn": "{name} | {kind} | {count}টি আইটেম\n{description}", + "pt": "{name} | {kind} | {count} itens\n{description}" + }, + "Smart": { + "de": "Smart", + "en": "Smart", + "es": "Inteligente", + "zh": "智能", + "ja": "スマート", + "ru": "Смарт", + "fr": "Intelligente", + "hi": "स्मार्ट", + "ar": "ذكية", + "bn": "স্মার্ট", + "pt": "Inteligente" + }, + "Manuell": { + "de": "Manuell", + "en": "Manual", + "es": "Manual", + "zh": "手动", + "ja": "手動", + "ru": "Вручную", + "fr": "Manuelle", + "hi": "मैन्युअल", + "ar": "يدوية", + "bn": "ম্যানুয়াল", + "pt": "Manual" + }, + "Playlist löschen": { + "de": "Playlist löschen", + "en": "Delete playlist", + "es": "Eliminar lista de reproducción", + "zh": "删除播放列表", + "ja": "プレイリストを削除", + "ru": "Удалить плейлист", + "fr": "Supprimer la playlist", + "hi": "प्लेलिस्ट हटाएँ", + "ar": "حذف قائمة التشغيل", + "bn": "প্লেলিস্ট মুছুন", + "pt": "Excluir playlist" + }, + "Playlist '{name}' wirklich löschen?\nManuelle Einträge werden ebenfalls entfernt; die Mediendateien bleiben unangetastet.": { + "de": "Playlist '{name}' wirklich löschen?\nManuelle Einträge werden ebenfalls entfernt; die Mediendateien bleiben unangetastet.", + "en": "Really delete playlist '{name}'?\nManual entries will also be removed; the media files will remain untouched.", + "es": "¿Eliminar la lista '{name}'?\nTambién se quitarán las entradas manuales; los archivos multimedia no se modificarán.", + "zh": "确定要删除播放列表“{name}”吗?\n手动添加的条目也会移除;媒体文件不会更改。", + "ja": "プレイリスト「{name}」を本当に削除しますか?\n手動で追加した項目も削除されますが、メディアファイルは変更されません。", + "ru": "Удалить плейлист «{name}»?\nРучные записи также будут удалены; медиафайлы останутся нетронутыми.", + "fr": "Voulez-vous vraiment supprimer la playlist « {name} » ?\nLes entrées manuelles seront également supprimées ; les fichiers média resteront intacts.", + "hi": "क्या '{name}' प्लेलिस्ट सचमुच हटाएँ?\nमैन्युअल प्रविष्टियाँ भी हटेंगी; मीडिया फ़ाइलें जस की तस रहेंगी।", + "ar": "هل تريد حقًا حذف قائمة التشغيل '{name}'؟\nستُزال العناصر اليدوية أيضًا، وستبقى ملفات الوسائط دون تغيير.", + "bn": "'{name}' প্লেলিস্টটি সত্যিই মুছবেন?\nম্যানুয়াল এন্ট্রিগুলিও সরানো হবে; মিডিয়া ফাইল অপরিবর্তিত থাকবে।", + "pt": "Excluir a playlist '{name}'?\nAs entradas manuais também serão removidas; os arquivos de mídia permanecerão intactos." + }, + "(kein Wert)": { + "de": "(kein Wert)", + "en": "(no value)", + "es": "(sin valor)", + "zh": "(无值)", + "ja": "(値なし)", + "ru": "(нет значения)", + "fr": "(aucune valeur)", + "hi": "(कोई मान नहीं)", + "ar": "(بلا قيمة)", + "bn": "(কোনো মান নেই)", + "pt": "(sem valor)" + }, + "Name fehlt": { + "de": "Name fehlt", + "en": "Name required", + "es": "Falta el nombre", + "zh": "缺少名称", + "ja": "名前が必要です", + "ru": "Не указано название", + "fr": "Nom manquant", + "hi": "नाम आवश्यक है", + "ar": "الاسم مطلوب", + "bn": "নাম প্রয়োজন", + "pt": "Nome ausente" + }, + "Bitte einen Namen für die Playlist angeben.": { + "de": "Bitte einen Namen für die Playlist angeben.", + "en": "Please enter a playlist name.", + "es": "Introduce un nombre para la lista de reproducción.", + "zh": "请输入播放列表名称。", + "ja": "プレイリスト名を入力してください。", + "ru": "Укажите название плейлиста.", + "fr": "Veuillez saisir un nom de playlist.", + "hi": "कृपया प्लेलिस्ट का नाम दें।", + "ar": "يُرجى إدخال اسم لقائمة التشغيل.", + "bn": "প্লেলিস্টের একটি নাম দিন।", + "pt": "Informe um nome para a playlist." + }, + "Ungültige Bedingungen": { + "de": "Ungültige Bedingungen", + "en": "Invalid conditions", + "es": "Condiciones no válidas", + "zh": "条件无效", + "ja": "条件が無効です", + "ru": "Недопустимые условия", + "fr": "Conditions non valides", + "hi": "अमान्य शर्तें", + "ar": "الشروط غير صالحة", + "bn": "শর্তগুলো অবৈধ", + "pt": "Condições inválidas" + }, + "Die Smart-Query konnte nicht erzeugt werden.": { + "de": "Die Smart-Query konnte nicht erzeugt werden.", + "en": "Could not create the smart query.", + "es": "No se pudo crear la consulta inteligente.", + "zh": "无法创建智能查询。", + "ja": "スマートクエリを作成できませんでした。", + "ru": "Не удалось создать смарт-запрос.", + "fr": "Impossible de créer la requête intelligente.", + "hi": "स्मार्ट क्वेरी नहीं बनाई जा सकी।", + "ar": "تعذر إنشاء الاستعلام الذكي.", + "bn": "স্মার্ট কোয়েরি তৈরি করা যায়নি।", + "pt": "Não foi possível criar a consulta inteligente." + }, + "Keine Bedingungen": { + "de": "Keine Bedingungen", + "en": "No conditions", + "es": "Sin condiciones", + "zh": "没有条件", + "ja": "条件なし", + "ru": "Нет условий", + "fr": "Aucune condition", + "hi": "कोई शर्त नहीं", + "ar": "لا توجد شروط", + "bn": "কোনো শর্ত নেই", + "pt": "Sem condições" + }, + "Diese Smart-Playlist enthält keine Bedingungen und liefert alle Medien zurück. Trotzdem speichern?": { + "de": "Diese Smart-Playlist enthält keine Bedingungen und liefert alle Medien zurück. Trotzdem speichern?", + "en": "This smart playlist has no conditions and will return all media. Save it anyway?", + "es": "Esta lista inteligente no tiene condiciones y devolverá todos los medios. ¿Guardarla de todos modos?", + "zh": "此智能播放列表没有任何条件,将返回所有媒体。仍要保存吗?", + "ja": "このスマートプレイリストには条件がなく、すべてのメディアが表示されます。それでも保存しますか?", + "ru": "В смарт-плейлисте нет условий, поэтому он покажет все медиа. Всё равно сохранить?", + "fr": "Cette playlist intelligente ne contient aucune condition et affichera tous les médias. La sauvegarder quand même ?", + "hi": "इस स्मार्ट प्लेलिस्ट में कोई शर्त नहीं है और यह सभी मीडिया दिखाएगी। फिर भी सहेजें?", + "ar": "لا تحتوي قائمة التشغيل الذكية على شروط، وستعرض كل الوسائط. هل تريد حفظها رغم ذلك؟", + "bn": "এই স্মার্ট প্লেলিস্টে কোনো শর্ত নেই এবং এটি সব মিডিয়া দেখাবে। তবুও সংরক্ষণ করবেন?", + "pt": "Esta playlist inteligente não tem condições e exibirá todas as mídias. Salvar mesmo assim?" + }, + "(keine Treffer)": { + "de": "(keine Treffer)", + "en": "(no matches)", + "es": "(sin coincidencias)", + "zh": "(没有匹配项)", + "ja": "(一致する項目なし)", + "ru": "(совпадений нет)", + "fr": "(aucun résultat)", + "hi": "(कोई मिलान नहीं)", + "ar": "(لا توجد نتائج مطابقة)", + "bn": "(কোনো মিল নেই)", + "pt": "(nenhuma correspondência)" + }, + "Ja": { + "de": "Ja", + "en": "Yes", + "es": "Sí", + "zh": "是", + "ja": "はい", + "ru": "Да", + "fr": "Oui", + "hi": "हाँ", + "ar": "نعم", + "bn": "হ্যাঁ", + "pt": "Sim" + }, + "Nein": { + "de": "Nein", + "en": "No", + "es": "No", + "zh": "否", + "ja": "いいえ", + "ru": "Нет", + "fr": "Non", + "hi": "नहीं", + "ar": "لا", + "bn": "না", + "pt": "Não" + }, + "OK": { + "de": "OK", + "en": "OK", + "es": "Aceptar", + "zh": "确定", + "ja": "OK", + "ru": "ОК", + "fr": "OK", + "hi": "ठीक है", + "ar": "موافق", + "bn": "ঠিক আছে", + "pt": "OK" + }, + "Abbrechen": { + "de": "Abbrechen", + "en": "Cancel", + "es": "Cancelar", + "zh": "取消", + "ja": "キャンセル", + "ru": "Отмена", + "fr": "Annuler", + "hi": "रद्द करें", + "ar": "إلغاء", + "bn": "বাতিল", + "pt": "Cancelar" + }, + "Bibliothek scannen": { + "de": "Bibliothek scannen", + "en": "Scan library", + "es": "Escanear la biblioteca", + "zh": "扫描媒体库", + "ja": "ライブラリをスキャン", + "ru": "Сканировать библиотеку", + "fr": "Scanner la bibliothèque", + "hi": "लाइब्रेरी स्कैन करें", + "ar": "فحص المكتبة", + "bn": "লাইব্রেরি স্ক্যান করুন", + "pt": "Examinar a biblioteca" + }, + "+ Neuer Tag...": { + "de": "+ Neuer Tag...", + "en": "+ New tag...", + "es": "+ Nueva etiqueta...", + "zh": "+ 新建标签…", + "ja": "+ 新しいタグ…", + "ru": "+ Новый тег…", + "fr": "+ Nouveau tag…", + "hi": "+ नया टैग…", + "ar": "+ وسم جديد…", + "bn": "+ নতুন ট্যাগ…", + "pt": "+ Nova etiqueta…" + }, + "Gesamtanzahl Medien: --": { + "de": "Gesamtanzahl Medien: --", + "en": "Total media: --", + "es": "Total de medios: --", + "zh": "媒体总数:--", + "ja": "メディア総数:--", + "ru": "Всего медиа: --", + "fr": "Nombre total de médias : --", + "hi": "कुल मीडिया: --", + "ar": "إجمالي الوسائط: --", + "bn": "মোট মিডিয়া: --", + "pt": "Total de mídias: --" + }, + "Blacklist-Eintrag konnte nicht entfernt werden: {error}": { + "de": "Blacklist-Eintrag konnte nicht entfernt werden: {error}", + "en": "Could not remove blacklist entry: {error}", + "es": "No se pudo eliminar la entrada de la lista de bloqueo: {error}", + "zh": "无法移除黑名单条目:{error}", + "ja": "ブラックリスト項目を削除できませんでした:{error}", + "ru": "Не удалось удалить запись из чёрного списка: {error}", + "fr": "Impossible de supprimer l’entrée de la liste noire : {error}", + "hi": "ब्लॉकलिस्ट प्रविष्टि हटाई नहीं जा सकी: {error}", + "ar": "تعذّر إزالة العنصر من قائمة الحظر: {error}", + "bn": "ব্লকলিস্টের এন্ট্রি সরানো যায়নি: {error}", + "pt": "Não foi possível remover a entrada da lista de bloqueio: {error}" + }, + "Dauer konnte nicht geändert werden: {error}": { + "de": "Dauer konnte nicht geändert werden: {error}", + "en": "Could not change duration: {error}", + "es": "No se pudo cambiar la duración: {error}", + "zh": "无法更改时长:{error}", + "ja": "期間を変更できませんでした:{error}", + "ru": "Не удалось изменить срок: {error}", + "fr": "Impossible de modifier la durée : {error}", + "hi": "अवधि बदली नहीं जा सकी: {error}", + "ar": "تعذّر تغيير المدة: {error}", + "bn": "সময়কাল পরিবর্তন করা যায়নি: {error}", + "pt": "Não foi possível alterar a duração: {error}" + }, + "Abgelaufene konnten nicht entfernt werden: {error}": { + "de": "Abgelaufene konnten nicht entfernt werden: {error}", + "en": "Could not remove expired entries: {error}", + "es": "No se pudieron eliminar las entradas caducadas: {error}", + "zh": "无法移除已过期条目:{error}", + "ja": "期限切れの項目を削除できませんでした:{error}", + "ru": "Не удалось удалить записи с истёкшим сроком: {error}", + "fr": "Impossible de supprimer les entrées expirées : {error}", + "hi": "समय-सीमा समाप्त प्रविष्टियाँ हटाई नहीं जा सकीं: {error}", + "ar": "تعذّر إزالة العناصر منتهية الصلاحية: {error}", + "bn": "মেয়াদোত্তীর্ণ এন্ট্রিগুলি সরানো যায়নি: {error}", + "pt": "Não foi possível remover as entradas expiradas: {error}" + }, + "Blacklist konnte nicht geleert werden: {error}": { + "de": "Blacklist konnte nicht geleert werden: {error}", + "en": "Could not clear the blacklist: {error}", + "es": "No se pudo vaciar la lista de bloqueo: {error}", + "zh": "无法清空黑名单:{error}", + "ja": "ブラックリストを空にできませんでした:{error}", + "ru": "Не удалось очистить чёрный список: {error}", + "fr": "Impossible de vider la liste noire : {error}", + "hi": "ब्लॉकलिस्ट खाली नहीं की जा सकी: {error}", + "ar": "تعذّر إفراغ قائمة الحظر: {error}", + "bn": "ব্লকলিস্ট খালি করা যায়নি: {error}", + "pt": "Não foi possível limpar a lista de bloqueio: {error}" + }, + "GUI konnte nicht vollständig aktualisiert werden: {error}": { + "de": "GUI konnte nicht vollständig aktualisiert werden: {error}", + "en": "The GUI could not be fully refreshed: {error}", + "es": "No se pudo actualizar por completo la interfaz: {error}", + "zh": "无法完整刷新图形界面:{error}", + "ja": "GUIを完全に更新できませんでした:{error}", + "ru": "Не удалось полностью обновить графический интерфейс: {error}", + "fr": "Impossible d’actualiser entièrement l’interface graphique : {error}", + "hi": "GUI पूरी तरह रीफ़्रेश नहीं हो सका: {error}", + "ar": "تعذّر تحديث الواجهة الرسومية بالكامل: {error}", + "bn": "GUI সম্পূর্ণ রিফ্রেশ করা যায়নি: {error}", + "pt": "Não foi possível atualizar totalmente a interface gráfica: {error}" + }, + "Kuenstler": { + "de": "Künstler", + "en": "Artist", + "es": "Artista", + "zh": "艺术家", + "ja": "アーティスト", + "ru": "Исполнитель", + "fr": "Artiste", + "hi": "कलाकार", + "ar": "الفنان", + "bn": "শিল্পী", + "pt": "Artista" + }, + "AND": { + "de": "UND", + "en": "AND", + "es": "Y", + "zh": "与", + "ja": "かつ", + "ru": "И", + "fr": "ET", + "hi": "और", + "ar": "و", + "bn": "এবং", + "pt": "E" + }, + "OR": { + "de": "ODER", + "en": "OR", + "es": "O", + "zh": "或", + "ja": "または", + "ru": "ИЛИ", + "fr": "OU", + "hi": "या", + "ar": "أو", + "bn": "অথবা", + "pt": "OU" + }, + "Laufzeit: {runtime} Min": { + "de": "Laufzeit: {runtime} Min", + "en": "Runtime: {runtime} min", + "es": "Duración: {runtime} min", + "zh": "时长:{runtime} 分钟", + "ja": "再生時間:{runtime} 分", + "ru": "Продолжительность: {runtime} мин", + "fr": "Durée : {runtime} min", + "hi": "अवधि: {runtime} मिनट", + "ar": "المدة: {runtime} دقيقة", + "bn": "সময়কাল: {runtime} মিনিট", + "pt": "Duração: {runtime} min" + }, + "Titel": { + "de": "Titel", + "en": "Title", + "es": "Título", + "zh": "标题", + "ja": "タイトル", + "ru": "Название", + "fr": "Titre", + "hi": "शीर्षक", + "ar": "العنوان", + "bn": "শিরোনাম", + "pt": "Título" + }, + "Typ": { + "de": "Typ", + "en": "Type", + "es": "Tipo", + "zh": "类型", + "ja": "種類", + "ru": "Тип", + "fr": "Type", + "hi": "प्रकार", + "ar": "النوع", + "bn": "ধরন", + "pt": "Tipo" + }, + "Quelle (Provider)": { + "de": "Quelle (Provider)", + "en": "Source (provider)", + "es": "Fuente (proveedor)", + "zh": "来源(提供方)", + "ja": "ソース(プロバイダー)", + "ru": "Источник (провайдер)", + "fr": "Source (fournisseur)", + "hi": "स्रोत (प्रदाता)", + "ar": "المصدر (المزوّد)", + "bn": "উৎস (প্রোভাইডার)", + "pt": "Origem (provedor)" + }, + "Album": { + "de": "Album", + "en": "Album", + "es": "Álbum", + "zh": "专辑", + "ja": "アルバム", + "ru": "Альбом", + "fr": "Album", + "hi": "एल्बम", + "ar": "الألبوم", + "bn": "অ্যালবাম", + "pt": "Álbum" + }, + "Kanal": { + "de": "Kanal", + "en": "Channel", + "es": "Canal", + "zh": "频道", + "ja": "チャンネル", + "ru": "Канал", + "fr": "Chaîne", + "hi": "चैनल", + "ar": "القناة", + "bn": "চ্যানেল", + "pt": "Canal" + }, + "Lokaler Pfad": { + "de": "Lokaler Pfad", + "en": "Local path", + "es": "Ruta local", + "zh": "本地路径", + "ja": "ローカルパス", + "ru": "Локальный путь", + "fr": "Chemin local", + "hi": "स्थानीय पथ", + "ar": "المسار المحلي", + "bn": "স্থানীয় পথ", + "pt": "Caminho local" + }, + "Länge (Sekunden)": { + "de": "Länge (Sekunden)", + "en": "Duration (seconds)", + "es": "Duración (segundos)", + "zh": "时长(秒)", + "ja": "長さ(秒)", + "ru": "Длительность (секунды)", + "fr": "Durée (secondes)", + "hi": "अवधि (सेकंड)", + "ar": "المدة (بالثواني)", + "bn": "সময়কাল (সেকেন্ড)", + "pt": "Duração (segundos)" + }, + "Staffel": { + "de": "Staffel", + "en": "Season", + "es": "Temporada", + "zh": "季", + "ja": "シーズン", + "ru": "Сезон", + "fr": "Saison", + "hi": "सीज़न", + "ar": "الموسم", + "bn": "মৌসুম", + "pt": "Temporada" + }, + "Episode": { + "de": "Episode", + "en": "Episode", + "es": "Episodio", + "zh": "集", + "ja": "エピソード", + "ru": "Эпизод", + "fr": "Épisode", + "hi": "एपिसोड", + "ar": "الحلقة", + "bn": "পর্ব", + "pt": "Episódio" + }, + "Lokale Datei": { + "de": "Lokale Datei", + "en": "Local file", + "es": "Archivo local", + "zh": "本地文件", + "ja": "ローカルファイル", + "ru": "Локальный файл", + "fr": "Fichier local", + "hi": "स्थानीय फ़ाइल", + "ar": "ملف محلي", + "bn": "স্থানীয় ফাইল", + "pt": "Arquivo local" + }, + "Auf Blacklist": { + "de": "Auf Blacklist", + "en": "Blacklisted", + "es": "En la lista de bloqueo", + "zh": "已加入黑名单", + "ja": "ブラックリストに登録", + "ru": "В чёрном списке", + "fr": "Sur liste noire", + "hi": "ब्लॉकलिस्ट में", + "ar": "مدرج في قائمة الحظر", + "bn": "ব্লকলিস্টে আছে", + "pt": "Na lista de bloqueio" + }, + "Erstellt am": { + "de": "Erstellt am", + "en": "Created at", + "es": "Creado el", + "zh": "创建时间", + "ja": "作成日時", + "ru": "Создано", + "fr": "Créé le", + "hi": "बनाया गया", + "ar": "تاريخ الإنشاء", + "bn": "তৈরির তারিখ", + "pt": "Criado em" + }, + "Tag": { + "de": "Tag", + "en": "Tag", + "es": "Etiqueta", + "zh": "标签", + "ja": "タグ", + "ru": "Тег", + "fr": "Étiquette", + "hi": "टैग", + "ar": "وسم", + "bn": "ট্যাগ", + "pt": "Etiqueta" + }, + "contains": { + "de": "enthält", + "en": "contains", + "es": "contiene", + "zh": "包含", + "ja": "次を含む", + "ru": "содержит", + "fr": "contient", + "hi": "इसमें शामिल है", + "ar": "يحتوي على", + "bn": "আছে", + "pt": "contém" + }, + "not_contains": { + "de": "enthält nicht", + "en": "does not contain", + "es": "no contiene", + "zh": "不包含", + "ja": "次を含まない", + "ru": "не содержит", + "fr": "ne contient pas", + "hi": "इसमें शामिल नहीं है", + "ar": "لا يحتوي على", + "bn": "নেই", + "pt": "não contém" + }, + "starts_with": { + "de": "beginnt mit", + "en": "starts with", + "es": "empieza por", + "zh": "开头为", + "ja": "次で始まる", + "ru": "начинается с", + "fr": "commence par", + "hi": "से शुरू होता है", + "ar": "يبدأ بـ", + "bn": "দিয়ে শুরু", + "pt": "começa com" + }, + "is_empty": { + "de": "ist leer", + "en": "is empty", + "es": "está vacío", + "zh": "为空", + "ja": "空である", + "ru": "пусто", + "fr": "est vide", + "hi": "खाली है", + "ar": "فارغ", + "bn": "খালি", + "pt": "está vazio" + }, + "is_not_empty": { + "de": "ist nicht leer", + "en": "is not empty", + "es": "no está vacío", + "zh": "不为空", + "ja": "空ではない", + "ru": "не пусто", + "fr": "n’est pas vide", + "hi": "खाली नहीं है", + "ar": "غير فارغ", + "bn": "খালি নয়", + "pt": "não está vazio" + }, + "{name} Öffnen mit:": { + "de": "{name} öffnen mit:", + "en": "Open with {name}:", + "es": "Abrir con {name}:", + "zh": "使用 {name} 打开:", + "ja": "{name} で開く:", + "ru": "Открывать с помощью {name}:", + "fr": "Ouvrir avec {name} :", + "hi": "{name} से खोलें:", + "ar": "الفتح باستخدام {name}:", + "bn": "{name} দিয়ে খুলুন:", + "pt": "Abrir com {name}:" + }, + "Typ: {type}": { + "de": "Typ: {type}", + "en": "Type: {type}", + "es": "Tipo: {type}", + "zh": "类型:{type}", + "ja": "種類:{type}", + "ru": "Тип: {type}", + "fr": "Type : {type}", + "hi": "प्रकार: {type}", + "ar": "النوع: {type}", + "bn": "ধরন: {type}", + "pt": "Tipo: {type}" + }, + "Typ: {type}\nProvider: {source}\nProvider-ID: {provider_id}\nErstellt am: {created_at}\nZuletzt geöffnet: {last_opened_at}\nÖffnungsmethode: {open_method}": { + "de": "Typ: {type}\nProvider: {source}\nProvider-ID: {provider_id}\nErstellt am: {created_at}\nZuletzt geöffnet: {last_opened_at}\nÖffnungsmethode: {open_method}", + "en": "Type: {type}\nProvider: {source}\nProvider ID: {provider_id}\nCreated at: {created_at}\nLast opened: {last_opened_at}\nOpening method: {open_method}", + "es": "Tipo: {type}\nProveedor: {source}\nID del proveedor: {provider_id}\nCreado el: {created_at}\nÚltima apertura: {last_opened_at}\nMétodo de apertura: {open_method}", + "zh": "类型:{type}\n提供方:{source}\n提供方 ID:{provider_id}\n创建时间:{created_at}\n上次打开:{last_opened_at}\n打开方式:{open_method}", + "ja": "種類:{type}\nプロバイダー:{source}\nプロバイダー ID:{provider_id}\n作成日時:{created_at}\n最終オープン日時:{last_opened_at}\n起動方法:{open_method}", + "ru": "Тип: {type}\nПровайдер: {source}\nID провайдера: {provider_id}\nСоздано: {created_at}\nПоследнее открытие: {last_opened_at}\nСпособ открытия: {open_method}", + "fr": "Type : {type}\nFournisseur : {source}\nID du fournisseur : {provider_id}\nCréé le : {created_at}\nDernière ouverture : {last_opened_at}\nMode d’ouverture : {open_method}", + "hi": "प्रकार: {type}\nप्रदाता: {source}\nप्रदाता ID: {provider_id}\nबनाया गया: {created_at}\nअंतिम बार खोला गया: {last_opened_at}\nखोलने का तरीका: {open_method}", + "ar": "النوع: {type}\nالمزوّد: {source}\nمعرّف المزوّد: {provider_id}\nتاريخ الإنشاء: {created_at}\nآخر فتح: {last_opened_at}\nطريقة الفتح: {open_method}", + "bn": "ধরন: {type}\nপ্রোভাইডার: {source}\nপ্রোভাইডার আইডি: {provider_id}\nতৈরির তারিখ: {created_at}\nসর্বশেষ খোলার সময়: {last_opened_at}\nখোলার পদ্ধতি: {open_method}", + "pt": "Tipo: {type}\nProvedor: {source}\nID do provedor: {provider_id}\nCriado em: {created_at}\nÚltima abertura: {last_opened_at}\nMétodo de abertura: {open_method}" + }, + " {icon} {type}: {count}": { + "de": " {icon} {type}: {count}", + "en": " {icon} {type}: {count}", + "es": " {icon} {type}: {count}", + "zh": " {icon} {type}:{count}", + "ja": " {icon} {type}:{count}", + "ru": " {icon} {type}: {count}", + "fr": " {icon} {type} : {count}", + "hi": " {icon} {type}: {count}", + "ar": " {icon} {type}: {count}", + "bn": " {icon} {type}: {count}", + "pt": " {icon} {type}: {count}" + }, + " {date} [{type}] {title}": { + "de": " {date} [{type}] {title}", + "en": " {date} [{type}] {title}", + "es": " {date} [{type}] {title}", + "zh": " {date} 〔{type}〕 {title}", + "ja": " {date} 〔{type}〕 {title}", + "ru": " {date} [{type}] {title}", + "fr": " {date} [{type}] {title}", + "hi": " {date} [{type}] {title}", + "ar": " {date} [{type}] {title}", + "bn": " {date} [{type}] {title}", + "pt": " {date} [{type}] {title}" + }, + " {title} [{mtype} | {source}]": { + "de": " {title} [{mtype} | {source}]", + "en": " {title} [{mtype} | {source}]", + "es": " {title} [{mtype} | {source}]", + "zh": " {title} 〔{mtype}|{source}〕", + "ja": " {title} 〔{mtype}|{source}〕", + "ru": " {title} [{mtype} | {source}]", + "fr": " {title} [{mtype} | {source}]", + "hi": " {title} [{mtype} | {source}]", + "ar": " {title} [{mtype} | {source}]", + "bn": " {title} [{mtype} | {source}]", + "pt": " {title} [{mtype} | {source}]" + }, + "Sprache der Oberfläche:": { + "de": "Sprache der Oberfläche:", + "en": "Interface language:", + "es": "Idioma de la interfaz:", + "zh": "界面语言:", + "ja": "表示言語:", + "ru": "Язык интерфейса:", + "fr": "Langue de l’interface :", + "hi": "इंटरफ़ेस की भाषा:", + "ar": "لغة الواجهة:", + "bn": "ইন্টারফেসের ভাষা:", + "pt": "Idioma da interface:" + }, + "Anzeigesprache gespeichert": { + "de": "Anzeigesprache gespeichert", + "en": "Display language saved", + "es": "Idioma de la interfaz guardado", + "zh": "显示语言已保存", + "ja": "表示言語を保存しました", + "ru": "Язык интерфейса сохранён", + "fr": "Langue de l’interface enregistrée", + "hi": "इंटरफ़ेस भाषा सहेजी गई", + "ar": "تم حفظ لغة الواجهة", + "bn": "ইন্টারফেসের ভাষা সংরক্ষিত হয়েছে", + "pt": "Idioma da interface salvo" + }, + "Die Änderung der Anzeigesprache wird nach einem Neustart wirksam.": { + "de": "Die Änderung der Anzeigesprache wird nach einem Neustart wirksam.", + "en": "The display language change will take effect after restarting.", + "es": "El cambio de idioma de la interfaz se aplicará tras reiniciar.", + "zh": "界面语言更改将在重启后生效。", + "ja": "表示言語の変更は再起動後に反映されます。", + "ru": "Изменение языка интерфейса вступит в силу после перезапуска.", + "fr": "Le changement de langue de l’interface prendra effet après le redémarrage.", + "hi": "इंटरफ़ेस की भाषा में बदलाव रीस्टार्ट के बाद लागू होगा।", + "ar": "سيُطبَّق تغيير لغة الواجهة بعد إعادة تشغيل التطبيق.", + "bn": "ইন্টারফেসের ভাষার পরিবর্তন পুনরায় চালু করলে কার্যকর হবে।", + "pt": "A alteração do idioma da interface terá efeito após reiniciar." + }, + "Lokal": { + "de": "Lokal", + "en": "Local", + "es": "Local", + "zh": "本地", + "ja": "ローカル", + "ru": "Локально", + "fr": "Local", + "hi": "स्थानीय", + "ar": "محلي", + "bn": "স্থানীয়", + "pt": "Local" + }, + "Titel A-Z": { + "de": "Titel A-Z", + "en": "Title A-Z", + "es": "Título A-Z", + "zh": "标题 A-Z", + "ja": "タイトル A-Z", + "ru": "Название А–Я", + "fr": "Titre A-Z", + "hi": "शीर्षक A-Z", + "ar": "العنوان A-Z", + "bn": "শিরোনাম A-Z", + "pt": "Título A-Z" + }, + "Titel Z-A": { + "de": "Titel Z-A", + "en": "Title Z-A", + "es": "Título Z-A", + "zh": "标题 Z-A", + "ja": "タイトル Z-A", + "ru": "Название Я–А", + "fr": "Titre Z-A", + "hi": "शीर्षक Z-A", + "ar": "العنوان Z-A", + "bn": "শিরোনাম Z-A", + "pt": "Título Z-A" + }, + "Hinzugefügt (neu)": { + "de": "Hinzugefügt (neu)", + "en": "Added (newest)", + "es": "Añadido (más reciente)", + "zh": "添加时间(最新)", + "ja": "追加日時(新しい順)", + "ru": "Добавлено (сначала новые)", + "fr": "Ajouté (plus récent)", + "hi": "जोड़ा गया (नवीनतम)", + "ar": "تاريخ الإضافة (الأحدث)", + "bn": "যোগ করা হয়েছে (সর্বশেষ)", + "pt": "Adicionado (mais recente)" + }, + "Hinzugefügt (alt)": { + "de": "Hinzugefügt (alt)", + "en": "Added (oldest)", + "es": "Añadido (más antiguo)", + "zh": "添加时间(最早)", + "ja": "追加日時(古い順)", + "ru": "Добавлено (сначала старые)", + "fr": "Ajouté (plus ancien)", + "hi": "जोड़ा गया (सबसे पुराना)", + "ar": "تاريخ الإضافة (الأقدم)", + "bn": "যোগ করা হয়েছে (প্রাচীনতম)", + "pt": "Adicionado (mais antigo)" + }, + "Alle Zeiten": { + "de": "Alle Zeiten", + "en": "Any time", + "es": "Cualquier fecha", + "zh": "不限时间", + "ja": "期間指定なし", + "ru": "За всё время", + "fr": "Toutes les périodes", + "hi": "किसी भी समय", + "ar": "في أي وقت", + "bn": "যেকোনো সময়", + "pt": "Todo o período" + }, + "Heute": { + "de": "Heute", + "en": "Today", + "es": "Hoy", + "zh": "今天", + "ja": "今日", + "ru": "Сегодня", + "fr": "Aujourd’hui", + "hi": "आज", + "ar": "اليوم", + "bn": "আজ", + "pt": "Hoje" + }, + "Letzte 7 Tage": { + "de": "Letzte 7 Tage", + "en": "Last 7 days", + "es": "Últimos 7 días", + "zh": "最近 7 天", + "ja": "過去7日間", + "ru": "Последние 7 дней", + "fr": "7 derniers jours", + "hi": "पिछले 7 दिन", + "ar": "آخر 7 أيام", + "bn": "গত ৭ দিন", + "pt": "Últimos 7 dias" + }, + "Letzte 30 Tage": { + "de": "Letzte 30 Tage", + "en": "Last 30 days", + "es": "Últimos 30 días", + "zh": "最近 30 天", + "ja": "過去30日間", + "ru": "Последние 30 дней", + "fr": "30 derniers jours", + "hi": "पिछले 30 दिन", + "ar": "آخر 30 يومًا", + "bn": "গত ৩০ দিন", + "pt": "Últimos 30 dias" + }, + "Letzte 90 Tage": { + "de": "Letzte 90 Tage", + "en": "Last 90 days", + "es": "Últimos 90 días", + "zh": "最近 90 天", + "ja": "過去90日間", + "ru": "Последние 90 дней", + "fr": "90 derniers jours", + "hi": "पिछले 90 दिन", + "ar": "آخر 90 يومًا", + "bn": "গত ৯০ দিন", + "pt": "Últimos 90 dias" + }, + "Dieses Jahr": { + "de": "Dieses Jahr", + "en": "This year", + "es": "Este año", + "zh": "今年", + "ja": "今年", + "ru": "В этом году", + "fr": "Cette année", + "hi": "इस वर्ष", + "ar": "هذا العام", + "bn": "এই বছর", + "pt": "Este ano" + }, + "Mediensuche": { + "de": "Mediensuche", + "en": "Media search", + "es": "Búsqueda de medios", + "zh": "媒体搜索", + "ja": "メディア検索", + "ru": "Поиск медиа", + "fr": "Recherche de médias", + "hi": "मीडिया खोज", + "ar": "بحث الوسائط", + "bn": "মিডিয়া অনুসন্ধান", + "pt": "Pesquisa de mídia" + }, + "Durchsucht Titel, Beschreibung und Tags der Bibliothek.": { + "de": "Durchsucht Titel, Beschreibung und Tags der Bibliothek.", + "en": "Searches library titles, descriptions, and tags.", + "es": "Busca en los títulos, las descripciones y las etiquetas de la biblioteca.", + "zh": "搜索媒体库中的标题、描述和标签。", + "ja": "ライブラリのタイトル、説明、タグを検索します。", + "ru": "Ищет в названиях, описаниях и тегах библиотеки.", + "fr": "Recherche dans les titres, descriptions et étiquettes de la bibliothèque.", + "hi": "लाइब्रेरी के शीर्षक, विवरण और टैग खोजता है।", + "ar": "يبحث في عناوين المكتبة وأوصافها ووسومها.", + "bn": "লাইব্রেরির শিরোনাম, বিবরণ ও ট্যাগে অনুসন্ধান করে।", + "pt": "Pesquisa títulos, descrições e etiquetas da biblioteca." + }, + "Titel, Beschreibung oder Tags durchsuchen": { + "de": "Titel, Beschreibung oder Tags durchsuchen", + "en": "Search titles, descriptions, or tags", + "es": "Buscar títulos, descripciones o etiquetas", + "zh": "搜索标题、描述或标签", + "ja": "タイトル、説明、タグを検索", + "ru": "Поиск по названиям, описаниям или тегам", + "fr": "Rechercher dans les titres, descriptions ou étiquettes", + "hi": "शीर्षक, विवरण या टैग खोजें", + "ar": "ابحث في العناوين أو الأوصاف أو الوسوم", + "bn": "শিরোনাম, বিবরণ বা ট্যাগে অনুসন্ধান করুন", + "pt": "Pesquisar títulos, descrições ou etiquetas" + }, + "Favoritenfilter": { + "de": "Favoritenfilter", + "en": "Favorites filter", + "es": "Filtro de favoritos", + "zh": "收藏筛选器", + "ja": "お気に入りフィルター", + "ru": "Фильтр избранного", + "fr": "Filtre des favoris", + "hi": "पसंदीदा फ़िल्टर", + "ar": "مرشح المفضلة", + "bn": "পছন্দের ফিল্টার", + "pt": "Filtro de favoritos" + }, + "Zeigt nur Favoriten in den Suchergebnissen an.": { + "de": "Zeigt nur Favoriten in den Suchergebnissen an.", + "en": "Shows only favorites in the search results.", + "es": "Muestra solo los favoritos en los resultados de búsqueda.", + "zh": "仅显示搜索结果中的收藏项。", + "ja": "検索結果にお気に入りのみを表示します。", + "ru": "Показывает в результатах поиска только избранное.", + "fr": "Affiche uniquement les favoris dans les résultats de recherche.", + "hi": "खोज परिणामों में केवल पसंदीदा दिखाता है।", + "ar": "يعرض العناصر المفضلة فقط في نتائج البحث.", + "bn": "অনুসন্ধানের ফলাফলে শুধু পছন্দের আইটেম দেখায়।", + "pt": "Mostra apenas os favoritos nos resultados da pesquisa." + }, + "Nur Favoriten anzeigen": { + "de": "Nur Favoriten anzeigen", + "en": "Show favorites only", + "es": "Mostrar solo favoritos", + "zh": "仅显示收藏项", + "ja": "お気に入りのみ表示", + "ru": "Показывать только избранное", + "fr": "Afficher uniquement les favoris", + "hi": "केवल पसंदीदा दिखाएँ", + "ar": "عرض المفضلة فقط", + "bn": "শুধু পছন্দের আইটেম দেখান", + "pt": "Mostrar apenas favoritos" + }, + "Nur Favoriten": { + "de": "Nur Favoriten", + "en": "Favorites only", + "es": "Solo favoritos", + "zh": "仅收藏项", + "ja": "お気に入りのみ", + "ru": "Только избранное", + "fr": "Favoris uniquement", + "hi": "केवल पसंदीदा", + "ar": "المفضلة فقط", + "bn": "শুধু পছন্দের আইটেম", + "pt": "Somente favoritos" + }, + "Medientyp-Filter": { + "de": "Medientyp-Filter", + "en": "Media type filter", + "es": "Filtro de tipo de medio", + "zh": "媒体类型筛选器", + "ja": "メディアタイプフィルター", + "ru": "Фильтр по типу медиа", + "fr": "Filtre par type de média", + "hi": "मीडिया प्रकार फ़िल्टर", + "ar": "مرشح نوع الوسائط", + "bn": "মিডিয়ার ধরন ফিল্টার", + "pt": "Filtro por tipo de mídia" + }, + "Begrenzt die Suche auf einen ausgewählten Medientyp.": { + "de": "Begrenzt die Suche auf einen ausgewählten Medientyp.", + "en": "Limits the search to the selected media type.", + "es": "Limita la búsqueda al tipo de medio seleccionado.", + "zh": "将搜索范围限定为所选媒体类型。", + "ja": "選択したメディアタイプに検索を限定します。", + "ru": "Ограничивает поиск выбранным типом медиа.", + "fr": "Limite la recherche au type de média sélectionné.", + "hi": "खोज को चुने हुए मीडिया प्रकार तक सीमित करता है।", + "ar": "يقصر البحث على نوع الوسائط المحدد.", + "bn": "অনুসন্ধানকে নির্বাচিত মিডিয়ার ধরনে সীমাবদ্ধ করে।", + "pt": "Limita a pesquisa ao tipo de mídia selecionado." + }, + "Nach Medientyp filtern": { + "de": "Nach Medientyp filtern", + "en": "Filter by media type", + "es": "Filtrar por tipo de medio", + "zh": "按媒体类型筛选", + "ja": "メディアタイプで絞り込む", + "ru": "Фильтр по типу медиа", + "fr": "Filtrer par type de média", + "hi": "मीडिया प्रकार के अनुसार फ़िल्टर करें", + "ar": "تصفية حسب نوع الوسائط", + "bn": "মিডিয়ার ধরন অনুযায়ী ফিল্টার করুন", + "pt": "Filtrar por tipo de mídia" + }, + "Anbieter-Filter": { + "de": "Anbieter-Filter", + "en": "Provider filter", + "es": "Filtro de proveedor", + "zh": "提供方筛选器", + "ja": "プロバイダーフィルター", + "ru": "Фильтр по провайдеру", + "fr": "Filtre du fournisseur", + "hi": "प्रदाता फ़िल्टर", + "ar": "مرشح المزوّد", + "bn": "প্রোভাইডার ফিল্টার", + "pt": "Filtro de provedor" + }, + "Begrenzt die Suche auf einen ausgewählten Anbieter.": { + "de": "Begrenzt die Suche auf einen ausgewählten Anbieter.", + "en": "Limits the search to the selected provider.", + "es": "Limita la búsqueda al proveedor seleccionado.", + "zh": "将搜索范围限定为所选提供方。", + "ja": "選択したプロバイダーに検索を限定します。", + "ru": "Ограничивает поиск выбранным провайдером.", + "fr": "Limite la recherche au fournisseur sélectionné.", + "hi": "खोज को चुने हुए प्रदाता तक सीमित करता है।", + "ar": "يقصر البحث على المزوّد المحدد.", + "bn": "অনুসন্ধানকে নির্বাচিত প্রোভাইডারে সীমাবদ্ধ করে।", + "pt": "Limita a pesquisa ao provedor selecionado." + }, + "Nach Anbieter filtern": { + "de": "Nach Anbieter filtern", + "en": "Filter by provider", + "es": "Filtrar por proveedor", + "zh": "按提供方筛选", + "ja": "プロバイダーで絞り込む", + "ru": "Фильтр по провайдеру", + "fr": "Filtrer par fournisseur", + "hi": "प्रदाता के अनुसार फ़िल्टर करें", + "ar": "تصفية حسب المزوّد", + "bn": "প্রোভাইডার অনুযায়ী ফিল্টার করুন", + "pt": "Filtrar por provedor" + }, + "Erweiterte Filter umschalten": { + "de": "Erweiterte Filter umschalten", + "en": "Toggle advanced filters", + "es": "Mostrar u ocultar filtros avanzados", + "zh": "切换高级筛选器", + "ja": "詳細フィルターの表示を切り替え", + "ru": "Переключить расширенные фильтры", + "fr": "Afficher/masquer les filtres avancés", + "hi": "उन्नत फ़िल्टर दिखाएँ या छिपाएँ", + "ar": "تبديل الفلاتر المتقدمة", + "bn": "উন্নত ফিল্টার দেখান বা লুকান", + "pt": "Alternar filtros avançados" + }, + "Blendet zusätzliche Suchfilter ein oder aus.": { + "de": "Blendet zusätzliche Suchfilter ein oder aus.", + "en": "Shows or hides additional search filters.", + "es": "Muestra u oculta filtros de búsqueda adicionales.", + "zh": "显示或隐藏其他搜索筛选项。", + "ja": "追加の検索フィルターを表示または非表示にします。", + "ru": "Показывает или скрывает дополнительные фильтры поиска.", + "fr": "Affiche ou masque des filtres de recherche supplémentaires.", + "hi": "अतिरिक्त खोज फ़िल्टर दिखाता या छिपाता है।", + "ar": "يُظهر عوامل تصفية البحث الإضافية أو يخفيها.", + "bn": "অতিরিক্ত অনুসন্ধান ফিল্টার দেখায় বা লুকায়।", + "pt": "Mostra ou oculta filtros de pesquisa adicionais." + }, + "Erweiterte Filter ein- oder ausblenden": { + "de": "Erweiterte Filter ein- oder ausblenden", + "en": "Show or hide advanced filters", + "es": "Mostrar u ocultar filtros avanzados", + "zh": "显示或隐藏高级筛选器", + "ja": "詳細フィルターを表示または非表示にする", + "ru": "Показать или скрыть расширенные фильтры", + "fr": "Afficher ou masquer les filtres avancés", + "hi": "उन्नत फ़िल्टर दिखाएँ या छिपाएँ", + "ar": "إظهار الفلاتر المتقدمة أو إخفاؤها", + "bn": "উন্নত ফিল্টার দেখান বা লুকান", + "pt": "Mostrar ou ocultar filtros avançados" + }, + "Erweiterte Filter ein-/ausblenden": { + "de": "Erweiterte Filter ein-/ausblenden", + "en": "Toggle advanced filters", + "es": "Alternar filtros avanzados", + "zh": "切换高级筛选器", + "ja": "詳細フィルターを切り替え", + "ru": "Переключить расширенные фильтры", + "fr": "Afficher/masquer les filtres avancés", + "hi": "उन्नत फ़िल्टर टॉगल करें", + "ar": "تبديل الفلاتر المتقدمة", + "bn": "উন্নত ফিল্টার টগল করুন", + "pt": "Alternar filtros avançados" + }, + "Suchfilter zurücksetzen": { + "de": "Suchfilter zurücksetzen", + "en": "Reset search filters", + "es": "Restablecer filtros de búsqueda", + "zh": "重置搜索筛选项", + "ja": "検索フィルターをリセット", + "ru": "Сбросить фильтры поиска", + "fr": "Réinitialiser les filtres de recherche", + "hi": "खोज फ़िल्टर रीसेट करें", + "ar": "إعادة ضبط فلاتر البحث", + "bn": "অনুসন্ধান ফিল্টার রিসেট করুন", + "pt": "Redefinir filtros de pesquisa" + }, + "Setzt alle aktiven Suchfilter auf den Standardzustand zurück.": { + "de": "Setzt alle aktiven Suchfilter auf den Standardzustand zurück.", + "en": "Resets all active search filters to their default state.", + "es": "Restablece todos los filtros activos a su estado predeterminado.", + "zh": "将所有启用的搜索筛选项恢复为默认状态。", + "ja": "有効な検索フィルターをすべて初期状態に戻します。", + "ru": "Сбрасывает все активные фильтры поиска к исходному состоянию.", + "fr": "Rétablit l’état par défaut de tous les filtres de recherche actifs.", + "hi": "सभी सक्रिय खोज फ़िल्टर को उनकी डिफ़ॉल्ट स्थिति में रीसेट करता है।", + "ar": "يعيد جميع فلاتر البحث النشطة إلى حالتها الافتراضية.", + "bn": "সব সক্রিয় অনুসন্ধান ফিল্টার ডিফল্ট অবস্থায় ফিরিয়ে দেয়।", + "pt": "Redefine todos os filtros de pesquisa ativos para o estado padrão." + }, + "Alle Suchfilter zurücksetzen": { + "de": "Alle Suchfilter zurücksetzen", + "en": "Reset all search filters", + "es": "Restablecer todos los filtros de búsqueda", + "zh": "重置所有搜索筛选项", + "ja": "すべての検索フィルターをリセット", + "ru": "Сбросить все фильтры поиска", + "fr": "Réinitialiser tous les filtres de recherche", + "hi": "सभी खोज फ़िल्टर रीसेट करें", + "ar": "إعادة ضبط جميع فلاتر البحث", + "bn": "সব অনুসন্ধান ফিল্টার রিসেট করুন", + "pt": "Redefinir todos os filtros de pesquisa" + }, + "Alle Filter zurücksetzen": { + "de": "Alle Filter zurücksetzen", + "en": "Reset all filters", + "es": "Restablecer todos los filtros", + "zh": "重置所有筛选项", + "ja": "すべてのフィルターをリセット", + "ru": "Сбросить все фильтры", + "fr": "Réinitialiser tous les filtres", + "hi": "सभी फ़िल्टर रीसेट करें", + "ar": "إعادة ضبط جميع الفلاتر", + "bn": "সব ফিল্টার রিসেট করুন", + "pt": "Redefinir todos os filtros" + }, + "Zeitraum": { + "de": "Zeitraum", + "en": "Time period", + "es": "Período", + "zh": "时间范围", + "ja": "期間", + "ru": "Период", + "fr": "Période", + "hi": "समयावधि", + "ar": "الفترة الزمنية", + "bn": "সময়সীমা", + "pt": "Período" + }, + "Zeitraum-Filter": { + "de": "Zeitraum-Filter", + "en": "Time period filter", + "es": "Filtro de período", + "zh": "时间范围筛选器", + "ja": "期間フィルター", + "ru": "Фильтр по периоду", + "fr": "Filtre de période", + "hi": "समयावधि फ़िल्टर", + "ar": "مرشح الفترة الزمنية", + "bn": "সময়সীমা ফিল্টার", + "pt": "Filtro de período" + }, + "Begrenzt die Suche auf einen ausgewählten Zeitraum.": { + "de": "Begrenzt die Suche auf einen ausgewählten Zeitraum.", + "en": "Limits the search to the selected time period.", + "es": "Limita la búsqueda al período seleccionado.", + "zh": "将搜索范围限定为所选时间段。", + "ja": "選択した期間に検索を限定します。", + "ru": "Ограничивает поиск выбранным периодом.", + "fr": "Limite la recherche à la période sélectionnée.", + "hi": "खोज को चुनी हुई समयावधि तक सीमित करता है।", + "ar": "يقصر البحث على الفترة الزمنية المحددة.", + "bn": "অনুসন্ধানকে নির্বাচিত সময়সীমায় সীমাবদ্ধ করে।", + "pt": "Limita a pesquisa ao período selecionado." + }, + "Nach Zeitraum filtern": { + "de": "Nach Zeitraum filtern", + "en": "Filter by time period", + "es": "Filtrar por período", + "zh": "按时间范围筛选", + "ja": "期間で絞り込む", + "ru": "Фильтр по периоду", + "fr": "Filtrer par période", + "hi": "समयावधि के अनुसार फ़िल्टर करें", + "ar": "تصفية حسب الفترة الزمنية", + "bn": "সময়সীমা অনুযায়ী ফিল্টার করুন", + "pt": "Filtrar por período" + }, + "Sortierung": { + "de": "Sortierung", + "en": "Sort order", + "es": "Ordenación", + "zh": "排序", + "ja": "並べ替え", + "ru": "Сортировка", + "fr": "Tri", + "hi": "क्रमबद्ध करना", + "ar": "الترتيب", + "bn": "সাজানোর ক্রম", + "pt": "Ordenação" + }, + "Legt fest, in welcher Reihenfolge Suchergebnisse angezeigt werden.": { + "de": "Legt fest, in welcher Reihenfolge Suchergebnisse angezeigt werden.", + "en": "Sets the order in which search results are displayed.", + "es": "Define el orden en que se muestran los resultados de búsqueda.", + "zh": "设置搜索结果的显示顺序。", + "ja": "検索結果を表示する順序を指定します。", + "ru": "Задаёт порядок отображения результатов поиска.", + "fr": "Définit l’ordre d’affichage des résultats de recherche.", + "hi": "खोज परिणामों के दिखने का क्रम तय करता है।", + "ar": "يحدد ترتيب عرض نتائج البحث.", + "bn": "অনুসন্ধানের ফলাফল কোন ক্রমে দেখানো হবে তা নির্ধারণ করে।", + "pt": "Define a ordem em que os resultados da pesquisa são exibidos." + }, + "Sortierung der Suchergebnisse wählen": { + "de": "Sortierung der Suchergebnisse wählen", + "en": "Choose search result order", + "es": "Elegir el orden de los resultados", + "zh": "选择搜索结果的排序方式", + "ja": "検索結果の並べ替え順を選択", + "ru": "Выберите порядок результатов поиска", + "fr": "Choisir l’ordre des résultats de recherche", + "hi": "खोज परिणामों का क्रम चुनें", + "ar": "اختر ترتيب نتائج البحث", + "bn": "অনুসন্ধানের ফলাফলের ক্রম বেছে নিন", + "pt": "Escolher a ordem dos resultados da pesquisa" + }, + "Optionen": { + "de": "Optionen", + "en": "Options", + "es": "Opciones", + "zh": "选项", + "ja": "オプション", + "ru": "Параметры", + "fr": "Options", + "hi": "विकल्प", + "ar": "الخيارات", + "bn": "বিকল্প", + "pt": "Opções" + }, + "Blacklist ausblenden": { + "de": "Blacklist ausblenden", + "en": "Hide blacklist entries", + "es": "Ocultar elementos de la lista negra", + "zh": "隐藏黑名单条目", + "ja": "ブラックリスト項目を非表示", + "ru": "Скрыть записи чёрного списка", + "fr": "Masquer les entrées de la liste noire", + "hi": "ब्लॉकलिस्ट प्रविष्टियाँ छिपाएँ", + "ar": "إخفاء عناصر قائمة الحظر", + "bn": "ব্লকলিস্টের এন্ট্রি লুকান", + "pt": "Ocultar itens da lista de bloqueio" + }, + "Blendet Medien aus, die auf der Blacklist stehen.": { + "de": "Blendet Medien aus, die auf der Blacklist stehen.", + "en": "Hides media that are on the blacklist.", + "es": "Oculta los medios que están en la lista negra.", + "zh": "隐藏已加入黑名单的媒体。", + "ja": "ブラックリストに登録されたメディアを非表示にします。", + "ru": "Скрывает медиа из чёрного списка.", + "fr": "Masque les médias inscrits sur la liste noire.", + "hi": "ब्लॉकलिस्ट में मौजूद मीडिया को छिपाता है।", + "ar": "يخفي الوسائط المدرجة في قائمة الحظر.", + "bn": "ব্লকলিস্টে থাকা মিডিয়া লুকায়।", + "pt": "Oculta mídias que estão na lista de bloqueio." + }, + "Blacklisteinträge aus den Suchergebnissen ausblenden": { + "de": "Blacklisteinträge aus den Suchergebnissen ausblenden", + "en": "Hide blacklist entries from search results", + "es": "Ocultar elementos de la lista negra de los resultados", + "zh": "从搜索结果中隐藏黑名单条目", + "ja": "ブラックリスト項目を検索結果から非表示にする", + "ru": "Скрывать записи чёрного списка из результатов поиска", + "fr": "Masquer de la recherche les entrées de la liste noire", + "hi": "ब्लॉकलिस्ट प्रविष्टियों को खोज परिणामों से छिपाएँ", + "ar": "إخفاء عناصر قائمة الحظر من نتائج البحث", + "bn": "ব্লকলিস্টের এন্ট্রি অনুসন্ধানের ফলাফল থেকে লুকান", + "pt": "Ocultar itens da lista de bloqueio dos resultados" + }, + "Nur lokale Dateien": { + "de": "Nur lokale Dateien", + "en": "Local files only", + "es": "Solo archivos locales", + "zh": "仅本地文件", + "ja": "ローカルファイルのみ", + "ru": "Только локальные файлы", + "fr": "Fichiers locaux uniquement", + "hi": "केवल स्थानीय फ़ाइलें", + "ar": "الملفات المحلية فقط", + "bn": "শুধু স্থানীয় ফাইল", + "pt": "Somente arquivos locais" + }, + "Zeigt nur Medien mit lokalem Dateipfad an.": { + "de": "Zeigt nur Medien mit lokalem Dateipfad an.", + "en": "Shows only media with a local file path.", + "es": "Muestra solo medios con una ruta de archivo local.", + "zh": "仅显示具有本地文件路径的媒体。", + "ja": "ローカルファイルパスがあるメディアのみを表示します。", + "ru": "Показывает только медиа с локальным путём к файлу.", + "fr": "Affiche uniquement les médias avec un chemin de fichier local.", + "hi": "केवल स्थानीय फ़ाइल पथ वाले मीडिया दिखाता है।", + "ar": "يعرض الوسائط التي لها مسار ملف محلي فقط.", + "bn": "শুধু স্থানীয় ফাইলপথ থাকা মিডিয়া দেখায়।", + "pt": "Mostra apenas mídias com um caminho de arquivo local." + }, + "Nur lokale Dateien anzeigen": { + "de": "Nur lokale Dateien anzeigen", + "en": "Show local files only", + "es": "Mostrar solo archivos locales", + "zh": "仅显示本地文件", + "ja": "ローカルファイルのみ表示", + "ru": "Показывать только локальные файлы", + "fr": "Afficher uniquement les fichiers locaux", + "hi": "केवल स्थानीय फ़ाइलें दिखाएँ", + "ar": "عرض الملفات المحلية فقط", + "bn": "শুধু স্থানীয় ফাইল দেখান", + "pt": "Mostrar somente arquivos locais" + }, + "Tags": { + "de": "Tags", + "en": "Tags", + "es": "Etiquetas", + "zh": "标签", + "ja": "タグ", + "ru": "Теги", + "fr": "Étiquettes", + "hi": "टैग", + "ar": "الوسوم", + "bn": "ট্যাগ", + "pt": "Etiquetas" + }, + "Tag-Filter hinzufügen": { + "de": "Tag-Filter hinzufügen", + "en": "Add tag filter", + "es": "Añadir filtro de etiqueta", + "zh": "添加标签筛选", + "ja": "タグフィルターを追加", + "ru": "Добавить фильтр по тегу", + "fr": "Ajouter un filtre d’étiquette", + "hi": "टैग फ़िल्टर जोड़ें", + "ar": "إضافة مرشح وسم", + "bn": "ট্যাগ ফিল্টার যোগ করুন", + "pt": "Adicionar filtro de etiqueta" + }, + "Fügt der Suche einen zusätzlichen Tag-Filter hinzu.": { + "de": "Fügt der Suche einen zusätzlichen Tag-Filter hinzu.", + "en": "Adds another tag filter to the search.", + "es": "Añade otro filtro de etiqueta a la búsqueda.", + "zh": "向搜索中添加一个标签筛选条件。", + "ja": "検索にタグフィルターを追加します。", + "ru": "Добавляет в поиск дополнительный фильтр по тегу.", + "fr": "Ajoute un filtre d’étiquette supplémentaire à la recherche.", + "hi": "खोज में एक और टैग फ़िल्टर जोड़ता है।", + "ar": "يضيف مرشح وسم إضافيًا إلى البحث.", + "bn": "অনুসন্ধানে আরও একটি ট্যাগ ফিল্টার যোগ করে।", + "pt": "Adiciona outro filtro de etiqueta à pesquisa." + }, + "Tag eingeben und mit Enter hinzufügen": { + "de": "Tag eingeben und mit Enter hinzufügen", + "en": "Enter a tag and press Enter to add it", + "es": "Escribe una etiqueta y pulsa Intro para añadirla", + "zh": "输入标签并按 Enter 添加", + "ja": "タグを入力して Enter キーで追加", + "ru": "Введите тег и нажмите Enter, чтобы добавить его", + "fr": "Saisissez une étiquette et appuyez sur Entrée pour l’ajouter", + "hi": "टैग लिखें और जोड़ने के लिए Enter दबाएँ", + "ar": "أدخل وسمًا واضغط Enter لإضافته", + "bn": "ট্যাগ লিখে যোগ করতে Enter চাপুন", + "pt": "Digite uma etiqueta e pressione Enter para adicioná-la" + }, + "Tag hinzufügen...": { + "de": "Tag hinzufügen...", + "en": "Add tag...", + "es": "Añadir etiqueta...", + "zh": "添加标签…", + "ja": "タグを追加…", + "ru": "Добавить тег…", + "fr": "Ajouter une étiquette…", + "hi": "टैग जोड़ें…", + "ar": "إضافة وسم…", + "bn": "ট্যাগ যোগ করুন…", + "pt": "Adicionar etiqueta…" + }, + "Tags: {tags}": { + "de": "Tags: {tags}", + "en": "Tags: {tags}", + "es": "Etiquetas: {tags}", + "zh": "标签:{tags}", + "ja": "タグ:{tags}", + "ru": "Теги: {tags}", + "fr": "Étiquettes : {tags}", + "hi": "टैग: {tags}", + "ar": "الوسوم: {tags}", + "bn": "ট্যাগ: {tags}", + "pt": "Etiquetas: {tags}" + }, + "Name für diese Suche...": { + "de": "Name für diese Suche...", + "en": "Name this search...", + "es": "Nombre para esta búsqueda...", + "zh": "为此搜索命名…", + "ja": "この検索に名前を付ける…", + "ru": "Название этого поиска…", + "fr": "Nom de cette recherche…", + "hi": "इस खोज का नाम…", + "ar": "اسم هذا البحث…", + "bn": "এই অনুসন্ধানের নাম…", + "pt": "Nome desta pesquisa..." + }, + "Name:": { + "de": "Name:", + "en": "Name:", + "es": "Nombre:", + "zh": "名称:", + "ja": "名前:", + "ru": "Название:", + "fr": "Nom :", + "hi": "नाम:", + "ar": "الاسم:", + "bn": "নাম:", + "pt": "Nome:" + }, + "Filter:": { + "de": "Filter:", + "en": "Filter:", + "es": "Filtro:", + "zh": "筛选条件:", + "ja": "フィルター:", + "ru": "Фильтр:", + "fr": "Filtre :", + "hi": "फ़िल्टर:", + "ar": "المرشح:", + "bn": "ফিল্টার:", + "pt": "Filtro:" + }, + "Text: \"{text}\"": { + "de": "Text: \"{text}\"", + "en": "Text: \"{text}\"", + "es": "Texto: \"{text}\"", + "zh": "文本:“{text}”", + "ja": "テキスト:「{text}」", + "ru": "Текст: «{text}»", + "fr": "Texte : « {text} »", + "hi": "पाठ: “{text}”", + "ar": "النص: «{text}»", + "bn": "লেখা: “{text}”", + "pt": "Texto: “{text}”" + }, + "Keine Filter": { + "de": "Keine Filter", + "en": "No filters", + "es": "Sin filtros", + "zh": "无筛选条件", + "ja": "フィルターなし", + "ru": "Нет фильтров", + "fr": "Aucun filtre", + "hi": "कोई फ़िल्टर नहीं", + "ar": "لا توجد فلاتر", + "bn": "কোনো ফিল্টার নেই", + "pt": "Sem filtros" + }, + "Filter": { + "de": "Filter", + "en": "Filter", + "es": "Filtros", + "zh": "筛选", + "ja": "フィルター", + "ru": "Фильтры", + "fr": "Filtres", + "hi": "फ़िल्टर", + "ar": "الفلاتر", + "bn": "ফিল্টার", + "pt": "Filtros" + }, + "Speichern": { + "de": "Speichern", + "en": "Save", + "es": "Guardar", + "zh": "保存", + "ja": "保存", + "ru": "Сохранить", + "fr": "Enregistrer", + "hi": "सहेजें", + "ar": "حفظ", + "bn": "সংরক্ষণ করুন", + "pt": "Salvar" + }, + "Titel: {title}": { + "de": "Titel: {title}", + "en": "Title: {title}", + "es": "Título: {title}", + "zh": "标题:{title}", + "ja": "タイトル:{title}", + "ru": "Название: {title}", + "fr": "Titre : {title}", + "hi": "शीर्षक: {title}", + "ar": "العنوان: {title}", + "bn": "শিরোনাম: {title}", + "pt": "Título: {title}" + }, + "Kanal: {channel}": { + "de": "Kanal: {channel}", + "en": "Channel: {channel}", + "es": "Canal: {channel}", + "zh": "频道:{channel}", + "ja": "チャンネル:{channel}", + "ru": "Канал: {channel}", + "fr": "Chaîne : {channel}", + "hi": "चैनल: {channel}", + "ar": "القناة: {channel}", + "bn": "চ্যানেল: {channel}", + "pt": "Canal: {channel}" + }, + "Quelle: {source}": { + "de": "Quelle: {source}", + "en": "Source: {source}", + "es": "Fuente: {source}", + "zh": "来源:{source}", + "ja": "ソース:{source}", + "ru": "Источник: {source}", + "fr": "Source : {source}", + "hi": "स्रोत: {source}", + "ar": "المصدر: {source}", + "bn": "উৎস: {source}", + "pt": "Fonte: {source}" + }, + "Provider-ID: {provider_id}": { + "de": "Provider-ID: {provider_id}", + "en": "Provider ID: {provider_id}", + "es": "ID del proveedor: {provider_id}", + "zh": "提供方 ID:{provider_id}", + "ja": "プロバイダー ID:{provider_id}", + "ru": "ID провайдера: {provider_id}", + "fr": "ID du fournisseur : {provider_id}", + "hi": "प्रदाता ID: {provider_id}", + "ar": "معرّف المزوّد: {provider_id}", + "bn": "প্রদানকারী ID: {provider_id}", + "pt": "ID do provedor: {provider_id}" + }, + "Erstellt am: {created_at}": { + "de": "Erstellt am: {created_at}", + "en": "Created at: {created_at}", + "es": "Creado el: {created_at}", + "zh": "创建时间:{created_at}", + "ja": "作成日時:{created_at}", + "ru": "Создано: {created_at}", + "fr": "Créé le : {created_at}", + "hi": "बनाया गया: {created_at}", + "ar": "تاريخ الإنشاء: {created_at}", + "bn": "তৈরির সময়: {created_at}", + "pt": "Criado em: {created_at}" + }, + "Zuletzt geöffnet: {last_opened_at}": { + "de": "Zuletzt geöffnet: {last_opened_at}", + "en": "Last opened: {last_opened_at}", + "es": "Última apertura: {last_opened_at}", + "zh": "上次打开:{last_opened_at}", + "ja": "最終閲覧:{last_opened_at}", + "ru": "Последнее открытие: {last_opened_at}", + "fr": "Dernière ouverture : {last_opened_at}", + "hi": "अंतिम बार खोला गया: {last_opened_at}", + "ar": "آخر فتح: {last_opened_at}", + "bn": "সর্বশেষ খোলা: {last_opened_at}", + "pt": "Última abertura: {last_opened_at}" + }, + "Öffnungsmethode: {open_method}": { + "de": "Öffnungsmethode: {open_method}", + "en": "Open method: {open_method}", + "es": "Método de apertura: {open_method}", + "zh": "打开方式:{open_method}", + "ja": "開く方法:{open_method}", + "ru": "Способ открытия: {open_method}", + "fr": "Méthode d’ouverture : {open_method}", + "hi": "खोलने का तरीका: {open_method}", + "ar": "طريقة الفتح: {open_method}", + "bn": "খোলার পদ্ধতি: {open_method}", + "pt": "Método de abertura: {open_method}" + }, + "🔍 Suchen... (Titel, Beschreibung, Tags)": { + "de": "🔍 Suchen... (Titel, Beschreibung, Tags)", + "en": "🔍 Search... (title, description, tags)", + "es": "🔍 Buscar... (título, descripción, etiquetas)", + "zh": "🔍 搜索…(标题、描述、标签)", + "ja": "🔍 検索…(タイトル、説明、タグ)", + "ru": "🔍 Поиск... (название, описание, теги)", + "fr": "🔍 Rechercher... (titre, description, tags)", + "hi": "🔍 खोजें... (शीर्षक, विवरण, टैग)", + "ar": "🔍 بحث... (العنوان، الوصف، الوسوم)", + "bn": "🔍 অনুসন্ধান... (শিরোনাম, বিবরণ, ট্যাগ)", + "pt": "🔍 Pesquisar... (título, descrição, etiquetas)" } } diff --git a/mediabrain_launcher.py b/mediabrain_launcher.py index e8e537f..2c06879 100644 --- a/mediabrain_launcher.py +++ b/mediabrain_launcher.py @@ -1,10 +1,13 @@ from __future__ import annotations import ctypes +import json +import os from pathlib import Path import shutil import subprocess import sys +from translator import TranslationSystem, resolve_language APP_NAME = "MediaBrain" @@ -22,7 +25,23 @@ def _project_dir() -> Path: return Path(__file__).resolve().parent -def _show_error(message: str) -> None: +def _translation_system(base_dir: Path): + saved_language = "de" + settings_path = base_dir / "settings.json" + try: + settings = json.loads(settings_path.read_text(encoding="utf-8")) + saved_language = settings.get("ui", {}).get("language", "de") + except (OSError, ValueError, AttributeError): + pass + language = resolve_language(os.environ.get("MEDIABRAIN_LANG"), saved_language) + return TranslationSystem( + language, + app_dir=base_dir, + auto_register_missing=False, + ) + + +def _show_error(message: str, translator=None) -> None: try: ctypes.windll.user32.MessageBoxW(0, message, APP_NAME, 0x10) except Exception: @@ -42,16 +61,19 @@ def _python_command() -> list[str] | None: def main() -> int: base_dir = _project_dir() + translator = _translation_system(base_dir) app_script = base_dir / SCRIPT_NAME if not app_script.exists(): - _show_error(f"{SCRIPT_NAME} wurde nicht gefunden:\n{app_script}") + _show_error(translator.t("{script} wurde nicht gefunden:\n{path}", script=SCRIPT_NAME, path=app_script), translator) return 1 python_cmd = _python_command() if not python_cmd: _show_error( - "Python wurde nicht gefunden.\n" - "Bitte Python 3.10+ installieren und die Abhängigkeiten aus requirements.txt einrichten." + translator.t( + "Python wurde nicht gefunden.\nBitte Python 3.10+ installieren und die Abhängigkeiten aus requirements.txt einrichten." + ), + translator, ) return 1 @@ -63,7 +85,7 @@ def main() -> int: creationflags=creationflags, ) except FileNotFoundError as exc: - _show_error(f"Start fehlgeschlagen:\n{exc}") + _show_error(translator.t("Start fehlgeschlagen:\n{error}", error=exc), translator) return 1 return 0 diff --git a/search_advanced.py b/search_advanced.py index 2421d1a..fd2965b 100644 --- a/search_advanced.py +++ b/search_advanced.py @@ -15,6 +15,7 @@ QDialog, QDialogButtonBox, QFormLayout ) from PySide6.QtCore import Signal +from translator import translate as _t, set_default_translation_system from datetime import datetime, timedelta import json @@ -130,19 +131,22 @@ class AdvancedSearchBar(QWidget): search_triggered = Signal(SearchCriteria) - def __init__(self, parent=None): + def __init__(self, parent=None, translator=None): super().__init__(parent) + self.translator = translator + if translator is not None: + set_default_translation_system(translator) self.criteria = SearchCriteria() self.is_expanded = False self._setup_ui() def _set_a11y(self, widget, *, name, description=None, tooltip=None): """Versorgt kompakte Controls mit stabilem Screenreader-Kontext.""" - widget.setAccessibleName(name) + widget.setAccessibleName(_t(name)) if description: - widget.setAccessibleDescription(description) + widget.setAccessibleDescription(_t(description)) if tooltip: - widget.setToolTip(tooltip) + widget.setToolTip(_t(tooltip)) def _setup_ui(self): main_layout = QVBoxLayout(self) @@ -159,7 +163,7 @@ def _setup_ui(self): description="Durchsucht Titel, Beschreibung und Tags der Bibliothek.", tooltip="Titel, Beschreibung oder Tags durchsuchen", ) - self.search_input.setPlaceholderText("🔍 Suchen... (Titel, Beschreibung, Tags)") + self.search_input.setPlaceholderText(_t("🔍 Suchen... (Titel, Beschreibung, Tags)")) self.search_input.textChanged.connect(self._on_text_changed) self.search_input.returnPressed.connect(self._trigger_search) search_row.addWidget(self.search_input, stretch=1) @@ -173,7 +177,7 @@ def _setup_ui(self): description="Zeigt nur Favoriten in den Suchergebnissen an.", tooltip="Nur Favoriten anzeigen", ) - self.btn_favorites.setToolTip("Nur Favoriten") + self.btn_favorites.setToolTip(_t("Nur Favoriten")) self.btn_favorites.setFixedWidth(40) self.btn_favorites.toggled.connect(self._on_favorites_toggle) search_row.addWidget(self.btn_favorites) @@ -188,7 +192,7 @@ def _setup_ui(self): ) self.combo_type.setFixedWidth(100) for label, value in MEDIA_TYPES: - self.combo_type.addItem(label, value) + self.combo_type.addItem(_t(label), value) self.combo_type.currentIndexChanged.connect(self._on_type_changed) search_row.addWidget(self.combo_type) @@ -202,31 +206,31 @@ def _setup_ui(self): ) self.combo_provider.setFixedWidth(120) for label, value in PROVIDERS: - self.combo_provider.addItem(label, value) + self.combo_provider.addItem(_t(label), value) self.combo_provider.currentIndexChanged.connect(self._on_provider_changed) search_row.addWidget(self.combo_provider) # Erweitert-Button - self.btn_expand = QPushButton("Filter \u25BC") + self.btn_expand = QPushButton(_t("Filter") + " ▼") self._set_a11y( self.btn_expand, name="Erweiterte Filter umschalten", description="Blendet zusätzliche Suchfilter ein oder aus.", tooltip="Erweiterte Filter ein- oder ausblenden", ) - self.btn_expand.setToolTip("Erweiterte Filter ein-/ausblenden") + self.btn_expand.setToolTip(_t("Erweiterte Filter ein-/ausblenden")) self.btn_expand.clicked.connect(self._toggle_expand) search_row.addWidget(self.btn_expand) # Reset-Button - self.btn_reset = QPushButton("Zurücksetzen") + self.btn_reset = QPushButton(_t("Zurücksetzen")) self._set_a11y( self.btn_reset, name="Suchfilter zurücksetzen", description="Setzt alle aktiven Suchfilter auf den Standardzustand zurück.", tooltip="Alle Suchfilter zurücksetzen", ) - self.btn_reset.setToolTip("Alle Filter zurücksetzen") + self.btn_reset.setToolTip(_t("Alle Filter zurücksetzen")) self.btn_reset.clicked.connect(self.reset_filters) search_row.addWidget(self.btn_reset) @@ -240,7 +244,7 @@ def _setup_ui(self): filter_layout = QHBoxLayout(self.filter_panel) # Zeitraum - time_group = QGroupBox("Zeitraum") + time_group = QGroupBox(_t("Zeitraum")) time_layout = QVBoxLayout(time_group) self.combo_time = QComboBox() self._set_a11y( @@ -250,13 +254,13 @@ def _setup_ui(self): tooltip="Nach Zeitraum filtern", ) for label, value in TIME_FILTERS: - self.combo_time.addItem(label, value) + self.combo_time.addItem(_t(label), value) self.combo_time.currentIndexChanged.connect(self._on_time_changed) time_layout.addWidget(self.combo_time) filter_layout.addWidget(time_group) # Sortierung - sort_group = QGroupBox("Sortierung") + sort_group = QGroupBox(_t("Sortierung")) sort_layout = QVBoxLayout(sort_group) self.combo_sort = QComboBox() self._set_a11y( @@ -266,16 +270,16 @@ def _setup_ui(self): tooltip="Sortierung der Suchergebnisse wählen", ) for label, field, desc in SORT_OPTIONS: - self.combo_sort.addItem(label, (field, desc)) + self.combo_sort.addItem(_t(label), (field, desc)) self.combo_sort.currentIndexChanged.connect(self._on_sort_changed) sort_layout.addWidget(self.combo_sort) filter_layout.addWidget(sort_group) # Optionen - options_group = QGroupBox("Optionen") + options_group = QGroupBox(_t("Optionen")) options_layout = QVBoxLayout(options_group) - self.chk_blacklist = QCheckBox("Blacklist ausblenden") + self.chk_blacklist = QCheckBox(_t("Blacklist ausblenden")) self._set_a11y( self.chk_blacklist, name="Blacklist ausblenden", @@ -286,7 +290,7 @@ def _setup_ui(self): self.chk_blacklist.toggled.connect(self._on_blacklist_toggle) options_layout.addWidget(self.chk_blacklist) - self.chk_local_only = QCheckBox("Nur lokale Dateien") + self.chk_local_only = QCheckBox(_t("Nur lokale Dateien")) self._set_a11y( self.chk_local_only, name="Nur lokale Dateien", @@ -299,7 +303,7 @@ def _setup_ui(self): filter_layout.addWidget(options_group) # Tags - tags_group = QGroupBox("Tags") + tags_group = QGroupBox(_t("Tags")) tags_layout = QVBoxLayout(tags_group) self.tag_input = QLineEdit() self._set_a11y( @@ -308,7 +312,7 @@ def _setup_ui(self): description="Fügt der Suche einen zusätzlichen Tag-Filter hinzu.", tooltip="Tag eingeben und mit Enter hinzufügen", ) - self.tag_input.setPlaceholderText("Tag hinzufügen...") + self.tag_input.setPlaceholderText(_t("Tag hinzufügen...")) self.tag_input.returnPressed.connect(self._add_tag) tags_layout.addWidget(self.tag_input) @@ -322,7 +326,7 @@ def _setup_ui(self): def _toggle_expand(self): self.is_expanded = not self.is_expanded self.filter_panel.setVisible(self.is_expanded) - self.btn_expand.setText("Filter \u25B2" if self.is_expanded else "Filter \u25BC") + self.btn_expand.setText(_t("Filter") + (" ▲" if self.is_expanded else " ▼")) def _on_text_changed(self, text): self.criteria.text = text @@ -368,7 +372,7 @@ def _add_tag(self): def _update_tag_display(self): if self.criteria.tags: - self.tag_list.setText("Tags: " + ", ".join(f"[{t}]" for t in self.criteria.tags)) + self.tag_list.setText(_t("Tags: {tags}", tags=", ".join(f"[{tag}]" for tag in self.criteria.tags))) else: self.tag_list.setText("") @@ -552,29 +556,37 @@ def list_profiles(self): class SaveSearchDialog(QDialog): """Dialog zum Speichern einer Suche als Profil.""" - def __init__(self, criteria, parent=None): + def __init__(self, criteria, parent=None, translator=None): super().__init__(parent) + self.translator = translator + if translator is not None: + set_default_translation_system(translator) self.criteria = criteria - self.setWindowTitle("Suche speichern") + self.setWindowTitle(_t("Suche speichern")) self.setMinimumWidth(300) layout = QFormLayout(self) self.name_input = QLineEdit() - self.name_input.setPlaceholderText("Name für diese Suche...") - layout.addRow("Name:", self.name_input) + self.name_input.setPlaceholderText(_t("Name für diese Suche...")) + layout.addRow(_t("Name:"), self.name_input) # Zusammenfassung summary = self._build_summary() summary_label = QLabel(summary) summary_label.setStyleSheet("color: gray; font-size: 11px;") - layout.addRow("Filter:", summary_label) + layout.addRow(_t("Filter:"), summary_label) # Buttons buttons = QDialogButtonBox( QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel ) + for standard, label in ((QDialogButtonBox.StandardButton.Save, "Speichern"), + (QDialogButtonBox.StandardButton.Cancel, "Abbrechen")): + button = buttons.button(standard) + if button is not None: + button.setText(_t(label)) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) layout.addRow(buttons) @@ -582,13 +594,13 @@ def __init__(self, criteria, parent=None): def _build_summary(self): parts = [] if self.criteria.text: - parts.append(f'Text: "{self.criteria.text}"') + parts.append(_t('Text: "{text}"', text=self.criteria.text)) if self.criteria.media_type: - parts.append(f"Typ: {self.criteria.media_type}") + parts.append(_t("Typ: {type}", type=_t(self.criteria.media_type))) if self.criteria.provider: - parts.append(f"Provider: {self.criteria.provider}") + parts.append(_t("Provider: {provider}", provider=_t(self.criteria.provider))) if self.criteria.favorites_only: - parts.append("Nur Favoriten") + parts.append(_t("Nur Favoriten")) return ", ".join(parts) if parts else "Keine Filter" def get_name(self): diff --git a/tests/test_gui_localization_i18n.py b/tests/test_gui_localization_i18n.py new file mode 100644 index 0000000..ccabcac --- /dev/null +++ b/tests/test_gui_localization_i18n.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +import os +import sys +from pathlib import Path + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") +PATCH_DIR = Path(__file__).resolve().parents[1] +REPO_DIR = Path(r"C:\_Local_DEV\_wt\mediabrain-worldset-20261025") +sys.path.insert(0, str(REPO_DIR)) +sys.path.insert(0, str(PATCH_DIR)) + +import pytest +from PySide6.QtWidgets import QApplication, QDialogButtonBox + +import config +import gui +from gui_playlists import ConditionRow, SmartPlaylistDialog +from search_advanced import AdvancedSearchBar +from translator import TranslationSystem, resolve_language + + +@pytest.fixture(scope="module") +def qapp(): + app = QApplication.instance() or QApplication([]) + yield app + + +def _translator(language: str) -> TranslationSystem: + return TranslationSystem( + language, + app_dir=PATCH_DIR, + auto_register_missing=False, + ) + + +def test_settings_language_persists_worldset_code_and_localizes_restart_notice(qapp, tmp_path, monkeypatch): + settings_path = tmp_path / "settings.json" + monkeypatch.setattr(config, "SETTINGS_PATH", settings_path) + test_config = config.Config() + monkeypatch.setattr(config, "config", test_config) + + translator = _translator("es") + messages = [] + monkeypatch.setattr( + gui, + "_show_message", + lambda icon, parent, title, body, *args: messages.append((title, body)) or 0, + ) + + window = gui.SettingsWindow(translator=translator) + assert window.windowTitle() == translator.t("Einstellungen") + ja_index = window.language_select.findData("ja") + assert ja_index >= 0 + assert window.language_select.itemText(ja_index) == "日本語" + + window.language_select.setCurrentIndex(ja_index) + + assert test_config.get("ui.language") == "ja" + assert window.language_select.currentData() == "ja" + assert messages == [( + translator.t("Anzeigesprache gespeichert"), + translator.t("Die Änderung der Anzeigesprache wird nach einem Neustart wirksam."), + )] + assert messages[0][0] != "Anzeigesprache gespeichert" + window.close() + + +def test_condition_row_locales_keep_filter_codes_in_item_data(qapp): + spanish = _translator("es") + es_row = ConditionRow(translator=spanish) + artist_index = es_row.field_combo.findData("artist") + assert artist_index >= 0 + assert es_row.field_combo.itemText(artist_index) == spanish.t("Kuenstler") + assert es_row.field_combo.itemData(artist_index) == "artist" + assert es_row.operator_combo.currentText() == spanish.t("contains") + assert es_row.operator_combo.currentData() == "contains" + assert es_row.conjunction_combo.currentText() == spanish.t("AND") + assert es_row.conjunction_combo.currentData() == "AND" + es_row.conjunction_combo.setCurrentIndex(es_row.conjunction_combo.findData("OR")) + condition = es_row.to_condition() + assert (condition.field, condition.operator, condition.conjunction) == ("title", "contains", "OR") + + japanese = _translator("ja") + ja_row = ConditionRow(translator=japanese) + assert ja_row.operator_combo.currentText() == japanese.t("contains") + assert ja_row.operator_combo.currentData() == "contains" + assert ja_row.field_combo.currentData() == "title" + + +def test_search_dropdown_labels_do_not_replace_filter_codes(qapp): + translator = _translator("ja") + search = AdvancedSearchBar(translator=translator) + movie_index = search.combo_type.findData("movie") + assert movie_index >= 0 + assert search.combo_type.itemText(movie_index) == translator.t("Filme") + assert search.combo_type.itemData(movie_index) == "movie" + + title_sort = next( + index for index in range(search.combo_sort.count()) + if tuple(search.combo_sort.itemData(index)) == ("title", False) + ) + assert search.combo_sort.itemText(title_sort) == translator.t("Titel A-Z") + assert tuple(search.combo_sort.itemData(title_sort)) == ("title", False) + + +def test_playlist_dialog_has_translated_standard_buttons_and_stable_operator_data(qapp): + translator = _translator("es") + dialog = SmartPlaylistDialog(translator=translator) + buttons = dialog.findChild(QDialogButtonBox) + assert buttons is not None + assert buttons.button(QDialogButtonBox.StandardButton.Ok).text() == translator.t("OK") + assert buttons.button(QDialogButtonBox.StandardButton.Cancel).text() == translator.t("Abbrechen") + + row = dialog._row_widgets()[0] + assert row.operator_combo.currentText() == translator.t("contains") + assert row.operator_combo.currentData() == "contains" + assert row.to_condition().operator == "contains" + dialog.close() + + +def test_language_resolution_keeps_historical_default_and_regional_env_values(): + assert resolve_language(None, None) == "de" + assert resolve_language("es-MX", "de") == "es" + assert resolve_language("unsupported", "ja") == "ja" diff --git a/translator.py b/translator.py index add714f..78dc2d8 100644 --- a/translator.py +++ b/translator.py @@ -22,6 +22,39 @@ SUPPORTED_LANGUAGES = ['de', 'en', 'es', 'zh', 'ja', 'ru', 'fr', 'hi', 'ar', 'bn', 'pt'] +_default_translation_system = None + +def set_default_translation_system(translator): + """Set the app-wide TranslationSystem used by optional GUI integrations.""" + global _default_translation_system + _default_translation_system = translator + + +def translate(key: str, translator=None, **kwargs) -> str: + """Translate a UI key while keeping the original text when no system is injected.""" + system = translator or _default_translation_system + if system is not None: + return system.t(key, **kwargs) + if kwargs: + try: + return key.format(**kwargs) + except (KeyError, IndexError, ValueError): + pass + return key + + +def resolve_language(*candidates) -> str: + """Resolve MEDIABRAIN_LANG/settings values, including regional subtags.""" + supported = set(SUPPORTED_LANGUAGES) + for candidate in candidates: + if not isinstance(candidate, str): + continue + normalized = candidate.strip().replace('_', '-').split('-', 1)[0].lower() + if normalized in supported: + return normalized + return 'de' + + class TranslationSystem: """Multi-Language Support System v1.1""" From 90e8ec2de0c9f66b8bedd4ea01f40cbc9dc0837d Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Sun, 25 Oct 2026 07:04:50 +0100 Subject: [PATCH 8/9] docs: document verified desktop and CLI language support --- README.md | 2 +- README_ar.md | 4 ++-- README_bn.md | 4 ++-- README_de.md | 2 +- README_es.md | 4 ++-- README_fr.md | 4 ++-- README_hi.md | 4 ++-- README_ja.md | 4 ++-- README_pt.md | 4 ++-- README_ru.md | 4 ++-- README_zh.md | 4 ++-- 11 files changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 302b35b..1cf3906 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ This list describes the README documentation languages only. -The Flutter app provides localized UI messages in German, English, Spanish, Chinese (Simplified), Japanese, Russian, French, Hindi, Arabic, Bengali, and Portuguese. Desktop/CLI localization remains separate; visual layout acceptance on target devices is still pending. +The Flutter app, desktop GUI, and Python CLI support German, English, Spanish, Simplified Chinese, Japanese, Russian, French, Hindi, Arabic, Bengali, and Portuguese. In the desktop app, choose a language under Settings → Language; the selection is saved and takes effect after restarting MediaBrain. The `MEDIABRAIN_LANG` environment variable overrides the saved selection, and the CLI also accepts `--lang `. Native Windows file-dialog frames follow the localization provided by Windows. ## Start Here diff --git a/README_ar.md b/README_ar.md index 4423861..c7558db 100644 --- a/README_ar.md +++ b/README_ar.md @@ -1,4 +1,4 @@ - + لافتة MediaBrain # MediaBrain @@ -40,7 +40,7 @@ تعرض هذه القائمة إصدارات README الموثقة فقط، ولا تصف اللغات التي يدعمها التطبيق أثناء التشغيل. -يوفّر تطبيق Flutter رسائل واجهة مستخدم مترجمة إلى الألمانية والإنجليزية والإسبانية والصينية المبسطة واليابانية والروسية والفرنسية والهندية والعربية والبنغالية والبرتغالية. يظل توطين تطبيق سطح المكتب وواجهة CLI عملاً منفصلاً؛ وما زال الاعتماد المرئي للتخطيط على الأجهزة المستهدفة معلقًا. +يدعم تطبيق MediaBrain المبني بـ Flutter وواجهة سطح المكتب وواجهة Python CLI اللغات الألمانية والإنجليزية والإسبانية والصينية المبسطة واليابانية والروسية والفرنسية والهندية والعربية والبنغالية والبرتغالية. في تطبيق سطح المكتب، اختر اللغة من الإعدادات ثم اللغة. يُحفَظ الاختيار ويُطبَّق بعد إعادة تشغيل MediaBrain. يتجاوز متغير البيئة `MEDIABRAIN_LANG` الاختيار المحفوظ، كما تدعم واجهة CLI الخيار `--lang `. وتظل إطارات مربعات حوار الملفات الأصلية في Windows خاضعة للتوطين الذي يوفره Windows. ## ابدأ من هنا diff --git a/README_bn.md b/README_bn.md index 525375f..fc1fa28 100644 --- a/README_bn.md +++ b/README_bn.md @@ -1,4 +1,4 @@ - + MediaBrain ব্যানার # MediaBrain @@ -40,7 +40,7 @@ এই তালিকায় শুধু README নথির ভাষাগত সংস্করণগুলো আছে। এটি অ্যাপের রানটাইমে কোন ভাষা সমর্থিত, তা জানায় না। -Flutter অ্যাপটি জার্মান, ইংরেজি, স্প্যানিশ, সরলীকৃত চীনা, জাপানি, রুশ, ফরাসি, হিন্দি, আরবি, বাংলা এবং পর্তুগিজ ভাষায় স্থানীয়কৃত UI বার্তা সরবরাহ করে। ডেস্কটপ অ্যাপ ও CLI-এর স্থানীয়করণ আলাদা কাজ; লক্ষ্য ডিভাইসে দৃশ্যমান বিন্যাসের গ্রহণযোগ্যতা এখনও বাকি। +MediaBrain-এর Flutter অ্যাপ, ডেস্কটপ GUI এবং Python CLI জার্মান, ইংরেজি, স্প্যানিশ, সরলীকৃত চীনা, জাপানি, রুশ, ফরাসি, হিন্দি, আরবি, বাংলা ও পর্তুগিজ ভাষা সমর্থন করে। ডেস্কটপ অ্যাপে সেটিংস → ভাষা থেকে ভাষা নির্বাচন করুন। নির্বাচনটি সংরক্ষিত হয় এবং MediaBrain পুনরায় চালু করার পর কার্যকর হয়। `MEDIABRAIN_LANG` পরিবেশ ভেরিয়েবল সংরক্ষিত ভাষাকে অগ্রাধিকার দেয়; CLI-তে `--lang `-ও ব্যবহার করা যায়। Windows-এর নেটিভ ফাইল ডায়ালগের ফ্রেম Windows-এর দেওয়া স্থানীয়করণ অনুযায়ী থাকে। ## এখান থেকে শুরু করুন diff --git a/README_de.md b/README_de.md index 8b5f3a1..b4585f5 100644 --- a/README_de.md +++ b/README_de.md @@ -39,7 +39,7 @@ Diese Liste beschreibt ausschließlich die Sprachfassungen der README-Dokumentation. -Die Flutter-App stellt lokalisierte Meldungen der Benutzeroberfläche auf Deutsch, Englisch, Spanisch, Chinesisch (vereinfacht), Japanisch, Russisch, Französisch, Hindi, Arabisch, Bengalisch und Portugiesisch bereit. Die Lokalisierung von Desktop-App und CLI bleibt davon getrennt; die visuelle Abnahme auf Zielgeräten steht noch aus. +Die Flutter-App, die Desktop-GUI und die Python-CLI von MediaBrain unterstützen Deutsch, Englisch, Spanisch, Chinesisch (vereinfacht), Japanisch, Russisch, Französisch, Hindi, Arabisch, Bengalisch und Portugiesisch. In der Desktop-App lässt sich die Sprache unter Einstellungen → Sprache auswählen. Die Auswahl wird gespeichert und nach einem Neustart von MediaBrain wirksam. Die Umgebungsvariable `MEDIABRAIN_LANG` überschreibt die gespeicherte Auswahl; die CLI unterstützt zusätzlich `--lang `. Die Rahmen nativer Windows-Dateidialoge folgen der von Windows bereitgestellten Lokalisierung. ## Schnellstart diff --git a/README_es.md b/README_es.md index 5376d5a..87b4f54 100644 --- a/README_es.md +++ b/README_es.md @@ -1,4 +1,4 @@ - + Banner de MediaBrain # MediaBrain @@ -40,7 +40,7 @@ Esta lista se refiere únicamente a las versiones documentales del README. -La aplicación Flutter ofrece mensajes de interfaz localizados en alemán, inglés, español, chino simplificado, japonés, ruso, francés, hindi, árabe, bengalí y portugués. La localización de la aplicación de escritorio y de la CLI sigue siendo independiente; la aceptación visual en los dispositivos de destino aún está pendiente. +La aplicación Flutter, la GUI de escritorio y la CLI de Python de MediaBrain admiten alemán, inglés, español, chino simplificado, japonés, ruso, francés, hindi, árabe, bengalí y portugués. En la aplicación de escritorio, elige el idioma en Ajustes → Idioma; la selección se guarda y se aplica después de reiniciar MediaBrain. La variable de entorno `MEDIABRAIN_LANG` sustituye el idioma guardado, y la CLI también admite `--lang `. Los marcos de los cuadros de diálogo nativos de Windows siguen la localización proporcionada por Windows. ## Primeros pasos diff --git a/README_fr.md b/README_fr.md index 6c022b7..7b928c2 100644 --- a/README_fr.md +++ b/README_fr.md @@ -1,4 +1,4 @@ - + Bannière MediaBrain # MediaBrain @@ -40,7 +40,7 @@ Cette liste présente uniquement les versions documentaires du README. Elle ne décrit pas les langues prises en charge par l’application à l’exécution. -L’application Flutter propose des messages d’interface localisés en allemand, anglais, espagnol, chinois simplifié, japonais, russe, français, hindi, arabe, bengali et portugais. La localisation de la version de bureau et de la CLI reste séparée ; la validation visuelle sur les appareils cibles est encore en attente. +L’application Flutter, l’interface de bureau et la CLI Python de MediaBrain prennent en charge l’allemand, l’anglais, l’espagnol, le chinois simplifié, le japonais, le russe, le français, l’hindi, l’arabe, le bengali et le portugais. Dans l’application de bureau, choisissez la langue dans Paramètres → Langue. Le choix est enregistré et prend effet après le redémarrage de MediaBrain. La variable d’environnement `MEDIABRAIN_LANG` remplace le choix enregistré, et la CLI accepte aussi `--lang `. Le cadre des boîtes de dialogue natives de Windows suit la localisation fournie par Windows. ## Pour commencer diff --git a/README_hi.md b/README_hi.md index f0e3f80..c96d7e5 100644 --- a/README_hi.md +++ b/README_hi.md @@ -1,4 +1,4 @@ - + MediaBrain बैनर # MediaBrain @@ -40,7 +40,7 @@ यह सूची केवल README के दस्तावेज़ी भाषा संस्करण दिखाती है। यह ऐप के रनटाइम भाषा समर्थन का दावा नहीं करती। -Flutter ऐप जर्मन, अंग्रेज़ी, स्पेनिश, सरलीकृत चीनी, जापानी, रूसी, फ़्रेंच, हिंदी, अरबी, बंगाली और पुर्तगाली में स्थानीयकृत यूज़र-इंटरफ़ेस संदेश प्रदान करता है। डेस्कटॉप ऐप और CLI का स्थानीयकरण अलग कार्य है; लक्षित उपकरणों पर दृश्य लेआउट की स्वीकृति अभी बाकी है। +MediaBrain का Flutter ऐप, डेस्कटॉप GUI और Python CLI जर्मन, अंग्रेज़ी, स्पेनिश, सरलीकृत चीनी, जापानी, रूसी, फ़्रेंच, हिंदी, अरबी, बंगाली और पुर्तगाली भाषाओं का समर्थन करते हैं। डेस्कटॉप ऐप में सेटिंग्स → भाषा से भाषा चुनें। चयन सहेजा जाता है और MediaBrain को पुनः शुरू करने के बाद लागू होता है। `MEDIABRAIN_LANG` वातावरण चर सहेजी गई भाषा को ओवरराइड करता है, और CLI में `--lang ` भी उपलब्ध है। Windows के मूल फ़ाइल संवादों के फ़्रेम Windows की भाषा के अनुसार स्थानीयकृत रहते हैं। ## यहाँ से शुरू करें diff --git a/README_ja.md b/README_ja.md index ee68bab..7d43519 100644 --- a/README_ja.md +++ b/README_ja.md @@ -1,4 +1,4 @@ - + MediaBrain バナー # MediaBrain @@ -40,7 +40,7 @@ この一覧は README の文書版のみを示します。アプリの実行時の言語対応を示すものではありません。 -Flutter アプリは、ドイツ語、英語、スペイン語、簡体字中国語、日本語、ロシア語、フランス語、ヒンディー語、アラビア語、ベンガル語、ポルトガル語のUIメッセージを提供します。デスクトップ版とCLIのローカライズは別途対応が必要で、対象デバイスでの視覚的なレイアウト確認は未完了です。 +MediaBrain の Flutter アプリ、デスクトップ GUI、Python CLI は、ドイツ語、英語、スペイン語、簡体字中国語、日本語、ロシア語、フランス語、ヒンディー語、アラビア語、ベンガル語、ポルトガル語に対応しています。デスクトップアプリでは「設定 → 言語」で選択します。選択は保存され、MediaBrain を再起動すると反映されます。環境変数 `MEDIABRAIN_LANG` は保存済みの選択を上書きし、CLI では `--lang ` も使用できます。Windows のネイティブファイルダイアログのウィンドウ枠は、Windows 側のローカライズに従います。 ## まずはこちら diff --git a/README_pt.md b/README_pt.md index 64a3eed..1b16918 100644 --- a/README_pt.md +++ b/README_pt.md @@ -1,4 +1,4 @@ - + Faixa do MediaBrain # MediaBrain @@ -40,7 +40,7 @@ Esta lista apresenta apenas as versões documentais do README. Não descreve os idiomas suportados pela aplicação em execução. -O aplicativo Flutter oferece mensagens de interface localizadas em alemão, inglês, espanhol, chinês simplificado, japonês, russo, francês, hindi, árabe, bengali e português. A localização da versão para desktop e da CLI continua separada; a validação visual nos dispositivos de destino ainda está pendente. +O aplicativo Flutter, a interface de desktop e a CLI Python do MediaBrain oferecem suporte a alemão, inglês, espanhol, chinês simplificado, japonês, russo, francês, hindi, árabe, bengali e português. No aplicativo de desktop, escolha o idioma em Configurações → Idioma. A escolha é salva e passa a valer depois que o MediaBrain for reiniciado. A variável de ambiente `MEDIABRAIN_LANG` substitui a escolha salva, e a CLI também aceita `--lang `. As molduras das caixas de diálogo nativas do Windows seguem a localização fornecida pelo Windows. ## Comece aqui diff --git a/README_ru.md b/README_ru.md index 728bd59..464d040 100644 --- a/README_ru.md +++ b/README_ru.md @@ -1,4 +1,4 @@ - + Баннер MediaBrain # MediaBrain @@ -40,7 +40,7 @@ В таблице перечислены только языковые версии документации README. Она не описывает поддержку языков в приложении во время выполнения. -Приложение Flutter предоставляет локализованные сообщения интерфейса на немецком, английском, испанском, упрощённом китайском, японском, русском, французском, хинди, арабском, бенгальском и португальском языках. Локализация настольной версии и CLI остаётся отдельной задачей; визуальная проверка на целевых устройствах ещё не завершена. +Приложение Flutter, настольный графический интерфейс и Python CLI MediaBrain поддерживают немецкий, английский, испанский, упрощённый китайский, японский, русский, французский, хинди, арабский, бенгальский и португальский языки. В настольном приложении выберите язык в разделе «Настройки → Язык»; выбор сохраняется и применяется после перезапуска MediaBrain. Переменная окружения `MEDIABRAIN_LANG` переопределяет сохранённый выбор, а CLI также поддерживает `--lang `. Рамки нативных окон выбора файлов Windows следуют локализации Windows. ## Начните здесь diff --git a/README_zh.md b/README_zh.md index 46f8883..15628e7 100644 --- a/README_zh.md +++ b/README_zh.md @@ -1,4 +1,4 @@ - + MediaBrain 横幅 # MediaBrain @@ -40,7 +40,7 @@ 此列表仅说明 README 文档版本,不代表应用运行时支持的语言。 -Flutter 应用提供德语、英语、西班牙语、简体中文、日语、俄语、法语、印地语、阿拉伯语、孟加拉语和葡萄牙语的本地化界面消息。桌面版和 CLI 的本地化仍需单独完成;目标设备上的视觉布局验收尚未完成。 +MediaBrain 的 Flutter 应用、桌面 GUI 和 Python CLI 支持德语、英语、西班牙语、简体中文、日语、俄语、法语、印地语、阿拉伯语、孟加拉语和葡萄牙语。在桌面应用中,可通过“设置 → 语言”选择语言;选择会保存,并在重启 MediaBrain 后生效。环境变量 `MEDIABRAIN_LANG` 可以覆盖已保存的语言,CLI 还支持 `--lang `。Windows 原生文件对话框的窗口框架仍按 Windows 提供的本地化语言显示。 ## 从这里开始 From 345bf2fd649ffa573db96e291acbad6a6e60a6c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 11:37:49 +0000 Subject: [PATCH 9/9] fix(l10n): restore singular plural for usageMinutes in DE/EN The eleven-language regeneration dropped the n == 1 branch, so the RE-SWEEP regression test failed in CI. Use ICU plural in the DE/EN ARB catalogs and generated code, and point the static test at them. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01THB2F2MDeT5zX4ahTjQx6w --- flutter_port/lib/l10n/app_de.arb | 2 +- flutter_port/lib/l10n/app_en.arb | 2 +- .../lib/l10n/app_localizations_de.dart | 8 +++++++- .../lib/l10n/app_localizations_en.dart | 8 +++++++- .../test/bugsweep_reswep_20260623_test.dart | 20 ++++++++++++------- 5 files changed, 29 insertions(+), 11 deletions(-) diff --git a/flutter_port/lib/l10n/app_de.arb b/flutter_port/lib/l10n/app_de.arb index 213b36c..1f5127a 100644 --- a/flutter_port/lib/l10n/app_de.arb +++ b/flutter_port/lib/l10n/app_de.arb @@ -161,7 +161,7 @@ } } }, - "usageMinutes": "{n} Minuten", + "usageMinutes": "{n, plural, one {{n} Minute} other {{n} Minuten}}", "@usageMinutes": { "placeholders": { "n": { diff --git a/flutter_port/lib/l10n/app_en.arb b/flutter_port/lib/l10n/app_en.arb index 1fa6a79..5aeecc5 100644 --- a/flutter_port/lib/l10n/app_en.arb +++ b/flutter_port/lib/l10n/app_en.arb @@ -161,7 +161,7 @@ } } }, - "usageMinutes": "{n} minutes", + "usageMinutes": "{n, plural, one {{n} minute} other {{n} minutes}}", "@usageMinutes": { "placeholders": { "n": { diff --git a/flutter_port/lib/l10n/app_localizations_de.dart b/flutter_port/lib/l10n/app_localizations_de.dart index 55dde6d..443f9d4 100644 --- a/flutter_port/lib/l10n/app_localizations_de.dart +++ b/flutter_port/lib/l10n/app_localizations_de.dart @@ -305,7 +305,13 @@ class AppLocalizationsDe extends AppLocalizations { @override String usageMinutes(int n) { - return '$n Minuten'; + String _temp0 = intl.Intl.pluralLogic( + n, + locale: localeName, + other: '$n Minuten', + one: '$n Minute', + ); + return '$_temp0'; } @override diff --git a/flutter_port/lib/l10n/app_localizations_en.dart b/flutter_port/lib/l10n/app_localizations_en.dart index 8cc3d9a..528fb4c 100644 --- a/flutter_port/lib/l10n/app_localizations_en.dart +++ b/flutter_port/lib/l10n/app_localizations_en.dart @@ -305,7 +305,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String usageMinutes(int n) { - return '$n minutes'; + String _temp0 = intl.Intl.pluralLogic( + n, + locale: localeName, + other: '$n minutes', + one: '$n minute', + ); + return '$_temp0'; } @override diff --git a/flutter_port/test/bugsweep_reswep_20260623_test.dart b/flutter_port/test/bugsweep_reswep_20260623_test.dart index 7bef0ad..5a29901 100644 --- a/flutter_port/test/bugsweep_reswep_20260623_test.dart +++ b/flutter_port/test/bugsweep_reswep_20260623_test.dart @@ -1,18 +1,24 @@ // Regressionstest fuer den Mobile-Bugsweep RE-SWEEP 2026-06-23 (MediaBrain/flutter_port). // // Statische Quelltext-Assertion (flutter/dart nicht im PATH). Red-on-revert. -// Befund des Entscheider-Re-Sweeps: usageMinutes(1) -> "1 Minuten" (Plural-1-Bug, -// in der 06-22-Runde uebersehen). Fix: Singular/Plural-Verzweigung. +// Befund: usageMinutes(1) -> "1 Minuten" (Plural-1-Bug). Fix: ICU-Plural in den +// ARB-Katalogen (DE/EN) und im generierten Code; die Singular/Plural-Verzweigung +// lebt seit der Elf-Sprachen-Lokalisierung nicht mehr in app_localizations.dart. import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; void main() { - final l10nSrc = File('lib/l10n/app_localizations.dart').readAsStringSync(); - test('usageMinutes pluralisiert korrekt (1 = Singular)', () { - expect(l10nSrc.contains("n == 1 ? 'Minute' : 'Minuten'"), isTrue); - expect(l10nSrc.contains("n == 1 ? 'minute' : 'minutes'"), isTrue); - expect(l10nSrc.contains("usageMinutes(int n) => '\$n Minuten'"), isFalse, + final de = File('lib/l10n/app_de.arb').readAsStringSync(); + final en = File('lib/l10n/app_en.arb').readAsStringSync(); + expect(de.contains('"usageMinutes": "{n, plural, one {{n} Minute} other {{n} Minuten}}"'), isTrue); + expect(en.contains('"usageMinutes": "{n, plural, one {{n} minute} other {{n} minutes}}"'), isTrue); + + final deGen = File('lib/l10n/app_localizations_de.dart').readAsStringSync(); + final enGen = File('lib/l10n/app_localizations_en.dart').readAsStringSync(); + expect(deGen.contains("one: '\$n Minute'"), isTrue); + expect(enGen.contains("one: '\$n minute'"), isTrue); + expect(deGen.contains("String usageMinutes(int n) {\n return '\$n Minuten';"), isFalse, reason: 'alte pluralfeste DE-Form entfernt'); }); }