Skip to content

Commit 22a64c5

Browse files
committed
Allow dictionaries as translation sources
1 parent e9c3664 commit 22a64c5

6 files changed

Lines changed: 128 additions & 36 deletions

File tree

README.md

Lines changed: 100 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -158,37 +158,37 @@ Library ${CURDIR}/PluginLib.py plugins=${CURDIR}/MyPlugin.py
158158

159159
# Translation
160160

161-
PLC supports translation of keywords names and documentation, but arguments names, tags and types
162-
can not be currently translated. Translation is provided as a file containing
163-
[Json](https://www.json.org/json-en.html) and as a
164-
[Path](https://docs.python.org/3/library/pathlib.html) object. Translation is provided in
165-
`translation` argument in the `HybridCore` or `DynamicCore` `__init__`. Providing translation
166-
file is optional, also it is not mandatory to provide translation to all keyword.
161+
PLC supports translation of keywords names and documentation. Translations must be provided as
162+
[JSON](https://www.json.org/json-en.html) data in the `translation` argument in the `HybridCore`
163+
or `DynamicCore` `__init__`, either directly or through a
164+
[Path](https://docs.python.org/3/library/pathlib.html) to a file. Providing translation data is
165+
optional, also it is not mandatory to provide translation to all keyword.
167166

168167
The keys of json are the methods names, not the keyword names, which implements keyword. Value
169168
of key is json object which contains two keys: `name` and `doc`. `name` key contains the keyword
170169
translated name and `doc` contains keyword translated documentation. Providing
171170
`doc` and `name` is optional, example translation json file can only provide translations only
172-
to keyword names or only to documentatin. But it is always recomended to provide translation to
171+
to keyword names or only to documentation. But it is always recommended to provide translation to
173172
both `name` and `doc`.
174173

175-
Library class documentation and instance documetation has special keys, `__init__` key will
176-
replace instance documentation and `__intro__` will replace libary class documentation.
174+
Library class documentation and instance documentation has special keys, `__init__` key will
175+
replace instance documentation and `__intro__` will replace library class documentation.
176+
177+
> [!NOTE]
178+
> Arguments names, tags and types can not be currently translated.
177179
178180
## Example
179181

180182
If there is library like this:
181183
```python
182-
from pathlib import Path
183-
184184
from robotlibcore import DynamicCore, keyword
185185

186186
class SmallLibrary(DynamicCore):
187187
"""Library documentation."""
188188

189-
def __init__(self, translation: Path):
189+
def __init__(self):
190190
"""__init__ documentation."""
191-
DynamicCore.__init__(self, [], translation.absolute())
191+
DynamicCore.__init__(self, [])
192192

193193
@keyword(tags=["tag1", "tag2"])
194194
def normal_keyword(self, arg: int, other: str) -> str:
@@ -212,8 +212,22 @@ class SmallLibrary(DynamicCore):
212212
return some + other
213213
```
214214

215-
And when there is translation file like:
216-
```json
215+
And we want to translate it as follows:
216+
217+
- keyword `normal_keyword` to `other_name`
218+
- its documentation to `This is new doc`
219+
- keyword `name_changed` to `name_changed_again`
220+
- its documentation to `This is also replaced.\n\nnew line.`.
221+
- the library constructor documentation to `Replaces init docs with this one.`
222+
- the library documentation to `New __intro__ documentation is here.`
223+
224+
225+
### Provide Translation As File
226+
227+
To provide the translation as a file, simply pass the path to a JSON file containing the translations:
228+
229+
```jsonc
230+
// my_translation.json
217231
{
218232
"normal_keyword": {
219233
"name": "other_name",
@@ -230,12 +244,76 @@ And when there is translation file like:
230244
"__intro__": {
231245
"name": "__intro__",
232246
"doc": "New __intro__ documentation is here."
233-
},
247+
}
234248
}
235249
```
236-
Then `normal_keyword` is translated to `other_name`. Also this keyword documentions is
237-
translted to `This is new doc`. The keyword is `name_changed` is translted to
238-
`name_changed_again` keyword and keyword documentation is translted to
239-
`This is also replaced.\n\nnew line.`. The library class documentation is translated
240-
to `Replaces init docs with this one.` and class documentation is translted to
241-
`New __intro__ documentation is here.`
250+
251+
```python
252+
from pathlib import Path
253+
254+
class SmallLibrary(DynamicCore):
255+
"""Library documentation."""
256+
257+
def __init__(self):
258+
"""__init__ documentation."""
259+
DynamicCore.__init__(self, [], translation=Path("/path/to/my_translation.json"))
260+
261+
# ...
262+
```
263+
264+
> [!IMPORTANT]
265+
> Translation files passed as paths must always be in JSON format.
266+
267+
### Provide Translation As Dictionary
268+
269+
You can also pass the translation data as a dictionary:
270+
271+
```python
272+
import json
273+
from pathlib import Path
274+
275+
class SmallLibrary(DynamicCore):
276+
"""Library documentation."""
277+
278+
def __init__(self):
279+
"""__init__ documentation."""
280+
translation_data = json.loads(Path("/path/to/my_translation.json").read_text(encoding="utf-8"))
281+
DynamicCore.__init__(self, [], translation=translation_data)
282+
283+
# ...
284+
```
285+
286+
This also allows you to use other data formats such as YAML:
287+
288+
```yaml
289+
normal_keyword:
290+
name: other_name
291+
doc: This is new doc
292+
name_changed:
293+
name: name_changed_again
294+
doc: |
295+
This is also replaced.
296+
297+
new line.
298+
__init__:
299+
name: __init__
300+
doc: Replaces init docs with this one.
301+
__intro__:
302+
name: __intro__
303+
doc: New __intro__ documentation is here.
304+
```
305+
306+
```python
307+
import yaml
308+
from pathlib import Path
309+
310+
class SmallLibrary(DynamicCore):
311+
"""Library documentation."""
312+
313+
def __init__(self, translation_file: Path):
314+
"""__init__ documentation."""
315+
translation_data = yaml.safe_load(translation_file.read_text(encoding="utf-8"))
316+
DynamicCore.__init__(self, [], translation=translation_data)
317+
318+
# ...
319+
```

atest/SmallLibrary.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from pathlib import Path
2-
from typing import Optional
2+
from typing import Optional, Union
33

44
from robot.api import logger
55
from robotlibcore import DynamicCore, keyword
@@ -14,12 +14,14 @@ def execute_something(self):
1414
class SmallLibrary(DynamicCore):
1515
"""Library documentation."""
1616

17-
def __init__(self, translation: Optional[Path] = None):
17+
def __init__(self, translation: Optional[Union[Path, dict]] = None):
1818
"""__init__ documentation."""
19-
if not isinstance(translation, Path):
19+
if isinstance(translation, (dict, Path)):
20+
DynamicCore.__init__(self, [KeywordClass()], translation)
21+
else:
2022
logger.warn("Convert to Path")
2123
translation = Path(translation)
22-
DynamicCore.__init__(self, [KeywordClass()], translation.absolute())
24+
DynamicCore.__init__(self, [KeywordClass()], translation.absolute())
2325

2426
@keyword(tags=["tag1", "tag2"])
2527
def normal_keyword(self, arg: int, other: str) -> str:

atest/tests_types.robot

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
Library DynamicTypesLibrary.py
33
Library DynamicTypesAnnotationsLibrary.py xxx
44
Library SmallLibrary.py ${CURDIR}/translation.json
5+
Library SmallLibrary.py ${ALL_TRANSLATIONS} AS TranslatedLibraryDict
56

67

78
*** Variables ***
89
${CUSTOM NONE} = ${None}
9-
10+
&{TRANSLATION_1}= name=Name Changed Through Dict doc=A translated docstring.
11+
&{ALL_TRANSLATIONS}= name_changed=${TRANSLATION_1}
1012

1113
*** Test Cases ***
1214
Keyword Default Argument As Abject None
@@ -122,6 +124,10 @@ SmallLibray With New Name
122124
${data} = SmallLibrary.name_changed_again 1 2
123125
Should Be Equal As Integers ${data} 3
124126

127+
TranslatedLibraryDict With New Name
128+
${data} = TranslatedLibraryDict.Name Changed Through Dict 1 2
129+
Should Be Equal As Integers ${data} 3
130+
125131

126132
*** Keywords ***
127133
Import DynamicTypesAnnotationsLibrary In Python 3.10 Only

src/robotlibcore/core/hybrid.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323

2424
class HybridCore:
25-
def __init__(self, library_components: list, translation: Path | None = None) -> None:
25+
def __init__(self, library_components: list, translation: Path | dict | None = None) -> None:
2626
self.keywords: dict = {}
2727
self.keywords_spec: dict = {}
2828
self.attributes: dict = {}

src/robotlibcore/utils/translations.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,16 @@
1919
from robot.api import logger
2020

2121

22-
def _translation(translation: Path | None = None):
23-
if translation and isinstance(translation, Path) and translation.is_file():
22+
def _translation(translation: Path | dict | None = None):
23+
if isinstance(translation, Path) and translation.is_file():
2424
with translation.open("r", encoding="utf-8") as file:
2525
try:
2626
return json.load(file)
2727
except json.decoder.JSONDecodeError:
2828
logger.warn(f"Could not convert json file {translation} to dictionary.")
2929
return {}
30+
elif isinstance(translation, dict):
31+
return translation
3032
else:
3133
return {}
3234

utest/test_translations.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1+
import json
12
from pathlib import Path
23

34
import pytest
45
from SmallLibrary import SmallLibrary
56

67

7-
@pytest.fixture(scope="module")
8-
def lib():
8+
@pytest.fixture(scope="module", params=["path", "dict"])
9+
def lib(request):
910
translation = Path(__file__).parent.parent / "atest" / "translation.json"
10-
return SmallLibrary(translation=translation)
11+
if request.param == "path":
12+
return SmallLibrary(translation=translation)
13+
if request.param == "dict":
14+
json_data = json.loads(translation.read_text(encoding="utf-8"))
15+
return SmallLibrary(translation=json_data)
16+
raise ValueError(request.param)
1117

1218

1319
def test_invalid_translation():
@@ -59,9 +65,7 @@ def test_kw_not_translated_but_doc_is(lib: SmallLibrary):
5965
assert doc == "Here is new doc"
6066

6167

62-
def test_rf_name_not_in_keywords():
63-
translation = Path(__file__).parent.parent / "atest" / "translation.json"
64-
lib = SmallLibrary(translation=translation)
68+
def test_rf_name_not_in_keywords(lib: SmallLibrary):
6569
kw = lib.keywords
6670
assert "Execute SomeThing" not in kw, f"Execute SomeThing should not be present: {kw}"
6771
assert len(kw) == 6, f"Too many keywords: {kw}"

0 commit comments

Comments
 (0)