From 95f1798908bb3e669b34bf207c06b9ec66d4cd1b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 9 Mar 2026 23:04:14 +0000
Subject: [PATCH 1/5] Initial plan
From ff76855f29343dae3eefe402a2130144c1a4291e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 9 Mar 2026 23:28:02 +0000
Subject: [PATCH 2/5] feat(i18n): add internationalization framework with 10
languages
- Create i18n utility module (app/utils/i18n.py) with translation loading,
browser language detection, AI fallback, and l10n helpers
- Add JSON translation files for EN, DE, FR, ES, IT, PT, NL, PL, ZH, RU
- Add preferred_language column to UserProfile model with migration
- Register _() translation function as Jinja2 global
- Update base.html with translated navigation, footer, cookie notice
- Add language selector dropdown in nav bar (desktop + mobile)
- Create API endpoints for language preference (POST/GET /api/i18n/)
- Support language detection: user profile > cookie > Accept-Language > default
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
---
app/api/__init__.py | 2 +
app/api/i18n.py | 136 +++++++
app/models.py | 4 +
app/utils/i18n.py | 367 ++++++++++++++++++
app/views/base.py | 41 ++
frontend/templates/base.html | 216 +++++++----
frontend/translations/de.json | 190 +++++++++
frontend/translations/en.json | 190 +++++++++
frontend/translations/es.json | 190 +++++++++
frontend/translations/fr.json | 190 +++++++++
frontend/translations/it.json | 190 +++++++++
frontend/translations/nl.json | 190 +++++++++
frontend/translations/pl.json | 190 +++++++++
frontend/translations/pt.json | 190 +++++++++
frontend/translations/ru.json | 190 +++++++++
frontend/translations/zh.json | 190 +++++++++
.../027_add_user_language_preference.py | 28 ++
17 files changed, 2618 insertions(+), 76 deletions(-)
create mode 100644 app/api/i18n.py
create mode 100644 app/utils/i18n.py
create mode 100644 frontend/translations/de.json
create mode 100644 frontend/translations/en.json
create mode 100644 frontend/translations/es.json
create mode 100644 frontend/translations/fr.json
create mode 100644 frontend/translations/it.json
create mode 100644 frontend/translations/nl.json
create mode 100644 frontend/translations/pl.json
create mode 100644 frontend/translations/pt.json
create mode 100644 frontend/translations/ru.json
create mode 100644 frontend/translations/zh.json
create mode 100644 migrations/versions/027_add_user_language_preference.py
diff --git a/app/api/__init__.py b/app/api/__init__.py
index ae98cbd7..aec13465 100644
--- a/app/api/__init__.py
+++ b/app/api/__init__.py
@@ -17,6 +17,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
@@ -82,3 +83,4 @@ router.include_router(imap_accounts_router)
router.include_router(integrations_router)
router.include_router(notifications_router)
router.include_router(scheduled_jobs_router)
+router.include_router(i18n_router)
diff --git a/app/api/i18n.py b/app/api/i18n.py
new file mode 100644
index 00000000..8c59ad60
--- /dev/null
+++ b/app/api/i18n.py
@@ -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="/api/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(
+ (l["native"] for l in SUPPORTED_LANGUAGES if l["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)
diff --git a/app/models.py b/app/models.py
index 0cc7b53a..3a1703b5 100644
--- a/app/models.py
+++ b/app/models.py
@@ -256,6 +256,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())
diff --git a/app/utils/i18n.py b/app/utils/i18n.py
new file mode 100644
index 00000000..c6cd9c27
--- /dev/null
+++ b/app/utils/i18n.py
@@ -0,0 +1,367 @@
+"""Internationalization (i18n) and localization (l10n) utilities.
+
+Provides a JSON-based translation system for the DocuElevate UI with:
+
+* **10 supported languages** (EN, DE, FR, ES, IT, PT, NL, PL, ZH, RU)
+* 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]] = [
+ {"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": "🇵🇹"},
+ {"code": "nl", "name": "Dutch", "native": "Nederlands", "flag": "🇳🇱"},
+ {"code": "pl", "name": "Polish", "native": "Polski", "flag": "🇵🇱"},
+ {"code": "zh", "name": "Chinese", "native": "中文", "flag": "🇨🇳"},
+ {"code": "ru", "name": "Russian", "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 part in header.split(","):
+ part = 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": ",",
+ },
+ "pl": {
+ "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
diff --git a/app/views/base.py b/app/views/base.py
index 010d4f21..fc9d5e0d 100644
--- a/app/views/base.py
+++ b/app/views/base.py
@@ -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):
diff --git a/frontend/templates/base.html b/frontend/templates/base.html
index e4bb200a..1ccdff9f 100644
--- a/frontend/templates/base.html
+++ b/frontend/templates/base.html
@@ -1,5 +1,5 @@
-
+
{% block title %}DocuElevate{% endblock %}
@@ -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
+ {{ _("nav.skip_to_content") }}
-