Files
meshcore-bot/tests/unit/test_gwx_display_units.py
agessaman 595f37a27b fix(i18n): address review findings on weather localization
- Extracted `modules/alert_format.py` as the single NWS alert formatter, replacing four copies of the event-type abbreviation table and two of the time compactor. `!wx alerts` and the proactive `WeatherService` broadcasts now localize from one code path, so a Russian bot no longer answers `!wx alerts` in English while its proactive alerts are Russian.
- Stopped leaking translation key paths into mesh broadcasts. `Translator.translate` returns the dotted key when a lookup misses in both the locale and the English fallback, which is right for development but reached the air in production: an unclassifiable NWS title rendered as `⚪Hazardous services.weather_service.event_types.Unknown`, an unmapped WMO code as `services.weather_service.weather_descriptions.4`, and an oddly-cased `wind_speed_unit` as `services.weather_service.wind_speed_units.KMH`. `alert_format.translate_or()` carries an English default at each site, and `WeatherService` now normalizes and validates its three `[Weather]` unit settings the way `GlobalWxCommand` already did.
- Fixed alert expiry rendering in every locale. The formatter rendered a timestamp to a string and re-parsed its own output with `(\d+)(AM|PM)` against a hardcoded English month list, so translated months took the wrong branch and truncated mid-string. Times now carry parsed parts and render through a per-locale `common.alerts.time_12h` template — the space before AM/PM was correct (Russian writes "6 дня", not "6дня"); the downstream regex was the bug.
- Restored month abbreviation. `_compact_time` iterated over abbreviations and replaced them in the string instead of mapping full names, so English stopped shortening "June 28" and Russian replaced the "Jun" inside "June", leaving a stray Latin "e" (`июнe 28`). Reuses the existing `common.date_time.month_abbreviations` rather than the duplicate `services.weather_service.months` block.
- Made `!gwx` display units follow `[Weather]` config instead of the response language. Visibility switched on `base_language != 'en'`, so `language = ru` with the default `temperature_unit = fahrenheit` printed Fahrenheit beside kilometers, and `en-GB` was forced to miles. Pressure is a locale convention rather than a metric/imperial split, so each catalog names its own via `commands.gwx.pressure_unit` — previously every non-English locale inherited mmHg from the English catalog, whose `pressure_mmhg` string contained Russian text, giving German and French users Cyrillic pressure units.
- Let localized `H`/`L` labels reach a standard install. `config.ini.example` shipped the three `temperature_*_format` keys uncommented with literal `H:`/`L:`, and a config value always beats the new locale-aware default, so a Russian bot built from the documented example still rendered `H:47°C L:33°C`. The example now uses the `{high_label}`/`{low_label}` placeholders, which were documented in the docstring but not in the file.
- Routed high/low labels through the reply's translator. `_format_high_low` passed `bot.translator`, so with `auto_detect_language` on, an English-default bot answering a Russian sender localized the rest of the line but not `H:`/`L:`. Added `BaseCommand.response_translator` for this, replacing `wx_international`'s reach into the private `_response_translator` ContextVar.
- Fixed a byte-budget overrun in `!gwx`. The guard on the extra conditions block compared a character count against a byte-derived budget while the rest of the function used `_count_display_width`; Cyrillic is two bytes per character, so the block was appended after the budget was spent.
- Reverted nine `commands.gwx` English rewordings that were not localization work, including the configuration hint in `mqtt_weather_no_subscriber` — dropping it left a mis-configured operator with no pointer to the two keys they need.
- Fixed the Russian `visibility` string, which said "км" on the miles key — the same locale/config conflation as the code bug, in the data. Shortened the Russian event-type abbreviations, which were full words consuming a quarter of the 130-byte budget at two bytes per character.
- Added `commands.wx.hourly_not_available`, missing from every catalog so `!wx hourly` printed the raw key path. Predates this branch; found while auditing every translation key the weather modules reference.
- Moved alert strings to `common.alerts.*` and wind directions to `common.wind_directions.*`, since a command and a service both read them.
2026-09-07 15:13:18 -07:00

95 lines
3.6 KiB
Python

#!/usr/bin/env python3
"""Unit tests for !gwx display units — driven by [Weather] config, not by locale."""
import pytest
from modules.commands.alternatives.wx_international import (
HPA_TO_MMHG,
MI_TO_KM,
VISIBILITY_CAP_KM,
VISIBILITY_CAP_MI,
GlobalWxCommand,
)
from modules.i18n import Translator
@pytest.fixture
def gwx_bot(mock_bot):
mock_bot.config.add_section("Weather")
mock_bot.config.set("Weather", "weather_provider", "openmeteo")
mock_bot.config.set("Weather", "default_country", "US")
return mock_bot
def build(gwx_bot, *, temperature_unit="fahrenheit", language="en"):
gwx_bot.config.set("Weather", "temperature_unit", temperature_unit)
gwx_bot.translator = Translator(language)
return GlobalWxCommand(gwx_bot)
@pytest.mark.unit
class TestMetricDistance:
def test_fahrenheit_is_imperial(self, gwx_bot):
assert build(gwx_bot, temperature_unit="fahrenheit").metric_distance is False
def test_celsius_is_metric(self, gwx_bot):
assert build(gwx_bot, temperature_unit="celsius").metric_distance is True
def test_language_does_not_decide(self, gwx_bot):
# A Fahrenheit bot answering in Russian must not print kilometers next
# to Fahrenheit temperatures.
cmd = build(gwx_bot, temperature_unit="fahrenheit", language="ru")
assert cmd.metric_distance is False
def test_en_gb_with_celsius_gets_metric(self, gwx_bot):
cmd = build(gwx_bot, temperature_unit="celsius", language="en-GB")
assert cmd.metric_distance is True
def test_invalid_unit_falls_back_to_imperial(self, gwx_bot):
cmd = build(gwx_bot, temperature_unit="kelvin")
assert cmd.temperature_unit == "fahrenheit"
assert cmd.metric_distance is False
@pytest.mark.unit
class TestVisibilityStrings:
def test_imperial_string_says_miles_in_english(self):
assert Translator("en").translate("commands.gwx.visibility", value=12) == "👁️12mi"
def test_imperial_string_says_miles_in_russian(self):
# This key means miles in every catalog now that the unit comes from
# config; it used to say "км" in ru.
out = Translator("ru").translate("commands.gwx.visibility", value=12)
assert "км" not in out
assert "миль" in out
def test_metric_string_says_km(self):
assert Translator("en").translate("commands.gwx.visibility_km", value=12) == "👁️12km"
def test_caps_are_equivalent(self):
assert pytest.approx(VISIBILITY_CAP_KM, abs=0.3) == VISIBILITY_CAP_MI * MI_TO_KM
@pytest.mark.unit
class TestPressureUnitConvention:
@pytest.mark.parametrize("language", ["en", "en-GB", "de", "fr", "fr-CA",
"es", "nl", "pl", "pt", "pt-BR"])
def test_hpa_locales(self, language):
assert Translator(language).translate("commands.gwx.pressure_unit") == "hpa"
def test_russian_uses_mmhg(self):
assert Translator("ru").translate("commands.gwx.pressure_unit") == "mmhg"
@pytest.mark.parametrize("language", ["en", "de", "fr", "es", "nl", "pl", "pt"])
def test_hpa_locales_render_no_cyrillic(self, language):
out = Translator(language).translate("commands.gwx.pressure", value=1013)
assert not any("Ѐ" <= ch <= "ӿ" for ch in out), out
def test_english_mmhg_string_is_english(self):
# The en catalog held "мм рт. ст.", which every non-ru locale inherited.
out = Translator("en").translate("commands.gwx.pressure_mmhg", value=760)
assert out == "📊760mmHg"
def test_conversion_factor(self):
assert round(1013 * HPA_TO_MMHG) == 760