Files
siftlode/backend/app/collation.py
T
peter 67c9f10153 feat(i18n): every server A→Z list in the reader's alphabet (E30 S3)
The Postgres image is Alpine, whose en_US.utf8 is byte order: "Életrajzi" and
"Ötödik" sorted after "Zorró" in the feed, the channel lists, the Plex grid,
its genres and collections. `app/collation.py` names the ICU collation of the
reader's language (hu-x-icu puts "Ö" after "O" and "Cs" after "Cu"), wraps
every text ORDER BY with it - the feed's keyset cursor compares with the same
expression, so paging walks the order it sorted in - and orders the lists built
in Python (genre and studio facets, collection members, the discovery re-sort)
through the same collation. Plex title tiebreaks use the shown (HU) title.
2026-09-29 22:39:57 +02:00

40 lines
1.8 KiB
Python

"""Alphabetical order in the reader's language (E30 S3).
Why not the database default: the Postgres image is Alpine, and musl's `en_US.utf8` collation is BYTE
order — every accented initial sorted after "Z" ("Zorró < Életrajzi < Ötödik"), in every A→Z list the
server orders. Postgres ships the ICU collations on the same image (dev, test, prod alike), so a text
sort names one: Hungarian puts "Ö" after "O" and "Cs" after "Cu", English folds the accents in.
`collated` wraps an ORDER BY expression — and because a keyset cursor compares with the SAME expression
(`feedquery.sort_keys`), the cursor pages in the same order it sorted in. `sorted_words` orders a Python
list the same way through the database, so a list built in Python matches one built in SQL exactly."""
from typing import Any
from sqlalchemy import SQLColumnExpression, column, func, select, values
from sqlalchemy.orm import Session
from sqlalchemy.sql.elements import ColumnElement
from sqlalchemy.types import String
from app.mailtext import normalize
ICU: dict[str, str] = {"en": "en-x-icu", "hu": "hu-x-icu"}
def collation(lang: str | None) -> str:
"""The ICU collation for a UI language; anything unknown sorts as English."""
return ICU[normalize(lang)]
def collated(expr: SQLColumnExpression[Any], lang: str | None) -> ColumnElement[Any]:
"""`expr` compared in the reader's alphabet. Case-folded, so "apple" and "Apple" sit together."""
return func.lower(expr).collate(collation(lang))
def sorted_words(db: Session, words: list[str], lang: str | None) -> list[str]:
"""`words` in the reader's alphabetical order, by the same ICU collation as `collated`."""
if not words:
return []
w = values(column("w", String), name="words").data([(x,) for x in words])
return list(db.scalars(select(w.c.w).order_by(collated(w.c.w, lang), w.c.w)))