File indexing completed on 2024-12-08 13:31:59
0001 # -*- coding: UTF-8 -*- 0002 0003 """ 0004 Resolve UI references in translation by following through original texts. 0005 0006 If PO files which are delivered are not the same PO files which 0007 are actually being translated, but there is processing step involved to 0008 get former from the latter, there is a possibility to automatically 0009 resolve references to user interface strings mentioned through messages 0010 (typical e.g. of documentation POs). Compared to hard-coding it, 0011 this enables referenced UI text to always be in sync with actual UI, 0012 without necessity for manual tracking of changes in the UI. 0013 0014 See C{doc/user/lingo.docbook#sec-lguirefs} for details. 0015 0016 @var default_headrefs: Default heads for explicit UI references. 0017 0018 @author: Chusslove Illich (Часлав Илић) <caslav.ilic@gmx.net> 0019 @license: GPLv3 0020 """ 0021 0022 # NOTE: The implementation is tuned to look for and open as few as possible 0023 # UI catalogs, and as lazily as possible. 0024 0025 import hashlib 0026 import os 0027 import re 0028 0029 from pology import _, n_ 0030 from pology.catalog import Catalog 0031 from pology.remove import remove_accel_msg, remove_markup_msg 0032 from pology.colors import cjoin 0033 from pology.fsops import collect_catalogs, collect_catalogs_by_env 0034 from pology.getfunc import get_hook_ireq 0035 from pology.msgreport import warning_on_msg 0036 from pology.report import warning 0037 0038 0039 default_headrefs = ["~%"] 0040 0041 0042 def resolve_ui (headrefs=default_headrefs, tagrefs=[], uipathseps=[], 0043 uicpaths=None, uicpathenv=None, xmlescape=False, pfhook=None, 0044 mkeyw=None, invmkeyw=False, quiet=False, fdiralt={}): 0045 """ 0046 Resolve general UI references in translations [hook factory]. 0047 0048 If UI catalogs are collected through the environment variable, 0049 a warning is issued if the given variable has not been set. 0050 0051 Resolved UI text can be postprocessed by an F1A hook (C{(text)->text}). 0052 It can be given either as the hook function itself, or as 0053 a L{language request<getfunc.get_hook_ireq>} string. 0054 0055 If one or several markup keywords are given as C{mkeyw} parameter, 0056 UI reference resolution is skipped for catalogs which do not report one 0057 of the given keywords by their L{markup()<catalog.Catalog.markup>} 0058 method. This match may be inverted by C{invmkeyw} parameter, i.e. 0059 to skip resolution for catalogs reporting one of given keywords. 0060 0061 The list of UI path separators given by the C{uipathseps} parameter 0062 is ordered by priority, such that the first one found in the composite 0063 reference text is used to split it into componental UI references. 0064 0065 If the UI reference contains a formatting directive/argument placeholder, 0066 and the UI reference is found in a message of the same format 0067 (e.g. a tooltip referencing another part of UI), then using the argument 0068 substitution syntax may make the message invalid for the C{msgfmt -c} 0069 check. In that case, an alternative directive start string can be given, 0070 which will mask it from C{msgfmt -c}. This is specified by C{fdiralt} 0071 parameter, as a dictionary of alternative (key) and normal (value) 0072 start strings. 0073 0074 @param headrefs: heads for explicit UI references 0075 @type headrefs: list of strings 0076 @param tagrefs: XML-like tags which define implicit UI references 0077 @type tagrefs: list of strings 0078 @param uipathseps: separators in composited UI references 0079 @type uipathseps: list of strings 0080 @param uicpaths: paths to UI catalogs in the project 0081 (both files and directories can be given) 0082 @type uicpaths: list of strings 0083 @param uicpathenv: environment variable defining directories 0084 where UI catalogs may be found (colon-separated directory paths) 0085 @type uicpathenv: string 0086 @param xmlescape: whether to normalize UI text for XML 0087 @type xmlescape: bool 0088 @param pfhook: F1A hook to postprocess resolved UI text 0089 @type pfhook: function or string 0090 @param mkeyw: markup keywords for taking catalogs into account 0091 @type mkeyw: string or list of strings 0092 @param invmkeyw: whether to invert the meaning of C{mkeyw} parameter 0093 @type invmkeyw: bool 0094 @param quiet: whether to output warnings of failed resolutions 0095 @type quiet: bool 0096 @param fdiralt: alternative and normal start strings for masking 0097 formatting directives 0098 @type fdiralt: {string: string} 0099 0100 @return: type F3C hook 0101 @rtype: C{(msgstr, msg, cat) -> msgstr} 0102 """ 0103 0104 return _resolve_ui_w(headrefs, tagrefs, uipathseps, uicpaths, uicpathenv, 0105 xmlescape, pfhook, mkeyw, invmkeyw, quiet, fdiralt, 0106 modtext=True, spanrep=False) 0107 0108 0109 def check_ui (headrefs=default_headrefs, tagrefs=[], uipathseps=[], 0110 uicpaths=None, uicpathenv=None, xmlescape=False, 0111 mkeyw=None, invmkeyw=False, fdiralt={}): 0112 """ 0113 Check general UI references in translations [hook factory]. 0114 0115 See L{resolve_ui} for description of parameters. 0116 0117 @return: type V3C hook 0118 @rtype: C{(msgstr, msg, cat) -> spans} 0119 """ 0120 0121 pfhook = None 0122 quiet = True 0123 return _resolve_ui_w(headrefs, tagrefs, uipathseps, uicpaths, uicpathenv, 0124 xmlescape, pfhook, mkeyw, invmkeyw, quiet, fdiralt, 0125 modtext=False, spanrep=True) 0126 0127 0128 _tagrefs_docbook4 = [ 0129 "guilabel", "guibutton", "guiicon", "guimenu", "guisubmenu", 0130 "guimenuitem", 0131 ] 0132 0133 def resolve_ui_docbook4 (headrefs=default_headrefs, 0134 uicpaths=None, uicpathenv=None, pfhook=None, 0135 mkeyw=None, quiet=False): 0136 """ 0137 Resolve UI references in Docbook 4.x translations [hook factory]. 0138 0139 A convenience hook which fixes some of the parameters to L{resolve_ui} 0140 to match implicit UI references and formatting needs for Docbook POs. 0141 0142 @return: type F3C hook 0143 @rtype: C{(msgstr, msg, cat) -> msgstr} 0144 """ 0145 0146 tagrefs = _tagrefs_docbook4 0147 uipathseps = [] 0148 xmlescape = True 0149 invmkeyw = False 0150 fdiralt = {} 0151 return _resolve_ui_w(headrefs, tagrefs, uipathseps, uicpaths, uicpathenv, 0152 xmlescape, pfhook, mkeyw, invmkeyw, quiet, fdiralt, 0153 modtext=True, spanrep=False) 0154 0155 0156 def check_ui_docbook4 (headrefs=default_headrefs, 0157 uicpaths=None, uicpathenv=None, mkeyw=None): 0158 """ 0159 Check UI references in Docbook 4.x translations [hook factory]. 0160 0161 A convenience resolver which fixes some of the parameters to L{check_ui} 0162 to match implicit UI references and formatting needs for Docbook POs. 0163 0164 @return: type V3C hook 0165 @rtype: C{(msgstr, msg, cat) -> spans} 0166 """ 0167 0168 tagrefs = _tagrefs_docbook4 0169 uipathseps = [] 0170 xmlescape = True 0171 invmkeyw = False 0172 pfhook = None 0173 quiet = True 0174 fdiralt = {} 0175 return _resolve_ui_w(headrefs, tagrefs, uipathseps, uicpaths, uicpathenv, 0176 xmlescape, pfhook, mkeyw, invmkeyw, quiet, fdiralt, 0177 modtext=False, spanrep=True) 0178 0179 0180 _tagrefs_kde4 = [ 0181 "interface", 0182 ] 0183 0184 def resolve_ui_kde4 (headrefs=default_headrefs, uipathseps=None, 0185 uicpaths=None, uicpathenv=None, pfhook=None, 0186 mkeyw=None, quiet=False): 0187 """ 0188 Resolve UI references in KDE4 UI translations [hook factory]. 0189 0190 A convenience resolver which fixes some of the parameters to L{resolve_ui} 0191 to match implicit UI references and formatting needs for KDE4 UI POs. 0192 0193 If C{uipathseps} is C{None}, separators known to KUIT C{<interface>} tag 0194 will be used automatically. 0195 0196 C{fdiralt} is set to C{{"%~": "%"}}. 0197 0198 @return: type F3C hook 0199 @rtype: C{(msgstr, msg, cat) -> msgstr} 0200 """ 0201 0202 tagrefs = _tagrefs_kde4 0203 if uipathseps is None: 0204 uipathseps = ["->"] 0205 xmlescape = True 0206 invmkeyw = False 0207 fdiralt = {"%~": "%"} 0208 return _resolve_ui_w(headrefs, tagrefs, uipathseps, uicpaths, uicpathenv, 0209 xmlescape, pfhook, mkeyw, invmkeyw, quiet, fdiralt, 0210 modtext=True, spanrep=False) 0211 0212 0213 def check_ui_kde4 (headrefs=default_headrefs, uipathseps=None, 0214 uicpaths=None, uicpathenv=None, mkeyw=None): 0215 """ 0216 Check UI references in KDE4 UI translations [hook factory]. 0217 0218 A convenience resolver which fixes some of the parameters to L{check_ui} 0219 to match implicit UI references and formatting needs for KDE4 UI POs. 0220 0221 If C{uipathseps} is C{None}, separators known to KUIT C{<interface>} tag 0222 will be used automatically. 0223 0224 C{fdiralt} is set to C{{"%~": "%"}}. 0225 0226 @return: type V3C hook 0227 @rtype: C{(msgstr, msg, cat) -> spans} 0228 """ 0229 0230 tagrefs = _tagrefs_kde4 0231 if uipathseps is None: 0232 uipathseps = ["->"] 0233 xmlescape = True 0234 invmkeyw = False 0235 pfhook = None 0236 quiet = True 0237 fdiralt = {"%~": "%"} 0238 return _resolve_ui_w(headrefs, tagrefs, uipathseps, uicpaths, uicpathenv, 0239 xmlescape, pfhook, mkeyw, invmkeyw, quiet, fdiralt, 0240 modtext=False, spanrep=True) 0241 0242 0243 def _resolve_ui_w (headrefs, tagrefs, uipathseps, uicpaths, uicpathenv, 0244 xmlescape, pfhook, mkeyw, invmkeyw, quiet, fdiralt, 0245 modtext, spanrep): 0246 """ 0247 Worker for resolver factories. 0248 """ 0249 0250 # Convert sequences into sets, for fast membership checks. 0251 if not isinstance(tagrefs, set): 0252 tagrefs = set(tagrefs) 0253 if not isinstance(headrefs, set): 0254 headrefs = set(headrefs) 0255 if not isinstance(uipathseps, set): 0256 uipathseps = set(uipathseps) 0257 0258 # Markup keywords should remain None if not a sequence or string. 0259 if mkeyw is not None: 0260 if isinstance(mkeyw, str): 0261 mkeyw = [mkeyw] 0262 mkeyw = set(mkeyw) 0263 0264 # Construct post-filtering hook. 0265 if pfhook is None: 0266 pfhook = lambda x: x 0267 elif isinstance(pfhook, str): 0268 pfhook = get_hook_ireq(pfhook) 0269 # ...else assume it is already a hook function. 0270 0271 # Regular expressions for finding and extracting UI references. 0272 # Add a never-match expression to start regexes for all reference types, 0273 # so that it can be applied even if the category has no entries. 0274 rxflags = re.U|re.I 0275 # - by tags 0276 rxstr = r"<\s*(%s)\b.*?>" % "|".join(list(tagrefs) + ["\x04"]) 0277 uiref_start_tag_rx = re.compile(rxstr, rxflags) 0278 uiref_extract_tag_rx = {} 0279 for tag in tagrefs: 0280 rxstr = r"<\s*(%s)\b.*?>(.*?)(<\s*/\s*\1\s*>)" % tag 0281 uiref_extract_tag_rx[tag] = re.compile(rxstr, rxflags) 0282 # - by heads 0283 rxstr = r"(%s)" % "|".join(list(headrefs) + ["\x04"]) 0284 uiref_start_head_rx = re.compile(rxstr, rxflags) 0285 uiref_extract_head_rx = {} 0286 for head in headrefs: 0287 rxstr = r"%s(.)(.*?)\1" % head 0288 uiref_extract_head_rx[head] = re.compile(rxstr, rxflags) 0289 0290 # Lazy-evaluated data. 0291 ldata = {} 0292 0293 # Function to split text by UI references, into list of tuples with 0294 # the text segment preceeding the reference as first element, 0295 # the reference as second element, and span indices of the reference 0296 # against complete text as the third and fourth elements; 0297 # trailing text segment has None as reference, and invalid span. 0298 # "Blah <ui>foo</ui> blah ~%/bar/ blah." -> 0299 # [("Blah <ui>", "foo", 9, 12), ("</ui> blah ", "bar", 26, 29), 0300 # (" blah.", None, -1, -1)] 0301 def split_by_uiref (text, msg, cat, errspans): 0302 0303 rsplit = [] 0304 0305 ltext = len(text) 0306 p = 0 0307 while True: 0308 mt = uiref_start_tag_rx.search(text, p) 0309 if mt: pt = mt.start() 0310 else: pt = ltext 0311 mh = uiref_start_head_rx.search(text, p) 0312 if mh: ph = mh.start() 0313 else: ph = ltext 0314 0315 if pt < ph: 0316 # Tagged UI reference. 0317 tag = mt.group(1) 0318 m = uiref_extract_tag_rx[tag].search(text, pt) 0319 if not m: 0320 errmsg = _("@info \"tag\" is a tag in HTML/XML context", 0321 "Non-terminated UI reference by tag '%(tag)s'.", 0322 tag=tag) 0323 errspans.append(mt.span() + (errmsg,)) 0324 if not spanrep and not quiet: 0325 warning_on_msg(errmsg, msg, cat) 0326 break 0327 0328 uirefpath = m.group(2) 0329 pe = m.end() - len(m.group(3)) 0330 ps = pe - len(uirefpath) 0331 0332 elif ph < pt: 0333 # Headed UI reference. 0334 head = mh.group(1) 0335 m = uiref_extract_head_rx[head].search(text, ph) 0336 if not m: 0337 errmsg = _("@info \"head\" is the leading part of " 0338 "UI reference, e.g. '~%' in '~%/Save All/'", 0339 "Non-terminated UI reference by " 0340 "head '%(head)s'.", 0341 head=head) 0342 errspans.append(mh.span() + (errmsg,)) 0343 if not spanrep and not quiet: 0344 warning_on_msg(errmsg, msg, cat) 0345 break 0346 0347 uirefpath = m.group(2) 0348 ps, pe = m.span() 0349 0350 else: 0351 # Both positions equal, meaning end of text. 0352 break 0353 0354 ptext_uiref = _split_uirefpath(text[p:ps], uirefpath, uipathseps) 0355 for ptext, uiref in ptext_uiref: 0356 rsplit.append((ptext, uiref, ps, pe)) 0357 p = pe 0358 0359 # Trailing segment (or everything after an error). 0360 rsplit.append((text[p:], None, -1, -1)) 0361 0362 return rsplit 0363 0364 0365 # Function to resolve given UI reference 0366 # (part that needs to be under closure). 0367 def resolve_single_uiref (uiref, msg, cat, resolver_helper): 0368 0369 if ldata.get("uicpaths") is None: 0370 ldata["uicpaths"] = _collect_ui_catpaths(uicpaths, uicpathenv) 0371 if ldata.get("actcatfile") != cat.filename: 0372 ldata["actcatfile"] = cat.filename 0373 ldata["normcats"] = _load_norm_ui_cats(cat, ldata["uicpaths"], 0374 xmlescape) 0375 normcats = ldata["normcats"] 0376 0377 hookcl_f3c = lambda uiref: resolver_helper(uiref, msg, cat, True, False) 0378 hookcl_v3c = lambda uiref: resolver_helper(uiref, msg, cat, False, True) 0379 uiref_res, errmsgs = _resolve_single_uiref(uiref, normcats, 0380 hookcl_f3c, hookcl_v3c, 0381 fdiralt) 0382 uiref_res = pfhook(uiref_res) 0383 0384 return uiref_res, errmsgs 0385 0386 0387 # The resolver itself, in two parts. 0388 def resolver_helper (msgstr, msg, cat, modtext, spanrep): 0389 0390 errspans = [] 0391 tsegs = [] 0392 0393 if ( mkeyw is None 0394 or (not invmkeyw and mkeyw.intersection(cat.markup() or set())) 0395 or (invmkeyw and not mkeyw.intersection(cat.markup() or set())) 0396 ): 0397 rsplit = split_by_uiref(msgstr, msg, cat, errspans) 0398 0399 for ptext, uiref, start, end in rsplit: 0400 tsegs.append(ptext) 0401 if uiref is not None: 0402 uiref_res, errmsgs = resolve_single_uiref(uiref, msg, cat, 0403 resolver_helper) 0404 tsegs.append(uiref_res) 0405 errspans.extend([(start, end, x) for x in errmsgs]) 0406 if not spanrep and not quiet: 0407 for errmsg in errmsgs: 0408 warning_on_msg(errmsg, msg, cat) 0409 0410 else: 0411 tsegs.append(msgstr) 0412 0413 if modtext: # F3C hook 0414 return "".join(tsegs) 0415 elif spanrep: # V3C hook 0416 return errspans 0417 else: # S3C hook 0418 return len(errspans) 0419 0420 def resolver (msgstr, msg, cat): 0421 0422 return resolver_helper(msgstr, msg, cat, modtext, spanrep) 0423 0424 return resolver 0425 0426 0427 def _collect_ui_catpaths (uicpaths, uicpathenv): 0428 0429 all_uicpaths = [] 0430 if uicpathenv is not None: 0431 all_uicpaths.extend(collect_catalogs_by_env(uicpathenv)) 0432 if uicpaths is not None: 0433 all_uicpaths.extend(collect_catalogs(uicpaths)) 0434 0435 # Convert into dictionary by catalog name. 0436 # If there are several catalogs with the same name among paths, 0437 # store them under that name in undefined order. 0438 uicpath_dict = {} 0439 for uicpath in all_uicpaths: 0440 catname = os.path.basename(uicpath) 0441 p = catname.rfind(".") 0442 if p >= 0: 0443 catname = catname[:p] 0444 if catname not in uicpath_dict: 0445 uicpath_dict[catname] = [] 0446 uicpath_dict[catname].append(uicpath) 0447 0448 return uicpath_dict 0449 0450 0451 # Cache for normalized UI catalogs. 0452 # Mapping by normalization options and catalog name. 0453 _norm_cats_cache = {} 0454 0455 def _load_norm_ui_cats (cat, uicpaths, xmlescape): 0456 0457 # Construct list of catalogs, by catalog name, from which this 0458 # catalog may draw UI strings. 0459 # The list should be ordered by decreasing priority, 0460 # used to resolve references in face of duplicates over catalogs. 0461 catnames = [] 0462 0463 # - catalogs listed in some header fields 0464 # NOTE: Mention in module docustring when adding/removing fields. 0465 afnames = ( 0466 "X-Associated-UI-Catalogs-H", 0467 "X-Associated-UI-Catalogs", 0468 "X-Associated-UI-Catalogs-L", 0469 ) 0470 for afname in afnames: 0471 for field in cat.header.select_fields(afname): 0472 # Field value is a list of catalog names. 0473 lststr = field[1] 0474 # Remove any summit-merging comments. 0475 p = lststr.find("~~") 0476 if p >= 0: 0477 lststr = lststr[:p] 0478 catnames.extend(lststr.split()) 0479 0480 # - the catalog itself, if among UI catalogs paths and not explicitly given 0481 if cat.name in uicpaths and not cat.name in catnames: 0482 catnames.insert(0, cat.name) # highest priority 0483 0484 # Make catalog names unique, preserving order. 0485 uniq_catnames = [] 0486 for catname in catnames: 0487 if catname not in uniq_catnames: 0488 uniq_catnames.append(catname) 0489 0490 # Open and normalize UI catalogs. 0491 # Cache catalogs for performance. 0492 uicats = [] 0493 chkeys = set() 0494 for catname in uniq_catnames: 0495 catpaths = uicpaths.get(catname) 0496 if not catpaths: 0497 warning(_("@info", 0498 "UI catalog '%(catname1)s' associated to '%(catname2)s' " 0499 "is not among known catalog paths.", 0500 catname1=catname, catname2=cat.name)) 0501 continue 0502 for catpath in catpaths: 0503 chkey = (xmlescape, catpath) 0504 chkeys.add(chkey) 0505 uicat = _norm_cats_cache.get(chkey) 0506 if uicat is None: 0507 uicat_raw = Catalog(catpath, monitored=False) 0508 uicat = _norm_ui_cat(uicat_raw, xmlescape) 0509 _norm_cats_cache[chkey] = uicat 0510 uicats.append(uicat) 0511 0512 # Remove previous catalogs not reused by this call. 0513 # TODO: Better strategy for removing from cache. 0514 for chkey in set(_norm_cats_cache.keys()).difference(chkeys): 0515 #print("Removing normalized UI catalog '%s'..." % list(chkey)) 0516 del _norm_cats_cache[chkey] 0517 0518 return uicats 0519 0520 0521 def _norm_ui_cat (cat, xmlescape): 0522 0523 norm_cat = Catalog("", create=True, monitored=False) 0524 norm_cat.filename = cat.filename + "~norm" 0525 0526 # Normalize messages and collect them by normalized keys. 0527 msgs_by_normkey = {} 0528 for msg in cat: 0529 if msg.obsolete: 0530 continue 0531 orig_msgkey = (msg.msgctxt, msg.msgid) 0532 remove_markup_msg(msg, cat) # before accelerator removal 0533 remove_accel_msg(msg, cat) # after markup removal 0534 normkey = (msg.msgctxt, msg.msgid) 0535 if normkey not in msgs_by_normkey: 0536 msgs_by_normkey[normkey] = [] 0537 msgs_by_normkey[normkey].append((msg, orig_msgkey)) 0538 0539 for msgs in list(msgs_by_normkey.values()): 0540 # If there are several messages with same normalized key and 0541 # different translations, add extra disambiguations to context. 0542 # These disambiguations must not depend on message ordering. 0543 if len(msgs) > 1: 0544 # Check equality of translations. 0545 msgstr0 = "" 0546 for msg, d1 in msgs: 0547 if msg.translated: 0548 if not msgstr0: 0549 msgstr0 = msg.msgstr[0] 0550 elif msgstr0 != msg.msgstr[0]: 0551 msgstr0 = None 0552 break 0553 if msgstr0 is None: # disambiguation necessary 0554 tails = set() 0555 for msg, (octxt, omsgid) in msgs: 0556 if msg.msgctxt is None: 0557 msg.msgctxt = "" 0558 tail = hashlib.md5(omsgid).hexdigest() 0559 n = 4 # minimum size of the disambiguation tail 0560 while tail[:n] in tails: 0561 n += 1 0562 if n > len(tail): 0563 raise PologyError( 0564 _("@info", 0565 "Hash function has returned same result " 0566 "for two different strings.")) 0567 tails.add(tail[:n]) 0568 msg.msgctxt += "~" + tail[:n] 0569 else: # all messages have same translation, use first 0570 msgs = msgs[:1] 0571 0572 # Escape text fields. 0573 if xmlescape: 0574 for msg, d1 in msgs: 0575 if msg.msgctxt: 0576 msg.msgctxt = _escape_to_xml(msg.msgctxt) 0577 msg.msgid = _escape_to_xml(msg.msgid) 0578 if msg.msgid_plural: 0579 msg.msgid_plural = _escape_to_xml(msg.msgid_plural) 0580 for i in range(len(msg.msgstr)): 0581 msg.msgstr[i] = _escape_to_xml(msg.msgstr[i]) 0582 0583 # Add normalized messages to normalized catalog. 0584 for msg, d1 in msgs: 0585 if msg.msgctxt or msg.msgid: 0586 norm_cat.add_last(msg) 0587 0588 return norm_cat 0589 0590 0591 def _escape_to_xml (text): 0592 0593 text = text.replace("&", "&") # must be first 0594 text = text.replace("<", "<") 0595 text = text.replace(">", ">") 0596 0597 return text 0598 0599 0600 _ts_fence = "|/|" 0601 0602 def _resolve_single_uiref (uitext, uicats, hookcl_f3c, hookcl_v3c, fdiralt): 0603 0604 errmsgs = [] 0605 0606 # Determine context separator in the reference. 0607 # If the arcane one is not present, use normal. 0608 ctxsep = _uiref_ctxsep2 0609 if ctxsep not in uitext: 0610 ctxsep = _uiref_ctxsep 0611 0612 # Return verbatim if requested (starts with two context separators). 0613 if uitext.startswith(ctxsep * 2): 0614 return uitext[len(ctxsep) * 2:], errmsgs 0615 0616 # Split into msgctxt and msgid. 0617 has_msgctxt = False 0618 msgctxt = None 0619 msgid = uitext 0620 if ctxsep in uitext: 0621 lst = uitext.split(ctxsep) 0622 if len(lst) > 2: 0623 rep = "..." + ctxsep + ctxsep.join(lst[2:]) 0624 errmsgs.append(_("@info \"tail\" is the trailing remainder of " 0625 "a UI reference string after parsing", 0626 "Superfluous tail '%(str)s' in " 0627 "UI reference '%(ref)s'.", 0628 str=rep, ref=uitext)) 0629 msgctxt, msgid = lst[:2] 0630 if not msgctxt: 0631 # FIXME: What about context with existing, but empty context? 0632 msgctxt = None 0633 has_msgctxt = True 0634 # msgctxt may be None while has_msgctxt is True. 0635 # This distinction is important when deciding between two msgids, 0636 # one having no context and one having a context. 0637 0638 # Split any arguments from msgid. 0639 args = [] 0640 argsep = _uiref_argsep2 0641 if _uiref_argsep2 not in msgid: 0642 argsep = _uiref_argsep 0643 if argsep in msgid: 0644 lst = msgid.split(argsep) 0645 msgid = lst[0] 0646 args_raw = lst[1:] 0647 for arg_raw in args_raw: 0648 alst = arg_raw.split(_uiref_argplsep) 0649 if len(alst) == 2: 0650 single = False 0651 if alst[0].startswith(_uiref_argsrepl): 0652 alst[0] = alst[0][1:] 0653 single = True 0654 for fdalt, fdnorm in list(fdiralt.items()): 0655 if alst[0].startswith(fdalt): 0656 plhold = alst[0].replace(fdalt, fdnorm, 1) 0657 if single: 0658 msgid = msgid.replace(alst[0], plhold, 1) 0659 else: 0660 msgid = msgid.replace(alst[0], plhold) 0661 alst[0] = plhold 0662 # Argument itself may contain UI references. 0663 local_errspans = hookcl_v3c(alst[1]) 0664 if local_errspans: 0665 errmsgs.extend([x[-1] for x in local_errspans]) 0666 else: 0667 alst[1] = hookcl_f3c(alst[1]) 0668 alst.append(single) 0669 args.append(alst) 0670 else: 0671 errmsgs.append(_("@info", 0672 "Invalid argument specification '%(arg)s' " 0673 "in UI reference '%(ref)s'.", 0674 arg=arg_raw, ref=uitext)) 0675 0676 # Try to find unambiguous match to msgctxt/msgid. 0677 rmsg = None 0678 rcat = None 0679 for uicat in uicats: 0680 if has_msgctxt: 0681 msgs = uicat.select_by_key(msgctxt, msgid) 0682 if not msgs: 0683 # Also try as if the context were regular expression. 0684 msgs = uicat.select_by_key_match(msgctxt, msgid, 0685 exctxt=False, exid=True, 0686 case=False) 0687 else: 0688 msgs = uicat.select_by_msgid(msgid) 0689 if len(msgs) == 1: 0690 rmsg = msgs[0] 0691 rcat = uicat 0692 break 0693 0694 # If unambiguous match found. 0695 if rmsg is not None: 0696 # If the message is translated, use its translation, 0697 # otherwise use original and report. 0698 if rmsg.translated: 0699 ruitext = rmsg.msgstr[0] 0700 else: 0701 ruitext = msgid 0702 errmsgs.append(_("@info", 0703 "UI reference '%(ref)s' not translated " 0704 "at %(file)s:%(line)d(#%(entry)d).", 0705 ref=uitext, file=rcat.filename, 0706 line=rmsg.refline, entry=rmsg.refentry)) 0707 0708 # If no unambiguous match found, collect all the approximate ones, 0709 # report and use the original UI text. 0710 else: 0711 ruitext = msgid 0712 approx = [] 0713 for uicat in uicats: 0714 nmsgs = uicat.select_by_msgid_fuzzy(msgid) 0715 for nmsg in nmsgs: 0716 if nmsg.translated: 0717 approx1 = _("@item condensed display of text and " 0718 "its translation; they should stand out " 0719 "well, hence the {{...}} wrapping", 0720 "{{%(text)s}}={{%(translation)s}} " 0721 "at %(file)s:%(line)d(#%(entry)d)", 0722 text=_to_uiref(nmsg), 0723 translation=nmsg.msgstr[0], 0724 file=uicat.filename, 0725 line=nmsg.refline, entry=nmsg.refentry) 0726 else: 0727 approx1 = _("@item condensed display of text without " 0728 "translation; it should stand out " 0729 "well, hence the {{...}} wrapping", 0730 "{{%(text)s}}=(untranslated) " 0731 "at %(file)s:%(line)d(#%(entry)d)", 0732 text=_to_uiref(nmsg), 0733 file=uicat.filename, 0734 line=nmsg.refline, entry=nmsg.refentry) 0735 approx.append(approx1) 0736 if approx: 0737 errmsgs.append(_("@info", 0738 "UI reference '%(ref)s' cannot be resolved; " 0739 "close matches:\n" 0740 "%(matches)s", 0741 ref=uitext, matches=cjoin(approx, "\n"))) 0742 else: 0743 errmsgs.append(_("@info", 0744 "UI reference '%(ref)s' cannot be resolved.", 0745 ref=uitext)) 0746 0747 # Strip scripted part if any. 0748 p = ruitext.find(_ts_fence) 0749 if p >= 0: 0750 ruitext = ruitext[:p] 0751 0752 # Replace any provided arguments. 0753 for plhold, value, single in args: 0754 if plhold in ruitext: 0755 if single: 0756 ruitext = ruitext.replace(plhold, value, 1) 0757 else: 0758 ruitext = ruitext.replace(plhold, value) 0759 else: 0760 errmsgs.append(_("@info", 0761 "Placeholder '%(plhold)s' not found in resolved " 0762 "UI reference text '%(text)s' " 0763 "to reference '%(ref)s'.", 0764 plhold=plhold, text=ruitext, ref=uitext)) 0765 0766 return ruitext, errmsgs 0767 0768 0769 # Special tokens used in UI references. 0770 _uiref_ctxsep = "|" # normal context separator 0771 _uiref_ctxsep2 = "¦" # arcane context separator (fallback) 0772 _uiref_argsep = "^" # normal argument separator 0773 _uiref_argsep2 = "ª" # arcane argument separator (fallback) 0774 _uiref_argplsep = ":" # placeholder separator in arguments 0775 _uiref_argsrepl = "!" # placeholder start to indicate single replacement 0776 0777 # Present message from a normalized catalog in reference format, 0778 # suitable for inserting as a reference. 0779 def _to_uiref (nmsg): 0780 0781 uiref = nmsg.msgid 0782 0783 if nmsg.msgctxt: 0784 # Use arcane separator if the msgid or msgctxt contain normal one. 0785 ctxsep = _uiref_ctxsep 0786 if ctxsep in uiref or ctxsep in nmsg.msgctxt: 0787 ctxsep = _uiref_ctxsep2 0788 uiref = nmsg.msgctxt + ctxsep + uiref 0789 elif _uiref_ctxsep in nmsg.msgid: 0790 # If the msgid contains normal separator, add one arcane separator 0791 # in front of it to indicate empty context. 0792 uiref = _uiref_ctxsep * 2 + uiref 0793 0794 # TODO: Analyze format directives to add dummy arguments? 0795 0796 return uiref 0797 0798 0799 # Split UI reference path as [(ptext, ref1), (sep, ref2), (sep, ref3), ...] 0800 def _split_uirefpath (ptext, uirefpath, uipathseps): 0801 0802 p = -1 0803 for sep in uipathseps: 0804 p = uirefpath.find(sep) 0805 if p >= 0: 0806 break 0807 if p < 0: 0808 return [(ptext, uirefpath)] 0809 else: 0810 rsplit = uirefpath.split(sep) 0811 return list(zip([ptext] + [sep] * (len(rsplit) - 1), rsplit)) 0812