30ee3cdc7d89976c771bfdca554f2ebe24b20d43
[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     (41, "Vietnamese (tiếng Việt)", "vi_VN"),
92 )
93
94 # Default context, in py!
95 DEFAULT_CONTEXT = bpy.app.translations.contexts.default
96
97 # Name of language file used by Blender to generate translations' menu.
98 LANGUAGES_FILE = "languages"
99
100 # The min level of completeness for a po file to be imported from /branches into /trunk, as a percentage.
101 IMPORT_MIN_LEVEL = 0.0
102
103 # Languages in /branches we do not want to import in /trunk currently...
104 IMPORT_LANGUAGES_SKIP = {
105     'am_ET', 'bg_BG', 'fi_FI', 'el_GR', 'et_EE', 'ne_NP', 'ro_RO', 'uz_UZ', 'uz_UZ@cyrillic',
106 }
107
108 # Languages that need RTL pre-processing.
109 IMPORT_LANGUAGES_RTL = {
110     'ar_EG', 'fa_IR', 'he_IL',
111 }
112
113 # The comment prefix used in generated messages.txt file.
114 MSG_COMMENT_PREFIX = "#~ "
115
116 # The comment prefix used in generated messages.txt file.
117 MSG_CONTEXT_PREFIX = "MSGCTXT:"
118
119 # The default comment prefix used in po's.
120 PO_COMMENT_PREFIX= "# "
121
122 # The comment prefix used to mark sources of msgids, in po's.
123 PO_COMMENT_PREFIX_SOURCE = "#: "
124
125 # The comment prefix used to mark sources of msgids, in po's.
126 PO_COMMENT_PREFIX_SOURCE_CUSTOM = "#. :src: "
127
128 # The general "generated" comment prefix, in po's.
129 PO_COMMENT_PREFIX_GENERATED = "#. "
130
131 # The comment prefix used to comment entries in po's.
132 PO_COMMENT_PREFIX_MSG= "#~ "
133
134 # The comment prefix used to mark fuzzy msgids, in po's.
135 PO_COMMENT_FUZZY = "#, fuzzy"
136
137 # The prefix used to define context, in po's.
138 PO_MSGCTXT = "msgctxt "
139
140 # The prefix used to define msgid, in po's.
141 PO_MSGID = "msgid "
142
143 # The prefix used to define msgstr, in po's.
144 PO_MSGSTR = "msgstr "
145
146 # The 'header' key of po files.
147 PO_HEADER_KEY = (DEFAULT_CONTEXT, "")
148
149 PO_HEADER_MSGSTR = (
150     "Project-Id-Version: {blender_ver} ({blender_hash})\\n\n"
151     "Report-Msgid-Bugs-To: \\n\n"
152     "POT-Creation-Date: {time}\\n\n"
153     "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\n"
154     "Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\n"
155     "Language-Team: LANGUAGE <LL@li.org>\\n\n"
156     "Language: {uid}\\n\n"
157     "MIME-Version: 1.0\\n\n"
158     "Content-Type: text/plain; charset=UTF-8\\n\n"
159     "Content-Transfer-Encoding: 8bit\n"
160 )
161 PO_HEADER_COMMENT_COPYRIGHT = (
162     "# Blender's translation file (po format).\n"
163     "# Copyright (C) {year} The Blender Foundation.\n"
164     "# This file is distributed under the same license as the Blender package.\n"
165     "#\n"
166 )
167 PO_HEADER_COMMENT = (
168     "# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.\n"
169     "#"
170 )
171
172 TEMPLATE_ISO_ID = "__TEMPLATE__"
173
174 # Num buttons report their label with a trailing ': '...
175 NUM_BUTTON_SUFFIX = ": "
176
177 # Undocumented operator placeholder string.
178 UNDOC_OPS_STR = "(undocumented operator)"
179
180 # The gettext domain.
181 DOMAIN = "blender"
182
183 # Our own "gettext" stuff.
184 # File type (ext) to parse.
185 PYGETTEXT_ALLOWED_EXTS = {".c", ".cpp", ".cxx", ".hpp", ".hxx", ".h"}
186
187 # Max number of contexts into a BLF_I18N_MSGID_MULTI_CTXT macro...
188 PYGETTEXT_MAX_MULTI_CTXT = 16
189
190 # Where to search contexts definitions, relative to SOURCE_DIR (defined below).
191 PYGETTEXT_CONTEXTS_DEFSRC = os.path.join("source", "blender", "blenfont", "BLF_translation.h")
192
193 # Regex to extract contexts defined in BLF_translation.h
194 # XXX Not full-proof, but should be enough here!
195 PYGETTEXT_CONTEXTS = "#define\\s+(BLF_I18NCONTEXT_[A-Z_0-9]+)\\s+\"([^\"]*)\""
196
197 # Keywords' regex.
198 # XXX Most unfortunately, we can't use named backreferences inside character sets,
199 #     which makes the regexes even more twisty... :/
200 _str_base = (
201     # Match void string
202     "(?P<{_}1>[\"'])(?P={_}1)"  # Get opening quote (' or "), and closing immediately.
203     "|"
204     # Or match non-void string
205     "(?P<{_}2>[\"'])"  # Get opening quote (' or ").
206         "(?{capt}(?:"
207             # This one is for crazy things like "hi \\\\\" folks!"...
208             r"(?:(?!<\\)(?:\\\\)*\\(?=(?P={_}2)))|"
209             # The most common case.
210             ".(?!(?P={_}2))"
211         ")+.)"  # Don't forget the last char!
212     "(?P={_}2)"  # And closing quote.
213 )
214 str_clean_re = _str_base.format(_="g", capt="P<clean>")
215 _inbetween_str_re = (
216     # XXX Strings may have comments between their pieces too, not only spaces!
217     r"(?:\s*(?:"
218         # A C comment
219         r"/\*.*(?!\*/).\*/|"
220         # Or a C++ one!
221         r"//[^\n]*\n"
222     # And we are done!
223     r")?)*"
224 )
225 # Here we have to consider two different cases (empty string and other).
226 _str_whole_re = (
227     _str_base.format(_="{_}1_", capt=":") +
228     # Optional loop start, this handles "split" strings...
229     "(?:(?<=[\"'])" + _inbetween_str_re + "(?=[\"'])(?:"
230         + _str_base.format(_="{_}2_", capt=":") +
231     # End of loop.
232     "))*"
233 )
234 _ctxt_re_gen = lambda uid : r"(?P<ctxt_raw{uid}>(?:".format(uid=uid) + \
235                             _str_whole_re.format(_="_ctxt{uid}".format(uid=uid)) + \
236                             r")|(?:[A-Z_0-9]+))"
237 _ctxt_re = _ctxt_re_gen("")
238 _msg_re = r"(?P<msg_raw>" + _str_whole_re.format(_="_msg") + r")"
239 PYGETTEXT_KEYWORDS = (() +
240     tuple((r"{}\(\s*" + _msg_re + r"\s*\)").format(it)
241           for it in ("IFACE_", "TIP_", "DATA_", "N_")) +
242
243     tuple((r"{}\(\s*" + _ctxt_re + r"\s*,\s*" + _msg_re + r"\s*\)").format(it)
244           for it in ("CTX_IFACE_", "CTX_TIP_", "CTX_DATA_", "CTX_N_")) +
245
246     tuple(("{}\\((?:[^\"',]+,){{1,2}}\\s*" + _msg_re + r"\s*(?:\)|,)").format(it)
247           for it in ("BKE_report", "BKE_reportf", "BKE_reports_prepend", "BKE_reports_prependf",
248                      "CTX_wm_operator_poll_msg_set")) +
249
250     tuple(("{}\\((?:[^\"',]+,){{3}}\\s*" + _msg_re + r"\s*\)").format(it)
251           for it in ("BMO_error_raise",)) +
252
253     tuple(("{}\\((?:[^\"',]+,)\\s*" + _msg_re + r"\s*(?:\)|,)").format(it)
254           for it in ("modifier_setError",)) +
255
256     tuple((r"{}\(\s*" + _msg_re + r"\s*,\s*(?:" +
257            r"\s*,\s*)?(?:".join(_ctxt_re_gen(i) for i in range(PYGETTEXT_MAX_MULTI_CTXT)) + r")?\s*\)").format(it)
258           for it in ("BLF_I18N_MSGID_MULTI_CTXT",))
259 )
260
261 # Check printf mismatches between msgid and msgstr.
262 CHECK_PRINTF_FORMAT = (
263     r"(?!<%)(?:%%)*%"          # Begining, with handling for crazy things like '%%%%%s'
264     r"[-+#0]?"                 # Flags (note: do not add the ' ' (space) flag here, generates too much false positives!)
265     r"(?:\*|[0-9]+)?"          # Width
266     r"(?:\.(?:\*|[0-9]+))?"    # Precision
267     r"(?:[hljztL]|hh|ll)?"     # Length
268     r"[tldiuoxXfFeEgGaAcspn]"  # Specifiers (note we have Blender-specific %t and %l ones too)
269 )
270
271 # Should po parser warn when finding a first letter not capitalized?
272 WARN_MSGID_NOT_CAPITALIZED = True
273
274 # Strings that should not raise above warning!
275 WARN_MSGID_NOT_CAPITALIZED_ALLOWED = {
276     "",                              # Simplifies things... :p
277     "ac3",
278     "along X",
279     "along Y",
280     "along Z",
281     "along %s X",
282     "along %s Y",
283     "along %s Z",
284     "along local Z",
285     "ascii",
286     "author",                        # Addons' field. :/
287     "bItasc",
288     "description",                   # Addons' field. :/
289     "dx",
290     "fBM",
291     "flac",
292     "fps: %.2f",
293     "fps: %i",
294     "gimbal",
295     "global",
296     "iScale",
297     "iso-8859-15",
298     "iTaSC",
299     "iTaSC parameters",
300     "kb",
301     "local",
302     "location",                      # Addons' field. :/
303     "locking %s X",
304     "locking %s Y",
305     "locking %s Z",
306     "mkv",
307     "mm",
308     "mp2",
309     "mp3",
310     "normal",
311     "ogg",
312     "p0",
313     "px",
314     "re",
315     "res",
316     "rv",
317     "sin(x) / x",
318     "sqrt(x*x+y*y+z*z)",
319     "sRGB",
320     "utf-8",
321     "var",
322     "vBVH",
323     "view",
324     "wav",
325     "y",
326     # Sub-strings.
327     "available with",
328     "brown fox",
329     "can't save image while rendering",
330     "expected a timeline/animation area to be active",
331     "expected a view3d region",
332     "expected a view3d region & editcurve",
333     "expected a view3d region & editmesh",
334     "image file not found",
335     "image path can't be written to",
336     "in memory to enable editing!",
337     "jumps over",
338     "the lazy dog",
339     "unable to load movie clip",
340     "unable to load text",
341     "unable to open the file",
342     "unknown error reading file",
343     "unknown error stating file",
344     "unknown error writing file",
345     "unsupported font format",
346     "unsupported format",
347     "unsupported image format",
348     "unsupported movie clip format",
349     "verts only",
350     "virtual parents",
351 }
352 WARN_MSGID_NOT_CAPITALIZED_ALLOWED |= set(lng[2] for lng in LANGUAGES)
353
354 WARN_MSGID_END_POINT_ALLOWED = {
355     "Numpad .",
356     "Circle|Alt .",
357     "Temp. Diff.",
358     "Float Neg. Exp.",
359     "    RNA Path: bpy.types.",
360     "Max Ext.",
361 }
362
363 PARSER_CACHE_HASH = 'sha1'
364
365 PARSER_TEMPLATE_ID = "__POT__"
366 PARSER_PY_ID = "__PY__"
367
368 PARSER_PY_MARKER_BEGIN = "\n# ##### BEGIN AUTOGENERATED I18N SECTION #####\n"
369 PARSER_PY_MARKER_END = "\n# ##### END AUTOGENERATED I18N SECTION #####\n"
370
371 PARSER_MAX_FILE_SIZE = 2 ** 24  # in bytes, i.e. 16 Mb.
372
373 ###############################################################################
374 # PATHS
375 ###############################################################################
376
377 # The Python3 executable.You’ll likely have to edit it in your user_settings.py
378 # if you’re under Windows.
379 PYTHON3_EXEC = "python3"
380
381 # The Blender executable!
382 # This is just an example, you’ll have to edit it in your user_settings.py!
383 BLENDER_EXEC = os.path.abspath(os.path.join("foo", "bar", "blender"))
384 # check for blender.bin
385 if not os.path.exists(BLENDER_EXEC):
386     if os.path.exists(BLENDER_EXEC + ".bin"):
387         BLENDER_EXEC = BLENDER_EXEC + ".bin"
388
389 # The gettext msgfmt "compiler". You’ll likely have to edit it in your user_settings.py if you’re under Windows.
390 GETTEXT_MSGFMT_EXECUTABLE = "msgfmt"
391
392 # The FriBidi C compiled library (.so under Linux, .dll under windows...).
393 # You’ll likely have to edit it in your user_settings.py if you’re under Windows., e.g. using the included one:
394 #     FRIBIDI_LIB = os.path.join(TOOLS_DIR, "libfribidi.dll")
395 FRIBIDI_LIB = "libfribidi.so.0"
396
397 # The name of the (currently empty) file that must be present in a po's directory to enable rtl-preprocess.
398 RTL_PREPROCESS_FILE = "is_rtl"
399
400 # The Blender source root path.
401 # This is just an example, you’ll have to override it in your user_settings.py!
402 SOURCE_DIR = os.path.abspath(os.path.join("blender"))
403
404 # The bf-translation repository (you'll have to override this in your user_settings.py).
405 I18N_DIR = os.path.abspath(os.path.join("i18n"))
406
407 # The /branches path (relative to I18N_DIR).
408 REL_BRANCHES_DIR = os.path.join("branches")
409
410 # The /trunk path (relative to I18N_DIR).
411 REL_TRUNK_DIR = os.path.join("trunk")
412
413 # The /trunk/po path (relative to I18N_DIR).
414 REL_TRUNK_PO_DIR = os.path.join(REL_TRUNK_DIR, "po")
415
416 # The /trunk/mo path (relative to I18N_DIR).
417 REL_TRUNK_MO_DIR = os.path.join(REL_TRUNK_DIR, "locale")
418
419 # The Blender source path to check for i18n macros (relative to SOURCE_DIR).
420 REL_POTFILES_SOURCE_DIR = os.path.join("source")
421
422 # The template messages file (relative to I18N_DIR).
423 REL_FILE_NAME_POT = os.path.join(REL_BRANCHES_DIR, DOMAIN + ".pot")
424
425 # Mo root datapath.
426 REL_MO_PATH_ROOT = os.path.join(REL_TRUNK_DIR, "locale")
427
428 # Mo path generator for a given language.
429 REL_MO_PATH_TEMPLATE = os.path.join(REL_MO_PATH_ROOT, "{}", "LC_MESSAGES")
430
431 # Mo path generator for a given language (relative to any "locale" dir).
432 MO_PATH_ROOT_RELATIVE = os.path.join("locale")
433 MO_PATH_TEMPLATE_RELATIVE = os.path.join(MO_PATH_ROOT_RELATIVE, "{}", "LC_MESSAGES")
434
435 # Mo file name.
436 MO_FILE_NAME = DOMAIN + ".mo"
437
438 # Where to search for py files that may contain ui strings (relative to one of the 'resource_path' of Blender).
439 CUSTOM_PY_UI_FILES = [
440     os.path.join("scripts", "startup", "bl_ui"),
441     os.path.join("scripts", "modules", "rna_prop_ui.py"),
442 ]
443
444 # An optional text file listing files to force include/exclude from py_xgettext process.
445 SRC_POTFILES = ""
446
447 # A cache storing validated msgids, to avoid re-spellchecking them.
448 SPELL_CACHE = os.path.join("/tmp", ".spell_cache")
449
450 # Threshold defining whether a new msgid is similar enough with an old one to reuse its translation...
451 SIMILAR_MSGID_THRESHOLD = 0.75
452
453 # Additional import paths to add to sys.path (';' separated)...
454 INTERN_PY_SYS_PATHS = ""
455
456 # Custom override settings must be one dir above i18n tools itself!
457 sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
458 try:
459     from bl_i18n_settings_override import *
460 except ImportError:  # If no i18n_override_settings available, it’s no error!
461     pass
462
463 # Override with custom user settings, if available.
464 try:
465     from settings_user import *
466 except ImportError:  # If no user_settings available, it’s no error!
467     pass
468
469
470 for p in set(INTERN_PY_SYS_PATHS.split(";")):
471     if p:
472         sys.path.append(p)
473
474
475 # The settings class itself!
476 def _do_get(ref, path):
477     return os.path.normpath(os.path.join(ref, path))
478
479
480 def _do_set(ref, path):
481     path = os.path.normpath(path)
482     # If given path is absolute, make it relative to current ref one (else we consider it is already the case!)
483     if os.path.isabs(path):
484         # can't always find the relative path (between drive letters on windows)
485         try:
486             return os.path.relpath(path, ref)
487         except ValueError:
488             pass
489     return path
490
491
492 def _gen_get_set_path(ref, name):
493     def _get(self):
494         return _do_get(getattr(self, ref), getattr(self, name))
495     def _set(self, value):
496         setattr(self, name, _do_set(getattr(self, ref), value))
497     return _get, _set
498
499
500 def _gen_get_set_paths(ref, name):
501     def _get(self):
502         return [_do_get(getattr(self, ref), p) for p in getattr(self, name)]
503     def _set(self, value):
504         setattr(self, name, [_do_set(getattr(self, ref), p) for p in value])
505     return _get, _set
506
507
508 class I18nSettings:
509     """
510     Class allowing persistence of our settings!
511     Saved in JSon format, so settings should be JSon'able objects!
512     """
513     _settings = None
514
515     def __new__(cls, *args, **kwargs):
516         # Addon preferences are singleton by definition, so is this class!
517         if not I18nSettings._settings:
518             cls._settings = super(I18nSettings, cls).__new__(cls)
519             cls._settings.__dict__ = {uid: data for uid, data in globals().items() if not uid.startswith("_")}
520         return I18nSettings._settings
521
522     def from_json(self, string):
523         data = dict(json.loads(string))
524         # Special case... :/
525         if "INTERN_PY_SYS_PATHS" in data:
526             self.PY_SYS_PATHS = data["INTERN_PY_SYS_PATHS"]
527         self.__dict__.update(data)
528
529     def to_json(self):
530         # Only save the diff from default i18n_settings!
531         glob = globals()
532         export_dict = {uid: val for uid, val in self.__dict__.items() if glob.get(uid) != val}
533         return json.dumps(export_dict)
534
535     def load(self, fname, reset=False):
536         if reset:
537             self.__dict__ = {uid: data for uid, data in globals().items() if not uid.startswith("_")}
538         if isinstance(fname, str):
539             if not os.path.isfile(fname):
540                 return
541             with open(fname) as f:
542                 self.from_json(f.read())
543         # Else assume fname is already a file(like) object!
544         else:
545             self.from_json(fname.read())
546
547     def save(self, fname):
548         if isinstance(fname, str):
549             with open(fname, 'w') as f:
550                 f.write(self.to_json())
551         # Else assume fname is already a file(like) object!
552         else:
553             fname.write(self.to_json())
554
555     BRANCHES_DIR = property(*(_gen_get_set_path("I18N_DIR", "REL_BRANCHES_DIR")))
556     TRUNK_DIR = property(*(_gen_get_set_path("I18N_DIR", "REL_TRUNK_DIR")))
557     TRUNK_PO_DIR = property(*(_gen_get_set_path("I18N_DIR", "REL_TRUNK_PO_DIR")))
558     TRUNK_MO_DIR = property(*(_gen_get_set_path("I18N_DIR", "REL_TRUNK_MO_DIR")))
559     POTFILES_SOURCE_DIR = property(*(_gen_get_set_path("SOURCE_DIR", "REL_POTFILES_SOURCE_DIR")))
560     FILE_NAME_POT = property(*(_gen_get_set_path("I18N_DIR", "REL_FILE_NAME_POT")))
561     MO_PATH_ROOT = property(*(_gen_get_set_path("I18N_DIR", "REL_MO_PATH_ROOT")))
562     MO_PATH_TEMPLATE = property(*(_gen_get_set_path("I18N_DIR", "REL_MO_PATH_TEMPLATE")))
563
564     def _get_py_sys_paths(self):
565         return self.INTERN_PY_SYS_PATHS
566     def _set_py_sys_paths(self, val):
567         old_paths = set(self.INTERN_PY_SYS_PATHS.split(";")) - {""}
568         new_paths = set(val.split(";")) - {""}
569         for p in old_paths - new_paths:
570             if p in sys.path:
571                 sys.path.remove(p)
572         for p in new_paths - old_paths:
573             sys.path.append(p)
574         self.INTERN_PY_SYS_PATHS = val
575     PY_SYS_PATHS = property(_get_py_sys_paths, _set_py_sys_paths)