Cleanup: do not use _reportf() when not doing any string formating!
[blender.git] / release / scripts / modules / bl_i18n_utils / settings.py
1 # ***** BEGIN GPL LICENSE BLOCK *****
2 #
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 #
17 # ***** END GPL LICENSE BLOCK *****
18
19 # <pep8 compliant>
20
21 # Global settings used by all scripts in this dir.
22 # XXX Before any use of the tools in this dir, please make a copy of this file
23 #     named "setting.py"
24 # XXX This is a template, most values should be OK, but some you’ll have to
25 #     edit (most probably, BLENDER_EXEC and SOURCE_DIR).
26
27
28 import json
29 import os
30 import sys
31
32 import bpy
33
34 ###############################################################################
35 # MISC
36 ###############################################################################
37
38 # The languages defined in Blender.
39 LANGUAGES_CATEGORIES = (
40     # Min completeness level, UI english label.
41     ( 0.95, "Complete"),
42     ( 0.33, "In Progress"),
43     ( -1.0, "Starting"),
44 )
45 LANGUAGES = (
46     # ID, UI english label, ISO code.
47     ( 0, "Default (Default)", "DEFAULT"),
48     ( 1, "English (English)", "en_US"),
49     ( 2, "Japanese (日本語)", "ja_JP"),
50     ( 3, "Dutch (Nederlandse taal)", "nl_NL"),
51     ( 4, "Italian (Italiano)", "it_IT"),
52     ( 5, "German (Deutsch)", "de_DE"),
53     ( 6, "Finnish (Suomi)", "fi_FI"),
54     ( 7, "Swedish (Svenska)", "sv_SE"),
55     ( 8, "French (Français)", "fr_FR"),
56     ( 9, "Spanish (Español)", "es"),
57     (10, "Catalan (Català)", "ca_AD"),
58     (11, "Czech (Český)", "cs_CZ"),
59     (12, "Portuguese (Português)", "pt_PT"),
60     (13, "Simplified Chinese (简体中文)", "zh_CN"),
61     (14, "Traditional Chinese (繁體中文)", "zh_TW"),
62     (15, "Russian (Русский)", "ru_RU"),
63     (16, "Croatian (Hrvatski)", "hr_HR"),
64     (17, "Serbian (Српски)", "sr_RS"),
65     (18, "Ukrainian (Український)", "uk_UA"),
66     (19, "Polish (Polski)", "pl_PL"),
67     (20, "Romanian (Român)", "ro_RO"),
68     # Using the utf8 flipped form of Arabic (العربية).
69     (21, "Arabic (ﺔﻴﺑﺮﻌﻟﺍ)", "ar_EG"),
70     (22, "Bulgarian (Български)", "bg_BG"),
71     (23, "Greek (Ελληνικά)", "el_GR"),
72     (24, "Korean (한국 언어)", "ko_KR"),
73     (25, "Nepali (नेपाली)", "ne_NP"),
74     # Using the utf8 flipped form of Persian (فارسی).
75     (26, "Persian (ﯽﺳﺭﺎﻓ)", "fa_IR"),
76     (27, "Indonesian (Bahasa indonesia)", "id_ID"),
77     (28, "Serbian Latin (Srpski latinica)", "sr_RS@latin"),
78     (29, "Kyrgyz (Кыргыз тили)", "ky_KG"),
79     (30, "Turkish (Türkçe)", "tr_TR"),
80     (31, "Hungarian (Magyar)", "hu_HU"),
81     (32, "Brazilian Portuguese (Português do Brasil)", "pt_BR"),
82     # Using the utf8 flipped form of Hebrew (עִבְרִית)).
83     (33, "Hebrew (תירִבְעִ)", "he_IL"),
84     (34, "Estonian (Eestlane)", "et_EE"),
85     (35, "Esperanto (Esperanto)", "eo"),
86     (36, "Spanish from Spain (Español de España)", "es_ES"),
87     (37, "Amharic (አማርኛ)", "am_ET"),
88     (38, "Uzbek (Oʻzbek)", "uz_UZ"),
89     (39, "Uzbek Cyrillic (Ўзбек)", "uz_UZ@cyrillic"),
90     (40, "Hindi (मानक हिन्दी)", "hi_IN"),
91 )
92
93 # Default context, in py!
94 DEFAULT_CONTEXT = bpy.app.translations.contexts.default
95
96 # Name of language file used by Blender to generate translations' menu.
97 LANGUAGES_FILE = "languages"
98
99 # The min level of completeness for a po file to be imported from /branches into /trunk, as a percentage.
100 IMPORT_MIN_LEVEL = 0.0
101
102 # Languages in /branches we do not want to import in /trunk currently...
103 IMPORT_LANGUAGES_SKIP = {
104     'am_ET', 'bg_BG', 'fi_FI', 'el_GR', 'et_EE', 'ne_NP', 'ro_RO', 'uz_UZ', 'uz_UZ@cyrillic',
105 }
106
107 # Languages that need RTL pre-processing.
108 IMPORT_LANGUAGES_RTL = {
109     'ar_EG', 'fa_IR', 'he_IL',
110 }
111
112 # The comment prefix used in generated messages.txt file.
113 MSG_COMMENT_PREFIX = "#~ "
114
115 # The comment prefix used in generated messages.txt file.
116 MSG_CONTEXT_PREFIX = "MSGCTXT:"
117
118 # The default comment prefix used in po's.
119 PO_COMMENT_PREFIX= "# "
120
121 # The comment prefix used to mark sources of msgids, in po's.
122 PO_COMMENT_PREFIX_SOURCE = "#: "
123
124 # The comment prefix used to mark sources of msgids, in po's.
125 PO_COMMENT_PREFIX_SOURCE_CUSTOM = "#. :src: "
126
127 # The general "generated" comment prefix, in po's.
128 PO_COMMENT_PREFIX_GENERATED = "#. "
129
130 # The comment prefix used to comment entries in po's.
131 PO_COMMENT_PREFIX_MSG= "#~ "
132
133 # The comment prefix used to mark fuzzy msgids, in po's.
134 PO_COMMENT_FUZZY = "#, fuzzy"
135
136 # The prefix used to define context, in po's.
137 PO_MSGCTXT = "msgctxt "
138
139 # The prefix used to define msgid, in po's.
140 PO_MSGID = "msgid "
141
142 # The prefix used to define msgstr, in po's.
143 PO_MSGSTR = "msgstr "
144
145 # The 'header' key of po files.
146 PO_HEADER_KEY = (DEFAULT_CONTEXT, "")
147
148 PO_HEADER_MSGSTR = (
149     "Project-Id-Version: {blender_ver} ({blender_hash})\\n\n"
150     "Report-Msgid-Bugs-To: \\n\n"
151     "POT-Creation-Date: {time}\\n\n"
152     "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\n"
153     "Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\n"
154     "Language-Team: LANGUAGE <LL@li.org>\\n\n"
155     "Language: {uid}\\n\n"
156     "MIME-Version: 1.0\\n\n"
157     "Content-Type: text/plain; charset=UTF-8\\n\n"
158     "Content-Transfer-Encoding: 8bit\n"
159 )
160 PO_HEADER_COMMENT_COPYRIGHT = (
161     "# Blender's translation file (po format).\n"
162     "# Copyright (C) {year} The Blender Foundation.\n"
163     "# This file is distributed under the same license as the Blender package.\n"
164     "#\n"
165 )
166 PO_HEADER_COMMENT = (
167     "# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.\n"
168     "#"
169 )
170
171 TEMPLATE_ISO_ID = "__TEMPLATE__"
172
173 # Num buttons report their label with a trailing ': '...
174 NUM_BUTTON_SUFFIX = ": "
175
176 # Undocumented operator placeholder string.
177 UNDOC_OPS_STR = "(undocumented operator)"
178
179 # The gettext domain.
180 DOMAIN = "blender"
181
182 # Our own "gettext" stuff.
183 # File type (ext) to parse.
184 PYGETTEXT_ALLOWED_EXTS = {".c", ".cpp", ".cxx", ".hpp", ".hxx", ".h"}
185
186 # Max number of contexts into a BLF_I18N_MSGID_MULTI_CTXT macro...
187 PYGETTEXT_MAX_MULTI_CTXT = 16
188
189 # Where to search contexts definitions, relative to SOURCE_DIR (defined below).
190 PYGETTEXT_CONTEXTS_DEFSRC = os.path.join("source", "blender", "blenfont", "BLF_translation.h")
191
192 # Regex to extract contexts defined in BLF_translation.h
193 # XXX Not full-proof, but should be enough here!
194 PYGETTEXT_CONTEXTS = "#define\\s+(BLF_I18NCONTEXT_[A-Z_0-9]+)\\s+\"([^\"]*)\""
195
196 # Keywords' regex.
197 # XXX Most unfortunately, we can't use named backreferences inside character sets,
198 #     which makes the regexes even more twisty... :/
199 _str_base = (
200     # Match void string
201     "(?P<{_}1>[\"'])(?P={_}1)"  # Get opening quote (' or "), and closing immediately.
202     "|"
203     # Or match non-void string
204     "(?P<{_}2>[\"'])"  # Get opening quote (' or ").
205         "(?{capt}(?:"
206             # This one is for crazy things like "hi \\\\\" folks!"...
207             r"(?:(?!<\\)(?:\\\\)*\\(?=(?P={_}2)))|"
208             # The most common case.
209             ".(?!(?P={_}2))"
210         ")+.)"  # Don't forget the last char!
211     "(?P={_}2)"  # And closing quote.
212 )
213 str_clean_re = _str_base.format(_="g", capt="P<clean>")
214 _inbetween_str_re = (
215     # XXX Strings may have comments between their pieces too, not only spaces!
216     r"(?:\s*(?:"
217         # A C comment
218         r"/\*.*(?!\*/).\*/|"
219         # Or a C++ one!
220         r"//[^\n]*\n"
221     # And we are done!
222     r")?)*"
223 )
224 # Here we have to consider two different cases (empty string and other).
225 _str_whole_re = (
226     _str_base.format(_="{_}1_", capt=":") +
227     # Optional loop start, this handles "split" strings...
228     "(?:(?<=[\"'])" + _inbetween_str_re + "(?=[\"'])(?:"
229         + _str_base.format(_="{_}2_", capt=":") +
230     # End of loop.
231     "))*"
232 )
233 _ctxt_re_gen = lambda uid : r"(?P<ctxt_raw{uid}>(?:".format(uid=uid) + \
234                             _str_whole_re.format(_="_ctxt{uid}".format(uid=uid)) + \
235                             r")|(?:[A-Z_0-9]+))"
236 _ctxt_re = _ctxt_re_gen("")
237 _msg_re = r"(?P<msg_raw>" + _str_whole_re.format(_="_msg") + r")"
238 PYGETTEXT_KEYWORDS = (() +
239     tuple((r"{}\(\s*" + _msg_re + r"\s*\)").format(it)
240           for it in ("IFACE_", "TIP_", "DATA_", "N_")) +
241
242     tuple((r"{}\(\s*" + _ctxt_re + r"\s*,\s*" + _msg_re + r"\s*\)").format(it)
243           for it in ("CTX_IFACE_", "CTX_TIP_", "CTX_DATA_", "CTX_N_")) +
244
245     tuple(("{}\\((?:[^\"',]+,){{1,2}}\\s*" + _msg_re + r"\s*(?:\)|,)").format(it)
246           for it in ("BKE_report", "BKE_reportf", "BKE_reports_prepend", "BKE_reports_prependf",
247                      "CTX_wm_operator_poll_msg_set")) +
248
249     tuple(("{}\\((?:[^\"',]+,){{3}}\\s*" + _msg_re + r"\s*\)").format(it)
250           for it in ("BMO_error_raise",)) +
251
252     tuple(("{}\\((?:[^\"',]+,)\\s*" + _msg_re + r"\s*(?:\)|,)").format(it)
253           for it in ("modifier_setError",)) +
254
255     tuple((r"{}\(\s*" + _msg_re + r"\s*,\s*(?:" +
256            r"\s*,\s*)?(?:".join(_ctxt_re_gen(i) for i in range(PYGETTEXT_MAX_MULTI_CTXT)) + r")?\s*\)").format(it)
257           for it in ("BLF_I18N_MSGID_MULTI_CTXT",))
258 )
259
260 # Check printf mismatches between msgid and msgstr.
261 CHECK_PRINTF_FORMAT = (
262     r"(?!<%)(?:%%)*%"          # Begining, with handling for crazy things like '%%%%%s'
263     r"[-+#0]?"                 # Flags (note: do not add the ' ' (space) flag here, generates too much false positives!)
264     r"(?:\*|[0-9]+)?"          # Width
265     r"(?:\.(?:\*|[0-9]+))?"    # Precision
266     r"(?:[hljztL]|hh|ll)?"     # Length
267     r"[tldiuoxXfFeEgGaAcspn]"  # Specifiers (note we have Blender-specific %t and %l ones too)
268 )
269
270 # Should po parser warn when finding a first letter not capitalized?
271 WARN_MSGID_NOT_CAPITALIZED = True
272
273 # Strings that should not raise above warning!
274 WARN_MSGID_NOT_CAPITALIZED_ALLOWED = {
275     "",                              # Simplifies things... :p
276     "ac3",
277     "along X",
278     "along Y",
279     "along Z",
280     "along %s X",
281     "along %s Y",
282     "along %s Z",
283     "along local Z",
284     "ascii",
285     "author",                        # Addons' field. :/
286     "bItasc",
287     "description",                   # Addons' field. :/
288     "dx",
289     "fBM",
290     "flac",
291     "fps: %.2f",
292     "fps: %i",
293     "gimbal",
294     "global",
295     "iScale",
296     "iso-8859-15",
297     "iTaSC",
298     "iTaSC parameters",
299     "kb",
300     "local",
301     "location",                      # Addons' field. :/
302     "locking %s X",
303     "locking %s Y",
304     "locking %s Z",
305     "mkv",
306     "mm",
307     "mp2",
308     "mp3",
309     "normal",
310     "ogg",
311     "p0",
312     "px",
313     "re",
314     "res",
315     "rv",
316     "sin(x) / x",
317     "sqrt(x*x+y*y+z*z)",
318     "sRGB",
319     "utf-8",
320     "var",
321     "vBVH",
322     "view",
323     "wav",
324     "y",
325     # Sub-strings.
326     "available with",
327     "brown fox",
328     "can't save image while rendering",
329     "expected a timeline/animation area to be active",
330     "expected a view3d region",
331     "expected a view3d region & editcurve",
332     "expected a view3d region & editmesh",
333     "image file not found",
334     "image path can't be written to",
335     "in memory to enable editing!",
336     "jumps over",
337     "the lazy dog",
338     "unable to load movie clip",
339     "unable to load text",
340     "unable to open the file",
341     "unknown error reading file",
342     "unknown error stating file",
343     "unknown error writing file",
344     "unsupported font format",
345     "unsupported format",
346     "unsupported image format",
347     "unsupported movie clip format",
348     "verts only",
349     "virtual parents",
350 }
351 WARN_MSGID_NOT_CAPITALIZED_ALLOWED |= set(lng[2] for lng in LANGUAGES)
352
353 WARN_MSGID_END_POINT_ALLOWED = {
354     "Numpad .",
355     "Circle|Alt .",
356     "Temp. Diff.",
357     "Float Neg. Exp.",
358     "    RNA Path: bpy.types.",
359     "Max Ext.",
360 }
361
362 PARSER_CACHE_HASH = 'sha1'
363
364 PARSER_TEMPLATE_ID = "__POT__"
365 PARSER_PY_ID = "__PY__"
366
367 PARSER_PY_MARKER_BEGIN = "\n# ##### BEGIN AUTOGENERATED I18N SECTION #####\n"
368 PARSER_PY_MARKER_END = "\n# ##### END AUTOGENERATED I18N SECTION #####\n"
369
370 PARSER_MAX_FILE_SIZE = 2 ** 24  # in bytes, i.e. 16 Mb.
371
372 ###############################################################################
373 # PATHS
374 ###############################################################################
375
376 # The Python3 executable.You’ll likely have to edit it in your user_settings.py
377 # if you’re under Windows.
378 PYTHON3_EXEC = "python3"
379
380 # The Blender executable!
381 # This is just an example, you’ll have to edit it in your user_settings.py!
382 BLENDER_EXEC = os.path.abspath(os.path.join("foo", "bar", "blender"))
383 # check for blender.bin
384 if not os.path.exists(BLENDER_EXEC):
385     if os.path.exists(BLENDER_EXEC + ".bin"):
386         BLENDER_EXEC = BLENDER_EXEC + ".bin"
387
388 # The gettext msgfmt "compiler". You’ll likely have to edit it in your user_settings.py if you’re under Windows.
389 GETTEXT_MSGFMT_EXECUTABLE = "msgfmt"
390
391 # The FriBidi C compiled library (.so under Linux, .dll under windows...).
392 # You’ll likely have to edit it in your user_settings.py if you’re under Windows., e.g. using the included one:
393 #     FRIBIDI_LIB = os.path.join(TOOLS_DIR, "libfribidi.dll")
394 FRIBIDI_LIB = "libfribidi.so.0"
395
396 # The name of the (currently empty) file that must be present in a po's directory to enable rtl-preprocess.
397 RTL_PREPROCESS_FILE = "is_rtl"
398
399 # The Blender source root path.
400 # This is just an example, you’ll have to override it in your user_settings.py!
401 SOURCE_DIR = os.path.abspath(os.path.join("blender"))
402
403 # The bf-translation repository (you'll have to override this in your user_settings.py).
404 I18N_DIR = os.path.abspath(os.path.join("i18n"))
405
406 # The /branches path (relative to I18N_DIR).
407 REL_BRANCHES_DIR = os.path.join("branches")
408
409 # The /trunk path (relative to I18N_DIR).
410 REL_TRUNK_DIR = os.path.join("trunk")
411
412 # The /trunk/po path (relative to I18N_DIR).
413 REL_TRUNK_PO_DIR = os.path.join(REL_TRUNK_DIR, "po")
414
415 # The /trunk/mo path (relative to I18N_DIR).
416 REL_TRUNK_MO_DIR = os.path.join(REL_TRUNK_DIR, "locale")
417
418 # The Blender source path to check for i18n macros (relative to SOURCE_DIR).
419 REL_POTFILES_SOURCE_DIR = os.path.join("source")
420
421 # The template messages file (relative to I18N_DIR).
422 REL_FILE_NAME_POT = os.path.join(REL_BRANCHES_DIR, DOMAIN + ".pot")
423
424 # Mo root datapath.
425 REL_MO_PATH_ROOT = os.path.join(REL_TRUNK_DIR, "locale")
426
427 # Mo path generator for a given language.
428 REL_MO_PATH_TEMPLATE = os.path.join(REL_MO_PATH_ROOT, "{}", "LC_MESSAGES")
429
430 # Mo path generator for a given language (relative to any "locale" dir).
431 MO_PATH_ROOT_RELATIVE = os.path.join("locale")
432 MO_PATH_TEMPLATE_RELATIVE = os.path.join(MO_PATH_ROOT_RELATIVE, "{}", "LC_MESSAGES")
433
434 # Mo file name.
435 MO_FILE_NAME = DOMAIN + ".mo"
436
437 # Where to search for py files that may contain ui strings (relative to one of the 'resource_path' of Blender).
438 CUSTOM_PY_UI_FILES = [
439     os.path.join("scripts", "startup", "bl_ui"),
440     os.path.join("scripts", "modules", "rna_prop_ui.py"),
441 ]
442
443 # An optional text file listing files to force include/exclude from py_xgettext process.
444 SRC_POTFILES = ""
445
446 # A cache storing validated msgids, to avoid re-spellchecking them.
447 SPELL_CACHE = os.path.join("/tmp", ".spell_cache")
448
449 # Threshold defining whether a new msgid is similar enough with an old one to reuse its translation...
450 SIMILAR_MSGID_THRESHOLD = 0.75
451
452 # Additional import paths to add to sys.path (';' separated)...
453 INTERN_PY_SYS_PATHS = ""
454
455 # Custom override settings must be one dir above i18n tools itself!
456 sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
457 try:
458     from bl_i18n_settings_override import *
459 except ImportError:  # If no i18n_override_settings available, it’s no error!
460     pass
461
462 # Override with custom user settings, if available.
463 try:
464     from settings_user import *
465 except ImportError:  # If no user_settings available, it’s no error!
466     pass
467
468
469 for p in set(INTERN_PY_SYS_PATHS.split(";")):
470     if p:
471         sys.path.append(p)
472
473
474 # The settings class itself!
475 def _do_get(ref, path):
476     return os.path.normpath(os.path.join(ref, path))
477
478
479 def _do_set(ref, path):
480     path = os.path.normpath(path)
481     # If given path is absolute, make it relative to current ref one (else we consider it is already the case!)
482     if os.path.isabs(path):
483         # can't always find the relative path (between drive letters on windows)
484         try:
485             return os.path.relpath(path, ref)
486         except ValueError:
487             pass
488     return path
489
490
491 def _gen_get_set_path(ref, name):
492     def _get(self):
493         return _do_get(getattr(self, ref), getattr(self, name))
494     def _set(self, value):
495         setattr(self, name, _do_set(getattr(self, ref), value))
496     return _get, _set
497
498
499 def _gen_get_set_paths(ref, name):
500     def _get(self):
501         return [_do_get(getattr(self, ref), p) for p in getattr(self, name)]
502     def _set(self, value):
503         setattr(self, name, [_do_set(getattr(self, ref), p) for p in value])
504     return _get, _set
505
506
507 class I18nSettings:
508     """
509     Class allowing persistence of our settings!
510     Saved in JSon format, so settings should be JSon'able objects!
511     """
512     _settings = None
513
514     def __new__(cls, *args, **kwargs):
515         # Addon preferences are singleton by definition, so is this class!
516         if not I18nSettings._settings:
517             cls._settings = super(I18nSettings, cls).__new__(cls)
518             cls._settings.__dict__ = {uid: data for uid, data in globals().items() if not uid.startswith("_")}
519         return I18nSettings._settings
520
521     def from_json(self, string):
522         data = dict(json.loads(string))
523         # Special case... :/
524         if "INTERN_PY_SYS_PATHS" in data:
525             self.PY_SYS_PATHS = data["INTERN_PY_SYS_PATHS"]
526         self.__dict__.update(data)
527
528     def to_json(self):
529         # Only save the diff from default i18n_settings!
530         glob = globals()
531         export_dict = {uid: val for uid, val in self.__dict__.items() if glob.get(uid) != val}
532         return json.dumps(export_dict)
533
534     def load(self, fname, reset=False):
535         if reset:
536             self.__dict__ = {uid: data for uid, data in globals().items() if not uid.startswith("_")}
537         if isinstance(fname, str):
538             if not os.path.isfile(fname):
539                 return
540             with open(fname) as f:
541                 self.from_json(f.read())
542         # Else assume fname is already a file(like) object!
543         else:
544             self.from_json(fname.read())
545
546     def save(self, fname):
547         if isinstance(fname, str):
548             with open(fname, 'w') as f:
549                 f.write(self.to_json())
550         # Else assume fname is already a file(like) object!
551         else:
552             fname.write(self.to_json())
553
554     BRANCHES_DIR = property(*(_gen_get_set_path("I18N_DIR", "REL_BRANCHES_DIR")))
555     TRUNK_DIR = property(*(_gen_get_set_path("I18N_DIR", "REL_TRUNK_DIR")))
556     TRUNK_PO_DIR = property(*(_gen_get_set_path("I18N_DIR", "REL_TRUNK_PO_DIR")))
557     TRUNK_MO_DIR = property(*(_gen_get_set_path("I18N_DIR", "REL_TRUNK_MO_DIR")))
558     POTFILES_SOURCE_DIR = property(*(_gen_get_set_path("SOURCE_DIR", "REL_POTFILES_SOURCE_DIR")))
559     FILE_NAME_POT = property(*(_gen_get_set_path("I18N_DIR", "REL_FILE_NAME_POT")))
560     MO_PATH_ROOT = property(*(_gen_get_set_path("I18N_DIR", "REL_MO_PATH_ROOT")))
561     MO_PATH_TEMPLATE = property(*(_gen_get_set_path("I18N_DIR", "REL_MO_PATH_TEMPLATE")))
562
563     def _get_py_sys_paths(self):
564         return self.INTERN_PY_SYS_PATHS
565     def _set_py_sys_paths(self, val):
566         old_paths = set(self.INTERN_PY_SYS_PATHS.split(";")) - {""}
567         new_paths = set(val.split(";")) - {""}
568         for p in old_paths - new_paths:
569             if p in sys.path:
570                 sys.path.remove(p)
571         for p in new_paths - old_paths:
572             sys.path.append(p)
573         self.INTERN_PY_SYS_PATHS = val
574     PY_SYS_PATHS = property(_get_py_sys_paths, _set_py_sys_paths)