Translations utils: add needed bits to update git repo together with svn trunk.
[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
420 # The path to the *git* translation repository (relative to SOURCE_DIR).
421 REL_GIT_I18N_DIR = os.path.join("release/datafiles/locale")
422
423
424 # The /po path of the *git* translation repository (relative to REL_GIT_I18N_DIR).
425 REL_GIT_I18N_PO_DIR = os.path.join("po")
426
427
428 # The Blender source path to check for i18n macros (relative to SOURCE_DIR).
429 REL_POTFILES_SOURCE_DIR = os.path.join("source")
430
431 # The template messages file (relative to I18N_DIR).
432 REL_FILE_NAME_POT = os.path.join(REL_BRANCHES_DIR, DOMAIN + ".pot")
433
434 # Mo root datapath.
435 REL_MO_PATH_ROOT = os.path.join(REL_TRUNK_DIR, "locale")
436
437 # Mo path generator for a given language.
438 REL_MO_PATH_TEMPLATE = os.path.join(REL_MO_PATH_ROOT, "{}", "LC_MESSAGES")
439
440 # Mo path generator for a given language (relative to any "locale" dir).
441 MO_PATH_ROOT_RELATIVE = os.path.join("locale")
442 MO_PATH_TEMPLATE_RELATIVE = os.path.join(MO_PATH_ROOT_RELATIVE, "{}", "LC_MESSAGES")
443
444 # Mo file name.
445 MO_FILE_NAME = DOMAIN + ".mo"
446
447 # Where to search for py files that may contain ui strings (relative to one of the 'resource_path' of Blender).
448 CUSTOM_PY_UI_FILES = [
449     os.path.join("scripts", "startup", "bl_ui"),
450     os.path.join("scripts", "modules", "rna_prop_ui.py"),
451 ]
452
453 # An optional text file listing files to force include/exclude from py_xgettext process.
454 SRC_POTFILES = ""
455
456 # A cache storing validated msgids, to avoid re-spellchecking them.
457 SPELL_CACHE = os.path.join("/tmp", ".spell_cache")
458
459 # Threshold defining whether a new msgid is similar enough with an old one to reuse its translation...
460 SIMILAR_MSGID_THRESHOLD = 0.75
461
462 # Additional import paths to add to sys.path (';' separated)...
463 INTERN_PY_SYS_PATHS = ""
464
465 # Custom override settings must be one dir above i18n tools itself!
466 sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
467 try:
468     from bl_i18n_settings_override import *
469 except ImportError:  # If no i18n_override_settings available, it’s no error!
470     pass
471
472 # Override with custom user settings, if available.
473 try:
474     from settings_user import *
475 except ImportError:  # If no user_settings available, it’s no error!
476     pass
477
478
479 for p in set(INTERN_PY_SYS_PATHS.split(";")):
480     if p:
481         sys.path.append(p)
482
483
484 # The settings class itself!
485 def _do_get(ref, path):
486     return os.path.normpath(os.path.join(ref, path))
487
488
489 def _do_set(ref, path):
490     path = os.path.normpath(path)
491     # If given path is absolute, make it relative to current ref one (else we consider it is already the case!)
492     if os.path.isabs(path):
493         # can't always find the relative path (between drive letters on windows)
494         try:
495             return os.path.relpath(path, ref)
496         except ValueError:
497             pass
498     return path
499
500
501 def _gen_get_set_path(ref, name):
502     def _get(self):
503         return _do_get(getattr(self, ref), getattr(self, name))
504     def _set(self, value):
505         setattr(self, name, _do_set(getattr(self, ref), value))
506     return _get, _set
507
508
509 class I18nSettings:
510     """
511     Class allowing persistence of our settings!
512     Saved in JSon format, so settings should be JSon'able objects!
513     """
514     _settings = None
515
516     def __new__(cls, *args, **kwargs):
517         # Addon preferences are singleton by definition, so is this class!
518         if not I18nSettings._settings:
519             cls._settings = super(I18nSettings, cls).__new__(cls)
520             cls._settings.__dict__ = {uid: data for uid, data in globals().items() if not uid.startswith("_")}
521         return I18nSettings._settings
522
523     def from_json(self, string):
524         data = dict(json.loads(string))
525         # Special case... :/
526         if "INTERN_PY_SYS_PATHS" in data:
527             self.PY_SYS_PATHS = data["INTERN_PY_SYS_PATHS"]
528         self.__dict__.update(data)
529
530     def to_json(self):
531         # Only save the diff from default i18n_settings!
532         glob = globals()
533         export_dict = {uid: val for uid, val in self.__dict__.items() if glob.get(uid) != val}
534         return json.dumps(export_dict)
535
536     def load(self, fname, reset=False):
537         if reset:
538             self.__dict__ = {uid: data for uid, data in globals().items() if not uid.startswith("_")}
539         if isinstance(fname, str):
540             if not os.path.isfile(fname):
541                 return
542             with open(fname) as f:
543                 self.from_json(f.read())
544         # Else assume fname is already a file(like) object!
545         else:
546             self.from_json(fname.read())
547
548     def save(self, fname):
549         if isinstance(fname, str):
550             with open(fname, 'w') as f:
551                 f.write(self.to_json())
552         # Else assume fname is already a file(like) object!
553         else:
554             fname.write(self.to_json())
555
556     BRANCHES_DIR = property(*(_gen_get_set_path("I18N_DIR", "REL_BRANCHES_DIR")))
557     TRUNK_DIR = property(*(_gen_get_set_path("I18N_DIR", "REL_TRUNK_DIR")))
558     TRUNK_PO_DIR = property(*(_gen_get_set_path("I18N_DIR", "REL_TRUNK_PO_DIR")))
559     TRUNK_MO_DIR = property(*(_gen_get_set_path("I18N_DIR", "REL_TRUNK_MO_DIR")))
560     GIT_I18N_ROOT = property(*(_gen_get_set_path("SOURCE_DIR", "REL_GIT_I18N_DIR")))
561     GIT_I18N_PO_DIR = property(*(_gen_get_set_path("GIT_I18N_ROOT", "REL_GIT_I18N_PO_DIR")))
562     POTFILES_SOURCE_DIR = property(*(_gen_get_set_path("SOURCE_DIR", "REL_POTFILES_SOURCE_DIR")))
563     FILE_NAME_POT = property(*(_gen_get_set_path("I18N_DIR", "REL_FILE_NAME_POT")))
564     MO_PATH_ROOT = property(*(_gen_get_set_path("I18N_DIR", "REL_MO_PATH_ROOT")))
565     MO_PATH_TEMPLATE = property(*(_gen_get_set_path("I18N_DIR", "REL_MO_PATH_TEMPLATE")))
566
567     def _get_py_sys_paths(self):
568         return self.INTERN_PY_SYS_PATHS
569     def _set_py_sys_paths(self, val):
570         old_paths = set(self.INTERN_PY_SYS_PATHS.split(";")) - {""}
571         new_paths = set(val.split(";")) - {""}
572         for p in old_paths - new_paths:
573             if p in sys.path:
574                 sys.path.remove(p)
575         for p in new_paths - old_paths:
576             sys.path.append(p)
577         self.INTERN_PY_SYS_PATHS = val
578     PY_SYS_PATHS = property(_get_py_sys_paths, _set_py_sys_paths)