Skip to content

Commit 07cb239

Browse files
committed
WIP: Adds new lang 'fr' for French
Adds new Internationalization Helper tool: i18n_helper.py Signed-off-by: Tom Marble <[email protected]>
1 parent e55c749 commit 07cb239

File tree

4 files changed

+270
-2
lines changed

4 files changed

+270
-2
lines changed

locales/README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,67 @@ needs to have some prompts for the user in order to unlock the filesystem in the
7777
Nothing prevents a more sophisticated application-level server later on that operates
7878
in `std` from pulling in a more featureful, dynamic localization framework; but it's an
7979
explicit goal to keep the kernel small, simple, and fast.
80+
81+
## Internationalization Helper
82+
83+
In the tools directory you find `i18n_helper.py` that can be
84+
very useful for managing and adding localizations:
85+
86+
```
87+
$ ./tools/i18n_helper.py -h
88+
usage: i18n_helper.py [-h] [-v] [-l] [-i] [-m] [-o] [-n NEW_LANG] [-f FROM_LANG]
89+
90+
Xous i18n Helper
91+
92+
options:
93+
-h, --help show this help message and exit
94+
-v, --verbose Prints details of each action
95+
-l, --list-languages Lists current translations
96+
-i, --list-i18n-files
97+
Lists i18n files
98+
-m, --missing Shows missing translations
99+
-o, --show-ok Shows OK translations
100+
-n NEW_LANG, --new-lang NEW_LANG
101+
Add support for a new lang
102+
-f FROM_LANG, --from-lang FROM_LANG
103+
Copy this existing lang for the new lang
104+
$
105+
```
106+
107+
The `--list-languages` function will show the currently supported langs:
108+
109+
```
110+
$ ./tools/i18n_helper.py --list-languages
111+
en
112+
ja
113+
zh
114+
en-tts
115+
$
116+
```
117+
118+
The `--list-languages` function will show the disposition of the translations
119+
(add `--show-ok` to see the complete translation status). The output
120+
is "FILE tab JQ-LIKE-PATH tab STATUS" which makes it easy to spot
121+
situations like completely absent translations that might cause a panic:
122+
123+
```
124+
$ ./tools/i18n_helper.py --missing | grep ABSENT
125+
services/status/locales/i18n.json rtc.set_time_modal.ja ABSENT
126+
services/status/locales/i18n.json rtc.set_time_modal.zh ABSENT
127+
services/status/locales/i18n.json rtc.set_time_modal.en-tts ABSENT
128+
$
129+
```
130+
131+
The `--new-lang` function will add a new lang by copying an
132+
existing lang and adding the suffix `" *EN*"` to the new translation
133+
(example if the `--from-lang` is `en`):
134+
135+
136+
```
137+
$ ./tools/i18n_helper.py --verbose --from-lang en --new-lang fr
138+
verbose mode
139+
-- get languages --
140+
adding new lang "fr" from "en" by appending " *EN*"
141+
NOT IMPLEMENTED YET
142+
$
143+
```

services/status/locales/i18n.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,10 @@
421421
"en-tts": "NTP query failed, please enter time manually."
422422
},
423423
"rtc.set_time_modal": {
424-
"en": "Enter time"
424+
"en": "Enter time",
425+
"ja": "Enter time *EN*",
426+
"zh": "Enter time *EN*",
427+
"en-tts": "Enter time *EN*"
425428
},
426429
"rtc.month": {
427430
"en": "Month (1-12)",
@@ -525,4 +528,4 @@
525528
"zh": "错误:输入超出范围",
526529
"en-tts": "Error: input out of range"
527530
}
528-
}
531+
}

tools/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ Found Xous Args Size at offset 8, setting total_words to 208
7474
$
7575
```
7676

77+
## Internationalization Helper
78+
79+
For more about `i18n_helper.py` please see the locales [README](../locales/README.md)
80+
7781
## Testing
7882

7983
_TBD_

tools/i18n_helper.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
#! /usr/bin/env python3
2+
3+
import argparse
4+
import json
5+
import sys
6+
import os
7+
import os.path
8+
import pwd
9+
import re
10+
11+
class Main(object):
12+
13+
def __init__(self):
14+
parser = argparse.ArgumentParser(description="Xous i18n Helper")
15+
parser.add_argument('-v', '--verbose',
16+
help='Prints details of each action',
17+
action='store_true', required=False)
18+
parser.add_argument('-l', '--list-languages',
19+
help='Lists current translations',
20+
action='store_true', required=False)
21+
parser.add_argument('-i', '--list-i18n-files',
22+
help='Lists i18n files',
23+
action='store_true', required=False)
24+
parser.add_argument('-m', '--missing',
25+
help='Shows missing translations',
26+
action='store_true', required=False)
27+
parser.add_argument('-o', '--show-ok',
28+
help='Shows OK translations',
29+
action='store_true', required=False)
30+
parser.add_argument('-n', '--new-lang',
31+
help='Add support for a new lang',
32+
required=False)
33+
parser.add_argument('-f', '--from-lang',
34+
help='Copy this existing lang for the new lang',
35+
required=False)
36+
self.args = parser.parse_args()
37+
self.args.program = sys.argv[0]
38+
if self.args.program[0] != '/':
39+
self.args.program = os.path.join(os.getcwd(), self.args.program)
40+
self.args.pdir = os.path.normpath(os.path.dirname(self.args.program))
41+
self.args.xousdir = os.path.normpath(os.path.dirname(self.args.pdir))
42+
self.args.program = os.path.basename(self.args.program)
43+
self.errfile = sys.stderr
44+
45+
def out(self, *objects):
46+
print(*objects)
47+
48+
def err(self, *objects):
49+
print(*objects, file=self.errfile)
50+
51+
def verr(self, *objects):
52+
if self.args.verbose:
53+
self.err(*objects)
54+
55+
def get_languages(self):
56+
self.verr('-- get languages --')
57+
# services/root-keys/locales/i18n.json
58+
root_keys = os.path.join(self.args.xousdir, 'services', 'root-keys', 'locales', 'i18n.json')
59+
if not os.path.exists(root_keys):
60+
self.err('cannot find: %s' % root_keys);
61+
return 1
62+
# read keys in "rootkeys.backup_key":
63+
essential = 'rootkeys.backup_key'
64+
f = open(root_keys,)
65+
obj = json.load(f)
66+
self.languages = []
67+
if essential in obj:
68+
for key in obj[essential].keys():
69+
self.languages.append(key)
70+
f.close()
71+
return 0
72+
73+
def list_languages(self):
74+
if self.get_languages() != 0:
75+
return 1
76+
self.verr('-- list languages --')
77+
for lang in self.languages:
78+
self.out(lang)
79+
return 0
80+
81+
def get_i18n_files(self):
82+
self.verr('-- get i18n files --')
83+
self.i18n_files = []
84+
topdirs = ['apps', 'services']
85+
for top in topdirs:
86+
topdir = os.path.join(self.args.xousdir, top)
87+
if os.path.exists(topdir):
88+
if top == 'apps':
89+
manifest = os.path.join(topdir, 'manifest.json')
90+
if os.path.exists(manifest):
91+
self.i18n_files.append(manifest)
92+
for thing in os.listdir(topdir):
93+
thingdir = os.path.join(topdir, thing)
94+
if os.path.isdir(thingdir):
95+
i18n_path = os.path.join(thingdir, 'locales', 'i18n.json')
96+
if os.path.exists(i18n_path):
97+
self.i18n_files.append(i18n_path[len(self.args.xousdir)+1:])
98+
return 0
99+
100+
def list_i18n_files(self):
101+
if self.get_i18n_files() != 0:
102+
return 1
103+
self.verr('-- list i18n files --')
104+
for pathname in self.i18n_files:
105+
self.out(pathname)
106+
return 0
107+
108+
# keys for
109+
# manifest APP, menu_name, appmenu.APP
110+
# other TAG
111+
def show_missing(self, i18n, is_manifest):
112+
i18n_path = os.path.join(self.args.xousdir, i18n)
113+
f = open(i18n_path,)
114+
obj = json.load(f)
115+
f.close()
116+
jqpath = None
117+
translation = None
118+
for tag in obj.keys():
119+
if is_manifest:
120+
appmenu = 'appmenu.' + tag
121+
jqpath = tag + '.menu_name.' + appmenu
122+
translation = obj[tag]['menu_name'][appmenu]
123+
else:
124+
jqpath = tag
125+
translation = obj[tag]
126+
for lang in self.languages:
127+
lang_path = jqpath + '.' + lang
128+
status = 'OK'
129+
if lang in translation:
130+
t = translation[lang]
131+
# to print translation
132+
# print('%s\t%s\t%s' % (i18n, lang_path, t))
133+
if t == '🔇':
134+
status = 'MISSING'
135+
elif self.regex.fullmatch(t):
136+
status = 'TEMPORARY'
137+
else:
138+
status = 'ABSENT'
139+
if self.args.show_ok or status != 'OK':
140+
print('%s\t%s\t%s' % (i18n, lang_path, status))
141+
return 0
142+
143+
def missing(self):
144+
if self.get_languages() != 0:
145+
return 1
146+
if self.get_i18n_files() != 0:
147+
return 1
148+
self.verr('-- missing --')
149+
# if end of string matches ' *EN*' (or some other lang)
150+
self.regex = re.compile('^.* \*[a-zA-Z]{2}\*$')
151+
for i18n in self.i18n_files:
152+
is_manifest = os.path.basename(i18n) == 'manifest.json'
153+
if self.show_missing(i18n, is_manifest) != 0:
154+
return 1
155+
return 0
156+
157+
def new_lang(self):
158+
self.from_hint = ' *%s*' % self.args.from_lang.upper()
159+
self.verr('adding new lang "%s" from "%s" by appending "%s"' % (self.args.new_lang, self.args.from_lang, self.from_hint))
160+
self.err('NOT IMPLEMENTED YET')
161+
return 1
162+
163+
def run(self):
164+
rc = 0
165+
if self.args.verbose:
166+
self.err('verbose mode')
167+
self.err('xous dir: "%s"' % self.args.xousdir)
168+
self.err('program dir: "%s"' % self.args.pdir)
169+
self.err('program: "%s"' % self.args.program)
170+
self.err('show-ok: "%s"' % self.args.show_ok)
171+
if self.args.list_languages:
172+
rc = self.list_languages()
173+
elif self.args.list_i18n_files:
174+
rc = self.list_i18n_files()
175+
elif self.args.missing:
176+
rc = self.missing()
177+
elif self.args.new_lang:
178+
if not self.args.from_lang:
179+
self.err('error: you must specify both --from-lang en --to-lang fr')
180+
rc = 1
181+
else:
182+
self.get_languages()
183+
if not self.args.from_lang in self.languages:
184+
self.err('error: --from-lang %s is not one of the existing langs: %s' % (self.args.from_lang, self.languages))
185+
rc = 1
186+
elif self.args.new_lang in self.languages:
187+
self.err('error: --new-lang %s must not be one of the existing langs: %s' % (self.args.from_lang, self.languages))
188+
rc = 1
189+
elif not re.fullmatch('^[a-z-]{2,6}$', self.args.new_lang):
190+
self.err('error: --new-lang %s is not valid lang' % self.args.new_lang)
191+
rc = 1
192+
else:
193+
rc = self.new_lang()
194+
return rc
195+
196+
if __name__ == "__main__":
197+
sys.exit(Main().run())

0 commit comments

Comments
 (0)