# templatefuncs.py - common template functions # # Copyright 2005, 2006 Matt Mackall # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. from __future__ import absolute_import import re from .i18n import _ from .node import ( bin, wdirid, ) from . import ( color, diffutil, encoding, error, minirst, obsutil, pycompat, registrar, revset as revsetmod, revsetlang, scmutil, templatefilters, templatekw, templateutil, util, ) from .utils import ( dateutil, stringutil, ) evalrawexp = templateutil.evalrawexp evalwrapped = templateutil.evalwrapped evalfuncarg = templateutil.evalfuncarg evalboolean = templateutil.evalboolean evaldate = templateutil.evaldate evalinteger = templateutil.evalinteger evalstring = templateutil.evalstring evalstringliteral = templateutil.evalstringliteral # dict of template built-in functions funcs = {} templatefunc = registrar.templatefunc(funcs) @templatefunc(b'date(date[, fmt])') def date(context, mapping, args): """Format a date. See :hg:`help dates` for formatting strings. The default is a Unix date format, including the timezone: "Mon Sep 04 15:13:13 2006 0700".""" if not (1 <= len(args) <= 2): # i18n: "date" is a keyword raise error.ParseError(_(b"date expects one or two arguments")) date = evaldate( context, mapping, args[0], # i18n: "date" is a keyword _(b"date expects a date information"), ) fmt = None if len(args) == 2: fmt = evalstring(context, mapping, args[1]) if fmt is None: return dateutil.datestr(date) else: return dateutil.datestr(date, fmt) @templatefunc(b'dict([[key=]value...])', argspec=b'*args **kwargs') def dict_(context, mapping, args): """Construct a dict from key-value pairs. A key may be omitted if a value expression can provide an unambiguous name.""" data = util.sortdict() for v in args[b'args']: k = templateutil.findsymbolicname(v) if not k: raise error.ParseError(_(b'dict key cannot be inferred')) if k in data or k in args[b'kwargs']: raise error.ParseError(_(b"duplicated dict key '%s' inferred") % k) data[k] = evalfuncarg(context, mapping, v) data.update( (k, evalfuncarg(context, mapping, v)) for k, v in pycompat.iteritems(args[b'kwargs']) ) return templateutil.hybriddict(data) @templatefunc( b'diff([includepattern [, excludepattern]])', requires={b'ctx', b'ui'} ) def diff(context, mapping, args): """Show a diff, optionally specifying files to include or exclude.""" if len(args) > 2: # i18n: "diff" is a keyword raise error.ParseError(_(b"diff expects zero, one, or two arguments")) def getpatterns(i): if i < len(args): s = evalstring(context, mapping, args[i]).strip() if s: return [s] return [] ctx = context.resource(mapping, b'ctx') ui = context.resource(mapping, b'ui') diffopts = diffutil.diffallopts(ui) chunks = ctx.diff( match=ctx.match([], getpatterns(0), getpatterns(1)), opts=diffopts ) return b''.join(chunks) @templatefunc( b'extdata(source)', argspec=b'source', requires={b'ctx', b'cache'} ) def extdata(context, mapping, args): """Show a text read from the specified extdata source. (EXPERIMENTAL)""" if b'source' not in args: # i18n: "extdata" is a keyword raise error.ParseError(_(b'extdata expects one argument')) source = evalstring(context, mapping, args[b'source']) if not source: sym = templateutil.findsymbolicname(args[b'source']) if sym: raise error.ParseError( _(b'empty data source specified'), hint=_(b"did you mean extdata('%s')?") % sym, ) else: raise error.ParseError(_(b'empty data source specified')) cache = context.resource(mapping, b'cache').setdefault(b'extdata', {}) ctx = context.resource(mapping, b'ctx') if source in cache: data = cache[source] else: data = cache[source] = scmutil.extdatasource(ctx.repo(), source) return data.get(ctx.rev(), b'') @templatefunc(b'files(pattern)', requires={b'ctx'}) def files(context, mapping, args): """All files of the current changeset matching the pattern. See :hg:`help patterns`.""" if not len(args) == 1: # i18n: "files" is a keyword raise error.ParseError(_(b"files expects one argument")) raw = evalstring(context, mapping, args[0]) ctx = context.resource(mapping, b'ctx') m = ctx.match([raw]) files = list(ctx.matches(m)) return templateutil.compatfileslist(context, mapping, b"file", files) @templatefunc(b'fill(text[, width[, initialident[, hangindent]]])') def fill(context, mapping, args): """Fill many paragraphs with optional indentation. See the "fill" filter.""" if not (1 <= len(args) <= 4): # i18n: "fill" is a keyword raise error.ParseError(_(b"fill expects one to four arguments")) text = evalstring(context, mapping, args[0]) width = 76 initindent = b'' hangindent = b'' if 2 <= len(args) <= 4: width = evalinteger( context, mapping, args[1], # i18n: "fill" is a keyword _(b"fill expects an integer width"), ) try: initindent = evalstring(context, mapping, args[2]) hangindent = evalstring(context, mapping, args[3]) except IndexError: pass return templatefilters.fill(text, width, initindent, hangindent) @templatefunc(b'filter(iterable[, expr])') def filter_(context, mapping, args): """Remove empty elements from a list or a dict. If expr specified, it's applied to each element to test emptiness.""" if not (1 <= len(args) <= 2): # i18n: "filter" is a keyword raise error.ParseError(_(b"filter expects one or two arguments")) iterable = evalwrapped(context, mapping, args[0]) if len(args) == 1: def select(w): return w.tobool(context, mapping) else: def select(w): if not isinstance(w, templateutil.mappable): raise error.ParseError(_(b"not filterable by expression")) lm = context.overlaymap(mapping, w.tomap(context)) return evalboolean(context, lm, args[1]) return iterable.filter(context, mapping, select) @templatefunc(b'formatnode(node)', requires={b'ui'}) def formatnode(context, mapping, args): """Obtain the preferred form of a changeset hash. (DEPRECATED)""" if len(args) != 1: # i18n: "formatnode" is a keyword raise error.ParseError(_(b"formatnode expects one argument")) ui = context.resource(mapping, b'ui') node = evalstring(context, mapping, args[0]) if ui.debugflag: return node return templatefilters.short(node) @templatefunc(b'mailmap(author)', requires={b'repo', b'cache'}) def mailmap(context, mapping, args): """Return the author, updated according to the value set in the .mailmap file""" if len(args) != 1: raise error.ParseError(_(b"mailmap expects one argument")) author = evalstring(context, mapping, args[0]) cache = context.resource(mapping, b'cache') repo = context.resource(mapping, b'repo') if b'mailmap' not in cache: data = repo.wvfs.tryread(b'.mailmap') cache[b'mailmap'] = stringutil.parsemailmap(data) return stringutil.mapname(cache[b'mailmap'], author) @templatefunc( b'pad(text, width[, fillchar=\' \'[, left=False[, truncate=False]]])', argspec=b'text width fillchar left truncate', ) def pad(context, mapping, args): """Pad text with a fill character.""" if b'text' not in args or b'width' not in args: # i18n: "pad" is a keyword raise error.ParseError(_(b"pad() expects two to four arguments")) width = evalinteger( context, mapping, args[b'width'], # i18n: "pad" is a keyword _(b"pad() expects an integer width"), ) text = evalstring(context, mapping, args[b'text']) truncate = False left = False fillchar = b' ' if b'fillchar' in args: fillchar = evalstring(context, mapping, args[b'fillchar']) if len(color.stripeffects(fillchar)) != 1: # i18n: "pad" is a keyword raise error.ParseError(_(b"pad() expects a single fill character")) if b'left' in args: left = evalboolean(context, mapping, args[b'left']) if b'truncate' in args: truncate = evalboolean(context, mapping, args[b'truncate']) fillwidth = width - encoding.colwidth(color.stripeffects(text)) if fillwidth < 0 and truncate: return encoding.trim(color.stripeffects(text), width, leftside=left) if fillwidth <= 0: return text if left: return fillchar * fillwidth + text else: return text + fillchar * fillwidth @templatefunc(b'indent(text, indentchars[, firstline])') def indent(context, mapping, args): """Indents all non-empty lines with the characters given in the indentchars string. An optional third parameter will override the indent for the first line only if present.""" if not (2 <= len(args) <= 3): # i18n: "indent" is a keyword raise error.ParseError(_(b"indent() expects two or three arguments")) text = evalstring(context, mapping, args[0]) indent = evalstring(context, mapping, args[1]) firstline = indent if len(args) == 3: firstline = evalstring(context, mapping, args[2]) return templatefilters.indent(text, indent, firstline=firstline) @templatefunc(b'get(dict, key)') def get(context, mapping, args): """Get an attribute/key from an object. Some keywords are complex types. This function allows you to obtain the value of an attribute on these types.""" if len(args) != 2: # i18n: "get" is a keyword raise error.ParseError(_(b"get() expects two arguments")) dictarg = evalwrapped(context, mapping, args[0]) key = evalrawexp(context, mapping, args[1]) try: return dictarg.getmember(context, mapping, key) except error.ParseError as err: # i18n: "get" is a keyword hint = _(b"get() expects a dict as first argument") raise error.ParseError(bytes(err), hint=hint) @templatefunc(b'config(section, name[, default])', requires={b'ui'}) def config(context, mapping, args): """Returns the requested hgrc config option as a string.""" fn = context.resource(mapping, b'ui').config return _config(context, mapping, args, fn, evalstring) @templatefunc(b'configbool(section, name[, default])', requires={b'ui'}) def configbool(context, mapping, args): """Returns the requested hgrc config option as a boolean.""" fn = context.resource(mapping, b'ui').configbool return _config(context, mapping, args, fn, evalboolean) @templatefunc(b'configint(section, name[, default])', requires={b'ui'}) def configint(context, mapping, args): """Returns the requested hgrc config option as an integer.""" fn = context.resource(mapping, b'ui').configint return _config(context, mapping, args, fn, evalinteger) def _config(context, mapping, args, configfn, defaultfn): if not (2 <= len(args) <= 3): raise error.ParseError(_(b"config expects two or three arguments")) # The config option can come from any section, though we specifically # reserve the [templateconfig] section for dynamically defining options # for this function without also requiring an extension. section = evalstringliteral(context, mapping, args[0]) name = evalstringliteral(context, mapping, args[1]) if len(args) == 3: default = defaultfn(context, mapping, args[2]) return configfn(section, name, default) else: return configfn(section, name) @templatefunc(b'if(expr, then[, else])') def if_(context, mapping, args): """Conditionally execute based on the result of an expression.""" if not (2 <= len(args) <= 3): # i18n: "if" is a keyword raise error.ParseError(_(b"if expects two or three arguments")) test = evalboolean(context, mapping, args[0]) if test: return evalrawexp(context, mapping, args[1]) elif len(args) == 3: return evalrawexp(context, mapping, args[2]) @templatefunc(b'ifcontains(needle, haystack, then[, else])') def ifcontains(context, mapping, args): """Conditionally execute based on whether the item "needle" is in "haystack".""" if not (3 <= len(args) <= 4): # i18n: "ifcontains" is a keyword raise error.ParseError(_(b"ifcontains expects three or four arguments")) haystack = evalwrapped(context, mapping, args[1]) try: needle = evalrawexp(context, mapping, args[0]) found = haystack.contains(context, mapping, needle) except error.ParseError: found = False if found: return evalrawexp(context, mapping, args[2]) elif len(args) == 4: return evalrawexp(context, mapping, args[3]) @templatefunc(b'ifeq(expr1, expr2, then[, else])') def ifeq(context, mapping, args): """Conditionally execute based on whether 2 items are equivalent.""" if not (3 <= len(args) <= 4): # i18n: "ifeq" is a keyword raise error.ParseError(_(b"ifeq expects three or four arguments")) test = evalstring(context, mapping, args[0]) match = evalstring(context, mapping, args[1]) if test == match: return evalrawexp(context, mapping, args[2]) elif len(args) == 4: return evalrawexp(context, mapping, args[3]) @templatefunc(b'join(list, sep)') def join(context, mapping, args): """Join items in a list with a delimiter.""" if not (1 <= len(args) <= 2): # i18n: "join" is a keyword raise error.ParseError(_(b"join expects one or two arguments")) joinset = evalwrapped(context, mapping, args[0]) joiner = b" " if len(args) > 1: joiner = evalstring(context, mapping, args[1]) return joinset.join(context, mapping, joiner) @templatefunc(b'label(label, expr)', requires={b'ui'}) def label(context, mapping, args): """Apply a label to generated content. Content with a label applied can result in additional post-processing, such as automatic colorization.""" if len(args) != 2: # i18n: "label" is a keyword raise error.ParseError(_(b"label expects two arguments")) ui = context.resource(mapping, b'ui') thing = evalstring(context, mapping, args[1]) # preserve unknown symbol as literal so effects like 'red', 'bold', # etc. don't need to be quoted label = evalstringliteral(context, mapping, args[0]) return ui.label(thing, label) @templatefunc(b'latesttag([pattern])') def latesttag(context, mapping, args): """The global tags matching the given pattern on the most recent globally tagged ancestor of this changeset. If no such tags exist, the "{tag}" template resolves to the string "null". See :hg:`help revisions.patterns` for the pattern syntax. """ if len(args) > 1: # i18n: "latesttag" is a keyword raise error.ParseError(_(b"latesttag expects at most one argument")) pattern = None if len(args) == 1: pattern = evalstring(context, mapping, args[0]) return templatekw.showlatesttags(context, mapping, pattern) @templatefunc(b'localdate(date[, tz])') def localdate(context, mapping, args): """Converts a date to the specified timezone. The default is local date.""" if not (1 <= len(args) <= 2): # i18n: "localdate" is a keyword raise error.ParseError(_(b"localdate expects one or two arguments")) date = evaldate( context, mapping, args[0], # i18n: "localdate" is a keyword _(b"localdate expects a date information"), ) if len(args) >= 2: tzoffset = None tz = evalfuncarg(context, mapping, args[1]) if isinstance(tz, bytes): tzoffset, remainder = dateutil.parsetimezone(tz) if remainder: tzoffset = None if tzoffset is None: try: tzoffset = int(tz) except (TypeError, ValueError): # i18n: "localdate" is a keyword raise error.ParseError(_(b"localdate expects a timezone")) else: tzoffset = dateutil.makedate()[1] return templateutil.date((date[0], tzoffset)) @templatefunc(b'max(iterable)') def max_(context, mapping, args, **kwargs): """Return the max of an iterable""" if len(args) != 1: # i18n: "max" is a keyword raise error.ParseError(_(b"max expects one argument")) iterable = evalwrapped(context, mapping, args[0]) try: return iterable.getmax(context, mapping) except error.ParseError as err: # i18n: "max" is a keyword hint = _(b"max first argument should be an iterable") raise error.ParseError(bytes(err), hint=hint) @templatefunc(b'min(iterable)') def min_(context, mapping, args, **kwargs): """Return the min of an iterable""" if len(args) != 1: # i18n: "min" is a keyword raise error.ParseError(_(b"min expects one argument")) iterable = evalwrapped(context, mapping, args[0]) try: return iterable.getmin(context, mapping) except error.ParseError as err: # i18n: "min" is a keyword hint = _(b"min first argument should be an iterable") raise error.ParseError(bytes(err), hint=hint) @templatefunc(b'mod(a, b)') def mod(context, mapping, args): """Calculate a mod b such that a / b + a mod b == a""" if not len(args) == 2: # i18n: "mod" is a keyword raise error.ParseError(_(b"mod expects two arguments")) func = lambda a, b: a % b return templateutil.runarithmetic( context, mapping, (func, args[0], args[1]) ) @templatefunc(b'obsfateoperations(markers)') def obsfateoperations(context, mapping, args): """Compute obsfate related information based on markers (EXPERIMENTAL)""" if len(args) != 1: # i18n: "obsfateoperations" is a keyword raise error.ParseError(_(b"obsfateoperations expects one argument")) markers = evalfuncarg(context, mapping, args[0]) try: data = obsutil.markersoperations(markers) return templateutil.hybridlist(data, name=b'operation') except (TypeError, KeyError): # i18n: "obsfateoperations" is a keyword errmsg = _(b"obsfateoperations first argument should be an iterable") raise error.ParseError(errmsg) @templatefunc(b'obsfatedate(markers)') def obsfatedate(context, mapping, args): """Compute obsfate related information based on markers (EXPERIMENTAL)""" if len(args) != 1: # i18n: "obsfatedate" is a keyword raise error.ParseError(_(b"obsfatedate expects one argument")) markers = evalfuncarg(context, mapping, args[0]) try: # TODO: maybe this has to be a wrapped list of date wrappers? data = obsutil.markersdates(markers) return templateutil.hybridlist(data, name=b'date', fmt=b'%d %d') except (TypeError, KeyError): # i18n: "obsfatedate" is a keyword errmsg = _(b"obsfatedate first argument should be an iterable") raise error.ParseError(errmsg) @templatefunc(b'obsfateusers(markers)') def obsfateusers(context, mapping, args): """Compute obsfate related information based on markers (EXPERIMENTAL)""" if len(args) != 1: # i18n: "obsfateusers" is a keyword raise error.ParseError(_(b"obsfateusers expects one argument")) markers = evalfuncarg(context, mapping, args[0]) try: data = obsutil.markersusers(markers) return templateutil.hybridlist(data, name=b'user') except (TypeError, KeyError, ValueError): # i18n: "obsfateusers" is a keyword msg = _( b"obsfateusers first argument should be an iterable of " b"obsmakers" ) raise error.ParseError(msg) @templatefunc(b'obsfateverb(successors, markers)') def obsfateverb(context, mapping, args): """Compute obsfate related information based on successors (EXPERIMENTAL)""" if len(args) != 2: # i18n: "obsfateverb" is a keyword raise error.ParseError(_(b"obsfateverb expects two arguments")) successors = evalfuncarg(context, mapping, args[0]) markers = evalfuncarg(context, mapping, args[1]) try: return obsutil.obsfateverb(successors, markers) except TypeError: # i18n: "obsfateverb" is a keyword errmsg = _(b"obsfateverb first argument should be countable") raise error.ParseError(errmsg) @templatefunc(b'relpath(path)', requires={b'repo'}) def relpath(context, mapping, args): """Convert a repository-absolute path into a filesystem path relative to the current working directory.""" if len(args) != 1: # i18n: "relpath" is a keyword raise error.ParseError(_(b"relpath expects one argument")) repo = context.resource(mapping, b'repo') path = evalstring(context, mapping, args[0]) return repo.pathto(path) @templatefunc(b'revset(query[, formatargs...])', requires={b'repo', b'cache'}) def revset(context, mapping, args): """Execute a revision set query. See :hg:`help revset`.""" if not len(args) > 0: # i18n: "revset" is a keyword raise error.ParseError(_(b"revset expects one or more arguments")) raw = evalstring(context, mapping, args[0]) repo = context.resource(mapping, b'repo') def query(expr): m = revsetmod.match(repo.ui, expr, lookup=revsetmod.lookupfn(repo)) return m(repo) if len(args) > 1: formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]] revs = query(revsetlang.formatspec(raw, *formatargs)) else: cache = context.resource(mapping, b'cache') revsetcache = cache.setdefault(b"revsetcache", {}) if raw in revsetcache: revs = revsetcache[raw] else: revs = query(raw) revsetcache[raw] = revs return templatekw.showrevslist(context, mapping, b"revision", revs) @templatefunc(b'rstdoc(text, style)') def rstdoc(context, mapping, args): """Format reStructuredText.""" if len(args) != 2: # i18n: "rstdoc" is a keyword raise error.ParseError(_(b"rstdoc expects two arguments")) text = evalstring(context, mapping, args[0]) style = evalstring(context, mapping, args[1]) return minirst.format(text, style=style, keep=[b'verbose']) @templatefunc(b'search(pattern, text)') def search(context, mapping, args): """Look for the first text matching the regular expression pattern. Groups are accessible as ``{1}``, ``{2}``, ... in %-mapped template.""" if len(args) != 2: # i18n: "search" is a keyword raise error.ParseError(_(b'search expects two arguments')) pat = evalstring(context, mapping, args[0]) src = evalstring(context, mapping, args[1]) try: patre = re.compile(pat) except re.error: # i18n: "search" is a keyword raise error.ParseError(_(b'search got an invalid pattern: %s') % pat) # named groups shouldn't shadow *reserved* resource keywords badgroups = context.knownresourcekeys() & set( pycompat.byteskwargs(patre.groupindex) ) if badgroups: raise error.ParseError( # i18n: "search" is a keyword _(b'invalid group %(group)s in search pattern: %(pat)s') % { b'group': b', '.join(b"'%s'" % g for g in sorted(badgroups)), b'pat': pat, } ) match = patre.search(src) if not match: return templateutil.mappingnone() lm = {b'0': match.group(0)} lm.update((b'%d' % i, v) for i, v in enumerate(match.groups(), 1)) lm.update(pycompat.byteskwargs(match.groupdict())) return templateutil.mappingdict(lm, tmpl=b'{0}') @templatefunc(b'separate(sep, args...)', argspec=b'sep *args') def separate(context, mapping, args): """Add a separator between non-empty arguments.""" if b'sep' not in args: # i18n: "separate" is a keyword raise error.ParseError(_(b"separate expects at least one argument")) sep = evalstring(context, mapping, args[b'sep']) first = True for arg in args[b'args']: argstr = evalstring(context, mapping, arg) if not argstr: continue if first: first = False else: yield sep yield argstr @templatefunc(b'shortest(node, minlength=4)', requires={b'repo', b'cache'}) def shortest(context, mapping, args): """Obtain the shortest representation of a node.""" if not (1 <= len(args) <= 2): # i18n: "shortest" is a keyword raise error.ParseError(_(b"shortest() expects one or two arguments")) hexnode = evalstring(context, mapping, args[0]) minlength = 4 if len(args) > 1: minlength = evalinteger( context, mapping, args[1], # i18n: "shortest" is a keyword _(b"shortest() expects an integer minlength"), ) repo = context.resource(mapping, b'repo') if len(hexnode) > 40: return hexnode elif len(hexnode) == 40: try: node = bin(hexnode) except TypeError: return hexnode else: try: node = scmutil.resolvehexnodeidprefix(repo, hexnode) except error.WdirUnsupported: node = wdirid except error.LookupError: return hexnode if not node: return hexnode cache = context.resource(mapping, b'cache') try: return scmutil.shortesthexnodeidprefix(repo, node, minlength, cache) except error.RepoLookupError: return hexnode @templatefunc(b'strip(text[, chars])') def strip(context, mapping, args): """Strip characters from a string. By default, strips all leading and trailing whitespace.""" if not (1 <= len(args) <= 2): # i18n: "strip" is a keyword raise error.ParseError(_(b"strip expects one or two arguments")) text = evalstring(context, mapping, args[0]) if len(args) == 2: chars = evalstring(context, mapping, args[1]) return text.strip(chars) return text.strip() @templatefunc(b'sub(pattern, replacement, expression)') def sub(context, mapping, args): """Perform text substitution using regular expressions.""" if len(args) != 3: # i18n: "sub" is a keyword raise error.ParseError(_(b"sub expects three arguments")) pat = evalstring(context, mapping, args[0]) rpl = evalstring(context, mapping, args[1]) src = evalstring(context, mapping, args[2]) try: patre = re.compile(pat) except re.error: # i18n: "sub" is a keyword raise error.ParseError(_(b"sub got an invalid pattern: %s") % pat) try: yield patre.sub(rpl, src) except re.error: # i18n: "sub" is a keyword raise error.ParseError(_(b"sub got an invalid replacement: %s") % rpl) @templatefunc(b'startswith(pattern, text)') def startswith(context, mapping, args): """Returns the value from the "text" argument if it begins with the content from the "pattern" argument.""" if len(args) != 2: # i18n: "startswith" is a keyword raise error.ParseError(_(b"startswith expects two arguments")) patn = evalstring(context, mapping, args[0]) text = evalstring(context, mapping, args[1]) if text.startswith(patn): return text return b'' @templatefunc(b'word(number, text[, separator])') def word(context, mapping, args): """Return the nth word from a string.""" if not (2 <= len(args) <= 3): # i18n: "word" is a keyword raise error.ParseError( _(b"word expects two or three arguments, got %d") % len(args) ) num = evalinteger( context, mapping, args[0], # i18n: "word" is a keyword _(b"word expects an integer index"), ) text = evalstring(context, mapping, args[1]) if len(args) == 3: splitter = evalstring(context, mapping, args[2]) else: splitter = None tokens = text.split(splitter) if num >= len(tokens) or num < -len(tokens): return b'' else: return tokens[num] def loadfunction(ui, extname, registrarobj): """Load template function from specified registrarobj """ for name, func in pycompat.iteritems(registrarobj._table): funcs[name] = func # tell hggettext to extract docstrings from these functions: i18nfunctions = funcs.values()