920a56a628b10017ffe5d3dc25432e94053c9717
[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     "can't save image while rendering",
328     "expected a timeline/animation area to be active",
329     "expected a view3d region",
330     "expected a view3d region & editcurve",
331     "expected a view3d region & editmesh",
332     "image file not found",
333     "image path can't be written to",
334     "in memory to enable editing!",
335     "unable to load movie clip",
336     "unable to load text",
337     "unable to open the file",
338     "unknown error reading file",
339     "unknown error stating file",
340     "unknown error writing file",
341     "unsupported font format",
342     "unsupported format",
343     "unsupported image format",
344     "unsupported movie clip format",
345     "verts only",
346     "virtual parents",
347 }
348 WARN_MSGID_NOT_CAPITALIZED_ALLOWED |= set(lng[2] for lng in LANGUAGES)
349
350 WARN_MSGID_END_POINT_ALLOWED = {
351     "Numpad .",
352     "Circle|Alt .",
353     "Temp. Diff.",
354     "Float Neg. Exp.",
355     "    RNA Path: bpy.types.",
356     "Max Ext.",
357 }
358
359 PARSER_CACHE_HASH = 'sha1'
360
361 PARSER_TEMPLATE_ID = "__POT__"
362 PARSER_PY_ID = "__PY__"
363
364 PARSER_PY_MARKER_BEGIN = "\n# ##### BEGIN AUTOGENERATED I18N SECTION #####\n"
365 PARSER_PY_MARKER_END = "\n# ##### END AUTOGENERATED I18N SECTION #####\n"
366
367 PARSER_MAX_FILE_SIZE = 2 ** 24  # in bytes, i.e. 16 Mb.
368
369 ###############################################################################
370 # PATHS
371 ###############################################################################
372
373 # The Python3 executable.You’ll likely have to edit it in your user_settings.py
374 # if you’re under Windows.
375 PYTHON3_EXEC = "python3"
376
377 # The Blender executable!
378 # This is just an example, you’ll have to edit it in your user_settings.py!
379 BLENDER_EXEC = os.path.abspath(os.path.join("foo", "bar", "blender"))
380 # check for blender.bin
381 if not os.path.exists(BLENDER_EXEC):
382     if os.path.exists(BLENDER_EXEC + ".bin"):
383         BLENDER_EXEC = BLENDER_EXEC + ".bin"
384
385 # The gettext msgfmt "compiler". You’ll likely have to edit it in your user_settings.py if you’re under Windows.
386 GETTEXT_MSGFMT_EXECUTABLE = "msgfmt"
387
388 # The FriBidi C compiled library (.so under Linux, .dll under windows...).
389 # You’ll likely have to edit it in your user_settings.py if you’re under Windows., e.g. using the included one:
390 #     FRIBIDI_LIB = os.path.join(TOOLS_DIR, "libfribidi.dll")
391 FRIBIDI_LIB = "libfribidi.so.0"
392
393 # The name of the (currently empty) file that must be present in a po's directory to enable rtl-preprocess.
394 RTL_PREPROCESS_FILE = "is_rtl"
395
396 # The Blender source root path.
397 # This is just an example, you’ll have to override it in your user_settings.py!
398 SOURCE_DIR = os.path.abspath(os.path.join("blender"))
399
400 # The bf-translation repository (you'll have to override this in your user_settings.py).
401 I18N_DIR = os.path.abspath(os.path.join("i18n"))
402
403 # The /branches path (relative to I18N_DIR).
404 REL_BRANCHES_DIR = os.path.join("branches")
405
406 # The /trunk path (relative to I18N_DIR).
407 REL_TRUNK_DIR = os.path.join("trunk")
408
409 # The /trunk/po path (relative to I18N_DIR).
410 REL_TRUNK_PO_DIR = os.path.join(REL_TRUNK_DIR, "po")
411
412 # The /trunk/mo path (relative to I18N_DIR).
413 REL_TRUNK_MO_DIR = os.path.join(REL_TRUNK_DIR, "locale")
414
415 # The Blender source path to check for i18n macros (relative to SOURCE_DIR).
416 REL_POTFILES_SOURCE_DIR = os.path.join("source")
417
418 # The template messages file (relative to I18N_DIR).
419 REL_FILE_NAME_POT = os.path.join(REL_BRANCHES_DIR, DOMAIN + ".pot")
420
421 # Mo root datapath.
422 REL_MO_PATH_ROOT = os.path.join(REL_TRUNK_DIR, "locale")
423
424 # Mo path generator for a given language.
425 REL_MO_PATH_TEMPLATE = os.path.join(REL_MO_PATH_ROOT, "{}", "LC_MESSAGES")
426
427 # Mo path generator for a given language (relative to any "locale" dir).
428 MO_PATH_ROOT_RELATIVE = os.path.join("locale")
429 MO_PATH_TEMPLATE_RELATIVE = os.path.join(MO_PATH_ROOT_RELATIVE, "{}", "LC_MESSAGES")
430
431 # Mo file name.
432 MO_FILE_NAME = DOMAIN + ".mo"
433
434 # Where to search for py files that may contain ui strings (relative to one of the 'resource_path' of Blender).
435 CUSTOM_PY_UI_FILES = [
436     os.path.join("scripts", "startup", "bl_ui"),
437     os.path.join("scripts", "modules", "rna_prop_ui.py"),
438 ]
439
440 # An optional text file listing files to force include/exclude from py_xgettext process.
441 SRC_POTFILES = ""
442
443 # A cache storing validated msgids, to avoid re-spellchecking them.
444 SPELL_CACHE = os.path.join("/tmp", ".spell_cache")
445
446 # Threshold defining whether a new msgid is similar enough with an old one to reuse its translation...
447 SIMILAR_MSGID_THRESHOLD = 0.75
448
449 # Additional import paths to add to sys.path (';' separated)...
450 INTERN_PY_SYS_PATHS = ""
451
452 # Custom override settings must be one dir above i18n tools itself!
453 sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
454 try:
455     from bl_i18n_settings_override import *
456 except ImportError:  # If no i18n_override_settings available, it’s no error!
457     pass
458
459 # Override with custom user settings, if available.
460 try:
461     from settings_user import *
462 except ImportError:  # If no user_settings available, it’s no error!
463     pass
464
465
466 for p in set(INTERN_PY_SYS_PATHS.split(";")):
467     if p:
468         sys.path.append(p)
469
470
471 # The settings class itself!
472 def _do_get(ref, path):
473     return os.path.normpath(os.path.join(ref, path))
474
475
476 def _do_set(ref, path):
477     path = os.path.normpath(path)
478     # If given path is absolute, make it relative to current ref one (else we consider it is already the case!)
479     if os.path.isabs(path):
480         # can't always find the relative path (between drive letters on windows)
481         try:
482             return os.path.relpath(path, ref)
483         except ValueError:
484             pass
485     return path
486
487
488 def _gen_get_set_path(ref, name):
489     def _get(self):
490         return _do_get(getattr(self, ref), getattr(self, name))
491     def _set(self, value):
492         setattr(self, name, _do_set(getattr(self, ref), value))
493     return _get, _set
494
495
496 def _gen_get_set_paths(ref, name):
497     def _get(self):
498         return [_do_get(getattr(self, ref), p) for p in getattr(self, name)]
499     def _set(self, value):
500         setattr(self, name, [_do_set(getattr(self, ref), p) for p in value])
501     return _get, _set
502
503
504 class I18nSettings:
505     """
506     Class allowing persistence of our settings!
507     Saved in JSon format, so settings should be JSon'able objects!
508     """
509     _settings = None
510
511     def __new__(cls, *args, **kwargs):
512         # Addon preferences are singleton by definition, so is this class!
513         if not I18nSettings._settings:
514             cls._settings = super(I18nSettings, cls).__new__(cls)
515             cls._settings.__dict__ = {uid: data for uid, data in globals().items() if not uid.startswith("_")}
516         return I18nSettings._settings
517
518     def from_json(self, string):
519         data = dict(json.loads(string))
520         # Special case... :/
521         if "INTERN_PY_SYS_PATHS" in data:
522             self.PY_SYS_PATHS = data["INTERN_PY_SYS_PATHS"]
523         self.__dict__.update(data)
524
525     def to_json(self):
526         # Only save the diff from default i18n_settings!
527         glob = globals()
528         export_dict = {uid: val for uid, val in self.__dict__.items() if glob.get(uid) != val}
529         return json.dumps(export_dict)
530
531     def load(self, fname, reset=False):
532         if reset:
533             self.__dict__ = {uid: data for uid, data in globals().items() if not uid.startswith("_")}
534         if isinstance(fname, str):
535             if not os.path.isfile(fname):
536                 return
537             with open(fname) as f:
538                 self.from_json(f.read())
539         # Else assume fname is already a file(like) object!
540         else:
541             self.from_json(fname.read())
542
543     def save(self, fname):
544         if isinstance(fname, str):
545             with open(fname, 'w') as f:
546                 f.write(self.to_json())
547         # Else assume fname is already a file(like) object!
548         else:
549             fname.write(self.to_json())
550
551     BRANCHES_DIR = property(*(_gen_get_set_path("I18N_DIR", "REL_BRANCHES_DIR")))
552     TRUNK_DIR = property(*(_gen_get_set_path("I18N_DIR", "REL_TRUNK_DIR")))
553     TRUNK_PO_DIR = property(*(_gen_get_set_path("I18N_DIR", "REL_TRUNK_PO_DIR")))
554     TRUNK_MO_DIR = property(*(_gen_get_set_path("I18N_DIR", "REL_TRUNK_MO_DIR")))
555     POTFILES_SOURCE_DIR = property(*(_gen_get_set_path("SOURCE_DIR", "REL_POTFILES_SOURCE_DIR")))
556     FILE_NAME_POT = property(*(_gen_get_set_path("I18N_DIR", "REL_FILE_NAME_POT")))
557     MO_PATH_ROOT = property(*(_gen_get_set_path("I18N_DIR", "REL_MO_PATH_ROOT")))
558     MO_PATH_TEMPLATE = property(*(_gen_get_set_path("I18N_DIR", "REL_MO_PATH_TEMPLATE")))
559
560     def _get_py_sys_paths(self):
561         return self.INTERN_PY_SYS_PATHS
562     def _set_py_sys_paths(self, val):
563         old_paths = set(self.INTERN_PY_SYS_PATHS.split(";")) - {""}
564         new_paths = set(val.split(";")) - {""}
565         for p in old_paths - new_paths:
566             if p in sys.path:
567                 sys.path.remove(p)
568         for p in new_paths - old_paths:
569             sys.path.append(p)
570         self.INTERN_PY_SYS_PATHS = val
571     PY_SYS_PATHS = property(_get_py_sys_paths, _set_py_sys_paths)