1# $Id$
2# Author: David Goodger <goodger@python.org>
3# Copyright: This module has been placed in the public domain.
4
5# Internationalization details are documented in
6# <https://docutils.sourceforge.io/docs/howto/i18n.html>.
7
8"""
9This package contains modules for language-dependent features of Docutils.
10"""
11
12from __future__ import annotations
13
14__docformat__ = 'reStructuredText'
15
16from importlib import import_module
17from typing import TYPE_CHECKING, overload
18
19from docutils.utils import normalize_language_tag
20
21if TYPE_CHECKING:
22 import types
23 from typing import NoReturn, Protocol, TypeVar
24
25 from docutils.utils import Reporter
26
27 class LanguageModule(Protocol):
28 __name__: str
29
30 labels: dict[str, str]
31 bibliographic_fields: dict[str, str]
32 author_separators: list[str]
33
34 LanguageModuleT = TypeVar('LanguageModuleT')
35
36
37class LanguageImporter:
38 """Import language modules.
39
40 When called with a BCP 47 language tag, instances return a module
41 with localisations from `docutils.languages` or the PYTHONPATH.
42
43 If there is no matching module, warn (if a `reporter` is passed)
44 and fall back to English.
45 """
46 packages = ('docutils.languages.', '')
47 warn_msg = ('Language "%s" not supported: '
48 'Docutils-generated text will be in English.')
49 fallback = 'en'
50 # TODO: use a dummy module returning empty strings?, configurable?
51
52 def __init__(self) -> None:
53 self.cache: dict[str, LanguageModuleT] = {}
54
55 def import_from_packages(self, name: str, reporter: Reporter = None
56 ) -> LanguageModuleT:
57 """Try loading module `name` from `self.packages`."""
58 module = None
59 for package in self.packages:
60 try:
61 module = import_module(package + name)
62 self.check_content(module)
63 except (ImportError, AttributeError):
64 if reporter and module:
65 reporter.info(f'{module} is no complete '
66 'Docutils language module.')
67 elif reporter:
68 reporter.info(f'Module "{package+name}" not found.')
69 continue
70 break
71 return module
72
73 @overload
74 def check_content(self, module: LanguageModule) -> None:
75 ...
76
77 @overload
78 def check_content(self, module: types.ModuleType) -> NoReturn:
79 ...
80
81 def check_content(self, module: LanguageModule | types.ModuleType) -> None:
82 """Check if we got a Docutils language module."""
83 if not (
84 isinstance(module.labels, dict)
85 and isinstance(module.bibliographic_fields, dict)
86 and isinstance(module.author_separators, list)
87 ):
88 raise ImportError
89
90 def __call__(self, language_code: str, reporter: Reporter = None
91 ) -> LanguageModuleT:
92 try:
93 return self.cache[language_code]
94 except KeyError:
95 pass
96 for tag in normalize_language_tag(language_code):
97 tag = tag.replace('-', '_') # '-' not valid in module names
98 module = self.import_from_packages(tag, reporter)
99 if module is not None:
100 break
101 else:
102 if reporter:
103 reporter.warning(self.warn_msg % language_code)
104 if self.fallback:
105 module = self.import_from_packages(self.fallback)
106 if reporter and (language_code != 'en'):
107 reporter.info(f'Using {module} for language "{language_code}".')
108 self.cache[language_code] = module
109 return module
110
111 def __class_getitem__(cls, name):
112 return cls
113
114
115get_language: LanguageImporter[LanguageModule] = LanguageImporter()