1 Commits

Author SHA1 Message Date
7c57f939c0 feat(static): update and optimizate 2026-04-24 14:12:14 +03:00
26 changed files with 280 additions and 87 deletions

View File

@@ -1,11 +1,15 @@
FROM python:3.12 AS builder FROM python:3.12 AS builder
ENV POETRY_HOME="/opt/poetry" ENV POETRY_HOME="/opt/poetry"
ENV PATH="$POETRY_HOME/bin:$PATH" ENV PATH="$POETRY_HOME/bin:$PATH"
RUN apt update && \
apt install -y gettext
WORKDIR /app WORKDIR /app
RUN curl -sSL https://install.python-poetry.org | python3 - RUN curl -sSL https://install.python-poetry.org | python3 -
COPY pyproject.toml poetry.lock README.md ./ COPY pyproject.toml poetry.lock README.md ./
RUN poetry config virtualenvs.in-project true RUN poetry config virtualenvs.in-project true
RUN poetry install --with app --no-root RUN poetry install --with app --no-root
COPY locales ./locales
RUN cd locales/ru/LC_MESSAGES && msgfmt messages.po
FROM node:24 AS node-builder FROM node:24 AS node-builder
ENV PATH=/app/node_modules/.bin:$PATH ENV PATH=/app/node_modules/.bin:$PATH
@@ -18,16 +22,12 @@ RUN npm run build
FROM python:3.12-slim FROM python:3.12-slim
ENV PATH="/app/.venv/bin:$PATH" ENV PATH="/app/.venv/bin:$PATH"
WORKDIR /app WORKDIR /app
RUN apt update && \
apt install -y locales gettext && \
sed -i -e 's/# ru_RU.UTF-8 UTF-8/ru_RU.UTF-8 UTF-8/' /etc/locale.gen && \
dpkg-reconfigure --frontend=noninteractive locales
ENV LANG=ru_RU.UTF-8
ENV LC_ALL=ru_RU.UTF-8
ENV TZ="Europe/Moscow" ENV TZ="Europe/Moscow"
COPY --from=builder /app ./ COPY --from=builder /app ./
COPY --from=node-builder /app/dist ./static/dist COPY --from=node-builder /app/dist ./static/dist
COPY gallery gallery/ COPY gallery gallery/
RUN cd gallery/easel/route/view/locales/ru/LC_MESSAGES && msgfmt messages.po #COPY --from=builder /app/gallery/easel/route/view/locales /app/gallery/easel/route/view/locales
COPY --from=builder --parents locales/**/*.mo ./
CMD ["uvicorn", "gallery.main:app", "--host", "0.0.0.0", "--port", "80", "--log-config", "gallery/logging.yaml"] CMD ["uvicorn", "gallery.main:app", "--host", "0.0.0.0", "--port", "80", "--log-config", "gallery/logging.yaml"]

View File

@@ -1,5 +1,3 @@
import locale as _locale
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
@@ -9,11 +7,8 @@ from gallery.util import root_path
from .route import api, doc from .route import api, doc
from .route.view import router as view_router from .route.view import router as view_router
DEFAULT_LOCALE = "ru_RU.UTF-8"
def build_app(api_bundle: ApiBundle) -> FastAPI:
def build_app(api_bundle: ApiBundle, *, locale: str = DEFAULT_LOCALE) -> FastAPI:
_locale.setlocale(_locale.LC_TIME, locale)
app = FastAPI( app = FastAPI(
title="Gallery", title="Gallery",
docs_url=None, docs_url=None,

View File

@@ -32,10 +32,10 @@
type="button" type="button"
aria-expanded="false" aria-expanded="false"
data-bs-toggle="dropdown" data-bs-toggle="dropdown"
aria-label="Select language (default)"> aria-label="{{_('Select language')}} (default)">
<span class="me-2 language-icon-active">🇬🇧</span> <span class="fi fi-gb me-2 language-icon-active"></span>
<span class="d-lg-none ms-2" <span class="d-lg-none ms-2"
id="bd-language-text">Select language</span> id="bd-language-text">{{_("Select language")}}</span>
</button> </button>
<ul class="dropdown-menu dropdown-menu-end" <ul class="dropdown-menu dropdown-menu-end"
aria-labelledby="bd-language-text"> aria-labelledby="bd-language-text">
@@ -44,8 +44,8 @@
class="dropdown-item d-flex align-items-center" class="dropdown-item d-flex align-items-center"
data-bs-language-value="en" data-bs-language-value="en"
aria-pressed="false"> aria-pressed="false">
<span class="me-2 language-icon">🇬🇧</span> <span class="fi fi-gb me-2 language-icon-active"></span>
English {{_("English")}}
</button> </button>
</li> </li>
<li> <li>
@@ -53,8 +53,8 @@
class="dropdown-item d-flex align-items-center" class="dropdown-item d-flex align-items-center"
data-bs-language-value="ru" data-bs-language-value="ru"
aria-pressed="false"> aria-pressed="false">
<span class="me-2 language-icon">🇷🇺</span> <span class="fi fi-ru me-2 language-icon-active"></span>
Russian {{_("Russian")}}
</button> </button>
</li> </li>
</ul> </ul>
@@ -66,9 +66,9 @@
aria-expanded="false" aria-expanded="false"
data-bs-toggle="dropdown" data-bs-toggle="dropdown"
aria-label="Toggle theme (auto)"> aria-label="Toggle theme (auto)">
<i class="bi me-2 opacity-50 theme-icon-active"></i> <span class="bi bi-circle-half me-2 opacity-50 theme-icon-active"></span>
<span class="d-lg-none ms-2" <span class="d-lg-none ms-2"
id="bd-theme-text">Toggle theme</span> id="bd-theme-text">{{_("Toggle theme")}}</span>
</button> </button>
<ul class="dropdown-menu dropdown-menu-end" <ul class="dropdown-menu dropdown-menu-end"
aria-labelledby="bd-theme-text"> aria-labelledby="bd-theme-text">
@@ -77,8 +77,8 @@
class="dropdown-item d-flex align-items-center" class="dropdown-item d-flex align-items-center"
data-bs-theme-value="light" data-bs-theme-value="light"
aria-pressed="false"> aria-pressed="false">
<i class="bi bi-sun-fill me-2 opacity-50 theme-icon"></i> <span class="bi bi-sun-fill me-2 opacity-50 theme-icon"></span>
Light {{_("Light")}}
</button> </button>
</li> </li>
<li> <li>
@@ -86,8 +86,8 @@
class="dropdown-item d-flex align-items-center" class="dropdown-item d-flex align-items-center"
data-bs-theme-value="dark" data-bs-theme-value="dark"
aria-pressed="false"> aria-pressed="false">
<i class="bi bi-moon-stars-fill me-2 opacity-50 theme-icon"></i> <span class="bi bi-moon-stars-fill me-2 opacity-50 theme-icon"></span>
Dark {{_("Dark")}}
</button> </button>
</li> </li>
<li> <li>
@@ -95,8 +95,8 @@
class="dropdown-item d-flex align-items-center active" class="dropdown-item d-flex align-items-center active"
data-bs-theme-value="auto" data-bs-theme-value="auto"
aria-pressed="true"> aria-pressed="true">
<i class="bi bi-circle-half me-2 opacity-50 theme-icon"></i> <span class="bi bi-circle-half me-2 opacity-50 theme-icon"></span>
Auto {{_("Auto")}}
</button> </button>
</li> </li>
</ul> </ul>

View File

@@ -1,11 +1,11 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Index{% endblock %} {% block title %}{{_("Index")}}{% endblock %}
{% block head %} {% block head %}
{{ super() }} {{ super() }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h1>View</h1> <h1>{{_("View")}}</h1>
<div class="list-group mb-5"> <div class="list-group mb-5">
{% for section in sections %} {% for section in sections %}
<a href="{{section.link}}" <a href="{{section.link}}"
@@ -18,7 +18,7 @@
{% endfor %} {% endfor %}
</div> </div>
<hr class="col-3 col-md-2 mb-5"> <hr class="col-3 col-md-2 mb-5">
<h1>Docs</h1> <h1>{{_("Docs")}}</h1>
<a href="/docs" <a href="/docs"
target="_blank"> target="_blank">
<h4>Swagger</h4> <h4>Swagger</h4>

View File

@@ -1,15 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Gallery\n"
"Last-Translator: shmyga <shmyga.z@gmail.com>\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
msgid "Weather"
msgstr "Погода"
msgid "TV program"
msgstr "Телепрограмма"

View File

@@ -1,6 +1,7 @@
import datetime import datetime
from pathlib import Path from pathlib import Path
from babel.dates import format_date
from fastapi import APIRouter from fastapi import APIRouter
from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
@@ -20,7 +21,13 @@ templates = Jinja2Templates(
base_dir / "templates", base_dir / "templates",
] ]
) )
templates.env.globals.update({"_": _}) templates.env.globals.update(
{
"_": _,
"version": __version__,
"format_date": format_date,
}
)
templates.env.filters["timedelta_format"] = timedelta_format templates.env.filters["timedelta_format"] = timedelta_format
router = APIRouter() router = APIRouter()

View File

@@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %} {% block title %}
Программа | {{response.channel.name}} | {{response.date.strftime('%a, %d %B %Y')}} {{_("TV program")}} | {{response.channel.name}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}
{% endblock %} {% endblock %}
{% block header %} {% block header %}
@@ -15,7 +15,7 @@
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a> href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a>
<a class="button" <a class="button"
href="../..">⬆️</a> href="../..">⬆️</a>
<span>{{response.channel.name}} | {{response.date.strftime('%a, %d %B %Y')}}</span> <span>{{response.channel.name}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}</span>
<a class="button" <a class="button"
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a> href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
</h4> </h4>

View File

@@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %} {% block title %}
{{'Прямые трансляции' if live else _("TV program")}} | {{response.date.strftime('%a, %d %B %Y')}} {{_("Live broadcasts") if live else _("TV program")}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}
{% endblock %} {% endblock %}
{% block header %} {% block header %}
@@ -15,7 +15,7 @@
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a> href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a>
<a class="button" <a class="button"
href="..">⬆️</a> href="..">⬆️</a>
<span>{{'Прямые трансляции' if live else 'Программа'}} | {{response.date.strftime('%a, %d %B %Y')}}</span> <span>{{_("Live broadcasts") if live else _("TV program")}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}</span>
<a class="button" <a class="button"
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a> href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
</h4> </h4>
@@ -36,8 +36,6 @@
<td colspan="3"> <td colspan="3">
<div>{{response.channel.name}}</div> <div>{{response.channel.name}}</div>
</td> </td>
<td></td>
<td></td>
</tr> </tr>
{% for value in values %} {% for value in values %}
<tr class="{{'table-success' if not live and value.live else ''}}"> <tr class="{{'table-success' if not live and value.live else ''}}">

View File

@@ -1,9 +1,10 @@
import gettext import gettext
from contextvars import ContextVar from contextvars import ContextVar
from pathlib import Path
from fastapi import Cookie, Header, Request from fastapi import Cookie, Header, Request
from gallery.util import root_path
_translation: ContextVar[gettext.GNUTranslations | gettext.NullTranslations] = ( _translation: ContextVar[gettext.GNUTranslations | gettext.NullTranslations] = (
ContextVar("translation") ContextVar("translation")
) )
@@ -19,7 +20,7 @@ async def set_language(
try: try:
t = gettext.translation( t = gettext.translation(
"messages", localedir=Path(__file__).parent / "locales", languages=[lang] "messages", localedir=root_path / "locales", languages=[lang]
) )
except FileNotFoundError: except FileNotFoundError:
t = gettext.NullTranslations() t = gettext.NullTranslations()

View File

@@ -1,11 +1,10 @@
import datetime import datetime
from pathlib import Path from pathlib import Path
from fastapi import APIRouter, FastAPI from fastapi import APIRouter
from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from babel.dates import format_date
from gallery.easel.core import AppRequest from gallery.easel.core import AppRequest
from gallery.sketch.weather.model import WeatherResponse from gallery.sketch.weather.model import WeatherResponse
from gallery.version import __version__ from gallery.version import __version__
@@ -22,7 +21,13 @@ templates = Jinja2Templates(
base_dir / "templates", base_dir / "templates",
] ]
) )
templates.env.globals.update({"_": _}) templates.env.globals.update(
{
"_": _,
"version": __version__,
"format_date": format_date,
}
)
templates.env.filters["wind_direction_icon"] = wind_direction_icon templates.env.filters["wind_direction_icon"] = wind_direction_icon
templates.env.filters["cloudness_icon"] = cloudness_icon templates.env.filters["cloudness_icon"] = cloudness_icon
@@ -32,15 +37,16 @@ def build_weather_response(request: AppRequest, response: WeatherResponse):
request=request, request=request,
name="weather.html", name="weather.html",
context={ context={
"version": __version__,
"tag_util": TagUtil, "tag_util": TagUtil,
"datetime": datetime, "datetime": datetime,
"response": response, "response": response,
}, },
) )
router = APIRouter() router = APIRouter()
@router.get("/weather", response_class=HTMLResponse) @router.get("/weather", response_class=HTMLResponse)
async def get_weather_index(request: AppRequest, query: str | None = None): async def get_weather_index(request: AppRequest, query: str | None = None):
weather_api = request.app.state.api.weather weather_api = request.app.state.api.weather
@@ -49,7 +55,6 @@ async def get_weather_index(request: AppRequest, query: str | None = None):
request=request, request=request,
name="index.html", name="index.html",
context={ context={
"version": __version__,
"locations": locations, "locations": locations,
}, },
) )

View File

@@ -1,8 +1,8 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Weather{% endblock %} {% block title %}{{_("Weather")}}{% endblock %}
{% block content %} {% block content %}
<h1>Weather</h1> <h1>{{_("Weather")}}</h1>
<form action="" <form action=""
method="get" method="get"
class="mb-4"> class="mb-4">
@@ -11,9 +11,9 @@
class="form-control" class="form-control"
id="query" id="query"
name="query" name="query"
placeholder="Enter the city name"> placeholder="{{_('Enter the city name')}}">
<button class="btn btn-primary" <button class="btn btn-primary"
type="submit">Search</button> type="submit">{{_("Search")}}</button>
</div> </div>
</form> </form>
<ul id="locations" <ul id="locations"
@@ -22,6 +22,7 @@
<a href="weather/{{location.id}}" <a href="weather/{{location.id}}"
class="list-group-item list-group-item-action px-4" class="list-group-item list-group-item-action px-4"
onclick="saveLocation({id:'{{location.id}}', name:'{{location.name}}'});"> onclick="saveLocation({id:'{{location.id}}', name:'{{location.name}}'});">
<span class="fi fi-{{location.country_code}} me-1"></span>
<span class="text-primary">{{location.name}}</span> <span class="text-primary">{{location.name}}</span>
<span class="small ms-1 text-secondary"> <span class="small ms-1 text-secondary">
{{location.country}}, {{location.district}}, {{location.subdistrict}} {{location.country}}, {{location.district}}, {{location.subdistrict}}

View File

@@ -13,12 +13,12 @@
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a> href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a>
<a class="button" <a class="button"
href="../tag/days-10">⬆️</a> href="../tag/days-10">⬆️</a>
<span>{{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</span> <span>{{response.location}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}</span>
<a class="button" <a class="button"
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a> href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
{% endif %} {% endif %}
{% if response.period == 'days' %} {% if response.period == 'days' %}
<span>{{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</span> <span>{{response.location}} | {{format_date(response.date, 'E, d MMMM Y', locale=request.state.language)}}</span>
{% endif %} {% endif %}
</h4> </h4>
<div class="table-responsive"> <div class="table-responsive">
@@ -36,9 +36,9 @@
{% endif %} {% endif %}
{% if response.period == 'days' %} {% if response.period == 'days' %}
<td class="date {{'now' if value.date.date() == datetime.date.today() else ''}}"> <td class="date {{'now' if value.date.date() == datetime.date.today() else ''}}">
<span class="value"> <span class="value {{'text-danger' if value.date.weekday() in [5,6] else ''}}">
<a href="../tag/{{tag_util.create_tag('day', value.date.date())}}"> <a href="../tag/{{tag_util.create_tag('day', value.date.date())}}">
{{value.date.strftime('%a %d')}} {{format_date(value.date, 'E d', locale=request.state.language)}}
</a> </a>
</span> </span>
</td> </td>
@@ -49,7 +49,7 @@
<tr> <tr>
<td colspan="{{response.values | length}}" <td colspan="{{response.values | length}}"
class="header"> class="header">
Облачность {{_("Cloudiness")}}
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -65,7 +65,7 @@
<tr> <tr>
<td colspan="{{response.values | length}}" <td colspan="{{response.values | length}}"
class="header"> class="header">
Температура, °C {{_("Temperature, °C")}}
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -84,7 +84,7 @@
<tr> <tr>
<td colspan="{{response.values | length}}" <td colspan="{{response.values | length}}"
class="header"> class="header">
Направление ветра {{_("Wind direction")}}
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -98,7 +98,7 @@
<tr> <tr>
<td colspan="{{response.values | length}}" <td colspan="{{response.values | length}}"
class="header"> class="header">
Скорость ветра, м/с {{_("Wind speed, m/s")}}
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -118,7 +118,7 @@
<tr> <tr>
<td colspan="{{response.values | length}}" <td colspan="{{response.values | length}}"
class="header"> class="header">
Осадки, мм {{_("Precipitation, mm")}}
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -133,7 +133,7 @@
<tr> <tr>
<td colspan="{{response.values | length}}" <td colspan="{{response.values | length}}"
class="header"> class="header">
Давление, мм рт. ст. {{_("Pressure, mmHg")}}
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -151,7 +151,7 @@
<tr> <tr>
<td colspan="{{response.values | length}}" <td colspan="{{response.values | length}}"
class="header"> class="header">
Влажность, % {{_("Humidity, %")}}
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@@ -87,6 +87,7 @@ class GismeteoApi(WeatherApi):
lat=item["coordinates"]["latitude"], lat=item["coordinates"]["latitude"],
lon=item["coordinates"]["longitude"], lon=item["coordinates"]["longitude"],
country=item["translations"]["kk"]["country"]["name"], country=item["translations"]["kk"]["country"]["name"],
country_code=item["country"]["code"].lower(),
district=item["translations"]["kk"]["district"]["name"], district=item["translations"]["kk"]["district"]["name"],
subdistrict=( subdistrict=(
item["translations"]["kk"]["subdistrict"]["name"] item["translations"]["kk"]["subdistrict"]["name"]

View File

@@ -15,6 +15,7 @@ class Location(Model):
lat: float lat: float
lon: float lon: float
country: str country: str
country_code: str
district: str district: str
subdistrict: str subdistrict: str

View File

@@ -0,0 +1,77 @@
msgid ""
msgstr ""
"Project-Id-Version: Gallery\n"
"Last-Translator: shmyga <shmyga.z@gmail.com>\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
msgid "Index"
msgstr "Содержание"
msgid "View"
msgstr "Просмотр"
msgid "Docs"
msgstr "Документация"
msgid "Toggle theme"
msgstr "Переключить тему"
msgid "Light"
msgstr "Светлая"
msgid "Dark"
msgstr "Тёмная"
msgid "Auto"
msgstr "Авто"
msgid "Select language"
msgstr "Выберите язык"
msgid "English"
msgstr "Английский"
msgid "Russian"
msgstr "Русский"
# weather
msgid "Weather"
msgstr "Погода"
msgid "Enter the city name"
msgstr "Введите название города"
msgid "Search"
msgstr "Поиск"
msgid "Cloudiness"
msgstr "Облачность"
msgid "Temperature, °C"
msgstr "Температура, °C"
msgid "Wind direction"
msgstr "Направление ветра"
msgid "Wind speed, m/s"
msgstr "Скорость ветра, м/с"
msgid "Precipitation, mm"
msgstr "Осадки, мм"
msgid "Pressure, mmHg"
msgstr "Давление, мм рт. ст."
msgid "Humidity, %"
msgstr "Влажность, %"
# tv
msgid "TV program"
msgstr "Телепрограмма"
msgid "Live broadcasts"
msgstr "Прямые трансляции"

17
poetry.lock generated
View File

@@ -196,6 +196,21 @@ tests = ["attrs[tests-no-zope]", "zope-interface"]
tests-mypy = ["mypy (>=1.6) ; platform_python_implementation == \"CPython\" and python_version >= \"3.8\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.8\""] tests-mypy = ["mypy (>=1.6) ; platform_python_implementation == \"CPython\" and python_version >= \"3.8\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.8\""]
tests-no-zope = ["attrs[tests-mypy]", "cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] tests-no-zope = ["attrs[tests-mypy]", "cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"]
[[package]]
name = "babel"
version = "2.18.0"
description = "Internationalization utilities"
optional = false
python-versions = ">=3.8"
groups = ["app"]
files = [
{file = "babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35"},
{file = "babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d"},
]
[package.extras]
dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""]
[[package]] [[package]]
name = "beautifulsoup4" name = "beautifulsoup4"
version = "4.12.3" version = "4.12.3"
@@ -1898,4 +1913,4 @@ multidict = ">=4.0"
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = "^3.12" python-versions = "^3.12"
content-hash = "7295e9ec7f7492017c5bbda489026f19bbf155f0ea82402d348b0aa4c03beaca" content-hash = "39107ded0683832d1ca3d637dc50fac7dd441375ead30cdb56d64edee13c97e2"

View File

@@ -17,6 +17,7 @@ aiocache = {extras = ["redis"], version = "^0.12.2"}
[tool.poetry.group.app.dependencies] [tool.poetry.group.app.dependencies]
fastapi = "^0.111.1" fastapi = "^0.111.1"
jinja2 = "^3.1.4" jinja2 = "^3.1.4"
babel = "^2.18.0"
[tool.poetry.group.test.dependencies] [tool.poetry.group.test.dependencies]
pytest = "^8.3.1" pytest = "^8.3.1"

View File

@@ -2,5 +2,5 @@
set -e set -e
cd "$(dirname $(dirname "$0"))" || exit cd "$(dirname $(dirname "$0"))" || exit
cd gallery/easel/route/view/locales/ru/LC_MESSAGES || exit cd locales/ru/LC_MESSAGES || exit
msgfmt messages.po msgfmt messages.po

View File

@@ -1,17 +1,18 @@
{ {
"name": "gallery-static", "name": "gallery",
"version": "0.2.1", "version": "0.2.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "gallery-static", "name": "gallery",
"version": "0.2.1", "version": "0.2.1",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1", "bootstrap-icons": "^1.13.1",
"flag-icons": "^7.5.0",
"sass": "^1.99.0" "sass": "^1.99.0"
}, },
"devDependencies": { "devDependencies": {
@@ -699,6 +700,12 @@
} }
} }
}, },
"node_modules/flag-icons": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-7.5.0.tgz",
"integrity": "sha512-kd+MNXviFIg5hijH766tt+3x76ele1AXlo4zDdCxIvqWZhKt4T83bOtxUOOMlTx/EcFdUMH5yvQgYlFh1EqqFg==",
"license": "MIT"
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",

View File

@@ -12,6 +12,7 @@
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1", "bootstrap-icons": "^1.13.1",
"flag-icons": "^7.5.0",
"sass": "^1.99.0" "sass": "^1.99.0"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -7,7 +7,7 @@ class AppLinkElement extends HTMLElement {
<a href="${this.getAttribute("href")}" <a href="${this.getAttribute("href")}"
class="d-flex align-items-center text-body text-decoration-none"> class="d-flex align-items-center text-body text-decoration-none">
<span class="fs-4"> <span class="fs-4">
<i class="bi bi-${this.getAttribute("icon")}"></i> <span class="bi bi-${this.getAttribute("icon")} me-1"></span>
<span>${this.textContent}</span> <span>${this.textContent}</span>
</span> </span>
</a>`; </a>`;

View File

@@ -28,7 +28,7 @@
const languageSwitcherText = document.querySelector("#bd-language-text"); const languageSwitcherText = document.querySelector("#bd-language-text");
const activeLanguageIcon = document.querySelector(".language-icon-active"); const activeLanguageIcon = document.querySelector(".language-icon-active");
const btnToActive = document.querySelector(`[data-bs-language-value="${language}"]`); const btnToActive = document.querySelector(`[data-bs-language-value="${language}"]`);
const activeLanguageIconContent = btnToActive?.querySelector("span")?.textContent; const activeLanguageIconClass = btnToActive.querySelector(".fi").className.match(/fi-[\w-]+/)[0];
document.querySelectorAll("[data-bs-language-value]").forEach((element) => { document.querySelectorAll("[data-bs-language-value]").forEach((element) => {
element.classList.remove("active"); element.classList.remove("active");
@@ -37,7 +37,9 @@
btnToActive.classList.add("active"); btnToActive.classList.add("active");
btnToActive.setAttribute("aria-pressed", "true"); btnToActive.setAttribute("aria-pressed", "true");
activeLanguageIcon.textContent = activeLanguageIconContent; const classesToRemove = Array.from(activeLanguageIcon.classList).filter((className) => className.startsWith("fi-"));
activeLanguageIcon.classList.remove(...classesToRemove);
activeLanguageIcon.classList.add(activeLanguageIconClass);
const languageSwitcherLabel = `${languageSwitcherText.textContent} (${btnToActive.dataset.bsLanguageValue})`; const languageSwitcherLabel = `${languageSwitcherText.textContent} (${btnToActive.dataset.bsLanguageValue})`;
languageSwitcher.setAttribute("aria-label", languageSwitcherLabel); languageSwitcher.setAttribute("aria-label", languageSwitcherLabel);

40
static/src/lib/bootstrap-icons.scss vendored Normal file
View File

@@ -0,0 +1,40 @@
//$bootstrap-icons: "circle-half", "moon-stars-fill", "brightness-high", "gear", "sun-fill", "tv";
.bi {
display: inline-block;
text-transform: none;
line-height: 1;
vertical-align: -0.125em;
width: 1em;
height: 1em;
mask-size: contain;
mask-position: 50%;
mask-repeat: no-repeat;
background-color: currentColor;
//@each $icon in $bootstrap-icons {
// &.bi-#{$icon} {
// mask-image: url("bootstrap-icons/icons/#{$icon}.svg");
// }
///
&.bi-circle-half {
mask-image: url(bootstrap-icons/icons/circle-half.svg);
}
&.bi-moon-stars-fill {
mask-image: url(bootstrap-icons/icons/moon-stars-fill.svg);
}
&.bi-brightness-high {
mask-image: url(bootstrap-icons/icons/brightness-high.svg);
}
&.bi-gear {
mask-image: url(bootstrap-icons/icons/gear.svg);
}
&.bi-sun-fill {
mask-image: url(bootstrap-icons/icons/sun-fill.svg);
}
&.bi-tv {
mask-image: url(bootstrap-icons/icons/tv.svg);
}
}

52
static/src/lib/bootstrap.scss vendored Normal file
View File

@@ -0,0 +1,52 @@
@import "bootstrap/scss/mixins/banner";
@include bsBanner("");
// scss-docs-start import-stack
// Configuration
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/variables-dark";
@import "bootstrap/scss/maps";
@import "bootstrap/scss/mixins";
@import "bootstrap/scss/utilities";
// Layout & components
@import "bootstrap/scss/root";
@import "bootstrap/scss/reboot";
@import "bootstrap/scss/type";
//@import "bootstrap/scss/images";
//@import "bootstrap/scss/containers";
@import "bootstrap/scss/grid";
@import "bootstrap/scss/tables";
@import "bootstrap/scss/forms";
@import "bootstrap/scss/buttons";
//@import "bootstrap/scss/transitions";
@import "bootstrap/scss/dropdown";
//@import "bootstrap/scss/button-group";
@import "bootstrap/scss/nav";
@import "bootstrap/scss/navbar";
//@import "bootstrap/scss/card";
//@import "bootstrap/scss/accordion";
//@import "bootstrap/scss/breadcrumb";
//@import "bootstrap/scss/pagination";
//@import "bootstrap/scss/badge";
//@import "bootstrap/scss/alert";
//@import "bootstrap/scss/progress";
@import "bootstrap/scss/list-group";
//@import "bootstrap/scss/close";
//@import "bootstrap/scss/toasts";
//@import "bootstrap/scss/modal";
//@import "bootstrap/scss/tooltip";
//@import "bootstrap/scss/popover";
//@import "bootstrap/scss/carousel";
//@import "bootstrap/scss/spinners";
//@import "bootstrap/scss/offcanvas";
//@import "bootstrap/scss/placeholders";
// Helpers
@import "bootstrap/scss/helpers";
// Utilities
@import "bootstrap/scss/utilities/api";
// scss-docs-end import-stack

View File

@@ -1,6 +1,10 @@
@import "bootstrap/scss/bootstrap"; @use "flag-icons/sass/flag-icons" with (
$bootstrap-icons-font-dir: "bootstrap-icons/font/fonts"; $flag-icons-path: "flag-icons/flags",
@import "bootstrap-icons/font/bootstrap-icons"; $flag-icons-included-countries: ("gb", "ru", "by", "ua", "kz")
);
@import "./lib/bootstrap";
@import "./lib/bootstrap-icons";
@import "./widget.scss"; @import "./widget.scss";
@import "./weather.scss"; @import "./weather.scss";

View File

@@ -34,7 +34,7 @@
const themeSwitcherText = document.querySelector("#bd-theme-text"); const themeSwitcherText = document.querySelector("#bd-theme-text");
const activeThemeIcon = document.querySelector(".theme-icon-active"); const activeThemeIcon = document.querySelector(".theme-icon-active");
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`); const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`);
const activeThemeIconClass = btnToActive.querySelector("i.bi").className.match(/bi-[\w-]+/)[0]; const activeThemeIconClass = btnToActive.querySelector(".bi").className.match(/bi-[\w-]+/)[0];
document.querySelectorAll("[data-bs-theme-value]").forEach((element) => { document.querySelectorAll("[data-bs-theme-value]").forEach((element) => {
element.classList.remove("active"); element.classList.remove("active");