File size: 4,106 Bytes
ce0bff3
 
 
 
 
 
a0dc23e
eb547cb
a0dc23e
 
eb547cb
 
 
 
 
 
 
 
 
ce0bff3
 
 
 
a0dc23e
eb547cb
 
 
a0dc23e
 
eb547cb
 
a0dc23e
 
eb547cb
 
a0dc23e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eb547cb
a0dc23e
 
 
eb547cb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce0bff3
 
 
 
 
 
 
 
 
 
 
 
 
 
a0dc23e
 
ce0bff3
eb547cb
ce0bff3
 
eb547cb
 
 
 
 
ce0bff3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
"""Labels i18n pour le rapport HTML et l'interface Picarones.

Langues supportées
------------------
- ``"fr"`` : français (défaut)
- ``"en"`` : anglais patrimonial (heritage English)

Depuis le Sprint 17, les traductions sont stockées dans
``picarones/report/i18n/{lang}.json`` et chargées au premier accès.
``TRANSLATIONS`` reste exposé comme dict pour compatibilité ascendante.

Sprint 30 — durcissement
------------------------
- Chargement lazy + thread-safe via verrou explicite ; les serveurs
  web sous charge concurrente ne peuvent plus initialiser deux fois.
- ``reload_translations()`` exposé pour les tests qui modifient les
  fichiers JSON à la volée.
- ``get_labels()`` mémoizé via ``functools.lru_cache`` pour absorber
  le fallback ``lang → fr`` sans relire le dict à chaque appel.
"""

from __future__ import annotations

import json
import logging
import threading
from functools import lru_cache
from pathlib import Path

logger = logging.getLogger(__name__)


_I18N_DIR = Path(__file__).parent / "report" / "i18n"
_LOAD_LOCK = threading.Lock()
_TRANSLATIONS_CACHE: dict[str, dict[str, str]] | None = None


def _load_translations() -> dict[str, dict[str, str]]:
    """Charge tous les fichiers JSON du dossier i18n.

    Un fichier ``{lang}.json`` définit les labels de la langue ``lang``.
    Retourne toujours un dict non-vide, même si le dossier est manquant
    (dans ce cas, le dict est vide et ``get_labels`` tombe sur un fallback).
    """
    translations: dict[str, dict[str, str]] = {}
    if not _I18N_DIR.is_dir():
        return translations
    for path in sorted(_I18N_DIR.glob("*.json")):
        lang = path.stem
        try:
            with path.open(encoding="utf-8") as fh:
                translations[lang] = json.load(fh)
        except (OSError, json.JSONDecodeError) as e:
            logger.warning("[i18n] fichier '%s' ignoré : %s", path, e)
    return translations


def _get_translations() -> dict[str, dict[str, str]]:
    """Retourne le cache de translations, initialisé une seule fois.

    Thread-safe : deux threads qui appellent simultanément en démarrage
    ne déclencheront qu'une seule lecture disque.
    """
    global _TRANSLATIONS_CACHE
    if _TRANSLATIONS_CACHE is not None:
        return _TRANSLATIONS_CACHE
    with _LOAD_LOCK:
        if _TRANSLATIONS_CACHE is None:
            _TRANSLATIONS_CACHE = _load_translations()
    return _TRANSLATIONS_CACHE


def reload_translations() -> None:
    """Force la relecture des fichiers JSON au prochain ``get_labels``.

    Utile pour les tests qui modifient ``report/i18n/*.json`` à la volée.
    """
    global _TRANSLATIONS_CACHE
    with _LOAD_LOCK:
        _TRANSLATIONS_CACHE = None
    _get_labels_cached.cache_clear()


@lru_cache(maxsize=None)
def _get_labels_cached(lang: str) -> tuple[tuple[str, str], ...]:
    """Cache mémoïsé : ``lang -> tuple ordonné des paires``.

    Le retour en tuple permet à ``lru_cache`` de mémoriser sans
    contrainte de hashabilité, et est trivialement converti en dict
    par ``get_labels`` à chaque appel (coût O(n)).
    """
    translations = _get_translations()
    labels = translations.get(lang) or translations.get("fr") or {}
    return tuple(labels.items())


def get_labels(lang: str = "fr") -> dict[str, str]:
    """Retourne le dictionnaire de labels pour la langue donnée.

    Parameters
    ----------
    lang:
        Code langue : ``"fr"`` (défaut) ou ``"en"``.

    Returns
    -------
    dict
        Labels traduits. Toujours valide : bascule sur ``"fr"`` si lang inconnu.
        Si ``"fr"`` lui-même manque, retourne un dict vide (comportement dégradé
        mais non bloquant).
    """
    return dict(_get_labels_cached(lang))


# ``TRANSLATIONS`` reste accessible comme attribut module pour les
# consommateurs externes qui le lisaient directement. Initialisé
# paresseusement à l'import — n'engendre **pas** de lecture si le
# module n'est jamais utilisé.
TRANSLATIONS: dict[str, dict[str, str]] = _get_translations()
SUPPORTED_LANGS: list[str] = list(TRANSLATIONS.keys())