File indexing completed on 2024-04-21 16:29:18

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("&", "&amp;") # must be first
0594     text = text.replace("<", "&lt;")
0595     text = text.replace(">", "&gt;")
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