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