Merge pull request #578 from christianlouis/copilot/implement-ui-i18n-support
feat(i18n): expand to 31 European languages, localize all in-product copy, merge with main
This commit is contained in:
@@ -18,6 +18,7 @@ from app.api.dropbox import router as dropbox_router
|
||||
from app.api.duplicates import router as duplicates_router
|
||||
from app.api.files import router as files_router
|
||||
from app.api.google_drive import router as google_drive_router
|
||||
from app.api.i18n import router as i18n_router
|
||||
from app.api.imap_accounts import router as imap_accounts_router
|
||||
from app.api.integrations import router as integrations_router
|
||||
from app.api.logs import router as logs_router
|
||||
@@ -84,3 +85,4 @@ router.include_router(integrations_router)
|
||||
router.include_router(notifications_router)
|
||||
router.include_router(scheduled_jobs_router)
|
||||
router.include_router(audit_logs_router)
|
||||
router.include_router(i18n_router)
|
||||
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
"""API endpoints for internationalization (i18n).
|
||||
|
||||
Provides endpoints for:
|
||||
* Listing available languages
|
||||
* Getting/setting user language preference (persisted in session + cookie + DB)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, Request, Response
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import UserProfile
|
||||
from app.utils.i18n import (
|
||||
DEFAULT_LANGUAGE,
|
||||
SUPPORTED_LANGUAGE_CODES,
|
||||
SUPPORTED_LANGUAGES,
|
||||
detect_language,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/i18n", tags=["i18n"])
|
||||
|
||||
|
||||
class LanguageInfo(BaseModel):
|
||||
"""Schema for a supported language."""
|
||||
|
||||
code: str
|
||||
name: str
|
||||
native: str
|
||||
flag: str
|
||||
|
||||
|
||||
class LanguageListResponse(BaseModel):
|
||||
"""Response for the list-languages endpoint."""
|
||||
|
||||
languages: list[LanguageInfo]
|
||||
current: str
|
||||
default: str
|
||||
|
||||
|
||||
class SetLanguageRequest(BaseModel):
|
||||
"""Request body for setting the preferred language."""
|
||||
|
||||
language: str
|
||||
|
||||
|
||||
class SetLanguageResponse(BaseModel):
|
||||
"""Response after changing the language."""
|
||||
|
||||
language: str
|
||||
message: str
|
||||
|
||||
|
||||
@router.get("/languages", response_model=LanguageListResponse)
|
||||
async def list_languages(request: Request) -> LanguageListResponse:
|
||||
"""Return all supported UI languages and the current active language."""
|
||||
current = detect_language(request)
|
||||
return LanguageListResponse(
|
||||
languages=[LanguageInfo(**lang) for lang in SUPPORTED_LANGUAGES],
|
||||
current=current,
|
||||
default=DEFAULT_LANGUAGE,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/language", response_model=SetLanguageResponse)
|
||||
async def set_language(
|
||||
body: SetLanguageRequest,
|
||||
request: Request,
|
||||
response: Response,
|
||||
db: Session = Depends(get_db),
|
||||
) -> SetLanguageResponse:
|
||||
"""Set the preferred UI language.
|
||||
|
||||
Persists the choice in:
|
||||
1. The server-side session
|
||||
2. A ``docuelevate_lang`` cookie (30-day expiry)
|
||||
3. The ``UserProfile.preferred_language`` column (if authenticated)
|
||||
"""
|
||||
lang = body.language.lower().strip()
|
||||
if lang not in SUPPORTED_LANGUAGE_CODES:
|
||||
lang = DEFAULT_LANGUAGE
|
||||
|
||||
# 1. Session
|
||||
if hasattr(request, "session"):
|
||||
request.session["preferred_language"] = lang
|
||||
|
||||
# 2. Cookie (30 days)
|
||||
response.set_cookie(
|
||||
key="docuelevate_lang",
|
||||
value=lang,
|
||||
max_age=30 * 24 * 60 * 60,
|
||||
httponly=False,
|
||||
samesite="lax",
|
||||
)
|
||||
|
||||
# 3. Database (if user is authenticated)
|
||||
_persist_language_to_profile(request, db, lang)
|
||||
|
||||
language_name = next(
|
||||
(entry["native"] for entry in SUPPORTED_LANGUAGES if entry["code"] == lang),
|
||||
lang,
|
||||
)
|
||||
logger.info("Language preference set to '%s'", lang)
|
||||
return SetLanguageResponse(
|
||||
language=lang,
|
||||
message=f"Language changed to {language_name}",
|
||||
)
|
||||
|
||||
|
||||
def _persist_language_to_profile(request: Request, db: Session, lang: str) -> None:
|
||||
"""Write language preference to the UserProfile row, if the user is logged in."""
|
||||
user_id: str | None = None
|
||||
if hasattr(request, "session"):
|
||||
user = request.session.get("user")
|
||||
if isinstance(user, dict):
|
||||
user_id = user.get("preferred_username") or user.get("email") or user.get("id")
|
||||
elif isinstance(user, str):
|
||||
user_id = user
|
||||
|
||||
if not user_id:
|
||||
return
|
||||
|
||||
try:
|
||||
profile = db.query(UserProfile).filter(UserProfile.user_id == user_id).first()
|
||||
if profile:
|
||||
profile.preferred_language = lang # type: ignore[attr-defined]
|
||||
db.commit()
|
||||
except Exception:
|
||||
db.rollback()
|
||||
logger.debug("Could not persist language preference for user_id=%s", user_id)
|
||||
@@ -277,6 +277,10 @@ class UserProfile(Base):
|
||||
preferred_destination = Column(String(50), nullable=True)
|
||||
stripe_customer_id = Column(String(64), nullable=True)
|
||||
|
||||
# UI language preference for i18n (ISO 639-1 code, e.g. "en", "de", "fr")
|
||||
# NULL means "auto-detect from browser Accept-Language header"
|
||||
preferred_language = Column(String(10), nullable=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
@@ -0,0 +1,539 @@
|
||||
"""Internationalization (i18n) and localization (l10n) utilities.
|
||||
|
||||
Provides a JSON-based translation system for the DocuElevate UI with:
|
||||
|
||||
* **31 supported languages** covering all major European languages plus ZH
|
||||
* Browser ``Accept-Language`` detection with cookie & user-profile persistence
|
||||
* AI-powered fallback translation via the configured LLM provider
|
||||
* Locale-aware date, number, and file-size formatting helpers
|
||||
* Jinja2 integration via a ``_()`` global function
|
||||
|
||||
Language resolution order:
|
||||
1. User profile ``preferred_language`` (persisted in DB)
|
||||
2. ``docuelevate_lang`` cookie
|
||||
3. ``Accept-Language`` HTTP header
|
||||
4. Default (``en``)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import date, datetime
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from starlette.requests import Request
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Supported languages (ordered by priority)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
SUPPORTED_LANGUAGES: list[dict[str, str]] = [
|
||||
# --- Tier 1: Primary European languages ---
|
||||
{"code": "en", "name": "English", "native": "English", "flag": "🇬🇧"},
|
||||
{"code": "de", "name": "German", "native": "Deutsch", "flag": "🇩🇪"},
|
||||
{"code": "fr", "name": "French", "native": "Français", "flag": "🇫🇷"},
|
||||
{"code": "es", "name": "Spanish", "native": "Español", "flag": "🇪🇸"},
|
||||
{"code": "it", "name": "Italian", "native": "Italiano", "flag": "🇮🇹"},
|
||||
{"code": "pt", "name": "Portuguese", "native": "Português", "flag": "🇵🇹"},
|
||||
# --- Tier 2: Western & Northern European ---
|
||||
{"code": "nl", "name": "Dutch", "native": "Nederlands", "flag": "🇳🇱"},
|
||||
{"code": "nb", "name": "Norwegian", "native": "Norsk", "flag": "🇳🇴"},
|
||||
{"code": "da", "name": "Danish", "native": "Dansk", "flag": "🇩🇰"},
|
||||
{"code": "sv", "name": "Swedish", "native": "Svenska", "flag": "🇸🇪"},
|
||||
{"code": "fi", "name": "Finnish", "native": "Suomi", "flag": "🇫🇮"},
|
||||
{"code": "is", "name": "Icelandic", "native": "Íslenska", "flag": "🇮🇸"},
|
||||
{"code": "ga", "name": "Irish", "native": "Gaeilge", "flag": "🇮🇪"},
|
||||
{"code": "lb", "name": "Luxembourgish", "native": "Lëtzebuergesch", "flag": "🇱🇺"},
|
||||
{"code": "ca", "name": "Catalan", "native": "Català", "flag": "🏴"},
|
||||
# --- Tier 3: Central & Eastern European ---
|
||||
{"code": "pl", "name": "Polish", "native": "Polski", "flag": "🇵🇱"},
|
||||
{"code": "cs", "name": "Czech", "native": "Čeština", "flag": "🇨🇿"},
|
||||
{"code": "sk", "name": "Slovak", "native": "Slovenčina", "flag": "🇸🇰"},
|
||||
{"code": "hu", "name": "Hungarian", "native": "Magyar", "flag": "🇭🇺"},
|
||||
{"code": "sl", "name": "Slovenian", "native": "Slovenščina", "flag": "🇸🇮"},
|
||||
{"code": "hr", "name": "Croatian", "native": "Hrvatski", "flag": "🇭🇷"},
|
||||
{"code": "ro", "name": "Romanian", "native": "Română", "flag": "🇷🇴"},
|
||||
{"code": "bg", "name": "Bulgarian", "native": "Български", "flag": "🇧🇬"},
|
||||
{"code": "el", "name": "Greek", "native": "Ελληνικά", "flag": "🇬🇷"},
|
||||
{"code": "et", "name": "Estonian", "native": "Eesti", "flag": "🇪🇪"},
|
||||
{"code": "lv", "name": "Latvian", "native": "Latviešu", "flag": "🇱🇻"},
|
||||
{"code": "lt", "name": "Lithuanian", "native": "Lietuvių", "flag": "🇱🇹"},
|
||||
# --- Tier 4: Non-EU European & Other ---
|
||||
{"code": "tr", "name": "Turkish", "native": "Türkçe", "flag": "🇹🇷"},
|
||||
{"code": "uk", "name": "Ukrainian", "native": "Українська", "flag": "🇺🇦"},
|
||||
{"code": "ru", "name": "Russian", "native": "Русский", "flag": "🇷🇺"},
|
||||
{"code": "zh", "name": "Chinese", "native": "中文", "flag": "🇨🇳"},
|
||||
]
|
||||
|
||||
SUPPORTED_LANGUAGE_CODES: set[str] = {lang["code"] for lang in SUPPORTED_LANGUAGES}
|
||||
DEFAULT_LANGUAGE = "en"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Translation file loading
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_TRANSLATIONS_DIR = Path(__file__).resolve().parent.parent.parent / "frontend" / "translations"
|
||||
_translation_cache: dict[str, dict[str, str]] = {}
|
||||
|
||||
|
||||
def _load_translations(locale: str) -> dict[str, str]:
|
||||
"""Load the translation JSON file for *locale*, with caching."""
|
||||
if locale in _translation_cache:
|
||||
return _translation_cache[locale]
|
||||
|
||||
filepath = _TRANSLATIONS_DIR / f"{locale}.json"
|
||||
if not filepath.is_file():
|
||||
logger.warning("Translation file not found for locale '%s'", locale)
|
||||
_translation_cache[locale] = {}
|
||||
return {}
|
||||
|
||||
try:
|
||||
data: dict[str, str] = json.loads(filepath.read_text(encoding="utf-8"))
|
||||
_translation_cache[locale] = data
|
||||
return data
|
||||
except (json.JSONDecodeError, OSError):
|
||||
logger.exception("Failed to load translations for '%s'", locale)
|
||||
_translation_cache[locale] = {}
|
||||
return {}
|
||||
|
||||
|
||||
def reload_translations() -> None:
|
||||
"""Clear the translation cache so files are re-read on next access."""
|
||||
_translation_cache.clear()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Core translation function
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def translate(key: str, locale: str | None = None, **kwargs: Any) -> str:
|
||||
"""Return the translated string for *key* in *locale*.
|
||||
|
||||
Falls back through:
|
||||
1. Requested *locale*
|
||||
2. English (``en``)
|
||||
3. The raw key itself (to keep the UI functional)
|
||||
|
||||
Positional placeholders ``{0}``, ``{1}`` or named placeholders
|
||||
``{name}`` in the translated string are interpolated via *kwargs*.
|
||||
"""
|
||||
locale = locale if locale and locale in SUPPORTED_LANGUAGE_CODES else DEFAULT_LANGUAGE
|
||||
|
||||
translations = _load_translations(locale)
|
||||
value = translations.get(key)
|
||||
|
||||
# Fallback to English
|
||||
if value is None and locale != DEFAULT_LANGUAGE:
|
||||
en_translations = _load_translations(DEFAULT_LANGUAGE)
|
||||
value = en_translations.get(key)
|
||||
|
||||
# Fallback to key itself
|
||||
if value is None:
|
||||
value = key
|
||||
|
||||
if kwargs:
|
||||
try:
|
||||
value = value.format(**kwargs)
|
||||
except (KeyError, IndexError):
|
||||
pass # Return unformatted string rather than crash
|
||||
|
||||
return value
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AI fallback translation (best-effort, non-blocking)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_ai_translation_cache: dict[tuple[str, str], str] = {}
|
||||
|
||||
|
||||
def translate_with_ai_fallback(text: str, target_locale: str) -> str:
|
||||
"""Translate *text* using the configured AI provider as a fallback.
|
||||
|
||||
Returns the original *text* unchanged when:
|
||||
* The target locale is English (source language)
|
||||
* The AI provider is unavailable or returns an error
|
||||
* The translation has already been cached
|
||||
|
||||
Results are cached in-memory for the lifetime of the process.
|
||||
"""
|
||||
if target_locale == DEFAULT_LANGUAGE or target_locale not in SUPPORTED_LANGUAGE_CODES:
|
||||
return text
|
||||
|
||||
cache_key = (text, target_locale)
|
||||
if cache_key in _ai_translation_cache:
|
||||
return _ai_translation_cache[cache_key]
|
||||
|
||||
target_name = next(
|
||||
(lang["name"] for lang in SUPPORTED_LANGUAGES if lang["code"] == target_locale),
|
||||
target_locale,
|
||||
)
|
||||
|
||||
try:
|
||||
from litellm import completion # type: ignore[import-untyped]
|
||||
|
||||
from app.config import settings
|
||||
|
||||
model = getattr(settings, "ai_model", None) or getattr(settings, "openai_model", "gpt-4o-mini")
|
||||
response = completion(
|
||||
model=model,
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
f"You are a professional translator. Translate the following UI text "
|
||||
f"from English to {target_name}. Return ONLY the translated text, "
|
||||
f"nothing else. Keep any HTML tags, placeholders like {{name}}, "
|
||||
f"and special characters intact."
|
||||
),
|
||||
},
|
||||
{"role": "user", "content": text},
|
||||
],
|
||||
max_tokens=256,
|
||||
temperature=0.1,
|
||||
)
|
||||
translated = response.choices[0].message.content.strip()
|
||||
_ai_translation_cache[cache_key] = translated
|
||||
return translated
|
||||
except Exception:
|
||||
logger.debug("AI fallback translation failed for '%s' → %s", text[:50], target_locale)
|
||||
return text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Language detection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def detect_language(request: Request) -> str:
|
||||
"""Determine the preferred UI language from the request context.
|
||||
|
||||
Resolution order:
|
||||
1. ``preferred_language`` stored in the user session
|
||||
2. ``docuelevate_lang`` cookie
|
||||
3. ``Accept-Language`` HTTP header (best match)
|
||||
4. Default → ``en``
|
||||
"""
|
||||
# 1. User session preference
|
||||
if hasattr(request, "session"):
|
||||
session_lang = request.session.get("preferred_language")
|
||||
if session_lang and session_lang in SUPPORTED_LANGUAGE_CODES:
|
||||
return session_lang
|
||||
|
||||
# 2. Cookie
|
||||
cookie_lang = request.cookies.get("docuelevate_lang")
|
||||
if cookie_lang and cookie_lang in SUPPORTED_LANGUAGE_CODES:
|
||||
return cookie_lang
|
||||
|
||||
# 3. Accept-Language header
|
||||
accept = request.headers.get("accept-language", "")
|
||||
lang = _parse_accept_language(accept)
|
||||
if lang:
|
||||
return lang
|
||||
|
||||
return DEFAULT_LANGUAGE
|
||||
|
||||
|
||||
def _parse_accept_language(header: str) -> str | None:
|
||||
"""Extract the best matching language from an ``Accept-Language`` header.
|
||||
|
||||
Parses quality values and returns the highest-priority match among
|
||||
:data:`SUPPORTED_LANGUAGE_CODES`, or ``None`` if nothing matches.
|
||||
"""
|
||||
if not header:
|
||||
return None
|
||||
|
||||
entries: list[tuple[float, str]] = []
|
||||
for raw_part in header.split(","):
|
||||
part = raw_part.strip()
|
||||
if not part:
|
||||
continue
|
||||
if ";q=" in part:
|
||||
lang_tag, _, q_str = part.partition(";q=")
|
||||
try:
|
||||
quality = float(q_str.strip())
|
||||
except ValueError:
|
||||
quality = 0.0
|
||||
else:
|
||||
lang_tag = part
|
||||
quality = 1.0
|
||||
entries.append((quality, lang_tag.strip().lower()))
|
||||
|
||||
# Sort by quality descending
|
||||
entries.sort(key=lambda e: e[0], reverse=True)
|
||||
|
||||
for _quality, tag in entries:
|
||||
# Try exact match first (e.g., "de", "zh")
|
||||
code = tag.split("-")[0]
|
||||
if code in SUPPORTED_LANGUAGE_CODES:
|
||||
return code
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Localization helpers (l10n)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Locale-specific formatting rules for date/number display
|
||||
_LOCALE_FORMATS: dict[str, dict[str, Any]] = {
|
||||
"en": {
|
||||
"date": "%B %d, %Y",
|
||||
"date_short": "%m/%d/%Y",
|
||||
"datetime": "%B %d, %Y %I:%M %p",
|
||||
"thousands_sep": ",",
|
||||
"decimal_sep": ".",
|
||||
},
|
||||
"de": {
|
||||
"date": "%d. %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d. %B %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"fr": {
|
||||
"date": "%d %B %Y",
|
||||
"date_short": "%d/%m/%Y",
|
||||
"datetime": "%d %B %Y %H:%M",
|
||||
"thousands_sep": "\u202f",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"es": {
|
||||
"date": "%d de %B de %Y",
|
||||
"date_short": "%d/%m/%Y",
|
||||
"datetime": "%d de %B de %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"it": {
|
||||
"date": "%d %B %Y",
|
||||
"date_short": "%d/%m/%Y",
|
||||
"datetime": "%d %B %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"pt": {
|
||||
"date": "%d de %B de %Y",
|
||||
"date_short": "%d/%m/%Y",
|
||||
"datetime": "%d de %B de %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"nl": {
|
||||
"date": "%d %B %Y",
|
||||
"date_short": "%d-%m-%Y",
|
||||
"datetime": "%d %B %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"nb": {
|
||||
"date": "%d. %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d. %B %Y %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"da": {
|
||||
"date": "%d. %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d. %B %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"sv": {
|
||||
"date": "%d %B %Y",
|
||||
"date_short": "%Y-%m-%d",
|
||||
"datetime": "%d %B %Y %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"fi": {
|
||||
"date": "%d. %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d. %B %Y %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"is": {
|
||||
"date": "%d. %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d. %B %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"ga": {
|
||||
"date": "%d %B %Y",
|
||||
"date_short": "%d/%m/%Y",
|
||||
"datetime": "%d %B %Y %H:%M",
|
||||
"thousands_sep": ",",
|
||||
"decimal_sep": ".",
|
||||
},
|
||||
"lb": {
|
||||
"date": "%d. %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d. %B %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"ca": {
|
||||
"date": "%d de %B de %Y",
|
||||
"date_short": "%d/%m/%Y",
|
||||
"datetime": "%d de %B de %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"pl": {
|
||||
"date": "%d %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d %B %Y %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"cs": {
|
||||
"date": "%d. %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d. %B %Y %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"sk": {
|
||||
"date": "%d. %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d. %B %Y %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"hu": {
|
||||
"date": "%Y. %B %d.",
|
||||
"date_short": "%Y.%m.%d.",
|
||||
"datetime": "%Y. %B %d. %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"sl": {
|
||||
"date": "%d. %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d. %B %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"hr": {
|
||||
"date": "%d. %B %Y.",
|
||||
"date_short": "%d.%m.%Y.",
|
||||
"datetime": "%d. %B %Y. %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"ro": {
|
||||
"date": "%d %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d %B %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"bg": {
|
||||
"date": "%d %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d %B %Y %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"el": {
|
||||
"date": "%d %B %Y",
|
||||
"date_short": "%d/%m/%Y",
|
||||
"datetime": "%d %B %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"et": {
|
||||
"date": "%d. %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d. %B %Y %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"lv": {
|
||||
"date": "%Y. gada %d. %B",
|
||||
"date_short": "%d.%m.%Y.",
|
||||
"datetime": "%Y. gada %d. %B %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"lt": {
|
||||
"date": "%Y m. %B %d d.",
|
||||
"date_short": "%Y-%m-%d",
|
||||
"datetime": "%Y m. %B %d d. %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"tr": {
|
||||
"date": "%d %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d %B %Y %H:%M",
|
||||
"thousands_sep": ".",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"uk": {
|
||||
"date": "%d %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d %B %Y %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
"zh": {
|
||||
"date": "%Y年%m月%d日",
|
||||
"date_short": "%Y/%m/%d",
|
||||
"datetime": "%Y年%m月%d日 %H:%M",
|
||||
"thousands_sep": ",",
|
||||
"decimal_sep": ".",
|
||||
},
|
||||
"ru": {
|
||||
"date": "%d %B %Y",
|
||||
"date_short": "%d.%m.%Y",
|
||||
"datetime": "%d %B %Y %H:%M",
|
||||
"thousands_sep": "\u00a0",
|
||||
"decimal_sep": ",",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def format_date(value: date | datetime | None, locale: str = DEFAULT_LANGUAGE, short: bool = False) -> str:
|
||||
"""Format a date/datetime value according to the locale conventions."""
|
||||
if value is None:
|
||||
return ""
|
||||
fmt_key = "date_short" if short else "date"
|
||||
fmt = _LOCALE_FORMATS.get(locale, _LOCALE_FORMATS[DEFAULT_LANGUAGE])[fmt_key]
|
||||
return value.strftime(fmt)
|
||||
|
||||
|
||||
def format_datetime(value: datetime | None, locale: str = DEFAULT_LANGUAGE) -> str:
|
||||
"""Format a datetime value according to the locale conventions."""
|
||||
if value is None:
|
||||
return ""
|
||||
fmt = _LOCALE_FORMATS.get(locale, _LOCALE_FORMATS[DEFAULT_LANGUAGE])["datetime"]
|
||||
return value.strftime(fmt)
|
||||
|
||||
|
||||
def format_number(value: int | float, locale: str = DEFAULT_LANGUAGE) -> str:
|
||||
"""Format a number with locale-appropriate thousand separators."""
|
||||
lf = _LOCALE_FORMATS.get(locale, _LOCALE_FORMATS[DEFAULT_LANGUAGE])
|
||||
if isinstance(value, float):
|
||||
int_part, _, dec_part = f"{value:,.2f}".partition(".")
|
||||
formatted_int = int_part.replace(",", lf["thousands_sep"])
|
||||
return f"{formatted_int}{lf['decimal_sep']}{dec_part}"
|
||||
return f"{value:,}".replace(",", lf["thousands_sep"])
|
||||
|
||||
|
||||
@lru_cache(maxsize=32)
|
||||
def get_language_info(code: str) -> dict[str, str] | None:
|
||||
"""Return the metadata dict for a supported language code, or ``None``."""
|
||||
for lang in SUPPORTED_LANGUAGES:
|
||||
if lang["code"] == code:
|
||||
return lang
|
||||
return None
|
||||
@@ -12,6 +12,14 @@ from sqlalchemy.orm import Session # noqa: F401
|
||||
from app.auth import require_login # noqa: F401
|
||||
from app.config import settings
|
||||
from app.database import get_db # noqa: F401
|
||||
from app.utils.i18n import (
|
||||
SUPPORTED_LANGUAGES,
|
||||
detect_language,
|
||||
format_date,
|
||||
format_datetime,
|
||||
format_number,
|
||||
translate,
|
||||
)
|
||||
|
||||
# Set up Jinja2 templates
|
||||
templates_dir = Path(__file__).parent.parent.parent / "frontend" / "templates"
|
||||
@@ -21,6 +29,16 @@ templates = Jinja2Templates(directory=str(templates_dir))
|
||||
templates.env.globals["min"] = min
|
||||
templates.env.globals["max"] = max
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# i18n Jinja2 integration
|
||||
# ---------------------------------------------------------------------------
|
||||
# The _() function is available in every template to translate UI strings.
|
||||
# Usage: {{ _("nav.dashboard") }} or {{ _("upload.max_size", size="10 MB") }}
|
||||
# The locale is automatically resolved from the request context.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
templates.env.globals["supported_languages"] = SUPPORTED_LANGUAGES
|
||||
|
||||
# Customize Jinja2Templates to include app_version in all templates
|
||||
original_template_response = templates.TemplateResponse
|
||||
|
||||
@@ -48,8 +66,31 @@ def _inject_global_context(ctx: dict) -> None:
|
||||
session_user = req.session.get("user")
|
||||
# When auth is disabled every visitor is effectively "logged in"
|
||||
ctx.setdefault("is_logged_in", not getattr(settings, "auth_enabled", True) or session_user is not None)
|
||||
|
||||
# --- i18n: detect language and register template helpers ---
|
||||
current_locale = detect_language(req)
|
||||
ctx.setdefault("current_locale", current_locale)
|
||||
|
||||
def _translate(key: str, **kwargs: object) -> str:
|
||||
return translate(key, current_locale, **kwargs)
|
||||
|
||||
def _format_date(value: object, short: bool = False) -> str:
|
||||
return format_date(value, current_locale, short=short) # type: ignore[arg-type]
|
||||
|
||||
def _format_datetime(value: object) -> str:
|
||||
return format_datetime(value, current_locale) # type: ignore[arg-type]
|
||||
|
||||
def _format_number(value: object) -> str:
|
||||
return format_number(value, current_locale) # type: ignore[arg-type]
|
||||
|
||||
ctx.setdefault("_", _translate)
|
||||
ctx.setdefault("format_date_l10n", _format_date)
|
||||
ctx.setdefault("format_datetime_l10n", _format_datetime)
|
||||
ctx.setdefault("format_number_l10n", _format_number)
|
||||
else:
|
||||
ctx.setdefault("is_logged_in", not getattr(settings, "auth_enabled", True))
|
||||
ctx.setdefault("current_locale", "en")
|
||||
ctx.setdefault("_", lambda key, **kw: translate(key, "en", **kw))
|
||||
|
||||
|
||||
def template_response_with_version(*args, **kwargs):
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
# Internationalization (i18n) & Localization (l10n) Guide
|
||||
|
||||
DocuElevate supports **10 languages** for its web UI, with automatic browser
|
||||
language detection, user-preference persistence, and an AI-powered fallback
|
||||
translator for strings that haven't been manually translated yet.
|
||||
|
||||
## Supported Languages
|
||||
|
||||
| Code | Language | Native Name | Priority |
|
||||
|------|------------|-------------|----------|
|
||||
| `en` | English | English | Tier 1 |
|
||||
| `de` | German | Deutsch | Tier 1 |
|
||||
| `fr` | French | Français | Tier 1 |
|
||||
| `es` | Spanish | Español | Tier 1 |
|
||||
| `it` | Italian | Italiano | Tier 1 |
|
||||
| `pt` | Portuguese | Português | Tier 1 |
|
||||
| `nl` | Dutch | Nederlands | Tier 2 |
|
||||
| `pl` | Polish | Polski | Tier 2 |
|
||||
| `zh` | Chinese | 中文 | Tier 2 |
|
||||
| `ru` | Russian | Русский | Tier 2 |
|
||||
|
||||
> **Tier 1** languages (European priority) have complete, manually-reviewed
|
||||
> translations. **Tier 2** languages have complete translations but may
|
||||
> receive less frequent updates.
|
||||
|
||||
## How Language Is Detected
|
||||
|
||||
DocuElevate resolves the display language in the following priority order:
|
||||
|
||||
1. **User profile preference** — stored in the database (`UserProfile.preferred_language`)
|
||||
and loaded into the session on login
|
||||
2. **Cookie** — `docuelevate_lang` cookie (30-day expiry, set when user selects a language)
|
||||
3. **Browser `Accept-Language` header** — the highest-priority match among supported languages
|
||||
4. **Default** — English (`en`)
|
||||
|
||||
## Selecting Your Language
|
||||
|
||||
### Via the Navigation Bar
|
||||
|
||||
Click the 🌐 **globe icon** in the top navigation bar. A dropdown menu shows all
|
||||
available languages with their native names and flag emoji. The current language
|
||||
is highlighted with a blue checkmark.
|
||||
|
||||
### Via the API
|
||||
|
||||
```bash
|
||||
# Set language to German
|
||||
curl -X POST http://localhost:8000/api/i18n/language \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"language": "de"}'
|
||||
|
||||
# List all available languages
|
||||
curl http://localhost:8000/api/i18n/languages
|
||||
```
|
||||
|
||||
### Via Cookie (Programmatic)
|
||||
|
||||
Set the `docuelevate_lang` cookie to any supported language code:
|
||||
|
||||
```javascript
|
||||
document.cookie = "docuelevate_lang=fr; max-age=2592000; path=/";
|
||||
location.reload();
|
||||
```
|
||||
|
||||
## For Developers
|
||||
|
||||
### Translation File Structure
|
||||
|
||||
Translations are stored as flat JSON files in `frontend/translations/`:
|
||||
|
||||
```
|
||||
frontend/translations/
|
||||
├── en.json # English (base / reference)
|
||||
├── de.json # German
|
||||
├── fr.json # French
|
||||
├── es.json # Spanish
|
||||
├── it.json # Italian
|
||||
├── pt.json # Portuguese
|
||||
├── nl.json # Dutch
|
||||
├── pl.json # Polish
|
||||
├── zh.json # Chinese
|
||||
└── ru.json # Russian
|
||||
```
|
||||
|
||||
Each file is a flat key-value dictionary with dot-notation namespacing:
|
||||
|
||||
```json
|
||||
{
|
||||
"nav.dashboard": "Dashboard",
|
||||
"nav.upload": "Upload",
|
||||
"upload.max_size": "Maximum file size: {size}",
|
||||
"footer.copyright": "DocuElevate {year}"
|
||||
}
|
||||
```
|
||||
|
||||
### Using Translations in Templates
|
||||
|
||||
The `_()` function is available globally in all Jinja2 templates:
|
||||
|
||||
```jinja2
|
||||
{# Simple translation #}
|
||||
<h1>{{ _("dashboard.title") }}</h1>
|
||||
|
||||
{# Translation with placeholders #}
|
||||
<p>{{ _("upload.max_size", size="50 MB") }}</p>
|
||||
|
||||
{# Translation in attributes #}
|
||||
<button aria-label="{{ _('common.save') }}">{{ _("common.save") }}</button>
|
||||
```
|
||||
|
||||
### Using Translations in Python
|
||||
|
||||
```python
|
||||
from app.utils.i18n import translate
|
||||
|
||||
# Basic translation
|
||||
text = translate("nav.dashboard", "de") # → "Übersicht"
|
||||
|
||||
# With placeholders
|
||||
text = translate("footer.copyright", "fr", year="2025") # → "DocuElevate 2025"
|
||||
```
|
||||
|
||||
### Localization Helpers
|
||||
|
||||
Format dates, times, and numbers according to locale conventions:
|
||||
|
||||
```jinja2
|
||||
{# In templates — locale is automatically detected #}
|
||||
<span>{{ format_date_l10n(document.created_at) }}</span>
|
||||
<span>{{ format_number_l10n(file_count) }}</span>
|
||||
```
|
||||
|
||||
```python
|
||||
# In Python
|
||||
from app.utils.i18n import format_date, format_number
|
||||
|
||||
format_date(date(2025, 3, 15), "de") # → "15. March 2025"
|
||||
format_date(date(2025, 3, 15), "de", short=True) # → "15.03.2025"
|
||||
format_number(1234567, "de") # → "1.234.567"
|
||||
format_number(1234.56, "en") # → "1,234.56"
|
||||
```
|
||||
|
||||
### Adding a New Translation Key
|
||||
|
||||
1. Add the key and English text to `frontend/translations/en.json`
|
||||
2. Add translations for all other languages in their respective files
|
||||
3. Use `{{ _("your.new.key") }}` in templates
|
||||
|
||||
### AI Fallback Translation
|
||||
|
||||
When a translation key exists in English but not in the target language,
|
||||
DocuElevate can use the configured AI provider (OpenAI, Anthropic, etc.)
|
||||
to translate the string on-the-fly:
|
||||
|
||||
```python
|
||||
from app.utils.i18n import translate_with_ai_fallback
|
||||
|
||||
# Falls back to AI if no manual translation exists
|
||||
translated = translate_with_ai_fallback("Welcome to our platform", "de")
|
||||
```
|
||||
|
||||
The AI fallback:
|
||||
- Uses the `AI_MODEL` or `OPENAI_MODEL` setting
|
||||
- Caches results in memory for the process lifetime
|
||||
- Returns the original English text if the AI call fails
|
||||
- Is designed for graceful degradation — the UI never breaks
|
||||
|
||||
### Adding a New Language
|
||||
|
||||
1. Create a new JSON file in `frontend/translations/` (e.g., `ja.json`)
|
||||
2. Copy the structure from `en.json` and translate all values
|
||||
3. Add the language to `SUPPORTED_LANGUAGES` in `app/utils/i18n.py`:
|
||||
```python
|
||||
{"code": "ja", "name": "Japanese", "native": "日本語", "flag": "🇯🇵"},
|
||||
```
|
||||
4. Add locale formatting rules to `_LOCALE_FORMATS` in the same file
|
||||
5. Create a database migration if needed (the `preferred_language` column
|
||||
already accepts any string up to 10 characters)
|
||||
|
||||
### Database Migration
|
||||
|
||||
Migration `027_add_user_language_preference` adds a `preferred_language`
|
||||
column to the `user_profiles` table. This column stores the user's chosen
|
||||
UI language as an ISO 639-1 code (e.g., `"de"`, `"fr"`). A `NULL` value
|
||||
means "auto-detect from browser settings."
|
||||
|
||||
### API Reference
|
||||
|
||||
#### `GET /api/i18n/languages`
|
||||
|
||||
Returns all supported languages and the current active language.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"languages": [
|
||||
{"code": "en", "name": "English", "native": "English", "flag": "🇬🇧"},
|
||||
{"code": "de", "name": "German", "native": "Deutsch", "flag": "🇩🇪"}
|
||||
],
|
||||
"current": "en",
|
||||
"default": "en"
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /api/i18n/language`
|
||||
|
||||
Set the preferred UI language. Persists in session, cookie, and database.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{"language": "de"}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"language": "de",
|
||||
"message": "Language changed to Deutsch"
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
No additional configuration is required. The i18n system works out of the box
|
||||
with the default English language and automatically detects browser preferences.
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| Browser `Accept-Language` | Auto-detected | Used when no explicit preference is set |
|
||||
| `docuelevate_lang` cookie | Not set | Set when user selects a language via the UI |
|
||||
| `UserProfile.preferred_language` | `NULL` | Stored in DB for authenticated users |
|
||||
+143
-76
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-color-scheme-default="{{ ui_default_color_scheme | default('system') }}">
|
||||
<html lang="{{ current_locale | default('en') }}" data-color-scheme-default="{{ ui_default_color_scheme | default('system') }}">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>{% block title %}DocuElevate{% endblock %}</title>
|
||||
@@ -37,10 +37,10 @@
|
||||
data-multi-user="{{ 'true' if multi_user_enabled else 'false' }}"
|
||||
data-allow-signup="{{ 'true' if allow_signup else 'false' }}">
|
||||
<!-- Skip to main content link for keyboard/screen reader users -->
|
||||
<a href="#main-content" class="skip-link">Skip to main content</a>
|
||||
<a href="#main-content" class="skip-link">{{ _("nav.skip_to_content") }}</a>
|
||||
|
||||
<!-- Global Nav -->
|
||||
<nav class="bg-white shadow relative" aria-label="Main navigation">
|
||||
<nav class="bg-white shadow relative" aria-label="{{ _('nav.main_navigation') }}">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between h-16 items-center">
|
||||
|
||||
<!-- Brand + Icon -->
|
||||
@@ -70,12 +70,12 @@
|
||||
<a href="/pricing"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100"
|
||||
{% if request and request.url.path == '/pricing' %}aria-current="page"{% endif %}>
|
||||
Pricing
|
||||
{{ _("nav.pricing") }}
|
||||
</a>
|
||||
<a href="/about"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100"
|
||||
{% if request and request.url.path == '/about' %}aria-current="page"{% endif %}>
|
||||
About
|
||||
{{ _("nav.about") }}
|
||||
</a>
|
||||
|
||||
{% else %}
|
||||
@@ -84,32 +84,32 @@
|
||||
<a href="/"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100"
|
||||
{% if request and request.url.path == '/' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-home mr-1 text-gray-400" aria-hidden="true"></i>Dashboard
|
||||
<i class="fas fa-home mr-1 text-gray-400" aria-hidden="true"></i>{{ _("nav.dashboard") }}
|
||||
</a>
|
||||
<a href="/upload"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium text-blue-600 hover:text-blue-800 hover:bg-blue-50"
|
||||
{% if request and request.url.path == '/upload' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-upload mr-1" aria-hidden="true"></i>Upload
|
||||
<i class="fas fa-upload mr-1" aria-hidden="true"></i>{{ _("nav.upload") }}
|
||||
</a>
|
||||
<a href="/files"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100"
|
||||
{% if request and request.url.path == '/files' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-folder-open mr-1 text-gray-400" aria-hidden="true"></i>Files
|
||||
<i class="fas fa-folder-open mr-1 text-gray-400" aria-hidden="true"></i>{{ _("nav.files") }}
|
||||
</a>
|
||||
<a href="/search"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100"
|
||||
{% if request and request.url.path == '/search' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-search mr-1 text-gray-400" aria-hidden="true"></i>Search
|
||||
<i class="fas fa-search mr-1 text-gray-400" aria-hidden="true"></i>{{ _("nav.search") }}
|
||||
</a>
|
||||
<a href="/pipelines"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100"
|
||||
{% if request and request.url.path == '/pipelines' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-project-diagram mr-1 text-gray-400" aria-hidden="true"></i>Pipelines
|
||||
<i class="fas fa-project-diagram mr-1 text-gray-400" aria-hidden="true"></i>{{ _("nav.pipelines") }}
|
||||
</a>
|
||||
<a href="/integrations"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100"
|
||||
{% if request and request.url.path == '/integrations' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-plug mr-1 text-gray-400" aria-hidden="true"></i>Integrations
|
||||
<i class="fas fa-plug mr-1 text-gray-400" aria-hidden="true"></i>{{ _("nav.integrations") }}
|
||||
</a>
|
||||
|
||||
<!-- Admin dropdown – shown only for admin users via JS -->
|
||||
@@ -124,7 +124,7 @@
|
||||
:aria-expanded="adminMenuOpen"
|
||||
>
|
||||
<i class="fas fa-shield-alt mr-1 text-red-500" aria-hidden="true"></i>
|
||||
Admin
|
||||
{{ _("nav.admin") }}
|
||||
<i class="fas fa-chevron-down ml-1 text-xs" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div
|
||||
@@ -141,49 +141,52 @@
|
||||
>
|
||||
<div class="py-1">
|
||||
<a href="/settings" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-cog w-4 mr-2 text-gray-500" aria-hidden="true"></i> Settings
|
||||
<i class="fas fa-cog w-4 mr-2 text-gray-500" aria-hidden="true"></i> {{ _("nav.settings") }}
|
||||
</a>
|
||||
<a href="/admin/users" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-users w-4 mr-2 text-blue-500" aria-hidden="true"></i> Users
|
||||
<i class="fas fa-users w-4 mr-2 text-blue-500" aria-hidden="true"></i> {{ _("nav.users") }}
|
||||
</a>
|
||||
<a href="/admin/plans" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-layer-group w-4 mr-2 text-indigo-500" aria-hidden="true"></i> Plan Designer
|
||||
<i class="fas fa-layer-group w-4 mr-2 text-indigo-500" aria-hidden="true"></i> {{ _("nav.plan_designer") }}
|
||||
</a>
|
||||
<a href="/admin/credentials" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-key w-4 mr-2 text-yellow-500" aria-hidden="true"></i> Credentials
|
||||
<i class="fas fa-key w-4 mr-2 text-yellow-500" aria-hidden="true"></i> {{ _("nav.credentials") }}
|
||||
</a>
|
||||
<a href="/admin/files" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-folder-open w-4 mr-2 text-gray-500" aria-hidden="true"></i> File Manager
|
||||
<i class="fas fa-folder-open w-4 mr-2 text-gray-500" aria-hidden="true"></i> {{ _("nav.file_manager") }}
|
||||
</a>
|
||||
<div class="border-t border-gray-100 my-1"></div>
|
||||
<a href="/duplicates" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-clone w-4 mr-2 text-orange-500" aria-hidden="true"></i> Duplicates
|
||||
<i class="fas fa-clone w-4 mr-2 text-orange-500" aria-hidden="true"></i> {{ _("nav.duplicates") }}
|
||||
</a>
|
||||
<a href="/similarity" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-sitemap w-4 mr-2 text-purple-500" aria-hidden="true"></i> Similarity
|
||||
<i class="fas fa-sitemap w-4 mr-2 text-purple-500" aria-hidden="true"></i> {{ _("nav.similarity") }}
|
||||
</a>
|
||||
<a href="/admin/queue" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-stream w-4 mr-2 text-blue-500" aria-hidden="true"></i> Queue Monitor
|
||||
<i class="fas fa-stream w-4 mr-2 text-blue-500" aria-hidden="true"></i> {{ _("nav.queue_monitor") }}
|
||||
</a>
|
||||
<a href="/admin/scheduled-jobs" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-clock w-4 mr-2 text-indigo-500" aria-hidden="true"></i> Scheduled Jobs
|
||||
<i class="fas fa-clock w-4 mr-2 text-indigo-500" aria-hidden="true"></i> {{ _("nav.scheduled_jobs") }}
|
||||
</a>
|
||||
<a href="/admin/backup" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-database w-4 mr-2 text-green-600" aria-hidden="true"></i> Backup & Restore
|
||||
<i class="fas fa-database w-4 mr-2 text-green-600" aria-hidden="true"></i> {{ _("nav.backup_restore") }}
|
||||
</a>
|
||||
<a href="/admin/audit-logs" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-shield-halved w-4 mr-2 text-indigo-500" aria-hidden="true"></i> Audit Logs
|
||||
</a>
|
||||
<a href="/admin/audit-logs" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-shield-halved w-4 mr-2 text-indigo-500" aria-hidden="true"></i> Audit Logs
|
||||
</a>
|
||||
<a href="/status" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
{% if request and request.url.path == '/status' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-circle-dot w-4 mr-2 text-gray-500" aria-hidden="true"></i> Status
|
||||
<i class="fas fa-circle-dot w-4 mr-2 text-gray-500" aria-hidden="true"></i> {{ _("nav.status") }}
|
||||
</a>
|
||||
<div class="border-t border-gray-100 my-1"></div>
|
||||
<a href="/admin/api-docs" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-code w-4 mr-2 text-teal-500" aria-hidden="true"></i> API Docs
|
||||
<i class="fas fa-code w-4 mr-2 text-teal-500" aria-hidden="true"></i> {{ _("nav.api_docs") }}
|
||||
</a>
|
||||
<a href="/developer-docs/" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-book w-4 mr-2 text-teal-500" aria-hidden="true"></i> Developer Docs
|
||||
<i class="fas fa-book w-4 mr-2 text-teal-500" aria-hidden="true"></i> {{ _("nav.developer_docs") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -194,18 +197,18 @@
|
||||
<!-- Help – always visible, for every visitor regardless of auth state -->
|
||||
<a href="/help"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100"
|
||||
aria-label="Help Center"
|
||||
title="Help Center"
|
||||
aria-label="{{ _('nav.help_center') }}"
|
||||
title="{{ _('nav.help_center') }}"
|
||||
{% if request and request.url.path == '/help' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-circle-question mr-1 text-gray-400" aria-hidden="true"></i>Help
|
||||
<i class="fas fa-circle-question mr-1 text-gray-400" aria-hidden="true"></i>{{ _("nav.help") }}
|
||||
</a>
|
||||
|
||||
<!-- Bell notification icon -->
|
||||
<a href="/notifications"
|
||||
id="notificationBell"
|
||||
class="relative p-1.5 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
|
||||
aria-label="Notifications"
|
||||
title="Notifications"
|
||||
aria-label="{{ _('nav.notifications') }}"
|
||||
title="{{ _('nav.notifications') }}"
|
||||
{% if request and request.url.path == '/notifications' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-bell" aria-hidden="true"></i>
|
||||
<span id="notificationBadge"
|
||||
@@ -213,14 +216,59 @@
|
||||
aria-live="polite"></span>
|
||||
</a>
|
||||
|
||||
<!-- Language selector dropdown -->
|
||||
<div x-data="{ langOpen: false }" class="relative">
|
||||
<button
|
||||
@click="langOpen = !langOpen"
|
||||
@click.outside="langOpen = false"
|
||||
type="button"
|
||||
class="p-1.5 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
|
||||
aria-label="{{ _('language.selector') }}"
|
||||
title="{{ _('language.selector') }}"
|
||||
:aria-expanded="langOpen"
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<i class="fas fa-globe"></i>
|
||||
</button>
|
||||
<div
|
||||
x-show="langOpen"
|
||||
x-transition:enter="transition ease-out duration-100 transform"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75 transform"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 z-50"
|
||||
role="menu"
|
||||
aria-label="{{ _('language.selector') }}"
|
||||
>
|
||||
<div class="py-1">
|
||||
{% for lang in supported_languages %}
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
class="flex items-center w-full px-4 py-2 text-sm text-left hover:bg-gray-100 {% if current_locale == lang.code %}bg-blue-50 text-blue-700 font-medium{% else %}text-gray-700{% endif %}"
|
||||
onclick="setLanguage('{{ lang.code }}')"
|
||||
>
|
||||
<span class="mr-2">{{ lang.flag }}</span>
|
||||
<span>{{ lang.native }}</span>
|
||||
{% if current_locale == lang.code %}
|
||||
<i class="fas fa-check ml-auto text-blue-500" aria-hidden="true"></i>
|
||||
{% endif %}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dark mode toggle -->
|
||||
<button
|
||||
id="darkModeToggle"
|
||||
onclick="toggleDarkMode()"
|
||||
type="button"
|
||||
class="p-1.5 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
|
||||
aria-label="Toggle dark mode"
|
||||
title="Toggle dark mode"
|
||||
aria-label="{{ _('nav.toggle_dark_mode') }}"
|
||||
title="{{ _('nav.toggle_dark_mode') }}"
|
||||
>
|
||||
<i class="fas fa-moon"></i>
|
||||
</button>
|
||||
@@ -235,10 +283,10 @@
|
||||
type="button"
|
||||
class="md:hidden inline-flex items-center justify-center p-3 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
|
||||
style="min-height:44px;min-width:44px;"
|
||||
aria-label="Toggle navigation menu"
|
||||
aria-label="{{ _('nav.toggle_nav') }}"
|
||||
:aria-expanded="mobileMenuOpen"
|
||||
>
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<span class="sr-only">{{ _("nav.open_main_menu") }}</span>
|
||||
<svg class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
@@ -262,12 +310,12 @@
|
||||
<a href="/pricing"
|
||||
class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
|
||||
{% if request and request.url.path == '/pricing' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-tag mr-2 text-gray-400" aria-hidden="true"></i>Pricing
|
||||
<i class="fas fa-tag mr-2 text-gray-400" aria-hidden="true"></i>{{ _("nav.pricing") }}
|
||||
</a>
|
||||
<a href="/about"
|
||||
class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
|
||||
{% if request and request.url.path == '/about' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-info-circle mr-2 text-gray-400" aria-hidden="true"></i>About
|
||||
<i class="fas fa-info-circle mr-2 text-gray-400" aria-hidden="true"></i>{{ _("nav.about") }}
|
||||
</a>
|
||||
|
||||
{% else %}
|
||||
@@ -275,84 +323,84 @@
|
||||
<a href="/"
|
||||
class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
|
||||
{% if request and request.url.path == '/' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-home mr-2 text-gray-400" aria-hidden="true"></i>Dashboard
|
||||
<i class="fas fa-home mr-2 text-gray-400" aria-hidden="true"></i>{{ _("nav.dashboard") }}
|
||||
</a>
|
||||
<a href="/upload"
|
||||
class="block px-3 py-3 rounded-md text-base font-medium text-blue-600 hover:text-blue-800 hover:bg-blue-50"
|
||||
{% if request and request.url.path == '/upload' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-upload mr-2" aria-hidden="true"></i>Upload
|
||||
<i class="fas fa-upload mr-2" aria-hidden="true"></i>{{ _("nav.upload") }}
|
||||
</a>
|
||||
<a href="/files"
|
||||
class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
|
||||
{% if request and request.url.path == '/files' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-folder-open mr-2 text-gray-400" aria-hidden="true"></i>Files
|
||||
<i class="fas fa-folder-open mr-2 text-gray-400" aria-hidden="true"></i>{{ _("nav.files") }}
|
||||
</a>
|
||||
<a href="/search"
|
||||
class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
|
||||
{% if request and request.url.path == '/search' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-search mr-2 text-gray-400" aria-hidden="true"></i>Search
|
||||
<i class="fas fa-search mr-2 text-gray-400" aria-hidden="true"></i>{{ _("nav.search") }}
|
||||
</a>
|
||||
<a href="/pipelines"
|
||||
class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
|
||||
{% if request and request.url.path == '/pipelines' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-project-diagram mr-2 text-gray-400" aria-hidden="true"></i>Pipelines
|
||||
<i class="fas fa-project-diagram mr-2 text-gray-400" aria-hidden="true"></i>{{ _("nav.pipelines") }}
|
||||
</a>
|
||||
<a href="/integrations"
|
||||
class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
|
||||
{% if request and request.url.path == '/integrations' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-plug mr-2 text-gray-400" aria-hidden="true"></i>Integrations
|
||||
<i class="fas fa-plug mr-2 text-gray-400" aria-hidden="true"></i>{{ _("nav.integrations") }}
|
||||
</a>
|
||||
<a href="/notifications"
|
||||
class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
|
||||
{% if request and request.url.path == '/notifications' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-bell mr-2 text-gray-400" aria-hidden="true"></i>Notifications
|
||||
<i class="fas fa-bell mr-2 text-gray-400" aria-hidden="true"></i>{{ _("nav.notifications") }}
|
||||
</a>
|
||||
|
||||
<!-- Admin section in mobile menu – shown only for admin users via JS -->
|
||||
<div id="mobileAdminSection" class="hidden">
|
||||
<div class="border-t border-gray-200 mt-1 pt-1">
|
||||
<p class="px-3 py-1 text-xs font-semibold text-red-600 uppercase tracking-wider flex items-center">
|
||||
<i class="fas fa-shield-alt mr-1" aria-hidden="true"></i> Admin
|
||||
<i class="fas fa-shield-alt mr-1" aria-hidden="true"></i> {{ _("nav.admin") }}
|
||||
</p>
|
||||
<a href="/settings" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-cog mr-2 text-gray-400" aria-hidden="true"></i> Settings
|
||||
<i class="fas fa-cog mr-2 text-gray-400" aria-hidden="true"></i> {{ _("nav.settings") }}
|
||||
</a>
|
||||
<a href="/admin/users" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-users mr-2 text-blue-400" aria-hidden="true"></i> Users
|
||||
<i class="fas fa-users mr-2 text-blue-400" aria-hidden="true"></i> {{ _("nav.users") }}
|
||||
</a>
|
||||
<a href="/admin/plans" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-layer-group mr-2 text-indigo-400" aria-hidden="true"></i> Plan Designer
|
||||
<i class="fas fa-layer-group mr-2 text-indigo-400" aria-hidden="true"></i> {{ _("nav.plan_designer") }}
|
||||
</a>
|
||||
<a href="/admin/credentials" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-key mr-2 text-yellow-400" aria-hidden="true"></i> Credentials
|
||||
<i class="fas fa-key mr-2 text-yellow-400" aria-hidden="true"></i> {{ _("nav.credentials") }}
|
||||
</a>
|
||||
<a href="/admin/files" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-folder-open mr-2 text-gray-400" aria-hidden="true"></i> File Manager
|
||||
<i class="fas fa-folder-open mr-2 text-gray-400" aria-hidden="true"></i> {{ _("nav.file_manager") }}
|
||||
</a>
|
||||
<a href="/duplicates" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-clone mr-2 text-orange-400" aria-hidden="true"></i> Duplicates
|
||||
<i class="fas fa-clone mr-2 text-orange-400" aria-hidden="true"></i> {{ _("nav.duplicates") }}
|
||||
</a>
|
||||
<a href="/similarity" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-sitemap mr-2 text-purple-400" aria-hidden="true"></i> Similarity
|
||||
<i class="fas fa-sitemap mr-2 text-purple-400" aria-hidden="true"></i> {{ _("nav.similarity") }}
|
||||
</a>
|
||||
<a href="/admin/queue" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-stream mr-2 text-blue-400" aria-hidden="true"></i> Queue Monitor
|
||||
<i class="fas fa-stream mr-2 text-blue-400" aria-hidden="true"></i> {{ _("nav.queue_monitor") }}
|
||||
</a>
|
||||
<a href="/admin/scheduled-jobs" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-clock mr-2 text-indigo-500" aria-hidden="true"></i> Scheduled Jobs
|
||||
<i class="fas fa-clock mr-2 text-indigo-500" aria-hidden="true"></i> {{ _("nav.scheduled_jobs") }}
|
||||
</a>
|
||||
<a href="/admin/backup" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-database mr-2 text-green-500" aria-hidden="true"></i> Backup & Restore
|
||||
<i class="fas fa-database mr-2 text-green-500" aria-hidden="true"></i> {{ _("nav.backup_restore") }}
|
||||
</a>
|
||||
<a href="/status" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
|
||||
{% if request and request.url.path == '/status' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-circle-dot mr-2 text-gray-400" aria-hidden="true"></i> Status
|
||||
<i class="fas fa-circle-dot mr-2 text-gray-400" aria-hidden="true"></i> {{ _("nav.status") }}
|
||||
</a>
|
||||
<a href="/admin/api-docs" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-code mr-2 text-teal-400" aria-hidden="true"></i> API Docs
|
||||
<i class="fas fa-code mr-2 text-teal-400" aria-hidden="true"></i> {{ _("nav.api_docs") }}
|
||||
</a>
|
||||
<a href="/developer-docs/" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-book mr-2 text-teal-400" aria-hidden="true"></i> Developer Docs
|
||||
<i class="fas fa-book mr-2 text-teal-400" aria-hidden="true"></i> {{ _("nav.developer_docs") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -362,9 +410,9 @@
|
||||
<!-- Help – always visible, for every visitor regardless of auth state -->
|
||||
<a href="/help"
|
||||
class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
|
||||
aria-label="Help Center"
|
||||
aria-label="{{ _('nav.help_center') }}"
|
||||
{% if request and request.url.path == '/help' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-circle-question mr-2 text-gray-400" aria-hidden="true"></i>Help
|
||||
<i class="fas fa-circle-question mr-2 text-gray-400" aria-hidden="true"></i>{{ _("nav.help") }}
|
||||
</a>
|
||||
|
||||
<!-- Dark mode toggle (mobile) -->
|
||||
@@ -373,10 +421,10 @@
|
||||
type="button"
|
||||
class="flex items-center w-full px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
|
||||
id="darkModeToggleMobile"
|
||||
aria-label="Toggle dark mode"
|
||||
aria-label="{{ _('nav.toggle_dark_mode') }}"
|
||||
>
|
||||
<i class="fas fa-moon mr-2" id="darkModeIconMobile" aria-hidden="true"></i>
|
||||
<span id="darkModeTextMobile">Dark Mode</span>
|
||||
<span id="darkModeTextMobile">{{ _("nav.dark_mode") }}</span>
|
||||
</button>
|
||||
|
||||
<!-- Mobile Auth / Account section (populated by common.js) -->
|
||||
@@ -397,14 +445,14 @@
|
||||
<!-- Footer -->
|
||||
<footer class="bg-white shadow" role="contentinfo">
|
||||
<div class="max-w-7xl mx-auto px-4 py-4 text-center text-gray-600">
|
||||
DocuElevate 2025 -
|
||||
<nav aria-label="Footer navigation" class="inline">
|
||||
<a href="/privacy" class="text-blue-500 hover:underline">Privacy</a> -
|
||||
<a href="/imprint" class="text-blue-500 hover:underline">Imprint</a> -
|
||||
<a href="/terms" class="text-blue-500 hover:underline">Terms</a> -
|
||||
<a href="/cookies" class="text-blue-500 hover:underline">Cookies</a> -
|
||||
<a href="/license" class="text-blue-500 hover:underline">License</a> -
|
||||
<a href="/attribution" class="text-blue-500 hover:underline">Attributions</a>
|
||||
{{ _("footer.copyright", year="2025") }} -
|
||||
<nav aria-label="{{ _('footer.navigation') }}" class="inline">
|
||||
<a href="/privacy" class="text-blue-500 hover:underline">{{ _("footer.privacy") }}</a> -
|
||||
<a href="/imprint" class="text-blue-500 hover:underline">{{ _("footer.imprint") }}</a> -
|
||||
<a href="/terms" class="text-blue-500 hover:underline">{{ _("footer.terms") }}</a> -
|
||||
<a href="/cookies" class="text-blue-500 hover:underline">{{ _("footer.cookies") }}</a> -
|
||||
<a href="/license" class="text-blue-500 hover:underline">{{ _("footer.license") }}</a> -
|
||||
<a href="/attribution" class="text-blue-500 hover:underline">{{ _("footer.attributions") }}</a>
|
||||
</nav> -
|
||||
<span class="text-xs">Version {{ app_version|default(version, true) }}{% if release_name %} "{{ release_name }}"{% endif %}</span>
|
||||
</div>
|
||||
@@ -419,19 +467,18 @@
|
||||
style="display:none!important"
|
||||
aria-live="polite"
|
||||
role="region"
|
||||
aria-label="Cookie notice">
|
||||
aria-label="{{ _('cookie.notice_label') }}">
|
||||
<p class="text-center sm:text-left">
|
||||
DocuElevate uses only essential session cookies required for authentication and service operation.
|
||||
No tracking or analytics cookies are used.
|
||||
<a href="/cookies" class="underline hover:text-blue-300 ml-1">Cookie Policy</a> ·
|
||||
<a href="/privacy" class="underline hover:text-blue-300 ml-1">Privacy Notice</a>
|
||||
{{ _("cookie.notice") }}
|
||||
<a href="/cookies" class="underline hover:text-blue-300 ml-1">{{ _("cookie.policy_link") }}</a> ·
|
||||
<a href="/privacy" class="underline hover:text-blue-300 ml-1">{{ _("cookie.privacy_link") }}</a>
|
||||
</p>
|
||||
<button
|
||||
id="cookieNoticeAccept"
|
||||
type="button"
|
||||
class="flex-shrink-0 bg-blue-600 hover:bg-blue-700 text-white font-semibold px-4 py-1.5 rounded focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||
aria-label="Acknowledge cookie notice">
|
||||
Got it
|
||||
aria-label="{{ _('cookie.accept') }}">
|
||||
{{ _("cookie.accept") }}
|
||||
</button>
|
||||
</div>
|
||||
<script>
|
||||
@@ -454,6 +501,26 @@
|
||||
<!-- Common JS (shared) -->
|
||||
<script src="/static/js/common.js"></script>
|
||||
|
||||
<!-- Language selector -->
|
||||
<script>
|
||||
function setLanguage(langCode) {
|
||||
var csrfToken = document.querySelector('meta[name="csrf-token"]');
|
||||
var headers = { 'Content-Type': 'application/json' };
|
||||
if (csrfToken && csrfToken.content) {
|
||||
headers['X-CSRF-Token'] = csrfToken.content;
|
||||
}
|
||||
fetch('/api/i18n/language', {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify({ language: langCode })
|
||||
}).then(function() {
|
||||
window.location.reload();
|
||||
}).catch(function() {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Notification badge updater -->
|
||||
<script>
|
||||
(function() {
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
{
|
||||
"nav.dashboard": "Übersicht",
|
||||
"nav.upload": "Hochladen",
|
||||
"nav.files": "Dateien",
|
||||
"nav.search": "Suche",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.help": "Hilfe",
|
||||
"nav.settings": "Einstellungen",
|
||||
"nav.login": "Anmelden",
|
||||
"nav.logout": "Abmelden",
|
||||
"nav.signup": "Registrieren",
|
||||
"nav.profile": "Profil",
|
||||
"nav.admin": "Administration",
|
||||
"nav.admin.users": "Benutzer",
|
||||
"nav.admin.plans": "Tarife",
|
||||
"nav.admin.scheduled_jobs": "Geplante Aufgaben",
|
||||
"nav.admin.backups": "Sicherungen",
|
||||
"nav.admin.audit_logs": "Prüfprotokolle",
|
||||
"nav.queue": "Warteschlange",
|
||||
"nav.integrations": "Integrationen",
|
||||
"nav.status": "Systemstatus",
|
||||
"nav.notifications": "Benachrichtigungen",
|
||||
"nav.shared_links": "Geteilte Links",
|
||||
"nav.duplicates": "Duplikate",
|
||||
"nav.api_tokens": "API-Token",
|
||||
"nav.subscription": "Abonnement",
|
||||
"nav.imap": "E-Mail-Import",
|
||||
"nav.version": "Versionsinformationen",
|
||||
"footer.copyright": "© {year} DocuElevate",
|
||||
"footer.about": "Über uns",
|
||||
"footer.privacy": "Datenschutz",
|
||||
"footer.terms": "Nutzungsbedingungen",
|
||||
"footer.cookies": "Cookie-Richtlinie",
|
||||
"footer.imprint": "Impressum",
|
||||
"footer.attribution": "Namensnennung",
|
||||
"footer.license": "Lizenz",
|
||||
"footer.version": "Version",
|
||||
"common.loading": "Laden...",
|
||||
"common.save": "Speichern",
|
||||
"common.cancel": "Abbrechen",
|
||||
"common.delete": "Löschen",
|
||||
"common.edit": "Bearbeiten",
|
||||
"common.create": "Erstellen",
|
||||
"common.confirm": "Bestätigen",
|
||||
"common.close": "Schließen",
|
||||
"common.back": "Zurück",
|
||||
"common.next": "Weiter",
|
||||
"common.previous": "Zurück",
|
||||
"common.search": "Suche",
|
||||
"common.filter": "Filtern",
|
||||
"common.reset": "Zurücksetzen",
|
||||
"common.submit": "Absenden",
|
||||
"common.download": "Herunterladen",
|
||||
"common.upload": "Hochladen",
|
||||
"common.actions": "Aktionen",
|
||||
"common.status": "Status",
|
||||
"common.name": "Name",
|
||||
"common.description": "Beschreibung",
|
||||
"common.type": "Typ",
|
||||
"common.date": "Datum",
|
||||
"common.size": "Größe",
|
||||
"common.enabled": "Aktiviert",
|
||||
"common.disabled": "Deaktiviert",
|
||||
"common.yes": "Ja",
|
||||
"common.no": "Nein",
|
||||
"common.none": "Keine",
|
||||
"common.error": "Fehler",
|
||||
"common.success": "Erfolg",
|
||||
"common.warning": "Warnung",
|
||||
"common.info": "Info",
|
||||
"common.tags": "Tags",
|
||||
"common.pending": "Ausstehend",
|
||||
"common.processing": "Verarbeitung",
|
||||
"common.completed": "Abgeschlossen",
|
||||
"common.failed": "Fehlgeschlagen",
|
||||
"common.duplicate": "Duplikat",
|
||||
"common.active": "Aktiv",
|
||||
"cookie.message": "Diese Website verwendet Cookies, um Ihr Erlebnis zu verbessern.",
|
||||
"cookie.accept": "Akzeptieren",
|
||||
"cookie.learn_more": "Mehr erfahren",
|
||||
"language.selector_label": "Sprache wählen",
|
||||
"language.change_success": "Sprache geändert zu {language}",
|
||||
"upload.page_title": "Dateien hochladen",
|
||||
"upload.section_device": "Vom Gerät hochladen",
|
||||
"upload.drop_hint_desktop": "Dateien oder Ordner hierher ziehen oder klicken, um Dateien auszuwählen.",
|
||||
"upload.drop_hint_mobile": "Tippen Sie, um Dateien auszuwählen, oder verwenden Sie die Kamera-Schaltfläche unten.",
|
||||
"upload.browse_button": "Dateien durchsuchen",
|
||||
"upload.file_types": "Erlaubte Typen: PDF, Office-Dokumente (Word, Excel, PowerPoint usw.), Bilder",
|
||||
"upload.file_size_hint": "Maximale Größe: 500 MB pro Datei",
|
||||
"upload.camera_button": "Foto aufnehmen / Dokument scannen",
|
||||
"upload.section_url": "Von URL hochladen",
|
||||
"upload.url_label": "Datei-URL",
|
||||
"upload.url_placeholder": "https://beispiel.de/dokument.pdf",
|
||||
"upload.url_description": "Geben Sie einen direkten Link zu einer Datei ein (PDF, Office-Dokumente oder Bilder)",
|
||||
"upload.filename_label": "Dateiname (optional)",
|
||||
"upload.filename_placeholder": "mein-dokument.pdf",
|
||||
"upload.filename_description": "Leer lassen, um den Dateinamen aus der URL zu verwenden",
|
||||
"upload.download_button": "Herunterladen und verarbeiten",
|
||||
"upload.error_url_required": "Bitte geben Sie eine URL ein",
|
||||
"upload.error_invalid_url": "Ungültiges URL-Format",
|
||||
"upload.downloading": "Datei wird von URL heruntergeladen...",
|
||||
"upload.button_processing": "Verarbeitung...",
|
||||
"files.page_title": "Dateiübersicht",
|
||||
"files.drop_overlay_title": "Dateien oder Ordner zum Hochladen hier ablegen",
|
||||
"files.drop_overlay_hint": "Unterstützt PDF, Office-Dokumente, Bilder, HTML, Markdown und mehr",
|
||||
"files.upload_modal_header": "Dateien hochladen",
|
||||
"files.queue_banner_link": "Warteschlange ansehen",
|
||||
"files.filter_search_placeholder": "Dateinamen eingeben...",
|
||||
"files.filter_mime_type": "MIME-Typ",
|
||||
"files.filter_all_types": "Alle Typen",
|
||||
"files.filter_all_statuses": "Alle Status",
|
||||
"files.filter_date_from": "Datum von",
|
||||
"files.filter_date_to": "Datum bis",
|
||||
"files.filter_storage_provider": "Speicheranbieter",
|
||||
"files.filter_all_providers": "Alle Anbieter",
|
||||
"files.filter_tags_placeholder": "z.B. Rechnung,Amazon",
|
||||
"files.filter_ocr_quality": "OCR-Qualität",
|
||||
"files.filter_ocr_all": "Alle Dateien",
|
||||
"files.filter_ocr_poor": "Schlechte Qualität",
|
||||
"files.filter_ocr_good": "Gute Qualität",
|
||||
"files.filter_ocr_unchecked": "Noch nicht bewertet",
|
||||
"files.filter_apply": "Filter anwenden",
|
||||
"files.filter_clear": "Zurücksetzen",
|
||||
"files.saved_searches_label": "Gespeicherte Suchen",
|
||||
"files.saved_searches_empty": "Noch keine gespeicherten Suchen",
|
||||
"files.saved_searches_save": "Aktuelle speichern",
|
||||
"files.saved_searches_error": "Gespeicherte Suchen konnten nicht geladen werden",
|
||||
"files.fulltext_search_label": "Volltextsuche",
|
||||
"files.fulltext_search_placeholder": "Dokumentinhalt, Absender, Tags, Typ durchsuchen...",
|
||||
"files.search_results_title": "Suchergebnisse",
|
||||
"files.search_results_empty": "Keine Ergebnisse gefunden.",
|
||||
"files.bulk_reprocess": "Ausgewählte erneut verarbeiten",
|
||||
"files.bulk_cloud_ocr": "Cloud-OCR erneut ausführen",
|
||||
"files.bulk_download": "Als ZIP herunterladen",
|
||||
"files.bulk_delete": "Ausgewählte löschen",
|
||||
"files.bulk_clear_selection": "Auswahl aufheben",
|
||||
"files.table_select_all": "Alle Dateien auf dieser Seite auswählen",
|
||||
"files.table_id": "ID",
|
||||
"files.table_original_filename": "Originaler Dateiname",
|
||||
"files.table_mime_type": "MIME-Typ",
|
||||
"files.table_created_at": "Erstellt am",
|
||||
"files.table_actions": "Aktionen",
|
||||
"files.table_empty": "Keine Dateien gefunden",
|
||||
"files.action_preview": "Schnellvorschau",
|
||||
"files.action_details": "Details anzeigen",
|
||||
"files.action_delete": "Datei löschen",
|
||||
"files.pagination_first": "Erste",
|
||||
"files.pagination_previous": "Vorherige",
|
||||
"files.pagination_next": "Nächste",
|
||||
"files.pagination_last": "Letzte",
|
||||
"files.delete_modal_title": "Löschung bestätigen",
|
||||
"files.delete_modal_message": "Sind Sie sicher, dass Sie diese Datei löschen möchten?",
|
||||
"files.delete_modal_cancel": "Abbrechen",
|
||||
"files.delete_modal_confirm": "Löschen",
|
||||
"files.preview_modal_title": "Vorschau",
|
||||
"files.preview_modal_close": "Vorschau schließen",
|
||||
"search.page_title": "Dokumente suchen",
|
||||
"search.heading": "Dokumentensuche",
|
||||
"search.input_placeholder": "Dokumente nach Inhalt, Absender, Tags, Typ suchen...",
|
||||
"search.button": "Suchen",
|
||||
"search.filter_document_type": "Dokumenttyp",
|
||||
"search.filter_document_type_placeholder": "z.B. Rechnung",
|
||||
"search.filter_tags_placeholder": "z.B. Amazon",
|
||||
"search.filter_sender": "Absender",
|
||||
"search.filter_sender_placeholder": "z.B. ACME GmbH",
|
||||
"search.filter_language": "Sprache",
|
||||
"search.filter_language_placeholder": "z.B. de",
|
||||
"search.filter_text_quality": "Textqualität",
|
||||
"search.filter_text_quality_all": "Alle",
|
||||
"search.filter_text_quality_high": "Hoch",
|
||||
"search.filter_text_quality_medium": "Mittel",
|
||||
"search.filter_text_quality_low": "Niedrig",
|
||||
"search.filter_text_quality_no_text": "Kein Text",
|
||||
"search.filter_date_from": "Datum von",
|
||||
"search.filter_date_to": "Datum bis",
|
||||
"search.filter_clear_button": "Filter zurücksetzen",
|
||||
"search.saved_label": "Gespeicherte Suchen",
|
||||
"search.saved_loading": "Laden...",
|
||||
"search.saved_empty": "Noch keine gespeicherten Suchen",
|
||||
"search.saved_error": "Gespeicherte Suchen konnten nicht geladen werden",
|
||||
"search.saved_button": "Aktuelle speichern",
|
||||
"search.result_empty": "Keine Dokumente gefunden, die Ihrer Suche entsprechen.",
|
||||
"search.loading_indicator": "Suche läuft…",
|
||||
"search.error_message": "Suche ist vorübergehend nicht verfügbar. Bitte versuchen Sie es gleich erneut.",
|
||||
"help.page_title": "Hilfezentrum",
|
||||
"help.heading": "Hilfezentrum",
|
||||
"help.subheading": "Alles, was Sie brauchen, um DocuElevate optimal zu nutzen. Durchsuchen Sie die Themen unten oder suchen Sie nach dem, was Sie brauchen.",
|
||||
"help.quickstart_heading": "Schnellstart",
|
||||
"help.quickstart_upload": "Dokumente hochladen",
|
||||
"help.quickstart_upload_desc": "Ziehen Sie Dateien auf die Upload-Seite oder klicken Sie auf Datei auswählen. DocuElevate konvertiert Bilder und Office-Dokumente in PDF, führt OCR durch und extrahiert Metadaten automatisch.",
|
||||
"help.quickstart_storage": "Speicher verbinden",
|
||||
"help.quickstart_storage_desc": "Gehen Sie zu Einstellungen und verknüpfen Sie Ihre Cloud-Konten. Verarbeitete Dokumente werden automatisch an jedes konfigurierte Ziel weitergeleitet.",
|
||||
"help.quickstart_workflows": "Arbeitsabläufe automatisieren",
|
||||
"help.quickstart_workflows_desc": "Erstellen Sie Pipelines, um mehrstufige Verarbeitungs- und Weiterleitungsregeln zu definieren. Kombinieren Sie OCR, KI-Extraktion, Formatkonvertierung und Zustellung in einem einzigen Ablauf.",
|
||||
"help.sources_heading": "Quellen – Dokumente einbringen",
|
||||
"help.sources_web_upload": "Web-Upload",
|
||||
"help.sources_web_upload_desc": "Der schnellste Weg, um loszulegen. Öffnen Sie die Upload-Seite, legen Sie eine oder mehrere Dateien ab, und DocuElevate kümmert sich um den Rest. Unterstützte Formate sind PDF, JPEG, PNG, TIFF, DOCX, XLSX und mehr.",
|
||||
"help.sources_email_ingestion": "E-Mail-Import (IMAP)",
|
||||
"help.sources_email_ingestion_desc": "Leiten Sie Dokumente an ein dediziertes Postfach weiter. Unter E-Mail-Import fügen Sie ein oder mehrere IMAP-Konten hinzu. DocuElevate prüft auf neue Nachrichten und verarbeitet Anhänge automatisch.",
|
||||
"help.sources_rest_api": "REST-API",
|
||||
"help.sources_rest_api_desc": "Integrieren Sie programmgesteuert, indem Sie Dateien an /api/upload senden. Ideal für Skripte, überwachte Ordner, Scanner oder Drittanbieter-Tools wie Zapier und n8n.",
|
||||
"help.sources_scanner": "Scanner & Mobil",
|
||||
"help.sources_scanner_desc": "Richten Sie Netzwerkscanner auf den Upload-Endpunkt von DocuElevate oder verwenden Sie eine mobile Scan-App, die benutzerdefinierte HTTP-Ziele unterstützt.",
|
||||
"help.destinations_heading": "Ziele – Wohin die Dokumente gehen",
|
||||
"help.destinations_dropbox": "Dropbox",
|
||||
"help.destinations_dropbox_desc": "OAuth-verknüpft. Dateien landen in Ihrem gewählten Ordner.",
|
||||
"help.destinations_google_drive": "Google Drive",
|
||||
"help.destinations_google_drive_desc": "Dienstkonto oder OAuth. Unterstützt geteilte Laufwerke.",
|
||||
"help.destinations_onedrive": "OneDrive",
|
||||
"help.destinations_onedrive_desc": "Microsoft Graph API-Integration.",
|
||||
"help.destinations_s3": "Amazon S3",
|
||||
"help.destinations_s3_desc": "Jeder S3-kompatible Bucket (AWS, MinIO, Wasabi).",
|
||||
"help.destinations_nextcloud": "Nextcloud / WebDAV",
|
||||
"help.destinations_nextcloud_desc": "Selbstgehosteter Cloud-Speicher über WebDAV.",
|
||||
"help.destinations_paperless": "Paperless-ngx",
|
||||
"help.destinations_paperless_desc": "Dokumente direkt in Paperless zur Archivierung übertragen.",
|
||||
"help.destinations_sftp": "SFTP / FTP",
|
||||
"help.destinations_sftp_desc": "Sichere Dateiübertragung auf beliebige Server.",
|
||||
"help.destinations_email": "E-Mail-Weiterleitung",
|
||||
"help.destinations_email_desc": "Verarbeitete Dateien als SMTP-Anhänge gesendet.",
|
||||
"help.destinations_webhook": "Webhook",
|
||||
"help.destinations_webhook_desc": "Metadaten per POST an einen externen Endpunkt senden.",
|
||||
"help.workflows_heading": "Arbeitsabläufe & Pipelines",
|
||||
"help.workflows_what_is": "Was ist eine Pipeline?",
|
||||
"help.workflows_definition": "Eine Pipeline ist eine Reihe von Verarbeitungsschritten, die automatisch ausgeführt werden, wenn ein Dokument aufgenommen wird. Jeder Schritt kann das Dokument transformieren, anreichern oder weiterleiten.",
|
||||
"help.workflows_typical_steps": "Typische Schritte",
|
||||
"help.workflows_step_1": "In PDF konvertieren",
|
||||
"help.workflows_step_2": "OCR – Text extrahieren",
|
||||
"help.workflows_step_3": "KI-Metadatenextraktion",
|
||||
"help.workflows_step_4": "An ein oder mehrere Ziele liefern",
|
||||
"help.workflows_creating": "Eine Pipeline erstellen",
|
||||
"help.workflows_step_1_create": "Gehen Sie im Hauptmenü zu Pipelines.",
|
||||
"help.workflows_step_2_create": "Klicken Sie auf Neue Pipeline und geben Sie ihr einen Namen.",
|
||||
"help.workflows_step_3_create": "Fügen Sie die benötigten Verarbeitungsschritte hinzu.",
|
||||
"help.workflows_step_4_create": "Wählen Sie ein oder mehrere Zustellungsziele.",
|
||||
"help.workflows_step_5_create": "Speichern – neue Dokumente werden automatisch durch diese Pipeline verarbeitet.",
|
||||
"help.faq_heading": "Häufig gestellte Fragen",
|
||||
"help.faq_1_q": "Wie lade ich Dokumente hoch?",
|
||||
"help.faq_1_a": "Navigieren Sie zur Upload-Seite, ziehen Sie Ihre Dateien per Drag-and-Drop oder klicken Sie auf Datei auswählen. DocuElevate konvertiert Bilder und Office-Dokumente in PDF, führt OCR durch und extrahiert Metadaten automatisch.",
|
||||
"help.faq_2_q": "Welche Dateiformate werden unterstützt?",
|
||||
"help.faq_2_a": "PDF, JPEG, PNG, TIFF, BMP, GIF, DOCX, XLSX, PPTX, ODT, ODS, TXT, RTF und HTML. Nicht-PDF-Dateien werden vor der Verarbeitung automatisch in PDF konvertiert.",
|
||||
"help.faq_3_q": "Kann ich Dokumente per E-Mail importieren?",
|
||||
"help.faq_3_a": "Ja. Gehen Sie zu E-Mail-Import, fügen Sie ein IMAP-Konto hinzu, und DocuElevate prüft auf neue Nachrichten und verarbeitet Anhänge automatisch.",
|
||||
"help.faq_4_q": "Wie funktionieren Verarbeitungs-Pipelines?",
|
||||
"help.faq_4_a": "Pipelines ermöglichen es Ihnen, Verarbeitungsschritte zu verketten – OCR, KI-Extraktion, Formatkonvertierung – und das Ergebnis an ein oder mehrere Ziele weiterzuleiten. Erstellen und verwalten Sie diese auf der Pipelines-Seite.",
|
||||
"help.faq_5_q": "Sind meine Daten sicher?",
|
||||
"help.faq_5_a": "DocuElevate verschlüsselt Anmeldedaten im Ruhezustand, kommuniziert über TLS und speichert Ihre Dokumente nie länger als nötig. Weitere Details finden Sie in der Datenschutzerklärung.",
|
||||
"help.support_heading": "Support kontaktieren",
|
||||
"help.support_description": "Können Sie nicht finden, was Sie suchen? Unser Support-Team hilft Ihnen gerne weiter.",
|
||||
"help.support_admin_message": "Wenden Sie sich an Ihren Administrator für Support-Informationen.",
|
||||
"index.page_title_public": "Intelligente Dokumentenverarbeitung",
|
||||
"index.page_title_dashboard": "Übersicht",
|
||||
"index.badge_intelligent": "Intelligente Dokumentenverarbeitung",
|
||||
"index.hero_heading": "Vom Hochladen zur Erkenntnis – automatisch.",
|
||||
"index.hero_description": "DocuElevate nimmt Ihre Dokumente auf, führt OCR durch, extrahiert Metadaten mit KI und leitet Dateien an Dropbox, Google Drive, OneDrive, S3, Nextcloud und mehr weiter – alles in einer nahtlosen Pipeline.",
|
||||
"index.hero_signup": "Kostenlos starten",
|
||||
"index.hero_login": "Anmelden",
|
||||
"index.hero_pricing": "Tarife & Preise ansehen",
|
||||
"index.feature_section_title": "Alles, was Sie für intelligente Dokumenten-Workflows brauchen",
|
||||
"index.feature_ocr": "OCR & Texterkennung",
|
||||
"index.feature_ocr_desc": "Azure Document Intelligence konvertiert gescannte PDFs und Bilder automatisch in vollständig durchsuchbaren Text.",
|
||||
"index.feature_ai": "KI-Metadatenextraktion",
|
||||
"index.feature_ai_desc": "OpenAI, Claude, Gemini und andere KI-Anbieter klassifizieren Dokumente und extrahieren wichtige Felder wie Daten, Beträge und Betreffzeilen.",
|
||||
"index.feature_cloud": "Multi-Cloud-Speicher",
|
||||
"index.feature_cloud_desc": "Leiten Sie verarbeitete Dateien an Dropbox, Google Drive, OneDrive, Amazon S3, Nextcloud, Paperless NGX, WebDAV, FTP/SFTP und mehr weiter.",
|
||||
"index.feature_email": "E-Mail- & IMAP-Import",
|
||||
"index.feature_email_desc": "Ziehen Sie Dokumente automatisch aus Gmail oder jedem IMAP-Postfach – keine manuellen Uploads nötig.",
|
||||
"index.feature_search": "Volltextsuche",
|
||||
"index.feature_search_desc": "Finden Sie sofort jedes Dokument nach Inhalt, Metadaten oder Tags in Ihrem gesamten Archiv.",
|
||||
"index.feature_pipelines": "Benutzerdefinierte Pipelines",
|
||||
"index.feature_pipelines_desc": "Erstellen Sie Verarbeitungs-Pipelines mit konfigurierbaren Schritten – OCR, KI-Extraktion, Formatkonvertierung und Speicher-Routing in beliebiger Reihenfolge.",
|
||||
"index.cta_heading": "Bereit, Ihren Dokumenten-Workflow zu verbessern?",
|
||||
"index.cta_description": "Schließen Sie sich Teams an, die ihre Dokumentenverarbeitung bereits mit DocuElevate automatisieren.",
|
||||
"index.cta_signup": "Kostenloses Konto erstellen",
|
||||
"index.cta_pricing": "Preise ansehen",
|
||||
"index.dashboard_subtitle": "Intelligente Dokumentenverarbeitung & -verwaltung",
|
||||
"index.platform_overview": "Plattformübersicht",
|
||||
"index.stat_total_files": "Dateien gesamt",
|
||||
"index.stat_files_today": "Dateien heute",
|
||||
"index.stat_files_month": "Dateien diesen Monat",
|
||||
"index.stat_active_users": "Aktive Benutzer",
|
||||
"index.usage_my_usage": "Meine Nutzung",
|
||||
"index.usage_lifetime": "Dateien gesamt",
|
||||
"index.usage_today": "Dateien heute",
|
||||
"index.usage_month": "Dateien diesen Monat",
|
||||
"index.usage_unlimited": "Unbegrenzt",
|
||||
"index.tier_plan": "Tarif",
|
||||
"index.tier_upgrade": "Upgrade",
|
||||
"index.tier_view_details": "Alle Details ansehen",
|
||||
"index.quick_actions": "Schnellaktionen",
|
||||
"index.quick_upload": "Dokument hochladen",
|
||||
"index.quick_upload_desc": "Eine neue Datei verarbeiten",
|
||||
"index.quick_documents": "Meine Dokumente",
|
||||
"index.quick_documents_desc": "Ihre verarbeiteten Dateien durchsuchen",
|
||||
"index.quick_subscription": "Mein Abonnement",
|
||||
"index.quick_subscription_desc": "Tarif & Nutzungsdetails anzeigen",
|
||||
"index.quick_search": "Suche",
|
||||
"index.quick_search_desc": "Volltextsuche über Dokumente",
|
||||
"index.upgrade_plan": "Tarif upgraden",
|
||||
"index.upgrade_description": "Mehr Dokumente, mehr Ziele und Prioritäts-Support freischalten.",
|
||||
"index.upgrade_daily_limits": "Höhere tägliche & monatliche Limits",
|
||||
"index.upgrade_destinations": "Mehr Speicherziele",
|
||||
"index.upgrade_ocr_pages": "Mehr OCR-Seiten",
|
||||
"index.upgrade_view_pricing": "Tarife & Preise ansehen",
|
||||
"index.integrations_title": "Integrationen",
|
||||
"index.integrations_active": "Aktive Integrationen",
|
||||
"index.integrations_storage": "Speicherziele",
|
||||
"index.integrations_view_status": "Systemstatus anzeigen",
|
||||
"index.single_user_heading": "DocuElevate Dashboard",
|
||||
"index.single_user_subtitle": "Intelligente Dokumentenverarbeitung & -verwaltung",
|
||||
"index.capabilities_title": "Funktionen",
|
||||
"index.capabilities_ocr": "OCR & Metadatenextraktion mit KI",
|
||||
"index.capabilities_cloud": "Cloud-Speicher: Dropbox, OneDrive, Google Drive, NextCloud",
|
||||
"index.capabilities_paperless": "Paperless-ngx-Integration für Dokumentenverwaltung",
|
||||
"index.capabilities_ingestion": "E-Mail- & URL-basierte Dokumentenaufnahme",
|
||||
"index.capabilities_workflows": "Automatisierte Klassifizierung & Routing-Workflows",
|
||||
"index.getting_started": "Erste Schritte",
|
||||
"index.getting_started_1": "Integrationen über Systemstatus konfigurieren",
|
||||
"index.getting_started_2": "Erstes Dokument hochladen",
|
||||
"index.getting_started_3": "Ergebnisse in Dateien überprüfen",
|
||||
"index.getting_started_learn": "Mehr über DocuElevate erfahren",
|
||||
"error.404_code": "404",
|
||||
"error.404_heading": "Ups, diese Seite konnten wir nicht finden!",
|
||||
"error.404_message": "Es scheint, als hätte DocuElevate das gesuchte Dokument verlegt. Keine Sorge – wir helfen Ihnen weiter.",
|
||||
"error.404_home": "Zur Startseite",
|
||||
"error.500_code": "500",
|
||||
"error.500_heading": "Ups! Etwas ist schiefgelaufen.",
|
||||
"error.500_description": "Unsere Server haben ein Problem und brauchen einen Moment.",
|
||||
"error.500_home": "Zur Startseite",
|
||||
"pipelines.page_title": "Verarbeitungs-Pipelines",
|
||||
"pipelines.system_label": "System",
|
||||
"pipelines.default_label": "Standard",
|
||||
"pipelines.inactive_label": "Inaktiv",
|
||||
"pipelines.disabled_label": "Deaktiviert",
|
||||
"pipelines.enabled_label": "Aktiviert",
|
||||
"pipelines.empty_state": "Noch keine Pipelines",
|
||||
"pipelines.set_default": "Als meine Standard-Pipeline festlegen",
|
||||
"pipelines.description_label": "Beschreibung",
|
||||
"pipelines.active_label": "Aktiv",
|
||||
"integrations.page_title": "Integrationen",
|
||||
"integrations.imap_settings": "IMAP-Einstellungen",
|
||||
"integrations.host_label": "Host",
|
||||
"integrations.port_label": "Port",
|
||||
"integrations.username_label": "Benutzername",
|
||||
"integrations.password_label": "Passwort",
|
||||
"integrations.folder_label": "Ordner",
|
||||
"integrations.empty_state": "Keine Integrationen konfiguriert",
|
||||
"status.page_title": "Systemstatus",
|
||||
"status.app_version": "App-Version",
|
||||
"status.build_date": "Build-Datum",
|
||||
"status.last_check": "Letzte Prüfung",
|
||||
"status.container_id": "Container-ID",
|
||||
"status.git_commit": "Git-Commit",
|
||||
"status.setting_label": "Einstellung",
|
||||
"status.value_label": "Wert",
|
||||
"notifications.page_title": "Benachrichtigungen",
|
||||
"notifications.manage_desc": "Verwalten Sie Ihren Posteingang, Ziele und Ereignispräferenzen",
|
||||
"notifications.tab_inbox": "Posteingang",
|
||||
"notifications.tab_settings": "Einstellungen",
|
||||
"notifications.filter_all": "Alle",
|
||||
"notifications.filter_unread": "Nur ungelesene",
|
||||
"notifications.filter_read": "Nur gelesene",
|
||||
"notifications.mark_all_read_btn": "Alle als gelesen markieren",
|
||||
"auth.login_title": "Anmelden",
|
||||
"auth.signup_title": "Registrieren",
|
||||
"auth.forgot_password": "Passwort vergessen?",
|
||||
"auth.remember_me": "Angemeldet bleiben",
|
||||
"auth.email_label": "E-Mail",
|
||||
"auth.password_label": "Passwort",
|
||||
"auth.confirm_password": "Passwort bestätigen",
|
||||
"auth.username_label": "Benutzername",
|
||||
"auth.display_name_label": "Anzeigename",
|
||||
"language.nb": "Norsk",
|
||||
"language.da": "Dansk",
|
||||
"language.sv": "Svenska",
|
||||
"language.fi": "Suomi",
|
||||
"language.is": "Íslenska",
|
||||
"language.ga": "Gaeilge",
|
||||
"language.hu": "Magyar",
|
||||
"language.cs": "Čeština",
|
||||
"language.sk": "Slovenčina",
|
||||
"language.sl": "Slovenščina",
|
||||
"language.hr": "Hrvatski",
|
||||
"language.ro": "Română",
|
||||
"language.bg": "Български",
|
||||
"language.uk": "Українська",
|
||||
"language.tr": "Türkçe",
|
||||
"language.el": "Ελληνικά",
|
||||
"language.et": "Eesti",
|
||||
"language.lv": "Latviešu",
|
||||
"language.lt": "Lietuvių",
|
||||
"language.lb": "Lëtzebuergesch",
|
||||
"language.ca": "Català"
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
{
|
||||
"app.name": "DocuElevate",
|
||||
"nav.dashboard": "Dashboard",
|
||||
"nav.upload": "Upload",
|
||||
"nav.files": "Files",
|
||||
"nav.search": "Search",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.integrations": "Integrations",
|
||||
"nav.help": "Help",
|
||||
"nav.notifications": "Notifications",
|
||||
"nav.pricing": "Pricing",
|
||||
"nav.about": "About",
|
||||
"nav.admin": "Admin",
|
||||
"nav.settings": "Settings",
|
||||
"nav.users": "Users",
|
||||
"nav.plan_designer": "Plan Designer",
|
||||
"nav.credentials": "Credentials",
|
||||
"nav.file_manager": "File Manager",
|
||||
"nav.duplicates": "Duplicates",
|
||||
"nav.similarity": "Similarity",
|
||||
"nav.queue_monitor": "Queue Monitor",
|
||||
"nav.scheduled_jobs": "Scheduled Jobs",
|
||||
"nav.backup_restore": "Backup & Restore",
|
||||
"nav.status": "Status",
|
||||
"nav.api_docs": "API Docs",
|
||||
"nav.developer_docs": "Developer Docs",
|
||||
"nav.dark_mode": "Dark Mode",
|
||||
"nav.light_mode": "Light Mode",
|
||||
"nav.toggle_dark_mode": "Toggle dark mode",
|
||||
"nav.toggle_nav": "Toggle navigation menu",
|
||||
"nav.open_main_menu": "Open main menu",
|
||||
"nav.skip_to_content": "Skip to main content",
|
||||
"nav.main_navigation": "Main navigation",
|
||||
"nav.admin_menu": "Admin menu",
|
||||
"nav.admin_actions": "Admin actions",
|
||||
"nav.help_center": "Help Center",
|
||||
"auth.login": "Log In",
|
||||
"auth.logout": "Log Out",
|
||||
"auth.signup": "Sign Up",
|
||||
"auth.my_account": "My Account",
|
||||
"auth.profile": "Profile",
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.privacy": "Privacy",
|
||||
"footer.imprint": "Imprint",
|
||||
"footer.terms": "Terms",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.license": "License",
|
||||
"footer.attributions": "Attributions",
|
||||
"footer.version": "Version {version}",
|
||||
"footer.navigation": "Footer navigation",
|
||||
"cookie.notice": "DocuElevate uses only essential session cookies required for authentication and service operation. No tracking or analytics cookies are used.",
|
||||
"cookie.policy_link": "Cookie Policy",
|
||||
"cookie.privacy_link": "Privacy Notice",
|
||||
"cookie.accept": "Got it",
|
||||
"cookie.notice_label": "Cookie notice",
|
||||
"common.save": "Save",
|
||||
"common.cancel": "Cancel",
|
||||
"common.delete": "Delete",
|
||||
"common.edit": "Edit",
|
||||
"common.close": "Close",
|
||||
"common.confirm": "Confirm",
|
||||
"common.back": "Back",
|
||||
"common.next": "Next",
|
||||
"common.loading": "Loading...",
|
||||
"common.error": "Error",
|
||||
"common.success": "Success",
|
||||
"common.warning": "Warning",
|
||||
"common.info": "Info",
|
||||
"common.yes": "Yes",
|
||||
"common.no": "No",
|
||||
"common.search": "Search",
|
||||
"common.filter": "Filter",
|
||||
"common.reset": "Reset",
|
||||
"common.refresh": "Refresh",
|
||||
"common.download": "Download",
|
||||
"common.actions": "Actions",
|
||||
"common.details": "Details",
|
||||
"common.name": "Name",
|
||||
"common.description": "Description",
|
||||
"common.type": "Type",
|
||||
"common.status": "Status",
|
||||
"common.date": "Date",
|
||||
"common.size": "Size",
|
||||
"common.created": "Created",
|
||||
"common.updated": "Updated",
|
||||
"common.enabled": "Enabled",
|
||||
"common.disabled": "Disabled",
|
||||
"common.active": "Active",
|
||||
"common.inactive": "Inactive",
|
||||
"common.all": "All",
|
||||
"common.none": "None",
|
||||
"common.select": "Select",
|
||||
"common.upload": "Upload",
|
||||
"common.processing": "Processing",
|
||||
"common.completed": "Completed",
|
||||
"common.failed": "Failed",
|
||||
"common.pending": "Pending",
|
||||
"common.retry": "Retry",
|
||||
"common.view": "View",
|
||||
"common.copy": "Copy",
|
||||
"common.copied": "Copied!",
|
||||
"language.selector": "Language",
|
||||
"language.en": "English",
|
||||
"language.de": "Deutsch",
|
||||
"language.fr": "Français",
|
||||
"language.es": "Español",
|
||||
"language.it": "Italiano",
|
||||
"language.pt": "Português",
|
||||
"language.nl": "Nederlands",
|
||||
"language.pl": "Polski",
|
||||
"language.zh": "中文",
|
||||
"language.ru": "Русский",
|
||||
"language.changed": "Language changed to {language}",
|
||||
"dashboard.title": "Dashboard",
|
||||
"dashboard.total_files": "Total Files",
|
||||
"dashboard.files_today": "Files Today",
|
||||
"dashboard.files_this_month": "Files This Month",
|
||||
"dashboard.ocr_processed": "OCR Processed",
|
||||
"dashboard.active_integrations": "Active Integrations",
|
||||
"dashboard.storage_targets": "Storage Targets",
|
||||
"dashboard.recent_activity": "Recent Activity",
|
||||
"dashboard.quick_actions": "Quick Actions",
|
||||
"dashboard.welcome": "Welcome to DocuElevate",
|
||||
"upload.title": "Upload Document",
|
||||
"upload.drag_drop": "Drag & drop files here or click to browse",
|
||||
"upload.select_file": "Select File",
|
||||
"upload.uploading": "Uploading...",
|
||||
"upload.success": "File uploaded successfully",
|
||||
"upload.error": "Upload failed",
|
||||
"upload.max_size": "Maximum file size: {size}",
|
||||
"files.title": "Files",
|
||||
"files.no_files": "No files found",
|
||||
"files.filename": "Filename",
|
||||
"files.document_title": "Document Title",
|
||||
"files.uploaded": "Uploaded",
|
||||
"files.file_size": "File Size",
|
||||
"files.ocr_status": "OCR Status",
|
||||
"files.tags": "Tags",
|
||||
"search.title": "Search Documents",
|
||||
"search.placeholder": "Search by filename, content, tags...",
|
||||
"search.no_results": "No results found",
|
||||
"search.results_count": "{count} results found",
|
||||
"settings.title": "Settings",
|
||||
"settings.save_success": "Setting saved successfully",
|
||||
"settings.save_error": "Failed to save setting",
|
||||
"settings.reset_confirm": "Are you sure you want to reset this setting?",
|
||||
"integrations.title": "Integrations",
|
||||
"integrations.connect": "Connect",
|
||||
"integrations.disconnect": "Disconnect",
|
||||
"integrations.connected": "Connected",
|
||||
"integrations.not_connected": "Not Connected",
|
||||
"integrations.configure": "Configure",
|
||||
"pipelines.title": "Processing Pipelines",
|
||||
"pipelines.create": "Create Pipeline",
|
||||
"pipelines.edit": "Edit Pipeline",
|
||||
"help.title": "Help Center",
|
||||
"help.getting_started": "Getting Started",
|
||||
"help.faq": "Frequently Asked Questions",
|
||||
"help.documentation": "Documentation",
|
||||
"help.support": "Support",
|
||||
"error.not_found": "Page not found",
|
||||
"error.not_found_message": "The page you are looking for does not exist.",
|
||||
"error.server_error": "Internal Server Error",
|
||||
"error.server_error_message": "Something went wrong. Please try again later.",
|
||||
"error.unauthorized": "Unauthorized",
|
||||
"error.unauthorized_message": "You need to log in to access this page.",
|
||||
"error.forbidden": "Forbidden",
|
||||
"error.forbidden_message": "You do not have permission to access this page.",
|
||||
"notifications.title": "Notifications",
|
||||
"notifications.mark_read": "Mark as Read",
|
||||
"notifications.mark_all_read": "Mark All as Read",
|
||||
"notifications.no_notifications": "No notifications",
|
||||
"notifications.unread_count": "{count} unread notifications",
|
||||
"upload.page_title": "Upload Files",
|
||||
"upload.section_device": "Upload from Device",
|
||||
"upload.drop_hint_desktop": "Drag & drop files or folders here, or click to select files.",
|
||||
"upload.drop_hint_mobile": "Tap to select files or use the camera button below.",
|
||||
"upload.browse_button": "Browse Files",
|
||||
"upload.file_types": "Allowed types: PDF, Office documents (Word, Excel, PowerPoint, etc.), Images",
|
||||
"upload.file_size_hint": "Maximum size: 500 MB per file",
|
||||
"upload.camera_button": "Take Photo / Scan Document",
|
||||
"upload.section_url": "Upload from URL",
|
||||
"upload.url_label": "File URL",
|
||||
"upload.url_placeholder": "https://example.com/document.pdf",
|
||||
"upload.url_description": "Enter a direct link to a file (PDF, Office documents, or images)",
|
||||
"upload.filename_label": "Filename (optional)",
|
||||
"upload.filename_placeholder": "my-document.pdf",
|
||||
"upload.filename_description": "Leave empty to use filename from URL",
|
||||
"upload.download_button": "Download and Process",
|
||||
"upload.error_url_required": "Please enter a URL",
|
||||
"upload.error_invalid_url": "Invalid URL format",
|
||||
"upload.downloading": "Downloading file from URL...",
|
||||
"upload.button_processing": "Processing...",
|
||||
"files.page_title": "File Records",
|
||||
"files.drop_overlay_title": "Drop files or folders anywhere to upload",
|
||||
"files.drop_overlay_hint": "Supports PDF, Office docs, images, HTML, Markdown, and more",
|
||||
"files.upload_modal_header": "Uploading Files",
|
||||
"files.queue_banner_link": "View Queue",
|
||||
"files.filter_search_placeholder": "Enter filename...",
|
||||
"files.filter_mime_type": "MIME Type",
|
||||
"files.filter_all_types": "All Types",
|
||||
"files.filter_all_statuses": "All Statuses",
|
||||
"files.filter_date_from": "Date From",
|
||||
"files.filter_date_to": "Date To",
|
||||
"files.filter_storage_provider": "Storage Provider",
|
||||
"files.filter_all_providers": "All Providers",
|
||||
"files.filter_tags_placeholder": "e.g. invoice,amazon",
|
||||
"files.filter_ocr_quality": "OCR Quality",
|
||||
"files.filter_ocr_all": "All Files",
|
||||
"files.filter_ocr_poor": "Poor quality",
|
||||
"files.filter_ocr_good": "Good quality",
|
||||
"files.filter_ocr_unchecked": "Not yet assessed",
|
||||
"files.filter_apply": "Apply Filters",
|
||||
"files.filter_clear": "Clear",
|
||||
"files.saved_searches_label": "Saved Searches",
|
||||
"files.saved_searches_empty": "No saved searches yet",
|
||||
"files.saved_searches_save": "Save Current",
|
||||
"files.saved_searches_error": "Could not load saved searches",
|
||||
"files.fulltext_search_label": "Full-Text Search",
|
||||
"files.fulltext_search_placeholder": "Search document content, sender, tags, type...",
|
||||
"files.search_results_title": "Search Results",
|
||||
"files.search_results_empty": "No results found.",
|
||||
"files.bulk_reprocess": "Reprocess Selected",
|
||||
"files.bulk_cloud_ocr": "Re-run Cloud OCR",
|
||||
"files.bulk_download": "Download as ZIP",
|
||||
"files.bulk_delete": "Delete Selected",
|
||||
"files.bulk_clear_selection": "Clear Selection",
|
||||
"files.table_select_all": "Select all files on this page",
|
||||
"files.table_id": "ID",
|
||||
"files.table_original_filename": "Original Filename",
|
||||
"files.table_mime_type": "MIME Type",
|
||||
"files.table_created_at": "Created At",
|
||||
"files.table_actions": "Actions",
|
||||
"files.table_empty": "No files found",
|
||||
"files.action_preview": "Quick preview",
|
||||
"files.action_details": "View details",
|
||||
"files.action_delete": "Delete file",
|
||||
"files.pagination_first": "First",
|
||||
"files.pagination_previous": "Previous",
|
||||
"files.pagination_next": "Next",
|
||||
"files.pagination_last": "Last",
|
||||
"files.delete_modal_title": "Confirm Deletion",
|
||||
"files.delete_modal_message": "Are you sure you want to delete this file?",
|
||||
"files.delete_modal_cancel": "Cancel",
|
||||
"files.delete_modal_confirm": "Delete",
|
||||
"files.preview_modal_title": "Preview",
|
||||
"files.preview_modal_close": "Close preview",
|
||||
"search.page_title": "Search Documents",
|
||||
"search.heading": "Document Search",
|
||||
"search.input_placeholder": "Search documents by content, sender, tags, type...",
|
||||
"search.button": "Search",
|
||||
"search.filter_document_type": "Document Type",
|
||||
"search.filter_document_type_placeholder": "e.g. Invoice",
|
||||
"search.filter_tags_placeholder": "e.g. amazon",
|
||||
"search.filter_sender": "Sender",
|
||||
"search.filter_sender_placeholder": "e.g. ACME Corp",
|
||||
"search.filter_language": "Language",
|
||||
"search.filter_language_placeholder": "e.g. de",
|
||||
"search.filter_text_quality": "Text Quality",
|
||||
"search.filter_text_quality_all": "All",
|
||||
"search.filter_text_quality_high": "High",
|
||||
"search.filter_text_quality_medium": "Medium",
|
||||
"search.filter_text_quality_low": "Low",
|
||||
"search.filter_text_quality_no_text": "No text",
|
||||
"search.filter_date_from": "Date From",
|
||||
"search.filter_date_to": "Date To",
|
||||
"search.filter_clear_button": "Clear Filters",
|
||||
"search.saved_label": "Saved Searches",
|
||||
"search.saved_loading": "Loading...",
|
||||
"search.saved_empty": "No saved searches yet",
|
||||
"search.saved_error": "Could not load saved searches",
|
||||
"search.saved_button": "Save Current",
|
||||
"search.result_empty": "No documents found matching your query.",
|
||||
"search.loading_indicator": "Searching…",
|
||||
"search.error_message": "Search is temporarily unavailable. Please try again in a moment.",
|
||||
"help.page_title": "Help Center",
|
||||
"help.heading": "Help Center",
|
||||
"help.subheading": "Everything you need to get the most out of DocuElevate. Browse topics below or search for what you need.",
|
||||
"help.quickstart_heading": "Quick Start",
|
||||
"help.quickstart_upload": "Upload Documents",
|
||||
"help.quickstart_upload_desc": "Drag & drop files onto the Upload page or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.quickstart_storage": "Connect Storage",
|
||||
"help.quickstart_storage_desc": "Head to Settings and link your cloud accounts. Processed documents are automatically routed to every destination you configure.",
|
||||
"help.quickstart_workflows": "Automate Workflows",
|
||||
"help.quickstart_workflows_desc": "Create Pipelines to define multi-step processing and routing rules. Combine OCR, AI extraction, format conversion, and delivery in a single flow.",
|
||||
"help.sources_heading": "Sources – Getting Documents In",
|
||||
"help.sources_web_upload": "Web Upload",
|
||||
"help.sources_web_upload_desc": "The fastest way to get started. Open the Upload page, drop one or more files, and DocuElevate takes care of the rest. Supported formats include PDF, JPEG, PNG, TIFF, DOCX, XLSX, and more.",
|
||||
"help.sources_email_ingestion": "Email Ingestion (IMAP)",
|
||||
"help.sources_email_ingestion_desc": "Forward documents to a dedicated mailbox. Under Email Ingestion, add one or more IMAP accounts. DocuElevate polls for new messages and processes attachments automatically.",
|
||||
"help.sources_rest_api": "REST API",
|
||||
"help.sources_rest_api_desc": "Integrate programmatically by POST-ing files to /api/upload. Ideal for scripts, watched folders, scanners, or third-party tools like Zapier and n8n.",
|
||||
"help.sources_scanner": "Scanner & Mobile",
|
||||
"help.sources_scanner_desc": "Point network scanners at DocuElevate's upload endpoint or use any mobile scanning app that supports custom HTTP destinations.",
|
||||
"help.destinations_heading": "Destinations – Where Documents Go",
|
||||
"help.destinations_dropbox": "Dropbox",
|
||||
"help.destinations_dropbox_desc": "OAuth-linked. Files land in your chosen folder.",
|
||||
"help.destinations_google_drive": "Google Drive",
|
||||
"help.destinations_google_drive_desc": "Service account or OAuth. Supports shared drives.",
|
||||
"help.destinations_onedrive": "OneDrive",
|
||||
"help.destinations_onedrive_desc": "Microsoft Graph API integration.",
|
||||
"help.destinations_s3": "Amazon S3",
|
||||
"help.destinations_s3_desc": "Any S3-compatible bucket (AWS, MinIO, Wasabi).",
|
||||
"help.destinations_nextcloud": "Nextcloud / WebDAV",
|
||||
"help.destinations_nextcloud_desc": "Self-hosted cloud storage via WebDAV.",
|
||||
"help.destinations_paperless": "Paperless-ngx",
|
||||
"help.destinations_paperless_desc": "Push documents straight into Paperless for archival.",
|
||||
"help.destinations_sftp": "SFTP / FTP",
|
||||
"help.destinations_sftp_desc": "Secure file transfer to any server.",
|
||||
"help.destinations_email": "Email Forwarding",
|
||||
"help.destinations_email_desc": "Processed files sent as SMTP attachments.",
|
||||
"help.destinations_webhook": "Webhook",
|
||||
"help.destinations_webhook_desc": "POST metadata to any external endpoint.",
|
||||
"help.workflows_heading": "Workflows & Pipelines",
|
||||
"help.workflows_what_is": "What is a Pipeline?",
|
||||
"help.workflows_definition": "A Pipeline is a series of processing steps that run automatically whenever a document is ingested. Each step can transform, enrich, or route the document.",
|
||||
"help.workflows_typical_steps": "Typical Steps",
|
||||
"help.workflows_step_1": "Convert to PDF",
|
||||
"help.workflows_step_2": "OCR – extract text",
|
||||
"help.workflows_step_3": "AI metadata extraction",
|
||||
"help.workflows_step_4": "Deliver to one or more destinations",
|
||||
"help.workflows_creating": "Creating a Pipeline",
|
||||
"help.workflows_step_1_create": "Go to Pipelines in the main menu.",
|
||||
"help.workflows_step_2_create": "Click New Pipeline and give it a name.",
|
||||
"help.workflows_step_3_create": "Add the processing steps you need.",
|
||||
"help.workflows_step_4_create": "Choose one or more delivery destinations.",
|
||||
"help.workflows_step_5_create": "Save – new documents will be processed through this pipeline automatically.",
|
||||
"help.faq_heading": "Frequently Asked Questions",
|
||||
"help.faq_1_q": "How do I upload documents?",
|
||||
"help.faq_1_a": "Navigate to the Upload page, drag-and-drop your files or click Choose File. DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.",
|
||||
"help.faq_2_q": "Which file formats are supported?",
|
||||
"help.faq_2_a": "PDF, JPEG, PNG, TIFF, BMP, GIF, DOCX, XLSX, PPTX, ODT, ODS, TXT, RTF, and HTML. Non-PDF files are automatically converted to PDF before processing.",
|
||||
"help.faq_3_q": "Can I ingest documents from email?",
|
||||
"help.faq_3_a": "Yes. Go to Email Ingestion, add an IMAP account, and DocuElevate will poll for new messages and process attachments automatically.",
|
||||
"help.faq_4_q": "How do processing pipelines work?",
|
||||
"help.faq_4_a": "Pipelines let you chain processing steps – OCR, AI extraction, format conversion – and route the result to one or more destinations. Create and manage them from the Pipelines page.",
|
||||
"help.faq_5_q": "Is my data secure?",
|
||||
"help.faq_5_a": "DocuElevate encrypts credentials at rest, communicates over TLS, and never stores your documents longer than necessary. See the Privacy Notice for full details.",
|
||||
"help.support_heading": "Contact Support",
|
||||
"help.support_description": "Can’t find what you’re looking for? Our support team is here to help.",
|
||||
"help.support_admin_message": "Contact your administrator for support information.",
|
||||
"index.page_title_public": "Intelligent Document Processing",
|
||||
"index.page_title_dashboard": "Dashboard",
|
||||
"index.badge_intelligent": "Intelligent Document Processing",
|
||||
"index.hero_heading": "From upload to insight — automatically.",
|
||||
"index.hero_description": "DocuElevate ingests your documents, runs OCR, extracts metadata with AI, and routes files to Dropbox, Google Drive, OneDrive, S3, Nextcloud, and more — all in one seamless pipeline.",
|
||||
"index.hero_signup": "Get Started — it’s free",
|
||||
"index.hero_login": "Log In",
|
||||
"index.hero_pricing": "View Plans & Pricing",
|
||||
"index.feature_section_title": "Everything you need for smart document workflows",
|
||||
"index.feature_ocr": "OCR & Text Extraction",
|
||||
"index.feature_ocr_desc": "Azure Document Intelligence converts scanned PDFs and images into fully searchable text automatically.",
|
||||
"index.feature_ai": "AI Metadata Extraction",
|
||||
"index.feature_ai_desc": "OpenAI, Claude, Gemini, and other pluggable AI providers classify documents and pull out key fields like dates, amounts, and subjects.",
|
||||
"index.feature_cloud": "Multi-Cloud Storage",
|
||||
"index.feature_cloud_desc": "Route processed files to Dropbox, Google Drive, OneDrive, Amazon S3, Nextcloud, Paperless NGX, WebDAV, FTP/SFTP, and more.",
|
||||
"index.feature_email": "Email & IMAP Ingestion",
|
||||
"index.feature_email_desc": "Automatically pull documents from Gmail or any IMAP mailbox — no manual uploads needed.",
|
||||
"index.feature_search": "Full-Text Search",
|
||||
"index.feature_search_desc": "Instantly find any document by content, metadata, or tags across your entire archive.",
|
||||
"index.feature_pipelines": "Custom Pipelines",
|
||||
"index.feature_pipelines_desc": "Build processing pipelines with configurable steps — OCR, AI extraction, format conversion, and storage routing in any order.",
|
||||
"index.cta_heading": "Ready to elevate your document workflow?",
|
||||
"index.cta_description": "Join teams already automating their document processing with DocuElevate.",
|
||||
"index.cta_signup": "Create a free account",
|
||||
"index.cta_pricing": "See pricing",
|
||||
"index.dashboard_subtitle": "Intelligent document processing & management",
|
||||
"index.platform_overview": "Platform overview",
|
||||
"index.stat_total_files": "Total files",
|
||||
"index.stat_files_today": "Files today",
|
||||
"index.stat_files_month": "Files this month",
|
||||
"index.stat_active_users": "Active users",
|
||||
"index.usage_my_usage": "My usage",
|
||||
"index.usage_lifetime": "Lifetime files",
|
||||
"index.usage_today": "Files today",
|
||||
"index.usage_month": "Files this month",
|
||||
"index.usage_unlimited": "Unlimited",
|
||||
"index.tier_plan": "Plan",
|
||||
"index.tier_upgrade": "Upgrade",
|
||||
"index.tier_view_details": "View full details",
|
||||
"index.quick_actions": "Quick Actions",
|
||||
"index.quick_upload": "Upload Document",
|
||||
"index.quick_upload_desc": "Process a new file",
|
||||
"index.quick_documents": "My Documents",
|
||||
"index.quick_documents_desc": "Browse your processed files",
|
||||
"index.quick_subscription": "My Subscription",
|
||||
"index.quick_subscription_desc": "View plan & usage details",
|
||||
"index.quick_search": "Search",
|
||||
"index.quick_search_desc": "Full-text search across documents",
|
||||
"index.upgrade_plan": "Upgrade your plan",
|
||||
"index.upgrade_description": "Unlock more documents, more destinations and priority support.",
|
||||
"index.upgrade_daily_limits": "Higher daily & monthly limits",
|
||||
"index.upgrade_destinations": "More storage destinations",
|
||||
"index.upgrade_ocr_pages": "More OCR pages",
|
||||
"index.upgrade_view_pricing": "View plans & pricing",
|
||||
"index.integrations_title": "Integrations",
|
||||
"index.integrations_active": "Active integrations",
|
||||
"index.integrations_storage": "Storage targets",
|
||||
"index.integrations_view_status": "View system status",
|
||||
"index.single_user_heading": "DocuElevate Dashboard",
|
||||
"index.single_user_subtitle": "Intelligent document processing & management",
|
||||
"index.capabilities_title": "Capabilities",
|
||||
"index.capabilities_ocr": "OCR & metadata extraction with AI",
|
||||
"index.capabilities_cloud": "Cloud storage: Dropbox, OneDrive, Google Drive, NextCloud",
|
||||
"index.capabilities_paperless": "Paperless-ngx integration for document management",
|
||||
"index.capabilities_ingestion": "Email & URL-based document ingestion",
|
||||
"index.capabilities_workflows": "Automated classification & routing workflows",
|
||||
"index.getting_started": "Getting Started",
|
||||
"index.getting_started_1": "Configure integrations via System Status",
|
||||
"index.getting_started_2": "Upload your first document",
|
||||
"index.getting_started_3": "Review results in Files",
|
||||
"index.getting_started_learn": "Learn more about DocuElevate",
|
||||
"error.404_code": "404",
|
||||
"error.404_heading": "Oops, we couldn’t find that page!",
|
||||
"error.404_message": "It seems DocuElevate has misplaced the document you were looking for. Don’t worry – we’ve got your back.",
|
||||
"error.404_home": "Return Home",
|
||||
"error.500_code": "500",
|
||||
"error.500_heading": "Oops! Something Went Wrong.",
|
||||
"error.500_description": "Our servers encountered a mishap and need a moment.",
|
||||
"error.500_home": "Go Home",
|
||||
"pipelines.page_title": "Processing Pipelines",
|
||||
"pipelines.system_label": "System",
|
||||
"pipelines.default_label": "Default",
|
||||
"pipelines.inactive_label": "Inactive",
|
||||
"pipelines.disabled_label": "Disabled",
|
||||
"pipelines.enabled_label": "Enabled",
|
||||
"pipelines.empty_state": "No pipelines yet",
|
||||
"pipelines.set_default": "Set as my default pipeline",
|
||||
"pipelines.description_label": "Description",
|
||||
"pipelines.active_label": "Active",
|
||||
"integrations.page_title": "Integrations",
|
||||
"integrations.imap_settings": "IMAP Settings",
|
||||
"integrations.host_label": "Host",
|
||||
"integrations.port_label": "Port",
|
||||
"integrations.username_label": "Username",
|
||||
"integrations.password_label": "Password",
|
||||
"integrations.folder_label": "Folder",
|
||||
"integrations.empty_state": "No integrations configured",
|
||||
"status.page_title": "System Status",
|
||||
"status.app_version": "App Version",
|
||||
"status.build_date": "Build Date",
|
||||
"status.last_check": "Last Check",
|
||||
"status.container_id": "Container ID",
|
||||
"status.git_commit": "Git Commit",
|
||||
"status.setting_label": "Setting",
|
||||
"status.value_label": "Value",
|
||||
"notifications.page_title": "Notifications",
|
||||
"notifications.manage_desc": "Manage your notification inbox, targets, and event preferences",
|
||||
"notifications.tab_inbox": "Inbox",
|
||||
"notifications.tab_settings": "Settings",
|
||||
"notifications.filter_all": "All",
|
||||
"notifications.filter_unread": "Unread only",
|
||||
"notifications.filter_read": "Read only",
|
||||
"notifications.mark_all_read_btn": "Mark all read",
|
||||
"auth.login_title": "Log In",
|
||||
"auth.signup_title": "Sign Up",
|
||||
"auth.forgot_password": "Forgot Password?",
|
||||
"auth.remember_me": "Remember me",
|
||||
"auth.email_label": "Email",
|
||||
"auth.password_label": "Password",
|
||||
"auth.confirm_password": "Confirm Password",
|
||||
"auth.username_label": "Username",
|
||||
"auth.display_name_label": "Display Name",
|
||||
"language.nb": "Norsk",
|
||||
"language.da": "Dansk",
|
||||
"language.sv": "Svenska",
|
||||
"language.fi": "Suomi",
|
||||
"language.is": "Íslenska",
|
||||
"language.ga": "Gaeilge",
|
||||
"language.hu": "Magyar",
|
||||
"language.cs": "Čeština",
|
||||
"language.sk": "Slovenčina",
|
||||
"language.sl": "Slovenščina",
|
||||
"language.hr": "Hrvatski",
|
||||
"language.ro": "Română",
|
||||
"language.bg": "Български",
|
||||
"language.uk": "Українська",
|
||||
"language.tr": "Türkçe",
|
||||
"language.el": "Ελληνικά",
|
||||
"language.et": "Eesti",
|
||||
"language.lv": "Latviešu",
|
||||
"language.lt": "Lietuvių",
|
||||
"language.lb": "Lëtzebuergesch",
|
||||
"language.ca": "Català"
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"app.name": "DocuElevate",
|
||||
|
||||
"nav.dashboard": "Panel",
|
||||
"nav.upload": "Subir",
|
||||
"nav.files": "Archivos",
|
||||
"nav.search": "Buscar",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.integrations": "Integraciones",
|
||||
"nav.help": "Ayuda",
|
||||
"nav.notifications": "Notificaciones",
|
||||
"nav.pricing": "Precios",
|
||||
"nav.about": "Acerca de",
|
||||
"nav.admin": "Admin",
|
||||
"nav.settings": "Configuración",
|
||||
"nav.users": "Usuarios",
|
||||
"nav.plan_designer": "Diseñador de planes",
|
||||
"nav.credentials": "Credenciales",
|
||||
"nav.file_manager": "Gestor de archivos",
|
||||
"nav.duplicates": "Duplicados",
|
||||
"nav.similarity": "Similitud",
|
||||
"nav.queue_monitor": "Monitor de cola",
|
||||
"nav.scheduled_jobs": "Tareas programadas",
|
||||
"nav.backup_restore": "Copia de seguridad y restauración",
|
||||
"nav.status": "Estado",
|
||||
"nav.api_docs": "Documentación API",
|
||||
"nav.developer_docs": "Documentación para desarrolladores",
|
||||
"nav.dark_mode": "Modo oscuro",
|
||||
"nav.light_mode": "Modo claro",
|
||||
"nav.toggle_dark_mode": "Alternar modo oscuro",
|
||||
"nav.toggle_nav": "Alternar menú de navegación",
|
||||
"nav.open_main_menu": "Abrir menú principal",
|
||||
"nav.skip_to_content": "Ir al contenido principal",
|
||||
"nav.main_navigation": "Navegación principal",
|
||||
"nav.admin_menu": "Menú de administración",
|
||||
"nav.admin_actions": "Acciones de administración",
|
||||
"nav.help_center": "Centro de ayuda",
|
||||
|
||||
"auth.login": "Iniciar sesión",
|
||||
"auth.logout": "Cerrar sesión",
|
||||
"auth.signup": "Registrarse",
|
||||
"auth.my_account": "Mi cuenta",
|
||||
"auth.profile": "Perfil",
|
||||
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.privacy": "Privacidad",
|
||||
"footer.imprint": "Aviso legal",
|
||||
"footer.terms": "Términos",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.license": "Licencia",
|
||||
"footer.attributions": "Atribuciones",
|
||||
"footer.version": "Versión {version}",
|
||||
"footer.navigation": "Navegación del pie de página",
|
||||
|
||||
"cookie.notice": "DocuElevate utiliza solo cookies de sesión esenciales necesarias para la autenticación y el funcionamiento del servicio. No se utilizan cookies de seguimiento ni analíticas.",
|
||||
"cookie.policy_link": "Política de cookies",
|
||||
"cookie.privacy_link": "Aviso de privacidad",
|
||||
"cookie.accept": "Entendido",
|
||||
"cookie.notice_label": "Aviso de cookies",
|
||||
|
||||
"common.save": "Guardar",
|
||||
"common.cancel": "Cancelar",
|
||||
"common.delete": "Eliminar",
|
||||
"common.edit": "Editar",
|
||||
"common.close": "Cerrar",
|
||||
"common.confirm": "Confirmar",
|
||||
"common.back": "Atrás",
|
||||
"common.next": "Siguiente",
|
||||
"common.loading": "Cargando...",
|
||||
"common.error": "Error",
|
||||
"common.success": "Éxito",
|
||||
"common.warning": "Advertencia",
|
||||
"common.info": "Información",
|
||||
"common.yes": "Sí",
|
||||
"common.no": "No",
|
||||
"common.search": "Buscar",
|
||||
"common.filter": "Filtrar",
|
||||
"common.reset": "Restablecer",
|
||||
"common.refresh": "Actualizar",
|
||||
"common.download": "Descargar",
|
||||
"common.actions": "Acciones",
|
||||
"common.details": "Detalles",
|
||||
"common.name": "Nombre",
|
||||
"common.description": "Descripción",
|
||||
"common.type": "Tipo",
|
||||
"common.status": "Estado",
|
||||
"common.date": "Fecha",
|
||||
"common.size": "Tamaño",
|
||||
"common.created": "Creado",
|
||||
"common.updated": "Actualizado",
|
||||
"common.enabled": "Habilitado",
|
||||
"common.disabled": "Deshabilitado",
|
||||
"common.active": "Activo",
|
||||
"common.inactive": "Inactivo",
|
||||
"common.all": "Todo",
|
||||
"common.none": "Ninguno",
|
||||
"common.select": "Seleccionar",
|
||||
"common.upload": "Subir",
|
||||
"common.processing": "Procesando",
|
||||
"common.completed": "Completado",
|
||||
"common.failed": "Fallido",
|
||||
"common.pending": "Pendiente",
|
||||
"common.retry": "Reintentar",
|
||||
"common.view": "Ver",
|
||||
"common.copy": "Copiar",
|
||||
"common.copied": "¡Copiado!",
|
||||
|
||||
"language.selector": "Idioma",
|
||||
"language.en": "English",
|
||||
"language.de": "Deutsch",
|
||||
"language.fr": "Français",
|
||||
"language.es": "Español",
|
||||
"language.it": "Italiano",
|
||||
"language.pt": "Português",
|
||||
"language.nl": "Nederlands",
|
||||
"language.pl": "Polski",
|
||||
"language.zh": "中文",
|
||||
"language.ru": "Русский",
|
||||
"language.changed": "Idioma cambiado a {language}",
|
||||
|
||||
"dashboard.title": "Panel",
|
||||
"dashboard.total_files": "Total de archivos",
|
||||
"dashboard.files_today": "Archivos hoy",
|
||||
"dashboard.files_this_month": "Archivos este mes",
|
||||
"dashboard.ocr_processed": "OCR procesados",
|
||||
"dashboard.active_integrations": "Integraciones activas",
|
||||
"dashboard.storage_targets": "Destinos de almacenamiento",
|
||||
"dashboard.recent_activity": "Actividad reciente",
|
||||
"dashboard.quick_actions": "Acciones rápidas",
|
||||
"dashboard.welcome": "Bienvenido a DocuElevate",
|
||||
|
||||
"upload.title": "Subir documento",
|
||||
"upload.drag_drop": "Arrastre archivos aquí o haga clic para buscar",
|
||||
"upload.select_file": "Seleccionar archivo",
|
||||
"upload.uploading": "Subiendo...",
|
||||
"upload.success": "Archivo subido con éxito",
|
||||
"upload.error": "Error al subir",
|
||||
"upload.max_size": "Tamaño máximo del archivo: {size}",
|
||||
|
||||
"files.title": "Archivos",
|
||||
"files.no_files": "No se encontraron archivos",
|
||||
"files.filename": "Nombre del archivo",
|
||||
"files.document_title": "Título del documento",
|
||||
"files.uploaded": "Subido",
|
||||
"files.file_size": "Tamaño del archivo",
|
||||
"files.ocr_status": "Estado OCR",
|
||||
"files.tags": "Etiquetas",
|
||||
|
||||
"search.title": "Buscar documentos",
|
||||
"search.placeholder": "Buscar por nombre, contenido, etiquetas...",
|
||||
"search.no_results": "No se encontraron resultados",
|
||||
"search.results_count": "{count} resultados encontrados",
|
||||
|
||||
"settings.title": "Configuración",
|
||||
"settings.save_success": "Configuración guardada con éxito",
|
||||
"settings.save_error": "Error al guardar la configuración",
|
||||
"settings.reset_confirm": "¿Está seguro de que desea restablecer esta configuración?",
|
||||
|
||||
"integrations.title": "Integraciones",
|
||||
"integrations.connect": "Conectar",
|
||||
"integrations.disconnect": "Desconectar",
|
||||
"integrations.connected": "Conectado",
|
||||
"integrations.not_connected": "No conectado",
|
||||
"integrations.configure": "Configurar",
|
||||
|
||||
"pipelines.title": "Pipelines de procesamiento",
|
||||
"pipelines.create": "Crear pipeline",
|
||||
"pipelines.edit": "Editar pipeline",
|
||||
|
||||
"help.title": "Centro de ayuda",
|
||||
"help.getting_started": "Primeros pasos",
|
||||
"help.faq": "Preguntas frecuentes",
|
||||
"help.documentation": "Documentación",
|
||||
"help.support": "Soporte",
|
||||
|
||||
"error.not_found": "Página no encontrada",
|
||||
"error.not_found_message": "La página que busca no existe.",
|
||||
"error.server_error": "Error interno del servidor",
|
||||
"error.server_error_message": "Algo salió mal. Inténtelo de nuevo más tarde.",
|
||||
"error.unauthorized": "No autorizado",
|
||||
"error.unauthorized_message": "Debe iniciar sesión para acceder a esta página.",
|
||||
"error.forbidden": "Prohibido",
|
||||
"error.forbidden_message": "No tiene permiso para acceder a esta página.",
|
||||
|
||||
"notifications.title": "Notificaciones",
|
||||
"notifications.mark_read": "Marcar como leído",
|
||||
"notifications.mark_all_read": "Marcar todo como leído",
|
||||
"notifications.no_notifications": "Sin notificaciones",
|
||||
"notifications.unread_count": "{count} notificaciones no leídas"
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"app.name": "DocuElevate",
|
||||
|
||||
"nav.dashboard": "Tableau de bord",
|
||||
"nav.upload": "Téléverser",
|
||||
"nav.files": "Fichiers",
|
||||
"nav.search": "Recherche",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.integrations": "Intégrations",
|
||||
"nav.help": "Aide",
|
||||
"nav.notifications": "Notifications",
|
||||
"nav.pricing": "Tarifs",
|
||||
"nav.about": "À propos",
|
||||
"nav.admin": "Admin",
|
||||
"nav.settings": "Paramètres",
|
||||
"nav.users": "Utilisateurs",
|
||||
"nav.plan_designer": "Concepteur de plans",
|
||||
"nav.credentials": "Identifiants",
|
||||
"nav.file_manager": "Gestionnaire de fichiers",
|
||||
"nav.duplicates": "Doublons",
|
||||
"nav.similarity": "Similarité",
|
||||
"nav.queue_monitor": "File d'attente",
|
||||
"nav.scheduled_jobs": "Tâches planifiées",
|
||||
"nav.backup_restore": "Sauvegarde et restauration",
|
||||
"nav.status": "Statut",
|
||||
"nav.api_docs": "Documentation API",
|
||||
"nav.developer_docs": "Documentation développeur",
|
||||
"nav.dark_mode": "Mode sombre",
|
||||
"nav.light_mode": "Mode clair",
|
||||
"nav.toggle_dark_mode": "Basculer le mode sombre",
|
||||
"nav.toggle_nav": "Basculer le menu de navigation",
|
||||
"nav.open_main_menu": "Ouvrir le menu principal",
|
||||
"nav.skip_to_content": "Aller au contenu principal",
|
||||
"nav.main_navigation": "Navigation principale",
|
||||
"nav.admin_menu": "Menu admin",
|
||||
"nav.admin_actions": "Actions admin",
|
||||
"nav.help_center": "Centre d'aide",
|
||||
|
||||
"auth.login": "Se connecter",
|
||||
"auth.logout": "Se déconnecter",
|
||||
"auth.signup": "S'inscrire",
|
||||
"auth.my_account": "Mon compte",
|
||||
"auth.profile": "Profil",
|
||||
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.privacy": "Confidentialité",
|
||||
"footer.imprint": "Mentions légales",
|
||||
"footer.terms": "Conditions",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.license": "Licence",
|
||||
"footer.attributions": "Attributions",
|
||||
"footer.version": "Version {version}",
|
||||
"footer.navigation": "Navigation du pied de page",
|
||||
|
||||
"cookie.notice": "DocuElevate utilise uniquement des cookies de session essentiels nécessaires à l'authentification et au fonctionnement du service. Aucun cookie de suivi ou d'analyse n'est utilisé.",
|
||||
"cookie.policy_link": "Politique de cookies",
|
||||
"cookie.privacy_link": "Avis de confidentialité",
|
||||
"cookie.accept": "Compris",
|
||||
"cookie.notice_label": "Avis relatif aux cookies",
|
||||
|
||||
"common.save": "Enregistrer",
|
||||
"common.cancel": "Annuler",
|
||||
"common.delete": "Supprimer",
|
||||
"common.edit": "Modifier",
|
||||
"common.close": "Fermer",
|
||||
"common.confirm": "Confirmer",
|
||||
"common.back": "Retour",
|
||||
"common.next": "Suivant",
|
||||
"common.loading": "Chargement...",
|
||||
"common.error": "Erreur",
|
||||
"common.success": "Succès",
|
||||
"common.warning": "Avertissement",
|
||||
"common.info": "Info",
|
||||
"common.yes": "Oui",
|
||||
"common.no": "Non",
|
||||
"common.search": "Rechercher",
|
||||
"common.filter": "Filtrer",
|
||||
"common.reset": "Réinitialiser",
|
||||
"common.refresh": "Actualiser",
|
||||
"common.download": "Télécharger",
|
||||
"common.actions": "Actions",
|
||||
"common.details": "Détails",
|
||||
"common.name": "Nom",
|
||||
"common.description": "Description",
|
||||
"common.type": "Type",
|
||||
"common.status": "Statut",
|
||||
"common.date": "Date",
|
||||
"common.size": "Taille",
|
||||
"common.created": "Créé",
|
||||
"common.updated": "Mis à jour",
|
||||
"common.enabled": "Activé",
|
||||
"common.disabled": "Désactivé",
|
||||
"common.active": "Actif",
|
||||
"common.inactive": "Inactif",
|
||||
"common.all": "Tout",
|
||||
"common.none": "Aucun",
|
||||
"common.select": "Sélectionner",
|
||||
"common.upload": "Téléverser",
|
||||
"common.processing": "En cours de traitement",
|
||||
"common.completed": "Terminé",
|
||||
"common.failed": "Échoué",
|
||||
"common.pending": "En attente",
|
||||
"common.retry": "Réessayer",
|
||||
"common.view": "Voir",
|
||||
"common.copy": "Copier",
|
||||
"common.copied": "Copié !",
|
||||
|
||||
"language.selector": "Langue",
|
||||
"language.en": "English",
|
||||
"language.de": "Deutsch",
|
||||
"language.fr": "Français",
|
||||
"language.es": "Español",
|
||||
"language.it": "Italiano",
|
||||
"language.pt": "Português",
|
||||
"language.nl": "Nederlands",
|
||||
"language.pl": "Polski",
|
||||
"language.zh": "中文",
|
||||
"language.ru": "Русский",
|
||||
"language.changed": "Langue changée en {language}",
|
||||
|
||||
"dashboard.title": "Tableau de bord",
|
||||
"dashboard.total_files": "Total des fichiers",
|
||||
"dashboard.files_today": "Fichiers aujourd'hui",
|
||||
"dashboard.files_this_month": "Fichiers ce mois-ci",
|
||||
"dashboard.ocr_processed": "OCR traités",
|
||||
"dashboard.active_integrations": "Intégrations actives",
|
||||
"dashboard.storage_targets": "Destinations de stockage",
|
||||
"dashboard.recent_activity": "Activité récente",
|
||||
"dashboard.quick_actions": "Actions rapides",
|
||||
"dashboard.welcome": "Bienvenue sur DocuElevate",
|
||||
|
||||
"upload.title": "Téléverser un document",
|
||||
"upload.drag_drop": "Glissez-déposez vos fichiers ici ou cliquez pour parcourir",
|
||||
"upload.select_file": "Sélectionner un fichier",
|
||||
"upload.uploading": "Téléversement en cours...",
|
||||
"upload.success": "Fichier téléversé avec succès",
|
||||
"upload.error": "Échec du téléversement",
|
||||
"upload.max_size": "Taille maximale du fichier : {size}",
|
||||
|
||||
"files.title": "Fichiers",
|
||||
"files.no_files": "Aucun fichier trouvé",
|
||||
"files.filename": "Nom du fichier",
|
||||
"files.document_title": "Titre du document",
|
||||
"files.uploaded": "Téléversé",
|
||||
"files.file_size": "Taille du fichier",
|
||||
"files.ocr_status": "Statut OCR",
|
||||
"files.tags": "Étiquettes",
|
||||
|
||||
"search.title": "Rechercher des documents",
|
||||
"search.placeholder": "Rechercher par nom, contenu, étiquettes...",
|
||||
"search.no_results": "Aucun résultat trouvé",
|
||||
"search.results_count": "{count} résultats trouvés",
|
||||
|
||||
"settings.title": "Paramètres",
|
||||
"settings.save_success": "Paramètre enregistré avec succès",
|
||||
"settings.save_error": "Échec de l'enregistrement du paramètre",
|
||||
"settings.reset_confirm": "Êtes-vous sûr de vouloir réinitialiser ce paramètre ?",
|
||||
|
||||
"integrations.title": "Intégrations",
|
||||
"integrations.connect": "Connecter",
|
||||
"integrations.disconnect": "Déconnecter",
|
||||
"integrations.connected": "Connecté",
|
||||
"integrations.not_connected": "Non connecté",
|
||||
"integrations.configure": "Configurer",
|
||||
|
||||
"pipelines.title": "Pipelines de traitement",
|
||||
"pipelines.create": "Créer un pipeline",
|
||||
"pipelines.edit": "Modifier le pipeline",
|
||||
|
||||
"help.title": "Centre d'aide",
|
||||
"help.getting_started": "Premiers pas",
|
||||
"help.faq": "Questions fréquentes",
|
||||
"help.documentation": "Documentation",
|
||||
"help.support": "Support",
|
||||
|
||||
"error.not_found": "Page non trouvée",
|
||||
"error.not_found_message": "La page que vous recherchez n'existe pas.",
|
||||
"error.server_error": "Erreur interne du serveur",
|
||||
"error.server_error_message": "Quelque chose s'est mal passé. Veuillez réessayer plus tard.",
|
||||
"error.unauthorized": "Non autorisé",
|
||||
"error.unauthorized_message": "Vous devez vous connecter pour accéder à cette page.",
|
||||
"error.forbidden": "Interdit",
|
||||
"error.forbidden_message": "Vous n'avez pas la permission d'accéder à cette page.",
|
||||
|
||||
"notifications.title": "Notifications",
|
||||
"notifications.mark_read": "Marquer comme lu",
|
||||
"notifications.mark_all_read": "Tout marquer comme lu",
|
||||
"notifications.no_notifications": "Aucune notification",
|
||||
"notifications.unread_count": "{count} notifications non lues"
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"app.name": "DocuElevate",
|
||||
|
||||
"nav.dashboard": "Cruscotto",
|
||||
"nav.upload": "Carica",
|
||||
"nav.files": "File",
|
||||
"nav.search": "Cerca",
|
||||
"nav.pipelines": "Pipeline",
|
||||
"nav.integrations": "Integrazioni",
|
||||
"nav.help": "Aiuto",
|
||||
"nav.notifications": "Notifiche",
|
||||
"nav.pricing": "Prezzi",
|
||||
"nav.about": "Informazioni",
|
||||
"nav.admin": "Admin",
|
||||
"nav.settings": "Impostazioni",
|
||||
"nav.users": "Utenti",
|
||||
"nav.plan_designer": "Designer dei piani",
|
||||
"nav.credentials": "Credenziali",
|
||||
"nav.file_manager": "Gestore file",
|
||||
"nav.duplicates": "Duplicati",
|
||||
"nav.similarity": "Similarità",
|
||||
"nav.queue_monitor": "Monitor coda",
|
||||
"nav.scheduled_jobs": "Attività pianificate",
|
||||
"nav.backup_restore": "Backup e ripristino",
|
||||
"nav.status": "Stato",
|
||||
"nav.api_docs": "Documentazione API",
|
||||
"nav.developer_docs": "Documentazione sviluppatore",
|
||||
"nav.dark_mode": "Modalità scura",
|
||||
"nav.light_mode": "Modalità chiara",
|
||||
"nav.toggle_dark_mode": "Attiva/disattiva modalità scura",
|
||||
"nav.toggle_nav": "Attiva/disattiva menu di navigazione",
|
||||
"nav.open_main_menu": "Apri menu principale",
|
||||
"nav.skip_to_content": "Vai al contenuto principale",
|
||||
"nav.main_navigation": "Navigazione principale",
|
||||
"nav.admin_menu": "Menu admin",
|
||||
"nav.admin_actions": "Azioni admin",
|
||||
"nav.help_center": "Centro assistenza",
|
||||
|
||||
"auth.login": "Accedi",
|
||||
"auth.logout": "Esci",
|
||||
"auth.signup": "Registrati",
|
||||
"auth.my_account": "Il mio account",
|
||||
"auth.profile": "Profilo",
|
||||
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.privacy": "Privacy",
|
||||
"footer.imprint": "Note legali",
|
||||
"footer.terms": "Termini",
|
||||
"footer.cookies": "Cookie",
|
||||
"footer.license": "Licenza",
|
||||
"footer.attributions": "Attribuzioni",
|
||||
"footer.version": "Versione {version}",
|
||||
"footer.navigation": "Navigazione a piè di pagina",
|
||||
|
||||
"cookie.notice": "DocuElevate utilizza solo cookie di sessione essenziali necessari per l'autenticazione e il funzionamento del servizio. Non vengono utilizzati cookie di tracciamento o analisi.",
|
||||
"cookie.policy_link": "Politica sui cookie",
|
||||
"cookie.privacy_link": "Informativa sulla privacy",
|
||||
"cookie.accept": "Ho capito",
|
||||
"cookie.notice_label": "Avviso sui cookie",
|
||||
|
||||
"common.save": "Salva",
|
||||
"common.cancel": "Annulla",
|
||||
"common.delete": "Elimina",
|
||||
"common.edit": "Modifica",
|
||||
"common.close": "Chiudi",
|
||||
"common.confirm": "Conferma",
|
||||
"common.back": "Indietro",
|
||||
"common.next": "Avanti",
|
||||
"common.loading": "Caricamento...",
|
||||
"common.error": "Errore",
|
||||
"common.success": "Successo",
|
||||
"common.warning": "Avviso",
|
||||
"common.info": "Info",
|
||||
"common.yes": "Sì",
|
||||
"common.no": "No",
|
||||
"common.search": "Cerca",
|
||||
"common.filter": "Filtra",
|
||||
"common.reset": "Reimposta",
|
||||
"common.refresh": "Aggiorna",
|
||||
"common.download": "Scarica",
|
||||
"common.actions": "Azioni",
|
||||
"common.details": "Dettagli",
|
||||
"common.name": "Nome",
|
||||
"common.description": "Descrizione",
|
||||
"common.type": "Tipo",
|
||||
"common.status": "Stato",
|
||||
"common.date": "Data",
|
||||
"common.size": "Dimensione",
|
||||
"common.created": "Creato",
|
||||
"common.updated": "Aggiornato",
|
||||
"common.enabled": "Abilitato",
|
||||
"common.disabled": "Disabilitato",
|
||||
"common.active": "Attivo",
|
||||
"common.inactive": "Inattivo",
|
||||
"common.all": "Tutto",
|
||||
"common.none": "Nessuno",
|
||||
"common.select": "Seleziona",
|
||||
"common.upload": "Carica",
|
||||
"common.processing": "In elaborazione",
|
||||
"common.completed": "Completato",
|
||||
"common.failed": "Fallito",
|
||||
"common.pending": "In attesa",
|
||||
"common.retry": "Riprova",
|
||||
"common.view": "Visualizza",
|
||||
"common.copy": "Copia",
|
||||
"common.copied": "Copiato!",
|
||||
|
||||
"language.selector": "Lingua",
|
||||
"language.en": "English",
|
||||
"language.de": "Deutsch",
|
||||
"language.fr": "Français",
|
||||
"language.es": "Español",
|
||||
"language.it": "Italiano",
|
||||
"language.pt": "Português",
|
||||
"language.nl": "Nederlands",
|
||||
"language.pl": "Polski",
|
||||
"language.zh": "中文",
|
||||
"language.ru": "Русский",
|
||||
"language.changed": "Lingua cambiata in {language}",
|
||||
|
||||
"dashboard.title": "Cruscotto",
|
||||
"dashboard.total_files": "File totali",
|
||||
"dashboard.files_today": "File oggi",
|
||||
"dashboard.files_this_month": "File questo mese",
|
||||
"dashboard.ocr_processed": "OCR elaborati",
|
||||
"dashboard.active_integrations": "Integrazioni attive",
|
||||
"dashboard.storage_targets": "Destinazioni di archiviazione",
|
||||
"dashboard.recent_activity": "Attività recente",
|
||||
"dashboard.quick_actions": "Azioni rapide",
|
||||
"dashboard.welcome": "Benvenuto su DocuElevate",
|
||||
|
||||
"upload.title": "Carica documento",
|
||||
"upload.drag_drop": "Trascina i file qui o fai clic per sfogliare",
|
||||
"upload.select_file": "Seleziona file",
|
||||
"upload.uploading": "Caricamento in corso...",
|
||||
"upload.success": "File caricato con successo",
|
||||
"upload.error": "Caricamento fallito",
|
||||
"upload.max_size": "Dimensione massima del file: {size}",
|
||||
|
||||
"files.title": "File",
|
||||
"files.no_files": "Nessun file trovato",
|
||||
"files.filename": "Nome del file",
|
||||
"files.document_title": "Titolo del documento",
|
||||
"files.uploaded": "Caricato",
|
||||
"files.file_size": "Dimensione del file",
|
||||
"files.ocr_status": "Stato OCR",
|
||||
"files.tags": "Tag",
|
||||
|
||||
"search.title": "Cerca documenti",
|
||||
"search.placeholder": "Cerca per nome, contenuto, tag...",
|
||||
"search.no_results": "Nessun risultato trovato",
|
||||
"search.results_count": "{count} risultati trovati",
|
||||
|
||||
"settings.title": "Impostazioni",
|
||||
"settings.save_success": "Impostazione salvata con successo",
|
||||
"settings.save_error": "Salvataggio impostazione fallito",
|
||||
"settings.reset_confirm": "Sei sicuro di voler reimpostare questa impostazione?",
|
||||
|
||||
"integrations.title": "Integrazioni",
|
||||
"integrations.connect": "Connetti",
|
||||
"integrations.disconnect": "Disconnetti",
|
||||
"integrations.connected": "Connesso",
|
||||
"integrations.not_connected": "Non connesso",
|
||||
"integrations.configure": "Configura",
|
||||
|
||||
"pipelines.title": "Pipeline di elaborazione",
|
||||
"pipelines.create": "Crea pipeline",
|
||||
"pipelines.edit": "Modifica pipeline",
|
||||
|
||||
"help.title": "Centro assistenza",
|
||||
"help.getting_started": "Per iniziare",
|
||||
"help.faq": "Domande frequenti",
|
||||
"help.documentation": "Documentazione",
|
||||
"help.support": "Supporto",
|
||||
|
||||
"error.not_found": "Pagina non trovata",
|
||||
"error.not_found_message": "La pagina che stai cercando non esiste.",
|
||||
"error.server_error": "Errore interno del server",
|
||||
"error.server_error_message": "Qualcosa è andato storto. Riprova più tardi.",
|
||||
"error.unauthorized": "Non autorizzato",
|
||||
"error.unauthorized_message": "Devi accedere per visualizzare questa pagina.",
|
||||
"error.forbidden": "Vietato",
|
||||
"error.forbidden_message": "Non hai il permesso di accedere a questa pagina.",
|
||||
|
||||
"notifications.title": "Notifiche",
|
||||
"notifications.mark_read": "Segna come letto",
|
||||
"notifications.mark_all_read": "Segna tutto come letto",
|
||||
"notifications.no_notifications": "Nessuna notifica",
|
||||
"notifications.unread_count": "{count} notifiche non lette"
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"app.name": "DocuElevate",
|
||||
|
||||
"nav.dashboard": "Dashboard",
|
||||
"nav.upload": "Uploaden",
|
||||
"nav.files": "Bestanden",
|
||||
"nav.search": "Zoeken",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.integrations": "Integraties",
|
||||
"nav.help": "Help",
|
||||
"nav.notifications": "Meldingen",
|
||||
"nav.pricing": "Prijzen",
|
||||
"nav.about": "Over ons",
|
||||
"nav.admin": "Admin",
|
||||
"nav.settings": "Instellingen",
|
||||
"nav.users": "Gebruikers",
|
||||
"nav.plan_designer": "Planontwerper",
|
||||
"nav.credentials": "Referenties",
|
||||
"nav.file_manager": "Bestandsbeheer",
|
||||
"nav.duplicates": "Duplicaten",
|
||||
"nav.similarity": "Gelijkenis",
|
||||
"nav.queue_monitor": "Wachtrijmonitor",
|
||||
"nav.scheduled_jobs": "Geplande taken",
|
||||
"nav.backup_restore": "Back-up en herstel",
|
||||
"nav.status": "Status",
|
||||
"nav.api_docs": "API-documentatie",
|
||||
"nav.developer_docs": "Ontwikkelaarsdocumentatie",
|
||||
"nav.dark_mode": "Donkere modus",
|
||||
"nav.light_mode": "Lichte modus",
|
||||
"nav.toggle_dark_mode": "Donkere modus schakelen",
|
||||
"nav.toggle_nav": "Navigatiemenu schakelen",
|
||||
"nav.open_main_menu": "Hoofdmenu openen",
|
||||
"nav.skip_to_content": "Ga naar hoofdinhoud",
|
||||
"nav.main_navigation": "Hoofdnavigatie",
|
||||
"nav.admin_menu": "Admin-menu",
|
||||
"nav.admin_actions": "Admin-acties",
|
||||
"nav.help_center": "Helpcentrum",
|
||||
|
||||
"auth.login": "Inloggen",
|
||||
"auth.logout": "Uitloggen",
|
||||
"auth.signup": "Registreren",
|
||||
"auth.my_account": "Mijn account",
|
||||
"auth.profile": "Profiel",
|
||||
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.privacy": "Privacy",
|
||||
"footer.imprint": "Colofon",
|
||||
"footer.terms": "Voorwaarden",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.license": "Licentie",
|
||||
"footer.attributions": "Attributies",
|
||||
"footer.version": "Versie {version}",
|
||||
"footer.navigation": "Voettekstnavigatie",
|
||||
|
||||
"cookie.notice": "DocuElevate gebruikt alleen essentiële sessiecookies die nodig zijn voor authenticatie en werking van de service. Er worden geen tracking- of analysecookies gebruikt.",
|
||||
"cookie.policy_link": "Cookiebeleid",
|
||||
"cookie.privacy_link": "Privacyverklaring",
|
||||
"cookie.accept": "Begrepen",
|
||||
"cookie.notice_label": "Cookiemelding",
|
||||
|
||||
"common.save": "Opslaan",
|
||||
"common.cancel": "Annuleren",
|
||||
"common.delete": "Verwijderen",
|
||||
"common.edit": "Bewerken",
|
||||
"common.close": "Sluiten",
|
||||
"common.confirm": "Bevestigen",
|
||||
"common.back": "Terug",
|
||||
"common.next": "Volgende",
|
||||
"common.loading": "Laden...",
|
||||
"common.error": "Fout",
|
||||
"common.success": "Succes",
|
||||
"common.warning": "Waarschuwing",
|
||||
"common.info": "Info",
|
||||
"common.yes": "Ja",
|
||||
"common.no": "Nee",
|
||||
"common.search": "Zoeken",
|
||||
"common.filter": "Filteren",
|
||||
"common.reset": "Herstellen",
|
||||
"common.refresh": "Vernieuwen",
|
||||
"common.download": "Downloaden",
|
||||
"common.actions": "Acties",
|
||||
"common.details": "Details",
|
||||
"common.name": "Naam",
|
||||
"common.description": "Beschrijving",
|
||||
"common.type": "Type",
|
||||
"common.status": "Status",
|
||||
"common.date": "Datum",
|
||||
"common.size": "Grootte",
|
||||
"common.created": "Aangemaakt",
|
||||
"common.updated": "Bijgewerkt",
|
||||
"common.enabled": "Ingeschakeld",
|
||||
"common.disabled": "Uitgeschakeld",
|
||||
"common.active": "Actief",
|
||||
"common.inactive": "Inactief",
|
||||
"common.all": "Alles",
|
||||
"common.none": "Geen",
|
||||
"common.select": "Selecteren",
|
||||
"common.upload": "Uploaden",
|
||||
"common.processing": "Verwerken",
|
||||
"common.completed": "Voltooid",
|
||||
"common.failed": "Mislukt",
|
||||
"common.pending": "In afwachting",
|
||||
"common.retry": "Opnieuw proberen",
|
||||
"common.view": "Bekijken",
|
||||
"common.copy": "Kopiëren",
|
||||
"common.copied": "Gekopieerd!",
|
||||
|
||||
"language.selector": "Taal",
|
||||
"language.en": "English",
|
||||
"language.de": "Deutsch",
|
||||
"language.fr": "Français",
|
||||
"language.es": "Español",
|
||||
"language.it": "Italiano",
|
||||
"language.pt": "Português",
|
||||
"language.nl": "Nederlands",
|
||||
"language.pl": "Polski",
|
||||
"language.zh": "中文",
|
||||
"language.ru": "Русский",
|
||||
"language.changed": "Taal gewijzigd naar {language}",
|
||||
|
||||
"dashboard.title": "Dashboard",
|
||||
"dashboard.total_files": "Totaal bestanden",
|
||||
"dashboard.files_today": "Bestanden vandaag",
|
||||
"dashboard.files_this_month": "Bestanden deze maand",
|
||||
"dashboard.ocr_processed": "OCR verwerkt",
|
||||
"dashboard.active_integrations": "Actieve integraties",
|
||||
"dashboard.storage_targets": "Opslagdoelen",
|
||||
"dashboard.recent_activity": "Recente activiteit",
|
||||
"dashboard.quick_actions": "Snelle acties",
|
||||
"dashboard.welcome": "Welkom bij DocuElevate",
|
||||
|
||||
"upload.title": "Document uploaden",
|
||||
"upload.drag_drop": "Sleep bestanden hierheen of klik om te bladeren",
|
||||
"upload.select_file": "Bestand selecteren",
|
||||
"upload.uploading": "Uploaden...",
|
||||
"upload.success": "Bestand succesvol geüpload",
|
||||
"upload.error": "Upload mislukt",
|
||||
"upload.max_size": "Maximale bestandsgrootte: {size}",
|
||||
|
||||
"files.title": "Bestanden",
|
||||
"files.no_files": "Geen bestanden gevonden",
|
||||
"files.filename": "Bestandsnaam",
|
||||
"files.document_title": "Documenttitel",
|
||||
"files.uploaded": "Geüpload",
|
||||
"files.file_size": "Bestandsgrootte",
|
||||
"files.ocr_status": "OCR-status",
|
||||
"files.tags": "Tags",
|
||||
|
||||
"search.title": "Documenten zoeken",
|
||||
"search.placeholder": "Zoeken op naam, inhoud, tags...",
|
||||
"search.no_results": "Geen resultaten gevonden",
|
||||
"search.results_count": "{count} resultaten gevonden",
|
||||
|
||||
"settings.title": "Instellingen",
|
||||
"settings.save_success": "Instelling succesvol opgeslagen",
|
||||
"settings.save_error": "Instelling opslaan mislukt",
|
||||
"settings.reset_confirm": "Weet u zeker dat u deze instelling wilt herstellen?",
|
||||
|
||||
"integrations.title": "Integraties",
|
||||
"integrations.connect": "Verbinden",
|
||||
"integrations.disconnect": "Verbreken",
|
||||
"integrations.connected": "Verbonden",
|
||||
"integrations.not_connected": "Niet verbonden",
|
||||
"integrations.configure": "Configureren",
|
||||
|
||||
"pipelines.title": "Verwerkingspipelines",
|
||||
"pipelines.create": "Pipeline maken",
|
||||
"pipelines.edit": "Pipeline bewerken",
|
||||
|
||||
"help.title": "Helpcentrum",
|
||||
"help.getting_started": "Aan de slag",
|
||||
"help.faq": "Veelgestelde vragen",
|
||||
"help.documentation": "Documentatie",
|
||||
"help.support": "Ondersteuning",
|
||||
|
||||
"error.not_found": "Pagina niet gevonden",
|
||||
"error.not_found_message": "De pagina die u zoekt bestaat niet.",
|
||||
"error.server_error": "Interne serverfout",
|
||||
"error.server_error_message": "Er is iets misgegaan. Probeer het later opnieuw.",
|
||||
"error.unauthorized": "Niet geautoriseerd",
|
||||
"error.unauthorized_message": "U moet inloggen om deze pagina te openen.",
|
||||
"error.forbidden": "Verboden",
|
||||
"error.forbidden_message": "U heeft geen toestemming om deze pagina te openen.",
|
||||
|
||||
"notifications.title": "Meldingen",
|
||||
"notifications.mark_read": "Markeren als gelezen",
|
||||
"notifications.mark_all_read": "Alles als gelezen markeren",
|
||||
"notifications.no_notifications": "Geen meldingen",
|
||||
"notifications.unread_count": "{count} ongelezen meldingen"
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"app.name": "DocuElevate",
|
||||
|
||||
"nav.dashboard": "Pulpit",
|
||||
"nav.upload": "Prześlij",
|
||||
"nav.files": "Pliki",
|
||||
"nav.search": "Szukaj",
|
||||
"nav.pipelines": "Potoki",
|
||||
"nav.integrations": "Integracje",
|
||||
"nav.help": "Pomoc",
|
||||
"nav.notifications": "Powiadomienia",
|
||||
"nav.pricing": "Cennik",
|
||||
"nav.about": "O nas",
|
||||
"nav.admin": "Admin",
|
||||
"nav.settings": "Ustawienia",
|
||||
"nav.users": "Użytkownicy",
|
||||
"nav.plan_designer": "Projektant planów",
|
||||
"nav.credentials": "Poświadczenia",
|
||||
"nav.file_manager": "Menedżer plików",
|
||||
"nav.duplicates": "Duplikaty",
|
||||
"nav.similarity": "Podobieństwo",
|
||||
"nav.queue_monitor": "Monitor kolejki",
|
||||
"nav.scheduled_jobs": "Zaplanowane zadania",
|
||||
"nav.backup_restore": "Kopia zapasowa i przywracanie",
|
||||
"nav.status": "Status",
|
||||
"nav.api_docs": "Dokumentacja API",
|
||||
"nav.developer_docs": "Dokumentacja dla programistów",
|
||||
"nav.dark_mode": "Tryb ciemny",
|
||||
"nav.light_mode": "Tryb jasny",
|
||||
"nav.toggle_dark_mode": "Przełącz tryb ciemny",
|
||||
"nav.toggle_nav": "Przełącz menu nawigacji",
|
||||
"nav.open_main_menu": "Otwórz menu główne",
|
||||
"nav.skip_to_content": "Przejdź do treści głównej",
|
||||
"nav.main_navigation": "Nawigacja główna",
|
||||
"nav.admin_menu": "Menu administratora",
|
||||
"nav.admin_actions": "Akcje administratora",
|
||||
"nav.help_center": "Centrum pomocy",
|
||||
|
||||
"auth.login": "Zaloguj się",
|
||||
"auth.logout": "Wyloguj się",
|
||||
"auth.signup": "Zarejestruj się",
|
||||
"auth.my_account": "Moje konto",
|
||||
"auth.profile": "Profil",
|
||||
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.privacy": "Prywatność",
|
||||
"footer.imprint": "Impressum",
|
||||
"footer.terms": "Regulamin",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.license": "Licencja",
|
||||
"footer.attributions": "Atrybuty",
|
||||
"footer.version": "Wersja {version}",
|
||||
"footer.navigation": "Nawigacja stopki",
|
||||
|
||||
"cookie.notice": "DocuElevate używa wyłącznie niezbędnych plików cookie sesji wymaganych do uwierzytelniania i działania usługi. Nie są używane pliki cookie śledzące ani analityczne.",
|
||||
"cookie.policy_link": "Polityka plików cookie",
|
||||
"cookie.privacy_link": "Informacja o prywatności",
|
||||
"cookie.accept": "Rozumiem",
|
||||
"cookie.notice_label": "Informacja o plikach cookie",
|
||||
|
||||
"common.save": "Zapisz",
|
||||
"common.cancel": "Anuluj",
|
||||
"common.delete": "Usuń",
|
||||
"common.edit": "Edytuj",
|
||||
"common.close": "Zamknij",
|
||||
"common.confirm": "Potwierdź",
|
||||
"common.back": "Wstecz",
|
||||
"common.next": "Dalej",
|
||||
"common.loading": "Ładowanie...",
|
||||
"common.error": "Błąd",
|
||||
"common.success": "Sukces",
|
||||
"common.warning": "Ostrzeżenie",
|
||||
"common.info": "Informacja",
|
||||
"common.yes": "Tak",
|
||||
"common.no": "Nie",
|
||||
"common.search": "Szukaj",
|
||||
"common.filter": "Filtruj",
|
||||
"common.reset": "Resetuj",
|
||||
"common.refresh": "Odśwież",
|
||||
"common.download": "Pobierz",
|
||||
"common.actions": "Akcje",
|
||||
"common.details": "Szczegóły",
|
||||
"common.name": "Nazwa",
|
||||
"common.description": "Opis",
|
||||
"common.type": "Typ",
|
||||
"common.status": "Status",
|
||||
"common.date": "Data",
|
||||
"common.size": "Rozmiar",
|
||||
"common.created": "Utworzono",
|
||||
"common.updated": "Zaktualizowano",
|
||||
"common.enabled": "Włączony",
|
||||
"common.disabled": "Wyłączony",
|
||||
"common.active": "Aktywny",
|
||||
"common.inactive": "Nieaktywny",
|
||||
"common.all": "Wszystko",
|
||||
"common.none": "Brak",
|
||||
"common.select": "Wybierz",
|
||||
"common.upload": "Prześlij",
|
||||
"common.processing": "Przetwarzanie",
|
||||
"common.completed": "Zakończono",
|
||||
"common.failed": "Nieudane",
|
||||
"common.pending": "Oczekujące",
|
||||
"common.retry": "Ponów",
|
||||
"common.view": "Wyświetl",
|
||||
"common.copy": "Kopiuj",
|
||||
"common.copied": "Skopiowano!",
|
||||
|
||||
"language.selector": "Język",
|
||||
"language.en": "English",
|
||||
"language.de": "Deutsch",
|
||||
"language.fr": "Français",
|
||||
"language.es": "Español",
|
||||
"language.it": "Italiano",
|
||||
"language.pt": "Português",
|
||||
"language.nl": "Nederlands",
|
||||
"language.pl": "Polski",
|
||||
"language.zh": "中文",
|
||||
"language.ru": "Русский",
|
||||
"language.changed": "Język zmieniony na {language}",
|
||||
|
||||
"dashboard.title": "Pulpit",
|
||||
"dashboard.total_files": "Pliki ogółem",
|
||||
"dashboard.files_today": "Pliki dzisiaj",
|
||||
"dashboard.files_this_month": "Pliki w tym miesiącu",
|
||||
"dashboard.ocr_processed": "OCR przetworzone",
|
||||
"dashboard.active_integrations": "Aktywne integracje",
|
||||
"dashboard.storage_targets": "Cele przechowywania",
|
||||
"dashboard.recent_activity": "Ostatnia aktywność",
|
||||
"dashboard.quick_actions": "Szybkie akcje",
|
||||
"dashboard.welcome": "Witamy w DocuElevate",
|
||||
|
||||
"upload.title": "Prześlij dokument",
|
||||
"upload.drag_drop": "Przeciągnij pliki tutaj lub kliknij, aby przeglądać",
|
||||
"upload.select_file": "Wybierz plik",
|
||||
"upload.uploading": "Przesyłanie...",
|
||||
"upload.success": "Plik przesłany pomyślnie",
|
||||
"upload.error": "Przesyłanie nie powiodło się",
|
||||
"upload.max_size": "Maksymalny rozmiar pliku: {size}",
|
||||
|
||||
"files.title": "Pliki",
|
||||
"files.no_files": "Nie znaleziono plików",
|
||||
"files.filename": "Nazwa pliku",
|
||||
"files.document_title": "Tytuł dokumentu",
|
||||
"files.uploaded": "Przesłano",
|
||||
"files.file_size": "Rozmiar pliku",
|
||||
"files.ocr_status": "Status OCR",
|
||||
"files.tags": "Tagi",
|
||||
|
||||
"search.title": "Szukaj dokumentów",
|
||||
"search.placeholder": "Szukaj wg nazwy, treści, tagów...",
|
||||
"search.no_results": "Nie znaleziono wyników",
|
||||
"search.results_count": "Znaleziono {count} wyników",
|
||||
|
||||
"settings.title": "Ustawienia",
|
||||
"settings.save_success": "Ustawienie zapisane pomyślnie",
|
||||
"settings.save_error": "Nie udało się zapisać ustawienia",
|
||||
"settings.reset_confirm": "Czy na pewno chcesz zresetować to ustawienie?",
|
||||
|
||||
"integrations.title": "Integracje",
|
||||
"integrations.connect": "Połącz",
|
||||
"integrations.disconnect": "Rozłącz",
|
||||
"integrations.connected": "Połączono",
|
||||
"integrations.not_connected": "Nie połączono",
|
||||
"integrations.configure": "Konfiguruj",
|
||||
|
||||
"pipelines.title": "Potoki przetwarzania",
|
||||
"pipelines.create": "Utwórz potok",
|
||||
"pipelines.edit": "Edytuj potok",
|
||||
|
||||
"help.title": "Centrum pomocy",
|
||||
"help.getting_started": "Pierwsze kroki",
|
||||
"help.faq": "Często zadawane pytania",
|
||||
"help.documentation": "Dokumentacja",
|
||||
"help.support": "Wsparcie",
|
||||
|
||||
"error.not_found": "Nie znaleziono strony",
|
||||
"error.not_found_message": "Szukana strona nie istnieje.",
|
||||
"error.server_error": "Wewnętrzny błąd serwera",
|
||||
"error.server_error_message": "Coś poszło nie tak. Spróbuj ponownie później.",
|
||||
"error.unauthorized": "Brak autoryzacji",
|
||||
"error.unauthorized_message": "Musisz się zalogować, aby uzyskać dostęp do tej strony.",
|
||||
"error.forbidden": "Zabroniono",
|
||||
"error.forbidden_message": "Nie masz uprawnień do dostępu do tej strony.",
|
||||
|
||||
"notifications.title": "Powiadomienia",
|
||||
"notifications.mark_read": "Oznacz jako przeczytane",
|
||||
"notifications.mark_all_read": "Oznacz wszystkie jako przeczytane",
|
||||
"notifications.no_notifications": "Brak powiadomień",
|
||||
"notifications.unread_count": "{count} nieprzeczytanych powiadomień"
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"app.name": "DocuElevate",
|
||||
|
||||
"nav.dashboard": "Painel",
|
||||
"nav.upload": "Carregar",
|
||||
"nav.files": "Ficheiros",
|
||||
"nav.search": "Pesquisar",
|
||||
"nav.pipelines": "Pipelines",
|
||||
"nav.integrations": "Integrações",
|
||||
"nav.help": "Ajuda",
|
||||
"nav.notifications": "Notificações",
|
||||
"nav.pricing": "Preços",
|
||||
"nav.about": "Sobre",
|
||||
"nav.admin": "Admin",
|
||||
"nav.settings": "Definições",
|
||||
"nav.users": "Utilizadores",
|
||||
"nav.plan_designer": "Designer de planos",
|
||||
"nav.credentials": "Credenciais",
|
||||
"nav.file_manager": "Gestor de ficheiros",
|
||||
"nav.duplicates": "Duplicados",
|
||||
"nav.similarity": "Similaridade",
|
||||
"nav.queue_monitor": "Monitor de fila",
|
||||
"nav.scheduled_jobs": "Tarefas agendadas",
|
||||
"nav.backup_restore": "Cópia de segurança e restauro",
|
||||
"nav.status": "Estado",
|
||||
"nav.api_docs": "Documentação API",
|
||||
"nav.developer_docs": "Documentação para programadores",
|
||||
"nav.dark_mode": "Modo escuro",
|
||||
"nav.light_mode": "Modo claro",
|
||||
"nav.toggle_dark_mode": "Alternar modo escuro",
|
||||
"nav.toggle_nav": "Alternar menu de navegação",
|
||||
"nav.open_main_menu": "Abrir menu principal",
|
||||
"nav.skip_to_content": "Ir para o conteúdo principal",
|
||||
"nav.main_navigation": "Navegação principal",
|
||||
"nav.admin_menu": "Menu de administração",
|
||||
"nav.admin_actions": "Ações de administração",
|
||||
"nav.help_center": "Centro de ajuda",
|
||||
|
||||
"auth.login": "Iniciar sessão",
|
||||
"auth.logout": "Terminar sessão",
|
||||
"auth.signup": "Registar",
|
||||
"auth.my_account": "A minha conta",
|
||||
"auth.profile": "Perfil",
|
||||
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.privacy": "Privacidade",
|
||||
"footer.imprint": "Aviso legal",
|
||||
"footer.terms": "Termos",
|
||||
"footer.cookies": "Cookies",
|
||||
"footer.license": "Licença",
|
||||
"footer.attributions": "Atribuições",
|
||||
"footer.version": "Versão {version}",
|
||||
"footer.navigation": "Navegação do rodapé",
|
||||
|
||||
"cookie.notice": "O DocuElevate utiliza apenas cookies de sessão essenciais necessários para a autenticação e o funcionamento do serviço. Não são utilizados cookies de rastreamento ou analíticos.",
|
||||
"cookie.policy_link": "Política de cookies",
|
||||
"cookie.privacy_link": "Aviso de privacidade",
|
||||
"cookie.accept": "Entendido",
|
||||
"cookie.notice_label": "Aviso de cookies",
|
||||
|
||||
"common.save": "Guardar",
|
||||
"common.cancel": "Cancelar",
|
||||
"common.delete": "Eliminar",
|
||||
"common.edit": "Editar",
|
||||
"common.close": "Fechar",
|
||||
"common.confirm": "Confirmar",
|
||||
"common.back": "Voltar",
|
||||
"common.next": "Seguinte",
|
||||
"common.loading": "A carregar...",
|
||||
"common.error": "Erro",
|
||||
"common.success": "Sucesso",
|
||||
"common.warning": "Aviso",
|
||||
"common.info": "Informação",
|
||||
"common.yes": "Sim",
|
||||
"common.no": "Não",
|
||||
"common.search": "Pesquisar",
|
||||
"common.filter": "Filtrar",
|
||||
"common.reset": "Repor",
|
||||
"common.refresh": "Atualizar",
|
||||
"common.download": "Descarregar",
|
||||
"common.actions": "Ações",
|
||||
"common.details": "Detalhes",
|
||||
"common.name": "Nome",
|
||||
"common.description": "Descrição",
|
||||
"common.type": "Tipo",
|
||||
"common.status": "Estado",
|
||||
"common.date": "Data",
|
||||
"common.size": "Tamanho",
|
||||
"common.created": "Criado",
|
||||
"common.updated": "Atualizado",
|
||||
"common.enabled": "Ativado",
|
||||
"common.disabled": "Desativado",
|
||||
"common.active": "Ativo",
|
||||
"common.inactive": "Inativo",
|
||||
"common.all": "Tudo",
|
||||
"common.none": "Nenhum",
|
||||
"common.select": "Selecionar",
|
||||
"common.upload": "Carregar",
|
||||
"common.processing": "A processar",
|
||||
"common.completed": "Concluído",
|
||||
"common.failed": "Falhado",
|
||||
"common.pending": "Pendente",
|
||||
"common.retry": "Tentar novamente",
|
||||
"common.view": "Ver",
|
||||
"common.copy": "Copiar",
|
||||
"common.copied": "Copiado!",
|
||||
|
||||
"language.selector": "Idioma",
|
||||
"language.en": "English",
|
||||
"language.de": "Deutsch",
|
||||
"language.fr": "Français",
|
||||
"language.es": "Español",
|
||||
"language.it": "Italiano",
|
||||
"language.pt": "Português",
|
||||
"language.nl": "Nederlands",
|
||||
"language.pl": "Polski",
|
||||
"language.zh": "中文",
|
||||
"language.ru": "Русский",
|
||||
"language.changed": "Idioma alterado para {language}",
|
||||
|
||||
"dashboard.title": "Painel",
|
||||
"dashboard.total_files": "Total de ficheiros",
|
||||
"dashboard.files_today": "Ficheiros hoje",
|
||||
"dashboard.files_this_month": "Ficheiros este mês",
|
||||
"dashboard.ocr_processed": "OCR processados",
|
||||
"dashboard.active_integrations": "Integrações ativas",
|
||||
"dashboard.storage_targets": "Destinos de armazenamento",
|
||||
"dashboard.recent_activity": "Atividade recente",
|
||||
"dashboard.quick_actions": "Ações rápidas",
|
||||
"dashboard.welcome": "Bem-vindo ao DocuElevate",
|
||||
|
||||
"upload.title": "Carregar documento",
|
||||
"upload.drag_drop": "Arraste ficheiros para aqui ou clique para procurar",
|
||||
"upload.select_file": "Selecionar ficheiro",
|
||||
"upload.uploading": "A carregar...",
|
||||
"upload.success": "Ficheiro carregado com sucesso",
|
||||
"upload.error": "Falha ao carregar",
|
||||
"upload.max_size": "Tamanho máximo do ficheiro: {size}",
|
||||
|
||||
"files.title": "Ficheiros",
|
||||
"files.no_files": "Nenhum ficheiro encontrado",
|
||||
"files.filename": "Nome do ficheiro",
|
||||
"files.document_title": "Título do documento",
|
||||
"files.uploaded": "Carregado",
|
||||
"files.file_size": "Tamanho do ficheiro",
|
||||
"files.ocr_status": "Estado OCR",
|
||||
"files.tags": "Etiquetas",
|
||||
|
||||
"search.title": "Pesquisar documentos",
|
||||
"search.placeholder": "Pesquisar por nome, conteúdo, etiquetas...",
|
||||
"search.no_results": "Nenhum resultado encontrado",
|
||||
"search.results_count": "{count} resultados encontrados",
|
||||
|
||||
"settings.title": "Definições",
|
||||
"settings.save_success": "Definição guardada com sucesso",
|
||||
"settings.save_error": "Falha ao guardar definição",
|
||||
"settings.reset_confirm": "Tem a certeza de que pretende repor esta definição?",
|
||||
|
||||
"integrations.title": "Integrações",
|
||||
"integrations.connect": "Ligar",
|
||||
"integrations.disconnect": "Desligar",
|
||||
"integrations.connected": "Ligado",
|
||||
"integrations.not_connected": "Não ligado",
|
||||
"integrations.configure": "Configurar",
|
||||
|
||||
"pipelines.title": "Pipelines de processamento",
|
||||
"pipelines.create": "Criar pipeline",
|
||||
"pipelines.edit": "Editar pipeline",
|
||||
|
||||
"help.title": "Centro de ajuda",
|
||||
"help.getting_started": "Primeiros passos",
|
||||
"help.faq": "Perguntas frequentes",
|
||||
"help.documentation": "Documentação",
|
||||
"help.support": "Suporte",
|
||||
|
||||
"error.not_found": "Página não encontrada",
|
||||
"error.not_found_message": "A página que procura não existe.",
|
||||
"error.server_error": "Erro interno do servidor",
|
||||
"error.server_error_message": "Algo correu mal. Tente novamente mais tarde.",
|
||||
"error.unauthorized": "Não autorizado",
|
||||
"error.unauthorized_message": "Precisa de iniciar sessão para aceder a esta página.",
|
||||
"error.forbidden": "Proibido",
|
||||
"error.forbidden_message": "Não tem permissão para aceder a esta página.",
|
||||
|
||||
"notifications.title": "Notificações",
|
||||
"notifications.mark_read": "Marcar como lida",
|
||||
"notifications.mark_all_read": "Marcar todas como lidas",
|
||||
"notifications.no_notifications": "Sem notificações",
|
||||
"notifications.unread_count": "{count} notificações por ler"
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"app.name": "DocuElevate",
|
||||
|
||||
"nav.dashboard": "Панель управления",
|
||||
"nav.upload": "Загрузить",
|
||||
"nav.files": "Файлы",
|
||||
"nav.search": "Поиск",
|
||||
"nav.pipelines": "Конвейеры",
|
||||
"nav.integrations": "Интеграции",
|
||||
"nav.help": "Помощь",
|
||||
"nav.notifications": "Уведомления",
|
||||
"nav.pricing": "Цены",
|
||||
"nav.about": "О нас",
|
||||
"nav.admin": "Админ",
|
||||
"nav.settings": "Настройки",
|
||||
"nav.users": "Пользователи",
|
||||
"nav.plan_designer": "Конструктор планов",
|
||||
"nav.credentials": "Учётные данные",
|
||||
"nav.file_manager": "Менеджер файлов",
|
||||
"nav.duplicates": "Дубликаты",
|
||||
"nav.similarity": "Сходство",
|
||||
"nav.queue_monitor": "Монитор очереди",
|
||||
"nav.scheduled_jobs": "Запланированные задачи",
|
||||
"nav.backup_restore": "Резервное копирование и восстановление",
|
||||
"nav.status": "Статус",
|
||||
"nav.api_docs": "Документация API",
|
||||
"nav.developer_docs": "Документация для разработчиков",
|
||||
"nav.dark_mode": "Тёмная тема",
|
||||
"nav.light_mode": "Светлая тема",
|
||||
"nav.toggle_dark_mode": "Переключить тёмную тему",
|
||||
"nav.toggle_nav": "Переключить меню навигации",
|
||||
"nav.open_main_menu": "Открыть главное меню",
|
||||
"nav.skip_to_content": "Перейти к основному содержанию",
|
||||
"nav.main_navigation": "Основная навигация",
|
||||
"nav.admin_menu": "Меню администратора",
|
||||
"nav.admin_actions": "Действия администратора",
|
||||
"nav.help_center": "Центр помощи",
|
||||
|
||||
"auth.login": "Войти",
|
||||
"auth.logout": "Выйти",
|
||||
"auth.signup": "Регистрация",
|
||||
"auth.my_account": "Мой аккаунт",
|
||||
"auth.profile": "Профиль",
|
||||
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.privacy": "Конфиденциальность",
|
||||
"footer.imprint": "Выходные данные",
|
||||
"footer.terms": "Условия",
|
||||
"footer.cookies": "Файлы cookie",
|
||||
"footer.license": "Лицензия",
|
||||
"footer.attributions": "Атрибуции",
|
||||
"footer.version": "Версия {version}",
|
||||
"footer.navigation": "Навигация подвала",
|
||||
|
||||
"cookie.notice": "DocuElevate использует только необходимые сессионные файлы cookie для аутентификации и работы сервиса. Файлы cookie для отслеживания и аналитики не используются.",
|
||||
"cookie.policy_link": "Политика файлов cookie",
|
||||
"cookie.privacy_link": "Уведомление о конфиденциальности",
|
||||
"cookie.accept": "Понятно",
|
||||
"cookie.notice_label": "Уведомление о файлах cookie",
|
||||
|
||||
"common.save": "Сохранить",
|
||||
"common.cancel": "Отмена",
|
||||
"common.delete": "Удалить",
|
||||
"common.edit": "Редактировать",
|
||||
"common.close": "Закрыть",
|
||||
"common.confirm": "Подтвердить",
|
||||
"common.back": "Назад",
|
||||
"common.next": "Далее",
|
||||
"common.loading": "Загрузка...",
|
||||
"common.error": "Ошибка",
|
||||
"common.success": "Успешно",
|
||||
"common.warning": "Предупреждение",
|
||||
"common.info": "Информация",
|
||||
"common.yes": "Да",
|
||||
"common.no": "Нет",
|
||||
"common.search": "Поиск",
|
||||
"common.filter": "Фильтр",
|
||||
"common.reset": "Сбросить",
|
||||
"common.refresh": "Обновить",
|
||||
"common.download": "Скачать",
|
||||
"common.actions": "Действия",
|
||||
"common.details": "Подробности",
|
||||
"common.name": "Название",
|
||||
"common.description": "Описание",
|
||||
"common.type": "Тип",
|
||||
"common.status": "Статус",
|
||||
"common.date": "Дата",
|
||||
"common.size": "Размер",
|
||||
"common.created": "Создано",
|
||||
"common.updated": "Обновлено",
|
||||
"common.enabled": "Включено",
|
||||
"common.disabled": "Отключено",
|
||||
"common.active": "Активно",
|
||||
"common.inactive": "Неактивно",
|
||||
"common.all": "Все",
|
||||
"common.none": "Нет",
|
||||
"common.select": "Выбрать",
|
||||
"common.upload": "Загрузить",
|
||||
"common.processing": "Обработка",
|
||||
"common.completed": "Завершено",
|
||||
"common.failed": "Ошибка",
|
||||
"common.pending": "В ожидании",
|
||||
"common.retry": "Повторить",
|
||||
"common.view": "Просмотр",
|
||||
"common.copy": "Копировать",
|
||||
"common.copied": "Скопировано!",
|
||||
|
||||
"language.selector": "Язык",
|
||||
"language.en": "English",
|
||||
"language.de": "Deutsch",
|
||||
"language.fr": "Français",
|
||||
"language.es": "Español",
|
||||
"language.it": "Italiano",
|
||||
"language.pt": "Português",
|
||||
"language.nl": "Nederlands",
|
||||
"language.pl": "Polski",
|
||||
"language.zh": "中文",
|
||||
"language.ru": "Русский",
|
||||
"language.changed": "Язык изменён на {language}",
|
||||
|
||||
"dashboard.title": "Панель управления",
|
||||
"dashboard.total_files": "Всего файлов",
|
||||
"dashboard.files_today": "Файлы сегодня",
|
||||
"dashboard.files_this_month": "Файлы за месяц",
|
||||
"dashboard.ocr_processed": "OCR обработано",
|
||||
"dashboard.active_integrations": "Активные интеграции",
|
||||
"dashboard.storage_targets": "Хранилища",
|
||||
"dashboard.recent_activity": "Последняя активность",
|
||||
"dashboard.quick_actions": "Быстрые действия",
|
||||
"dashboard.welcome": "Добро пожаловать в DocuElevate",
|
||||
|
||||
"upload.title": "Загрузить документ",
|
||||
"upload.drag_drop": "Перетащите файлы сюда или нажмите для выбора",
|
||||
"upload.select_file": "Выбрать файл",
|
||||
"upload.uploading": "Загрузка...",
|
||||
"upload.success": "Файл успешно загружен",
|
||||
"upload.error": "Ошибка загрузки",
|
||||
"upload.max_size": "Максимальный размер файла: {size}",
|
||||
|
||||
"files.title": "Файлы",
|
||||
"files.no_files": "Файлы не найдены",
|
||||
"files.filename": "Имя файла",
|
||||
"files.document_title": "Название документа",
|
||||
"files.uploaded": "Загружено",
|
||||
"files.file_size": "Размер файла",
|
||||
"files.ocr_status": "Статус OCR",
|
||||
"files.tags": "Теги",
|
||||
|
||||
"search.title": "Поиск документов",
|
||||
"search.placeholder": "Поиск по имени, содержимому, тегам...",
|
||||
"search.no_results": "Результаты не найдены",
|
||||
"search.results_count": "Найдено результатов: {count}",
|
||||
|
||||
"settings.title": "Настройки",
|
||||
"settings.save_success": "Настройка сохранена",
|
||||
"settings.save_error": "Не удалось сохранить настройку",
|
||||
"settings.reset_confirm": "Вы уверены, что хотите сбросить эту настройку?",
|
||||
|
||||
"integrations.title": "Интеграции",
|
||||
"integrations.connect": "Подключить",
|
||||
"integrations.disconnect": "Отключить",
|
||||
"integrations.connected": "Подключено",
|
||||
"integrations.not_connected": "Не подключено",
|
||||
"integrations.configure": "Настроить",
|
||||
|
||||
"pipelines.title": "Конвейеры обработки",
|
||||
"pipelines.create": "Создать конвейер",
|
||||
"pipelines.edit": "Редактировать конвейер",
|
||||
|
||||
"help.title": "Центр помощи",
|
||||
"help.getting_started": "Начало работы",
|
||||
"help.faq": "Часто задаваемые вопросы",
|
||||
"help.documentation": "Документация",
|
||||
"help.support": "Поддержка",
|
||||
|
||||
"error.not_found": "Страница не найдена",
|
||||
"error.not_found_message": "Запрашиваемая страница не существует.",
|
||||
"error.server_error": "Внутренняя ошибка сервера",
|
||||
"error.server_error_message": "Что-то пошло не так. Пожалуйста, попробуйте позже.",
|
||||
"error.unauthorized": "Не авторизован",
|
||||
"error.unauthorized_message": "Для доступа к этой странице необходимо войти в систему.",
|
||||
"error.forbidden": "Доступ запрещён",
|
||||
"error.forbidden_message": "У вас нет прав для доступа к этой странице.",
|
||||
|
||||
"notifications.title": "Уведомления",
|
||||
"notifications.mark_read": "Отметить как прочитанное",
|
||||
"notifications.mark_all_read": "Отметить все как прочитанные",
|
||||
"notifications.no_notifications": "Нет уведомлений",
|
||||
"notifications.unread_count": "{count} непрочитанных уведомлений"
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"app.name": "DocuElevate",
|
||||
|
||||
"nav.dashboard": "仪表盘",
|
||||
"nav.upload": "上传",
|
||||
"nav.files": "文件",
|
||||
"nav.search": "搜索",
|
||||
"nav.pipelines": "处理流程",
|
||||
"nav.integrations": "集成",
|
||||
"nav.help": "帮助",
|
||||
"nav.notifications": "通知",
|
||||
"nav.pricing": "价格",
|
||||
"nav.about": "关于",
|
||||
"nav.admin": "管理",
|
||||
"nav.settings": "设置",
|
||||
"nav.users": "用户",
|
||||
"nav.plan_designer": "方案设计",
|
||||
"nav.credentials": "凭据",
|
||||
"nav.file_manager": "文件管理器",
|
||||
"nav.duplicates": "重复文件",
|
||||
"nav.similarity": "相似度",
|
||||
"nav.queue_monitor": "队列监控",
|
||||
"nav.scheduled_jobs": "计划任务",
|
||||
"nav.backup_restore": "备份与恢复",
|
||||
"nav.status": "状态",
|
||||
"nav.api_docs": "API 文档",
|
||||
"nav.developer_docs": "开发者文档",
|
||||
"nav.dark_mode": "深色模式",
|
||||
"nav.light_mode": "浅色模式",
|
||||
"nav.toggle_dark_mode": "切换深色模式",
|
||||
"nav.toggle_nav": "切换导航菜单",
|
||||
"nav.open_main_menu": "打开主菜单",
|
||||
"nav.skip_to_content": "跳至主要内容",
|
||||
"nav.main_navigation": "主导航",
|
||||
"nav.admin_menu": "管理菜单",
|
||||
"nav.admin_actions": "管理操作",
|
||||
"nav.help_center": "帮助中心",
|
||||
|
||||
"auth.login": "登录",
|
||||
"auth.logout": "退出",
|
||||
"auth.signup": "注册",
|
||||
"auth.my_account": "我的账户",
|
||||
"auth.profile": "个人资料",
|
||||
|
||||
"footer.copyright": "DocuElevate {year}",
|
||||
"footer.privacy": "隐私",
|
||||
"footer.imprint": "法律声明",
|
||||
"footer.terms": "条款",
|
||||
"footer.cookies": "Cookie",
|
||||
"footer.license": "许可",
|
||||
"footer.attributions": "致谢",
|
||||
"footer.version": "版本 {version}",
|
||||
"footer.navigation": "页脚导航",
|
||||
|
||||
"cookie.notice": "DocuElevate 仅使用身份验证和服务运行所需的基本会话 Cookie。不使用任何跟踪或分析 Cookie。",
|
||||
"cookie.policy_link": "Cookie 政策",
|
||||
"cookie.privacy_link": "隐私声明",
|
||||
"cookie.accept": "我知道了",
|
||||
"cookie.notice_label": "Cookie 通知",
|
||||
|
||||
"common.save": "保存",
|
||||
"common.cancel": "取消",
|
||||
"common.delete": "删除",
|
||||
"common.edit": "编辑",
|
||||
"common.close": "关闭",
|
||||
"common.confirm": "确认",
|
||||
"common.back": "返回",
|
||||
"common.next": "下一步",
|
||||
"common.loading": "加载中...",
|
||||
"common.error": "错误",
|
||||
"common.success": "成功",
|
||||
"common.warning": "警告",
|
||||
"common.info": "信息",
|
||||
"common.yes": "是",
|
||||
"common.no": "否",
|
||||
"common.search": "搜索",
|
||||
"common.filter": "筛选",
|
||||
"common.reset": "重置",
|
||||
"common.refresh": "刷新",
|
||||
"common.download": "下载",
|
||||
"common.actions": "操作",
|
||||
"common.details": "详情",
|
||||
"common.name": "名称",
|
||||
"common.description": "描述",
|
||||
"common.type": "类型",
|
||||
"common.status": "状态",
|
||||
"common.date": "日期",
|
||||
"common.size": "大小",
|
||||
"common.created": "创建时间",
|
||||
"common.updated": "更新时间",
|
||||
"common.enabled": "已启用",
|
||||
"common.disabled": "已禁用",
|
||||
"common.active": "活跃",
|
||||
"common.inactive": "不活跃",
|
||||
"common.all": "全部",
|
||||
"common.none": "无",
|
||||
"common.select": "选择",
|
||||
"common.upload": "上传",
|
||||
"common.processing": "处理中",
|
||||
"common.completed": "已完成",
|
||||
"common.failed": "失败",
|
||||
"common.pending": "待处理",
|
||||
"common.retry": "重试",
|
||||
"common.view": "查看",
|
||||
"common.copy": "复制",
|
||||
"common.copied": "已复制!",
|
||||
|
||||
"language.selector": "语言",
|
||||
"language.en": "English",
|
||||
"language.de": "Deutsch",
|
||||
"language.fr": "Français",
|
||||
"language.es": "Español",
|
||||
"language.it": "Italiano",
|
||||
"language.pt": "Português",
|
||||
"language.nl": "Nederlands",
|
||||
"language.pl": "Polski",
|
||||
"language.zh": "中文",
|
||||
"language.ru": "Русский",
|
||||
"language.changed": "语言已更改为{language}",
|
||||
|
||||
"dashboard.title": "仪表盘",
|
||||
"dashboard.total_files": "文件总数",
|
||||
"dashboard.files_today": "今日文件",
|
||||
"dashboard.files_this_month": "本月文件",
|
||||
"dashboard.ocr_processed": "OCR 已处理",
|
||||
"dashboard.active_integrations": "活跃集成",
|
||||
"dashboard.storage_targets": "存储目标",
|
||||
"dashboard.recent_activity": "最近活动",
|
||||
"dashboard.quick_actions": "快捷操作",
|
||||
"dashboard.welcome": "欢迎使用 DocuElevate",
|
||||
|
||||
"upload.title": "上传文档",
|
||||
"upload.drag_drop": "将文件拖放到此处或点击浏览",
|
||||
"upload.select_file": "选择文件",
|
||||
"upload.uploading": "上传中...",
|
||||
"upload.success": "文件上传成功",
|
||||
"upload.error": "上传失败",
|
||||
"upload.max_size": "最大文件大小:{size}",
|
||||
|
||||
"files.title": "文件",
|
||||
"files.no_files": "未找到文件",
|
||||
"files.filename": "文件名",
|
||||
"files.document_title": "文档标题",
|
||||
"files.uploaded": "已上传",
|
||||
"files.file_size": "文件大小",
|
||||
"files.ocr_status": "OCR 状态",
|
||||
"files.tags": "标签",
|
||||
|
||||
"search.title": "搜索文档",
|
||||
"search.placeholder": "按文件名、内容、标签搜索...",
|
||||
"search.no_results": "未找到结果",
|
||||
"search.results_count": "找到 {count} 个结果",
|
||||
|
||||
"settings.title": "设置",
|
||||
"settings.save_success": "设置保存成功",
|
||||
"settings.save_error": "设置保存失败",
|
||||
"settings.reset_confirm": "确定要重置此设置吗?",
|
||||
|
||||
"integrations.title": "集成",
|
||||
"integrations.connect": "连接",
|
||||
"integrations.disconnect": "断开",
|
||||
"integrations.connected": "已连接",
|
||||
"integrations.not_connected": "未连接",
|
||||
"integrations.configure": "配置",
|
||||
|
||||
"pipelines.title": "处理流程",
|
||||
"pipelines.create": "创建流程",
|
||||
"pipelines.edit": "编辑流程",
|
||||
|
||||
"help.title": "帮助中心",
|
||||
"help.getting_started": "入门指南",
|
||||
"help.faq": "常见问题",
|
||||
"help.documentation": "文档",
|
||||
"help.support": "支持",
|
||||
|
||||
"error.not_found": "页面未找到",
|
||||
"error.not_found_message": "您要查找的页面不存在。",
|
||||
"error.server_error": "服务器内部错误",
|
||||
"error.server_error_message": "出了点问题,请稍后再试。",
|
||||
"error.unauthorized": "未授权",
|
||||
"error.unauthorized_message": "您需要登录才能访问此页面。",
|
||||
"error.forbidden": "禁止访问",
|
||||
"error.forbidden_message": "您没有权限访问此页面。",
|
||||
|
||||
"notifications.title": "通知",
|
||||
"notifications.mark_read": "标记为已读",
|
||||
"notifications.mark_all_read": "全部标记为已读",
|
||||
"notifications.no_notifications": "没有通知",
|
||||
"notifications.unread_count": "{count} 条未读通知"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Add preferred_language column to user_profiles for i18n support.
|
||||
|
||||
Revision ID: 029_add_user_language_preference
|
||||
Revises: 028_add_audit_logs
|
||||
Create Date: 2026-03-09
|
||||
"""
|
||||
|
||||
from typing import Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "029_add_user_language_preference"
|
||||
down_revision: Union[str, None] = "028_add_audit_logs"
|
||||
depends_on: Union[str, None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Add preferred_language column to user_profiles table."""
|
||||
op.add_column(
|
||||
"user_profiles",
|
||||
sa.Column("preferred_language", sa.String(10), nullable=True, server_default=None),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Remove preferred_language column from user_profiles table."""
|
||||
op.drop_column("user_profiles", "preferred_language")
|
||||
@@ -0,0 +1,415 @@
|
||||
"""Tests for the i18n (internationalization) and l10n (localization) utilities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.utils.i18n import (
|
||||
DEFAULT_LANGUAGE,
|
||||
SUPPORTED_LANGUAGE_CODES,
|
||||
SUPPORTED_LANGUAGES,
|
||||
_parse_accept_language,
|
||||
detect_language,
|
||||
format_date,
|
||||
format_datetime,
|
||||
format_number,
|
||||
get_language_info,
|
||||
reload_translations,
|
||||
translate,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Translation file integrity
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestTranslationFiles:
|
||||
"""Verify that all translation JSON files are valid and complete."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_cache(self) -> None:
|
||||
"""Clear translation cache before each test."""
|
||||
reload_translations()
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_all_translation_files_exist(self) -> None:
|
||||
"""Every supported language must have a corresponding JSON file."""
|
||||
translations_dir = Path(__file__).resolve().parent.parent / "frontend" / "translations"
|
||||
for lang in SUPPORTED_LANGUAGES:
|
||||
filepath = translations_dir / f"{lang['code']}.json"
|
||||
assert filepath.is_file(), f"Missing translation file for {lang['code']}"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_all_translation_files_are_valid_json(self) -> None:
|
||||
"""All translation files must be parseable JSON."""
|
||||
translations_dir = Path(__file__).resolve().parent.parent / "frontend" / "translations"
|
||||
for lang in SUPPORTED_LANGUAGES:
|
||||
filepath = translations_dir / f"{lang['code']}.json"
|
||||
data = json.loads(filepath.read_text(encoding="utf-8"))
|
||||
assert isinstance(data, dict), f"{lang['code']}.json must be a dict"
|
||||
assert len(data) > 0, f"{lang['code']}.json must not be empty"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_all_languages_have_same_keys(self) -> None:
|
||||
"""All translation files should have the same set of keys as English."""
|
||||
translations_dir = Path(__file__).resolve().parent.parent / "frontend" / "translations"
|
||||
en_path = translations_dir / "en.json"
|
||||
en_keys = set(json.loads(en_path.read_text(encoding="utf-8")).keys())
|
||||
|
||||
for lang in SUPPORTED_LANGUAGES:
|
||||
if lang["code"] == "en":
|
||||
continue
|
||||
filepath = translations_dir / f"{lang['code']}.json"
|
||||
lang_keys = set(json.loads(filepath.read_text(encoding="utf-8")).keys())
|
||||
missing = en_keys - lang_keys
|
||||
assert not missing, f"{lang['code']}.json missing keys: {missing}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Core translate() function
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestTranslate:
|
||||
"""Tests for the translate() function."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_cache(self) -> None:
|
||||
reload_translations()
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_translate_english_key(self) -> None:
|
||||
"""English keys should resolve to English text."""
|
||||
result = translate("nav.dashboard", "en")
|
||||
assert result == "Dashboard"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_translate_german_key(self) -> None:
|
||||
"""German locale should return German text."""
|
||||
result = translate("nav.dashboard", "de")
|
||||
assert result == "Übersicht"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_translate_french_key(self) -> None:
|
||||
"""French locale should return French text."""
|
||||
result = translate("nav.dashboard", "fr")
|
||||
assert result == "Tableau de bord"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_translate_chinese_key(self) -> None:
|
||||
"""Chinese locale should return Chinese text."""
|
||||
result = translate("nav.dashboard", "zh")
|
||||
assert result == "仪表盘"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_translate_fallback_to_english(self) -> None:
|
||||
"""Unknown locale falls back to English."""
|
||||
result = translate("nav.dashboard", "xx")
|
||||
assert result == "Dashboard"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_translate_missing_key_returns_key(self) -> None:
|
||||
"""Missing key falls back to the key itself."""
|
||||
result = translate("nonexistent.key", "en")
|
||||
assert result == "nonexistent.key"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_translate_none_locale_uses_default(self) -> None:
|
||||
"""None locale defaults to English."""
|
||||
result = translate("nav.dashboard", None)
|
||||
assert result == "Dashboard"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_translate_with_kwargs(self) -> None:
|
||||
"""Placeholders should be interpolated via kwargs."""
|
||||
result = translate("footer.copyright", "en", year="2025")
|
||||
assert result == "DocuElevate 2025"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_translate_with_kwargs_german(self) -> None:
|
||||
"""Placeholder interpolation in German."""
|
||||
result = translate("language.changed", "de", language="English")
|
||||
assert result == "Sprache geändert zu English"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Accept-Language header parsing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestParseAcceptLanguage:
|
||||
"""Tests for parsing the Accept-Language HTTP header."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_simple_language(self) -> None:
|
||||
assert _parse_accept_language("de") == "de"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_language_with_region(self) -> None:
|
||||
assert _parse_accept_language("de-DE") == "de"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_multiple_languages_quality(self) -> None:
|
||||
result = _parse_accept_language("fr;q=0.9, de;q=1.0, en;q=0.8")
|
||||
assert result == "de"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_unsupported_language_fallback(self) -> None:
|
||||
result = _parse_accept_language("ja, ko")
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_empty_header(self) -> None:
|
||||
assert _parse_accept_language("") is None
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_complex_accept_language(self) -> None:
|
||||
header = "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7"
|
||||
result = _parse_accept_language(header)
|
||||
assert result == "zh"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Language detection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestDetectLanguage:
|
||||
"""Tests for detecting language from request context."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_session_preference_takes_priority(self) -> None:
|
||||
request = MagicMock()
|
||||
request.session = {"preferred_language": "de"}
|
||||
request.cookies = {}
|
||||
request.headers = {}
|
||||
assert detect_language(request) == "de"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_cookie_fallback(self) -> None:
|
||||
request = MagicMock()
|
||||
request.session = {}
|
||||
request.cookies = {"docuelevate_lang": "fr"}
|
||||
request.headers = {}
|
||||
assert detect_language(request) == "fr"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_accept_language_fallback(self) -> None:
|
||||
request = MagicMock()
|
||||
request.session = {}
|
||||
request.cookies = {}
|
||||
request.headers = {"accept-language": "es-ES,es;q=0.9"}
|
||||
assert detect_language(request) == "es"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_default_fallback(self) -> None:
|
||||
request = MagicMock()
|
||||
request.session = {}
|
||||
request.cookies = {}
|
||||
request.headers = {}
|
||||
assert detect_language(request) == DEFAULT_LANGUAGE
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_invalid_session_language_ignored(self) -> None:
|
||||
request = MagicMock()
|
||||
request.session = {"preferred_language": "invalid"}
|
||||
request.cookies = {"docuelevate_lang": "it"}
|
||||
request.headers = {}
|
||||
assert detect_language(request) == "it"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Localization helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestL10nFormatters:
|
||||
"""Tests for locale-aware formatting functions."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_date_english(self) -> None:
|
||||
d = date(2025, 3, 15)
|
||||
result = format_date(d, "en")
|
||||
assert "March" in result
|
||||
assert "15" in result
|
||||
assert "2025" in result
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_date_german(self) -> None:
|
||||
d = date(2025, 3, 15)
|
||||
result = format_date(d, "de")
|
||||
assert "15." in result
|
||||
assert "2025" in result
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_date_short(self) -> None:
|
||||
d = date(2025, 3, 15)
|
||||
result = format_date(d, "en", short=True)
|
||||
assert result == "03/15/2025"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_date_short_german(self) -> None:
|
||||
d = date(2025, 3, 15)
|
||||
result = format_date(d, "de", short=True)
|
||||
assert result == "15.03.2025"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_date_none(self) -> None:
|
||||
assert format_date(None) == ""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_datetime_none(self) -> None:
|
||||
assert format_datetime(None) == ""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_number_english(self) -> None:
|
||||
result = format_number(1234567, "en")
|
||||
assert result == "1,234,567"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_number_german(self) -> None:
|
||||
result = format_number(1234567, "de")
|
||||
assert result == "1.234.567"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_number_float_english(self) -> None:
|
||||
result = format_number(1234.56, "en")
|
||||
assert result == "1,234.56"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_number_float_german(self) -> None:
|
||||
result = format_number(1234.56, "de")
|
||||
assert result == "1.234,56"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_datetime_chinese(self) -> None:
|
||||
dt = datetime(2025, 3, 15, 14, 30)
|
||||
result = format_datetime(dt, "zh")
|
||||
assert "2025" in result
|
||||
assert "03" in result
|
||||
assert "15" in result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_language_info()
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetLanguageInfo:
|
||||
"""Tests for get_language_info() utility."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_known_language(self) -> None:
|
||||
info = get_language_info("de")
|
||||
assert info is not None
|
||||
assert info["name"] == "German"
|
||||
assert info["native"] == "Deutsch"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_unknown_language(self) -> None:
|
||||
assert get_language_info("xx") is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SUPPORTED_LANGUAGES metadata
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSupportedLanguages:
|
||||
"""Tests for language metadata constants."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_ten_languages_supported(self) -> None:
|
||||
assert len(SUPPORTED_LANGUAGES) == 10
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_supported_codes_set(self) -> None:
|
||||
expected = {"en", "de", "fr", "es", "it", "pt", "nl", "pl", "zh", "ru"}
|
||||
assert SUPPORTED_LANGUAGE_CODES == expected
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_default_language_is_english(self) -> None:
|
||||
assert DEFAULT_LANGUAGE == "en"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# API endpoint tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestI18nAPI:
|
||||
"""Tests for the i18n API endpoints."""
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_list_languages(self, client: TestClient) -> None:
|
||||
"""GET /api/i18n/languages should return all supported languages."""
|
||||
response = client.get("/api/i18n/languages")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "languages" in data
|
||||
assert len(data["languages"]) == 10
|
||||
assert data["default"] == "en"
|
||||
# Verify each language has required fields
|
||||
for lang in data["languages"]:
|
||||
assert "code" in lang
|
||||
assert "name" in lang
|
||||
assert "native" in lang
|
||||
assert "flag" in lang
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_set_language(self, client: TestClient) -> None:
|
||||
"""POST /api/i18n/language should set language preference."""
|
||||
response = client.post(
|
||||
"/api/i18n/language",
|
||||
json={"language": "de"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["language"] == "de"
|
||||
# Verify cookie was set
|
||||
assert "docuelevate_lang" in response.cookies
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_set_language_invalid_falls_back_to_default(self, client: TestClient) -> None:
|
||||
"""Invalid language code should fall back to default."""
|
||||
response = client.post(
|
||||
"/api/i18n/language",
|
||||
json={"language": "invalid"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["language"] == "en"
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_set_language_persists_in_cookie(self, client: TestClient) -> None:
|
||||
"""Language setting should be persisted in a cookie."""
|
||||
client.post("/api/i18n/language", json={"language": "fr"})
|
||||
# Subsequent requests should detect the language from cookie
|
||||
response = client.get("/api/i18n/languages")
|
||||
data = response.json()
|
||||
assert data["current"] == "fr"
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_base_html_uses_current_locale(self, client: TestClient) -> None:
|
||||
"""The base template should set lang attribute to current locale."""
|
||||
# Set language to German
|
||||
client.post("/api/i18n/language", json={"language": "de"})
|
||||
# Load homepage
|
||||
response = client.get("/", follow_redirects=True)
|
||||
assert response.status_code == 200
|
||||
# The lang attribute should reflect the locale
|
||||
assert 'lang="de"' in response.text or 'lang="en"' in response.text
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_language_selector_in_nav(self, client: TestClient) -> None:
|
||||
"""The navigation should contain the language selector globe icon."""
|
||||
response = client.get("/", follow_redirects=True)
|
||||
if response.status_code == 200:
|
||||
assert "fa-globe" in response.text
|
||||
assert "setLanguage" in response.text
|
||||
Reference in New Issue
Block a user