1"""Server functions for loading translations"""
2
3from __future__ import annotations
4
5import errno
6import json
7import re
8from collections import defaultdict
9from os.path import dirname
10from os.path import join as pjoin
11from typing import Any
12
13I18N_DIR = dirname(__file__)
14# Cache structure:
15# {'nbjs': { # Domain
16# 'zh-CN': { # Language code
17# <english string>: <translated string>
18# ...
19# }
20# }}
21TRANSLATIONS_CACHE: dict[str, Any] = {"nbjs": {}}
22
23
24_accept_lang_re = re.compile(
25 r"""
26(?P<lang>[a-zA-Z]{1,8}(-[a-zA-Z]{1,8})?)
27(\s*;\s*q\s*=\s*
28 (?P<qvalue>[01](.\d+)?)
29)?""",
30 re.VERBOSE,
31)
32
33
34def parse_accept_lang_header(accept_lang):
35 """Parses the 'Accept-Language' HTTP header.
36
37 Returns a list of language codes in *ascending* order of preference
38 (with the most preferred language last).
39 """
40 by_q = defaultdict(list)
41 for part in accept_lang.split(","):
42 m = _accept_lang_re.match(part.strip())
43 if not m:
44 continue
45 lang, qvalue = m.group("lang", "qvalue")
46 # Browser header format is zh-CN, gettext uses zh_CN
47 lang = lang.replace("-", "_")
48 qvalue = 1.0 if qvalue is None else float(qvalue)
49 if qvalue == 0:
50 continue # 0 means not accepted
51 by_q[qvalue].append(lang)
52
53 res = []
54 for _, langs in sorted(by_q.items()):
55 res.extend(sorted(langs))
56 return res
57
58
59def load(language, domain="nbjs"):
60 """Load translations from an nbjs.json file"""
61 try:
62 f = open(pjoin(I18N_DIR, language, "LC_MESSAGES", "nbjs.json"), encoding="utf-8") # noqa: SIM115
63 except OSError as e:
64 if e.errno != errno.ENOENT:
65 raise
66 return {}
67
68 with f:
69 data = json.load(f)
70 return data["locale_data"][domain]
71
72
73def cached_load(language, domain="nbjs"):
74 """Load translations for one language, using in-memory cache if available"""
75 domain_cache = TRANSLATIONS_CACHE[domain]
76 try:
77 return domain_cache[language]
78 except KeyError:
79 data = load(language, domain)
80 domain_cache[language] = data
81 return data
82
83
84def combine_translations(accept_language, domain="nbjs"):
85 """Combine translations for multiple accepted languages.
86
87 Returns data re-packaged in jed1.x format.
88 """
89 lang_codes = parse_accept_lang_header(accept_language)
90 combined: dict[str, Any] = {}
91 for language in lang_codes:
92 if language == "en":
93 # en is default, all translations are in frontend.
94 combined.clear()
95 else:
96 combined.update(cached_load(language, domain))
97
98 combined[""] = {"domain": "nbjs"}
99
100 return {"domain": domain, "locale_data": {domain: combined}}