-
Notifications
You must be signed in to change notification settings - Fork 48
Expand file tree
/
Copy pathi18n.py
More file actions
88 lines (72 loc) · 3.34 KB
/
Copy pathi18n.py
File metadata and controls
88 lines (72 loc) · 3.34 KB
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
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (C) 2026 lollapalooza <https://github.com/aqua5230>
#
# Part of "usage". Free software licensed under the GNU Affero General Public
# License v3.0 only; see the LICENSE file for full terms and the warranty disclaimer.
from __future__ import annotations
import json
import os
import sys
import sysconfig
from functools import lru_cache
from pathlib import Path
from usage_lang import detect_lang
def packaged_resource_path(filename: str, source_mode_path: Path) -> Path:
"""Resolve a data file across source-mode and py2app-bundle layouts.
py2app declares data files via setup_app.py ``OPTIONS["resources"]`` and
copies them to ``Contents/Resources/`` — adjacent to ``lib/python313.zip``,
not inside it. py2app injects the ``RESOURCEPATH`` env var at launch
pointing at that directory; we prefer it when present.
Why this exists: in py2app builds this module is compiled into
``lib/python313.zip``, so ``Path(__file__).with_name("i18n.json")``
resolves to ``lib/python313.zip/i18n.json`` — an invalid path through
the zipfile that raises ``NotADirectoryError`` at first read. In source
mode (and tests) ``RESOURCEPATH`` is unset and the source-adjacent
fallback path is correct. Wheels installed by pip or uvx place data files
under the interpreter's sysconfig data directory, so that location is
checked before the source fallback.
The callers pass the source-mode path explicitly (as the literal
``Path(__file__).with_name("...")``) so that
``tests/test_packaged_resources.py`` can still statically detect every
declared resource and enforce that ``setup_app.py`` lists it.
"""
resource_root = os.environ.get("RESOURCEPATH")
if resource_root:
bundled = Path(resource_root) / filename
if bundled.exists():
return bundled
frozen_root = getattr(sys, "_MEIPASS", None)
if frozen_root:
bundled = Path(frozen_root) / filename
if bundled.exists():
return bundled
wheel_data = Path(sysconfig.get_path("data")) / "share" / "usage" / filename
if wheel_data.exists():
return wheel_data
return source_mode_path
I18N_PATH = packaged_resource_path("i18n.json", Path(__file__).with_name("i18n.json"))
@lru_cache(maxsize=1)
def _load_i18n_bundle() -> dict[str, dict[str, str]]:
data = json.loads(I18N_PATH.read_text(encoding="utf-8"))
return {
str(lang): {str(key): str(value) for key, value in values.items()}
for lang, values in data.items()
}
def _t(language: str, key: str, **kwargs: object) -> str:
bundle = _load_i18n_bundle()
table = bundle.get(language) or bundle["en"]
template = table.get(key) or bundle["en"].get(key) or key
try:
return template.format(**kwargs)
except (KeyError, IndexError, ValueError, TypeError):
# A malformed placeholder in one locale's string must not crash the UI;
# fall back to the English template, then to the raw key.
en_template = bundle["en"].get(key)
if en_template is not None and en_template != template:
try:
return en_template.format(**kwargs)
except (KeyError, IndexError, ValueError, TypeError):
pass
return key
def t(key: str, **kwargs: object) -> str:
return _t(detect_lang(), key, **kwargs)