Compare commits
1
Commits
release/v2.20.3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7be6b7ac31 |
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [2.20.3] — 2026-09-12
|
||||
|
||||
### Fixed
|
||||
- **A custom agenda range crashed the app in French.** Choosing a custom range
|
||||
for the agenda — or simply typing a number into that field — closed the app on
|
||||
any phone set to French. The French translation of the "%d days" label was
|
||||
missing the wording French uses for counts of two and above, and Android has
|
||||
nothing to fall back on when that particular form is absent, so every value
|
||||
except one day was affected ([#297], [#298]).
|
||||
|
||||
## [2.20.2] — 2026-09-12
|
||||
|
||||
### Fixed
|
||||
|
||||
+12
-8
@@ -28,8 +28,8 @@ android {
|
||||
// which builds this version and then creates the matching vX.Y.Z tag +
|
||||
// release itself (versionCode is pinned to MAJOR*10000 + MINOR*100 +
|
||||
// PATCH from versionName, e.g. 2.7.2 -> 20702). See docs/RELEASING.md.
|
||||
versionCode = 22002
|
||||
versionName = "2.20.2"
|
||||
versionCode = 22003
|
||||
versionName = "2.20.3"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -113,12 +113,16 @@ android {
|
||||
lint {
|
||||
// Community translations are expected to be partial — a missing string
|
||||
// falls back to the English base at runtime — so don't fail the build on
|
||||
// it. Likewise a translated <plurals> may not fill every CLDR quantity
|
||||
// form its locale defines (e.g. Arabic needs "zero"); the missing form
|
||||
// falls back to "other" at runtime, so MissingQuantity is informational
|
||||
// too. Stale/extra keys (ExtraTranslation) stay fatal; scripts/
|
||||
// check_translations.py guards the same invariants with clearer,
|
||||
// translator-facing messages.
|
||||
// it. A translated <plurals> may likewise skip a CLDR quantity form its
|
||||
// locale defines (e.g. Arabic "zero"): Android falls back to "other" for
|
||||
// any form it cannot find, so MissingQuantity is informational too.
|
||||
// What a translation must NOT skip is "other" itself — nothing falls back
|
||||
// for that one, not even the base locale, so it throws
|
||||
// Resources$NotFoundException at runtime (Codeberg #297/#298).
|
||||
// MissingQuantity doesn't tell the two cases apart, so that invariant is
|
||||
// enforced by scripts/check_translations.py instead, with a clearer
|
||||
// translator-facing message. Stale/extra keys (ExtraTranslation) stay
|
||||
// fatal.
|
||||
informational += listOf("MissingTranslation", "MissingQuantity")
|
||||
}
|
||||
|
||||
|
||||
@@ -655,12 +655,9 @@
|
||||
<string name="crash_dialog_message">Апошнім разам %1$s нечакана закрылася. Вы можаце дапамагчы выправіць гэта, адправіўшы справаздачу аб праблеме. Яна застанецца на вашай прыладзе, пакуль вы не вырашыце падзяліцца ёю, і ня ўтрымлівае асабістых даных або зьмесціва календара — толькі тэхнічныя падрабязнасьці, паданыя ніжэй.</string>
|
||||
<string name="crash_dialog_report">Справаздача</string>
|
||||
<string name="crash_dialog_dismiss">Ня зараз</string>
|
||||
<string name="crash_report_issue_title">Справаздача аб краху</string>
|
||||
<string name="crash_report_clip_label">Справаздача аб краху %1$s</string>
|
||||
<string name="crash_report_copied">Справаздача скапіявана ў буфэр абмену</string>
|
||||
<string name="crash_report_open_failed">Не атрымалася адкрыць сыстэму адсочваньня праблемаў. Справаздача знаходзіцца ў буфэры абмену.</string>
|
||||
<string name="crash_report_body_template">Дзякуй за справаздачу аб краху ў %1$s. Дадайце ўсё, што памятаеце пра тое, што рабілі, а затым адпраўце.\n\n### Што здарылася\n\n\n### Справаздача аб краху\n%2$s\n</string>
|
||||
<string name="crash_report_body_paste">_(Справаздача задоўгая для гэтай спасылкі — устаўце яе з буфэра абмену сюды.)_</string>
|
||||
<string name="special_dates_calendar_birthday">Дні нараджэньня</string>
|
||||
<string name="special_dates_calendar_anniversary">Юбілейныя даты</string>
|
||||
<string name="special_dates_calendar_custom">Асаблівыя даты</string>
|
||||
|
||||
@@ -323,6 +323,7 @@
|
||||
<plurals name="agenda_range_days">
|
||||
<item quantity="one">%d jour</item>
|
||||
<item quantity="many">%d jours</item>
|
||||
<item quantity="other">%d jours</item>
|
||||
</plurals>
|
||||
<string name="settings_section_views">Vues</string>
|
||||
<string name="settings_quick_switch_header">Bouton d’interrupteur rapide</string>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
إصلاحات
|
||||
• لم يعد النطاق المخصص في الجدول يُعطِّل التطبيق بالفرنسية. كان اختيار نطاق مخصص — أو مجرّد كتابة رقم في ذلك الحقل — يُغلق التطبيق على أي هاتف مضبوط على الفرنسية، ولم ينجُ سوى نطاق يوم واحد بالضبط. أما اللغات الأخرى فلم تتأثّر قطّ.
|
||||
@@ -0,0 +1,2 @@
|
||||
Behoben
|
||||
• Ein benutzerdefinierter Agenda-Zeitraum stürzt auf Französisch nicht mehr ab. Einen eigenen Zeitraum zu wählen – oder auch nur eine Zahl in das Feld zu tippen – beendete die App auf jedem Gerät mit französischer Sprache; nur genau ein Tag blieb verschont. Andere Sprachen waren nie betroffen.
|
||||
@@ -0,0 +1,2 @@
|
||||
Fixed
|
||||
• A custom Agenda range no longer crashes the app in French. Choosing a custom range — or just typing a number into that field — closed the app on any phone set to French, and only a range of exactly one day escaped it. Other languages were never affected.
|
||||
@@ -0,0 +1,2 @@
|
||||
Fixed
|
||||
• A custom Agenda range no longer crashes the app in French. Choosing a custom range — or just typing a number into that field — closed the app on any phone set to French, and only a range of exactly one day escaped it. Other languages were never affected.
|
||||
@@ -0,0 +1,2 @@
|
||||
Correcciones
|
||||
• El intervalo personalizado de la Agenda ya no bloquea la aplicación en francés. Elegir un intervalo propio —o solo escribir un número en ese campo— cerraba la aplicación en cualquier teléfono en francés, y únicamente un intervalo de exactamente un día se salvaba. Los demás idiomas nunca se vieron afectados.
|
||||
@@ -0,0 +1,2 @@
|
||||
Corrections
|
||||
• La plage personnalisée de l’Agenda ne fait plus planter l’application en français. Choisir une plage personnalisée — ou simplement saisir un nombre dans ce champ — fermait l’application sur tout téléphone en français ; seule une plage d’exactement un jour y échappait.
|
||||
@@ -0,0 +1,2 @@
|
||||
Correzioni
|
||||
• L’intervallo personalizzato dell’Agenda non fa più chiudere l’app in francese. Scegliere un intervallo personalizzato — o anche solo digitare un numero in quel campo — chiudeva l’app su qualsiasi telefono impostato in francese; solo un intervallo di esattamente un giorno ne era immune. Le altre lingue non sono mai state interessate.
|
||||
@@ -0,0 +1,2 @@
|
||||
Poprawki
|
||||
• Niestandardowy zakres Agendy nie powoduje już awarii aplikacji w języku francuskim. Wybranie własnego zakresu – albo samo wpisanie liczby w to pole – zamykało aplikację na każdym telefonie ustawionym na francuski; bezpieczny był tylko zakres dokładnie jednego dnia. Innych języków to nigdy nie dotyczyło.
|
||||
@@ -0,0 +1,2 @@
|
||||
Correções
|
||||
• O intervalo personalizado da Agenda não fecha mais o aplicativo em francês. Escolher um intervalo personalizado — ou apenas digitar um número nesse campo — fechava o aplicativo em qualquer telefone configurado em francês; só um intervalo de exatamente um dia escapava. Os outros idiomas nunca foram afetados.
|
||||
@@ -0,0 +1,2 @@
|
||||
Correções
|
||||
• O intervalo personalizado da Agenda já não fecha a aplicação em francês. Escolher um intervalo personalizado — ou apenas escrever um número nesse campo — fechava a aplicação em qualquer telemóvel definido em francês; só um intervalo de exatamente um dia escapava. Os outros idiomas nunca foram afetados.
|
||||
@@ -0,0 +1,2 @@
|
||||
Исправлено
|
||||
• Свой диапазон в Расписании больше не приводит к сбою на французском языке. Выбор своего диапазона — или даже простой ввод числа в это поле — закрывал приложение на любом телефоне с французским языком; уцелел только диапазон ровно в один день. Другие языки это никогда не затрагивало.
|
||||
@@ -0,0 +1,2 @@
|
||||
修复
|
||||
• 法语下的日程自定义范围不再导致应用崩溃。选择自定义范围,甚至只是在该字段中输入数字,都会让应用在设置为法语的手机上关闭,只有恰好一天的范围不受影响。其他语言从未受到影响。
|
||||
@@ -8,7 +8,10 @@ and are produced via Weblate. This guard keeps incoming translation PRs honest:
|
||||
* a translation must not define keys absent from the base — those are stale
|
||||
keys left behind after a rename/removal upstream;
|
||||
* a translation must not translate strings marked ``translatable="false"`` in
|
||||
the base (URLs, IDs and the like).
|
||||
the base (URLs, IDs and the like);
|
||||
* every translated ``<plurals>`` must carry an ``other`` item, because that
|
||||
is the one quantity form Android cannot fall back for (see
|
||||
``plurals_missing_other``).
|
||||
|
||||
Missing keys are *allowed* and only reported as coverage: a missing string
|
||||
falls back to the English base at runtime, so partial translations are fine
|
||||
@@ -27,10 +30,13 @@ RES_DIR = Path("app/src/main/res")
|
||||
BASE = RES_DIR / "values" / "strings.xml"
|
||||
RESOURCE_TAGS = ("string", "plurals", "string-array")
|
||||
|
||||
# The quantity Android itself falls back to, and therefore the one a translated
|
||||
# <plurals> may never omit.
|
||||
PLURAL_FALLBACK = "other"
|
||||
|
||||
def entries(path: Path) -> dict[str, bool]:
|
||||
"""Map resource name -> is-translatable for every entry in ``path``."""
|
||||
root = ET.parse(path).getroot()
|
||||
|
||||
def entries(root: ET.Element) -> dict[str, bool]:
|
||||
"""Map resource name -> is-translatable for every entry under ``root``."""
|
||||
return {
|
||||
el.attrib["name"]: el.attrib.get("translatable", "true") != "false"
|
||||
for el in root
|
||||
@@ -38,12 +44,35 @@ def entries(path: Path) -> dict[str, bool]:
|
||||
}
|
||||
|
||||
|
||||
def plurals_missing_other(root: ET.Element) -> list[str]:
|
||||
"""Names of ``<plurals>`` under ``root`` that lack an ``other`` item.
|
||||
|
||||
Android resolves the CLDR quantity for a count and, if that specific form is
|
||||
absent, falls back to ``other`` — so a translation may legitimately skip
|
||||
forms its locale rarely uses (Arabic ``zero``, French ``many``). There is no
|
||||
fallback for ``other`` itself, and none to the base locale either: a
|
||||
translated ``<plurals>`` without it raises
|
||||
``Resources$NotFoundException`` as soon as a count selects the missing form.
|
||||
|
||||
That is Codeberg #297/#298 — ``values-fr`` translated ``agenda_range_days``
|
||||
with only ``one`` and ``many``, and since French ``many`` matches only
|
||||
millions, every custom agenda range from 2 days up crashed the app.
|
||||
"""
|
||||
return sorted(
|
||||
el.attrib["name"]
|
||||
for el in root
|
||||
if el.tag == "plurals"
|
||||
and "name" in el.attrib
|
||||
and not any(item.attrib.get("quantity") == PLURAL_FALLBACK for item in el.findall("item"))
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not BASE.exists():
|
||||
print(f"::error::base resource file {BASE} not found", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
base = entries(BASE)
|
||||
base = entries(ET.parse(BASE).getroot())
|
||||
base_keys = set(base)
|
||||
nontranslatable = {name for name, ok in base.items() if not ok}
|
||||
translatable_total = len(base_keys - nontranslatable)
|
||||
@@ -57,16 +86,18 @@ def main() -> int:
|
||||
for path in files:
|
||||
locale = path.parent.name[len("values-"):]
|
||||
try:
|
||||
translated = entries(path)
|
||||
root = ET.parse(path).getroot()
|
||||
except ET.ParseError as exc:
|
||||
print(f"::error file={path}::{locale}: malformed XML: {exc}")
|
||||
errors += 1
|
||||
continue
|
||||
|
||||
translated = entries(root)
|
||||
keys = set(translated)
|
||||
stale = sorted(keys - base_keys)
|
||||
translated_fixed = sorted(keys & nontranslatable)
|
||||
missing = base_keys - nontranslatable - keys
|
||||
no_other = plurals_missing_other(root)
|
||||
|
||||
for name in stale:
|
||||
print(f"::error file={path}::{locale}: stale key '{name}' is not in the base strings.xml")
|
||||
@@ -77,10 +108,18 @@ def main() -> int:
|
||||
"in the base and must not be translated"
|
||||
)
|
||||
errors += 1
|
||||
for name in no_other:
|
||||
print(
|
||||
f"::error file={path}::{locale}: plurals '{name}' has no "
|
||||
f"<item quantity=\"{PLURAL_FALLBACK}\"> — Android cannot fall back for it "
|
||||
"and will crash when a count selects it; translate that form or drop the "
|
||||
"whole plurals to inherit the base"
|
||||
)
|
||||
errors += 1
|
||||
|
||||
covered = translatable_total - len(missing)
|
||||
pct = covered * 100 // translatable_total if translatable_total else 100
|
||||
verdict = "OK" if not (stale or translated_fixed) else "FAIL"
|
||||
verdict = "OK" if not (stale or translated_fixed or no_other) else "FAIL"
|
||||
print(f"{locale:<10} {covered}/{translatable_total} keys ({pct}%) — {verdict}")
|
||||
|
||||
if errors:
|
||||
|
||||
Reference in New Issue
Block a user