Compare commits

...
2 Commits
Author SHA1 Message Date
makiolaj 0b07fcf917 Release v2.20.3
Bump versionName to 2.20.3 (versionCode 22003, per the MAJOR*10000 +
MINOR*100 + PATCH rule), promote the CHANGELOG's [Unreleased] section to
[2.20.3], and hand-write the per-version "What's New" for all twelve
store locales.

The release carries one user-visible fix: the French agenda-range plural
crash reported as #297 and #298.

2.20.0, 2.20.1 and 2.20.2 each shipped with only an en-US changelog, so
every other locale published nothing; this restores the full coverage
2.19.1 had (ar, de-DE, en-GB, en-US, es-ES, fr-FR, it-IT, pl-PL, pt-BR,
pt-PT, ru-RU, zh-CN). en-GB matters on its own because it, not en-US, is
the Play Console's default locale and what Play falls back to. Every file
is well inside Play's 500-character cap — the longest is it-IT at 348 —
and scripts/check_changelog_lengths.sh passes.

Per docs/RELEASING.md this bump is what triggers release.yaml once the
branch reaches main, so the on-device gate (step 4, scripts/verify-release.sh)
still has to pass before that merge.
2026-09-12 16:48:27 +02:00
Jean-Luc Makiola 08f7228a69 fix: French agenda-range plural crash (#297, #298) (#301) 2026-09-12 16:27:30 +02:00
17 changed files with 93 additions and 18 deletions
+10
View File
@@ -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
View File
@@ -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")
}
-3
View File
@@ -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>
+1
View File
@@ -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 dinterrupteur 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 lAgenda ne fait plus planter lapplication en français. Choisir une plage personnalisée — ou simplement saisir un nombre dans ce champ — fermait lapplication sur tout téléphone en français ; seule une plage dexactement un jour y échappait.
@@ -0,0 +1,2 @@
Correzioni
• Lintervallo personalizzato dellAgenda non fa più chiudere lapp in francese. Scegliere un intervallo personalizzato — o anche solo digitare un numero in quel campo — chiudeva lapp 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 @@
修复
• 法语下的日程自定义范围不再导致应用崩溃。选择自定义范围,甚至只是在该字段中输入数字,都会让应用在设置为法语的手机上关闭,只有恰好一天的范围不受影响。其他语言从未受到影响。
+46 -7
View File
@@ -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: