feat: EN/ES language option for the psychoacoustics/hearing/vibration plots#253
Conversation
📝 WalkthroughWalkthroughPlot methods for hearing, psychoacoustics, and vibration results now accept a keyword-only ChangesPlot internationalization
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces internationalization support for plotting methods across the library, allowing users to specify a language ('en' or 'es') for axis labels, titles, and legends. The implementation includes a new locale helper module and updates to various plotting functions to handle locale-aware number formatting and decimal separators. The review feedback highlights that the current approach of clearing and restoring categorical labels in localize_axes is fragile and redundant. The reviewer suggests improving localize_axes to automatically skip axes using FixedFormatter, which would eliminate the need for manual restoration logic in multiple plotting functions.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| def localize_axes(ax: Any, language: str = "en") -> None: | ||
| """Localise linear-axis tick decimals to a comma for Spanish; no-op for English.""" | ||
| if language != "es": | ||
| return | ||
| from matplotlib.ticker import FuncFormatter, NullFormatter | ||
|
|
||
| def _comma(x: float, _pos: int) -> str: | ||
| return f"{x:g}".replace(".", ",") | ||
|
|
||
| for axis in (ax.xaxis, ax.yaxis): | ||
| if axis.get_scale() != "linear": | ||
| continue | ||
| if isinstance(axis.get_major_formatter(), NullFormatter): | ||
| continue | ||
| axis.set_major_formatter(FuncFormatter(_comma)) |
There was a problem hiding this comment.
Instead of blindly applying FuncFormatter to both axes and then having to manually "restore" categorical/fixed labels in almost every plotting function, we can make localize_axes smarter by skipping axes that use a FixedFormatter (which is what categorical/fixed labels use under the hood). This eliminates the fragile and redundant "destroy and restore" pattern across the codebase.
| def localize_axes(ax: Any, language: str = "en") -> None: | |
| """Localise linear-axis tick decimals to a comma for Spanish; no-op for English.""" | |
| if language != "es": | |
| return | |
| from matplotlib.ticker import FuncFormatter, NullFormatter | |
| def _comma(x: float, _pos: int) -> str: | |
| return f"{x:g}".replace(".", ",") | |
| for axis in (ax.xaxis, ax.yaxis): | |
| if axis.get_scale() != "linear": | |
| continue | |
| if isinstance(axis.get_major_formatter(), NullFormatter): | |
| continue | |
| axis.set_major_formatter(FuncFormatter(_comma)) | |
| def localize_axes(ax: Any, language: str = "en") -> None: | |
| """Localise linear-axis tick decimals to a comma for Spanish; no-op for English.""" | |
| if language != "es": | |
| return | |
| from matplotlib.ticker import FixedFormatter, FuncFormatter, NullFormatter | |
| def _comma(x: float, _pos: int) -> str: | |
| return f"{x:g}".replace(".", ",") | |
| for axis in (ax.xaxis, ax.yaxis): | |
| if axis.get_scale() != "linear": | |
| continue | |
| if isinstance(axis.get_major_formatter(), (NullFormatter, FixedFormatter)): | |
| continue | |
| axis.set_major_formatter(FuncFormatter(_comma)) |
| localize_axes(ax, language) | ||
| if language == "es" and banded: # restore categorical band labels | ||
| _band_axis(ax, np.asarray(_STI_BAND_CENTERS), xlabel=_t("freq_hz", language)) | ||
| return ax |
| ax.set_xticks(positions) | ||
| ax.set_xticklabels([f"{f:g}" for f in freqs], rotation=45, ha="right") | ||
| ax.set_xlabel("One-third-octave band [Hz]") | ||
| ax.set_ylabel("Mean intermediate correlation") | ||
| ax.set_xlabel(_t("third_octave_band", language)) | ||
| ax.set_ylabel(_t("mean_corr", language)) |
There was a problem hiding this comment.
We can localize the categorical band labels at creation time using decimal_comma instead of doing it in a separate restore block at the end of the function.
| ax.set_xticks(positions) | |
| ax.set_xticklabels([f"{f:g}" for f in freqs], rotation=45, ha="right") | |
| ax.set_xlabel("One-third-octave band [Hz]") | |
| ax.set_ylabel("Mean intermediate correlation") | |
| ax.set_xlabel(_t("third_octave_band", language)) | |
| ax.set_ylabel(_t("mean_corr", language)) | |
| ax.set_xticks(positions) | |
| ax.set_xticklabels([decimal_comma(f"{f:g}", language) for f in freqs], rotation=45, ha="right") | |
| ax.set_xlabel(_t("third_octave_band", language)) | |
| ax.set_ylabel(_t("mean_corr", language)) |
| localize_axes(ax, language) | ||
| if language == "es" and banded: # restore categorical band labels | ||
| ax.set_xticklabels([decimal_comma(f"{f:g}", language) for f in freqs], | ||
| rotation=45, ha="right") | ||
| return ax |
| localize_axes(ax, language) | ||
| if language == "es": # restore categorical band labels | ||
| _band_axis(ax, freqs, xlabel=_t("freq_hz", language)) | ||
| return ax |
| localize_axes(ax, language) | ||
| if language == "es": # restore the categorical x labels localize_axes cleared | ||
| ax.set_xticklabels(labels) | ||
| return ax |
| ax.set_xticks(positions) | ||
| ax.set_xticklabels([f"{f:g}" for f in freqs], rotation=45, ha="right") | ||
| ax.set_xlabel("Tone frequency [Hz]") | ||
| ax.set_ylabel(r"Audibility $\Delta L$ [dB]") | ||
| ax.set_title("ISO/PAS 20065 tonal audibility") | ||
| ax.set_xlabel(_t("tone_frequency", language)) | ||
| ax.set_ylabel(_t("audibility_dl", language)) | ||
| ax.set_title(_t("tone_audibility_title", language)) |
There was a problem hiding this comment.
We can localize the categorical tone frequency labels at creation time using decimal_comma instead of doing it in a separate restore block at the end of the function.
| ax.set_xticks(positions) | |
| ax.set_xticklabels([f"{f:g}" for f in freqs], rotation=45, ha="right") | |
| ax.set_xlabel("Tone frequency [Hz]") | |
| ax.set_ylabel(r"Audibility $\Delta L$ [dB]") | |
| ax.set_title("ISO/PAS 20065 tonal audibility") | |
| ax.set_xlabel(_t("tone_frequency", language)) | |
| ax.set_ylabel(_t("audibility_dl", language)) | |
| ax.set_title(_t("tone_audibility_title", language)) | |
| ax.set_xticks(positions) | |
| ax.set_xticklabels([decimal_comma(f"{f:g}", language) for f in freqs], rotation=45, ha="right") | |
| ax.set_xlabel(_t("tone_frequency", language)) | |
| ax.set_ylabel(_t("audibility_dl", language)) | |
| ax.set_title(_t("tone_audibility_title", language)) |
| localize_axes(ax, language) | ||
| if language == "es": # restore the categorical x labels localize_axes cleared | ||
| ax.set_xticklabels([decimal_comma(f"{f:g}", language) for f in freqs], | ||
| rotation=45, ha="right") | ||
| return ax |
| localize_axes(ax, language) | ||
| if language == "es": # restore categorical band labels | ||
| _band_axis(ax, freqs, xlabel=_t("freq_hz", language)) | ||
| return ax |
| localize_axes(ax, language) | ||
| if language == "es": # restore categorical operation labels | ||
| ax.set_xticklabels(labels, rotation=45, ha="right") | ||
| return ax |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #253 +/- ##
==========================================
- Coverage 96.13% 96.12% -0.02%
==========================================
Files 157 157
Lines 20448 20548 +100
==========================================
+ Hits 19658 19751 +93
- Misses 790 797 +7 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Numerical conformance report✅ 354/354 conformance checks pass across 44 domains and 225 standards - filters class 1 - weightings within IEC 61672-1 class 1. Each row pins a standard clause to its expected normative value and the value the library computes. Every section below is collapsible and stays collapsed while all of its rows pass; a section with any failing row opens automatically. ✅ Numerical validation - filters & weightings: class showcase (IEC 61260-1 · IEC 61672-1 · ISO 7196)IEC 61260-1:2014 class per filter architecture (order 6, one-third-octave, 100 Hz-10 kHz, fs = 48 kHz). For each architecture the table shows, at its binding band, the measured relative attenuation and the class-1 limit it must clear, so the number and the range it must sit in are both visible. A positive margin means the acceptance limits are met with that much room.
Only Butterworth (the library default) and Chebyshev-II are class-compliant architectures. Chebyshev-I and elliptic trade the mask for passband ripple, and Bessel for a maximally-flat group delay (soft rolloff); they cannot satisfy the IEC 61260-1 Class 1/2 attenuation mask by construction, so they are labelled By design - this is expected, not a failure or regression. Frequency-weighting conformance (A/C: IEC 61672-1 Table 3; G: ISO 7196 A.3). The max deviation from nominal is informational (it falls at a frequency extreme where the tolerance is widest and asymmetric); compliance is judged at the binding frequency - the one with the least headroom - where the deviation, the applicable tolerance band and the headroom are shown together.
✅ Filters & weightings: 100% (7/7)
✅ Levels & dosimetry: 100% (6/6)
✅ Room acoustics: 100% (12/12)
✅ Psychoacoustics: 100% (12/12)
✅ Speech transmission (IEC 60268-16): 100% (9/9)
✅ Intensity & sound power: 100% (4/4)
✅ Room & building acoustics: 100% (51/51)
✅ Building prediction & uncertainty: 100% (15/15)
✅ Outdoor propagation & occupational exposure: 100% (10/10)
✅ Materials: absorption, airflow & impedance: 100% (6/6)
✅ Scattering & diffusion (ISO 17497): 100% (5/5)
✅ In-situ road absorption (ISO 13472): 100% (3/3)
✅ Precision sound power (ISO 3745 / 9614-3): 100% (4/4)
✅ Human vibration (ISO 8041 / 2631 / 5349): 100% (15/15)
✅ Speech intelligibility (ANSI S3.5-1997): 100% (7/7)
✅ Objective intelligibility (STOI / ESTOI): 100% (3/3)
✅ Impulsive-sound prominence (NT ACOU 112): 100% (2/2)
✅ Room noise (ANSI S12.2-2019): 100% (3/3)
✅ Hearing threshold (ISO 7029 / ISO 389-7): 100% (3/3)
✅ Measurement uncertainty (GUM / Supplement 1): 100% (7/7)
✅ Noise-induced hearing loss (ISO 1999): 100% (3/3)
✅ Multiple-shock whole-body vibration (ISO 2631-5): 100% (6/6)
✅ Sound absorption in enclosed spaces (EN 12354-6): 100% (2/2)
✅ Prominent discrete tones (ECMA-418-1): 100% (2/2)
✅ Tonal audibility (ISO/PAS 20065): 100% (11/11)
✅ Psychoacoustic annoyance & fluctuation strength (Fastl & Zwicker): 100% (3/3)
✅ Electroacoustics: distortion & frequency response: 100% (14/14)
✅ Calibrated spectral analysis (Bendat & Piersol): 100% (6/6)
✅ Correlation, time delay and envelope (B&P / Knapp & Carter): 100% (7/7)
✅ Underwater acoustics (ISO 18405/17208/18406): 100% (6/6)
✅ Underwater sound propagation (transmission loss): 100% (15/15)
✅ Underwater numerical propagation (modes / rays / PE): 100% (4/4)
✅ Aircraft noise (ICAO Annex 16 / IEC 61265): 100% (14/14)
✅ Rotorcraft noise (ECAC Doc 32 / NORAH2): 100% (12/12)
✅ Wind-turbine noise (IEC 61400-11): 100% (3/3)
✅ Porous & multilayer absorbers (Mechel / Bies / Cox & D'Antonio): 100% (10/10)
✅ Program loudness (ITU-R BS.1770 / EBU R 128): 100% (8/8)
✅ 2D FDTD wave simulation (Attenborough & Van Renterghem 2021, Ch. 4): 100% (2/2)
✅ Swept-sine distortion & phase utilities (Farina / Novak): 100% (7/7)
✅ Spherical ground & barriers (Attenborough / Salomons / Bies): 100% (6/6)
✅ Panel & aperture sound insulation (Bies / Hopkins / Cremer): 100% (11/11)
✅ Atmospheric refraction (Salomons rays / GFPE): 100% (3/3)
✅ Electroacoustics: 100% (6/6)
✅ Industrial noise control: 100% (9/9)
Tests & coverage — 24912 tests, 0 failures (✅ all green)
Conformance harness: |
… plots
Add a `language` keyword ("en" default, "es") to the `.plot()` methods of
every psychoacoustics, hearing and vibration result. Spanish renders the axis
labels, titles and legends in Spanish and switches the decimal separator to a
comma; the English output is unchanged.
- New `phonometry._i18n` module with the shared locale helpers (language
validation, locale-aware number formatting and linear-axis tick localisation).
- Each `_plot` renderer gains a module-level string table and localises its
fixed text; the `_i18n` helpers are imported lazily inside the functions so
the render leaves keep no module-level domain imports.
- Per-domain tests covering a Spanish label and the unknown-language error.
- Regenerated the API reference for the new `.plot()` signatures.
8814f70 to
02b8f4e
Compare
|
Thanks for the review. Applied in the latest push (after rebasing onto
Verified: 814 domain tests pass, committed English figures are byte-identical, ruff and the package-architecture test are clean. |
|
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/phonometry/_plot/hearing.py`:
- Around line 34-119: Move the shared localization lookup/fallback logic from
hearing.py’s _t helper into the existing _i18n.py module as a generic translate
helper accepting a strings table, key, and language. Update hearing.py’s _t and
the corresponding helper in psychoacoustics.py to delegate to translate while
preserving domain-specific _STRINGS tables and English fallback behavior.
- Around line 284-285: Translate the raw sex value before formatting both
localized plot titles: add or reuse a helper such as _sex and pass its result to
iso7029_title at src/phonometry/_plot/hearing.py lines 284-285 and htlan_title
at lines 359-362. Support the existing "male" and "female" values for English
and Spanish, preserving the raw domain values elsewhere.
In `@src/phonometry/_plot/vibration.py`:
- Around line 246-249: Update the EAV and ELV legend labels in the vibration
plotting code to use localized acronym strings from _STRINGS, mapping them to
the Spanish VLA and VLE translations while preserving the existing numeric
formatting and legend structure.
- Around line 252-256: Localize interpolated properties before rendering titles:
in the daily exposure title construction around kind and zone, pass
assessment.kind and assessment.zone through _t() using their corresponding
_STRINGS keys; likewise, in the injury_title construction, localize result.sex
through _t() before interpolation. Apply these changes at
src/phonometry/_plot/vibration.py lines 252-256 and 405 respectively.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 89db9545-4128-46f4-a990-fe6a5ec32c5d
📒 Files selected for processing (49)
CHANGELOG.mdsite/src/content/docs/reference/api/hearing/noise-induced-hearing-loss.mdsite/src/content/docs/reference/api/hearing/occupational-exposure.mdsite/src/content/docs/reference/api/hearing/threshold.mdsite/src/content/docs/reference/api/psychoacoustics/fluctuation-strength-ecma.mdsite/src/content/docs/reference/api/psychoacoustics/fluctuation-strength.mdsite/src/content/docs/reference/api/psychoacoustics/loudness-ecma.mdsite/src/content/docs/reference/api/psychoacoustics/loudness-moore-glasberg-time.mdsite/src/content/docs/reference/api/psychoacoustics/loudness-moore-glasberg.mdsite/src/content/docs/reference/api/psychoacoustics/loudness-zwicker.mdsite/src/content/docs/reference/api/psychoacoustics/psychoacoustic-annoyance.mdsite/src/content/docs/reference/api/psychoacoustics/roughness-ecma.mdsite/src/content/docs/reference/api/psychoacoustics/tonality-ecma.mdsite/src/content/docs/reference/api/psychoacoustics/tone-audibility.mdsite/src/content/docs/reference/api/speech/objective-intelligibility.mdsite/src/content/docs/reference/api/speech/sii.mdsite/src/content/docs/reference/api/speech/sti.mdsite/src/content/docs/reference/api/vibration/human-vibration.mdsite/src/content/docs/reference/api/vibration/mechanical-mobility.mdsite/src/content/docs/reference/api/vibration/multiple-shock-vibration.mdsite/src/content/docs/reference/api/vibration/radiation-efficiency.mdsite/src/content/docs/reference/api/vibration/transfer-stiffness.mdsrc/phonometry/_plot/hearing.pysrc/phonometry/_plot/psychoacoustics.pysrc/phonometry/_plot/vibration.pysrc/phonometry/hearing/noise_induced_hearing_loss.pysrc/phonometry/hearing/objective_intelligibility.pysrc/phonometry/hearing/occupational_exposure.pysrc/phonometry/hearing/sii.pysrc/phonometry/hearing/sti.pysrc/phonometry/hearing/threshold.pysrc/phonometry/psychoacoustics/fluctuation_strength.pysrc/phonometry/psychoacoustics/fluctuation_strength_ecma.pysrc/phonometry/psychoacoustics/loudness_ecma.pysrc/phonometry/psychoacoustics/loudness_moore_glasberg.pysrc/phonometry/psychoacoustics/loudness_moore_glasberg_time.pysrc/phonometry/psychoacoustics/loudness_zwicker.pysrc/phonometry/psychoacoustics/psychoacoustic_annoyance.pysrc/phonometry/psychoacoustics/roughness_ecma.pysrc/phonometry/psychoacoustics/tonality_ecma.pysrc/phonometry/psychoacoustics/tone_audibility.pysrc/phonometry/vibration/human_vibration.pysrc/phonometry/vibration/mechanical_mobility.pysrc/phonometry/vibration/multiple_shock_vibration.pysrc/phonometry/vibration/radiation_efficiency.pysrc/phonometry/vibration/transfer_stiffness.pytests/hearing/test_hearing_plot_i18n.pytests/psychoacoustics/test_psychoacoustics_plot_i18n.pytests/vibration/test_vibration_plot_i18n.py
| #: EN/ES text for every fixed label, title and legend of this module. The | ||
| #: English entries are byte-for-byte the historical strings; ``_t`` returns | ||
| #: them unchanged for ``language="en"``. | ||
| _STRINGS: dict[str, dict[str, str]] = { | ||
| "freq_hz": {"en": "Frequency [Hz]", "es": "Frecuencia [Hz]"}, | ||
| "band": {"en": "Band", "es": "Banda"}, | ||
| "mti": { | ||
| "en": "Modulation transfer index MTI", | ||
| "es": "Índice de transferencia de modulación MTI", | ||
| }, | ||
| "third_octave_band": { | ||
| "en": "One-third-octave band [Hz]", | ||
| "es": "Banda de tercio de octava [Hz]", | ||
| }, | ||
| "mean_corr": { | ||
| "en": "Mean intermediate correlation", | ||
| "es": "Correlación intermedia media", | ||
| }, | ||
| "spectral_corr": { | ||
| "en": r"Spectral correlation $d_m$", | ||
| "es": r"Correlación espectral $d_m$", | ||
| }, | ||
| "analysis_segment": {"en": "Analysis segment", "es": "Segmento de análisis"}, | ||
| "band_audibility": {"en": "Band audibility", "es": "Audibilidad de banda"}, | ||
| "band_audibility_ai": { | ||
| "en": r"Band audibility $A_i$", | ||
| "es": r"Audibilidad de banda $A_i$", | ||
| }, | ||
| "importance_weighted": { | ||
| "en": r"Importance-weighted $I_i A_i$ (scaled)", | ||
| "es": r"$I_i A_i$ ponderada por importancia (escalada)", | ||
| }, | ||
| "median": {"en": "Median", "es": "Mediana"}, | ||
| "median_n50": {"en": r"Median $N_{50}$", "es": r"Mediana $N_{50}$"}, | ||
| "threshold_dev_18": { | ||
| "en": "Threshold deviation from age 18 [dB]", | ||
| "es": "Desviación del umbral respecto a 18 años [dB]", | ||
| }, | ||
| "nipts_db": {"en": "NIPTS [dB]", "es": "NIPTS [dB]"}, | ||
| "htla_age": {"en": "Age (HTLA, ISO 7029)", "es": "Edad (HTLA, ISO 7029)"}, | ||
| "noise_nipts": {"en": "Noise (NIPTS)", "es": "Ruido (NIPTS)"}, | ||
| "age_noise_htlan": { | ||
| "en": "Age + noise (HTLAN)", | ||
| "es": "Edad + ruido (HTLAN)", | ||
| }, | ||
| "htl_level": { | ||
| "en": "Hearing threshold level [dB]", | ||
| "es": "Nivel del umbral de audición [dB]", | ||
| }, | ||
| "a_weighted_level": { | ||
| "en": "A-weighted level [dB]", | ||
| "es": "Nivel ponderado A [dB]", | ||
| }, | ||
| # Templates (``.format`` fields hold already-localised numbers). | ||
| "sti_title": { | ||
| "en": "IEC 60268-16 STI = {sti} (rating {rating})", | ||
| "es": "IEC 60268-16 STI = {sti} (calificación {rating})", | ||
| }, | ||
| "stoi_title": {"en": "{name} = {v}", "es": "{name} = {v}"}, | ||
| "sii_title": {"en": "ANSI S3.5 SII = {sii}", "es": "ANSI S3.5 SII = {sii}"}, | ||
| "fractile": {"en": "Fractile {v}", "es": "Fractil {v}"}, | ||
| "iso7029_title": { | ||
| "en": "ISO 7029 hearing threshold — {sex}, age {age}", | ||
| "es": "ISO 7029 umbral de audición — {sex}, edad {age}", | ||
| }, | ||
| "nipts_title": { | ||
| "en": "ISO 1999 NIPTS — $L_{{EX,8h}}$ = {lex} dB, {years} yr", | ||
| "es": "ISO 1999 NIPTS — $L_{{EX,8h}}$ = {lex} dB, {years} años", | ||
| }, | ||
| "htlan_title": { | ||
| "en": "ISO 1999 HTLAN — {sex}, age {age}, {lex} dB / {years} yr", | ||
| "es": "ISO 1999 HTLAN — {sex}, edad {age}, {lex} dB / {years} años", | ||
| }, | ||
| "occupational_title": { | ||
| "en": ("ISO 9612 daily noise exposure — $L_{{EX,8h}}$ = " | ||
| "{lex} dB (U = {u} dB)"), | ||
| "es": ("ISO 9612 exposición diaria al ruido — $L_{{EX,8h}}$ = " | ||
| "{lex} dB (U = {u} dB)"), | ||
| }, | ||
| } | ||
|
|
||
|
|
||
| def _t(key: str, language: str) -> str: | ||
| """Look up the localised text for ``key`` (falls back to English).""" | ||
| entry = _STRINGS[key] | ||
| return entry.get(language, entry["en"]) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial
Consider sharing the _t lookup helper via _i18n.py.
The entry.get(language, entry["en"]) pattern in _t (Line 116-119) is duplicated verbatim across plot modules (e.g. _plot/psychoacoustics.py also defines its own _t). Each module's _STRINGS table is naturally domain-specific, but the lookup/fallback logic itself could be a single generic helper in _i18n.py (e.g. translate(strings, key, language)) that every plot module reuses with its own dict.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/phonometry/_plot/hearing.py` around lines 34 - 119, Move the shared
localization lookup/fallback logic from hearing.py’s _t helper into the existing
_i18n.py module as a generic translate helper accepting a strings table, key,
and language. Update hearing.py’s _t and the corresponding helper in
psychoacoustics.py to delegate to translate while preserving domain-specific
_STRINGS tables and English fallback behavior.
| ax.set_title(_t("iso7029_title", language).format( | ||
| sex=result.sex, age=decimal_comma(f"{result.age:g}", language))) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Spanish titles leave sex untranslated ("male"/"female" literals in Spanish text).
Both plot_age_threshold's iso7029_title and plot_htlan's htlan_title format sex=result.sex directly into the title, but AgeThresholdResult.sex/HtlanResult.sex only ever hold the raw domain string "male"/"female" — never localized. For language="es" this renders e.g. "ISO 7029 umbral de audición — male, edad 60", mixing English into an otherwise Spanish title, contrary to the PR's goal of fully localized titles. The existing test (tests/hearing/test_hearing_plot_i18n.py) only checks a substring of the title, so it doesn't catch this.
src/phonometry/_plot/hearing.py#L284-L285: translateresult.sex(e.g. via a small{"male": {"en": "male", "es": "hombre"}, "female": {...}}lookup similar to_t) before formattingiso7029_title.src/phonometry/_plot/hearing.py#L359-L362: apply the samesextranslation before formattinghtlan_title.
🌐 Proposed fix sketch
+_SEX_LABELS: dict[str, dict[str, str]] = {
+ "male": {"en": "male", "es": "hombre"},
+ "female": {"en": "female", "es": "mujer"},
+}
+
+
+def _sex(value: str, language: str) -> str:
+ return _SEX_LABELS.get(value, {}).get(language, value)Then use sex=_sex(result.sex, language) in both iso7029_title and htlan_title .format() calls.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ax.set_title(_t("iso7029_title", language).format( | |
| sex=result.sex, age=decimal_comma(f"{result.age:g}", language))) | |
| _SEX_LABELS: dict[str, dict[str, str]] = { | |
| "male": {"en": "male", "es": "hombre"}, | |
| "female": {"en": "female", "es": "mujer"}, | |
| } | |
| def _sex(value: str, language: str) -> str: | |
| return _SEX_LABELS.get(value, {}).get(language, value) |
📍 Affects 1 file
src/phonometry/_plot/hearing.py#L284-L285(this comment)src/phonometry/_plot/hearing.py#L359-L362
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/phonometry/_plot/hearing.py` around lines 284 - 285, Translate the raw
sex value before formatting both localized plot titles: add or reuse a helper
such as _sex and pass its result to iso7029_title at
src/phonometry/_plot/hearing.py lines 284-285 and htlan_title at lines 359-362.
Support the existing "male" and "female" values for English and Spanish,
preserving the raw domain values elsewhere.
| ax.axhline(eav, color=_C_SECONDARY, ls="--", | ||
| label="EAV = " + decimal_comma(f"{eav:g}", language)) | ||
| ax.axhline(elv, color=_C_REFERENCE, ls="--", | ||
| label="ELV = " + decimal_comma(f"{elv:g}", language)) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
Consider localizing the EAV and ELV acronyms.
EAV (Exposure Action Value) and ELV (Exposure Limit Value) are English acronyms. In the official Spanish translation of Directive 2002/44/EC, these are typically referred to as VLA (Valor que da lugar a una acción) and VLE (Valor límite de exposición). You may want to fetch these from _STRINGS so the legend is fully localized.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/phonometry/_plot/vibration.py` around lines 246 - 249, Update the EAV and
ELV legend labels in the vibration plotting code to use localized acronym
strings from _STRINGS, mapping them to the Spanish VLA and VLE translations
while preserving the existing numeric formatting and legend structure.
| kind = str(assessment.kind).upper() | ||
| ax.set_title( | ||
| f"Directive 2002/44/EC daily {kind} exposure " | ||
| f"(A(8) = {float(result.a8):.2f} " rf"m/s$^2$, {assessment.zone})" | ||
| ) | ||
| ax.set_title(_t("daily_exposure_title", language).format( | ||
| kind=kind, a8=format_number(float(result.a8), language, decimals=2), | ||
| zone=assessment.zone, | ||
| )) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Localize interpolated string properties to prevent mixed-language output.
When localized string templates are populated with unlocalized properties, the resulting Spanish text contains untranslated English words. The affected properties should be mapped through _STRINGS using _t() before interpolation.
src/phonometry/_plot/vibration.py#L252-L256: localizeassessment.kind(e.g.,"hand-arm") andassessment.zone(e.g.,"below action value") before passing them to thedaily_exposure_titletemplate.src/phonometry/_plot/vibration.py#L405-L405: localizeresult.sex("male"or"female") before passing it to theinjury_titletemplate.
📍 Affects 1 file
src/phonometry/_plot/vibration.py#L252-L256(this comment)src/phonometry/_plot/vibration.py#L405-L405
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/phonometry/_plot/vibration.py` around lines 252 - 256, Localize
interpolated properties before rendering titles: in the daily exposure title
construction around kind and zone, pass assessment.kind and assessment.zone
through _t() using their corresponding _STRINGS keys; likewise, in the
injury_title construction, localize result.sex through _t() before
interpolation. Apply these changes at src/phonometry/_plot/vibration.py lines
252-256 and 405 respectively.



Summary
The
.plot()methods of the psychoacoustics, hearing and vibration results now accept alanguagekeyword."en"is the default and its output is unchanged;"es"renders the axis labels, titles and legends in Spanish and switches the decimal separator to a comma.What changed
phonometry._i18nmodule with the shared locale helpers: language validation, locale-aware number formatting, and linear-axis tick localisation._plotrenderers (psychoacoustics.py,hearing.py,vibration.py, 24 plot functions in total) gain a module-level string table and localise every fixed label, title and legend. Unit symbols are kept; Spanish terminology follows the convention already used in the site (sonios, fonios, and so on).languagekeyword is threaded through the delegating.plot()methods, which validate it before forwarding._i18nhelpers are imported lazily inside the render functions, so the_plotleaves keep no module-level domain imports (the package-architecture test stays green).ValueError..plot()signatures.Verification
ruff check .,mypy src scripts, andpytest tests/psychoacoustics tests/hearing tests/vibration tests/test_package_architecture.pyall pass (767 passed, 50 skipped).Summary by CodeRabbit
New Features
languageoption to plotting methods, defaulting to English.Documentation
Tests