Skip to content

Commit a506175

Browse files
csvtudaaaltat
authored andcommitted
Allow dictionaries as translation sources
1 parent e9c3664 commit a506175

6 files changed

Lines changed: 132 additions & 40 deletions

File tree

README.md

Lines changed: 104 additions & 26 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.
167-
168-
The keys of json are the methods names, not the keyword names, which implements keyword. Value
169-
of key is json object which contains two keys: `name` and `doc`. `name` key contains the keyword
161+
PLC supports translation of keywords names and documentation. Translations must be provided in
162+
the `translation` argument in the `HybridCore` or `DynamicCore` `__init__`, either as a
163+
dictionary or through a [Path](https://docs.python.org/3/library/pathlib.html) to a
164+
[JSON](https://www.json.org/json-en.html) file. Providing translation data is optional, also it
165+
is not mandatory to provide translation to all keyword.
166+
167+
The keys of the dictionary are the methods names, not the keyword names, which implements keyword.
168+
Values are objects which contains two keys: `name` and `doc`. `name` key contains the keyword
170169
translated name and `doc` contains keyword translated documentation. Providing
171-
`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
170+
`doc` and `name` is optional, i.e. translations data can also provide translations only
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)