Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
python-version: ["3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v7
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/update-mo-files.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: "3.10"
python-version: "3.14"

- name: Install Poethepoet
run: pipx install poethepoet
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/update-source-language-file.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: "3.10"
python-version: "3.14"

- name: Install Poethepoet
run: pipx install poethepoet
Expand Down
65 changes: 49 additions & 16 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,47 @@
from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING
from urllib.parse import urlsplit

import streamlit as st
from streamlit_gettext import get_preferred_languages

from package_build.download_parts import download_parts
from package_build.file_list import show_file_list
from package_build.i18n import gettext as _
from package_build.i18n import (
get_available_languages,
get_preferred_languages,
set_preferred_languages,
)
from package_build.i18n import (
gettext as _,
)
from package_build.metadata import get_dict_metadata, get_hook_metadata
from package_build.models import DictInfoEntry
from package_build.package import build_package, package_up_to_date, remove_stale_packages
from package_build.parse_metadata import parse_metadata

if TYPE_CHECKING:
from package_build.models import DictInfoEntry


def is_running_locally() -> bool:
app_url = st.context.url
return app_url is None or urlsplit(app_url).hostname in {"localhost", "127.0.0.1", "::1"}


st.set_page_config(page_title=_("DF localization package builder"), page_icon="📦")

if is_running_locally():
application_language = st.segmented_control(
"Language",
options=get_available_languages(),
label_visibility="collapsed",
required=True,
key="application_language",
)
if application_language:
set_preferred_languages([application_language])

st.header(_("DF localization package builder"))
st.write(_("Build/download a package, unpack into the game's directory with file repalacement."))
st.markdown(
Expand Down Expand Up @@ -44,26 +73,30 @@
variants = sorted(df_version_options[df_version].variants, key=lambda v: variants_priority.get(v, 3))


def normalize_language_code(language_code: str) -> str:
return language_code.replace("_", "-").casefold()


def get_language_priority(user_languages: list[str], language_code: str) -> int:
if language_code.startswith("en"):
return 1000

try:
normalized_language_code = normalize_language_code(language_code)
return user_languages.index(normalized_language_code) - len(user_languages)
except ValueError:
return 0


def language_ordering(dict_info_entry: DictInfoEntry) -> tuple[int, str]:
"""
Set max priority to user's languages (according to their index in the user's browser settings)
and the lowest priority to English.

All other languages are sorted by their codes.
"""
user_languages = get_preferred_languages()

def get_language_priority(language_code: str) -> int:
if language_code.startswith("en"):
return 1000

try:
return user_languages.index(dict_info_entry.code) - len(user_languages)
except ValueError:
return 0

return (get_language_priority(dict_info_entry.code), dict_info_entry.code)

user_languages = [normalize_language_code(language) for language in get_preferred_languages()]
return get_language_priority(user_languages, dict_info_entry.code), dict_info_entry.code


with column2:
Expand Down
7 changes: 5 additions & 2 deletions package_build/download_parts.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
from typing import NamedTuple
from __future__ import annotations

from typing import TYPE_CHECKING, NamedTuple

import requests
import streamlit as st

from package_build.i18n import gettext as _

from .models import DictInfoEntry, HookInfoEntry
if TYPE_CHECKING:
from .models import DictInfoEntry, HookInfoEntry


def download(url: str) -> bytes:
Expand Down
14 changes: 9 additions & 5 deletions package_build/file_list.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
from collections.abc import Callable
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from __future__ import annotations

from datetime import UTC, datetime
from typing import TYPE_CHECKING, Any

import streamlit as st

from package_build.i18n import gettext as _
from package_build.i18n import ngettext
from package_build.package import get_file_modification_datetime, package_up_to_date

if TYPE_CHECKING:
from collections.abc import Callable
from pathlib import Path


def show_file_list(root_dir: Path, glob_filter: str, sort_key: Callable[[Path], Any] | None=None) -> None:
st.subheader(_("Package files available to download"))
Expand All @@ -26,7 +30,7 @@ def show_file_list(root_dir: Path, glob_filter: str, sort_key: Callable[[Path],
for package_path in sorted(file_list, key=sort_key):
column1, column2, column3 = st.columns([4, 3, 2], vertical_alignment="center")
column1.write(package_path.relative_to(root_dir).name)
hours_ago = (datetime.now(tz=timezone.utc) - get_file_modification_datetime(package_path)).seconds // 3600
hours_ago = (datetime.now(tz=UTC) - get_file_modification_datetime(package_path)).seconds // 3600
if hours_ago == 0:
column2.write(_("less than an hour ago"))
else:
Expand Down
3 changes: 3 additions & 0 deletions package_build/i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@

gettext = _gettext_wrapper.gettext
ngettext = _gettext_wrapper.ngettext
get_available_languages = _gettext_wrapper.get_available_languages
get_preferred_languages = _gettext_wrapper.get_preferred_languages
set_preferred_languages = _gettext_wrapper.set_preferred_languages
17 changes: 11 additions & 6 deletions package_build/package.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
from __future__ import annotations

import contextlib
import shutil
from datetime import datetime, timedelta, timezone
from pathlib import Path
from datetime import UTC, datetime, timedelta
from typing import TYPE_CHECKING
from zipfile import ZIP_DEFLATED, ZipFile

from package_build.download_parts import DownloadedParts
from package_build.models import HookInfoEntry
if TYPE_CHECKING:
from pathlib import Path

from package_build.download_parts import DownloadedParts
from package_build.models import HookInfoEntry


def zip_directory(directory: Path, filename: Path) -> None:
Expand Down Expand Up @@ -51,7 +56,7 @@ def build_package(


def get_file_modification_datetime(path: Path) -> datetime:
return datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc)
return datetime.fromtimestamp(path.stat().st_mtime, tz=UTC)


def package_up_to_date(package_path: Path, lifetime: timedelta = timedelta(hours=12)) -> bool:
Expand All @@ -63,7 +68,7 @@ def package_up_to_date(package_path: Path, lifetime: timedelta = timedelta(hours
return False

modification_datetime = get_file_modification_datetime(package_path)
return (modification_datetime + lifetime) > datetime.now(tz=timezone.utc)
return (modification_datetime + lifetime) > datetime.now(tz=UTC)


def remove_stale_packages(root_dir: Path) -> None:
Expand Down
7 changes: 5 additions & 2 deletions package_build/parse_metadata.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from __future__ import annotations

import re
from dataclasses import dataclass, field
from typing import NamedTuple
from typing import TYPE_CHECKING, NamedTuple

from .models import HookInfoEntry
if TYPE_CHECKING:
from .models import HookInfoEntry


@dataclass
Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ name = "package-build"
version = "0.1.0"
description = ""
authors = [{ name = "insolor" }]
requires-python = ">=3.10"
requires-python = ">=3.12"
readme = "README.md"
license = "MIT"
dependencies = [
"streamlit>=1.60.0,<2",
"requests>=2.34.2,<3",
"pydantic>=2.13.4,<3",
"loguru>=0.7.3,<0.8",
"streamlit-gettext>=0.0.4,<0.0.5",
"streamlit-gettext>=0.1.2,<0.2",
]

[dependency-groups]
Expand All @@ -27,7 +27,7 @@ xgettext.shell = "xgettext -d messages -o package_build/locale/messages.pot app.
msgfmt = "find ./package_build/locale -name \\*.po -execdir msgfmt messages.po -o messages.mo \\;"

[tool.ruff]
target-version = "py310"
target-version = "py312"
line-length = 120

[tool.ruff.lint]
Expand Down
101 changes: 73 additions & 28 deletions tests/test_app.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,89 @@
from collections.abc import Callable
from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING
from unittest.mock import patch

import pytest
from streamlit.testing.v1 import AppTest

from package_build.i18n import set_preferred_languages
from package_build.models import DictInfoEntry, HookInfoEntry

if TYPE_CHECKING:
from collections.abc import Iterator

project_root = Path(__file__).resolve().parent.parent


@pytest.fixture
@patch("package_build.metadata.get_hook_metadata")
@patch("package_build.metadata.get_dict_metadata")
def apptest(get_dict_metadata: Callable, get_hook_metadata: Callable) -> AppTest:
get_hook_metadata.return_value = [
HookInfoEntry(
df=0,
checksum=0,
lib="https://example.com/hook_0.1.0.dll",
config="https://example.com/hook_0.1.0.dll",
offsets="https://example.com/offsets/50.10_classic_win64.toml",
dfhooks="https://example.com/dfhooks.dll",
),
]

get_dict_metadata.return_value = [
DictInfoEntry(
language="English",
code="en",
csv="https://example.com/en.csv",
font="https://example.com/cp437.png",
encoding="https://example.com/cp437.toml",
checksum=0,
),
]

app_path = project_root / "app.py"
return AppTest.from_file(app_path).run()
def apptest() -> Iterator[AppTest]:
with (
patch("package_build.metadata.get_hook_metadata") as get_hook_metadata,
patch("package_build.metadata.get_dict_metadata") as get_dict_metadata,
):
get_hook_metadata.return_value = [
HookInfoEntry(
df=0,
checksum=0,
lib="https://example.com/hook_0.1.0.dll",
config="https://example.com/hook_0.1.0.dll",
offsets="https://example.com/offsets/50.10_classic_win64.toml",
dfhooks="https://example.com/dfhooks.dll",
),
]

get_dict_metadata.return_value = [
DictInfoEntry(
language="English",
code="en",
csv="https://example.com/en.csv",
font="https://example.com/cp437.png",
encoding="https://example.com/cp437.toml",
checksum=0,
),
DictInfoEntry(
language="Belarusian",
code="be",
csv="https://example.com/be.csv",
font="https://example.com/cp437.png",
encoding="https://example.com/cp437.toml",
checksum=0,
),
DictInfoEntry(
language="Portuguese (Brazil)",
code="pt_BR",
csv="https://example.com/pt_BR.csv",
font="https://example.com/cp437.png",
encoding="https://example.com/cp437.toml",
checksum=0,
),
]

app_path = project_root / "app.py"
yield AppTest.from_file(app_path).run()


def test_smoketest(apptest: AppTest):
assert not apptest.exception, apptest.exception[0].stack_trace


def test_language_selector(apptest: AppTest):
language_selector = apptest.segmented_control[0]
assert "ru" in language_selector.options

try:
language_selector.set_value("ru").run()
assert not apptest.exception, apptest.exception[0].stack_trace
assert apptest.header[0].value == "Сборщик пакетов локализации для DF"
finally:
set_preferred_languages(None)


def test_brazilian_portuguese_is_prioritized(apptest: AppTest):
try:
apptest.segmented_control[0].set_value("pt_BR").run()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
language_selector = apptest.selectbox[-1]
assert language_selector.value.code == "pt_BR"
finally:
set_preferred_languages(None)
Loading
Loading