diff --git a/100_core/src/gplx/Char_.java b/100_core/src/gplx/Char_.java index 54bf0b9ac..55b0792c8 100644 --- a/100_core/src/gplx/Char_.java +++ b/100_core/src/gplx/Char_.java @@ -63,6 +63,7 @@ public class Char_ { default: return or; } } + public static int To_int(char c) {return (int)c;} public static String To_str(char[] ary, int pos, int length) {return new String(ary, pos, length);} public static String To_str(int b) {return To_str((char)b);} public static String To_str(char c) {return String.valueOf(c);} diff --git a/100_core/src/gplx/Decimal_adp.java b/100_core/src/gplx/Decimal_adp.java index 752b8ce38..673356c3f 100644 --- a/100_core/src/gplx/Decimal_adp.java +++ b/100_core/src/gplx/Decimal_adp.java @@ -20,6 +20,7 @@ import java.math.RoundingMode; import java.text.DecimalFormat; public class Decimal_adp implements CompareAble { public int compareTo(Object obj) {Decimal_adp comp = (Decimal_adp)obj; return under.compareTo(comp.under);} + public Decimal_adp Floor() {return Decimal_adp_.int_(this.To_int());} protected Decimal_adp(BigDecimal v) {this.under = v;} private final BigDecimal under; protected Decimal_adp(int v) {this.under = new BigDecimal(v);} public Object Under() {return under;} diff --git a/100_core/src/gplx/Hash_adp.java b/100_core/src/gplx/Hash_adp.java index e1ca3d844..1808f4b1d 100644 --- a/100_core/src/gplx/Hash_adp.java +++ b/100_core/src/gplx/Hash_adp.java @@ -21,6 +21,7 @@ public interface Hash_adp extends gplx.core.lists.EnumerAble { Object Get_by_or_fail(Object key); void Add(Object key, Object val); Hash_adp Add_and_more(Object key, Object val); + Hash_adp Add_many_as_key_and_val(Object... ary); void Add_as_key_and_val(Object val); boolean Add_if_dupe_use_1st(Object key, Object val); void Add_if_dupe_use_nth(Object key, Object val); diff --git a/100_core/src/gplx/Hash_adp_.java b/100_core/src/gplx/Hash_adp_.java index 8f6476b9b..d3bec8f4e 100644 --- a/100_core/src/gplx/Hash_adp_.java +++ b/100_core/src/gplx/Hash_adp_.java @@ -27,6 +27,7 @@ class Hash_adp_noop implements Hash_adp { public Object Get_by_or_fail(Object key) {throw Err_.new_missing_key(Object_.Xto_str_strict_or_null_mark(key));} public void Add(Object key, Object val) {} public Hash_adp Add_and_more(Object key, Object val) {return this;} + public Hash_adp Add_many_as_key_and_val(Object... ary) {return this;} public void Add_as_key_and_val(Object val) {} public void Add_if_dupe_use_nth(Object key, Object val) {} public boolean Add_if_dupe_use_1st(Object key, Object val) {return false;} diff --git a/100_core/src/gplx/Math_.java b/100_core/src/gplx/Math_.java index 6fb8f5f8e..ac21f43df 100644 --- a/100_core/src/gplx/Math_.java +++ b/100_core/src/gplx/Math_.java @@ -16,6 +16,7 @@ Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt package gplx; public class Math_ { public static double Pow(double val, double exponent) {return java.lang.Math.pow(val, exponent);} + public static int Pow_int(int val, int exponent) {return (int)java.lang.Math.pow(val, exponent);} public static double Pi = java.lang.Math.PI; public static double E = java.lang.Math.E; public static int Ceil_as_int(double v) {return (int)Ceil(v);} diff --git a/100_core/src/gplx/core/lists/Hash_adp_base.java b/100_core/src/gplx/core/lists/Hash_adp_base.java index f819abc84..d9dc2c2d9 100644 --- a/100_core/src/gplx/core/lists/Hash_adp_base.java +++ b/100_core/src/gplx/core/lists/Hash_adp_base.java @@ -20,6 +20,11 @@ public abstract class Hash_adp_base implements Hash_adp { public Object Get_by_or_fail(Object key) {return Get_by_or_fail_base(key);} public void Add(Object key, Object val) {Add_base(key, val);} public Hash_adp Add_and_more(Object key, Object val) {Add_base(key, val); return this;} + public Hash_adp Add_many_as_key_and_val(Object... ary) { + for (Object itm : ary) + Add_base(itm, itm); + return this; + } public void Add_as_key_and_val(Object val) {Add_base(val, val);} public void Add_if_dupe_use_nth(Object key, Object val) { Object existing = Fetch_base(key); if (existing != null) Del(key); // overwrite if exists diff --git a/100_core/src/gplx/core/lists/Sorted_hash.java b/100_core/src/gplx/core/lists/Sorted_hash.java index 54a3f5ef0..2ee93190f 100644 --- a/100_core/src/gplx/core/lists/Sorted_hash.java +++ b/100_core/src/gplx/core/lists/Sorted_hash.java @@ -22,6 +22,11 @@ public class Sorted_hash implements Hash_adp { public Object Get_by_or_fail(Object key) {return Get_by_or_fail_base(key);} public void Add(Object key, Object val) {Add_base(key, val);} public Hash_adp Add_and_more(Object key, Object val) {Add_base(key, val); return this;} + public Hash_adp Add_many_as_key_and_val(Object... ary) { + for (Object itm : ary) + Add_base(itm, itm); + return this; + } public void Add_as_key_and_val(Object val) {Add_base(val, val);} public void Add_if_dupe_use_nth(Object key, Object val) { Object existing = Fetch_base(key); if (existing != null) Del(key); // overwrite if exists diff --git a/100_core/src/gplx/core/strings/String_bldr.java b/100_core/src/gplx/core/strings/String_bldr.java index 466f5080d..edc0f7066 100644 --- a/100_core/src/gplx/core/strings/String_bldr.java +++ b/100_core/src/gplx/core/strings/String_bldr.java @@ -38,6 +38,7 @@ public interface String_bldr { String_bldr Add(int i); String_bldr Add_obj(Object o); String_bldr Add_mid(char[] ary, int bgn, int count); + String_bldr Add_mid(String str, int bgn, int count); String_bldr Add_at(int idx, String s); String_bldr Del(int bgn, int len); } @@ -83,6 +84,7 @@ abstract class String_bldr_base implements String_bldr { public abstract String_bldr Add(char c); public abstract String_bldr Add(int i); public abstract String_bldr Add_mid(char[] ary, int bgn, int count); + public abstract String_bldr Add_mid(String str, int bgn, int count); public abstract String_bldr Add_obj(Object o); public abstract String_bldr Del(int bgn, int len); } @@ -96,6 +98,7 @@ class String_bldr_thread_single extends String_bldr_base { @Override public String_bldr Add(char c) {sb.append(c); return this;} @Override public String_bldr Add(int i) {sb.append(i); return this;} @Override public String_bldr Add_mid(char[] ary, int bgn, int count) {sb.append(ary, bgn, count); return this;} + @Override public String_bldr Add_mid(String str, int bgn, int count) {sb.append(str, bgn, count); return this;} @Override public String_bldr Add_obj(Object o) {sb.append(o); return this;} @Override public String_bldr Del(int bgn, int len) {sb.delete(bgn, len); return this;} } @@ -109,6 +112,7 @@ class String_bldr_thread_multiple extends String_bldr_base { @Override public String_bldr Add(char c) {sb.append(c); return this;} @Override public String_bldr Add(int i) {sb.append(i); return this;} @Override public String_bldr Add_mid(char[] ary, int bgn, int count) {sb.append(ary, bgn, count); return this;} + @Override public String_bldr Add_mid(String str, int bgn, int count) {sb.append(str, bgn, count); return this;} @Override public String_bldr Add_obj(Object o) {sb.append(o); return this;} @Override public String_bldr Del(int bgn, int len) {sb.delete(bgn, len); return this;} } diff --git a/100_core/src/gplx/langs/regxs/Regx_adp.java b/100_core/src/gplx/langs/regxs/Regx_adp.java index b0b240aef..fc23d38d5 100644 --- a/100_core/src/gplx/langs/regxs/Regx_adp.java +++ b/100_core/src/gplx/langs/regxs/Regx_adp.java @@ -56,9 +56,13 @@ public class Regx_adp { Regx_group[] ary = Regx_group.Ary_empty; int groups_len = match.groupCount(); if (success && groups_len > 0) { + // NOTE: by convention, there are n groups, but groups.count is n - 1 and groups[0] is entire match (not 1st group); see TEST: DATE:2019-12-28 + groups_len++; ary = new Regx_group[groups_len]; - for (int i = 0; i < groups_len; i++) - ary[i] = new Regx_group(true, match.start(i + 1), match.end(i + 1), match.group(i + 1)); + for (int i = 0; i < groups_len; i++) { + int match_start = match.start(i); + ary[i] = new Regx_group(match_start != -1, match_start, match.end(i), match.group(i)); + } } return new Regx_match(success, match_bgn, match_end, ary); } diff --git a/100_core/src/gplx/langs/regxs/Regx_adp__tst.java b/100_core/src/gplx/langs/regxs/Regx_adp__tst.java index a9ebfd8fb..6404831a0 100644 --- a/100_core/src/gplx/langs/regxs/Regx_adp__tst.java +++ b/100_core/src/gplx/langs/regxs/Regx_adp__tst.java @@ -49,7 +49,9 @@ public class Regx_adp__tst implements TfdsEqListItmStr { tst_Matches("b", "a b c b a b b", matches_(2, 6, 10, 12)); // BUGFIX: multiple entries did not work b/c of += instead of + } @Test public void Groups() { - tst_Groups("abc def ghi dz", "(d\\p{L}+)", "def", "dz"); + tst_Groups("abc def ghi dz", "(d\\p{L}+)", "def", "def", "dz", "dz"); + tst_Groups("abc def", "(de)(g?)", "de", "de", ""); // NOTE: (g?) doesn't capture anything, but still add a group for it; DATE:2019-12-28 + tst_Groups("-123.456", "^-?(([0-9]+)(?:\\.([0-9]+))?)", "-123.456", "123.456", "123", "456"); // NOTE: -123.456 captured even though it's not part of a group; DATE:2019-12-28 } Regx_match[] matches_(int... bgnAry) { int aryLen = Array_.Len(bgnAry); diff --git a/100_core/src/gplx/langs/xmls/XmlAtrList.java b/100_core/src/gplx/langs/xmls/XmlAtrList.java index ff7914c27..cd8bb2434 100644 --- a/100_core/src/gplx/langs/xmls/XmlAtrList.java +++ b/100_core/src/gplx/langs/xmls/XmlAtrList.java @@ -22,6 +22,11 @@ public class XmlAtrList { Node xatr = list.getNamedItem(key); return (xatr == null) ? or : xatr.getNodeValue(); } + public XmlAtr Get_by(String key) { + Node xatr = list.getNamedItem(key); + if (xatr == null) throw Err_.new_missing_key(key); + return new XmlAtr(xatr); + } public XmlAtr Fetch(String key) { Node xatr = list.getNamedItem(key); if (xatr == null) throw Err_.new_missing_key(key); return new XmlAtr(xatr); diff --git a/100_core/src/gplx/langs/xmls/XmlDoc_.java b/100_core/src/gplx/langs/xmls/XmlDoc_.java index a38f9d5c6..e84a4c3b6 100644 --- a/100_core/src/gplx/langs/xmls/XmlDoc_.java +++ b/100_core/src/gplx/langs/xmls/XmlDoc_.java @@ -29,23 +29,52 @@ import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.w3c.dom.Document; import org.w3c.dom.NodeList; +import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; import org.xml.sax.SAXException; public class XmlDoc_ { - public static XmlDoc parse(String raw) {return new XmlDoc(doc_(raw));} + public static XmlNdeList Select_tags(XmlNde cur, String tag) { + XmlNdeList_cls_list rv = new XmlNdeList_cls_list(4); // NOTE: pass in an initial amount; do not pass 0 + Select_tags(rv, cur, tag); + return rv; + } + private static void Select_tags(XmlNdeList_cls_list rv, XmlNde cur, String tag) { + if (String_.Eq(cur.Name(), tag)) { + rv.Add(cur); + } + XmlNdeList sub_ndes = cur.SubNdes(); + int sub_ndes_len = sub_ndes.Count(); + for (int i = 0; i < sub_ndes_len; i++) { + XmlNde sub_nde = sub_ndes.Get_at(i); + Select_tags(rv, sub_nde, tag); + } + } + public static XmlDoc parse(String raw) {return new XmlDoc(doc_(raw));} static Document doc_(String raw) { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder bldr = null; - try {bldr = factory.newDocumentBuilder();} - catch (ParserConfigurationException e) {throw Err_.new_exc(e, "xml", "failed to create newDocumentBuilder");} + try { + // NOTE: disable DTD validation else errors for "ldmlSupplemental.dtd" in plurals.xml; DATE:2020-01-01 + // REF:https://stackoverflow.com/questions/24744175/non-validating-documentbuilder-trying-to-read-dtd-file + // REF:https://stackoverflow.com/questions/6204827/xml-parsing-too-slow + factory.setNamespaceAware(false); + factory.setValidating(false); + factory.setFeature("http://xml.org/sax/features/namespaces", false); + factory.setFeature("http://xml.org/sax/features/validation", false); + factory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); + factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + bldr = factory.newDocumentBuilder(); + } + catch (ParserConfigurationException e) { + throw Err_.new_exc(e, "xml", "failed to create newDocumentBuilder"); + } StringReader reader = new StringReader(raw); InputSource source = new InputSource(reader); Document doc = null; try {doc = bldr.parse(source);} catch (SAXException e) {throw Err_.new_exc(e, "xml", "failed to parse xml", "raw", raw);} catch (IOException e) {throw Err_.new_exc(e, "xml", "failed to parse xml", "raw", raw);} - return doc; + return doc; } public static final String Err_XmlException = "gplx.xmls.XmlException"; -} -//#} \ No newline at end of file + } diff --git a/400_xowa/src/gplx/xowa/Xoae_app.java b/400_xowa/src/gplx/xowa/Xoae_app.java index ab1168fd3..640b734dc 100644 --- a/400_xowa/src/gplx/xowa/Xoae_app.java +++ b/400_xowa/src/gplx/xowa/Xoae_app.java @@ -37,6 +37,7 @@ public class Xoae_app implements Xoa_app, Gfo_invk { this.mode = mode; Io_url.Http_file_str_encoder = Gfo_url_encoder_.New__fsys_lnx().Make(); fsys_mgr = new Xoa_fsys_mgr(bin_dir_name, root_dir, wiki_dir, file_dir, css_dir, root_dir); + gplx.xowa.mediawiki.includes.cache.localisation.XomwLocalisationCacheForXowa.Init_ip(fsys_mgr.Bin_any_dir().GenSubDir("mediawiki")); log_wtr = usr_dlg.Log_wkr(); api_root = new Xoapi_root(this); diff --git a/400_xowa/src/gplx/xowa/guis/cmds/Xog_cmd_itm_.java b/400_xowa/src/gplx/xowa/guis/cmds/Xog_cmd_itm_.java index 05d399cf9..afbdd39aa 100644 --- a/400_xowa/src/gplx/xowa/guis/cmds/Xog_cmd_itm_.java +++ b/400_xowa/src/gplx/xowa/guis/cmds/Xog_cmd_itm_.java @@ -16,7 +16,7 @@ Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt package gplx.xowa.guis.cmds; import gplx.*; import gplx.xowa.*; import gplx.xowa.guis.*; public class Xog_cmd_itm_ { private static final Ordered_hash regy = Ordered_hash_.New(); // NOTE: must be defined at top - public static final String + public static final String Key_app_exit = new_dflt_(Xog_ctg_itm_.Tid_app , "xowa.app.exit") , Key_nav_go_bwd = new_dflt_(Xog_ctg_itm_.Tid_nav , "xowa.nav.go_bwd") diff --git a/400_xowa/src/gplx/xowa/langs/Xol_lang_itm.java b/400_xowa/src/gplx/xowa/langs/Xol_lang_itm.java index 7dfeeada9..5fb7788ed 100644 --- a/400_xowa/src/gplx/xowa/langs/Xol_lang_itm.java +++ b/400_xowa/src/gplx/xowa/langs/Xol_lang_itm.java @@ -19,6 +19,7 @@ import gplx.gfui.draws.*; import gplx.xowa.langs.cases.*; import gplx.xowa.langs.msgs.*; import gplx.xowa.langs.kwds.*; import gplx.xowa.langs.grammars.*; import gplx.xowa.langs.genders.*; import gplx.xowa.langs.plurals.*; import gplx.xowa.langs.vnts.*; import gplx.xowa.langs.vnts.converts.*; import gplx.xowa.langs.numbers.*; import gplx.xowa.langs.durations.*; import gplx.xowa.langs.lnki_trails.*; import gplx.xowa.langs.funcs.*; import gplx.xowa.langs.specials.*; import gplx.xowa.langs.bldrs.*; import gplx.xowa.langs.commas.*; import gplx.xowa.apps.gfs.*; import gplx.xowa.apps.fsys.*; import gplx.core.intls.*; import gplx.xowa.wikis.nss.*; import gplx.xowa.xtns.lst.*; import gplx.xowa.wikis.caches.*; import gplx.xowa.parsers.lnkis.*; import gplx.xowa.guis.langs.*; +import gplx.xowa.mediawiki.languages.*; public class Xol_lang_itm implements Gfo_invk { private boolean loaded = false; private final Object thread_lock = new Object(); @@ -26,6 +27,7 @@ public class Xol_lang_itm implements Gfo_invk { this.lang_mgr = lang_mgr; this.key_bry = key_bry; this.key_str = String_.new_u8(key_bry); Xol_lang_stub lang_itm = Xol_lang_stub_.Get_by_key_or_null(key_bry); if (lang_itm == null) throw Err_.new_wo_type("unknown lang_key", "key", String_.new_u8(key_bry)); this.lang_id = lang_itm.Id(); + this.mw_lang = new XomwLanguage(this); this.func_regy = new Xol_func_regy(lang_mgr, this); this.ns_names = new Xol_ns_grp(this); this.ns_aliases = new Xol_ns_grp(this); this.kwd_mgr = new Xol_kwd_mgr(this); @@ -40,6 +42,7 @@ public class Xol_lang_itm implements Gfo_invk { this.duration_mgr = new Xol_duration_mgr(this); if (lang_id != Xol_lang_stub_.Id_en) fallback_bry_ary = Fallback_bry_ary__en; // NOTE: do not set fallback_ary for en to en, else recursive loop } + public XomwLanguage Mw_lang() {return mw_lang;} private final XomwLanguage mw_lang; public Xoa_lang_mgr Lang_mgr() {return lang_mgr;} private final Xoa_lang_mgr lang_mgr; public byte[] Key_bry() {return key_bry;} private final byte[] key_bry; public String Key_str() {return key_str;} private final String key_str; diff --git a/400_xowa/src/gplx/xowa/mediawiki/XomwLog_.java b/400_xowa/src/gplx/xowa/mediawiki/XomwLog_.java new file mode 100644 index 000000000..c679ab5dc --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/XomwLog_.java @@ -0,0 +1,21 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; +public class XomwLog_ { + public static void wfDebug_by_method(String method, String msg) { + Gfo_log_.Instance.Note(method + msg); + } +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpArray.java b/400_xowa/src/gplx/xowa/mediawiki/XophpArray.java index eaaa54593..677672598 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpArray.java +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpArray.java @@ -19,7 +19,9 @@ public class XophpArray implements Bry_bfr_able { private final Ordered_hash hash = Ordered_hash_.New(); private int nxt_idx; public int Len() {return hash.Len();} + // TODO: lowercase count public int Count() {return hash.Len();} + public boolean Count_bool() {return hash.Len() > 0;} public void Clear() { hash.Clear(); nxt_idx = 0; @@ -57,6 +59,18 @@ public class XophpArray implements Bry_bfr_able { } return this; } + public XophpArray Add_as_key_and_val_many(String... val) { + for (String itm : val) { + Add(itm, itm); + } + return this; + } + public XophpArray Add_many(Object... val) { + for (Object itm : val) { + Add(itm); + } + return this; + } public XophpArray Get_at_ary(int i) {return (XophpArray)Get_at(i);} public String Get_at_str(int i) {return (String)Get_at(i);} public int Get_at_int(int i) {return Int_.Cast(Get_at(i));} @@ -65,33 +79,39 @@ public class XophpArray implements Bry_bfr_able { XophpArrayItm itm = (XophpArrayItm)hash.Get_at(i); return itm == null ? null : itm.Val(); } + public XophpArrayItm Get_at_itm(int i) { + if (i < 0 || i >= hash.Len()) return null; + return (XophpArrayItm)hash.Get_at(i); + } public void Del_at(int i) { XophpArrayItm itm = (XophpArrayItm)hash.Get_at(i); if (itm != null) { hash.Del(itm.Key()); } } - public XophpArray Add_many(Object... val) { - for (Object itm : val) { - Add(itm); - } - return this; - } public Object Get_by_obj(Object key) {return Get_by(Object_.Xto_str_strict_or_null(key));} public Object Get_by(int key) {return Get_by(Int_.To_str(key));} + public boolean Get_by_bool(String key) {return Bool_.Cast(this.Get_by(key));} public int Get_by_int(String key) {return Int_.Cast(this.Get_by(key));} - public Object Get_by_str(String key) {return (String)this.Get_by(key);} + public XophpArray Get_by_ary(String key) {return (XophpArray)this.Get_by(key);} + public String Get_by_str(char key) {return (String)this.Get_by(Char_.To_str(key));} + public String Get_by_str(String key) {return (String)this.Get_by(key);} public Object Get_by(String key) { XophpArrayItm itm = (XophpArrayItm)hash.Get_by(key); - return itm.Val(); + return itm == null ? null : itm.Val(); } public void Set(int key, Object val) { this.Set(XophpArrayItm.New_int(key, val)); } + public void Set(String key, Object val) { + this.Set(XophpArrayItm.New_str(key, val)); + } + // TODO: lowercase unset public void Unset(int key) {Unset(Int_.To_str(key));} public void Unset(String key) { hash.Del(key); } + public boolean in_array(String v) {return Has(v);} public boolean Has_obj(Object key) {return Has(Object_.Xto_str_strict_or_null(key));} public boolean Has(String key) { return hash.Has(key); @@ -122,8 +142,11 @@ public class XophpArray implements Bry_bfr_able { cur.Val_(itm.Val()); } } + public Object pop() {return Pop();} + // TODO: remove uppercase Pop public Object Pop() { int pos = this.Count() - 1; + if (pos < 0) return null; XophpArrayItm itm = (XophpArrayItm)hash.Get_at(pos); this.Del_at(pos); return itm.Val(); @@ -133,6 +156,15 @@ public class XophpArray implements Bry_bfr_able { itm += v; this.Set(idx, itm); } + public XophpArray Clone() { + XophpArray rv = new XophpArray(); + int len = hash.Len(); + for (int i = 0; i < len; i++) { + XophpArrayItm itm = (XophpArrayItm)hash.Get_at(i); + rv.Add(itm.Key(), itm.Val()); + } + return rv; + } public static XophpArray New(Object... vals) { XophpArray rv = new XophpArray(); for (Object val : vals) @@ -142,5 +174,9 @@ public class XophpArray implements Bry_bfr_able { public static boolean is_array(Object val) { return Type_.Eq_by_obj(val, XophpArray.class); } + public Object end() { + int len = hash.Len(); + return len == 0 ? null : ((XophpArrayItm)hash.Get_at(len - 1)).Val(); + } public static final XophpArray False = null; // handles code like "if ($var === false)" where var is an Object; } diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpArrayUtl.java b/400_xowa/src/gplx/xowa/mediawiki/XophpArrayUtl.java index 58b6951f7..5a9f65684 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpArrayUtl.java +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpArrayUtl.java @@ -25,7 +25,7 @@ public class XophpArrayUtl { return rv; } public static boolean isset(XophpArray ary, int idx) { - return ary.Get_at(idx) == null; + return ary.Get_at(idx) != null; } public static String[] array_keys_str(Ordered_hash array) { int len = array.Len(); @@ -150,4 +150,14 @@ public class XophpArrayUtl { } return rv; } + + // REF.PHP: https://www.php.net/manual/en/function.array-values.php + public static XophpArray array_values(XophpArray array) { + XophpArray rv = new XophpArray(); + int len = array.Len(); + for (int i = 0; i < len; i++) { + rv.Add(i, array.Get_at(i)); + } + return rv; + } } diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpArrayUtl_tst.java b/400_xowa/src/gplx/xowa/mediawiki/XophpArrayUtl_tst.java index 30197ea12..c51126cc2 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpArrayUtl_tst.java +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpArrayUtl_tst.java @@ -215,6 +215,13 @@ public class XophpArrayUtl_tst { // REF:https://www.php.net/manual/en/function.a , del ); } + @Test public void array_values() { + XophpArray orig = fxt.Make().Add("size", "XL").Add("color", "gold"); + fxt.Test__eq + ( fxt.Make().Add(0, "XL").Add(1, "gold") + , XophpArrayUtl.array_values(orig) + ); + } } class XophpArrayUtl_fxt { public XophpArray Make() {return new XophpArray();} diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpArray_tst.java b/400_xowa/src/gplx/xowa/mediawiki/XophpArray_tst.java index bb66466b9..25948f0df 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpArray_tst.java +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpArray_tst.java @@ -129,6 +129,7 @@ public class XophpArray_tst { // REF: http://php.net/manual/en/language.types.ar fxt.Test__Pop(ary, "b"); fxt.Test__Pop(ary, "a"); fxt.Test__Count(ary, 0); + fxt.Test__Pop(ary, null); } @Test public void Itm_str_concat_end() { XophpArray ary = XophpArray.New(); @@ -139,6 +140,19 @@ public class XophpArray_tst { // REF: http://php.net/manual/en/language.types.ar fxt.Test__Itm_str_concat_end(ary, "b1", 1, "1"); fxt.Test__Itm_str_concat_end(ary, "c2", 2, "2"); } + @Test public void Clone() { + XophpArray ary = XophpArray.New(); + ary.Add(0, "a").Add(1, "b").Add(2, "c"); + + fxt.Test__Eq(ary, ary.Clone()); + } + @Test public void Get_by() { + XophpArray ary = XophpArray.New(); + ary.Add("0", "a").Add("1", "b").Add("2", "c"); + + fxt.Test__Get_by(ary, "0", "a"); + fxt.Test__Get_by(ary, "missing", null); + } } class XophpArray_fxt { public void Test__Count(XophpArray ary, int expd) { @@ -161,4 +175,10 @@ class XophpArray_fxt { String actl = ary.Get_at_str(idx); Gftest.Eq__str(expd, actl); } + public void Test__Eq(XophpArray lhs, XophpArray rhs) { + Gftest.Eq__ary(lhs.To_ary(), rhs.To_ary()); + } + public void Test__Get_by(XophpArray ary, String key, Object expd) { + Gftest.Eq__obj_or_null(expd, ary.Get_by(key)); + } } diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpError.java b/400_xowa/src/gplx/xowa/mediawiki/XophpError.java new file mode 100644 index 000000000..eccd9208c --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpError.java @@ -0,0 +1,19 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; +public class XophpError extends Err { public XophpError(String msg) {super(true, "", "", msg); + } +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpFloat_.java b/400_xowa/src/gplx/xowa/mediawiki/XophpFloat_.java new file mode 100644 index 000000000..6e2765610 --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpFloat_.java @@ -0,0 +1,22 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; +public class XophpFloat_ { + // REF.PHP:https://www.php.net/manual/en/language.types.float.php + public static double floatval(String val) { + return Double_.parse(val); // NOTE:PHP float has roughly 14 decimal digits of precision which is more similar to Java's double than float + } +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpInt_.java b/400_xowa/src/gplx/xowa/mediawiki/XophpInt_.java new file mode 100644 index 000000000..cb1dcf292 --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpInt_.java @@ -0,0 +1,25 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; +public class XophpInt_ { + public static final int False = -1; // handles code like "if ($var === false)" where var is an Object; + public static String strval(int number) { + return Int_.To_str(number); + } + public static int intval(String val) { + return Int_.Parse_or(val, 0); + } +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpIo_.java b/400_xowa/src/gplx/xowa/mediawiki/XophpIo_.java new file mode 100644 index 000000000..353b2f461 --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpIo_.java @@ -0,0 +1,25 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; +public class XophpIo_ { + public static String file_get_contents(String path) { + String rv = Io_mgr.Instance.LoadFilStr(path); + return String_.Eq(rv, String_.Empty) ? XophpString_.Null : rv; + } + public static boolean file_exists(String path) { + return Io_mgr.Instance.ExistsFil(Io_url_.new_fil_(path)); + } +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpMath.java b/400_xowa/src/gplx/xowa/mediawiki/XophpMath.java index 211018a8b..2a5179fe6 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpMath.java +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpMath.java @@ -23,5 +23,39 @@ public class XophpMath { else { return Math_.Round(v, places); } - } + } + public static int min(int lhs, int rhs) { + return Math_.Min(lhs, rhs); + } + public static int min_many(int... ary) { + int rv = Int_.Max_value; + for (int itm : ary) { + if (itm < rv) + rv = itm; + } + return rv; + } + public static int max_many(int... ary) { + int rv = Int_.Min_value; + for (int itm : ary) { + if (itm > rv) + rv = itm; + } + return rv; + } + // REF.PHP:https://www.php.net/manual/en/function.fmod.php + public static Decimal_adp fmod_decimal(Decimal_adp lhs, Decimal_adp rhs) {return Decimal_adp_.double_(fmod(lhs.To_double(), rhs.To_double()));} + public static double fmod(double lhs, double rhs) { + return (double)lhs % (double)rhs; + } + +/* + +fmod + +$x = 5.7; +$y = 1.3; +$r = fmod($x, $y); +// $r equals 0.5, because 4 * 1.3 + 0.5 = 5.7 +*/ } diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpMath__tst.java b/400_xowa/src/gplx/xowa/mediawiki/XophpMath__tst.java new file mode 100644 index 000000000..fa001ba7a --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpMath__tst.java @@ -0,0 +1,30 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; +import org.junit.*; import gplx.core.tests.*; +public class XophpMath__tst { + private final XophpMath__fxt fxt = new XophpMath__fxt(); + @Test public void fmod() { + fxt.Test__fmod(8, 2, 0); + fxt.Test__fmod(7, 2, 1); + fxt.Test__fmod(5.7d, 1.3d, .5d); + } +} +class XophpMath__fxt { + public void Test__fmod(double lhs, double rhs, double expd) { + Gftest.Eq__double(expd, XophpMath.fmod(lhs, rhs)); + } +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpObject.java b/400_xowa/src/gplx/xowa/mediawiki/XophpObject.java index 2bc93c60d..227b17fb5 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpObject.java +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpObject.java @@ -17,4 +17,6 @@ package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; public class XophpObject { public static final Object False = null; // handles code like "if ($var === false)" where var is an Object; public static boolean is_true(Object val) {return val != null;} + public static boolean is_null(Object val) {return val == null;} + public static Object coalesce(Object val, Object if_null) {return val == null ? if_null : val;} } diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpPerfTimer.java b/400_xowa/src/gplx/xowa/mediawiki/XophpPerfTimer.java new file mode 100644 index 000000000..859685fb8 --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpPerfTimer.java @@ -0,0 +1,20 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; +public interface XophpPerfTimer { + void Bgn(); + void End(); +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpPerfTimer_.java b/400_xowa/src/gplx/xowa/mediawiki/XophpPerfTimer_.java new file mode 100644 index 000000000..85066d91f --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpPerfTimer_.java @@ -0,0 +1,23 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; +public class XophpPerfTimer_ { + public static final XophpPerfTimer Noop = new XophpPerfTimerNoop(); +} +class XophpPerfTimerNoop implements XophpPerfTimer { + public void Bgn() {} + public void End() {} +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpRegex_.java b/400_xowa/src/gplx/xowa/mediawiki/XophpRegex_.java new file mode 100644 index 000000000..adafaccdc --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpRegex_.java @@ -0,0 +1,99 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; +import gplx.langs.regxs.*; +public class XophpRegex_ { + public static boolean preg_match_bool(Regx_adp pattern, int modifier, String subject) {return preg_match_bool(pattern, modifier, subject, null, 0, 0);} + public static boolean preg_match_bool(Regx_adp pattern, String subject, XophpArray matches, int flags, int offset) {return preg_match(pattern, MODIFIER_NONE, subject, matches, flags, offset) == FOUND;} + public static boolean preg_match_bool(Regx_adp pattern, int modifier, String subject, XophpArray matches, int flags, int offset) {return preg_match(pattern, modifier, subject, matches, flags, offset) == FOUND;} + public static int preg_match(Regx_adp pattern, String subject) {return preg_match(pattern, MODIFIER_NONE, subject, null, 0, 0);} + public static int preg_match(Regx_adp pattern, int modifier, String subject) {return preg_match(pattern, modifier, subject, null, 0, 0);} + // REF.PHP: https://www.php.net/manual/en/function.preg-match.php + public static int preg_match(Regx_adp pattern, int modifier, String subject, XophpArray matches, int flags, int offset) { + // handle offset + int subject_len = String_.Len(subject); + if (offset >= subject_len || offset < 0) return PREG_ERR; + + // exec match + // FUTURE: offset is in bytes, whereas subject will be in chars + Regx_match match = pattern.Match(subject, offset); + + // update vars if something found + int rv = NOT_FOUND; + if (match.Rslt()) { + rv = FOUND; + int find_bgn = match.Find_bgn(); + String match_str = String_.Mid(subject, find_bgn, match.Find_end()); + Regx_group[] grps = match.Groups(); + int grps_len = grps.length; + + // handle grps + if (matches != null) { + if (grps_len == 0) { + if (flags == PREG_OFFSET_CAPTURE) { + matches.Add(XophpArray.New(match_str, find_bgn)); + } + else { + matches.Add(match_str); + } + } + else { + preg_match_fill(subject, matches, flags, match, match_str, grps, grps_len); + } + } + } + return rv; + } + private static void preg_match_fill(String subject, XophpArray matches, int flags, Regx_match match, String match_str, Regx_group[] grps, int grps_len) { + for (int i = 0; i < grps_len; i++) { + Regx_group grp = grps[i]; + if (!grp.Rslt()) continue; // ignore non matches in group; EX: "1" and "^-?(([0-9]+)(?:\\.([0-9]+))?)" returns a match=false for group(2) + String grp_match = grp.Val(); + if (flags == PREG_OFFSET_CAPTURE) { + matches.Add(XophpArray.New(grp_match, grp.Bgn())); + } + else { + matches.Add(grp_match); + } + } + } + // REF.PHP:https://www.php.net/manual/en/pcre.constants.php + public static final int + PREG_OFFSET_CAPTURE = 256 + , PREG_UNMATCHED_AS_NULL = 0 + , PREG_NO_FLAG = Int_.Min_value + , PREG_ERR = XophpInt_.False + ; + + public static final int NOT_FOUND = 0, FOUND = 1; + + // REF.PHP:https://www.php.net/manual/en/reference.pcre.pattern.modifiers.php + public static final int + MODIFIER_NONE = 0 + , MODIFIER_i = Math_.Pow_int(2, 0) // PCRE_CASELESS: If this modifier is set, letters in the pattern match both upper and lower case letters. + , MODIFIER_m = Math_.Pow_int(2, 1) // PCRE_MULTILINE: By default, PCRE treats the subject String as consisting of a single "line" of characters (even if it actually contains several newlines). The "start of line" metacharacter (^) matches only at the start of the String, while the "end of line" metacharacter ($) matches only at the end of the String, or before a terminating newline (unless D modifier is set). This is the same as Perl. When this modifier is set, the "start of line" and "end of line" constructs match immediately following or immediately before any newline in the subject String, respectively, as well as at the very start and end. This is equivalent to Perl's /m modifier. If there are no "\n" characters in a subject String, or no occurrences of ^ or $ in a pattern, setting this modifier has no effect. + , MODIFIER_s = Math_.Pow_int(2, 2) // PCRE_DOTALL: If this modifier is set, a dot metacharacter in the pattern matches all characters, including newlines. Without it, newlines are excluded. This modifier is equivalent to Perl's /s modifier. A negative class such as [^a] always matches a newline character, independent of the setting of this modifier. + , MODIFIER_x = Math_.Pow_int(2, 3) // PCRE_EXTENDED: If this modifier is set, whitespace data characters in the pattern are totally ignored except when escaped or inside a character class, and characters between an unescaped # outside a character class and the next newline character, inclusive, are also ignored. This is equivalent to Perl's /x modifier, and makes it possible to include commentary inside complicated patterns. Note, however, that this applies only to data characters. Whitespace characters may never appear within special character sequences in a pattern, for example within the sequence (?( which introduces a conditional subpattern. + , MODIFIER_e = Math_.Pow_int(2, 4) // PREG_REPLACE_EVAL: If this deprecated modifier is set, preg_replace() does normal substitution of backreferences in the replacement String, evaluates it as PHP code, and uses the result for replacing the search String. Single quotes, double quotes, backslashes (\) and NULL chars will be escaped by backslashes in substituted backreferences. + , MODIFIER_A = Math_.Pow_int(2, 5) // PREG_ANCHORED: If this modifier is set, the pattern is forced to be "anchored", that is, it is constrained to match only at the start of the String which is being searched (the "subject String"). This effect can also be achieved by appropriate constructs in the pattern itself, which is the only way to do it in Perl. + , MODIFIER_D = Math_.Pow_int(2, 6) // PCRE_DOLLAR_ENDONLY: If this modifier is set, a dollar metacharacter in the pattern matches only at the end of the subject String. Without this modifier, a dollar also matches immediately before the final character if it is a newline (but not before any other newlines). This modifier is ignored if m modifier is set. There is no equivalent to this modifier in Perl. + , MODIFIER_S = Math_.Pow_int(2, 7) // PCRE_STUDY: When a pattern is going to be used several times, it is worth spending more time analyzing it in order to speed up the time taken for matching. If this modifier is set, then this extra analysis is performed. At present, studying a pattern is useful only for non-anchored patterns that do not have a single fixed starting character. + , MODIFIER_U = Math_.Pow_int(2, 8) // PCRE_UNGREEDY: This modifier inverts the "greediness" of the quantifiers so that they are not greedy by default, but become greedy if followed by ?. It is not compatible with Perl. It can also be set by a (?U) modifier setting within the pattern or by a question mark behind a quantifier (e.g. .*?). + , MODIFIER_X = Math_.Pow_int(2, 9) // PCRE_EXTRA: This modifier turns on additional functionality of PCRE that is incompatible with Perl. Any backslash in a pattern that is followed by a letter that has no special meaning causes an error, thus reserving these combinations for future expansion. By default, as in Perl, a backslash followed by a letter with no special meaning is treated as a literal. There are at present no other features controlled by this modifier. + , MODIFIER_J = Math_.Pow_int(2, 10) // PCRE_INFO_JCHANGED: The (?J) @gplx.Internal protected option setting changes the local PCRE_DUPNAMES option. Allow duplicate names for subpatterns. As of PHP 7.2.0 J is supported as modifier as well. + , MODIFIER_u = Math_.Pow_int(2, 11) // PCRE_UTF8: This modifier turns on additional functionality of PCRE that is incompatible with Perl. Pattern and subject strings are treated as UTF-8. An invalid subject will cause the preg_* function to match nothing; an invalid pattern will trigger an error of level E_WARNING. Five and six octet UTF-8 sequences are regarded as invalid since PHP 5.3.4 (resp. PCRE 7.3 2007-08-28); formerly those have been regarded as valid UTF-8. + ; +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpRegex__tst.java b/400_xowa/src/gplx/xowa/mediawiki/XophpRegex__tst.java new file mode 100644 index 000000000..7fbf58d13 --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpRegex__tst.java @@ -0,0 +1,102 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; +import org.junit.*; import gplx.core.tests.*; import gplx.core.strings.*; +import gplx.langs.regxs.*; +public class XophpRegex__tst { + private final XophpRegex__fxt fxt = new XophpRegex__fxt(); + @After public void term() {fxt.Term();} + @Test public void Basic() { + fxt.Test__preg_match("a", "abc", fxt.Expd__y().Add("a")); // found + fxt.Test__preg_match("z", "abc", fxt.Expd__n()); // not found + fxt.Test__preg_match("c", "abc", 1, fxt.Expd__y().Add("c")); // offset + fxt.Test__preg_match("c", "abc", 3, fxt.Expd__err()); // offset: too large + fxt.Test__preg_match("c", "abc", -1, fxt.Expd__err()); // offset: negative + } + @Test public void Not_found() { + fxt.Test__preg_match("a", "abc", fxt.Expd__y().Add("a")); // found + fxt.Test__preg_match("z", "abc", fxt.Expd__n()); // not found + fxt.Test__preg_match("c", "abc", 1, fxt.Expd__y().Add("c")); // offset + fxt.Test__preg_match("c", "abc", 3, fxt.Expd__err()); // offset: too large + fxt.Test__preg_match("c", "abc", -1, fxt.Expd__err()); // offset: negative + } + @Test public void Character_classes() { + fxt.Test__preg_match("[bc]", "abc", fxt.Expd__y().Add("b")); // character class + fxt.Test__preg_match("[bc]", "abc", XophpRegex_.PREG_OFFSET_CAPTURE, 2, fxt.Expd__y().Add("c", 2)); // character class + } + @Test public void Groups() { + fxt.Test__preg_match("(foo)(bar)(baz)", "foobarbaz", XophpRegex_.PREG_OFFSET_CAPTURE, 0, fxt.Expd__y() + .Add("foobarbaz", 0) + .Add("foo", 0) + .Add("bar", 3) + .Add("baz", 6) + ); + fxt.Test__preg_match("(foo)(bar)(baz)", "foobarbaz", XophpRegex_.PREG_NO_FLAG, 0, fxt.Expd__y() + .Add("foobarbaz") + .Add("foo") + .Add("bar") + .Add("baz") + ); + } +} +class XophpRegex__fxt { + private String_bldr print_php = null;//String_bldr_.new_(); + public XophpRegex__expd Expd__err() {return new XophpRegex__expd(XophpRegex_.PREG_ERR);} + public XophpRegex__expd Expd__y() {return new XophpRegex__expd(1);} + public XophpRegex__expd Expd__n() {return new XophpRegex__expd(XophpRegex_.NOT_FOUND);} + public void Test__preg_match(String pattern, String str, XophpRegex__expd rslt) {Test__preg_match(pattern, str, XophpRegex_.PREG_NO_FLAG, 0, rslt);} + public void Test__preg_match(String pattern, String str, int offset, XophpRegex__expd rslt) {Test__preg_match(pattern, str, XophpRegex_.PREG_NO_FLAG, offset, rslt);} + public void Test__preg_match(String pattern, String str, int flags, int offset, XophpRegex__expd rslt) { + if (print_php != null) { + String flag_str = ""; + switch (flags) { + case XophpRegex_.PREG_OFFSET_CAPTURE: flag_str = "PREG_OFFSET_CAPTURE"; break; + case XophpRegex_.PREG_UNMATCHED_AS_NULL: flag_str = "PREG_UNMATCHED_AS_NULL"; break; + case XophpRegex_.PREG_NO_FLAG: flag_str = "0"; break; + } + print_php.Add(String_.Format("\necho \"
\" . preg_match('/{0}/', '{1}', $m, {2}, {3}) . ' '; var_dump($m);", pattern, str, flag_str, offset)); + } + XophpArray actl_matches = XophpArray.New(); + int actl_pos = XophpRegex_.preg_match(Regx_adp_.new_(pattern), XophpRegex_.MODIFIER_NONE, str, actl_matches, flags, offset); + Gftest.Eq__int(rslt.Pos(), actl_pos); + XophpArray expd_matches = rslt.Matches(); + if (expd_matches != null) { + Gftest.Eq__ary__lines(expd_matches.To_str(), actl_matches.To_str()); + } + } + public void Term() { + if (print_php != null) + Tfds.Write(print_php.Add_char_nl().To_str_and_clear()); + } +} +class XophpRegex__expd { + public XophpRegex__expd(int pos) { + this.pos = pos; + } + public int Pos() {return pos;} private final int pos; + public XophpArray Matches() {return matches;} private XophpArray matches; + public XophpRegex__expd Add(String... ary) { + if (matches == null) matches = XophpArray.New(); + for (Object itm : ary) + matches.Add(itm); + return this; + } + public XophpRegex__expd Add(String s, int pos) { + if (matches == null) matches = XophpArray.New(); + matches.Add(XophpArray.New(s, pos)); + return this; + } +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpString.java b/400_xowa/src/gplx/xowa/mediawiki/XophpString.java deleted file mode 100644 index 6e3001bca..000000000 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpString.java +++ /dev/null @@ -1,177 +0,0 @@ -/* -XOWA: the XOWA Offline Wiki Application -Copyright (C) 2012-2017 gnosygnu@gmail.com - -XOWA is licensed under the terms of the General Public License (GPL) Version 3, -or alternatively under the terms of the Apache License Version 2.0. - -You may use XOWA according to either of these licenses as is most appropriate -for your project on a case-by-case basis. - -The terms of each license can be found in the source code repository: - -GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt -Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt -*/ -package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; -import gplx.core.btries.*; -public class XophpString { - public static int strpos(byte[] src, byte find) {return strpos(src, find, 0, src.length);} - public static int strpos(byte[] src, byte find, int bgn, int end) { - return Bry_find_.Find_fwd(src, find, bgn, end); - } - public static String substr(String src, int bgn, int len) {return String_.new_u8(substr(Bry_.new_u8(src), bgn, len));} - public static String substr(String src, int bgn) {return String_.new_u8(substr(Bry_.new_u8(src), bgn, String_.Len(src)));} - public static byte[] substr(byte[] src, int bgn) {return substr(src, bgn, src.length);} - public static byte[] substr(byte[] src, int bgn, int len) { - int src_len = src.length; - if (bgn < 0) bgn = src_len + bgn; // handle negative - if (bgn < 0) bgn = 0; // handle out of bounds; EX: ("a", -1, -1) - int end = len < 0 ? src_len + len : bgn + len; - if (end > src.length) end = src.length;; // handle out of bounds; - return Bry_.Mid(src, bgn, end); - } - public static byte substr_byte(byte[] src, int bgn) {return substr_byte(src, bgn, src.length);} - public static byte substr_byte(byte[] src, int bgn, int len) { - int src_len = src.length; - if (src_len == 0) return Byte_ascii.Null; - if (bgn < 0) bgn = src_len + bgn; // handle negative - if (bgn < 0) bgn = 0; // handle out of bounds; EX: ("a", -1, -1) - int end = len < 0 ? src_len + len : bgn + len; - if (end > src.length) end = src.length;; // handle out of bounds; - return src[bgn]; - } - public static int strspn_fwd__ary(byte[] src, boolean[] find, int bgn, int max, int src_len) { - if (max == -1) max = src_len; - int rv = 0; - for (int i = bgn; i < src_len; i++) { - if (find[src[i] & 0xFF] && rv < max) // PATCH.JAVA:need to convert to unsigned byte - rv++; - else - break; - } - return rv; - } - public static int strspn_fwd__byte(byte[] src, byte find, int bgn, int max, int src_len) { - if (max == -1) max = src_len; - int rv = 0; - for (int i = bgn; i < src_len; i++) { - if (find == src[i] && rv < max) - rv++; - else - break; - } - return rv; - } - public static int strspn_fwd__space_or_tab(byte[] src, int bgn, int max, int src_len) { - if (max == -1) max = src_len; - int rv = 0; - for (int i = bgn; i < src_len; i++) { - switch (src[i]) { - case Byte_ascii.Space: - case Byte_ascii.Tab: - if (rv < max) { - rv++; - continue; - } - break; - } - break; - } - return rv; - } - public static int strspn_bwd__byte(byte[] src, byte find, int bgn, int max) { - if (max == -1) max = Int_.Max_value; - int rv = 0; - for (int i = bgn - 1; i > -1; i--) { - if (find == src[i] && rv < max) - rv++; - else - break; - } - return rv; - } - public static int strspn_bwd__ary(byte[] src, boolean[] find, int bgn, int max) { - if (max == -1) max = Int_.Max_value; - int rv = 0; - for (int i = bgn - 1; i > -1; i--) { - if (find[src[i & 0xFF]] && rv < max) // PATCH.JAVA:need to convert to unsigned byte - rv++; - else - break; - } - return rv; - } - public static int strspn_bwd__space_or_tab(byte[] src, int bgn, int max) { - if (max == -1) max = Int_.Max_value; - int rv = 0; - for (int i = bgn - 1; i > -1; i--) { - switch (src[i]) { - case Byte_ascii.Space: - case Byte_ascii.Tab: - if (rv < max) { - rv++; - continue; - } - break; - } - break; - } - return rv; - } - public static byte[] strtr(byte[] src, Btrie_slim_mgr trie, Bry_bfr tmp, Btrie_rv trv) { - boolean dirty = false; - int src_bgn = 0; - int src_end = src.length; - int i = src_bgn; - - while (true) { - if (i == src_end) break; - byte b = src[i]; - Object o = trie.Match_at_w_b0(trv, b, src, i, src_end); - if (o == null) { - if (dirty) { - tmp.Add_byte(b); - } - i++; - } - else { - if (!dirty) { - dirty = true; - tmp.Add_mid(src, 0, i); - } - tmp.Add((byte[])o); - i = trv.Pos(); - } - } - return dirty ? tmp.To_bry_and_clear() : src; - } - public static byte[] strtr(byte[] src, byte find, byte repl) { - return Bry_.Replace(src, 0, src.length, find, repl); - } - public static byte[] str_replace(byte find, byte repl, byte[] src) { - return Bry_.Replace(src, 0, src.length, find, repl); - } - public static byte[] str_replace(byte[] find, byte[] repl, byte[] src) { - return Bry_.Replace(src, find, repl); - } - public static byte[] strstr(byte[] src, byte[] find) { - int pos = Bry_find_.Find_fwd(src, find); - return pos == Bry_find_.Not_found ? null : Bry_.Mid(src, pos, src.length); - } - public static int strlen(byte[] src) {return src.length;} - public static String str_repeat(String val, int count) { - int val_len = String_.Len(val); - int chry_len = val_len * count; - char[] chry = new char[chry_len]; - for (int i = 0; i < count; i++) { - for (int j = 0; j < val_len; j++) { - chry[(i * val_len) + j] = String_.CharAt(val, j); - } - } - return String_.new_charAry_(chry, 0, chry_len); - } - public static boolean is_string(Object o) { - return String_.as_(o) != null; - } -} diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpString_.java b/400_xowa/src/gplx/xowa/mediawiki/XophpString_.java index 508993e6d..56d1e9e8b 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpString_.java +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpString_.java @@ -14,6 +14,452 @@ GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt */ package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; +import gplx.core.btries.*; +import gplx.core.intls.*; +import gplx.objects.strings.unicodes.*; +import gplx.core.primitives.*; public class XophpString_ { - public static final String False = null; // handles code like "if ($var === false)" where var is an Object; + public static final String Null = null; + public static boolean is_true(String s) {return s != null;} // handles code like "if ($var)" where var is an Object; + + // REF.PHP: https://www.php.net/manual/en/function.strpos.php + public static int strpos(String haystack, String needle) {return strpos(haystack, needle, 0);} + public static int strpos(String haystack, String needle, int offset) { + if (offset < 0) { + offset = String_.Len(haystack) + offset; + } + return String_.FindFwd(haystack, needle, offset); + } + public static int strpos(byte[] src, byte find) {return strpos(src, find, 0, src.length);} + public static int strpos(byte[] src, byte find, int bgn, int end) { + return Bry_find_.Find_fwd(src, find, bgn, end); + } + + // REF.PHP: https://www.php.net/manual/en/function.substr.php + public static String substr(String src, int bgn, int len) {return String_.new_u8(substr(Bry_.new_u8(src), bgn, len));} + public static String substr(String src, int bgn) {return String_.new_u8(substr(Bry_.new_u8(src), bgn, String_.Len(src)));} + public static byte[] substr(byte[] src, int bgn) {return substr(src, bgn, src.length);} + public static byte[] substr(byte[] src, int bgn, int len) { + int src_len = src.length; + if (bgn < 0) bgn = src_len + bgn; // handle negative + if (bgn < 0) bgn = 0; // handle out of bounds; EX: ("a", -1, -1) + int end = len < 0 ? src_len + len : bgn + len; + if (end > src.length) end = src.length;; // handle out of bounds; + return Bry_.Mid(src, bgn, end); + } + public static byte substr_byte(byte[] src, int bgn) {return substr_byte(src, bgn, src.length);} + public static byte substr_byte(byte[] src, int bgn, int len) { + int src_len = src.length; + if (src_len == 0) return Byte_ascii.Null; + if (bgn < 0) bgn = src_len + bgn; // handle negative + if (bgn < 0) bgn = 0; // handle out of bounds; EX: ("a", -1, -1) + int end = len < 0 ? src_len + len : bgn + len; + if (end > src.length) end = src.length;; // handle out of bounds; + return src[bgn]; + } + // REF.PHP: https://www.php.net/manual/en/function.strspn.php + public static Hash_adp strspn_hash(String mask) { + Hash_adp rv = Hash_adp_.New(); + int mask_len = String_.Len(mask); + int i = 0; + while (i < mask_len) { + char hi_char = String_.CharAt(mask, i); + String key = ""; + if (Utf16_.Len_by_char(hi_char) == 2) { + i++; + char lo_char = String_.CharAt(mask, i); + int surrogate_char = Utf16_.Surrogate_merge(Char_.To_int(hi_char), Char_.To_int(lo_char)); + key = String_.new_u8(Utf16_.Encode_int_to_bry(surrogate_char)); + } + else { + key = Char_.To_str(hi_char); + } + rv.Add_if_dupe_use_1st(key, key); + i++; + } + return rv; + } + public static int strspn(String subject, Hash_adp mask, int start) {return strspn(subject, mask, start, Int_.Null);} + public static int strspn(String subject, Hash_adp mask, int start, int length) { + int subject_len = String_.Len(subject); + + // get subject_end + int subject_end = 0; + if (length == Int_.Null) { + subject_end = subject_len; + } + else if (length < 0) { + subject_end = subject_len + length; // If length is given and is negative, then subject will be examined from the starting position up to length characters from the end of subject. + if (subject_end < start) + subject_end = start; + } + else { + subject_end = start + length; // If length is given and is non-negative, then subject will be examined for length characters after the starting position. + if (subject_end > subject_len) + subject_end = subject_len; + } + + // loop subject until encountering character not in mask + int rv = 0; + int i = start; + while (i < subject_end) { + char subject_char = String_.CharAt(subject, i); + String mask_key = ""; + if (Utf16_.Len_by_char(subject_char) == 2) { + i++; + char lo_char = String_.CharAt(subject, i); + // TODO: change Char_.To_int_or to Char_.To_digit + int surrogate_char = Utf16_.Surrogate_merge(Char_.To_int(subject_char), Char_.To_int(lo_char)); + mask_key = String_.new_u8(Utf16_.Encode_int_to_bry(surrogate_char)); + } + else { + mask_key = Char_.To_str(subject_char); + } + + if (mask.Has(mask_key)) { + rv++; + } + else { + break; + } + i++; + } + return rv; + } + public static int strspn_fwd__ary(byte[] src, boolean[] find, int bgn, int max, int src_len) { + if (max == -1) max = src_len; + int rv = 0; + for (int i = bgn; i < src_len; i++) { + if (find[src[i] & 0xFF] && rv < max) // PATCH.JAVA:need to convert to unsigned byte + rv++; + else + break; + } + return rv; + } + public static int strspn_fwd__byte(byte[] src, byte find, int bgn, int max, int src_len) { + if (max == -1) max = src_len; + int rv = 0; + for (int i = bgn; i < src_len; i++) { + if (find == src[i] && rv < max) + rv++; + else + break; + } + return rv; + } + public static int strspn_fwd__space_or_tab(byte[] src, int bgn, int max, int src_len) { + if (max == -1) max = src_len; + int rv = 0; + for (int i = bgn; i < src_len; i++) { + switch (src[i]) { + case Byte_ascii.Space: + case Byte_ascii.Tab: + if (rv < max) { + rv++; + continue; + } + break; + } + break; + } + return rv; + } + public static int strspn_bwd__byte(byte[] src, byte find, int bgn, int max) { + if (max == -1) max = Int_.Max_value; + int rv = 0; + for (int i = bgn - 1; i > -1; i--) { + if (find == src[i] && rv < max) + rv++; + else + break; + } + return rv; + } + public static int strspn_bwd__ary(byte[] src, boolean[] find, int bgn, int max) { + if (max == -1) max = Int_.Max_value; + int rv = 0; + for (int i = bgn - 1; i > -1; i--) { + if (find[src[i & 0xFF]] && rv < max) // PATCH.JAVA:need to convert to unsigned byte + rv++; + else + break; + } + return rv; + } + public static int strspn_bwd__space_or_tab(byte[] src, int bgn, int max) { + if (max == -1) max = Int_.Max_value; + int rv = 0; + for (int i = bgn - 1; i > -1; i--) { + switch (src[i]) { + case Byte_ascii.Space: + case Byte_ascii.Tab: + if (rv < max) { + rv++; + continue; + } + break; + } + break; + } + return rv; + } + public static byte[] strtr(byte[] src, Btrie_slim_mgr trie, Bry_bfr tmp, Btrie_rv trv) { + boolean dirty = false; + int src_bgn = 0; + int src_end = src.length; + int i = src_bgn; + + while (true) { + if (i == src_end) break; + byte b = src[i]; + Object o = trie.Match_at_w_b0(trv, b, src, i, src_end); + if (o == null) { + if (dirty) { + tmp.Add_byte(b); + } + i++; + } + else { + if (!dirty) { + dirty = true; + tmp.Add_mid(src, 0, i); + } + tmp.Add((byte[])o); + i = trv.Pos(); + } + } + return dirty ? tmp.To_bry_and_clear() : src; + } + public static byte[] strtr(byte[] src, byte find, byte repl) { + return Bry_.Replace(src, 0, src.length, find, repl); + } + public static byte[] str_replace(byte find, byte repl, byte[] src) { + return Bry_.Replace(src, 0, src.length, find, repl); + } + public static byte[] str_replace(byte[] find, byte[] repl, byte[] src) { + return Bry_.Replace(src, find, repl); + } + public static byte[] strstr(byte[] src, byte[] find) { + int pos = Bry_find_.Find_fwd(src, find); + return pos == Bry_find_.Not_found ? null : Bry_.Mid(src, pos, src.length); + } + public static int strlen(String src) {return String_.Len(src);} + public static int strlen(byte[] src) {return src.length;} + + // REF.PHP: https://www.php.net/manual/en/function.rtrim.php + private static final Hash_adp trim_ws_hash = Hash_adp_.New().Add_many_as_key_and_val + ( Int_obj_ref.New(Byte_ascii.Space) + , Int_obj_ref.New(Byte_ascii.Tab) + , Int_obj_ref.New(Byte_ascii.Nl) + , Int_obj_ref.New(Byte_ascii.Cr) + , Int_obj_ref.New(Byte_ascii.Null) + , Int_obj_ref.New(Byte_ascii.Vertical_tab) + ); + public static String rtrim(String src) {return rtrim(src, null);} + public static String rtrim(String src_str, String pad_str) { + Hash_adp pad_hash = null; + if (pad_str == null) pad_hash = trim_ws_hash; + + // init brys / lens + byte[] src_bry = Bry_.new_u8(src_str); + int src_len = src_bry.length; + byte[] pad_bry = Bry_.new_u8(pad_str); + int pad_len = pad_bry.length; + + // ---------------------- + // 0, 1 chars (optimized) + // ---------------------- + int last = 0; + switch (pad_len) { + // pad is "" + case 0: + return src_str; + // pad is 1 char + case 1: + last = src_len; + byte pad_byte = pad_bry[0]; + for (int i = src_len - 1; i > -1; i--) { + byte cur = src_bry[i]; + last = i + 1; + if (cur != pad_byte) { + break; + } + } + return (last == src_len) ? src_str : String_.new_u8(Bry_.Mid(src_bry, 0, last)); + } + + // -------- + // 2+ chars + // -------- + // create pad_hash if not ws_hash + // NOTE: PHP does not support multibyte strings; see TEST + if (pad_hash == null) { + pad_hash = Hash_adp_.New(); + byte prv_byte = Byte_.Zero; + for (int i = 0; i < pad_len; i++) { + byte pad_byte = pad_bry[i]; + if (pad_byte == Byte_ascii.Dot && i < pad_len - 1) { + byte nxt_byte = pad_bry[i + 1]; + if (nxt_byte == Byte_ascii.Dot) { + if (i == 0) { + throw new XophpError(".. found but at start of String; src=" + pad_str); + } + else if (i == pad_len - 2) { + throw new XophpError(".. found but at end of String; src=" + pad_str); + } + else { + nxt_byte = pad_bry[i + 2]; + if (nxt_byte > prv_byte) { + for (byte j = prv_byte; j < nxt_byte; j++) { + Byte_obj_ref rng_obj = Byte_obj_ref.new_(j); + if (!pad_hash.Has(rng_obj)) + pad_hash.Add_as_key_and_val(rng_obj); + } + i += 2; + continue; + } + else { + throw new XophpError(".. found but next byte must be greater than previous byte; src=" + pad_str); + } + } + } + } + prv_byte = pad_byte; + Byte_obj_ref pad_obj = Byte_obj_ref.new_(pad_byte); + if (!pad_hash.Has(pad_obj)) + pad_hash.Add_as_key_and_val(pad_obj); + } + } + + // loop src until non-matching pad int + Byte_obj_ref temp = Byte_obj_ref.zero_(); + last = src_len; + for (int i = src_len - 1; i > -1; i--) { + temp.Val_(src_bry[i]); + last = i + 1; + if (!pad_hash.Has(temp)) { + break; + } + } + return (last == src_len) ? src_str : String_.new_u8(Bry_.Mid(src_bry, 0, last)); + } + public static String str_repeat(String val, int count) { + int val_len = String_.Len(val); + int chry_len = val_len * count; + char[] chry = new char[chry_len]; + for (int i = 0; i < count; i++) { + for (int j = 0; j < val_len; j++) { + chry[(i * val_len) + j] = String_.CharAt(val, j); + } + } + return String_.new_charAry_(chry, 0, chry_len); + } + public static boolean is_string(Object o) { + return String_.as_(o) != null; + } + public static String strtolower(String s) { + return String_.Lower(s); + } + // REF.PHP: https://www.php.net/manual/en/function.ord.php + public static int ord(String s) { + return String_.Len_eq_0(s) ? 0 : Char_.To_int(String_.CharAt(s, 0)); + } + public static String[] explode(String delimiter, String str) { + return String_.Split(str, delimiter); + } + // NOTE: support simple syntax only + // REF.PHP: https://www.php.net/manual/en/language.types.String.php#language.types.String.parsing + public static String Fmt(String fmt_str, Object... args) { + byte[] fmt = Bry_.new_u8(fmt_str); + int len = fmt.length; + Bry_bfr bfr = Bry_bfr_.New(); + int pos = 0; + int arg_idx = 0; + while (pos < len) { + // find next $ + int dollar_pos = Bry_find_.Find_fwd(fmt, Byte_ascii.Dollar, pos); + + // no more $ + if (dollar_pos == Bry_find_.Not_found) { + // add rest of fmt + bfr.Add_mid(fmt, pos, len); + break; + } + + int key_bgn = dollar_pos + 1; + // if $ at end, then just add it literally; also bound-check + if (key_bgn == len) { + bfr.Add_mid(fmt, pos, len); + break; + } + + int key_end = len; + byte key_bgn_byte = fmt[key_bgn]; + // if { after $, then search forward for } + if (key_bgn_byte == Byte_ascii.Curly_bgn) { + key_end = Bry_find_.Find_fwd(fmt, Byte_ascii.Curly_end, key_bgn + 1, len); + + // no } found; fail; EX: $b = 'z'; echo("a${b"); + if (key_end == Bry_find_.Not_found) { + throw Err_.new_wo_type("invalid fmt; fmt=" + fmt); + } + + // skip past "}" + key_end++; + } + // no "{" + else { + // search forward according to regex; ^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$; REF.PHP: https://www.php.net/manual/en/language.variables.basics.php + for (int i = key_bgn; i < key_end; i++) { + byte key_cur = fmt[i]; + if (!Is_identifier_char(key_cur, i == key_bgn)) { + key_end = i; + break; + } + } + } + + // invalid key; EX: $0 + if (key_bgn == key_end) { + bfr.Add_mid(fmt, pos, key_bgn); + pos = key_bgn; + continue; + } + + // valid key; add everything before key_bgn + bfr.Add_mid(fmt, pos, dollar_pos); + + // add arg_idx + bfr.Add_str_u8(Object_.Xto_str_strict_or_empty(args[arg_idx++])); + + // update pos + pos = key_end; + } + return bfr.To_str_and_clear(); + } + private static boolean Is_identifier_char(byte b, boolean is_first) { + switch (b) { + // alpha and _ is always valid + case Byte_ascii.Ltr_A: case Byte_ascii.Ltr_B: case Byte_ascii.Ltr_C: case Byte_ascii.Ltr_D: case Byte_ascii.Ltr_E: + case Byte_ascii.Ltr_F: case Byte_ascii.Ltr_G: case Byte_ascii.Ltr_H: case Byte_ascii.Ltr_I: case Byte_ascii.Ltr_J: + case Byte_ascii.Ltr_K: case Byte_ascii.Ltr_L: case Byte_ascii.Ltr_M: case Byte_ascii.Ltr_N: case Byte_ascii.Ltr_O: + case Byte_ascii.Ltr_P: case Byte_ascii.Ltr_Q: case Byte_ascii.Ltr_R: case Byte_ascii.Ltr_S: case Byte_ascii.Ltr_T: + case Byte_ascii.Ltr_U: case Byte_ascii.Ltr_V: case Byte_ascii.Ltr_W: case Byte_ascii.Ltr_X: case Byte_ascii.Ltr_Y: case Byte_ascii.Ltr_Z: + case Byte_ascii.Ltr_a: case Byte_ascii.Ltr_b: case Byte_ascii.Ltr_c: case Byte_ascii.Ltr_d: case Byte_ascii.Ltr_e: + case Byte_ascii.Ltr_f: case Byte_ascii.Ltr_g: case Byte_ascii.Ltr_h: case Byte_ascii.Ltr_i: case Byte_ascii.Ltr_j: + case Byte_ascii.Ltr_k: case Byte_ascii.Ltr_l: case Byte_ascii.Ltr_m: case Byte_ascii.Ltr_n: case Byte_ascii.Ltr_o: + case Byte_ascii.Ltr_p: case Byte_ascii.Ltr_q: case Byte_ascii.Ltr_r: case Byte_ascii.Ltr_s: case Byte_ascii.Ltr_t: + case Byte_ascii.Ltr_u: case Byte_ascii.Ltr_v: case Byte_ascii.Ltr_w: case Byte_ascii.Ltr_x: case Byte_ascii.Ltr_y: case Byte_ascii.Ltr_z: + case Byte_ascii.Underline: + return true; + // number is only valid if !is_first + case Byte_ascii.Num_0: case Byte_ascii.Num_1: case Byte_ascii.Num_2: case Byte_ascii.Num_3: case Byte_ascii.Num_4: + case Byte_ascii.Num_5: case Byte_ascii.Num_6: case Byte_ascii.Num_7: case Byte_ascii.Num_8: case Byte_ascii.Num_9: + return !is_first; + default: + // \x80-\xff is always true; + return b >= 128 && b <= 255; + } + } } diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpString__tst.java b/400_xowa/src/gplx/xowa/mediawiki/XophpString__tst.java new file mode 100644 index 000000000..64ccde268 --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/XophpString__tst.java @@ -0,0 +1,177 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; +import org.junit.*; import gplx.core.tests.*; import gplx.core.btries.*; +public class XophpString__tst { + private final XophpString__fxt fxt = new XophpString__fxt(); + @Test public void Strspn_fwd__byte() { + fxt.Test_strspn_fwd__byte("aaaaab", Byte_ascii.Ltr_a, 0, -1, 5); // basic + fxt.Test_strspn_fwd__byte("aaaaab", Byte_ascii.Ltr_a, 1, -1, 4); // bgn + fxt.Test_strspn_fwd__byte("aaaaab", Byte_ascii.Ltr_a, 1, 2, 2); // max + } + @Test public void Strspn_fwd__space_or_tab() { + fxt.Test_strspn_fwd__space_or_tab(" a", 0, -1, 5); // basic + fxt.Test_strspn_fwd__space_or_tab(" a", 1, -1, 4); // bgn + fxt.Test_strspn_fwd__space_or_tab(" a", 1, 2, 2); // max + } + @Test public void Strspn_bwd__byte() { + fxt.Test_strspn_bwd__byte("aaaaab", Byte_ascii.Ltr_a, 5, -1, 5); // basic + fxt.Test_strspn_bwd__byte("aaaaab", Byte_ascii.Ltr_a, 4, -1, 4); // bgn + fxt.Test_strspn_bwd__byte("aaaaab", Byte_ascii.Ltr_a, 4, 2, 2); // max + } + @Test public void Strspn_bwd__space_or_tab() { + fxt.Test_strspn_bwd__space_or_tab(" a", 5, -1, 5); // basic + fxt.Test_strspn_bwd__space_or_tab(" a", 4, -1, 4); // bgn + fxt.Test_strspn_bwd__space_or_tab(" a", 4, 2, 2); // max + } + @Test public void Substr__bgn_is_neg() { + fxt.Test_substr("abcde" , -1, "e"); + fxt.Test_substr("abcde" , -3, -1, "cd"); + } + @Test public void Strtr() { + fxt.Init_strtr_by_trie("01", "89", "02", "79"); + fxt.Test_strtr_by_trie("abc" , "abc"); // found=none + fxt.Test_strtr_by_trie("ab_01_cd" , "ab_89_cd"); // found=one + fxt.Test_strtr_by_trie("ab_01_cd_02_ef", "ab_89_cd_79_ef"); // found=many + fxt.Test_strtr_by_trie("01_ab" , "89_ab"); // BOS + fxt.Test_strtr_by_trie("ab_01" , "ab_89"); // EOS + } + @Test public void Str_repeat() { + fxt.Test_str_repeat("abc", 3, "abcabcabc"); + fxt.Test_str_repeat("", 3, ""); + fxt.Test_str_repeat("abc", 0, ""); + } + @Test public void Strpos() { + fxt.Test__strpos("abc", "b", 0, 1); + fxt.Test__strpos("abc", "z", 0, XophpInt_.False); + fxt.Test__strpos("aba", "a", 1, 2); + fxt.Test__strpos("aba", "a", -2, 2); + } + @Test public void strspn() { + fxt.Test__strspn("42 is the answer to the 128th question", fxt.Init__strspn_hash("1234567890"), 0, Int_.Min_value, 2); + fxt.Test__strspn("foo", fxt.Init__strspn_hash("o"), 0, Int_.Min_value, 0); + fxt.Test__strspn("foo", fxt.Init__strspn_hash("o"), 1, 2, 2); + fxt.Test__strspn("foo", fxt.Init__strspn_hash("o"), 1, 1, 1); + } + @Test public void rtrim() { + // pad is 0, 1 char + fxt.Test__rtrim("0100", "", "0100"); // empty pad returns String + fxt.Test__rtrim("010", "0", "01"); // basic test; trim 1; + fxt.Test__rtrim("0100", "0", "01"); // basic test; trim 2; + fxt.Test__rtrim("01", "0", "01"); // nothing to trim + + // pad is 2+char + fxt.Test__rtrim("10ab10", "01", "10ab"); // basic test + fxt.Test__rtrim("10ab10", "34", "10ab10"); // nothing to trim + fxt.Test__rtrim("10ab10", "010", "10ab"); // don't fail if repeated chars + + // pad has .. + fxt.Test__rtrim("23ab23", "0..4", "23ab"); // basic test + fxt.Test__rtrim("23ab23.", "0.4", "23ab23"); // single dot is not range + fxt.Test__rtrim__fail("abc", "0..", ".. found but at end of String"); + fxt.Test__rtrim__fail("abc", "..0", ".. found but at start of String"); + fxt.Test__rtrim__fail("abc", "4..0", ".. found but next byte must be greater than previous byte"); + + // PHP samples + fxt.Test__rtrim("\t\tThese are a few words :) ... ", " \t.", "\t\tThese are a few words :)"); + fxt.Test__rtrim("Hello World", "Hdle", "Hello Wor"); + fxt.Test__rtrim("\u0009Example String\n", "\u0000..\u001F", "\u0009Example String"); + + // non breaking-space is "\xA0" or "\xC2\xA0" in utf-8, "µ" is "\xB5" or "\xC2\xB5" in utf-8 and "à" is "\xE0" or "\xC3\xA0" in utf-8 + // REF.MW:https://www.php.net/manual/en/function.trim.php + fxt.Test__rtrim("\u00A0µ déjà\u00A0", "\u00A0", "\u00A0µ déj�");// NOTE: technically should be "...j\xC3", but String_.new_u8 ignores invalid bytes + } + @Test public void ord() { + fxt.Test__ord("a", 97); // 1 char + fxt.Test__ord("abc", 97); // 2+ chars takes first + fxt.Test__ord("", 0); // no chars returns 0 + fxt.Test__ord(null, 0); // null returns 0 + } + @Test public void Fmt() { + fxt.Test__Fmt("a", "a"); // no keys + fxt.Test__Fmt("a$", "a$"); // key at end + fxt.Test__Fmt("ax", "a${x}", "x"); // curly + fxt.Test__Fmt("ax", "a$x", "x"); // basic + fxt.Test__Fmt("axyz", "a$xb$yc$zd", "x", "y", "z"); // multiple + fxt.Test__Fmt("a$0b", "a$0b", "z"); // invalid identifier + fxt.Test__Fmt("a0", "a$xyz0b", "0"); // long identifier + } +} +class XophpString__fxt { + public void Test_strspn_fwd__byte(String src_str, byte find, int bgn, int max, int expd) { + byte[] src_bry = Bry_.new_u8(src_str); + Gftest.Eq__int(expd, XophpString_.strspn_fwd__byte(src_bry, find, bgn, max, src_bry.length)); + } + public void Test_strspn_fwd__space_or_tab(String src_str, int bgn, int max, int expd) { + byte[] src_bry = Bry_.new_u8(src_str); + Gftest.Eq__int(expd, XophpString_.strspn_fwd__space_or_tab(src_bry, bgn, max, src_bry.length)); + } + public void Test_strspn_bwd__byte(String src_str, byte find, int bgn, int max, int expd) { + Gftest.Eq__int(expd, XophpString_.strspn_bwd__byte(Bry_.new_u8(src_str), find, bgn, max)); + } + public void Test_strspn_bwd__space_or_tab(String src_str, int bgn, int max, int expd) { + Gftest.Eq__int(expd, XophpString_.strspn_bwd__space_or_tab(Bry_.new_u8(src_str), bgn, max)); + } + public void Test_substr(String src_str, int bgn, String expd) {Test_substr(src_str, bgn, String_.Len(src_str), expd);} + public void Test_substr(String src_str, int bgn, int len, String expd) { + Gftest.Eq__str(expd, XophpString_.substr(Bry_.new_u8(src_str), bgn, len)); + } + private Btrie_slim_mgr strtr_trie; + public void Init_strtr_by_trie(String... kvs) { + if (strtr_trie == null) strtr_trie = Btrie_slim_mgr.cs(); + int len = kvs.length; + for (int i = 0; i < len; i += 2) { + strtr_trie.Add_str_str(kvs[i], kvs[i + 1]); + } + } + public void Test_strtr_by_trie(String src, String expd) { + Bry_bfr tmp = Bry_bfr_.New(); + Btrie_rv trv = new Btrie_rv(); + Gftest.Eq__str(expd, XophpString_.strtr(Bry_.new_u8(src), strtr_trie, tmp, trv)); + } + public void Test_str_repeat(String str, int count, String expd) { + Gftest.Eq__str(expd, XophpString_.str_repeat(str, count)); + } + public void Test__strpos(String haystack, String needle, int offset, int expd) { + Gftest.Eq__int(expd, XophpString_.strpos(haystack, needle, offset)); + } + public Hash_adp Init__strspn_hash(String mask) {return XophpString_.strspn_hash(mask);} + public void Test__strspn(String subject, Hash_adp mask, int start, int length, int expd) { + int actl = XophpString_.strspn(subject, mask, start, length); + Gftest.Eq__int(expd, actl); + } + public void Test__rtrim(String str, String character_mask, String expd) { + Gftest.Eq__str(expd, XophpString_.rtrim(str, character_mask)); + } + public void Test__rtrim__fail(String str, String character_mask, String expd_exc) { + try { + XophpString_.rtrim(str, character_mask); + } catch (Exception exc) { + String actl_exc = Err_.Message_lang(exc); + if (!String_.Has(actl_exc, expd_exc)) { + Gftest.Fail("expected failure, but got this: " + actl_exc); + } + return; + } + Gftest.Fail("expected failure, but got none: " + character_mask); + } + public void Test__ord(String str, int expd) { + Gftest.Eq__int(expd, XophpString_.ord(str)); + } + public void Test__Fmt(String expd, String fmt, Object... args) { + Gftest.Eq__str(expd, XophpString_.Fmt(fmt, args)); + } +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/XophpString_tst.java b/400_xowa/src/gplx/xowa/mediawiki/XophpString_tst.java deleted file mode 100644 index 5aa6f9aa9..000000000 --- a/400_xowa/src/gplx/xowa/mediawiki/XophpString_tst.java +++ /dev/null @@ -1,93 +0,0 @@ -/* -XOWA: the XOWA Offline Wiki Application -Copyright (C) 2012-2017 gnosygnu@gmail.com - -XOWA is licensed under the terms of the General Public License (GPL) Version 3, -or alternatively under the terms of the Apache License Version 2.0. - -You may use XOWA according to either of these licenses as is most appropriate -for your project on a case-by-case basis. - -The terms of each license can be found in the source code repository: - -GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt -Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt -*/ -package gplx.xowa.mediawiki; import gplx.*; import gplx.xowa.*; -import org.junit.*; import gplx.core.tests.*; import gplx.core.btries.*; -public class XophpString_tst { - private final XophpString_fxt fxt = new XophpString_fxt(); - @Test public void Strspn_fwd__byte() { - fxt.Test_strspn_fwd__byte("aaaaab", Byte_ascii.Ltr_a, 0, -1, 5); // basic - fxt.Test_strspn_fwd__byte("aaaaab", Byte_ascii.Ltr_a, 1, -1, 4); // bgn - fxt.Test_strspn_fwd__byte("aaaaab", Byte_ascii.Ltr_a, 1, 2, 2); // max - } - @Test public void Strspn_fwd__space_or_tab() { - fxt.Test_strspn_fwd__space_or_tab(" a", 0, -1, 5); // basic - fxt.Test_strspn_fwd__space_or_tab(" a", 1, -1, 4); // bgn - fxt.Test_strspn_fwd__space_or_tab(" a", 1, 2, 2); // max - } - @Test public void Strspn_bwd__byte() { - fxt.Test_strspn_bwd__byte("aaaaab", Byte_ascii.Ltr_a, 5, -1, 5); // basic - fxt.Test_strspn_bwd__byte("aaaaab", Byte_ascii.Ltr_a, 4, -1, 4); // bgn - fxt.Test_strspn_bwd__byte("aaaaab", Byte_ascii.Ltr_a, 4, 2, 2); // max - } - @Test public void Strspn_bwd__space_or_tab() { - fxt.Test_strspn_bwd__space_or_tab(" a", 5, -1, 5); // basic - fxt.Test_strspn_bwd__space_or_tab(" a", 4, -1, 4); // bgn - fxt.Test_strspn_bwd__space_or_tab(" a", 4, 2, 2); // max - } - @Test public void Substr__bgn_is_neg() { - fxt.Test_substr("abcde" , -1, "e"); - fxt.Test_substr("abcde" , -3, -1, "cd"); - } - @Test public void Strtr() { - fxt.Init_strtr_by_trie("01", "89", "02", "79"); - fxt.Test_strtr_by_trie("abc" , "abc"); // found=none - fxt.Test_strtr_by_trie("ab_01_cd" , "ab_89_cd"); // found=one - fxt.Test_strtr_by_trie("ab_01_cd_02_ef", "ab_89_cd_79_ef"); // found=many - fxt.Test_strtr_by_trie("01_ab" , "89_ab"); // BOS - fxt.Test_strtr_by_trie("ab_01" , "ab_89"); // EOS - } - @Test public void Str_repeat() { - fxt.Test_str_repeat("abc", 3, "abcabcabc"); - fxt.Test_str_repeat("", 3, ""); - fxt.Test_str_repeat("abc", 0, ""); - } -} -class XophpString_fxt { - public void Test_strspn_fwd__byte(String src_str, byte find, int bgn, int max, int expd) { - byte[] src_bry = Bry_.new_u8(src_str); - Gftest.Eq__int(expd, XophpString.strspn_fwd__byte(src_bry, find, bgn, max, src_bry.length)); - } - public void Test_strspn_fwd__space_or_tab(String src_str, int bgn, int max, int expd) { - byte[] src_bry = Bry_.new_u8(src_str); - Gftest.Eq__int(expd, XophpString.strspn_fwd__space_or_tab(src_bry, bgn, max, src_bry.length)); - } - public void Test_strspn_bwd__byte(String src_str, byte find, int bgn, int max, int expd) { - Gftest.Eq__int(expd, XophpString.strspn_bwd__byte(Bry_.new_u8(src_str), find, bgn, max)); - } - public void Test_strspn_bwd__space_or_tab(String src_str, int bgn, int max, int expd) { - Gftest.Eq__int(expd, XophpString.strspn_bwd__space_or_tab(Bry_.new_u8(src_str), bgn, max)); - } - public void Test_substr(String src_str, int bgn, String expd) {Test_substr(src_str, bgn, String_.Len(src_str), expd);} - public void Test_substr(String src_str, int bgn, int len, String expd) { - Gftest.Eq__str(expd, XophpString.substr(Bry_.new_u8(src_str), bgn, len)); - } - private Btrie_slim_mgr strtr_trie; - public void Init_strtr_by_trie(String... kvs) { - if (strtr_trie == null) strtr_trie = Btrie_slim_mgr.cs(); - int len = kvs.length; - for (int i = 0; i < len; i += 2) { - strtr_trie.Add_str_str(kvs[i], kvs[i + 1]); - } - } - public void Test_strtr_by_trie(String src, String expd) { - Bry_bfr tmp = Bry_bfr_.New(); - Btrie_rv trv = new Btrie_rv(); - Gftest.Eq__str(expd, XophpString.strtr(Bry_.new_u8(src), strtr_trie, tmp, trv)); - } - public void Test_str_repeat(String str, int count, String expd) { - Gftest.Eq__str(expd, XophpString.str_repeat(str, count)); - } -} diff --git a/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/client/includes/Wbase_repo_linker.java b/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/client/includes/Wbase_repo_linker.java index 481e73a0b..6936c7cd7 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/client/includes/Wbase_repo_linker.java +++ b/400_xowa/src/gplx/xowa/mediawiki/extensions/Wikibase/client/includes/Wbase_repo_linker.java @@ -28,7 +28,7 @@ public class Wbase_repo_linker { public byte[] getPageUrl(byte[] page) { byte[] encodedPage = this.encodePage(page); - return Bry_.Add(this.getBaseUrl(), XophpString.str_replace(Format_Arg1, encodedPage, this.articlePath)); + return Bry_.Add(this.getBaseUrl(), XophpString_.str_replace(Format_Arg1, encodedPage, this.articlePath)); } private byte[] encodePage(byte[] page) { diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/XomwHtml.java b/400_xowa/src/gplx/xowa/mediawiki/includes/XomwHtml.java index 231bd7f4e..a363d4aaf 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/includes/XomwHtml.java +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/XomwHtml.java @@ -519,7 +519,7 @@ public class XomwHtml { } else { // PORTED.HEADER:atrValEncodings - val = XophpString.strtr(val, atrValEncodings, temp.bfr, temp.trv); + val = XophpString_.strtr(val, atrValEncodings, temp.bfr, temp.trv); bfr.Add_byte_space().Add(key).Add(ATR_VAL_QUOTE).Add(val).Add_byte_quote(); } } diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/XomwMessage.java b/400_xowa/src/gplx/xowa/mediawiki/includes/XomwMessage.java index 648e81a0e..257f96f42 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/includes/XomwMessage.java +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/XomwMessage.java @@ -838,7 +838,7 @@ public class XomwMessage { } // Replace $* with a list of parameters for &uselang=qqx. -// if (XophpString.strpos(s, "$*") != false) { +// if (XophpString_.strpos(s, "$*") != false) { // String paramlist = ""; // if (this.parameters != []) { // paramlist = ": $" . implode(", $", range(1, count(this.parameters))); diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/XomwTitle.java b/400_xowa/src/gplx/xowa/mediawiki/includes/XomwTitle.java index 012462711..0b7be2160 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/includes/XomwTitle.java +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/XomwTitle.java @@ -309,7 +309,7 @@ public class XomwTitle { byte[] filteredText = text; XomwTitle t = new XomwTitle(env); - t.mDbkeyform = XophpString.strtr(filteredText, Byte_ascii.Space, Byte_ascii.Underline); + t.mDbkeyform = XophpString_.strtr(filteredText, Byte_ascii.Space, Byte_ascii.Underline); t.mDefaultNamespace = defaultNamespace; t.secureAndSplit(env); @@ -1435,7 +1435,7 @@ public class XomwTitle { */ public byte[] getPrefixedDBkey() { byte[] s = this.prefix(this.mDbkeyform); - s = XophpString.strtr(s, Byte_ascii.Space, Byte_ascii.Underline); + s = XophpString_.strtr(s, Byte_ascii.Space, Byte_ascii.Underline); return s; } public String getPrefixedDBkeyStr() {return String_.new_u8(getPrefixedDBkey());} @@ -1449,7 +1449,7 @@ public class XomwTitle { public byte[] getPrefixedText() { if (this.mPrefixedText == null) { byte[] s = this.prefix(this.mTextform); - s = XophpString.strtr(s, Byte_ascii.Underline, Byte_ascii.Space); + s = XophpString_.strtr(s, Byte_ascii.Underline, Byte_ascii.Space); this.mPrefixedText = s; } return this.mPrefixedText; @@ -3380,7 +3380,7 @@ public class XomwTitle { this.mDbkeyform = parts.dbkey; this.mUrlform = XomwGlobalFunctions.wfUrlencode(this.mDbkeyform); - this.mTextform = XophpString.strtr(this.mDbkeyform, Byte_ascii.Underline, Byte_ascii.Space); + this.mTextform = XophpString_.strtr(this.mDbkeyform, Byte_ascii.Underline, Byte_ascii.Space); // We already know that some pages won't be in the database! if (this.isExternal() || this.mNamespace == XomwDefines.NS_SPECIAL) { diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/cache/XomwCacheDependency.java b/400_xowa/src/gplx/xowa/mediawiki/includes/cache/XomwCacheDependency.java new file mode 100644 index 000000000..af6051c3e --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/cache/XomwCacheDependency.java @@ -0,0 +1,286 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki.includes.cache; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.includes.*; +// /** +// * This class stores an arbitrary value along with its dependencies. +// * Users should typically only use DependencyWrapper::getValueFromCache(), +// * rather than instantiating one of these objects directly. +// * @ingroup Cache +// */ +// class DependencyWrapper { +// private $value; +// /** @var CacheDependency[] */ +// private $deps; +// +// /** +// * Create an instance. +// * @param mixed $value The user-supplied value +// * @param CacheDependency|CacheDependency[] $deps A dependency or dependency +// * array. All dependencies must be objects implementing CacheDependency. +// */ +// function __construct( $value = false, $deps = [] ) { +// $this->value = $value; +// +// if ( !is_array( $deps ) ) { +// $deps = [ $deps ]; +// } +// +// $this->deps = $deps; +// } +// +// /** +// * Returns true if any of the dependencies have expired +// * +// * @return boolean +// */ +// function isExpired() { +// foreach ( $this->deps as $dep ) { +// if ( $dep->isExpired() ) { +// return true; +// } +// } +// +// return false; +// } +// +// /** +// * Initialise dependency values in preparation for storing. This must be +// * called before serialization. +// */ +// function initialiseDeps() { +// foreach ( $this->deps as $dep ) { +// $dep->loadDependencyValues(); +// } +// } +// +// /** +// * Get the user-defined value +// * @return boolean|mixed +// */ +// function getValue() { +// return $this->value; +// } +// +// /** +// * Store the wrapper to a cache +// * +// * @param BagOStuff $cache +// * @param String $key +// * @param int $expiry +// */ +// function storeToCache( $cache, $key, $expiry = 0 ) { +// $this->initialiseDeps(); +// $cache->set( $key, $this, $expiry ); +// } +// +// /** +// * Attempt to get a value from the cache. If the value is expired or missing, +// * it will be generated with the callback function (if present), and the newly +// * calculated value will be stored to the cache in a wrapper. +// * +// * @param BagOStuff $cache A cache Object +// * @param String $key The cache key +// * @param int $expiry The expiry timestamp or interval in seconds +// * @param boolean|callable $callback The callback for generating the value, or false +// * @param array $callbackParams The function parameters for the callback +// * @param array $deps The dependencies to store on a cache miss. Note: these +// * are not the dependencies used on a cache hit! Cache hits use the stored +// * dependency array. +// * +// * @return mixed The value, or null if it was not present in the cache and no +// * callback was defined. +// */ +// static function getValueFromCache( $cache, $key, $expiry = 0, $callback = false, +// $callbackParams = [], $deps = [] +// ) { +// $obj = $cache->get( $key ); +// +// if ( is_object( $obj ) && $obj instanceof DependencyWrapper && !$obj->isExpired() ) { +// $value = $obj->value; +// } elseif ( $callback ) { +// $value = call_user_func_array( $callback, $callbackParams ); +// # Cache the newly-generated value +// $wrapper = new DependencyWrapper( $value, $deps ); +// $wrapper->storeToCache( $cache, $key, $expiry ); +// } else { +// $value = null; +// } +// +// return $value; +// } +// } +// +// /** +// * @ingroup Cache +// */ +// abstract class CacheDependency { +// /** +// * Returns true if the dependency is expired, false otherwise +// */ +// abstract function isExpired(); +// +// /** +// * Hook to perform any expensive pre-serialize loading of dependency values. +// */ +// function loadDependencyValues() { +// } +// } +// +// /** +// * @ingroup Cache +// */ +// class FileDependency extends CacheDependency { +// private $filename; +// private $timestamp; +// +// /** +// * Create a file dependency +// * +// * @param String $filename The name of the file, preferably fully qualified +// * @param null|boolean|int $timestamp The unix last modified timestamp, or false if the +// * file does not exist. If omitted, the timestamp will be loaded from +// * the file. +// * +// * A dependency on a nonexistent file will be triggered when the file is +// * created. A dependency on an existing file will be triggered when the +// * file is changed. +// */ +// function __construct( $filename, $timestamp = null ) { +// $this->filename = $filename; +// $this->timestamp = $timestamp; +// } +// +// /** +// * @return array +// */ +// function __sleep() { +// $this->loadDependencyValues(); +// +// return [ 'filename', 'timestamp' ]; +// } +// +// function loadDependencyValues() { +// if ( is_null( $this->timestamp ) ) { +// MediaWiki\suppressWarnings(); +// # Dependency on a non-existent file stores "false" +// # This is a valid concept! +// $this->timestamp = filemtime( $this->filename ); +// MediaWiki\restoreWarnings(); +// } +// } +// +// /** +// * @return boolean +// */ +// function isExpired() { +// MediaWiki\suppressWarnings(); +// $lastmod = filemtime( $this->filename ); +// MediaWiki\restoreWarnings(); +// if ( $lastmod === false ) { +// if ( $this->timestamp === false ) { +// # Still nonexistent +// return false; +// } else { +// # Deleted +// wfDebug( "Dependency triggered: {$this->filename} deleted.\n" ); +// +// return true; +// } +// } else { +// if ( $lastmod > $this->timestamp ) { +// # Modified or created +// wfDebug( "Dependency triggered: {$this->filename} changed.\n" ); +// +// return true; +// } else { +// # Not modified +// return false; +// } +// } +// } +// } +// +// /** +// * @ingroup Cache +// */ +// class GlobalDependency extends CacheDependency { +// private $name; +// private $value; +// +// function __construct( $name ) { +// $this->name = $name; +// $this->value = $GLOBALS[$name]; +// } +// +// /** +// * @return boolean +// */ +// function isExpired() { +// if ( !isset( $GLOBALS[$this->name] ) ) { +// return true; +// } +// +// return $GLOBALS[$this->name] != $this->value; +// } +// } +// +// /** +// * @ingroup Cache +// */ +// class MainConfigDependency extends CacheDependency { +// private $name; +// private $value; +// +// function __construct( $name ) { +// $this->name = $name; +// $this->value = $this->getConfig()->get( $this->name ); +// } +// +// private function getConfig() { +// return MediaWikiServices::getInstance()->getMainConfig(); +// } +// +// /** +// * @return boolean +// */ +// function isExpired() { +// if ( !$this->getConfig()->has( $this->name ) ) { +// return true; +// } +// +// return $this->getConfig()->get( $this->name ) != $this->value; +// } +// } +// +// /** +// * @ingroup Cache +// */ +// class ConstantDependency extends CacheDependency { +// private $name; +// private $value; +// +// function __construct( $name ) { +// $this->name = $name; +// $this->value = constant( $name ); +// } +// +// /** +// * @return boolean +// */ +// function isExpired() { +// return constant( $this->name ) != $this->value; +// } +// } diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/cache/localisation/XomwLCStore.java b/400_xowa/src/gplx/xowa/mediawiki/includes/cache/localisation/XomwLCStore.java new file mode 100644 index 000000000..85a06cb39 --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/cache/localisation/XomwLCStore.java @@ -0,0 +1,62 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki.includes.cache.localisation; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.includes.*; import gplx.xowa.mediawiki.includes.cache.*; +// MW.SRC:1.33 +/** +* Interface for the persistence layer of LocalisationCache. +* +* The persistence layer is two-level hierarchical cache. The first level +* is the language, the second level is the item or subitem. +* +* Since the data for a whole language is rebuilt in one operation, it needs +* to have a fast and atomic method for deleting or replacing all of the +* current data for a given language. The interface reflects this bulk update +* operation. Callers writing to the cache must first call startWrite(), then +* will call set() a couple of thousand times, then will call finishWrite() +* to commit the operation. When finishWrite() is called, the cache is +* expected to delete all data previously stored for that language. +* +* The values stored are PHP variables suitable for serialize(). Implementations +* of LCStore are responsible for serializing and unserializing. +*/ +interface XomwLCStore { + + /** + * Get a value. + * @param String code Language code + * @param String key Cache key + */ + Object get(String code, String key); + + /** + * Start a write transaction. + * @param String code Language code + */ + void startWrite(String code); + + /** + * Finish a write transaction. + */ + void finishWrite(); + + /** + * Set a key to a given value. startWrite() must be called before this + * is called, and finishWrite() must be called afterwards. + * @param String key + * @param mixed value + */ + void set(String key, String value); +} \ No newline at end of file diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/cache/localisation/XomwLCStoreNull.java b/400_xowa/src/gplx/xowa/mediawiki/includes/cache/localisation/XomwLCStoreNull.java new file mode 100644 index 000000000..2b8567c68 --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/cache/localisation/XomwLCStoreNull.java @@ -0,0 +1,35 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki.includes.cache.localisation; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.includes.*; import gplx.xowa.mediawiki.includes.cache.*; +// MW.SRC:1.33 +/** +* Null store backend, used to avoid DB errors during install +*/ +class XomwLCStoreNull implements XomwLCStore { + public Object get(String code, String key) { + return null; + } + + public void startWrite(String code) { + } + + public void finishWrite() { + } + + public void set(String key, String value) { + } + +} \ No newline at end of file diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/cache/localisation/XomwLocalisationCache.java b/400_xowa/src/gplx/xowa/mediawiki/includes/cache/localisation/XomwLocalisationCache.java new file mode 100644 index 000000000..3b3bc07fc --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/cache/localisation/XomwLocalisationCache.java @@ -0,0 +1,1094 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki.includes.cache.localisation; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.includes.*; import gplx.xowa.mediawiki.includes.cache.*; +// MW.SRC:1.33 +import gplx.xowa.mediawiki.xml.*; +import gplx.xowa.mediawiki.vendor.wikimedia.cldr_plural_rule_parser.src.*; +public class XomwLocalisationCache { +// static final VERSION = 4; +// +// /** Configuration associative array */ +// private conf; +// +// /** +// * True if recaching should only be done on an explicit call to recache(). +// * Setting this reduces the overhead of cache freshness checking, which +// * requires doing a stat() for every extension i18n file. +// */ +// private manualRecache = false; +// +// /** +// * True to treat all files as expired until they are regenerated by this Object. +// */ +// private forceRecache = false; + + /** + * The cache data. 3-d array, where the first key is the language code, + * the second key is the item key e.g. 'messages', and the third key is + * an item specific subkey index. Some items are not arrays and so for those + * items, there are no subkeys. + */ + protected XophpArray data = XophpArray.New(); + + /** + * The persistent store Object. An instance of LCStore. + * + * @var LCStore + */ +// private XomwLCStore store = new XomwLCStoreNull(); + + /** + * A 2-d associative array, code/key, where presence indicates that the item + * is loaded. Value arbitrary. + * + * For split items, if set, this indicates that all of the subitems have been + * loaded. + */ + private XophpArray loadedItems = XophpArray.New(); + + /** + * A 3-d associative array, code/key/subkey, where presence indicates that + * the subitem is loaded. Only used for the split items, i.e. messages. + */ +// private XophpArray loadedSubitems = XophpArray.New(); + + /** + * An array where presence of a key indicates that that language has been + * initialised. Initialisation includes checking for cache expiry and doing + * any necessary updates. + */ +// private XophpArray initialisedLangs = XophpArray.New(); + + /** + * An array mapping non-existent pseudo-languages to fallback languages. This + * is filled by initShallowFallback() when data is requested from a language + * that lacks a Messages*.php file. + */ + private XophpArray shallowFallbacks = XophpArray.New(); +// +// /** +// * An array where the keys are codes that have been recached by this instance. +// */ +// private recachedLangs = []; +// +// /** +// * All item keys +// */ +// public static allKeys = [ +// 'fallback', 'name+spaceNames', 'bookstoreList', +// 'magicWords', 'messages', 'rtl', 'capitalizeAllNouns', 'digitTransformTable', +// 'separatorTransformTable', 'minimumGroupingDigits', +// 'fallback8bitEncoding', 'linkPrefixExtension', +// 'linkTrail', 'linkPrefixCharset', 'name+spaceAliases', +// 'dateFormats', 'datePreferences', 'datePreferenceMigrationMap', +// 'defaultDateFormat', 'extraUserToggles', 'specialPageAliases', +// 'imageFiles', 'preloadedMessages', 'name+spaceGenderAliases', +// 'digitGroupingPattern', 'pluralRules', 'pluralRuleTypes', 'compiledPluralRules', +// ]; +// +// /** +// * Keys for items which consist of associative arrays, which may be merged +// * by a fallback sequence. +// */ +// public static mergeableMapKeys = [ 'messages', 'name+spaceNames', +// 'name+spaceAliases', 'dateFormats', 'imageFiles', 'preloadedMessages' +// ]; +// +// /** +// * Keys for items which are a numbered array. +// */ +// public static mergeableListKeys = [ 'extraUserToggles' ]; +// +// /** +// * Keys for items which contain an array of arrays of equivalent aliases +// * for each subitem. The aliases may be merged by a fallback sequence. +// */ +// public static mergeableAliasListKeys = [ 'specialPageAliases' ]; +// +// /** +// * Keys for items which contain an associative array, and may be merged if +// * the primary value contains the special array key "inherit". That array +// * key is removed after the first merge. +// */ +// public static optionalMergeKeys = [ 'bookstoreList' ]; +// +// /** +// * Keys for items that are formatted like magicWords +// */ +// public static magicWordKeys = [ 'magicWords' ]; + + /** + * Keys for items where the subitems are stored in the backend separately. + */ + public static XophpArray splitKeys = XophpArray.New().Add("messages"); + +// /** +// * Keys which are loaded automatically by initLanguage() +// */ +// public static preloadedKeys = [ 'dateFormats', 'name+spaceNames' ]; + + /** + * Associative array of cached plural rules. The key is the language code, + * the value is an array of plural rules for that language. + */ + protected XophpArray pluralRules = XophpArray.New(); + + /** + * Associative array of cached plural rule types. The key is the language + * code, the value is an array of plural rule types for that language. For + * example, pluralRuleTypes['ar'] = ['zero', 'one', 'two', 'few', 'many']. + * The index for each rule type matches the index for the rule in + * pluralRules, thus allowing correlation between the two. The reason we + * don't just use the type names as the keys in pluralRules is because + * XomwLanguage.convertPlural applies the rules based on numeric order (or + * explicit numeric parameter), not based on the name of the rule type. For + * example, {{plural:count|wordform1|wordform2|wordform3}}, rather than + * {{plural:count|one=wordform1|two=wordform2|many=wordform3}}. + */ + private XophpArray pluralRuleTypes = XophpArray.New(); + +// private mergeableKeys = null; +// +// /** +// * For constructor parameters, see the documentation in DefaultSettings.php +// * for wgLocalisationCacheConf. +// * +// * @param array conf +// * @throws MWException +// */ +// function __construct(conf) { +// global wgCacheDirectory; +// +// this.conf = conf; +// storeConf = []; +// if (!empty(conf['storecl+ass'])) { +// storecl+ass = conf['storecl+ass']; +// } else { +// switch (conf['store']) { +// case 'files': +// case 'file': +// storecl+ass = LCStoreCDB::cl+ass; +// break; +// case 'db': +// storecl+ass = LCStoreDB::cl+ass; +// storeConf['server'] = conf['storeServer'] ?? []; +// break; +// case 'array': +// storecl+ass = LCStoreStaticArray::cl+ass; +// break; +// case 'detect': +// if (!empty(conf['storeDirectory'])) { +// storecl+ass = LCStoreCDB::cl+ass; +// } elseif (wgCacheDirectory) { +// storeConf['directory'] = wgCacheDirectory; +// storecl+ass = LCStoreCDB::cl+ass; +// } else { +// storecl+ass = LCStoreDB::cl+ass; +// storeConf['server'] = conf['storeServer'] ?? []; +// } +// break; +// default: +// throw new MWException( +// 'Please set wgLocalisationCacheConf[\'store\'] to something sensible.' +// ); +// } +// } +// +// wfDebugLog('caches', static::cl+ass . ": using store storecl+ass"); +// if (!empty(conf['storeDirectory'])) { +// storeConf['directory'] = conf['storeDirectory']; +// } +// +// this.store = new storecl+ass(storeConf); +// foreach ([ 'manualRecache', 'forceRecache' ] as var) { +// if (isset(conf[var])) { +// this.var = conf[var]; +// } +// } +// } +// +// /** +// * Returns true if the given key is mergeable, that is, if it is an associative +// * array which can be merged through a fallback sequence. +// * @param String key +// * @return boolean +// */ +// public function isMergeableKey(key) { +// if (this.mergeableKeys === null) { +// this.mergeableKeys = array_flip(array_merge( +// XomwLocalisationCache.mergeableMapKeys, +// XomwLocalisationCache.mergeableListKeys, +// XomwLocalisationCache.mergeableAliasListKeys, +// XomwLocalisationCache.optionalMergeKeys, +// XomwLocalisationCache.magicWordKeys +// )); +// } +// +// return isset(this.mergeableKeys[key]); +// } + + /** + * Get a cache item. + * + * Warning: this may be slow for split items (messages), since it will + * need to fetch all of the subitems from the cache individually. + * @param String code + * @param String key + * @return mixed + */ + public Object getItem(String code, String key) { + if (!this.loadedItems.Get_by_ary(code).isset(key)) { +// this.loadItem(code, key); + } + + if (String_.Eq(key, "fallback") && this.shallowFallbacks.isset(code)) { + return this.shallowFallbacks.Get_by(code); + } + + return this.data.Get_by_ary(code).Get_by(key); + } + + /** + * Get a subitem, for instance a single message for a given language. + * @param String code + * @param String key + * @param String subkey + * @return mixed|null + */ +// public XophpArray getSubitem(String code, String key, String subkey) { +// if (!this.loadedSubitems.Get_by_ary(code).Get_by_ary(key).isset(subkey) && +// !this.loadedItems.Get_by_ary(code).isset(key) +// ) { +// this.loadSubitem(code, key, subkey); +// } +// +// return this.data.Get_by_ary(code).Get_by_ary(key).Get_by_ary(subkey); +// } +// +// /** +// * Get the list of subitem keys for a given item. +// * +// * This is faster than array_keys(lc.getItem(...)) for the items listed in +// * XomwLocalisationCache.splitKeys. +// * +// * Will return null if the item is not found, or false if the item is not an +// * array. +// * @param String code +// * @param String key +// * @return boolean|null|String|String[] +// */ +// public function getSubitemList(code, key) { +// if (in_array(key, XomwLocalisationCache.splitKeys)) { +// return this.getSubitem(code, 'list', key); +// } else { +// item = this.getItem(code, key); +// if (is_array(item)) { +// return array_keys(item); +// } else { +// return false; +// } +// } +// } + + /** + * Load an item into the cache. + * @param String code + * @param String key + */ +// protected void loadItem(String code, String key) { +// if (!this.initialisedLangs.isset(code)) { +// this.initLanguage(code); +// } +// +// // Check to see if initLanguage() loaded it for us +// if (this.loadedItems.Get_by_ary(code).Get_by_bool(key)) { +// return; +// } +// +// if (this.shallowFallbacks.isset(code)) { +// this.loadItem(this.shallowFallbacks.Get_by_str(code), key); +// +// return; +// } +// +// if (XomwLocalisationCache.splitKeys.in_array(key)) { +// XophpArray subkeyList = this.getSubitem(code, "list", key); +// int subkeyListLen = subkeyList.Len(); +// for (int i = 0; i < subkeyListLen; i++) { +// String subkey = subkeyList.Get_at_str(i); +// if (this.data.Get_by_ary(code).Get_by_ary(key).isset(subkey)) { +// continue; +// } +// this.data.Get_by_ary(code).Get_by_ary(key).Set(subkey, this.getSubitem(code, key, subkey)); +// } +// } else { +// this.data.Get_by_ary(code).Set(key, this.store.get(code, key)); +// } +// +// this.loadedItems.Get_by_ary(code).Set(key, true); +// } + + /** + * Load a subitem into the cache + * @param String code + * @param String key + * @param String subkey + */ +// protected void loadSubitem(String code, String key, String subkey) { +// if (!XomwLocalisationCache.splitKeys.in_array(key)) { +// this.loadItem(code, key); +// +// return; +// } +// +// if (!this.initialisedLangs.isset(code)) { +// this.initLanguage(code); +// } +// +// // Check to see if initLanguage() loaded it for us +// if (this.loadedItems.Get_by_ary(code).isset(key) || +// this.loadedSubitems.Get_by_ary(code).Get_by_ary(key).isset(subkey) +// ) { +// return; +// } +// +// if (this.shallowFallbacks.isset(code)) { +// this.loadSubitem(this.shallowFallbacks.Get_by_str(code), key, subkey); +// +// return; +// } +// +// Object value = this.store.get(code, key + ":" + subkey); +// this.data.Get_by_ary(code).Get_by_ary(key).Set(subkey, value); +// this.loadedSubitems.Get_by_ary(code).Get_by_ary(key).Set(subkey, true); +// } + +// /** +// * Returns true if the cache identified by code is missing or expired. +// * +// * @param String code +// * +// * @return boolean +// */ +// public function isExpired(code) { +// if (this.forceRecache && !isset(this.recachedLangs[code])) { +// wfDebug(__METHOD__ . "(code): forced reload\n"); +// +// return true; +// } +// +// deps = this.store.get(code, 'deps'); +// keys = this.store.get(code, 'list'); +// preload = this.store.get(code, 'preload'); +// // Different keys may expire separately for some stores +// if (deps === null || keys === null || preload === null) { +// wfDebug(__METHOD__ . "(code): cache missing, need to make one\n"); +// +// return true; +// } +// +// foreach (deps as dep) { +// // Because we're unserializing stuff from cache, we +// // could receive objects of cl+asses that don't exist +// // anymore (e.g. uninstalled extensions) +// // When this happens, always expire the cache +// if (!dep instanceof CacheDependency || dep.isExpired()) { +// wfDebug(__METHOD__ . "(code): cache for code expired due to " . +// get_cl+ass(dep) . "\n"); +// +// return true; +// } +// } +// +// return false; +// } + + /** + * Initialise a language in this Object. Rebuild the cache if necessary. + * @param String code + * @throws MWException + */ + protected void initLanguage(String code) { +// if (this.initialisedLangs.isset(code)) { +// return; +// } +// +// this.initialisedLangs.Set(code, true); +// +// // If the code is of the wrong form for a Messages*.php file, do a shallow fallback +// if (!XomwLanguage.isValidBuiltInCode(code)) { +// this.initShallowFallback(code, "en"); +// +// return; +// } +// +// // Recache the data if necessary +// if (!this.manualRecache && this.isExpired(code)) { +// if (XomwLanguage.isSupportedLanguage(code)) { +// this.recache(code); +// } else if (String_.Eq(code, "en")) { +// throw new XomwMWException("MessagesEn.php is missing."); +// } else { +// this.initShallowFallback(code, "en"); +// } +// +// return; +// } + +// // Preload some stuff +// preload = this.getItem(code, "preload"); +// if (preload === null) { +// if (this.manualRecache) { +// // No Messages*.php file. Do shallow fallback to en. +// if (code === "en") { +// throw new MWException("No localisation cache found for English. " . +// "Please run maintenance/rebuildLocalisationCache.php."); +// } +// this.initShallowFallback(code, "en"); +// +// return; +// } else { +// throw new MWException("Invalid or missing localisation cache."); +// } +// } +// this.data[code] = preload; +// foreach (preload as key => item) { +// if (in_array(key, XomwLocalisationCache.splitKeys)) { +// foreach (item as subkey => subitem) { +// this.loadedSubitems[code][key][subkey] = true; +// } +// } else { +// this.loadedItems[code][key] = true; +// } +// } + } +// +// /** +// * Create a fallback from one language to another, without creating a +// * complete persistent cache. +// * @param String primaryCode +// * @param String fallbackCode +// */ +// public function initShallowFallback(primaryCode, fallbackCode) { +// this.data[primaryCode] =& this.data[fallbackCode]; +// this.loadedItems[primaryCode] =& this.loadedItems[fallbackCode]; +// this.loadedSubitems[primaryCode] =& this.loadedSubitems[fallbackCode]; +// this.shallowFallbacks[primaryCode] = fallbackCode; +// } +// +// /** +// * Read a PHP file containing localisation data. +// * @param String _fileName +// * @param String _fileType +// * @throws MWException +// * @return array +// */ +// protected function readPHPFile(_fileName, _fileType) { +// // Disable APC caching +// Wikimedia\suppressWarnings(); +// _apcEnabled = ini_set('apc.cache_by_default', '0'); +// Wikimedia\restoreWarnings(); +// +// include _fileName; +// +// Wikimedia\suppressWarnings(); +// ini_set('apc.cache_by_default', _apcEnabled); +// Wikimedia\restoreWarnings(); +// +// data = []; +// if (_fileType == 'core' || _fileType == 'extension') { +// foreach (XomwLocalisationCache.allKeys as key) { +// // Not all keys are set in language files, so +// // check they exist first +// if (isset(key)) { +// data[key] = key; +// } +// } +// } elseif (_fileType == 'aliases') { +// if (isset(aliases)) { +// data['aliases'] = aliases; +// } +// } else { +// throw new MWException(__METHOD__ . ": Invalid file type: _fileType"); +// } +// +// return data; +// } +// +// /** +// * Read a JSON file containing localisation messages. +// * @param String fileName Name of file to read +// * @throws MWException If there is a syntax error in the JSON file +// * @return array Array with a 'messages' key, or empty array if the file doesn't exist +// */ +// public function readJSONFile(fileName) { +// if (!is_readable(fileName)) { +// return []; +// } +// +// json = file_get_contents(fileName); +// if (json === false) { +// return []; +// } +// +// data = FormatJson::decode(json, true); +// if (data === null) { +// throw new MWException(__METHOD__ . ": Invalid JSON file: fileName"); +// } +// +// // Remove keys starting with '@', they're reserved for metadata and non-message data +// foreach (data as key => unused) { +// if (key === '' || key[0] === '@') { +// unset(data[key]); +// } +// } +// +// // The JSON format only supports messages, none of the other variables, so wrap the data +// return [ 'messages' => data ]; +// } + + /** + * Get the compiled plural rules for a given language from the XML files. + * @since 1.20 + * @param String code + * @return array|null + */ + public XophpArray getCompiledPluralRules(String code) { + XophpArray rules = this.getPluralRules(code); + if (rules == null) { + return null; + } +// try { + XophpArray compiledRules = XomwEvaluator.compile(rules); +// } catch (CLDRPluralRuleError e) { +// wfDebugLog("l10n", e.getMessage()); +// +// return []; +// } + + return compiledRules; + } + + /** + * Get the plural rules for a given language from the XML files. + * Cached. + * @since 1.20 + * @param String code + * @return array|null + */ + public XophpArray getPluralRules(String code) { + if (XophpObject.is_null(this.pluralRules)) { + this.loadPluralFiles(); + } + return (XophpArray)XophpObject.coalesce(this.pluralRules.Get_by_ary(code), null); + } +// +// /** +// * Get the plural rule types for a given language from the XML files. +// * Cached. +// * @since 1.22 +// * @param String code +// * @return array|null +// */ +// public function getPluralRuleTypes(code) { +// if (this.pluralRuleTypes === null) { +// this.loadPluralFiles(); +// } +// return this.pluralRuleTypes[code] ?? null; +// } + + /** + * Load the plural XML files. + */ + protected void loadPluralFiles() { + String cldrPlural = Io_url_.new_dir_(IP).GenSubFil_nest("languages", "data", "plurals.xml").Xto_api(); + String mwPlural = Io_url_.new_dir_(IP).GenSubFil_nest("languages", "data", "plurals-mediawiki.xml").Xto_api(); + // Load CLDR plural rules + this.loadPluralFile(cldrPlural); + if (XophpIo_.file_exists(mwPlural)) { + // Override or extend + this.loadPluralFile(mwPlural); + } + } + /** + * Load a plural XML file with the given filename, compile the relevant + * rules, and save the compiled rules in a process-local cache. + * + * @param String fileName + * @throws MWException + */ + protected void loadPluralFile(String fileName) { + // Use file_get_contents instead of DOMDocument::load (T58439) + String xml = XophpIo_.file_get_contents(fileName); + if (!XophpString_.is_true(xml)) { + if (gplx.core.envs.Env_.Mode_testing()) { + xml = String_.Concat_lines_nl + ( "i = 1 and v = 0" + ); + } + else { + throw new gplx.xowa.mediawiki.includes.exception.XomwMWException("Unable to read plurals file " + fileName); + } + } + XophpDOMDocument doc = new XophpDOMDocument(); + doc.loadXML(xml); + XophpDOMNodeList rulesets = doc.getElementsByTagName("pluralRules"); + int rulesets_len = rulesets.count(); + for (int i = 0; i < rulesets_len; i++) { + XophpDOMNode ruleset = rulesets.item(i); + String codes = ruleset.getAttribute("locales"); + XophpArray rules = XophpArray.New(); + XophpArray ruleTypes = XophpArray.New(); + XophpDOMNodeList ruleElements = ruleset.getElementsByTagName("pluralRule"); + int elements_len = ruleElements.count(); + for (int j = 0; j < elements_len; j++) { + XophpDOMNode elt = ruleElements.item(j); + String ruleType = elt.getAttribute("count"); + if (String_.Eq(ruleType, "other")) { + // Don"t record "other" rules, which have an empty condition + continue; + } + rules.Add(elt.nodeValue); + ruleTypes.Add(ruleType); + } + String[] code_ary = XophpString_.explode(" ", codes); + for (String code : code_ary) { + this.pluralRules.Set(code, rules); + this.pluralRuleTypes.Set(code, ruleTypes); + } + } + } + + public static String IP; + /** + * Read the data from the source files for a given language, and register + * the relevant dependencies in the deps array. If the localisation + * exists, the data array is returned, otherwise false is returned. + * + * @param String code + * @param array &deps + * @return array + */ + protected XophpArray readSourceFilesAndRegisterDeps(String code, XophpArray deps) { + // This reads in the PHP i18n file with non-messages l10n data +// String fileName = XomwLanguage.getMessagesFileName(code); +// if (!file_exists(fileName)) { +// data = XophpArray.New(); +// } else { +// deps.Add(new FileDependency(fileName)); +// data = this.readPHPFile(fileName, "core"); +// } +// +// // Load CLDR plural rules for JavaScript +// data.Set("pluralRules", this.getPluralRules(code)); +// // And for PHP +// data.Set("compiledPluralRules", this.getCompiledPluralRules(code)); +// // Load plural rule types +// data.Set("pluralRuleTypes", this.getPluralRuleTypes(code)); +// +// deps.Set("plurals", new FileDependency("IP/languages/data/plurals.xml")); +// deps.Set("plurals-mw", new FileDependency("IP/languages/data/plurals-mediawiki.xml")); +// +// return data; + return null; + } + +// /** +// * Merge two localisation values, a primary and a fallback, overwriting the +// * primary value in place. +// * @param String key +// * @param mixed &value +// * @param mixed fallbackValue +// */ +// protected function mergeItem(key, &value, fallbackValue) { +// if (!is_null(value)) { +// if (!is_null(fallbackValue)) { +// if (in_array(key, XomwLocalisationCache.mergeableMapKeys)) { +// value = value + fallbackValue; +// } elseif (in_array(key, XomwLocalisationCache.mergeableListKeys)) { +// value = array_unique(array_merge(fallbackValue, value)); +// } elseif (in_array(key, XomwLocalisationCache.mergeableAliasListKeys)) { +// value = array_merge_recursive(value, fallbackValue); +// } elseif (in_array(key, XomwLocalisationCache.optionalMergeKeys)) { +// if (!empty(value['inherit'])) { +// value = array_merge(fallbackValue, value); +// } +// +// if (isset(value['inherit'])) { +// unset(value['inherit']); +// } +// } elseif (in_array(key, XomwLocalisationCache.magicWordKeys)) { +// this.mergeMagicWords(value, fallbackValue); +// } +// } +// } else { +// value = fallbackValue; +// } +// } +// +// /** +// * @param mixed &value +// * @param mixed fallbackValue +// */ +// protected function mergeMagicWords(&value, fallbackValue) { +// foreach (fallbackValue as magicName => fallbackInfo) { +// if (!isset(value[magicName])) { +// value[magicName] = fallbackInfo; +// } else { +// oldSynonyms = array_slice(fallbackInfo, 1); +// newSynonyms = array_slice(value[magicName], 1); +// synonyms = array_values(array_unique(array_merge( +// newSynonyms, oldSynonyms))); +// value[magicName] = array_merge([ fallbackInfo[0] ], synonyms); +// } +// } +// } +// +// /** +// * Given an array mapping language code to localisation value, such as is +// * found in extension *.i18n.php files, iterate through a fallback sequence +// * to merge the given data with an existing primary value. +// * +// * Returns true if any data from the extension array was used, false +// * otherwise. +// * @param array codeSequence +// * @param String key +// * @param mixed &value +// * @param mixed fallbackValue +// * @return boolean +// */ +// protected function mergeExtensionItem(codeSequence, key, &value, fallbackValue) { +// used = false; +// foreach (codeSequence as code) { +// if (isset(fallbackValue[code])) { +// this.mergeItem(key, value, fallbackValue[code]); +// used = true; +// } +// } +// +// return used; +// } +// +// /** +// * Gets the combined list of messages dirs from +// * core and extensions +// * +// * @since 1.25 +// * @return array +// */ +// public function getMessagesDirs() { +// global IP; +// +// config = MediaWikiServices::getInstance().getMainConfig(); +// messagesDirs = config.get('MessagesDirs'); +// return [ +// 'core' => "IP/languages/i18n", +// 'exif' => "IP/languages/i18n/exif", +// 'api' => "IP/includes/api/i18n", +// 'oojs-ui' => "IP/resources/lib/ooui/i18n", +// ] + messagesDirs; +// } + + /** + * Load localisation data for a given language for both core and extensions + * and save it to the persistent cache store and the process cache + * @param String code + * @throws MWException + */ + public void recache(String code) { +// global wgExtensionMessagesFiles; +// +// if (!code) { +// throw new MWException("Invalid language code requested"); +// } +// this.recachedLangs[code] = true; + + // Initial values +// XophpArray initialData = array_fill_keys(XomwLocalisationCache.allKeys, null); + XophpArray initialData = XophpArray.New(); //array_fill_keys(XomwLocalisationCache.allKeys, null); +// coreData = initialData; +// deps = []; +// +// // Load the primary localisation from the source file +// data = this.readSourceFilesAndRegisterDeps(code, deps); +// if (data === false) { +// wfDebug(__METHOD__ . ": no localisation file for code, using fallback to en\n"); +// coreData['fallback'] = 'en'; +// } else { +// wfDebug(__METHOD__ . ": got localisation for code from source\n"); +// +// // Merge primary localisation +// foreach (data as key => value) { +// this.mergeItem(key, coreData[key], value); +// } +// } +// +// // Fill in the fallback if it's not there already +// if ((is_null(coreData['fallback']) || coreData['fallback'] === false) && code === 'en') { +// coreData['fallback'] = false; +// coreData['originalFallbackSequence'] = coreData['fallbackSequence'] = []; +// } else { +// if (!is_null(coreData['fallback'])) { +// coreData['fallbackSequence'] = array_map('trim', explode(',', coreData['fallback'])); +// } else { +// coreData['fallbackSequence'] = []; +// } +// len = count(coreData['fallbackSequence']); +// +// // Before we add the 'en' fallback for messages, keep a copy of +// // the original fallback sequence +// coreData['originalFallbackSequence'] = coreData['fallbackSequence']; +// +// // Ensure that the sequence ends at 'en' for messages +// if (!len || coreData['fallbackSequence'][len - 1] !== 'en') { +// coreData['fallbackSequence'][] = 'en'; +// } +// } +// +// codeSequence = array_merge([ code ], coreData['fallbackSequence']); +// messageDirs = this.getMessagesDirs(); +// +// // Load non-JSON localisation data for extensions +// extensionData = array_fill_keys(codeSequence, initialData); +// foreach (wgExtensionMessagesFiles as extension => fileName) { +// if (isset(messageDirs[extension])) { +// // This extension has JSON message data; skip the PHP shim +// continue; +// } +// +// data = this.readPHPFile(fileName, 'extension'); +// used = false; +// +// foreach (data as key => item) { +// foreach (codeSequence as csCode) { +// if (isset(item[csCode])) { +// this.mergeItem(key, extensionData[csCode][key], item[csCode]); +// used = true; +// } +// } +// } +// +// if (used) { +// deps[] = new FileDependency(fileName); +// } +// } +// + // Load the localisation data for each fallback, then merge it into the full array + XophpArray allData = initialData; +// foreach (codeSequence as csCode) { +// csData = initialData; +// +// // Load core messages and the extension localisations. +// foreach (messageDirs as dirs) { +// foreach ((array)dirs as dir) { +// fileName = "dir/csCode.json"; +// data = this.readJSONFile(fileName); +// +// foreach (data as key => item) { +// this.mergeItem(key, csData[key], item); +// } +// +// deps[] = new FileDependency(fileName); +// } +// } +// +// // Merge non-JSON extension data +// if (isset(extensionData[csCode])) { +// foreach (extensionData[csCode] as key => item) { +// this.mergeItem(key, csData[key], item); +// } +// } +// +// if (csCode === code) { +// // Merge core data into extension data +// foreach (coreData as key => item) { +// this.mergeItem(key, csData[key], item); +// } +// } else { +// // Load the secondary localisation from the source file to +// // avoid infinite cycles on cyclic fallbacks +// fbData = this.readSourceFilesAndRegisterDeps(csCode, deps); +// if (fbData !== false) { +// // Only merge the keys that make sense to merge +// foreach (XomwLocalisationCache.allKeys as key) { +// if (!isset(fbData[key])) { +// continue; +// } +// +// if (is_null(coreData[key]) || this.isMergeableKey(key)) { +// this.mergeItem(key, csData[key], fbData[key]); +// } +// } +// } +// } +// +// // Allow extensions an opportunity to adjust the data for this +// // fallback +// Hooks::run('LocalisationCacheRecacheFallback', [ this, csCode, &csData ]); +// +// // Merge the data for this fallback into the final array +// if (csCode === code) { +// allData = csData; +// } else { +// foreach (XomwLocalisationCache.allKeys as key) { +// if (!isset(csData[key])) { +// continue; +// } +// +// if (is_null(allData[key]) || this.isMergeableKey(key)) { +// this.mergeItem(key, allData[key], csData[key]); +// } +// } +// } +// } +// +// // Add cache dependencies for any referenced globals +// deps['wgExtensionMessagesFiles'] = new GlobalDependency('wgExtensionMessagesFiles'); +// // The 'MessagesDirs' config setting is used in LocalisationCache::getMessagesDirs(). +// // We use the key 'wgMessagesDirs' for historical reasons. +// deps['wgMessagesDirs'] = new MainConfigDependency('MessagesDirs'); +// deps['version'] = new ConstantDependency('LocalisationCache::VERSION'); +// +// // Add dependencies to the cache entry +// allData['deps'] = deps; +// +// // Replace spaces with underscores in name+space names +// allData['name+spaceNames'] = str_replace(' ', '_', allData['name+spaceNames']); +// +// // And do the same for special page aliases. page is an array. +// foreach (allData['specialPageAliases'] as &page) { +// page = str_replace(' ', '_', page); +// } +// // Decouple the reference to prevent accidental damage +// unset(page); +// +// // If there were no plural rules, return an empty array +// if (allData['pluralRules'] === null) { +// allData['pluralRules'] = []; +// } + if (allData.Has("compiledPluralRules")) { + allData.Set("compiledPluralRules", XophpArray.New()); + } +// // If there were no plural rule types, return an empty array +// if (allData['pluralRuleTypes'] === null) { +// allData['pluralRuleTypes'] = []; +// } +// +// // Set the list keys +// allData['list'] = []; +// foreach (XomwLocalisationCache.splitKeys as key) { +// allData['list'][key] = array_keys(allData[key]); +// } +// // Run hooks +// purgeBlobs = true; +// Hooks::run('LocalisationCacheRecache', [ this, code, &allData, &purgeBlobs ]); +// +// if (is_null(allData['name+spaceNames'])) { +// throw new MWException(__METHOD__ . ': Localisation data failed sanity check! ' . +// 'Check that your languages/messages/MessagesEn.php file is intact.'); +// } +// +// // Set the preload key +// allData['preload'] = this.buildPreload(allData); +// +// // Save to the process cache and register the items loaded +// this.data[code] = allData; +// foreach (allData as key => item) { +// this.loadedItems[code][key] = true; +// } +// +// // Save to the persistent cache +// this.store.startWrite(code); +// foreach (allData as key => value) { +// if (in_array(key, XomwLocalisationCache.splitKeys)) { +// foreach (value as subkey => subvalue) { +// this.store.set("key:subkey", subvalue); +// } +// } else { +// this.store.set(key, value); +// } +// } +// this.store.finishWrite(); +// +// // Clear out the MessageBlobStore +// // HACK: If using a null (i.e. disabled) storage backend, we +// // can't write to the MessageBlobStore either +// if (purgeBlobs && !this.store instanceof LCStoreNull) { +// blobStore = new MessageBlobStore( +// MediaWikiServices::getInstance().getResourceLoader() +// ); +// blobStore.clear(); +// } + } +// +// /** +// * Build the preload item from the given pre-cache data. +// * +// * The preload item will be loaded automatically, improving performance +// * for the commonly-requested items it contains. +// * @param array data +// * @return array +// */ +// protected function buildPreload(data) { +// preload = [ 'messages' => [] ]; +// foreach (XomwLocalisationCache.preloadedKeys as key) { +// preload[key] = data[key]; +// } +// +// foreach (data['preloadedMessages'] as subkey) { +// subitem = data['messages'][subkey] ?? null; +// preload['messages'][subkey] = subitem; +// } +// +// return preload; +// } +// +// /** +// * Unload the data for a given language from the Object cache. +// * Reduces memory usage. +// * @param String code +// */ +// public function unload(code) { +// unset(this.data[code]); +// unset(this.loadedItems[code]); +// unset(this.loadedSubitems[code]); +// unset(this.initialisedLangs[code]); +// unset(this.shallowFallbacks[code]); +// +// foreach (this.shallowFallbacks as shallowCode => fbCode) { +// if (fbCode === code) { +// this.unload(shallowCode); +// } +// } +// } +// +// /** +// * Unload all data +// */ +// public function unloadAll() { +// foreach (this.initialisedLangs as lang => unused) { +// this.unload(lang); +// } +// } +// +// /** +// * Disable the storage backend +// */ +// public function disableBackend() { +// this.store = new LCStoreNull; +// this.manualRecache = false; +// } +} diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/cache/localisation/XomwLocalisationCacheForXowa.java b/400_xowa/src/gplx/xowa/mediawiki/includes/cache/localisation/XomwLocalisationCacheForXowa.java new file mode 100644 index 000000000..5cd58991d --- /dev/null +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/cache/localisation/XomwLocalisationCacheForXowa.java @@ -0,0 +1,31 @@ +/* +XOWA: the XOWA Offline Wiki Application +Copyright (C) 2012-2017 gnosygnu@gmail.com + +XOWA is licensed under the terms of the General Public License (GPL) Version 3, +or alternatively under the terms of the Apache License Version 2.0. + +You may use XOWA according to either of these licenses as is most appropriate +for your project on a case-by-case basis. + +The terms of each license can be found in the source code repository: + +GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt +Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt +*/ +package gplx.xowa.mediawiki.includes.cache.localisation; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.includes.*; import gplx.xowa.mediawiki.includes.cache.*; +public class XomwLocalisationCacheForXowa extends XomwLocalisationCache { private static boolean init; + public static void Init_ip(Io_url val) { + init = false; + IP = val.Raw(); + } + public XophpArray getItem_ary(String code, String key) { + if (!init) { + init = true; + this.loadPluralFiles(); + } + + XophpArray rv = getCompiledPluralRules(code); + return rv; + } +} \ No newline at end of file diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/exception/XomwMWException.java b/400_xowa/src/gplx/xowa/mediawiki/includes/exception/XomwMWException.java index 194c83cce..1d33b7238 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/includes/exception/XomwMWException.java +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/exception/XomwMWException.java @@ -14,6 +14,7 @@ GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt */ package gplx.xowa.mediawiki.includes.exception; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.includes.*; +import gplx.core.strings.*; public class XomwMWException extends Err { public XomwMWException(String msg) {super(true, "", "", msg); } diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/filerepo/file/XomwFile.java b/400_xowa/src/gplx/xowa/mediawiki/includes/filerepo/file/XomwFile.java index 565b90103..68b88ffbb 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/includes/filerepo/file/XomwFile.java +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/filerepo/file/XomwFile.java @@ -293,9 +293,9 @@ public class XomwFile { */ private byte[] getExtension() { if (!XophpUtility.isset(this.extension)) { - int n = XophpString.strpos(this.getName(), Byte_ascii.Dot); + int n = XophpString_.strpos(this.getName(), Byte_ascii.Dot); this.extension = normalizeExtension( - n != Bry_find_.Not_found ? XophpString.substr(this.getName(), n + 1) : Bry_.Empty); + n != Bry_find_.Not_found ? XophpString_.substr(this.getName(), n + 1) : Bry_.Empty); } return this.extension; diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/interwiki/XomwInterwiki.java b/400_xowa/src/gplx/xowa/mediawiki/includes/interwiki/XomwInterwiki.java index 974c02f2a..7c3f934b2 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/includes/interwiki/XomwInterwiki.java +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/interwiki/XomwInterwiki.java @@ -111,7 +111,7 @@ public class XomwInterwiki { public byte[] getURL(byte[] title) { byte[] url = this.mURL; if (title != null) { - url = XophpString.str_replace(ARG_1, XomwGlobalFunctions.wfUrlencode(title), url); + url = XophpString_.str_replace(ARG_1, XomwGlobalFunctions.wfUrlencode(title), url); } return url; diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/XomwBlockLevelPass.java b/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/XomwBlockLevelPass.java index a2b147615..84283395d 100644 --- a/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/XomwBlockLevelPass.java +++ b/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/XomwBlockLevelPass.java @@ -278,8 +278,8 @@ public class XomwBlockLevelPass { // If not in a
 element, scan for and figure out what prefixes are there.
 			if (!this.inPre) {
 				// Multiple prefixes may abut each other for nested lists.
-				prefixLen = XophpString.strspn_fwd__ary(src, block_chars_ary, lineBgn, lineEnd, lineEnd); // strspn($oLine, '*#:;');
-				prefix = XophpString.substr(src, lineBgn, prefixLen);
+				prefixLen = XophpString_.strspn_fwd__ary(src, block_chars_ary, lineBgn, lineEnd, lineEnd); // strspn($oLine, '*#:;');
+				prefix = XophpString_.substr(src, lineBgn, prefixLen);
 
 				// eh?
 				// ; and : are both from definition-lists, so they're equivalent
@@ -302,7 +302,7 @@ public class XomwBlockLevelPass {
 			int commonPrefixLen = -1;
 			if (prefixLen > 0 && Bry_.Eq(lastPrefix, prefix2)) {
 				// Same as the last item, so no need to deal with nesting or opening stuff
-				bfr.Add(this.nextItem(XophpString.substr_byte(prefix, -1)));
+				bfr.Add(this.nextItem(XophpString_.substr_byte(prefix, -1)));
 				pendingPTag = PARA_STACK_NONE;
 
 				if (prefixLen > 0 && prefix[prefixLen - 1] == Byte_ascii.Semic) {
@@ -342,7 +342,7 @@ public class XomwBlockLevelPass {
 					bfr.Add_byte_nl();
 				}
 				while (prefixLen > commonPrefixLen) {
-					byte c = XophpString.substr_byte(prefix, commonPrefixLen, 1);
+					byte c = XophpString_.substr_byte(prefix, commonPrefixLen, 1);
 					bfr.Add(this.openList(c));
 
 					if (c == Byte_ascii.Semic) {
@@ -396,7 +396,7 @@ public class XomwBlockLevelPass {
 					inBlockElem = !closeMatch;
 				}
 				else if (!inBlockElem && !this.inPre) {
-					if (XophpString.substr_byte(t, 0) == Byte_ascii.Space
+					if (XophpString_.substr_byte(t, 0) == Byte_ascii.Space
 						&& (this.lastSection == LAST_SECTION_PRE || Bry_.Trim(t) != Bry_.Empty)
 						&& !inBlockquote
 					) {
@@ -481,18 +481,18 @@ public class XomwBlockLevelPass {
 	*/
 	private int findColonNoLinks(byte[] str, byte[] before, byte[] after) {
 		int len = str.length;
-		int colonPos = XophpString.strpos(str, Byte_ascii.Colon, 0, len);
+		int colonPos = XophpString_.strpos(str, Byte_ascii.Colon, 0, len);
 		if (colonPos == Bry_find_.Not_found) {
 			// Nothing to find!
 			return Bry_find_.Not_found;
 		}
 
-		int ltPos = XophpString.strpos(str, Byte_ascii.Angle_bgn, 0, len);
+		int ltPos = XophpString_.strpos(str, Byte_ascii.Angle_bgn, 0, len);
 		if (ltPos == Bry_find_.Not_found || ltPos > colonPos) {
 			// Easy; no tag nesting to worry about
 			// XOMW: MW passes before / after by reference; XO: changes member and depends on callers to update
-			find_colon_no_links__before = XophpString.substr(str, 0, colonPos);
-			find_colon_no_links__after = XophpString.substr(str, colonPos + 1);
+			find_colon_no_links__before = XophpString_.substr(str, 0, colonPos);
+			find_colon_no_links__after = XophpString_.substr(str, colonPos + 1);
 			return colonPos;
 		}
 
@@ -512,25 +512,25 @@ public class XomwBlockLevelPass {
 						case Byte_ascii.Colon:
 							if (level == 0) {
 								// We found it!
-								find_colon_no_links__before = XophpString.substr(str, 0, i);
-								find_colon_no_links__after = XophpString.substr(str, i + 1);
+								find_colon_no_links__before = XophpString_.substr(str, 0, i);
+								find_colon_no_links__after = XophpString_.substr(str, i + 1);
 								return i;
 							}
 							// Embedded in a tag; don't break it.
 							break;
 						default:
 							// Skip ahead looking for something interesting
-							colonPos = XophpString.strpos(str, Byte_ascii.Colon, i, len);
+							colonPos = XophpString_.strpos(str, Byte_ascii.Colon, i, len);
 							if (colonPos == Bry_find_.Not_found) {
 								// Nothing else interesting
 								return Bry_find_.Not_found;
 							}
-							ltPos = XophpString.strpos(str, Byte_ascii.Angle_bgn, i, len);
+							ltPos = XophpString_.strpos(str, Byte_ascii.Angle_bgn, i, len);
 							if (level == 0) {
 								if (ltPos == Bry_find_.Not_found || colonPos < ltPos) {
 									// We found it!
-									find_colon_no_links__before = XophpString.substr(str, 0, colonPos);
-									find_colon_no_links__after = XophpString.substr(str, colonPos + 1);
+									find_colon_no_links__before = XophpString_.substr(str, 0, colonPos);
+									find_colon_no_links__after = XophpString_.substr(str, colonPos + 1);
 									return i;
 								}
 							}
diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/XomwPreprocessor.java b/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/XomwPreprocessor.java
index f9994a7fd..cd027befa 100644
--- a/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/XomwPreprocessor.java
+++ b/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/XomwPreprocessor.java
@@ -431,11 +431,11 @@ public abstract class XomwPreprocessor {
 					}
 					else {
 						// Search backwards for leading whitespace
-						int ws_bgn = i > 0 ? i - XophpString.strspn_bwd__space_or_tab(src, i, -1) : 0;
+						int ws_bgn = i > 0 ? i - XophpString_.strspn_bwd__space_or_tab(src, i, -1) : 0;
 
 						// Search forwards for trailing whitespace
 						// $wsEnd will be the position of the last space (or the '>' if there's none)
-						int ws_end = end_pos + 2 + XophpString.strspn_fwd__space_or_tab(src, end_pos + 3, -1, src_len);
+						int ws_end = end_pos + 2 + XophpString_.strspn_fwd__space_or_tab(src, end_pos + 3, -1, src_len);
 
 						// Keep looking forward as long as we're finding more
 						// comments.
@@ -446,7 +446,7 @@ public abstract class XomwPreprocessor {
 							if (cur_char_pos == Bry_find_.Not_found) {
 								break;
 							}
-							cur_char_pos = cur_char_pos + 2 + XophpString.strspn_fwd__space_or_tab(src, cur_char_pos + 3, -1, src_len);
+							cur_char_pos = cur_char_pos + 2 + XophpString_.strspn_fwd__space_or_tab(src, cur_char_pos + 3, -1, src_len);
 							comments_list.Add(new int[] {ws_end + 1, cur_char_pos});
 							ws_end = cur_char_pos;
 						}
@@ -609,7 +609,7 @@ public abstract class XomwPreprocessor {
 					i++;
 				}
 
-				int count = XophpString.strspn_fwd__byte(src, Byte_ascii.Eq, i, 6, src_len);
+				int count = XophpString_.strspn_fwd__byte(src, Byte_ascii.Eq, i, 6, src_len);
 				if (count == 1 && findEquals) {	// EX: "{{a|\n=b=\n"
 					// DWIM: This looks kind of like a name/value separator.
 					// Let's let the equals handler have it and break the
@@ -619,7 +619,7 @@ public abstract class XomwPreprocessor {
 				}
 				else if (count > 0) {
 					XomwPPDStackElement piece = Factory__stack_element(Factory__part(), String_.Nl, String_.Nl, count, i, false);
-					piece.addPart(XophpString.str_repeat("=", count));
+					piece.addPart(XophpString_.str_repeat("=", count));
 					stack.push(piece);
 					accum = this.Accum__set(stack.getAccum());
 					XomwPPDStackElementFlags flags = stack.getFlags();
@@ -638,7 +638,7 @@ public abstract class XomwPreprocessor {
 				// Search back through the input to see if it has a proper close.
 				// Do this using the reversed String since the other solutions
 				// (end anchor, etc.) are inefficient.
-				int ws_len = XophpString.strspn_bwd__space_or_tab(src, src_len - i, -1);
+				int ws_len = XophpString_.strspn_bwd__space_or_tab(src, src_len - i, -1);
 				int search_bgn = i - ws_len;
 
 				if (part.commentEnd != -1 && search_bgn -1 == part.commentEnd) {
@@ -646,10 +646,10 @@ public abstract class XomwPreprocessor {
 					// Search for equals signs before the comment
 					search_bgn = part.visualEnd;
 					search_bgn = Bry_find_.Find_bwd__while_space_or_tab(src, search_bgn, 0);
-					search_bgn -= XophpString.strspn_bwd__space_or_tab(src, search_bgn, -1);
+					search_bgn -= XophpString_.strspn_bwd__space_or_tab(src, search_bgn, -1);
 				}
 				int count = piece.count;
-				int eq_len = XophpString.strspn_bwd__byte(src, Byte_ascii.Eq, search_bgn, -1);
+				int eq_len = XophpString_.strspn_bwd__byte(src, Byte_ascii.Eq, search_bgn, -1);
 
 				Xomw_prepro_accum element = null;
 				if (eq_len > 0) {
@@ -702,7 +702,7 @@ public abstract class XomwPreprocessor {
 			}
 			else if (found == Found__open) {
 				// count opening brace characters
-				int count = XophpString.strspn_fwd__byte(src, cur_char[0], i, -1, src_len);	// NOTE: don't know how MediaWiki will handle "-{"
+				int count = XophpString_.strspn_fwd__byte(src, cur_char[0], i, -1, src_len);	// NOTE: don't know how MediaWiki will handle "-{"
 
 				// we need to add to stack only if opening brace count is enough for one of the rules
 				if (count >= rule.min) {
@@ -726,7 +726,7 @@ public abstract class XomwPreprocessor {
 				XomwPPDStackElement piece = stack.top;
 				// lets check if there are enough characters for closing brace
 				int max_count = piece.count;
-				int count = XophpString.strspn_fwd__byte(src, cur_char[0], i, max_count, src_len);
+				int count = XophpString_.strspn_fwd__byte(src, cur_char[0], i, max_count, src_len);
 
 				// check for maximum matching characters (if there are 5 closing characters, we will probably need only 3 - depending on the rules)
 				rule = Get_rule(piece.open);
@@ -785,7 +785,7 @@ public abstract class XomwPreprocessor {
 						this.accum = this.Accum__set(stack.getAccum());
 					}
 					else {
-						this.preprocessToObj_literal(Bry_.new_u8(XophpString.str_repeat(piece.open, piece.count)));
+						this.preprocessToObj_literal(Bry_.new_u8(XophpString_.str_repeat(piece.open, piece.count)));
 					}
 				}
 
diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/XomwPreprocessor_DOM.java b/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/XomwPreprocessor_DOM.java
index b14bca6b5..efb986cdb 100644
--- a/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/XomwPreprocessor_DOM.java
+++ b/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/XomwPreprocessor_DOM.java
@@ -57,7 +57,7 @@ class XomwPreprocessor_DOM extends XomwPreprocessor { 	private final    Bry_bfr
 	@Override protected void preprocessToObj_removeLeadingWhitespaceFromEnd(int ws_len) {
 		int accum_dom_len = accum_dom.Len();
 		if (	ws_len > 0
-			&&	XophpString.strspn_fwd__space_or_tab(accum_dom.Bfr_bry(), accum_dom_len - ws_len, -1, accum_dom_len) == ws_len) {
+			&&	XophpString_.strspn_fwd__space_or_tab(accum_dom.Bfr_bry(), accum_dom_len - ws_len, -1, accum_dom_len) == ws_len) {
 			accum_dom.Del_at_end(ws_len);
 		}
 	}
diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/XomwPreprocessor_Hash.java b/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/XomwPreprocessor_Hash.java
index 196551a87..bf2a029ab 100644
--- a/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/XomwPreprocessor_Hash.java
+++ b/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/XomwPreprocessor_Hash.java
@@ -46,13 +46,13 @@ class XomwPreprocessor_Hash extends XomwPreprocessor { 	private XophpArray accum
 	@Override protected void preprocessToObj_root() {} // NOTE: deliberately empty;
 
 	@Override protected void preprocessToObj_ignore(byte[] src, int bgn, int end) {
-		accum.Add(XophpArray.New("ignore", XophpArray.New(XophpString.substr(src, bgn, end - bgn))));
+		accum.Add(XophpArray.New("ignore", XophpArray.New(XophpString_.substr(src, bgn, end - bgn))));
 	}
 	@Override protected void preprocessToObj_literal(byte[] src, int bgn, int end) {
-		addLiteral(accum, XophpString.substr(src, bgn, end - bgn));
+		addLiteral(accum, XophpString_.substr(src, bgn, end - bgn));
 	}
 	@Override protected void preprocessToObj_comment(byte[] src, int bgn, int end) {
-		accum.Add(XophpArray.New("comment", XophpArray.New(XophpString.substr(src, bgn, end - bgn))));
+		accum.Add(XophpArray.New("comment", XophpArray.New(XophpString_.substr(src, bgn, end - bgn))));
 	}
 	@Override protected void preprocessToObj_removeLeadingWhitespaceFromEnd(int ws_len) {
 		int endIndex = accum.Len() - 1;
@@ -61,8 +61,8 @@ class XomwPreprocessor_Hash extends XomwPreprocessor { 	private XophpArray accum
 			Object itm_obj = accum.Get_at(endIndex);
 			if (XophpTypeUtl.is_string(itm_obj)) {
 				byte[] itm = Bry_.new_u8((String)itm_obj);
-				if (XophpString.strspn_fwd__space_or_tab(itm, itm.length - ws_len, -1, itm.length) == ws_len) {
-					accum.Set(endIndex, XophpString.substr(itm, 0, -ws_len));
+				if (XophpString_.strspn_fwd__space_or_tab(itm, itm.length - ws_len, -1, itm.length) == ws_len) {
+					accum.Set(endIndex, XophpString_.substr(itm, 0, -ws_len));
 				}
 			}
 		}
@@ -103,7 +103,7 @@ class XomwPreprocessor_Hash extends XomwPreprocessor { 	private XophpArray accum
 
 	@Override protected Xomw_prepro_accum preprocessToObj_text(XomwPPDStackElement piece, byte[] rule_end, int matching_count) {
 		XophpArray array = (XophpArray)((XomwPPDStackElement)piece).breakSyntax(matching_count);
-		addLiteral(array, XophpString.str_repeat(String_.new_u8(rule_end), matching_count));
+		addLiteral(array, XophpString_.str_repeat(String_.new_u8(rule_end), matching_count));
 		return new Xomw_prepro_accum__hash(array);
 	}
 	@Override protected Xomw_prepro_accum preprocessToObj_xml(XomwPPDStackElement piece, byte[] name_bry, int max_count, int matching_count) {
diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/magiclinks/Xomw_magiclinks_wkr.java b/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/magiclinks/Xomw_magiclinks_wkr.java
index 291faa89d..5fa88c3c7 100644
--- a/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/magiclinks/Xomw_magiclinks_wkr.java
+++ b/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/magiclinks/Xomw_magiclinks_wkr.java
@@ -224,12 +224,12 @@ public class Xomw_magiclinks_wkr {
 		// XO.MW: if (strpos($url, '(') === false) {$sep .= ')';}
 		url_separators[Byte_ascii.Paren_end] = Bry_find_.Find_fwd(url, Byte_ascii.Paren_bgn, 0, url_len) == Bry_find_.Not_found;
 		
-		int num_sep_chars = XophpString.strspn_bwd__ary(url, url_separators, url_len, -1);
+		int num_sep_chars = XophpString_.strspn_bwd__ary(url, url_separators, url_len, -1);
 		// Don't break a trailing HTML entity by moving the ; into $trail
 		// This is in hot code, so use substr_compare to avoid having to
 		// create a new String Object for the comparison
 		// XO.MW.NOTE: ignore semic if part of entity; EX: "http://a.org'!."
-		if (num_sep_chars > 0 && XophpString.substr_byte(url, -num_sep_chars) == Byte_ascii.Semic) {
+		if (num_sep_chars > 0 && XophpString_.substr_byte(url, -num_sep_chars) == Byte_ascii.Semic) {
 			// more optimization: instead of running preg_match with a $
 			// anchor, which can be slow, do the match on the reversed
 			// String starting at the desired offset.
@@ -241,8 +241,8 @@ public class Xomw_magiclinks_wkr {
 		}
 
 		if (num_sep_chars > 0) {
-			trail = Bry_.Add(XophpString.substr(url, -num_sep_chars), trail);
-			url = XophpString.substr(url, 0, -num_sep_chars);
+			trail = Bry_.Add(XophpString_.substr(url, -num_sep_chars), trail);
+			url = XophpString_.substr(url, 0, -num_sep_chars);
 		}
 
 		// Verify that we still have a real URL after trail removal, and
diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/preprocessors/XomwPPDStackElement.java b/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/preprocessors/XomwPPDStackElement.java
index 3f7a4cde2..9506c7dd5 100644
--- a/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/preprocessors/XomwPPDStackElement.java
+++ b/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/preprocessors/XomwPPDStackElement.java
@@ -106,7 +106,7 @@ public class XomwPPDStackElement {
 			if (openingCount == -1) {
 				openingCount = this.count;
 			}
-			bfr.Add_str(XophpString.str_repeat(this.open, openingCount));
+			bfr.Add_str(XophpString_.str_repeat(this.open, openingCount));
 			boolean first = true;
 			int parts_len = parts.Len();
 			for (int i = 0; i < parts_len; i++) {
diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/preprocessors/XomwPPDStackElement_Hash.java b/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/preprocessors/XomwPPDStackElement_Hash.java
index caf3c09ac..a0e93132c 100644
--- a/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/preprocessors/XomwPPDStackElement_Hash.java
+++ b/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/preprocessors/XomwPPDStackElement_Hash.java
@@ -39,7 +39,7 @@ public class XomwPPDStackElement_Hash extends XomwPPDStackElement { 	public Xomw
 			if (openingCount == -1) {
 				openingCount = this.count;
 			}
-			accum = XophpArray.New(XophpString.str_repeat(this.open, openingCount));
+			accum = XophpArray.New(XophpString_.str_repeat(this.open, openingCount));
 			int lastIndex = 0;
 			boolean first = true;
 			int parts_len = parts.Len();
diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/preprocessors/XomwPPFrame_Hash.java b/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/preprocessors/XomwPPFrame_Hash.java
index 71bf8b404..9f155c4a8 100644
--- a/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/preprocessors/XomwPPFrame_Hash.java
+++ b/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/preprocessors/XomwPPFrame_Hash.java
@@ -64,7 +64,7 @@ class XomwPPFrame_Hash extends XomwPPFrame { 	/**
 		this.preprocessor = preprocessor;
 		this.parser = preprocessor.Parser();
 		this.title = this.parser.mTitle;
-		this.titleCache = XophpArray.New().Add(XophpObject.is_true(this.title) ? this.title.getPrefixedDBkeyStr() : XophpString_.False);
+		this.titleCache = XophpArray.New().Add(XophpObject.is_true(this.title) ? this.title.getPrefixedDBkeyStr() : XophpString_.Null);
 		this.loopCheckHash = XophpArray.New();
 		this.depth = 0;
 		this.childExpansionCache = XophpArray.New();
@@ -153,7 +153,7 @@ class XomwPPFrame_Hash extends XomwPPFrame { 	/**
 	* @return String
 	*/
 	public String expand(Object root, int flags) {
-		if (XophpString.is_string(root)) {
+		if (XophpString_.is_string(root)) {
 			return (String)root;
 		}
 
@@ -214,12 +214,12 @@ class XomwPPFrame_Hash extends XomwPPFrame { 	/**
 			}
 
 			Object newIterator = XophpObject.False;
-			String contextName = XophpString_.False;
+			String contextName = XophpString_.Null;
 			XophpArray contextChildren = XophpArray.False;
 
 			if (contextNode == XophpObject.False) {
 				// nothing to do
-			} else if (XophpString.is_string(contextNode)) {
+			} else if (XophpString_.is_string(contextNode)) {
 				outItm += (String)contextNode;
 			} else if (Type_.Eq_by_obj(contextNode, XomwPPNode_Hash_Array.class)) {
 				newIterator = contextNode;
@@ -245,7 +245,7 @@ class XomwPPFrame_Hash extends XomwPPFrame { 	/**
 			}
 
 			// Handle node descriptor array or tree Object
-			if (contextName == XophpString_.False) {
+			if (!XophpString_.is_true(contextName)) {
 				// Not a node, already handled above
 			} else if (String_.CharAt(contextName, 0) == '@') {
 				// Attribute: no output
@@ -346,7 +346,7 @@ class XomwPPFrame_Hash extends XomwPPFrame { 	/**
 //						this.parser.mHeadings[] = [titleText, bits['i']];
 //						serial = count(this.parser.mHeadings) - 1;
 					String marker = XomwParser.MARKER_PREFIX + "-h-serial-" + XomwParser.MARKER_SUFFIX;
-					s = XophpString.substr(s, 0, bits.Get_by_int("level")) + marker + XophpString.substr(s, bits.Get_by_int("level"));
+					s = XophpString_.substr(s, 0, bits.Get_by_int("level")) + marker + XophpString_.substr(s, bits.Get_by_int("level"));
 //						this.parser.mStripState.addGeneral(marker, '');
 					outItm += s;
 				} else {
@@ -523,7 +523,7 @@ class XomwPPFrame_Hash extends XomwPPFrame { 	/**
 			return this.title.getPrefixedDBkeyStr();
 		} else {
 			// return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false;
-			return this.titleCache.Count() > 0 ? ((String)this.titleCache.Get_at(0)) : XophpString_.False;
+			return this.titleCache.Count() > 0 ? ((String)this.titleCache.Get_at(0)) : XophpString_.Null;
 		}
 	}
 
@@ -562,7 +562,7 @@ class XomwPPFrame_Hash extends XomwPPFrame { 	/**
 	* @return boolean Always false in this implementation.
 	*/
 	@Override public String getArgument(String name) {
-		return XophpString_.False;
+		return XophpString_.Null;
 	}
 
 	/**
diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/preprocessors/XomwPPNode_Hash_Attr.java b/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/preprocessors/XomwPPNode_Hash_Attr.java
index 0664df4a6..1390aec57 100644
--- a/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/preprocessors/XomwPPNode_Hash_Attr.java
+++ b/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/preprocessors/XomwPPNode_Hash_Attr.java
@@ -35,7 +35,7 @@ public class XomwPPNode_Hash_Attr extends XomwPPNode { 	public String name, valu
 		if (!(String_.CharAt(descriptor_name, 0) ==  '@')) {
 			throw Err_.new_wo_type("XomwPPNode_Hash_Attr.CTOR: invalid name in attribute descriptor");
 		}
-		this.name = String_.new_u8(XophpString.substr(Bry_.new_u8(descriptor_name), 1));
+		this.name = String_.new_u8(XophpString_.substr(Bry_.new_u8(descriptor_name), 1));
 		XophpArray descriptor_children = (XophpArray)descriptor.Get_at(XomwPPNode_Hash_Tree.CHILDREN);
 		Object value_obj = descriptor_children.Get_at(0);
 		this.value = Type_.Eq_by_obj(value_obj, byte[].class) ? String_.new_u8((byte[])value_obj): value_obj.toString();
diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/quotes/Xomw_quote_wkr.java b/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/quotes/Xomw_quote_wkr.java
index adc95d582..e4c98449b 100644
--- a/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/quotes/Xomw_quote_wkr.java
+++ b/400_xowa/src/gplx/xowa/mediawiki/includes/parsers/quotes/Xomw_quote_wkr.java
@@ -110,8 +110,8 @@ public class Xomw_quote_wkr {// THREAD.UNSAFE: caching for repeated calls
 			for (int i = 1; i < arr_len; i += 2) {
 				if (arr[i].length == 3) {
 					byte[] prv = arr[i - 1];
-					byte prv__last_char = XophpString.substr_byte(prv, -1);
-					byte prv__last_minus_1_char = XophpString.substr_byte(prv, -2, 1);
+					byte prv__last_char = XophpString_.substr_byte(prv, -1);
+					byte prv__last_minus_1_char = XophpString_.substr_byte(prv, -2, 1);
 					if (prv__last_char == Byte_ascii.Space) {              // NOTE: prv ends in space; EX: "''prv '''"
 						if (prv_ends_w_space == -1) {
 							prv_ends_w_space = i;
diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/site/XomwMediaWikiSite.java b/400_xowa/src/gplx/xowa/mediawiki/includes/site/XomwMediaWikiSite.java
index acd46fe28..c6d7d0416 100644
--- a/400_xowa/src/gplx/xowa/mediawiki/includes/site/XomwMediaWikiSite.java
+++ b/400_xowa/src/gplx/xowa/mediawiki/includes/site/XomwMediaWikiSite.java
@@ -44,7 +44,7 @@ public class XomwMediaWikiSite extends XomwSite {	private static final String PA
 	* @return String
 	*/
 	public byte[] toDBKey(byte[] title) {
-		return XophpString.str_replace(Byte_ascii.Space_bry, Byte_ascii.Underline_bry, title);
+		return XophpString_.str_replace(Byte_ascii.Space_bry, Byte_ascii.Underline_bry, title);
 	}
 
 	/**
diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/site/XomwSite.java b/400_xowa/src/gplx/xowa/mediawiki/includes/site/XomwSite.java
index c85bf033b..a4ef595a2 100644
--- a/400_xowa/src/gplx/xowa/mediawiki/includes/site/XomwSite.java
+++ b/400_xowa/src/gplx/xowa/mediawiki/includes/site/XomwSite.java
@@ -371,7 +371,7 @@ public class XomwSite {
 		}
 
 		if (pageName != null) {
-			url = String_.new_u8(XophpString.str_replace(Bry_.new_a7("$1"), XophpEncode.rawurlencode(Bry_.new_u8(pageName)), Bry_.new_u8(url)));
+			url = String_.new_u8(XophpString_.str_replace(Bry_.new_a7("$1"), XophpEncode.rawurlencode(Bry_.new_u8(pageName)), Bry_.new_u8(url)));
 		}
 
 		return url;
diff --git a/400_xowa/src/gplx/xowa/mediawiki/includes/title/XomwMediaWikiTitleCodec.java b/400_xowa/src/gplx/xowa/mediawiki/includes/title/XomwMediaWikiTitleCodec.java
index c3a5e923d..9f96c8f36 100644
--- a/400_xowa/src/gplx/xowa/mediawiki/includes/title/XomwMediaWikiTitleCodec.java
+++ b/400_xowa/src/gplx/xowa/mediawiki/includes/title/XomwMediaWikiTitleCodec.java
@@ -239,7 +239,7 @@ public class XomwMediaWikiTitleCodec implements XomwTitleFormatter {
 	*/
 	private final    byte[][] tmpPrefixRegex = new byte[2][];
 	public XomwMediaWikiTitleCodecParts splitTitleString(byte[] text, int defaultNamespace) {
-		byte[] dbkey = XophpString.str_replace(Byte_ascii.Space, Byte_ascii.Underline, text);
+		byte[] dbkey = XophpString_.str_replace(Byte_ascii.Space, Byte_ascii.Underline, text);
 
 		// Initialisation
 		XomwMediaWikiTitleCodecParts parts = new XomwMediaWikiTitleCodecParts(dbkey, defaultNamespace);
@@ -339,7 +339,7 @@ public class XomwMediaWikiTitleCodec implements XomwTitleFormatter {
 					// resets the default namespace
 					if (dbkey != Bry_.Empty && dbkey[0] == Byte_ascii.Colon) {
 						parts.ns = XomwDefines.NS_MAIN;
-						dbkey = XophpString.substr(dbkey, 1);
+						dbkey = XophpString_.substr(dbkey, 1);
 					}
 				}
 				// If there's no recognized interwiki or namespace,
@@ -348,10 +348,10 @@ public class XomwMediaWikiTitleCodec implements XomwTitleFormatter {
 			break;
 		} while (true);
 
-		byte[] fragment = XophpString.strstr(dbkey, Byte_ascii.Hash_bry);
+		byte[] fragment = XophpString_.strstr(dbkey, Byte_ascii.Hash_bry);
 		if (null != fragment) {
-			parts.fragment = XophpString.str_replace(Byte_ascii.Underline, Byte_ascii.Space, XophpString.substr(fragment, 1));
-			dbkey = XophpString.substr(dbkey, 0, XophpString.strlen(dbkey) - XophpString.strlen(fragment));
+			parts.fragment = XophpString_.str_replace(Byte_ascii.Underline, Byte_ascii.Space, XophpString_.substr(fragment, 1));
+			dbkey = XophpString_.substr(dbkey, 0, XophpString_.strlen(dbkey) - XophpString_.strlen(fragment));
 			// remove whitespace again: prevents "Foo_bar_#"
 			// becoming "Foo_bar_"
 			dbkey = Bry_.Replace(dbkey, Byte_ascii.Underline_bry, Bry_.Empty);
diff --git a/400_xowa/src/gplx/xowa/mediawiki/languages/XomwLanguage.java b/400_xowa/src/gplx/xowa/mediawiki/languages/XomwLanguage.java
index cd3f7a15b..c212017a6 100644
--- a/400_xowa/src/gplx/xowa/mediawiki/languages/XomwLanguage.java
+++ b/400_xowa/src/gplx/xowa/mediawiki/languages/XomwLanguage.java
@@ -14,23 +14,28 @@ GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt
 Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
 */
 package gplx.xowa.mediawiki.languages; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*;
-import gplx.xowa.mediawiki.includes.*;
+import gplx.langs.regxs.*;
 import gplx.core.primitives.*;
 import gplx.xowa.langs.*;
+import gplx.xowa.mediawiki.includes.*;
+import gplx.xowa.mediawiki.vendor.wikimedia.cldr_plural_rule_parser.src.*;
+import gplx.xowa.mediawiki.includes.cache.localisation.*;
+import gplx.xowa.mediawiki.includes.exception.*;
 public class XomwLanguage {
 	public Xol_lang_itm XoLang() {return xoLang;} private Xol_lang_itm xoLang = null;
 	private final    Bry_bfr tmpBfr = Bry_bfr_.New();
 //		/**
 //		* @var LanguageConverter
 //		*/
-//		public $mConverter;
+//		public mConverter;
 //
-//		public $mVariants, $mCode, $mLoaded = false;
-//		public $mMagicExtensions = [], $mMagicHookDone = false;
-//		private $mHtmlCode = null, $mParentLanguage = false;
+	public String mCode;
+//		public mVariants, mCode, mLoaded = false;
+//		public mMagicExtensions = [], mMagicHookDone = false;
+//		private mHtmlCode = null, mParentLanguage = false;
 //
-//		public $dateFormatStrings = [];
-//		public $mExtendedSpecialPageAliases;
+//		public dateFormatStrings = [];
+//		public mExtendedSpecialPageAliases;
 //
 //		/** @var array|null */
 	private XomwNamespacesById namespaceNames;
@@ -42,47 +47,47 @@ public class XomwLanguage {
 //		/**
 //		* ReplacementArray Object caches
 //		*/
-//		public $transformData = [];
+//		public transformData = [];
+
+	/**
+	* @var LocalisationCache
+	*/
+	static public XomwLocalisationCache dataCache; // equivalent to MessagesLangCode.php
+
+//		static public mLangObjCache = [];
 //
-//		/**
-//		* @var LocalisationCache
-//		*/
-//		static public $dataCache; // equivalent to MessagesLangCode.php
-//
-//		static public $mLangObjCache = [];
-//
-//		static public $mWeekdayMsgs = [
+//		static public mWeekdayMsgs = [
 //			'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
 //			'friday', 'saturday'
 //		];
 //
-//		static public $mWeekdayAbbrevMsgs = [
+//		static public mWeekdayAbbrevMsgs = [
 //			'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
 //		];
 //
-//		static public $mMonthMsgs = [
+//		static public mMonthMsgs = [
 //			'january', 'february', 'march', 'april', 'may_long', 'june',
 //			'july', 'august', 'september', 'october', 'november',
 //			'december'
 //		];
-//		static public $mMonthGenMsgs = [
+//		static public mMonthGenMsgs = [
 //			'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
 //			'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
 //			'december-gen'
 //		];
-//		static public $mMonthAbbrevMsgs = [
+//		static public mMonthAbbrevMsgs = [
 //			'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
 //			'sep', 'oct', 'nov', 'dec'
 //		];
 //
-//		static public $mIranianCalendarMonthMsgs = [
+//		static public mIranianCalendarMonthMsgs = [
 //			'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
 //			'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
 //			'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
 //			'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
 //		];
 //
-//		static public $mHebrewCalendarMonthMsgs = [
+//		static public mHebrewCalendarMonthMsgs = [
 //			'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
 //			'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
 //			'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
@@ -90,7 +95,7 @@ public class XomwLanguage {
 //			'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
 //		];
 //
-//		static public $mHebrewCalendarMonthGenMsgs = [
+//		static public mHebrewCalendarMonthGenMsgs = [
 //			'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
 //			'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
 //			'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
@@ -98,7 +103,7 @@ public class XomwLanguage {
 //			'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
 //		];
 //
-//		static public $mHijriCalendarMonthMsgs = [
+//		static public mHijriCalendarMonthMsgs = [
 //			'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
 //			'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
 //			'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
@@ -109,7 +114,7 @@ public class XomwLanguage {
 //		* @since 1.20
 //		* @var array
 //		*/
-//		static public $durationIntervals = [
+//		static public durationIntervals = [
 //			'millennia' => 31556952000,
 //			'centuries' => 3155695200,
 //			'decades' => 315569520,
@@ -123,30 +128,30 @@ public class XomwLanguage {
 //
 //		/**
 //		* Cache for language fallbacks.
-//		* @see Language::getFallbacksIncludingSiteLanguage
+//		* @see XomwLanguage.getFallbacksIncludingSiteLanguage
 //		* @since 1.21
 //		* @var array
 //		*/
-//		static private $fallbackLanguageCache = [];
+//		static private fallbackLanguageCache = [];
 //
 //		/**
 //		* Cache for grammar rules data
 //		* @var MapCacheLRU|null
 //		*/
-//		static private $grammarTransformations;
+//		static private grammarTransformations;
 //
 //		/**
 //		* Cache for language names
 //		* @var HashBagOStuff|null
 //		*/
-//		static private $languageNameCache;
+//		static private languageNameCache;
 //
 //		/**
 //		* Unicode directional formatting characters, for embedBidi()
 //		*/
-//		static private $lre = "\xE2\x80\xAA"; // U+202A LEFT-TO-RIGHT EMBEDDING
-//		static private $rle = "\xE2\x80\xAB"; // U+202B RIGHT-TO-LEFT EMBEDDING
-//		static private $pdf = "\xE2\x80\xAC"; // U+202C POP DIRECTIONAL FORMATTING
+//		static private lre = "\xE2\x80\xAA"; // U+202A LEFT-TO-RIGHT EMBEDDING
+//		static private rle = "\xE2\x80\xAB"; // U+202B RIGHT-TO-LEFT EMBEDDING
+//		static private pdf = "\xE2\x80\xAC"; // U+202C POP DIRECTIONAL FORMATTING
 //
 //		/**
 //		* Directionality test regex for embedBidi(). Matches the first strong directionality codepoint:
@@ -161,7 +166,7 @@ public class XomwLanguage {
 //		*/
 //		// @codingStandardsIgnoreStart
 //		// @codeCoverageIgnoreStart
-//		static private $strongDirRegex = '/(?:([\x{41}-\x{5a}\x{61}-\x{7a}\x{aa}\x{b5}\x{ba}\x{c0}-\x{d6}\x{d8}-\x{f6}\x{f8}-\x{2b8}\x{2bb}-\x{2c1}\x{2d0}\x{2d1}\x{2e0}-\x{2e4}\x{2ee}\x{370}-\x{373}\x{376}\x{377}\x{37a}-\x{37d}\x{37f}\x{386}\x{388}
+//		static private strongDirRegex = '/(?:([\x{41}-\x{5a}\x{61}-\x{7a}\x{aa}\x{b5}\x{ba}\x{c0}-\x{d6}\x{d8}-\x{f6}\x{f8}-\x{2b8}\x{2bb}-\x{2c1}\x{2d0}\x{2d1}\x{2e0}-\x{2e4}\x{2ee}\x{370}-\x{373}\x{376}\x{377}\x{37a}-\x{37d}\x{37f}\x{386}\x{388}
 //               -\x{38a}\x{38c}\x{38e}-\x{3a1}\x{3a3}-\x{3f5}\x{3f7}-\x{482}\x{48a}-\x{52f}\x{531}-\x{556}\x{559}-\x{55f}\x{561}-\x{587}\x{589}\x{903}-\x{939}\x{93b}\x{93d}-\x{940}\x{949}-\x{94c}\x{94e}-\x{950}\x{958}-\x{961}\x{964}
 //               -\x{980}\x{982}\x{983}\x{985}-\x{98c}\x{98f}\x{990}\x{993}-\x{9a8}\x{9aa}-\x{9b0}\x{9b2}\x{9b6}-\x{9b9}\x{9bd}-\x{9c0}\x{9c7}\x{9c8}\x{9cb}\x{9cc}\x{9ce}\x{9d7}\x{9dc}\x{9dd}\x{9df}-\x{9e1}\x{9e6}-\x{9f1}\x{9f4}-\x{9fa}\x{a03}\x{a05}
 //               -\x{a0a}\x{a0f}\x{a10}\x{a13}-\x{a28}\x{a2a}-\x{a30}\x{a32}\x{a33}\x{a35}\x{a36}\x{a38}\x{a39}\x{a3e}-\x{a40}\x{a59}-\x{a5c}\x{a5e}\x{a66}-\x{a6f}\x{a72}-\x{a74}\x{a83}\x{a85}-\x{a8d}\x{a8f}-\x{a91}\x{a93}-\x{aa8}\x{aaa}-\x{ab0}\x{ab2}\x{ab3}\x{ab5}
@@ -202,92 +207,92 @@ public class XomwLanguage {
 
 //		/**
 //		* Get a cached or new language Object for a given language code
-//		* @param String $code
+//		* @param String code
 //		* @return Language
 //		*/
-//		static function factory($code) {
-//			global $wgDummyLanguageCodes, $wgLangObjCacheSize;
+//		static function factory(code) {
+//			global wgDummyLanguageCodes, wgLangObjCacheSize;
 //
-//			if (isset($wgDummyLanguageCodes[$code])) {
-//				$code = $wgDummyLanguageCodes[$code];
+//			if (isset(wgDummyLanguageCodes[code])) {
+//				code = wgDummyLanguageCodes[code];
 //			}
 //
 //			// get the language Object to process
-//			$langObj = isset(self::$mLangObjCache[$code])
-//				? self::$mLangObjCache[$code]
-//				: self::newFromCode($code);
+//			langObj = isset(XomwLanguage.mLangObjCache[code])
+//				? XomwLanguage.mLangObjCache[code]
+//				: XomwLanguage.newFromCode(code);
 //
 //			// merge the language Object in to get it up front in the cache
-//			self::$mLangObjCache = array_merge([ $code => $langObj ], self::$mLangObjCache);
+//			XomwLanguage.mLangObjCache = array_merge([ code => langObj ], XomwLanguage.mLangObjCache);
 //			// get rid of the oldest ones in case we have an overflow
-//			self::$mLangObjCache = array_slice(self::$mLangObjCache, 0, $wgLangObjCacheSize, true);
+//			XomwLanguage.mLangObjCache = array_slice(XomwLanguage.mLangObjCache, 0, wgLangObjCacheSize, true);
 //
-//			return $langObj;
+//			return langObj;
 //		}
 //
 //		/**
 //		* Create a language Object for a given language code
-//		* @param String $code
+//		* @param String code
 //		* @throws MWException
 //		* @return Language
 //		*/
-//		protected static function newFromCode($code) {
-//			if (!Language::isValidCode($code)) {
-//				throw new MWException("Invalid language code \"$code\"");
+//		protected static function newFromCode(code) {
+//			if (!XomwLanguage.isValidCode(code)) {
+//				throw new MWException("Invalid language code \"code\"");
 //			}
 //
-//			if (!Language::isValidBuiltInCode($code)) {
+//			if (!XomwLanguage.isValidBuiltInCode(code)) {
 //				// It's not possible to customise this code with class files, so
 //				// just return a Language Object. This is to support uselang= hacks.
-//				$lang = new Language;
-//				$lang->setCode($code);
-//				return $lang;
+//				lang = new Language;
+//				lang.setCode(code);
+//				return lang;
 //			}
 //
 //			// Check if there is a language class for the code
-//			$class = self::classFromCode($code);
-//			if (class_exists($class)) {
-//				$lang = new $class;
-//				return $lang;
+//			class = XomwLanguage.classFromCode(code);
+//			if (class_exists(class)) {
+//				lang = new class;
+//				return lang;
 //			}
 //
 //			// Keep trying the fallback list until we find an existing class
-//			$fallbacks = Language::getFallbacksFor($code);
-//			foreach ($fallbacks as $fallbackCode) {
-//				if (!Language::isValidBuiltInCode($fallbackCode)) {
-//					throw new MWException("Invalid fallback '$fallbackCode' in fallback sequence for '$code'");
+//			fallbacks = XomwLanguage.getFallbacksFor(code);
+//			foreach (fallbacks as fallbackCode) {
+//				if (!XomwLanguage.isValidBuiltInCode(fallbackCode)) {
+//					throw new MWException("Invalid fallback 'fallbackCode' in fallback sequence for 'code'");
 //				}
 //
-//				$class = self::classFromCode($fallbackCode);
-//				if (class_exists($class)) {
-//					$lang = new $class;
-//					$lang->setCode($code);
-//					return $lang;
+//				class = XomwLanguage.classFromCode(fallbackCode);
+//				if (class_exists(class)) {
+//					lang = new class;
+//					lang.setCode(code);
+//					return lang;
 //				}
 //			}
 //
-//			throw new MWException("Invalid fallback sequence for language '$code'");
+//			throw new MWException("Invalid fallback sequence for language 'code'");
 //		}
 //
 //		/**
 //		* Checks whether any localisation is available for that language tag
 //		* in MediaWiki (MessagesXx.php exists).
 //		*
-//		* @param String $code Language tag (in lower case)
+//		* @param String code Language tag (in lower case)
 //		* @return boolean Whether language is supported
 //		* @since 1.21
 //		*/
-//		public static function isSupportedLanguage($code) {
-//			if (!self::isValidBuiltInCode($code)) {
+//		public static function isSupportedLanguage(code) {
+//			if (!XomwLanguage.isValidBuiltInCode(code)) {
 //				return false;
 //			}
 //
-//			if ($code == 'qqq') {
+//			if (code == 'qqq') {
 //				return false;
 //			}
 //
-//			return is_readable(self::getMessagesFileName($code)) ||
-//				is_readable(self::getJsonMessagesFileName($code));
+//			return is_readable(XomwLanguage.getMessagesFileName(code)) ||
+//				is_readable(XomwLanguage.getJsonMessagesFileName(code));
 //		}
 //
 //		/**
@@ -299,26 +304,26 @@ public class XomwLanguage {
 //		* Based on regexes by Mark Davis of the Unicode Consortium:
 //		* http://unicode.org/repos/cldr/trunk/tools/java/org/unicode/cldr/util/data/langtagRegex.txt
 //		*
-//		* @param String $code
-//		* @param boolean $lenient Whether to allow '_' as separator. The default is only '-'.
+//		* @param String code
+//		* @param boolean lenient Whether to allow '_' as separator. The default is only '-'.
 //		*
 //		* @return boolean
 //		* @since 1.21
 //		*/
-//		public static function isWellFormedLanguageTag($code, $lenient = false) {
-//			$alpha = '[a-z]';
-//			$digit = '[0-9]';
-//			$alphanum = '[a-z0-9]';
-//			$x = 'x'; # private use singleton
-//			$singleton = '[a-wy-z]'; # other singleton
-//			$s = $lenient ? '[-_]' : '-';
+//		public static function isWellFormedLanguageTag(code, lenient = false) {
+//			alpha = '[a-z]';
+//			digit = '[0-9]';
+//			alphanum = '[a-z0-9]';
+//			x = 'x'; # private use singleton
+//			singleton = '[a-wy-z]'; # other singleton
+//			s = lenient ? '[-_]' : '-';
 //
-//			$language = "$alpha{2,8}|$alpha{2,3}$s$alpha{3}";
-//			$script = "$alpha{4}"; # ISO 15924
-//			$region = "(?:$alpha{2}|$digit{3})"; # ISO 3166-1 alpha-2 or UN M.49
-//			$variant = "(?:$alphanum{5,8}|$digit$alphanum{3})";
-//			$extension = "$singleton(?:$s$alphanum{2,8})+";
-//			$privateUse = "$x(?:$s$alphanum{1,8})+";
+//			language = "alpha{2,8}|alpha{2,3}salpha{3}";
+//			script = "alpha{4}"; # ISO 15924
+//			region = "(?:alpha{2}|digit{3})"; # ISO 3166-1 alpha-2 or UN M.49
+//			variant = "(?:alphanum{5,8}|digitalphanum{3})";
+//			extension = "singleton(?:salphanum{2,8})+";
+//			privateUse = "x(?:salphanum{1,8})+";
 //
 //			# Define certain grandfathered codes, since otherwise the regex is pretty useless.
 //			# Since these are limited, this is safe even later changes to the registry --
@@ -326,28 +331,28 @@ public class XomwLanguage {
 //			# the results from the capturing groups.
 //			# https://www.iana.org/assignments/language-subtag-registry
 //
-//			$grandfathered = "en{$s}GB{$s}oed"
-//				. "|i{$s}(?:ami|bnn|default|enochian|hak|klingon|lux|mingo|navajo|pwn|tao|tay|tsu)"
-//				. "|no{$s}(?:bok|nyn)"
-//				. "|sgn{$s}(?:BE{$s}(?:fr|nl)|CH{$s}de)"
-//				. "|zh{$s}min{$s}nan";
+//			grandfathered = "en{s}GB{s}oed"
+//				. "|i{s}(?:ami|bnn|default|enochian|hak|klingon|lux|mingo|navajo|pwn|tao|tay|tsu)"
+//				. "|no{s}(?:bok|nyn)"
+//				. "|sgn{s}(?:BE{s}(?:fr|nl)|CH{s}de)"
+//				. "|zh{s}min{s}nan";
 //
-//			$variantList = "$variant(?:$s$variant)*";
-//			$extensionList = "$extension(?:$s$extension)*";
+//			variantList = "variant(?:svariant)*";
+//			extensionList = "extension(?:sextension)*";
 //
-//			$langtag = "(?:($language)"
-//				. "(?:$s$script)?"
-//				. "(?:$s$region)?"
-//				. "(?:$s$variantList)?"
-//				. "(?:$s$extensionList)?"
-//				. "(?:$s$privateUse)?)";
+//			langtag = "(?:(language)"
+//				. "(?:sscript)?"
+//				. "(?:sregion)?"
+//				. "(?:svariantList)?"
+//				. "(?:sextensionList)?"
+//				. "(?:sprivateUse)?)";
 //
 //			# The final breakdown, with capturing groups for each of these components
 //			# The variants, extensions, grandfathered, and private-use may have interior '-'
 //
-//			$root = "^(?:$langtag|$privateUse|$grandfathered)$";
+//			root = "^(?:langtag|privateUse|grandfathered)";
 //
-//			return (boolean)preg_match("/$root/", strtolower($code));
+//			return (boolean)preg_match("/root/", strtolower(code));
 //		}
 //
 //		/**
@@ -355,108 +360,111 @@ public class XomwLanguage {
 //		* not it exists. This includes codes which are used solely for
 //		* customisation via the MediaWiki namespace.
 //		*
-//		* @param String $code
+//		* @param String code
 //		*
 //		* @return boolean
 //		*/
-//		public static function isValidCode($code) {
-//			static $cache = [];
-//			if (!isset($cache[$code])) {
+//		public static function isValidCode(code) {
+//			static cache = [];
+//			if (!isset(cache[code])) {
 //				// People think language codes are html safe, so enforce it.
 //				// Ideally we should only allow a-zA-Z0-9-
 //				// but, .+ and other chars are often used for {{int:}} hacks
 //				// see bugs T39564, T39587, T38938
-//				$cache[$code] =
+//				cache[code] =
 //					// Protect against path traversal
-//					strcspn($code, ":/\\\000&<>'\"") == strlen($code)
-//					&& !preg_match(MediaWikiTitleCodec::getTitleInvalidRegex(), $code);
+//					strcspn(code, ":/\\\000&<>'\"") == strlen(code)
+//					&& !preg_match(MediaWikiTitleCodec::getTitleInvalidRegex(), code);
 //			}
-//			return $cache[$code];
+//			return cache[code];
 //		}
-//
-//		/**
-//		* Returns true if a language code is of a valid form for the purposes of
-//		* @gplx.Internal protected customisation of MediaWiki, via Messages*.php or *.json.
-//		*
-//		* @param String $code
-//		*
-//		* @throws MWException
-//		* @since 1.18
-//		* @return boolean
-//		*/
-//		public static function isValidBuiltInCode($code) {
-//
-//			if (!is_string($code)) {
-//				if (is_object($code)) {
-//					$addmsg = " of class " . get_class($code);
+
+	/**
+	* Returns true if a language code is of a valid form for the purposes of
+	* @gplx.Internal protected customisation of MediaWiki, via Messages*.php or *.json.
+	*
+	* @param String code
+	*
+	* @throws MWException
+	* @since 1.18
+	* @return boolean
+	*/
+	public static boolean isValidBuiltInCode(String code) {
+
+		if (!XophpString_.is_string(code)) {
+//				if (XophpObject.is_object(code)) {
+//					addmsg = " of class " . get_class(code);
 //				} else {
-//					$addmsg = '';
+//					addmsg = "";
 //				}
-//				$type = gettype($code);
-//				throw new MWException(__METHOD__ . " must be passed a String, $type given$addmsg");
-//			}
-//
-//			return (boolean)preg_match('/^[a-z0-9-]{2,}$/', $code);
-//		}
+//				type = gettype(code);
+			String addmsg = "";
+			String type = "";
+			throw XomwMWException.New_by_method(XomwLanguage.class, "isValidBuildInCode", " must be passed a String, " + type + " given " + addmsg);
+		}
+
+		return XophpRegex_.preg_match_bool(Regx_adp_.new_("^[a-z0-9-]{2,}"), code, null, 0, 0);
+	}
 //
 //		/**
 //		* Returns true if a language code is an IETF tag known to MediaWiki.
 //		*
-//		* @param String $tag
+//		* @param String tag
 //		*
 //		* @since 1.21
 //		* @return boolean
 //		*/
-//		public static function isKnownLanguageTag($tag) {
+//		public static function isKnownLanguageTag(tag) {
 //			// Quick escape for invalid input to avoid exceptions down the line
 //			// when code tries to process tags which are not valid at all.
-//			if (!self::isValidBuiltInCode($tag)) {
+//			if (!XomwLanguage.isValidBuiltInCode(tag)) {
 //				return false;
 //			}
 //
-//			if (isset(MediaWiki\Languages\Data\Names::$names[$tag])
-//				|| self::fetchLanguageName($tag, $tag) != ''
+//			if (isset(MediaWiki\Languages\Data\Names::names[tag])
+//				|| XomwLanguage.fetchLanguageName(tag, tag) != ''
 //			) {
 //				return true;
 //			}
 //
 //			return false;
 //		}
-//
-//		/**
-//		* Get the LocalisationCache instance
-//		*
-//		* @return LocalisationCache
-//		*/
-//		public static function getLocalisationCache() {
-//			if (is_null(self::$dataCache)) {
-//				global $wgLocalisationCacheConf;
-//				$class = $wgLocalisationCacheConf['class'];
-//				self::$dataCache = new $class($wgLocalisationCacheConf);
+
+	/**
+	* Get the LocalisationCache instance
+	*
+	* @return LocalisationCache
+	*/
+	public static XomwLocalisationCache getLocalisationCache() {
+//			if (is_null(XomwLanguage.dataCache)) {
+//				global wgLocalisationCacheConf;
+//				class = wgLocalisationCacheConf['class'];
+//				XomwLanguage.dataCache = new class(wgLocalisationCacheConf);
 //			}
-//			return self::$dataCache;
-//		}
+		return XomwLanguage.dataCache;
+	}
 
 //		function __construct() {
-//			this.mConverter = new FakeConverter($this);
+//			this.mConverter = new FakeConverter(this);
 //			// Set the code to the name of the descendant
-//			if (get_class($this) == 'Language') {
+//			if (get_class(this) == 'Language') {
 //				this.mCode = 'en';
 //			} else {
-//				this.mCode = str_replace('_', '-', strtolower(substr(get_class($this), 8)));
+//				this.mCode = str_replace('_', '-', strtolower(substr(get_class(this), 8)));
 //			}
-//			self::getLocalisationCache();
+//			XomwLanguage.getLocalisationCache();
 //		}
 	public XomwLanguage(Xol_lang_itm xoLang) {
 		this.xoLang = xoLang;
+		this.mCode = xoLang.Key_str();
 	}
 
 //		/**
 //		* Reduce memory usage
 //		*/
 //		function __destruct() {
-//			foreach ($this as $name => $value) {
-//				unset(this.$name);
+//			foreach (this as name => value) {
+//				unset(this.name);
 //			}
 //		}
 //
@@ -472,15 +480,15 @@ public class XomwLanguage {
 //		* @since 1.19
 //		*/
 //		public function getFallbackLanguages() {
-//			return self::getFallbacksFor(this.mCode);
+//			return XomwLanguage.getFallbacksFor(this.mCode);
 //		}
 //
 //		/**
-//		* Exports $wgBookstoreListEn
+//		* Exports wgBookstoreListEn
 //		* @return array
 //		*/
 //		public function getBookstoreList() {
-//			return self::$dataCache->getItem(this.mCode, 'bookstoreList');
+//			return XomwLanguage.dataCache.getItem(this.mCode, 'bookstoreList');
 //		}
 
 	/**
@@ -491,28 +499,28 @@ public class XomwLanguage {
 	*/
 	public XomwNamespacesById getNamespaces() {
 		if (this.namespaceNames == null) {
-//				global $wgMetaNamespace, $wgMetaNamespaceTalk, $wgExtraNamespaces;
+//				global wgMetaNamespace, wgMetaNamespaceTalk, wgExtraNamespaces;
 //
 			XomwNamespacesById validNamespaces = XomwNamespace.getCanonicalNamespaces();
 //
-//				this.namespaceNames = $wgExtraNamespaces +
-//					self::$dataCache->getItem(this.mCode, 'namespaceNames');
-//				this.namespaceNames += $validNamespaces;
+//				this.namespaceNames = wgExtraNamespaces +
+//					XomwLanguage.dataCache.getItem(this.mCode, 'namespaceNames');
+//				this.namespaceNames += validNamespaces;
 			this.namespaceNames = validNamespaces;
 
-//				this.namespaceNames[NS_PROJECT] = $wgMetaNamespace;
-//				if ($wgMetaNamespaceTalk) {
-//					this.namespaceNames[NS_PROJECT_TALK] = $wgMetaNamespaceTalk;
+//				this.namespaceNames[NS_PROJECT] = wgMetaNamespace;
+//				if (wgMetaNamespaceTalk) {
+//					this.namespaceNames[NS_PROJECT_TALK] = wgMetaNamespaceTalk;
 //				} else {
-//					$talk = this.namespaceNames[NS_PROJECT_TALK];
+//					talk = this.namespaceNames[NS_PROJECT_TALK];
 //					this.namespaceNames[NS_PROJECT_TALK] =
-//						this.fixVariableInNamespace($talk);
+//						this.fixVariableInNamespace(talk);
 //				}
 //
 //				# Sometimes a language will be localised but not actually exist on this wiki.
-//				foreach (this.namespaceNames as $key => $text) {
-//					if (!isset($validNamespaces[$key])) {
-//						unset(this.namespaceNames[$key]);
+//				foreach (this.namespaceNames as key => text) {
+//					if (!isset(validNamespaces[key])) {
+//						unset(this.namespaceNames[key]);
 //					}
 //				}
 //
@@ -528,10 +536,10 @@ public class XomwLanguage {
 
 //		/**
 //		* Arbitrarily set all of the namespace names at once. Mainly used for testing
-//		* @param array $namespaces Array of namespaces (id => name)
+//		* @param array namespaces Array of namespaces (id => name)
 //		*/
-//		public function setNamespaces(array $namespaces) {
-//			this.namespaceNames = $namespaces;
+//		public function setNamespaces(array namespaces) {
+//			this.namespaceNames = namespaces;
 //			this.mNamespaceIds = null;
 //		}
 //
@@ -551,22 +559,22 @@ public class XomwLanguage {
 //		* @return array
 //		*/
 //		public function getFormattedNamespaces() {
-//			$ns = this.getNamespaces();
-//			foreach ($ns as $k => $v) {
-//				$ns[$k] = strtr($v, '_', ' ');
+//			ns = this.getNamespaces();
+//			foreach (ns as k => v) {
+//				ns[k] = strtr(v, '_', ' ');
 //			}
-//			return $ns;
+//			return ns;
 //		}
 
 	/**
 	* Get a namespace value by key
 	*
 	* 
-	* $mw_ns = $wgContLang->getNsText(NS_MEDIAWIKI);
-	* echo $mw_ns; // prints 'MediaWiki'
+	* mw_ns = wgContLang.getNsText(NS_MEDIAWIKI);
+	* echo mw_ns; // prints 'MediaWiki'
 	* 
 	*
-	* @param int $index The array key of the namespace to return
+	* @param int index The array key of the namespace to return
 	* @return String|boolean String if the namespace value exists, otherwise false
 	*/
 	public byte[] getNsText(int index) {
@@ -580,54 +588,54 @@ public class XomwLanguage {
 //		* producing output.
 //		*
 //		* 
-//		* $mw_ns = $wgContLang->getFormattedNsText(NS_MEDIAWIKI_TALK);
-//		* echo $mw_ns; // prints 'MediaWiki talk'
+//		* mw_ns = wgContLang.getFormattedNsText(NS_MEDIAWIKI_TALK);
+//		* echo mw_ns; // prints 'MediaWiki talk'
 //		* 
 //		*
-//		* @param int $index The array key of the namespace to return
+//		* @param int index The array key of the namespace to return
 //		* @return String Namespace name without underscores (empty String if namespace does not exist)
 //		*/
-//		public function getFormattedNsText($index) {
-//			$ns = this.getNsText($index);
-//			return strtr($ns, '_', ' ');
+//		public function getFormattedNsText(index) {
+//			ns = this.getNsText(index);
+//			return strtr(ns, '_', ' ');
 //		}
 //
 //		/**
 //		* Returns gender-dependent namespace alias if available.
-//		* See https://www.mediawiki.org/wiki/Manual:$wgExtraGenderNamespaces
-//		* @param int $index Namespace index
-//		* @param String $gender Gender key (male, female...)
+//		* See https://www.mediawiki.org/wiki/Manual:wgExtraGenderNamespaces
+//		* @param int index Namespace index
+//		* @param String gender Gender key (male, female...)
 //		* @return String
 //		* @since 1.18
 //		*/
-//		public function getGenderNsText($index, $gender) {
-//			global $wgExtraGenderNamespaces;
+//		public function getGenderNsText(index, gender) {
+//			global wgExtraGenderNamespaces;
 //
-//			$ns = $wgExtraGenderNamespaces +
-//				(array)self::$dataCache->getItem(this.mCode, 'namespaceGenderAliases');
+//			ns = wgExtraGenderNamespaces +
+//				(array)XomwLanguage.dataCache.getItem(this.mCode, 'namespaceGenderAliases');
 //
-//			return isset($ns[$index][$gender]) ? $ns[$index][$gender] : this.getNsText($index);
+//			return isset(ns[index][gender]) ? ns[index][gender] : this.getNsText(index);
 //		}
 //
 //		/**
 //		* Whether this language uses gender-dependent namespace aliases.
-//		* See https://www.mediawiki.org/wiki/Manual:$wgExtraGenderNamespaces
+//		* See https://www.mediawiki.org/wiki/Manual:wgExtraGenderNamespaces
 //		* @return boolean
 //		* @since 1.18
 //		*/
 //		public function needsGenderDistinction() {
-//			global $wgExtraGenderNamespaces, $wgExtraNamespaces;
-//			if (count($wgExtraGenderNamespaces) > 0) {
-//				// $wgExtraGenderNamespaces overrides everything
+//			global wgExtraGenderNamespaces, wgExtraNamespaces;
+//			if (count(wgExtraGenderNamespaces) > 0) {
+//				// wgExtraGenderNamespaces overrides everything
 //				return true;
-//			} elseif (isset($wgExtraNamespaces[NS_USER]) && isset($wgExtraNamespaces[NS_USER_TALK])) {
+//			} elseif (isset(wgExtraNamespaces[NS_USER]) && isset(wgExtraNamespaces[NS_USER_TALK])) {
 //				/// @todo There may be other gender namespace than NS_USER & NS_USER_TALK in the future
-//				// $wgExtraNamespaces overrides any gender aliases specified in i18n files
+//				// wgExtraNamespaces overrides any gender aliases specified in i18n files
 //				return false;
 //			} else {
 //				// Check what is in i18n files
-//				$aliases = self::$dataCache->getItem(this.mCode, 'namespaceGenderAliases');
-//				return count($aliases) > 0;
+//				aliases = XomwLanguage.dataCache.getItem(this.mCode, 'namespaceGenderAliases');
+//				return count(aliases) > 0;
 //			}
 //		}
 //
@@ -636,13 +644,13 @@ public class XomwLanguage {
 //		* Only matches namespace names for the current language, not the
 //		* canonical ones defined in Namespace.php.
 //		*
-//		* @param String $text
-//		* @return int|boolean An integer if $text is a valid value otherwise false
+//		* @param String text
+//		* @return int|boolean An integer if text is a valid value otherwise false
 //		*/
-//		function getLocalNsIndex($text) {
-//			$lctext = this.lc($text);
-//			$ids = this.getNamespaceIds();
-//			return isset($ids[$lctext]) ? $ids[$lctext] : false;
+//		function getLocalNsIndex(text) {
+//			lctext = this.lc(text);
+//			ids = this.getNamespaceIds();
+//			return isset(ids[lctext]) ? ids[lctext] : false;
 //		}
 
 	/**
@@ -652,40 +660,40 @@ public class XomwLanguage {
 		if (this.namespaceAliases == null) {
 			// XO.MW: MW uses two sets: "aliases" + "convertedNames" and then combines them; XO just uses one
 			this.namespaceAliases = new XomwNamespacesByName();
-//				$aliases = self::$dataCache->getItem(this.mCode, 'namespaceAliases');
-//				if (!$aliases) {
-//					$aliases = [];
+//				aliases = XomwLanguage.dataCache.getItem(this.mCode, 'namespaceAliases');
+//				if (!aliases) {
+//					aliases = [];
 //				} else {
-//					foreach ($aliases as $name => $index) {
-//						if ($index == NS_PROJECT_TALK) {
-//							unset($aliases[$name]);
-//							$name = this.fixVariableInNamespace($name);
-//							$aliases[$name] = $index;
+//					foreach (aliases as name => index) {
+//						if (index == NS_PROJECT_TALK) {
+//							unset(aliases[name]);
+//							name = this.fixVariableInNamespace(name);
+//							aliases[name] = index;
 //						}
 //					}
 //				}
 //
-//				global $wgExtraGenderNamespaces;
-//				$genders = $wgExtraGenderNamespaces +
-//					(array)self::$dataCache->getItem(this.mCode, 'namespaceGenderAliases');
-//				foreach ($genders as $index => $forms) {
-//					foreach ($forms as $alias) {
-//						$aliases[$alias] = $index;
+//				global wgExtraGenderNamespaces;
+//				genders = wgExtraGenderNamespaces +
+//					(array)XomwLanguage.dataCache.getItem(this.mCode, 'namespaceGenderAliases');
+//				foreach (genders as index => forms) {
+//					foreach (forms as alias) {
+//						aliases[alias] = index;
 //					}
 //				}
 
 			// Also add converted namespace names as aliases, to avoid confusion.
-//				$convertedNames = [];
-//				foreach (this.getVariants() as $variant) {
-//					if ($variant == this.mCode) {
+//				convertedNames = [];
+//				foreach (this.getVariants() as variant) {
+//					if (variant == this.mCode) {
 //						continue;
 //					}
-//					foreach (this.getNamespaces() as $ns => $_) {
-//						$convertedNames[this.getConverter()->convertNamespace($ns, $variant)] = $ns;
+//					foreach (this.getNamespaces() as ns => _) {
+//						convertedNames[this.getConverter().convertNamespace(ns, variant)] = ns;
 //					}
 //				}
 //
-//				this.namespaceAliases = $aliases + $convertedNames;
+//				this.namespaceAliases = aliases + convertedNames;
 		}
 
 		return this.namespaceAliases;
@@ -727,8 +735,8 @@ public class XomwLanguage {
 	* Get a namespace key by value, case insensitive.  Canonical namespace
 	* @Override names custom ones defined for the current language.
 	*
-	* @param String $text
-	* @return int|boolean An integer if $text is a valid value otherwise false
+	* @param String text
+	* @return int|boolean An integer if text is a valid value otherwise false
 	*/
 	public int getNsIndex(byte[] text) {
 		byte[] lctext = this.lc(text);
@@ -743,21 +751,21 @@ public class XomwLanguage {
 //		/**
 //		* short names for language variants used for language conversion links.
 //		*
-//		* @param String $code
-//		* @param boolean $usemsg Use the "variantname-xyz" message if it exists
+//		* @param String code
+//		* @param boolean usemsg Use the "variantname-xyz" message if it exists
 //		* @return String
 //		*/
-//		public function getVariantname($code, $usemsg = true) {
-//			$msg = "variantname-$code";
-//			if ($usemsg && wfMessage($msg)->exists()) {
-//				return this.getMessageFromDB($msg);
+//		public function getVariantname(code, usemsg = true) {
+//			msg = "variantname-code";
+//			if (usemsg && wfMessage(msg).exists()) {
+//				return this.getMessageFromDB(msg);
 //			}
-//			$name = self::fetchLanguageName($code);
-//			if ($name) {
-//				return $name; # if it's defined as a language name, show that
+//			name = XomwLanguage.fetchLanguageName(code);
+//			if (name) {
+//				return name; # if it's defined as a language name, show that
 //			} else {
 //				# otherwise, output the language code
-//				return $code;
+//				return code;
 //			}
 //		}
 //
@@ -765,26 +773,26 @@ public class XomwLanguage {
 //		* @return array
 //		*/
 //		public function getDatePreferences() {
-//			return self::$dataCache->getItem(this.mCode, 'datePreferences');
+//			return XomwLanguage.dataCache.getItem(this.mCode, 'datePreferences');
 //		}
 //
 //		/**
 //		* @return array
 //		*/
 //		function getDateFormats() {
-//			return self::$dataCache->getItem(this.mCode, 'dateFormats');
+//			return XomwLanguage.dataCache.getItem(this.mCode, 'dateFormats');
 //		}
 //
 //		/**
 //		* @return array|String
 //		*/
 //		public function getDefaultDateFormat() {
-//			$df = self::$dataCache->getItem(this.mCode, 'defaultDateFormat');
-//			if ($df == 'dmy or mdy') {
-//				global $wgAmericanDates;
-//				return $wgAmericanDates ? 'mdy' : 'dmy';
+//			df = XomwLanguage.dataCache.getItem(this.mCode, 'defaultDateFormat');
+//			if (df == 'dmy or mdy') {
+//				global wgAmericanDates;
+//				return wgAmericanDates ? 'mdy' : 'dmy';
 //			} else {
-//				return $df;
+//				return df;
 //			}
 //		}
 //
@@ -792,15 +800,15 @@ public class XomwLanguage {
 //		* @return array
 //		*/
 //		public function getDatePreferenceMigrationMap() {
-//			return self::$dataCache->getItem(this.mCode, 'datePreferenceMigrationMap');
+//			return XomwLanguage.dataCache.getItem(this.mCode, 'datePreferenceMigrationMap');
 //		}
 //
 //		/**
-//		* @param String $image
+//		* @param String image
 //		* @return array|null
 //		*/
-//		function getImageFile($image) {
-//			return self::$dataCache->getSubitem(this.mCode, 'imageFiles', $image);
+//		function getImageFile(image) {
+//			return XomwLanguage.dataCache.getSubitem(this.mCode, 'imageFiles', image);
 //		}
 //
 //		/**
@@ -808,258 +816,258 @@ public class XomwLanguage {
 //		* @since 1.24
 //		*/
 //		public function getImageFiles() {
-//			return self::$dataCache->getItem(this.mCode, 'imageFiles');
+//			return XomwLanguage.dataCache.getItem(this.mCode, 'imageFiles');
 //		}
 //
 //		/**
 //		* @return array
 //		*/
 //		public function getExtraUserToggles() {
-//			return (array)self::$dataCache->getItem(this.mCode, 'extraUserToggles');
+//			return (array)XomwLanguage.dataCache.getItem(this.mCode, 'extraUserToggles');
 //		}
 //
 //		/**
-//		* @param String $tog
+//		* @param String tog
 //		* @return String
 //		*/
-//		function getUserToggle($tog) {
-//			return this.getMessageFromDB("tog-$tog");
+//		function getUserToggle(tog) {
+//			return this.getMessageFromDB("tog-tog");
 //		}
 //
 //		/**
 //		* Get an array of language names, indexed by code.
-//		* @param null|String $inLanguage Code of language in which to return the names
+//		* @param null|String inLanguage Code of language in which to return the names
 //		*		Use null for autonyms (native names)
-//		* @param String $include One of:
+//		* @param String include One of:
 //		*		'all' all available languages
 //		*		'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default)
 //		*		'mwfile' only if the language is in 'mw' *and* has a message file
 //		* @return array Language code => language name
 //		* @since 1.20
 //		*/
-//		public static function fetchLanguageNames($inLanguage = null, $include = 'mw') {
-//			$cacheKey = $inLanguage == null ? 'null' : $inLanguage;
-//			$cacheKey .= ":$include";
-//			if (self::$languageNameCache == null) {
-//				self::$languageNameCache = new HashBagOStuff([ 'maxKeys' => 20 ]);
+//		public static function fetchLanguageNames(inLanguage = null, include = 'mw') {
+//			cacheKey = inLanguage == null ? 'null' : inLanguage;
+//			cacheKey .= ":include";
+//			if (XomwLanguage.languageNameCache == null) {
+//				XomwLanguage.languageNameCache = new HashBagOStuff([ 'maxKeys' => 20 ]);
 //			}
 //
-//			$ret = self::$languageNameCache->get($cacheKey);
-//			if (!$ret) {
-//				$ret = self::fetchLanguageNamesUncached($inLanguage, $include);
-//				self::$languageNameCache->set($cacheKey, $ret);
+//			ret = XomwLanguage.languageNameCache.get(cacheKey);
+//			if (!ret) {
+//				ret = XomwLanguage.fetchLanguageNamesUncached(inLanguage, include);
+//				XomwLanguage.languageNameCache.set(cacheKey, ret);
 //			}
-//			return $ret;
+//			return ret;
 //		}
 //
 //		/**
 //		* Uncached helper for fetchLanguageNames
-//		* @param null|String $inLanguage Code of language in which to return the names
+//		* @param null|String inLanguage Code of language in which to return the names
 //		*		Use null for autonyms (native names)
-//		* @param String $include One of:
+//		* @param String include One of:
 //		*		'all' all available languages
 //		*		'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default)
 //		*		'mwfile' only if the language is in 'mw' *and* has a message file
 //		* @return array Language code => language name
 //		*/
-//		private static function fetchLanguageNamesUncached($inLanguage = null, $include = 'mw') {
-//			global $wgExtraLanguageNames;
+//		private static function fetchLanguageNamesUncached(inLanguage = null, include = 'mw') {
+//			global wgExtraLanguageNames;
 //
 //			// If passed an invalid language code to use, fallback to en
-//			if ($inLanguage != null && !Language::isValidCode($inLanguage)) {
-//				$inLanguage = 'en';
+//			if (inLanguage != null && !XomwLanguage.isValidCode(inLanguage)) {
+//				inLanguage = 'en';
 //			}
 //
-//			$names = [];
+//			names = [];
 //
-//			if ($inLanguage) {
-//				# TODO: also include when $inLanguage is null, when this code is more efficient
-//				Hooks::run('LanguageGetTranslatedLanguageNames', [ &$names, $inLanguage ]);
+//			if (inLanguage) {
+//				# TODO: also include when inLanguage is null, when this code is more efficient
+//				Hooks::run('LanguageGetTranslatedLanguageNames', [ &names, inLanguage ]);
 //			}
 //
-//			$mwNames = $wgExtraLanguageNames + MediaWiki\Languages\Data\Names::$names;
-//			foreach ($mwNames as $mwCode => $mwName) {
+//			mwNames = wgExtraLanguageNames + MediaWiki\Languages\Data\Names::names;
+//			foreach (mwNames as mwCode => mwName) {
 //				# - Prefer own MediaWiki native name when not using the hook
 //				# - For other names just add if not added through the hook
-//				if ($mwCode == $inLanguage || !isset($names[$mwCode])) {
-//					$names[$mwCode] = $mwName;
+//				if (mwCode == inLanguage || !isset(names[mwCode])) {
+//					names[mwCode] = mwName;
 //				}
 //			}
 //
-//			if ($include == 'all') {
-//				ksort($names);
-//				return $names;
+//			if (include == 'all') {
+//				ksort(names);
+//				return names;
 //			}
 //
-//			$returnMw = [];
-//			$coreCodes = array_keys($mwNames);
-//			foreach ($coreCodes as $coreCode) {
-//				$returnMw[$coreCode] = $names[$coreCode];
+//			returnMw = [];
+//			coreCodes = array_keys(mwNames);
+//			foreach (coreCodes as coreCode) {
+//				returnMw[coreCode] = names[coreCode];
 //			}
 //
-//			if ($include == 'mwfile') {
-//				$namesMwFile = [];
+//			if (include == 'mwfile') {
+//				namesMwFile = [];
 //				# We do this using a foreach over the codes instead of a directory
 //				# loop so that messages files in extensions will work correctly.
-//				foreach ($returnMw as $code => $value) {
-//					if (is_readable(self::getMessagesFileName($code))
-//						|| is_readable(self::getJsonMessagesFileName($code))
+//				foreach (returnMw as code => value) {
+//					if (is_readable(XomwLanguage.getMessagesFileName(code))
+//						|| is_readable(XomwLanguage.getJsonMessagesFileName(code))
 //					) {
-//						$namesMwFile[$code] = $names[$code];
+//						namesMwFile[code] = names[code];
 //					}
 //				}
 //
-//				ksort($namesMwFile);
-//				return $namesMwFile;
+//				ksort(namesMwFile);
+//				return namesMwFile;
 //			}
 //
-//			ksort($returnMw);
+//			ksort(returnMw);
 //			# 'mw' option; default if it's not one of the other two options (all/mwfile)
-//			return $returnMw;
+//			return returnMw;
 //		}
 //
 //		/**
-//		* @param String $code The code of the language for which to get the name
-//		* @param null|String $inLanguage Code of language in which to return the name (null for autonyms)
-//		* @param String $include 'all', 'mw' or 'mwfile'; see fetchLanguageNames()
+//		* @param String code The code of the language for which to get the name
+//		* @param null|String inLanguage Code of language in which to return the name (null for autonyms)
+//		* @param String include 'all', 'mw' or 'mwfile'; see fetchLanguageNames()
 //		* @return String Language name or empty
 //		* @since 1.20
 //		*/
-//		public static function fetchLanguageName($code, $inLanguage = null, $include = 'all') {
-//			$code = strtolower($code);
-//			$array = self::fetchLanguageNames($inLanguage, $include);
-//			return !array_key_exists($code, $array) ? '' : $array[$code];
+//		public static function fetchLanguageName(code, inLanguage = null, include = 'all') {
+//			code = strtolower(code);
+//			array = XomwLanguage.fetchLanguageNames(inLanguage, include);
+//			return !array_key_exists(code, array) ? '' : array[code];
 //		}
 //
 //		/**
 //		* Get a message from the MediaWiki namespace.
 //		*
-//		* @param String $msg Message name
+//		* @param String msg Message name
 //		* @return String
 //		*/
-//		public function getMessageFromDB($msg) {
-//			return this.msg($msg)->text();
+//		public function getMessageFromDB(msg) {
+//			return this.msg(msg).text();
 //		}
 //
 //		/**
 //		* Get message Object in this language. Only for use inside this class.
 //		*
-//		* @param String $msg Message name
+//		* @param String msg Message name
 //		* @return Message
 //		*/
-//		protected function msg($msg) {
-//			return wfMessage($msg)->inLanguage($this);
+//		protected function msg(msg) {
+//			return wfMessage(msg).inLanguage(this);
 //		}
 //
 //		/**
-//		* @param String $key
+//		* @param String key
 //		* @return String
 //		*/
-//		public function getMonthName($key) {
-//			return this.getMessageFromDB(self::$mMonthMsgs[$key - 1]);
+//		public function getMonthName(key) {
+//			return this.getMessageFromDB(XomwLanguage.mMonthMsgs[key - 1]);
 //		}
 //
 //		/**
 //		* @return array
 //		*/
 //		public function getMonthNamesArray() {
-//			$monthNames = [ '' ];
-//			for ($i = 1; $i < 13; $i++) {
-//				$monthNames[] = this.getMonthName($i);
+//			monthNames = [ '' ];
+//			for (i = 1; i < 13; i++) {
+//				monthNames[] = this.getMonthName(i);
 //			}
-//			return $monthNames;
+//			return monthNames;
 //		}
 //
 //		/**
-//		* @param String $key
+//		* @param String key
 //		* @return String
 //		*/
-//		public function getMonthNameGen($key) {
-//			return this.getMessageFromDB(self::$mMonthGenMsgs[$key - 1]);
+//		public function getMonthNameGen(key) {
+//			return this.getMessageFromDB(XomwLanguage.mMonthGenMsgs[key - 1]);
 //		}
 //
 //		/**
-//		* @param String $key
+//		* @param String key
 //		* @return String
 //		*/
-//		public function getMonthAbbreviation($key) {
-//			return this.getMessageFromDB(self::$mMonthAbbrevMsgs[$key - 1]);
+//		public function getMonthAbbreviation(key) {
+//			return this.getMessageFromDB(XomwLanguage.mMonthAbbrevMsgs[key - 1]);
 //		}
 //
 //		/**
 //		* @return array
 //		*/
 //		public function getMonthAbbreviationsArray() {
-//			$monthNames = [ '' ];
-//			for ($i = 1; $i < 13; $i++) {
-//				$monthNames[] = this.getMonthAbbreviation($i);
+//			monthNames = [ '' ];
+//			for (i = 1; i < 13; i++) {
+//				monthNames[] = this.getMonthAbbreviation(i);
 //			}
-//			return $monthNames;
+//			return monthNames;
 //		}
 //
 //		/**
-//		* @param String $key
+//		* @param String key
 //		* @return String
 //		*/
-//		public function getWeekdayName($key) {
-//			return this.getMessageFromDB(self::$mWeekdayMsgs[$key - 1]);
+//		public function getWeekdayName(key) {
+//			return this.getMessageFromDB(XomwLanguage.mWeekdayMsgs[key - 1]);
 //		}
 //
 //		/**
-//		* @param String $key
+//		* @param String key
 //		* @return String
 //		*/
-//		function getWeekdayAbbreviation($key) {
-//			return this.getMessageFromDB(self::$mWeekdayAbbrevMsgs[$key - 1]);
+//		function getWeekdayAbbreviation(key) {
+//			return this.getMessageFromDB(XomwLanguage.mWeekdayAbbrevMsgs[key - 1]);
 //		}
 //
 //		/**
-//		* @param String $key
+//		* @param String key
 //		* @return String
 //		*/
-//		function getIranianCalendarMonthName($key) {
-//			return this.getMessageFromDB(self::$mIranianCalendarMonthMsgs[$key - 1]);
+//		function getIranianCalendarMonthName(key) {
+//			return this.getMessageFromDB(XomwLanguage.mIranianCalendarMonthMsgs[key - 1]);
 //		}
 //
 //		/**
-//		* @param String $key
+//		* @param String key
 //		* @return String
 //		*/
-//		function getHebrewCalendarMonthName($key) {
-//			return this.getMessageFromDB(self::$mHebrewCalendarMonthMsgs[$key - 1]);
+//		function getHebrewCalendarMonthName(key) {
+//			return this.getMessageFromDB(XomwLanguage.mHebrewCalendarMonthMsgs[key - 1]);
 //		}
 //
 //		/**
-//		* @param String $key
+//		* @param String key
 //		* @return String
 //		*/
-//		function getHebrewCalendarMonthNameGen($key) {
-//			return this.getMessageFromDB(self::$mHebrewCalendarMonthGenMsgs[$key - 1]);
+//		function getHebrewCalendarMonthNameGen(key) {
+//			return this.getMessageFromDB(XomwLanguage.mHebrewCalendarMonthGenMsgs[key - 1]);
 //		}
 //
 //		/**
-//		* @param String $key
+//		* @param String key
 //		* @return String
 //		*/
-//		function getHijriCalendarMonthName($key) {
-//			return this.getMessageFromDB(self::$mHijriCalendarMonthMsgs[$key - 1]);
+//		function getHijriCalendarMonthName(key) {
+//			return this.getMessageFromDB(XomwLanguage.mHijriCalendarMonthMsgs[key - 1]);
 //		}
 //
 //		/**
-//		* Pass through result from $dateTimeObj->format()
-//		* @param DateTime|boolean|null &$dateTimeObj
-//		* @param String $ts
-//		* @param DateTimeZone|boolean|null $zone
-//		* @param String $code
+//		* Pass through result from dateTimeObj.format()
+//		* @param DateTime|boolean|null &dateTimeObj
+//		* @param String ts
+//		* @param DateTimeZone|boolean|null zone
+//		* @param String code
 //		* @return String
 //		*/
-//		private static function dateTimeObjFormat(&$dateTimeObj, $ts, $zone, $code) {
-//			if (!$dateTimeObj) {
-//				$dateTimeObj = DateTime::createFromFormat(
-//					'YmdHis', $ts, $zone ?: new DateTimeZone('UTC')
+//		private static function dateTimeObjFormat(&dateTimeObj, ts, zone, code) {
+//			if (!dateTimeObj) {
+//				dateTimeObj = DateTime::createFromFormat(
+//					'YmdHis', ts, zone ?: new DateTimeZone('UTC')
 //				);
 //			}
-//			return $dateTimeObj->format($code);
+//			return dateTimeObj.format(code);
 //		}
 //
 //		/**
@@ -1117,491 +1125,491 @@ public class XomwLanguage {
 //		*
 //		* Input timestamp is assumed to be pre-normalized to the desired local
 //		* time zone, if any. Note that the format characters crUeIOPTZ will assume
-//		* $ts is UTC if $zone is not given.
+//		* ts is UTC if zone is not given.
 //		*
-//		* @param String $format
-//		* @param String $ts 14-character timestamp
+//		* @param String format
+//		* @param String ts 14-character timestamp
 //		*      YYYYMMDDHHMMSS
 //		*      01234567890123
-//		* @param DateTimeZone $zone Timezone of $ts
-//		* @param[out] int $ttl The amount of time (in seconds) the output may be cached for.
-//		* Only makes sense if $ts is the current time.
+//		* @param DateTimeZone zone Timezone of ts
+//		* @param[out] int ttl The amount of time (in seconds) the output may be cached for.
+//		* Only makes sense if ts is the current time.
 //		* @todo handling of "o" format character for Iranian, Hebrew, Hijri & Thai?
 //		*
 //		* @throws MWException
 //		* @return String
 //		*/
-//		public function sprintfDate($format, $ts, DateTimeZone $zone = null, &$ttl = 'unused') {
-//			$s = '';
-//			$raw = false;
-//			$roman = false;
-//			$hebrewNum = false;
-//			$dateTimeObj = false;
-//			$rawToggle = false;
-//			$iranian = false;
-//			$hebrew = false;
-//			$hijri = false;
-//			$thai = false;
-//			$minguo = false;
-//			$tenno = false;
+//		public function sprintfDate(format, ts, DateTimeZone zone = null, &ttl = 'unused') {
+//			s = '';
+//			raw = false;
+//			roman = false;
+//			hebrewNum = false;
+//			dateTimeObj = false;
+//			rawToggle = false;
+//			iranian = false;
+//			hebrew = false;
+//			hijri = false;
+//			thai = false;
+//			minguo = false;
+//			tenno = false;
 //
-//			$usedSecond = false;
-//			$usedMinute = false;
-//			$usedHour = false;
-//			$usedAMPM = false;
-//			$usedDay = false;
-//			$usedWeek = false;
-//			$usedMonth = false;
-//			$usedYear = false;
-//			$usedISOYear = false;
-//			$usedIsLeapYear = false;
+//			usedSecond = false;
+//			usedMinute = false;
+//			usedHour = false;
+//			usedAMPM = false;
+//			usedDay = false;
+//			usedWeek = false;
+//			usedMonth = false;
+//			usedYear = false;
+//			usedISOYear = false;
+//			usedIsLeapYear = false;
 //
-//			$usedHebrewMonth = false;
-//			$usedIranianMonth = false;
-//			$usedHijriMonth = false;
-//			$usedHebrewYear = false;
-//			$usedIranianYear = false;
-//			$usedHijriYear = false;
-//			$usedTennoYear = false;
+//			usedHebrewMonth = false;
+//			usedIranianMonth = false;
+//			usedHijriMonth = false;
+//			usedHebrewYear = false;
+//			usedIranianYear = false;
+//			usedHijriYear = false;
+//			usedTennoYear = false;
 //
-//			if (strlen($ts) != 14) {
-//				throw new MWException(__METHOD__ . ": The timestamp $ts should have 14 characters");
+//			if (strlen(ts) != 14) {
+//				throw new MWException(__METHOD__ . ": The timestamp ts should have 14 characters");
 //			}
 //
-//			if (!ctype_digit($ts)) {
-//				throw new MWException(__METHOD__ . ": The timestamp $ts should be a number");
+//			if (!ctype_digit(ts)) {
+//				throw new MWException(__METHOD__ . ": The timestamp ts should be a number");
 //			}
 //
-//			$formatLength = strlen($format);
-//			for ($p = 0; $p < $formatLength; $p++) {
-//				$num = false;
-//				$code = $format[$p];
-//				if ($code == 'x' && $p < $formatLength - 1) {
-//					$code .= $format[++$p];
+//			formatLength = strlen(format);
+//			for (p = 0; p < formatLength; p++) {
+//				num = false;
+//				code = format[p];
+//				if (code == 'x' && p < formatLength - 1) {
+//					code .= format[++p];
 //				}
 //
-//				if (($code == 'xi'
-//						|| $code == 'xj'
-//						|| $code == 'xk'
-//						|| $code == 'xm'
-//						|| $code == 'xo'
-//						|| $code == 'xt')
-//					&& $p < $formatLength - 1) {
-//					$code .= $format[++$p];
+//				if ((code == 'xi'
+//						|| code == 'xj'
+//						|| code == 'xk'
+//						|| code == 'xm'
+//						|| code == 'xo'
+//						|| code == 'xt')
+//					&& p < formatLength - 1) {
+//					code .= format[++p];
 //				}
 //
-//				switch ($code) {
+//				switch (code) {
 //					case 'xx':
-//						$s .= 'x';
+//						s .= 'x';
 //						break;
 //					case 'xn':
-//						$raw = true;
+//						raw = true;
 //						break;
 //					case 'xN':
-//						$rawToggle = !$rawToggle;
+//						rawToggle = !rawToggle;
 //						break;
 //					case 'xr':
-//						$roman = true;
+//						roman = true;
 //						break;
 //					case 'xh':
-//						$hebrewNum = true;
+//						hebrewNum = true;
 //						break;
 //					case 'xg':
-//						$usedMonth = true;
-//						$s .= this.getMonthNameGen(substr($ts, 4, 2));
+//						usedMonth = true;
+//						s .= this.getMonthNameGen(substr(ts, 4, 2));
 //						break;
 //					case 'xjx':
-//						$usedHebrewMonth = true;
-//						if (!$hebrew) {
-//							$hebrew = self::tsToHebrew($ts);
+//						usedHebrewMonth = true;
+//						if (!hebrew) {
+//							hebrew = XomwLanguage.tsToHebrew(ts);
 //						}
-//						$s .= this.getHebrewCalendarMonthNameGen($hebrew[1]);
+//						s .= this.getHebrewCalendarMonthNameGen(hebrew[1]);
 //						break;
 //					case 'd':
-//						$usedDay = true;
-//						$num = substr($ts, 6, 2);
+//						usedDay = true;
+//						num = substr(ts, 6, 2);
 //						break;
 //					case 'D':
-//						$usedDay = true;
-//						$s .= this.getWeekdayAbbreviation(
-//							Language::dateTimeObjFormat($dateTimeObj, $ts, $zone, 'w') + 1
+//						usedDay = true;
+//						s .= this.getWeekdayAbbreviation(
+//							XomwLanguage.dateTimeObjFormat(dateTimeObj, ts, zone, 'w') + 1
 //						);
 //						break;
 //					case 'j':
-//						$usedDay = true;
-//						$num = intval(substr($ts, 6, 2));
+//						usedDay = true;
+//						num = intval(substr(ts, 6, 2));
 //						break;
 //					case 'xij':
-//						$usedDay = true;
-//						if (!$iranian) {
-//							$iranian = self::tsToIranian($ts);
+//						usedDay = true;
+//						if (!iranian) {
+//							iranian = XomwLanguage.tsToIranian(ts);
 //						}
-//						$num = $iranian[2];
+//						num = iranian[2];
 //						break;
 //					case 'xmj':
-//						$usedDay = true;
-//						if (!$hijri) {
-//							$hijri = self::tsToHijri($ts);
+//						usedDay = true;
+//						if (!hijri) {
+//							hijri = XomwLanguage.tsToHijri(ts);
 //						}
-//						$num = $hijri[2];
+//						num = hijri[2];
 //						break;
 //					case 'xjj':
-//						$usedDay = true;
-//						if (!$hebrew) {
-//							$hebrew = self::tsToHebrew($ts);
+//						usedDay = true;
+//						if (!hebrew) {
+//							hebrew = XomwLanguage.tsToHebrew(ts);
 //						}
-//						$num = $hebrew[2];
+//						num = hebrew[2];
 //						break;
 //					case 'l':
-//						$usedDay = true;
-//						$s .= this.getWeekdayName(
-//							Language::dateTimeObjFormat($dateTimeObj, $ts, $zone, 'w') + 1
+//						usedDay = true;
+//						s .= this.getWeekdayName(
+//							XomwLanguage.dateTimeObjFormat(dateTimeObj, ts, zone, 'w') + 1
 //						);
 //						break;
 //					case 'F':
-//						$usedMonth = true;
-//						$s .= this.getMonthName(substr($ts, 4, 2));
+//						usedMonth = true;
+//						s .= this.getMonthName(substr(ts, 4, 2));
 //						break;
 //					case 'xiF':
-//						$usedIranianMonth = true;
-//						if (!$iranian) {
-//							$iranian = self::tsToIranian($ts);
+//						usedIranianMonth = true;
+//						if (!iranian) {
+//							iranian = XomwLanguage.tsToIranian(ts);
 //						}
-//						$s .= this.getIranianCalendarMonthName($iranian[1]);
+//						s .= this.getIranianCalendarMonthName(iranian[1]);
 //						break;
 //					case 'xmF':
-//						$usedHijriMonth = true;
-//						if (!$hijri) {
-//							$hijri = self::tsToHijri($ts);
+//						usedHijriMonth = true;
+//						if (!hijri) {
+//							hijri = XomwLanguage.tsToHijri(ts);
 //						}
-//						$s .= this.getHijriCalendarMonthName($hijri[1]);
+//						s .= this.getHijriCalendarMonthName(hijri[1]);
 //						break;
 //					case 'xjF':
-//						$usedHebrewMonth = true;
-//						if (!$hebrew) {
-//							$hebrew = self::tsToHebrew($ts);
+//						usedHebrewMonth = true;
+//						if (!hebrew) {
+//							hebrew = XomwLanguage.tsToHebrew(ts);
 //						}
-//						$s .= this.getHebrewCalendarMonthName($hebrew[1]);
+//						s .= this.getHebrewCalendarMonthName(hebrew[1]);
 //						break;
 //					case 'm':
-//						$usedMonth = true;
-//						$num = substr($ts, 4, 2);
+//						usedMonth = true;
+//						num = substr(ts, 4, 2);
 //						break;
 //					case 'M':
-//						$usedMonth = true;
-//						$s .= this.getMonthAbbreviation(substr($ts, 4, 2));
+//						usedMonth = true;
+//						s .= this.getMonthAbbreviation(substr(ts, 4, 2));
 //						break;
 //					case 'n':
-//						$usedMonth = true;
-//						$num = intval(substr($ts, 4, 2));
+//						usedMonth = true;
+//						num = intval(substr(ts, 4, 2));
 //						break;
 //					case 'xin':
-//						$usedIranianMonth = true;
-//						if (!$iranian) {
-//							$iranian = self::tsToIranian($ts);
+//						usedIranianMonth = true;
+//						if (!iranian) {
+//							iranian = XomwLanguage.tsToIranian(ts);
 //						}
-//						$num = $iranian[1];
+//						num = iranian[1];
 //						break;
 //					case 'xmn':
-//						$usedHijriMonth = true;
-//						if (!$hijri) {
-//							$hijri = self::tsToHijri($ts);
+//						usedHijriMonth = true;
+//						if (!hijri) {
+//							hijri = XomwLanguage.tsToHijri(ts);
 //						}
-//						$num = $hijri[1];
+//						num = hijri[1];
 //						break;
 //					case 'xjn':
-//						$usedHebrewMonth = true;
-//						if (!$hebrew) {
-//							$hebrew = self::tsToHebrew($ts);
+//						usedHebrewMonth = true;
+//						if (!hebrew) {
+//							hebrew = XomwLanguage.tsToHebrew(ts);
 //						}
-//						$num = $hebrew[1];
+//						num = hebrew[1];
 //						break;
 //					case 'xjt':
-//						$usedHebrewMonth = true;
-//						if (!$hebrew) {
-//							$hebrew = self::tsToHebrew($ts);
+//						usedHebrewMonth = true;
+//						if (!hebrew) {
+//							hebrew = XomwLanguage.tsToHebrew(ts);
 //						}
-//						$num = $hebrew[3];
+//						num = hebrew[3];
 //						break;
 //					case 'Y':
-//						$usedYear = true;
-//						$num = substr($ts, 0, 4);
+//						usedYear = true;
+//						num = substr(ts, 0, 4);
 //						break;
 //					case 'xiY':
-//						$usedIranianYear = true;
-//						if (!$iranian) {
-//							$iranian = self::tsToIranian($ts);
+//						usedIranianYear = true;
+//						if (!iranian) {
+//							iranian = XomwLanguage.tsToIranian(ts);
 //						}
-//						$num = $iranian[0];
+//						num = iranian[0];
 //						break;
 //					case 'xmY':
-//						$usedHijriYear = true;
-//						if (!$hijri) {
-//							$hijri = self::tsToHijri($ts);
+//						usedHijriYear = true;
+//						if (!hijri) {
+//							hijri = XomwLanguage.tsToHijri(ts);
 //						}
-//						$num = $hijri[0];
+//						num = hijri[0];
 //						break;
 //					case 'xjY':
-//						$usedHebrewYear = true;
-//						if (!$hebrew) {
-//							$hebrew = self::tsToHebrew($ts);
+//						usedHebrewYear = true;
+//						if (!hebrew) {
+//							hebrew = XomwLanguage.tsToHebrew(ts);
 //						}
-//						$num = $hebrew[0];
+//						num = hebrew[0];
 //						break;
 //					case 'xkY':
-//						$usedYear = true;
-//						if (!$thai) {
-//							$thai = self::tsToYear($ts, 'thai');
+//						usedYear = true;
+//						if (!thai) {
+//							thai = XomwLanguage.tsToYear(ts, 'thai');
 //						}
-//						$num = $thai[0];
+//						num = thai[0];
 //						break;
 //					case 'xoY':
-//						$usedYear = true;
-//						if (!$minguo) {
-//							$minguo = self::tsToYear($ts, 'minguo');
+//						usedYear = true;
+//						if (!minguo) {
+//							minguo = XomwLanguage.tsToYear(ts, 'minguo');
 //						}
-//						$num = $minguo[0];
+//						num = minguo[0];
 //						break;
 //					case 'xtY':
-//						$usedTennoYear = true;
-//						if (!$tenno) {
-//							$tenno = self::tsToYear($ts, 'tenno');
+//						usedTennoYear = true;
+//						if (!tenno) {
+//							tenno = XomwLanguage.tsToYear(ts, 'tenno');
 //						}
-//						$num = $tenno[0];
+//						num = tenno[0];
 //						break;
 //					case 'y':
-//						$usedYear = true;
-//						$num = substr($ts, 2, 2);
+//						usedYear = true;
+//						num = substr(ts, 2, 2);
 //						break;
 //					case 'xiy':
-//						$usedIranianYear = true;
-//						if (!$iranian) {
-//							$iranian = self::tsToIranian($ts);
+//						usedIranianYear = true;
+//						if (!iranian) {
+//							iranian = XomwLanguage.tsToIranian(ts);
 //						}
-//						$num = substr($iranian[0], -2);
+//						num = substr(iranian[0], -2);
 //						break;
 //					case 'xit':
-//						$usedIranianYear = true;
-//						if (!$iranian) {
-//							$iranian = self::tsToIranian($ts);
+//						usedIranianYear = true;
+//						if (!iranian) {
+//							iranian = XomwLanguage.tsToIranian(ts);
 //						}
-//						$num = self::$IRANIAN_DAYS[$iranian[1] - 1];
+//						num = XomwLanguage.IRANIAN_DAYS[iranian[1] - 1];
 //						break;
 //					case 'xiz':
-//						$usedIranianYear = true;
-//						if (!$iranian) {
-//							$iranian = self::tsToIranian($ts);
+//						usedIranianYear = true;
+//						if (!iranian) {
+//							iranian = XomwLanguage.tsToIranian(ts);
 //						}
-//						$num = $iranian[3];
+//						num = iranian[3];
 //						break;
 //					case 'a':
-//						$usedAMPM = true;
-//						$s .= intval(substr($ts, 8, 2)) < 12 ? 'am' : 'pm';
+//						usedAMPM = true;
+//						s .= intval(substr(ts, 8, 2)) < 12 ? 'am' : 'pm';
 //						break;
 //					case 'A':
-//						$usedAMPM = true;
-//						$s .= intval(substr($ts, 8, 2)) < 12 ? 'AM' : 'PM';
+//						usedAMPM = true;
+//						s .= intval(substr(ts, 8, 2)) < 12 ? 'AM' : 'PM';
 //						break;
 //					case 'g':
-//						$usedHour = true;
-//						$h = substr($ts, 8, 2);
-//						$num = $h % 12 ? $h % 12 : 12;
+//						usedHour = true;
+//						h = substr(ts, 8, 2);
+//						num = h % 12 ? h % 12 : 12;
 //						break;
 //					case 'G':
-//						$usedHour = true;
-//						$num = intval(substr($ts, 8, 2));
+//						usedHour = true;
+//						num = intval(substr(ts, 8, 2));
 //						break;
 //					case 'h':
-//						$usedHour = true;
-//						$h = substr($ts, 8, 2);
-//						$num = sprintf('%02d', $h % 12 ? $h % 12 : 12);
+//						usedHour = true;
+//						h = substr(ts, 8, 2);
+//						num = sprintf('%02d', h % 12 ? h % 12 : 12);
 //						break;
 //					case 'H':
-//						$usedHour = true;
-//						$num = substr($ts, 8, 2);
+//						usedHour = true;
+//						num = substr(ts, 8, 2);
 //						break;
 //					case 'i':
-//						$usedMinute = true;
-//						$num = substr($ts, 10, 2);
+//						usedMinute = true;
+//						num = substr(ts, 10, 2);
 //						break;
 //					case 's':
-//						$usedSecond = true;
-//						$num = substr($ts, 12, 2);
+//						usedSecond = true;
+//						num = substr(ts, 12, 2);
 //						break;
 //					case 'c':
 //					case 'r':
-//						$usedSecond = true;
+//						usedSecond = true;
 //						// fall through
 //					case 'e':
 //					case 'O':
 //					case 'P':
 //					case 'T':
-//						$s .= Language::dateTimeObjFormat($dateTimeObj, $ts, $zone, $code);
+//						s .= XomwLanguage.dateTimeObjFormat(dateTimeObj, ts, zone, code);
 //						break;
 //					case 'w':
 //					case 'N':
 //					case 'z':
-//						$usedDay = true;
-//						$num = Language::dateTimeObjFormat($dateTimeObj, $ts, $zone, $code);
+//						usedDay = true;
+//						num = XomwLanguage.dateTimeObjFormat(dateTimeObj, ts, zone, code);
 //						break;
 //					case 'W':
-//						$usedWeek = true;
-//						$num = Language::dateTimeObjFormat($dateTimeObj, $ts, $zone, $code);
+//						usedWeek = true;
+//						num = XomwLanguage.dateTimeObjFormat(dateTimeObj, ts, zone, code);
 //						break;
 //					case 't':
-//						$usedMonth = true;
-//						$num = Language::dateTimeObjFormat($dateTimeObj, $ts, $zone, $code);
+//						usedMonth = true;
+//						num = XomwLanguage.dateTimeObjFormat(dateTimeObj, ts, zone, code);
 //						break;
 //					case 'L':
-//						$usedIsLeapYear = true;
-//						$num = Language::dateTimeObjFormat($dateTimeObj, $ts, $zone, $code);
+//						usedIsLeapYear = true;
+//						num = XomwLanguage.dateTimeObjFormat(dateTimeObj, ts, zone, code);
 //						break;
 //					case 'o':
-//						$usedISOYear = true;
-//						$num = Language::dateTimeObjFormat($dateTimeObj, $ts, $zone, $code);
+//						usedISOYear = true;
+//						num = XomwLanguage.dateTimeObjFormat(dateTimeObj, ts, zone, code);
 //						break;
 //					case 'U':
-//						$usedSecond = true;
+//						usedSecond = true;
 //						// fall through
 //					case 'I':
 //					case 'Z':
-//						$num = Language::dateTimeObjFormat($dateTimeObj, $ts, $zone, $code);
+//						num = XomwLanguage.dateTimeObjFormat(dateTimeObj, ts, zone, code);
 //						break;
 //					case '\\':
 //						# Backslash escaping
-//						if ($p < $formatLength - 1) {
-//							$s .= $format[++$p];
+//						if (p < formatLength - 1) {
+//							s .= format[++p];
 //						} else {
-//							$s .= '\\';
+//							s .= '\\';
 //						}
 //						break;
 //					case '"':
 //						# Quoted literal
-//						if ($p < $formatLength - 1) {
-//							$endQuote = strpos($format, '"', $p + 1);
-//							if ($endQuote == false) {
+//						if (p < formatLength - 1) {
+//							endQuote = strpos(format, '"', p + 1);
+//							if (endQuote == false) {
 //								# No terminating quote, assume literal "
-//								$s .= '"';
+//								s .= '"';
 //							} else {
-//								$s .= substr($format, $p + 1, $endQuote - $p - 1);
-//								$p = $endQuote;
+//								s .= substr(format, p + 1, endQuote - p - 1);
+//								p = endQuote;
 //							}
 //						} else {
 //							# Quote at end of String, assume literal "
-//							$s .= '"';
+//							s .= '"';
 //						}
 //						break;
 //					default:
-//						$s .= $format[$p];
+//						s .= format[p];
 //				}
-//				if ($num != false) {
-//					if ($rawToggle || $raw) {
-//						$s .= $num;
-//						$raw = false;
-//					} elseif ($roman) {
-//						$s .= Language::romanNumeral($num);
-//						$roman = false;
-//					} elseif ($hebrewNum) {
-//						$s .= self::hebrewNumeral($num);
-//						$hebrewNum = false;
+//				if (num != false) {
+//					if (rawToggle || raw) {
+//						s .= num;
+//						raw = false;
+//					} elseif (roman) {
+//						s .= XomwLanguage.romanNumeral(num);
+//						roman = false;
+//					} elseif (hebrewNum) {
+//						s .= XomwLanguage.hebrewNumeral(num);
+//						hebrewNum = false;
 //					} else {
-//						$s .= this.formatNum($num, true);
+//						s .= this.formatNum(num, true);
 //					}
 //				}
 //			}
 //
-//			if ($ttl == 'unused') {
+//			if (ttl == 'unused') {
 //				// No need to calculate the TTL, the caller wont use it anyway.
-//			} elseif ($usedSecond) {
-//				$ttl = 1;
-//			} elseif ($usedMinute) {
-//				$ttl = 60 - substr($ts, 12, 2);
-//			} elseif ($usedHour) {
-//				$ttl = 3600 - substr($ts, 10, 2) * 60 - substr($ts, 12, 2);
-//			} elseif ($usedAMPM) {
-//				$ttl = 43200 - (substr($ts, 8, 2) % 12) * 3600 -
-//					substr($ts, 10, 2) * 60 - substr($ts, 12, 2);
+//			} elseif (usedSecond) {
+//				ttl = 1;
+//			} elseif (usedMinute) {
+//				ttl = 60 - substr(ts, 12, 2);
+//			} elseif (usedHour) {
+//				ttl = 3600 - substr(ts, 10, 2) * 60 - substr(ts, 12, 2);
+//			} elseif (usedAMPM) {
+//				ttl = 43200 - (substr(ts, 8, 2) % 12) * 3600 -
+//					substr(ts, 10, 2) * 60 - substr(ts, 12, 2);
 //			} elseif (
-//				$usedDay ||
-//				$usedHebrewMonth ||
-//				$usedIranianMonth ||
-//				$usedHijriMonth ||
-//				$usedHebrewYear ||
-//				$usedIranianYear ||
-//				$usedHijriYear ||
-//				$usedTennoYear
+//				usedDay ||
+//				usedHebrewMonth ||
+//				usedIranianMonth ||
+//				usedHijriMonth ||
+//				usedHebrewYear ||
+//				usedIranianYear ||
+//				usedHijriYear ||
+//				usedTennoYear
 //			) {
 //				// @todo Someone who understands the non-Gregorian calendars
 //				// should write proper logic for them so that they don't need purged every day.
-//				$ttl = 86400 - substr($ts, 8, 2) * 3600 -
-//					substr($ts, 10, 2) * 60 - substr($ts, 12, 2);
+//				ttl = 86400 - substr(ts, 8, 2) * 3600 -
+//					substr(ts, 10, 2) * 60 - substr(ts, 12, 2);
 //			} else {
-//				$possibleTtls = [];
-//				$timeRemainingInDay = 86400 - substr($ts, 8, 2) * 3600 -
-//					substr($ts, 10, 2) * 60 - substr($ts, 12, 2);
-//				if ($usedWeek) {
-//					$possibleTtls[] =
-//						(7 - Language::dateTimeObjFormat($dateTimeObj, $ts, $zone, 'N')) * 86400 +
-//						$timeRemainingInDay;
-//				} elseif ($usedISOYear) {
+//				possibleTtls = [];
+//				timeRemainingInDay = 86400 - substr(ts, 8, 2) * 3600 -
+//					substr(ts, 10, 2) * 60 - substr(ts, 12, 2);
+//				if (usedWeek) {
+//					possibleTtls[] =
+//						(7 - XomwLanguage.dateTimeObjFormat(dateTimeObj, ts, zone, 'N')) * 86400 +
+//						timeRemainingInDay;
+//				} elseif (usedISOYear) {
 //					// December 28th falls on the last ISO week of the year, every year.
 //					// The last ISO week of a year can be 52 or 53.
-//					$lastWeekOfISOYear = DateTime::createFromFormat(
+//					lastWeekOfISOYear = DateTime::createFromFormat(
 //						'Ymd',
-//						substr($ts, 0, 4) . '1228',
-//						$zone ?: new DateTimeZone('UTC')
-//					)->format('W');
-//					$currentISOWeek = Language::dateTimeObjFormat($dateTimeObj, $ts, $zone, 'W');
-//					$weeksRemaining = $lastWeekOfISOYear - $currentISOWeek;
-//					$timeRemainingInWeek =
-//						(7 - Language::dateTimeObjFormat($dateTimeObj, $ts, $zone, 'N')) * 86400
-//						+ $timeRemainingInDay;
-//					$possibleTtls[] = $weeksRemaining * 604800 + $timeRemainingInWeek;
+//						substr(ts, 0, 4) . '1228',
+//						zone ?: new DateTimeZone('UTC')
+//					).format('W');
+//					currentISOWeek = XomwLanguage.dateTimeObjFormat(dateTimeObj, ts, zone, 'W');
+//					weeksRemaining = lastWeekOfISOYear - currentISOWeek;
+//					timeRemainingInWeek =
+//						(7 - XomwLanguage.dateTimeObjFormat(dateTimeObj, ts, zone, 'N')) * 86400
+//						+ timeRemainingInDay;
+//					possibleTtls[] = weeksRemaining * 604800 + timeRemainingInWeek;
 //				}
 //
-//				if ($usedMonth) {
-//					$possibleTtls[] =
-//						(Language::dateTimeObjFormat($dateTimeObj, $ts, $zone, 't') -
-//							substr($ts, 6, 2)) * 86400
-//						+ $timeRemainingInDay;
-//				} elseif ($usedYear) {
-//					$possibleTtls[] =
-//						(Language::dateTimeObjFormat($dateTimeObj, $ts, $zone, 'L') + 364 -
-//							Language::dateTimeObjFormat($dateTimeObj, $ts, $zone, 'z')) * 86400
-//						+ $timeRemainingInDay;
-//				} elseif ($usedIsLeapYear) {
-//					$year = substr($ts, 0, 4);
-//					$timeRemainingInYear =
-//						(Language::dateTimeObjFormat($dateTimeObj, $ts, $zone, 'L') + 364 -
-//							Language::dateTimeObjFormat($dateTimeObj, $ts, $zone, 'z')) * 86400
-//						+ $timeRemainingInDay;
-//					$mod = $year % 4;
-//					if ($mod || (!($year % 100) && $year % 400)) {
+//				if (usedMonth) {
+//					possibleTtls[] =
+//						(XomwLanguage.dateTimeObjFormat(dateTimeObj, ts, zone, 't') -
+//							substr(ts, 6, 2)) * 86400
+//						+ timeRemainingInDay;
+//				} elseif (usedYear) {
+//					possibleTtls[] =
+//						(XomwLanguage.dateTimeObjFormat(dateTimeObj, ts, zone, 'L') + 364 -
+//							XomwLanguage.dateTimeObjFormat(dateTimeObj, ts, zone, 'z')) * 86400
+//						+ timeRemainingInDay;
+//				} elseif (usedIsLeapYear) {
+//					year = substr(ts, 0, 4);
+//					timeRemainingInYear =
+//						(XomwLanguage.dateTimeObjFormat(dateTimeObj, ts, zone, 'L') + 364 -
+//							XomwLanguage.dateTimeObjFormat(dateTimeObj, ts, zone, 'z')) * 86400
+//						+ timeRemainingInDay;
+//					mod = year % 4;
+//					if (mod || (!(year % 100) && year % 400)) {
 //						// this isn't a leap year. see when the next one starts
-//						$nextCandidate = $year - $mod + 4;
-//						if ($nextCandidate % 100 || !($nextCandidate % 400)) {
-//							$possibleTtls[] = ($nextCandidate - $year - 1) * 365 * 86400 +
-//								$timeRemainingInYear;
+//						nextCandidate = year - mod + 4;
+//						if (nextCandidate % 100 || !(nextCandidate % 400)) {
+//							possibleTtls[] = (nextCandidate - year - 1) * 365 * 86400 +
+//								timeRemainingInYear;
 //						} else {
-//							$possibleTtls[] = ($nextCandidate - $year + 3) * 365 * 86400 +
-//								$timeRemainingInYear;
+//							possibleTtls[] = (nextCandidate - year + 3) * 365 * 86400 +
+//								timeRemainingInYear;
 //						}
 //					} else {
 //						// this is a leap year, so the next year isn't
-//						$possibleTtls[] = $timeRemainingInYear;
+//						possibleTtls[] = timeRemainingInYear;
 //					}
 //				}
 //
-//				if ($possibleTtls) {
-//					$ttl = min($possibleTtls);
+//				if (possibleTtls) {
+//					ttl = min(possibleTtls);
 //				}
 //			}
 //
-//			return $s;
+//			return s;
 //		}
 //
-//		private static $GREG_DAYS = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
-//		private static $IRANIAN_DAYS = [ 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 ];
+//		private static GREG_DAYS = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
+//		private static IRANIAN_DAYS = [ 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 ];
 //
 //		/**
 //		* Algorithm by Roozbeh Pournader and Mohammad Toossi to convert
@@ -1611,57 +1619,57 @@ public class XomwLanguage {
 //		*
 //		* Link: http://www.farsiweb.info/jalali/jalali.c
 //		*
-//		* @param String $ts
+//		* @param String ts
 //		*
 //		* @return int[]
 //		*/
-//		private static function tsToIranian($ts) {
-//			$gy = substr($ts, 0, 4) -1600;
-//			$gm = substr($ts, 4, 2) -1;
-//			$gd = substr($ts, 6, 2) -1;
+//		private static function tsToIranian(ts) {
+//			gy = substr(ts, 0, 4) -1600;
+//			gm = substr(ts, 4, 2) -1;
+//			gd = substr(ts, 6, 2) -1;
 //
 //			# Days passed from the beginning (including leap years)
-//			$gDayNo = 365 * $gy
-//				+ floor(($gy + 3) / 4)
-//				- floor(($gy + 99) / 100)
-//				+ floor(($gy + 399) / 400);
+//			gDayNo = 365 * gy
+//				+ floor((gy + 3) / 4)
+//				- floor((gy + 99) / 100)
+//				+ floor((gy + 399) / 400);
 //
 //			// Add days of the past months of this year
-//			for ($i = 0; $i < $gm; $i++) {
-//				$gDayNo += self::$GREG_DAYS[$i];
+//			for (i = 0; i < gm; i++) {
+//				gDayNo += XomwLanguage.GREG_DAYS[i];
 //			}
 //
 //			// Leap years
-//			if ($gm > 1 && (($gy % 4 == 0 && $gy % 100 != 0 || ($gy % 400 == 0)))) {
-//				$gDayNo++;
+//			if (gm > 1 && ((gy % 4 == 0 && gy % 100 != 0 || (gy % 400 == 0)))) {
+//				gDayNo++;
 //			}
 //
 //			// Days passed in current month
-//			$gDayNo += (int)$gd;
+//			gDayNo += (int)gd;
 //
-//			$jDayNo = $gDayNo - 79;
+//			jDayNo = gDayNo - 79;
 //
-//			$jNp = floor($jDayNo / 12053);
-//			$jDayNo %= 12053;
+//			jNp = floor(jDayNo / 12053);
+//			jDayNo %= 12053;
 //
-//			$jy = 979 + 33 * $jNp + 4 * floor($jDayNo / 1461);
-//			$jDayNo %= 1461;
+//			jy = 979 + 33 * jNp + 4 * floor(jDayNo / 1461);
+//			jDayNo %= 1461;
 //
-//			if ($jDayNo >= 366) {
-//				$jy += floor(($jDayNo - 1) / 365);
-//				$jDayNo = floor(($jDayNo - 1) % 365);
+//			if (jDayNo >= 366) {
+//				jy += floor((jDayNo - 1) / 365);
+//				jDayNo = floor((jDayNo - 1) % 365);
 //			}
 //
-//			$jz = $jDayNo;
+//			jz = jDayNo;
 //
-//			for ($i = 0; $i < 11 && $jDayNo >= self::$IRANIAN_DAYS[$i]; $i++) {
-//				$jDayNo -= self::$IRANIAN_DAYS[$i];
+//			for (i = 0; i < 11 && jDayNo >= XomwLanguage.IRANIAN_DAYS[i]; i++) {
+//				jDayNo -= XomwLanguage.IRANIAN_DAYS[i];
 //			}
 //
-//			$jm = $i + 1;
-//			$jd = $jDayNo + 1;
+//			jm = i + 1;
+//			jd = jDayNo + 1;
 //
-//			return [ $jy, $jm, $jd, $jz ];
+//			return [ jy, jm, jd, jz ];
 //		}
 //
 //		/**
@@ -1671,45 +1679,45 @@ public class XomwLanguage {
 //		*
 //		* @see https://phpnuke.org/modules.php?name=News&file=article&sid=8234&mode=thread&order=0&thold=0
 //		*
-//		* @param String $ts
+//		* @param String ts
 //		*
 //		* @return int[]
 //		*/
-//		private static function tsToHijri($ts) {
-//			$year = substr($ts, 0, 4);
-//			$month = substr($ts, 4, 2);
-//			$day = substr($ts, 6, 2);
+//		private static function tsToHijri(ts) {
+//			year = substr(ts, 0, 4);
+//			month = substr(ts, 4, 2);
+//			day = substr(ts, 6, 2);
 //
-//			$zyr = $year;
-//			$zd = $day;
-//			$zm = $month;
-//			$zy = $zyr;
+//			zyr = year;
+//			zd = day;
+//			zm = month;
+//			zy = zyr;
 //
 //			if (
-//				($zy > 1582) || (($zy == 1582) && ($zm > 10)) ||
-//				(($zy == 1582) && ($zm == 10) && ($zd > 14))
+//				(zy > 1582) || ((zy == 1582) && (zm > 10)) ||
+//				((zy == 1582) && (zm == 10) && (zd > 14))
 //			) {
-//				$zjd = (int)((1461 * ($zy + 4800 + (int)(($zm - 14) / 12))) / 4) +
-//						(int)((367 * ($zm - 2 - 12 * ((int)(($zm - 14) / 12)))) / 12) -
-//						(int)((3 * (int)((($zy + 4900 + (int)(($zm - 14) / 12)) / 100))) / 4) +
-//						$zd - 32075;
+//				zjd = (int)((1461 * (zy + 4800 + (int)((zm - 14) / 12))) / 4) +
+//						(int)((367 * (zm - 2 - 12 * ((int)((zm - 14) / 12)))) / 12) -
+//						(int)((3 * (int)(((zy + 4900 + (int)((zm - 14) / 12)) / 100))) / 4) +
+//						zd - 32075;
 //			} else {
-//				$zjd = 367 * $zy - (int)((7 * ($zy + 5001 + (int)(($zm - 9) / 7))) / 4) +
-//									(int)((275 * $zm) / 9) + $zd + 1729777;
+//				zjd = 367 * zy - (int)((7 * (zy + 5001 + (int)((zm - 9) / 7))) / 4) +
+//									(int)((275 * zm) / 9) + zd + 1729777;
 //			}
 //
-//			$zl = $zjd -1948440 + 10632;
-//			$zn = (int)(($zl - 1) / 10631);
-//			$zl = $zl - 10631 * $zn + 354;
-//			$zj = ((int)((10985 - $zl) / 5316)) * ((int)((50 * $zl) / 17719)) +
-//				((int)($zl / 5670)) * ((int)((43 * $zl) / 15238));
-//			$zl = $zl - ((int)((30 - $zj) / 15)) * ((int)((17719 * $zj) / 50)) -
-//				((int)($zj / 16)) * ((int)((15238 * $zj) / 43)) + 29;
-//			$zm = (int)((24 * $zl) / 709);
-//			$zd = $zl - (int)((709 * $zm) / 24);
-//			$zy = 30 * $zn + $zj - 30;
+//			zl = zjd -1948440 + 10632;
+//			zn = (int)((zl - 1) / 10631);
+//			zl = zl - 10631 * zn + 354;
+//			zj = ((int)((10985 - zl) / 5316)) * ((int)((50 * zl) / 17719)) +
+//				((int)(zl / 5670)) * ((int)((43 * zl) / 15238));
+//			zl = zl - ((int)((30 - zj) / 15)) * ((int)((17719 * zj) / 50)) -
+//				((int)(zj / 16)) * ((int)((15238 * zj) / 43)) + 29;
+//			zm = (int)((24 * zl) / 709);
+//			zd = zl - (int)((709 * zm) / 24);
+//			zy = 30 * zn + zj - 30;
 //
-//			return [ $zy, $zm, $zd ];
+//			return [ zy, zm, zd ];
 //		}
 //
 //		/**
@@ -1723,140 +1731,140 @@ public class XomwLanguage {
 //		* The months are counted from Tishrei = 1. In a leap year, Adar I is 13
 //		* and Adar II is 14. In a non-leap year, Adar is 6.
 //		*
-//		* @param String $ts
+//		* @param String ts
 //		*
 //		* @return int[]
 //		*/
-//		private static function tsToHebrew($ts) {
+//		private static function tsToHebrew(ts) {
 //			# Parse date
-//			$year = substr($ts, 0, 4);
-//			$month = substr($ts, 4, 2);
-//			$day = substr($ts, 6, 2);
+//			year = substr(ts, 0, 4);
+//			month = substr(ts, 4, 2);
+//			day = substr(ts, 6, 2);
 //
 //			# Calculate Hebrew year
-//			$hebrewYear = $year + 3760;
+//			hebrewYear = year + 3760;
 //
 //			# Month number when September = 1, August = 12
-//			$month += 4;
-//			if ($month > 12) {
+//			month += 4;
+//			if (month > 12) {
 //				# Next year
-//				$month -= 12;
-//				$year++;
-//				$hebrewYear++;
+//				month -= 12;
+//				year++;
+//				hebrewYear++;
 //			}
 //
 //			# Calculate day of year from 1 September
-//			$dayOfYear = $day;
-//			for ($i = 1; $i < $month; $i++) {
-//				if ($i == 6) {
+//			dayOfYear = day;
+//			for (i = 1; i < month; i++) {
+//				if (i == 6) {
 //					# February
-//					$dayOfYear += 28;
+//					dayOfYear += 28;
 //					# Check if the year is leap
-//					if ($year % 400 == 0 || ($year % 4 == 0 && $year % 100 > 0)) {
-//						$dayOfYear++;
+//					if (year % 400 == 0 || (year % 4 == 0 && year % 100 > 0)) {
+//						dayOfYear++;
 //					}
-//				} elseif ($i == 8 || $i == 10 || $i == 1 || $i == 3) {
-//					$dayOfYear += 30;
+//				} elseif (i == 8 || i == 10 || i == 1 || i == 3) {
+//					dayOfYear += 30;
 //				} else {
-//					$dayOfYear += 31;
+//					dayOfYear += 31;
 //				}
 //			}
 //
 //			# Calculate the start of the Hebrew year
-//			$start = self::hebrewYearStart($hebrewYear);
+//			start = XomwLanguage.hebrewYearStart(hebrewYear);
 //
 //			# Calculate next year's start
-//			if ($dayOfYear <= $start) {
+//			if (dayOfYear <= start) {
 //				# Day is before the start of the year - it is the previous year
 //				# Next year's start
-//				$nextStart = $start;
+//				nextStart = start;
 //				# Previous year
-//				$year--;
-//				$hebrewYear--;
+//				year--;
+//				hebrewYear--;
 //				# Add days since previous year's 1 September
-//				$dayOfYear += 365;
-//				if (($year % 400 == 0) || ($year % 100 != 0 && $year % 4 == 0)) {
+//				dayOfYear += 365;
+//				if ((year % 400 == 0) || (year % 100 != 0 && year % 4 == 0)) {
 //					# Leap year
-//					$dayOfYear++;
+//					dayOfYear++;
 //				}
 //				# Start of the new (previous) year
-//				$start = self::hebrewYearStart($hebrewYear);
+//				start = XomwLanguage.hebrewYearStart(hebrewYear);
 //			} else {
 //				# Next year's start
-//				$nextStart = self::hebrewYearStart($hebrewYear + 1);
+//				nextStart = XomwLanguage.hebrewYearStart(hebrewYear + 1);
 //			}
 //
 //			# Calculate Hebrew day of year
-//			$hebrewDayOfYear = $dayOfYear - $start;
+//			hebrewDayOfYear = dayOfYear - start;
 //
 //			# Difference between year's days
-//			$diff = $nextStart - $start;
+//			diff = nextStart - start;
 //			# Add 12 (or 13 for leap years) days to ignore the difference between
 //			# Hebrew and Gregorian year (353 at least vs. 365/6) - now the
 //			# difference is only about the year type
-//			if (($year % 400 == 0) || ($year % 100 != 0 && $year % 4 == 0)) {
-//				$diff += 13;
+//			if ((year % 400 == 0) || (year % 100 != 0 && year % 4 == 0)) {
+//				diff += 13;
 //			} else {
-//				$diff += 12;
+//				diff += 12;
 //			}
 //
 //			# Check the year pattern, and is leap year
 //			# 0 means an incomplete year, 1 means a regular year, 2 means a complete year
 //			# This is mod 30, to work on both leap years (which add 30 days of Adar I)
 //			# and non-leap years
-//			$yearPattern = $diff % 30;
+//			yearPattern = diff % 30;
 //			# Check if leap year
-//			$isLeap = $diff >= 30;
+//			isLeap = diff >= 30;
 //
 //			# Calculate day in the month from number of day in the Hebrew year
 //			# Don't check Adar - if the day is not in Adar, we will stop before;
 //			# if it is in Adar, we will use it to check if it is Adar I or Adar II
-//			$hebrewDay = $hebrewDayOfYear;
-//			$hebrewMonth = 1;
-//			$days = 0;
-//			while ($hebrewMonth <= 12) {
+//			hebrewDay = hebrewDayOfYear;
+//			hebrewMonth = 1;
+//			days = 0;
+//			while (hebrewMonth <= 12) {
 //				# Calculate days in this month
-//				if ($isLeap && $hebrewMonth == 6) {
+//				if (isLeap && hebrewMonth == 6) {
 //					# Adar in a leap year
-//					if ($isLeap) {
+//					if (isLeap) {
 //						# Leap year - has Adar I, with 30 days, and Adar II, with 29 days
-//						$days = 30;
-//						if ($hebrewDay <= $days) {
+//						days = 30;
+//						if (hebrewDay <= days) {
 //							# Day in Adar I
-//							$hebrewMonth = 13;
+//							hebrewMonth = 13;
 //						} else {
 //							# Subtract the days of Adar I
-//							$hebrewDay -= $days;
+//							hebrewDay -= days;
 //							# Try Adar II
-//							$days = 29;
-//							if ($hebrewDay <= $days) {
+//							days = 29;
+//							if (hebrewDay <= days) {
 //								# Day in Adar II
-//								$hebrewMonth = 14;
+//								hebrewMonth = 14;
 //							}
 //						}
 //					}
-//				} elseif ($hebrewMonth == 2 && $yearPattern == 2) {
+//				} elseif (hebrewMonth == 2 && yearPattern == 2) {
 //					# Cheshvan in a complete year (otherwise as the rule below)
-//					$days = 30;
-//				} elseif ($hebrewMonth == 3 && $yearPattern == 0) {
+//					days = 30;
+//				} elseif (hebrewMonth == 3 && yearPattern == 0) {
 //					# Kislev in an incomplete year (otherwise as the rule below)
-//					$days = 29;
+//					days = 29;
 //				} else {
 //					# Odd months have 30 days, even have 29
-//					$days = 30 - ($hebrewMonth - 1) % 2;
+//					days = 30 - (hebrewMonth - 1) % 2;
 //				}
-//				if ($hebrewDay <= $days) {
+//				if (hebrewDay <= days) {
 //					# In the current month
 //					break;
 //				} else {
 //					# Subtract the days of the current month
-//					$hebrewDay -= $days;
+//					hebrewDay -= days;
 //					# Try in the next month
-//					$hebrewMonth++;
+//					hebrewMonth++;
 //				}
 //			}
 //
-//			return [ $hebrewYear, $hebrewMonth, $hebrewDay, $days ];
+//			return [ hebrewYear, hebrewMonth, hebrewDay, days ];
 //		}
 //
 //		/**
@@ -1864,34 +1872,34 @@ public class XomwLanguage {
 //		* Based on Carl Friedrich Gauss algorithm for finding Easter date.
 //		* Used for Hebrew date.
 //		*
-//		* @param int $year
+//		* @param int year
 //		*
 //		* @return String
 //		*/
-//		private static function hebrewYearStart($year) {
-//			$a = intval((12 * ($year - 1) + 17) % 19);
-//			$b = intval(($year - 1) % 4);
-//			$m = 32.044093161144 + 1.5542417966212 * $a + $b / 4.0 - 0.0031777940220923 * ($year - 1);
-//			if ($m < 0) {
-//				$m--;
+//		private static function hebrewYearStart(year) {
+//			a = intval((12 * (year - 1) + 17) % 19);
+//			b = intval((year - 1) % 4);
+//			m = 32.044093161144 + 1.5542417966212 * a + b / 4.0 - 0.0031777940220923 * (year - 1);
+//			if (m < 0) {
+//				m--;
 //			}
-//			$Mar = intval($m);
-//			if ($m < 0) {
-//				$m++;
+//			Mar = intval(m);
+//			if (m < 0) {
+//				m++;
 //			}
-//			$m -= $Mar;
+//			m -= Mar;
 //
-//			$c = intval(($Mar + 3 * ($year - 1) + 5 * $b + 5) % 7);
-//			if ($c == 0 && $a > 11 && $m >= 0.89772376543210) {
-//				$Mar++;
-//			} elseif ($c == 1 && $a > 6 && $m >= 0.63287037037037) {
-//				$Mar += 2;
-//			} elseif ($c == 2 || $c == 4 || $c == 6) {
-//				$Mar++;
+//			c = intval((Mar + 3 * (year - 1) + 5 * b + 5) % 7);
+//			if (c == 0 && a > 11 && m >= 0.89772376543210) {
+//				Mar++;
+//			} elseif (c == 1 && a > 6 && m >= 0.63287037037037) {
+//				Mar += 2;
+//			} elseif (c == 2 || c == 4 || c == 6) {
+//				Mar++;
 //			}
 //
-//			$Mar += intval(($year - 3761) / 100) - intval(($year - 3761) / 400) - 24;
-//			return $Mar;
+//			Mar += intval((year - 3761) / 100) - intval((year - 3761) / 400) - 24;
+//			return Mar;
 //		}
 //
 //		/**
@@ -1902,81 +1910,81 @@ public class XomwLanguage {
 //		*       https://en.wikipedia.org/wiki/Minguo_calendar
 //		*       https://en.wikipedia.org/wiki/Japanese_era_name
 //		*
-//		* @param String $ts 14-character timestamp
-//		* @param String $cName Calender name
+//		* @param String ts 14-character timestamp
+//		* @param String cName Calender name
 //		* @return array Converted year, month, day
 //		*/
-//		private static function tsToYear($ts, $cName) {
-//			$gy = substr($ts, 0, 4);
-//			$gm = substr($ts, 4, 2);
-//			$gd = substr($ts, 6, 2);
+//		private static function tsToYear(ts, cName) {
+//			gy = substr(ts, 0, 4);
+//			gm = substr(ts, 4, 2);
+//			gd = substr(ts, 6, 2);
 //
-//			if (!strcmp($cName, 'thai')) {
+//			if (!strcmp(cName, 'thai')) {
 //				# Thai solar dates
 //				# Add 543 years to the Gregorian calendar
 //				# Months and days are identical
-//				$gy_offset = $gy + 543;
-//			} elseif ((!strcmp($cName, 'minguo')) || !strcmp($cName, 'juche')) {
+//				gy_offset = gy + 543;
+//			} elseif ((!strcmp(cName, 'minguo')) || !strcmp(cName, 'juche')) {
 //				# Minguo dates
 //				# Deduct 1911 years from the Gregorian calendar
 //				# Months and days are identical
-//				$gy_offset = $gy - 1911;
-//			} elseif (!strcmp($cName, 'tenno')) {
-//				# Nengō dates up to Meiji period
+//				gy_offset = gy - 1911;
+//			} elseif (!strcmp(cName, 'tenno')) {
+//				# Nengo dates up to Meiji period
 //				# Deduct years from the Gregorian calendar
 //				# depending on the nengo periods
 //				# Months and days are identical
-//				if (($gy < 1912)
-//					|| (($gy == 1912) && ($gm < 7))
-//					|| (($gy == 1912) && ($gm == 7) && ($gd < 31))
+//				if ((gy < 1912)
+//					|| ((gy == 1912) && (gm < 7))
+//					|| ((gy == 1912) && (gm == 7) && (gd < 31))
 //				) {
 //					# Meiji period
-//					$gy_gannen = $gy - 1868 + 1;
-//					$gy_offset = $gy_gannen;
-//					if ($gy_gannen == 1) {
-//						$gy_offset = '元';
+//					gy_gannen = gy - 1868 + 1;
+//					gy_offset = gy_gannen;
+//					if (gy_gannen == 1) {
+//						gy_offset = '?';
 //					}
-//					$gy_offset = '明治' . $gy_offset;
+//					gy_offset = '??' . gy_offset;
 //				} elseif (
-//					(($gy == 1912) && ($gm == 7) && ($gd == 31)) ||
-//					(($gy == 1912) && ($gm >= 8)) ||
-//					(($gy > 1912) && ($gy < 1926)) ||
-//					(($gy == 1926) && ($gm < 12)) ||
-//					(($gy == 1926) && ($gm == 12) && ($gd < 26))
+//					((gy == 1912) && (gm == 7) && (gd == 31)) ||
+//					((gy == 1912) && (gm >= 8)) ||
+//					((gy > 1912) && (gy < 1926)) ||
+//					((gy == 1926) && (gm < 12)) ||
+//					((gy == 1926) && (gm == 12) && (gd < 26))
 //				) {
-//					# Taishō period
-//					$gy_gannen = $gy - 1912 + 1;
-//					$gy_offset = $gy_gannen;
-//					if ($gy_gannen == 1) {
-//						$gy_offset = '元';
+//					# Taisho period
+//					gy_gannen = gy - 1912 + 1;
+//					gy_offset = gy_gannen;
+//					if (gy_gannen == 1) {
+//						gy_offset = '?';
 //					}
-//					$gy_offset = '大正' . $gy_offset;
+//					gy_offset = '??' . gy_offset;
 //				} elseif (
-//					(($gy == 1926) && ($gm == 12) && ($gd >= 26)) ||
-//					(($gy > 1926) && ($gy < 1989)) ||
-//					(($gy == 1989) && ($gm == 1) && ($gd < 8))
+//					((gy == 1926) && (gm == 12) && (gd >= 26)) ||
+//					((gy > 1926) && (gy < 1989)) ||
+//					((gy == 1989) && (gm == 1) && (gd < 8))
 //				) {
-//					# Shōwa period
-//					$gy_gannen = $gy - 1926 + 1;
-//					$gy_offset = $gy_gannen;
-//					if ($gy_gannen == 1) {
-//						$gy_offset = '元';
+//					# Showa period
+//					gy_gannen = gy - 1926 + 1;
+//					gy_offset = gy_gannen;
+//					if (gy_gannen == 1) {
+//						gy_offset = '?';
 //					}
-//					$gy_offset = '昭和' . $gy_offset;
+//					gy_offset = '??' . gy_offset;
 //				} else {
 //					# Heisei period
-//					$gy_gannen = $gy - 1989 + 1;
-//					$gy_offset = $gy_gannen;
-//					if ($gy_gannen == 1) {
-//						$gy_offset = '元';
+//					gy_gannen = gy - 1989 + 1;
+//					gy_offset = gy_gannen;
+//					if (gy_gannen == 1) {
+//						gy_offset = '?';
 //					}
-//					$gy_offset = '平成' . $gy_offset;
+//					gy_offset = '??' . gy_offset;
 //				}
 //			} else {
-//				$gy_offset = $gy;
+//				gy_offset = gy;
 //			}
 //
-//			return [ $gy_offset, $gm, $gd ];
+//			return [ gy_offset, gm, gd ];
 //		}
 //
 //		/**
@@ -1989,14 +1997,14 @@ public class XomwLanguage {
 //		* TODO: Does not handle BIDI control characters inside the text.
 //		* TODO: Does not handle unallocated characters.
 //		*
-//		* @param String $text Text to test
+//		* @param String text Text to test
 //		* @return null|String Directionality ('ltr' or 'rtl') or null
 //		*/
-//		private static function strongDirFromContent($text = '') {
-//			if (!preg_match(self::$strongDirRegex, $text, $matches)) {
+//		private static function strongDirFromContent(text = '') {
+//			if (!preg_match(XomwLanguage.strongDirRegex, text, matches)) {
 //				return null;
 //			}
-//			if ($matches[1] == '') {
+//			if (matches[1] == '') {
 //				return 'rtl';
 //			}
 //			return 'ltr';
@@ -2005,12 +2013,12 @@ public class XomwLanguage {
 //		/**
 //		* Roman number formatting up to 10000
 //		*
-//		* @param int $num
+//		* @param int num
 //		*
 //		* @return String
 //		*/
-//		static function romanNumeral($num) {
-//			static $table = [
+//		static function romanNumeral(num) {
+//			static table = [
 //				[ '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ],
 //				[ '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ],
 //				[ '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ],
@@ -2018,176 +2026,176 @@ public class XomwLanguage {
 //					'MMMMMMMM', 'MMMMMMMMM', 'MMMMMMMMMM' ]
 //			];
 //
-//			$num = intval($num);
-//			if ($num > 10000 || $num <= 0) {
-//				return $num;
+//			num = intval(num);
+//			if (num > 10000 || num <= 0) {
+//				return num;
 //			}
 //
-//			$s = '';
-//			for ($pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i--) {
-//				if ($num >= $pow10) {
-//					$s .= $table[$i][(int)floor($num / $pow10)];
+//			s = '';
+//			for (pow10 = 1000, i = 3; i >= 0; pow10 /= 10, i--) {
+//				if (num >= pow10) {
+//					s .= table[i][(int)floor(num / pow10)];
 //				}
-//				$num = $num % $pow10;
+//				num = num % pow10;
 //			}
-//			return $s;
+//			return s;
 //		}
 //
 //		/**
 //		* Hebrew Gematria number formatting up to 9999
 //		*
-//		* @param int $num
+//		* @param int num
 //		*
 //		* @return String
 //		*/
-//		static function hebrewNumeral($num) {
-//			static $table = [
-//				[ '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ],
-//				[ '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ],
+//		static function hebrewNumeral(num) {
+//			static table = [
+//				[ '', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?' ],
+//				[ '', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?' ],
 //				[ '',
-//					[ 'ק' ],
-//					[ 'ר' ],
-//					[ 'ש' ],
-//					[ 'ת' ],
-//					[ 'ת', 'ק' ],
-//					[ 'ת', 'ר' ],
-//					[ 'ת', 'ש' ],
-//					[ 'ת', 'ת' ],
-//					[ 'ת', 'ת', 'ק' ],
-//					[ 'ת', 'ת', 'ר' ],
+//					[ '?' ],
+//					[ '?' ],
+//					[ '?' ],
+//					[ '?' ],
+//					[ '?', '?' ],
+//					[ '?', '?' ],
+//					[ '?', '?' ],
+//					[ '?', '?' ],
+//					[ '?', '?', '?' ],
+//					[ '?', '?', '?' ],
 //				],
-//				[ '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ]
+//				[ '', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?' ]
 //			];
 //
-//			$num = intval($num);
-//			if ($num > 9999 || $num <= 0) {
-//				return $num;
+//			num = intval(num);
+//			if (num > 9999 || num <= 0) {
+//				return num;
 //			}
 //
 //			// Round thousands have special notations
-//			if ($num == 1000) {
-//				return "א' אלף";
-//			} elseif ($num % 1000 == 0) {
-//				return $table[0][$num / 1000] . "' אלפים";
+//			if (num == 1000) {
+//				return "?' ???";
+//			} elseif (num % 1000 == 0) {
+//				return table[0][num / 1000] . "' ?????";
 //			}
 //
-//			$letters = [];
+//			letters = [];
 //
-//			for ($pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i--) {
-//				if ($num >= $pow10) {
-//					if ($num == 15 || $num == 16) {
-//						$letters[] = $table[0][9];
-//						$letters[] = $table[0][$num - 9];
-//						$num = 0;
+//			for (pow10 = 1000, i = 3; i >= 0; pow10 /= 10, i--) {
+//				if (num >= pow10) {
+//					if (num == 15 || num == 16) {
+//						letters[] = table[0][9];
+//						letters[] = table[0][num - 9];
+//						num = 0;
 //					} else {
-//						$letters = array_merge(
-//							$letters,
-//							(array)$table[$i][intval($num / $pow10)]
+//						letters = array_merge(
+//							letters,
+//							(array)table[i][intval(num / pow10)]
 //						);
 //
-//						if ($pow10 == 1000) {
-//							$letters[] = "'";
+//						if (pow10 == 1000) {
+//							letters[] = "'";
 //						}
 //					}
 //				}
 //
-//				$num = $num % $pow10;
+//				num = num % pow10;
 //			}
 //
-//			$preTransformLength = count($letters);
-//			if ($preTransformLength == 1) {
+//			preTransformLength = count(letters);
+//			if (preTransformLength == 1) {
 //				// Add geresh (single quote) to one-letter numbers
-//				$letters[] = "'";
+//				letters[] = "'";
 //			} else {
-//				$lastIndex = $preTransformLength - 1;
-//				$letters[$lastIndex] = str_replace(
-//					[ 'כ', 'מ', 'נ', 'פ', 'צ' ],
-//					[ 'ך', 'ם', 'ן', 'ף', 'ץ' ],
-//					$letters[$lastIndex]
+//				lastIndex = preTransformLength - 1;
+//				letters[lastIndex] = str_replace(
+//					[ '?', '?', '?', '?', '?' ],
+//					[ '?', '?', '?', '?', '?' ],
+//					letters[lastIndex]
 //				);
 //
 //				// Add gershayim (double quote) to multiple-letter numbers,
 //				// but exclude numbers with only one letter after the thousands
 //				// (1001-1009, 1020, 1030, 2001-2009, etc.)
-//				if ($letters[1] == "'" && $preTransformLength == 3) {
-//					$letters[] = "'";
+//				if (letters[1] == "'" && preTransformLength == 3) {
+//					letters[] = "'";
 //				} else {
-//					array_splice($letters, -1, 0, '"');
+//					array_splice(letters, -1, 0, '"');
 //				}
 //			}
 //
-//			return implode($letters);
+//			return implode(letters);
 //		}
 //
 //		/**
 //		* Used by date() and time() to adjust the time output.
 //		*
-//		* @param String $ts The time in date('YmdHis') format
-//		* @param mixed $tz Adjust the time by this amount (default false, mean we
+//		* @param String ts The time in date('YmdHis') format
+//		* @param mixed tz Adjust the time by this amount (default false, mean we
 //		*   get user timecorrection setting)
 //		* @return int
 //		*/
-//		public function userAdjust($ts, $tz = false) {
-//			global $wgUser, $wgLocalTZoffset;
+//		public function userAdjust(ts, tz = false) {
+//			global wgUser, wgLocalTZoffset;
 //
-//			if ($tz == false) {
-//				$tz = $wgUser->getOption('timecorrection');
+//			if (tz == false) {
+//				tz = wgUser.getOption('timecorrection');
 //			}
 //
-//			$data = explode('|', $tz, 3);
+//			data = explode('|', tz, 3);
 //
-//			if ($data[0] == 'ZoneInfo') {
+//			if (data[0] == 'ZoneInfo') {
 //				try {
-//					$userTZ = new DateTimeZone($data[2]);
-//					$date = new DateTime($ts, new DateTimeZone('UTC'));
-//					$date->setTimezone($userTZ);
-//					return $date->format('YmdHis');
-//				} catch (Exception $e) {
+//					userTZ = new DateTimeZone(data[2]);
+//					date = new DateTime(ts, new DateTimeZone('UTC'));
+//					date.setTimezone(userTZ);
+//					return date.format('YmdHis');
+//				} catch (Exception e) {
 //					// Unrecognized timezone, default to 'Offset' with the stored offset.
-//					$data[0] = 'Offset';
+//					data[0] = 'Offset';
 //				}
 //			}
 //
-//			if ($data[0] == 'System' || $tz == '') {
+//			if (data[0] == 'System' || tz == '') {
 //				# Global offset in minutes.
-//				$minDiff = $wgLocalTZoffset;
-//			} elseif ($data[0] == 'Offset') {
-//				$minDiff = intval($data[1]);
+//				minDiff = wgLocalTZoffset;
+//			} elseif (data[0] == 'Offset') {
+//				minDiff = intval(data[1]);
 //			} else {
-//				$data = explode(':', $tz);
-//				if (count($data) == 2) {
-//					$data[0] = intval($data[0]);
-//					$data[1] = intval($data[1]);
-//					$minDiff = abs($data[0]) * 60 + $data[1];
-//					if ($data[0] < 0) {
-//						$minDiff = -$minDiff;
+//				data = explode(':', tz);
+//				if (count(data) == 2) {
+//					data[0] = intval(data[0]);
+//					data[1] = intval(data[1]);
+//					minDiff = abs(data[0]) * 60 + data[1];
+//					if (data[0] < 0) {
+//						minDiff = -minDiff;
 //					}
 //				} else {
-//					$minDiff = intval($data[0]) * 60;
+//					minDiff = intval(data[0]) * 60;
 //				}
 //			}
 //
 //			# No difference ? Return time unchanged
-//			if (0 == $minDiff) {
-//				return $ts;
+//			if (0 == minDiff) {
+//				return ts;
 //			}
 //
 //			MediaWiki\suppressWarnings(); // E_STRICT system time bitching
 //			# Generate an adjusted date; take advantage of the fact that mktime
-//			# will normalize out-of-range values so we don't have to split $minDiff
+//			# will normalize out-of-range values so we don't have to split minDiff
 //			# into hours and minutes.
-//			$t = mktime((
-//				(int)substr($ts, 8, 2)), # Hours
-//				(int)substr($ts, 10, 2) + $minDiff, # Minutes
-//				(int)substr($ts, 12, 2), # Seconds
-//				(int)substr($ts, 4, 2), # Month
-//				(int)substr($ts, 6, 2), # Day
-//				(int)substr($ts, 0, 4)); # Year
+//			t = mktime((
+//				(int)substr(ts, 8, 2)), # Hours
+//				(int)substr(ts, 10, 2) + minDiff, # Minutes
+//				(int)substr(ts, 12, 2), # Seconds
+//				(int)substr(ts, 4, 2), # Month
+//				(int)substr(ts, 6, 2), # Day
+//				(int)substr(ts, 0, 4)); # Year
 //
-//			$date = date('YmdHis', $t);
+//			date = date('YmdHis', t);
 //			MediaWiki\restoreWarnings();
 //
-//			return $date;
+//			return date;
 //		}
 //
 //		/**
@@ -2196,128 +2204,128 @@ public class XomwLanguage {
 //		* all children.
 //		*
 //		*
-//		* function timeanddate([...], $format = true) {
-//		* 	$datePreference = this.dateFormat($format);
+//		* function timeanddate([...], format = true) {
+//		* 	datePreference = this.dateFormat(format);
 //		* [...]
 //		* }
 //		*
 //		*
-//		* @param int|String|boolean $usePrefs If true, the user's preference is used
+//		* @param int|String|boolean usePrefs If true, the user's preference is used
 //		*   if false, the site/language default is used
 //		*   if int/String, assumed to be a format.
 //		* @return String
 //		*/
-//		function dateFormat($usePrefs = true) {
-//			global $wgUser;
+//		function dateFormat(usePrefs = true) {
+//			global wgUser;
 //
-//			if (is_bool($usePrefs)) {
-//				if ($usePrefs) {
-//					$datePreference = $wgUser->getDatePreference();
+//			if (is_bool(usePrefs)) {
+//				if (usePrefs) {
+//					datePreference = wgUser.getDatePreference();
 //				} else {
-//					$datePreference = (String)User::getDefaultOption('date');
+//					datePreference = (String)User::getDefaultOption('date');
 //				}
 //			} else {
-//				$datePreference = (String)$usePrefs;
+//				datePreference = (String)usePrefs;
 //			}
 //
 //			// return int
-//			if ($datePreference == '') {
+//			if (datePreference == '') {
 //				return 'default';
 //			}
 //
-//			return $datePreference;
+//			return datePreference;
 //		}
 //
 //		/**
 //		* Get a format String for a given type and preference
-//		* @param String $type May be 'date', 'time', 'both', or 'pretty'.
-//		* @param String $pref The format name as it appears in Messages*.php under
-//		*  $datePreferences.
+//		* @param String type May be 'date', 'time', 'both', or 'pretty'.
+//		* @param String pref The format name as it appears in Messages*.php under
+//		*  datePreferences.
 //		*
 //		* @since 1.22 New type 'pretty' that provides a more readable timestamp format
 //		*
 //		* @return String
 //		*/
-//		function getDateFormatString($type, $pref) {
-//			$wasDefault = false;
-//			if ($pref == 'default') {
-//				$wasDefault = true;
-//				$pref = this.getDefaultDateFormat();
+//		function getDateFormatString(type, pref) {
+//			wasDefault = false;
+//			if (pref == 'default') {
+//				wasDefault = true;
+//				pref = this.getDefaultDateFormat();
 //			}
 //
-//			if (!isset(this.dateFormatStrings[$type][$pref])) {
-//				$df = self::$dataCache->getSubitem(this.mCode, 'dateFormats', "$pref $type");
+//			if (!isset(this.dateFormatStrings[type][pref])) {
+//				df = XomwLanguage.dataCache.getSubitem(this.mCode, 'dateFormats', "pref type");
 //
-//				if ($type == 'pretty' && $df == null) {
-//					$df = this.getDateFormatString('date', $pref);
+//				if (type == 'pretty' && df == null) {
+//					df = this.getDateFormatString('date', pref);
 //				}
 //
-//				if (!$wasDefault && $df == null) {
-//					$pref = this.getDefaultDateFormat();
-//					$df = self::$dataCache->getSubitem(this.mCode, 'dateFormats', "$pref $type");
+//				if (!wasDefault && df == null) {
+//					pref = this.getDefaultDateFormat();
+//					df = XomwLanguage.dataCache.getSubitem(this.mCode, 'dateFormats', "pref type");
 //				}
 //
-//				this.dateFormatStrings[$type][$pref] = $df;
+//				this.dateFormatStrings[type][pref] = df;
 //			}
-//			return this.dateFormatStrings[$type][$pref];
+//			return this.dateFormatStrings[type][pref];
 //		}
 //
 //		/**
-//		* @param String $ts The time format which needs to be turned into a
-//		*   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
-//		* @param boolean $adj Whether to adjust the time output according to the
-//		*   user configured offset ($timecorrection)
-//		* @param mixed $format True to use user's date format preference
-//		* @param String|boolean $timecorrection The time offset as returned by
+//		* @param String ts The time format which needs to be turned into a
+//		*   date('YmdHis') format with wfTimestamp(TS_MW,ts)
+//		* @param boolean adj Whether to adjust the time output according to the
+//		*   user configured offset (timecorrection)
+//		* @param mixed format True to use user's date format preference
+//		* @param String|boolean timecorrection The time offset as returned by
 //		*   validateTimeZone() in Special:Preferences
 //		* @return String
 //		*/
-//		public function date($ts, $adj = false, $format = true, $timecorrection = false) {
-//			$ts = wfTimestamp(TS_MW, $ts);
-//			if ($adj) {
-//				$ts = this.userAdjust($ts, $timecorrection);
+//		public function date(ts, adj = false, format = true, timecorrection = false) {
+//			ts = wfTimestamp(TS_MW, ts);
+//			if (adj) {
+//				ts = this.userAdjust(ts, timecorrection);
 //			}
-//			$df = this.getDateFormatString('date', this.dateFormat($format));
-//			return this.sprintfDate($df, $ts);
+//			df = this.getDateFormatString('date', this.dateFormat(format));
+//			return this.sprintfDate(df, ts);
 //		}
 //
 //		/**
-//		* @param String $ts The time format which needs to be turned into a
-//		*   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
-//		* @param boolean $adj Whether to adjust the time output according to the
-//		*   user configured offset ($timecorrection)
-//		* @param mixed $format True to use user's date format preference
-//		* @param String|boolean $timecorrection The time offset as returned by
+//		* @param String ts The time format which needs to be turned into a
+//		*   date('YmdHis') format with wfTimestamp(TS_MW,ts)
+//		* @param boolean adj Whether to adjust the time output according to the
+//		*   user configured offset (timecorrection)
+//		* @param mixed format True to use user's date format preference
+//		* @param String|boolean timecorrection The time offset as returned by
 //		*   validateTimeZone() in Special:Preferences
 //		* @return String
 //		*/
-//		public function time($ts, $adj = false, $format = true, $timecorrection = false) {
-//			$ts = wfTimestamp(TS_MW, $ts);
-//			if ($adj) {
-//				$ts = this.userAdjust($ts, $timecorrection);
+//		public function time(ts, adj = false, format = true, timecorrection = false) {
+//			ts = wfTimestamp(TS_MW, ts);
+//			if (adj) {
+//				ts = this.userAdjust(ts, timecorrection);
 //			}
-//			$df = this.getDateFormatString('time', this.dateFormat($format));
-//			return this.sprintfDate($df, $ts);
+//			df = this.getDateFormatString('time', this.dateFormat(format));
+//			return this.sprintfDate(df, ts);
 //		}
 //
 //		/**
-//		* @param String $ts The time format which needs to be turned into a
-//		*   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
-//		* @param boolean $adj Whether to adjust the time output according to the
-//		*   user configured offset ($timecorrection)
-//		* @param mixed $format What format to return, if it's false output the
+//		* @param String ts The time format which needs to be turned into a
+//		*   date('YmdHis') format with wfTimestamp(TS_MW,ts)
+//		* @param boolean adj Whether to adjust the time output according to the
+//		*   user configured offset (timecorrection)
+//		* @param mixed format What format to return, if it's false output the
 //		*   default one (default true)
-//		* @param String|boolean $timecorrection The time offset as returned by
+//		* @param String|boolean timecorrection The time offset as returned by
 //		*   validateTimeZone() in Special:Preferences
 //		* @return String
 //		*/
-//		public function timeanddate($ts, $adj = false, $format = true, $timecorrection = false) {
-//			$ts = wfTimestamp(TS_MW, $ts);
-//			if ($adj) {
-//				$ts = this.userAdjust($ts, $timecorrection);
+//		public function timeanddate(ts, adj = false, format = true, timecorrection = false) {
+//			ts = wfTimestamp(TS_MW, ts);
+//			if (adj) {
+//				ts = this.userAdjust(ts, timecorrection);
 //			}
-//			$df = this.getDateFormatString('both', this.dateFormat($format));
-//			return this.sprintfDate($df, $ts);
+//			df = this.getDateFormatString('both', this.dateFormat(format));
+//			return this.sprintfDate(df, ts);
 //		}
 //
 //		/**
@@ -2325,24 +2333,24 @@ public class XomwLanguage {
 //		*
 //		* @since 1.20
 //		*
-//		* @param int $seconds The amount of seconds.
-//		* @param array $chosenIntervals The intervals to enable.
+//		* @param int seconds The amount of seconds.
+//		* @param array chosenIntervals The intervals to enable.
 //		*
 //		* @return String
 //		*/
-//		public function formatDuration($seconds, array $chosenIntervals = []) {
-//			$intervals = this.getDurationIntervals($seconds, $chosenIntervals);
+//		public function formatDuration(seconds, array chosenIntervals = []) {
+//			intervals = this.getDurationIntervals(seconds, chosenIntervals);
 //
-//			$segments = [];
+//			segments = [];
 //
-//			foreach ($intervals as $intervalName => $intervalValue) {
+//			foreach (intervals as intervalName => intervalValue) {
 //				// Messages: duration-seconds, duration-minutes, duration-hours, duration-days, duration-weeks,
 //				// duration-years, duration-decades, duration-centuries, duration-millennia
-//				$message = wfMessage('duration-' . $intervalName)->numParams($intervalValue);
-//				$segments[] = $message->inLanguage($this)->escaped();
+//				message = wfMessage('duration-' . intervalName).numParams(intervalValue);
+//				segments[] = message.inLanguage(this).escaped();
 //			}
 //
-//			return this.listToText($segments);
+//			return this.listToText(segments);
 //		}
 //
 //		/**
@@ -2351,14 +2359,14 @@ public class XomwLanguage {
 //		*
 //		* @since 1.20
 //		*
-//		* @param int $seconds The amount of seconds.
-//		* @param array $chosenIntervals The intervals to enable.
+//		* @param int seconds The amount of seconds.
+//		* @param array chosenIntervals The intervals to enable.
 //		*
 //		* @return array
 //		*/
-//		public function getDurationIntervals($seconds, array $chosenIntervals = []) {
-//			if (empty($chosenIntervals)) {
-//				$chosenIntervals = [
+//		public function getDurationIntervals(seconds, array chosenIntervals = []) {
+//			if (empty(chosenIntervals)) {
+//				chosenIntervals = [
 //					'millennia',
 //					'centuries',
 //					'decades',
@@ -2370,32 +2378,32 @@ public class XomwLanguage {
 //				];
 //			}
 //
-//			$intervals = array_intersect_key(self::$durationIntervals, array_flip($chosenIntervals));
-//			$sortedNames = array_keys($intervals);
-//			$smallestInterval = array_pop($sortedNames);
+//			intervals = array_intersect_key(XomwLanguage.durationIntervals, array_flip(chosenIntervals));
+//			sortedNames = array_keys(intervals);
+//			smallestInterval = array_pop(sortedNames);
 //
-//			$segments = [];
+//			segments = [];
 //
-//			foreach ($intervals as $name => $length) {
-//				$value = floor($seconds / $length);
+//			foreach (intervals as name => length) {
+//				value = floor(seconds / length);
 //
-//				if ($value > 0 || ($name == $smallestInterval && empty($segments))) {
-//					$seconds -= $value * $length;
-//					$segments[$name] = $value;
+//				if (value > 0 || (name == smallestInterval && empty(segments))) {
+//					seconds -= value * length;
+//					segments[name] = value;
 //				}
 //			}
 //
-//			return $segments;
+//			return segments;
 //		}
 //
 //		/**
 //		* Internal helper function for userDate(), userTime() and userTimeAndDate()
 //		*
-//		* @param String $type Can be 'date', 'time' or 'both'
-//		* @param String $ts The time format which needs to be turned into a
-//		*   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
-//		* @param User $user User Object used to get preferences for timezone and format
-//		* @param array $options Array, can contain the following keys:
+//		* @param String type Can be 'date', 'time' or 'both'
+//		* @param String ts The time format which needs to be turned into a
+//		*   date('YmdHis') format with wfTimestamp(TS_MW,ts)
+//		* @param User user User Object used to get preferences for timezone and format
+//		* @param array options Array, can contain the following keys:
 //		*   - 'timecorrection': time correction, can have the following values:
 //		*     - true: use user's preference
 //		*     - false: don't use time correction
@@ -2407,34 +2415,34 @@ public class XomwLanguage {
 //		* @since 1.19
 //		* @return String
 //		*/
-//		private function internalUserTimeAndDate($type, $ts, User $user, array $options) {
-//			$ts = wfTimestamp(TS_MW, $ts);
-//			$options += [ 'timecorrection' => true, 'format' => true ];
-//			if ($options['timecorrection'] != false) {
-//				if ($options['timecorrection'] == true) {
-//					$offset = $user->getOption('timecorrection');
+//		private function internalUserTimeAndDate(type, ts, User user, array options) {
+//			ts = wfTimestamp(TS_MW, ts);
+//			options += [ 'timecorrection' => true, 'format' => true ];
+//			if (options['timecorrection'] != false) {
+//				if (options['timecorrection'] == true) {
+//					offset = user.getOption('timecorrection');
 //				} else {
-//					$offset = $options['timecorrection'];
+//					offset = options['timecorrection'];
 //				}
-//				$ts = this.userAdjust($ts, $offset);
+//				ts = this.userAdjust(ts, offset);
 //			}
-//			if ($options['format'] == true) {
-//				$format = $user->getDatePreference();
+//			if (options['format'] == true) {
+//				format = user.getDatePreference();
 //			} else {
-//				$format = $options['format'];
+//				format = options['format'];
 //			}
-//			$df = this.getDateFormatString($type, this.dateFormat($format));
-//			return this.sprintfDate($df, $ts);
+//			df = this.getDateFormatString(type, this.dateFormat(format));
+//			return this.sprintfDate(df, ts);
 //		}
 //
 //		/**
 //		* Get the formatted date for the given timestamp and formatted for
 //		* the given user.
 //		*
-//		* @param mixed $ts Mixed: the time format which needs to be turned into a
-//		*   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
-//		* @param User $user User Object used to get preferences for timezone and format
-//		* @param array $options Array, can contain the following keys:
+//		* @param mixed ts Mixed: the time format which needs to be turned into a
+//		*   date('YmdHis') format with wfTimestamp(TS_MW,ts)
+//		* @param User user User Object used to get preferences for timezone and format
+//		* @param array options Array, can contain the following keys:
 //		*   - 'timecorrection': time correction, can have the following values:
 //		*     - true: use user's preference
 //		*     - false: don't use time correction
@@ -2446,18 +2454,18 @@ public class XomwLanguage {
 //		* @since 1.19
 //		* @return String
 //		*/
-//		public function userDate($ts, User $user, array $options = []) {
-//			return this.internalUserTimeAndDate('date', $ts, $user, $options);
+//		public function userDate(ts, User user, array options = []) {
+//			return this.internalUserTimeAndDate('date', ts, user, options);
 //		}
 //
 //		/**
 //		* Get the formatted time for the given timestamp and formatted for
 //		* the given user.
 //		*
-//		* @param mixed $ts The time format which needs to be turned into a
-//		*   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
-//		* @param User $user User Object used to get preferences for timezone and format
-//		* @param array $options Array, can contain the following keys:
+//		* @param mixed ts The time format which needs to be turned into a
+//		*   date('YmdHis') format with wfTimestamp(TS_MW,ts)
+//		* @param User user User Object used to get preferences for timezone and format
+//		* @param array options Array, can contain the following keys:
 //		*   - 'timecorrection': time correction, can have the following values:
 //		*     - true: use user's preference
 //		*     - false: don't use time correction
@@ -2469,18 +2477,18 @@ public class XomwLanguage {
 //		* @since 1.19
 //		* @return String
 //		*/
-//		public function userTime($ts, User $user, array $options = []) {
-//			return this.internalUserTimeAndDate('time', $ts, $user, $options);
+//		public function userTime(ts, User user, array options = []) {
+//			return this.internalUserTimeAndDate('time', ts, user, options);
 //		}
 //
 //		/**
 //		* Get the formatted date and time for the given timestamp and formatted for
 //		* the given user.
 //		*
-//		* @param mixed $ts The time format which needs to be turned into a
-//		*   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
-//		* @param User $user User Object used to get preferences for timezone and format
-//		* @param array $options Array, can contain the following keys:
+//		* @param mixed ts The time format which needs to be turned into a
+//		*   date('YmdHis') format with wfTimestamp(TS_MW,ts)
+//		* @param User user User Object used to get preferences for timezone and format
+//		* @param array options Array, can contain the following keys:
 //		*   - 'timecorrection': time correction, can have the following values:
 //		*     - true: use user's preference
 //		*     - false: don't use time correction
@@ -2492,8 +2500,8 @@ public class XomwLanguage {
 //		* @since 1.19
 //		* @return String
 //		*/
-//		public function userTimeAndDate($ts, User $user, array $options = []) {
-//			return this.internalUserTimeAndDate('both', $ts, $user, $options);
+//		public function userTimeAndDate(ts, User user, array options = []) {
+//			return this.internalUserTimeAndDate('both', ts, user, options);
 //		}
 //
 //		/**
@@ -2505,232 +2513,232 @@ public class XomwLanguage {
 //		*
 //		* @since 1.26 (Prior to 1.26 method existed but was not meant to be used directly)
 //		*
-//		* @param MWTimestamp $time
-//		* @param MWTimestamp|null $relativeTo The super timestamp to compare to (defaults to now)
-//		* @param User|null $user User the timestamp is being generated for
+//		* @param MWTimestamp time
+//		* @param MWTimestamp|null relativeTo The super timestamp to compare to (defaults to now)
+//		* @param User|null user User the timestamp is being generated for
 //		*  (or null to use main context's user)
 //		* @return String Formatted timestamp
 //		*/
 //		public function getHumanTimestamp(
-//			MWTimestamp $time, MWTimestamp $relativeTo = null, User $user = null
+//			MWTimestamp time, MWTimestamp relativeTo = null, User user = null
 //		) {
-//			if ($relativeTo == null) {
-//				$relativeTo = new MWTimestamp();
+//			if (relativeTo == null) {
+//				relativeTo = new MWTimestamp();
 //			}
-//			if ($user == null) {
-//				$user = RequestContext::getMain()->getUser();
+//			if (user == null) {
+//				user = RequestContext::getMain().getUser();
 //			}
 //
 //			// Adjust for the user's timezone.
-//			$offsetThis = $time->offsetForUser($user);
-//			$offsetRel = $relativeTo->offsetForUser($user);
+//			offsetThis = time.offsetForUser(user);
+//			offsetRel = relativeTo.offsetForUser(user);
 //
-//			$ts = '';
-//			if (Hooks::run('GetHumanTimestamp', [ &$ts, $time, $relativeTo, $user, $this ])) {
-//				$ts = this.getHumanTimestampInternal($time, $relativeTo, $user);
+//			ts = '';
+//			if (Hooks::run('GetHumanTimestamp', [ &ts, time, relativeTo, user, this ])) {
+//				ts = this.getHumanTimestampInternal(time, relativeTo, user);
 //			}
 //
 //			// Reset the timezone on the objects.
-//			$time->timestamp->sub($offsetThis);
-//			$relativeTo->timestamp->sub($offsetRel);
+//			time.timestamp.sub(offsetThis);
+//			relativeTo.timestamp.sub(offsetRel);
 //
-//			return $ts;
+//			return ts;
 //		}
 //
 //		/**
 //		* Convert an MWTimestamp into a pretty human-readable timestamp using
 //		* the given user preferences and relative super time.
 //		*
-//		* @see Language::getHumanTimestamp
-//		* @param MWTimestamp $ts Timestamp to prettify
-//		* @param MWTimestamp $relativeTo Base timestamp
-//		* @param User $user User preferences to use
+//		* @see XomwLanguage.getHumanTimestamp
+//		* @param MWTimestamp ts Timestamp to prettify
+//		* @param MWTimestamp relativeTo Base timestamp
+//		* @param User user User preferences to use
 //		* @return String Human timestamp
 //		* @since 1.26
 //		*/
 //		private function getHumanTimestampInternal(
-//			MWTimestamp $ts, MWTimestamp $relativeTo, User $user
+//			MWTimestamp ts, MWTimestamp relativeTo, User user
 //		) {
-//			$diff = $ts->diff($relativeTo);
-//			$diffDay = (boolean)((int)$ts->timestamp->format('w') -
-//				(int)$relativeTo->timestamp->format('w'));
-//			$days = $diff->days ?: (int)$diffDay;
-//			if ($diff->invert || $days > 5
-//				&& $ts->timestamp->format('Y') != $relativeTo->timestamp->format('Y')
+//			diff = ts.diff(relativeTo);
+//			diffDay = (boolean)((int)ts.timestamp.format('w') -
+//				(int)relativeTo.timestamp.format('w'));
+//			days = diff.days ?: (int)diffDay;
+//			if (diff.invert || days > 5
+//				&& ts.timestamp.format('Y') != relativeTo.timestamp.format('Y')
 //			) {
 //				// Timestamps are in different years: use full timestamp
 //				// Also do full timestamp for future dates
 //				/**
 //				* @todo FIXME: Add better handling of future timestamps.
 //				*/
-//				$format = this.getDateFormatString('both', $user->getDatePreference() ?: 'default');
-//				$ts = this.sprintfDate($format, $ts->getTimestamp(TS_MW));
-//			} elseif ($days > 5) {
+//				format = this.getDateFormatString('both', user.getDatePreference() ?: 'default');
+//				ts = this.sprintfDate(format, ts.getTimestamp(TS_MW));
+//			} elseif (days > 5) {
 //				// Timestamps are in same year,  but more than 5 days ago: show day and month only.
-//				$format = this.getDateFormatString('pretty', $user->getDatePreference() ?: 'default');
-//				$ts = this.sprintfDate($format, $ts->getTimestamp(TS_MW));
-//			} elseif ($days > 1) {
+//				format = this.getDateFormatString('pretty', user.getDatePreference() ?: 'default');
+//				ts = this.sprintfDate(format, ts.getTimestamp(TS_MW));
+//			} elseif (days > 1) {
 //				// Timestamp within the past week: show the day of the week and time
-//				$format = this.getDateFormatString('time', $user->getDatePreference() ?: 'default');
-//				$weekday = self::$mWeekdayMsgs[$ts->timestamp->format('w')];
+//				format = this.getDateFormatString('time', user.getDatePreference() ?: 'default');
+//				weekday = XomwLanguage.mWeekdayMsgs[ts.timestamp.format('w')];
 //				// Messages:
 //				// sunday-at, monday-at, tuesday-at, wednesday-at, thursday-at, friday-at, saturday-at
-//				$ts = wfMessage("$weekday-at")
-//					->inLanguage($this)
-//					->params(this.sprintfDate($format, $ts->getTimestamp(TS_MW)))
-//					->text();
-//			} elseif ($days == 1) {
+//				ts = wfMessage("weekday-at")
+//					.inLanguage(this)
+//					.params(this.sprintfDate(format, ts.getTimestamp(TS_MW)))
+//					.text();
+//			} elseif (days == 1) {
 //				// Timestamp was yesterday: say 'yesterday' and the time.
-//				$format = this.getDateFormatString('time', $user->getDatePreference() ?: 'default');
-//				$ts = wfMessage('yesterday-at')
-//					->inLanguage($this)
-//					->params(this.sprintfDate($format, $ts->getTimestamp(TS_MW)))
-//					->text();
-//			} elseif ($diff->h > 1 || $diff->h == 1 && $diff->i > 30) {
+//				format = this.getDateFormatString('time', user.getDatePreference() ?: 'default');
+//				ts = wfMessage('yesterday-at')
+//					.inLanguage(this)
+//					.params(this.sprintfDate(format, ts.getTimestamp(TS_MW)))
+//					.text();
+//			} elseif (diff.h > 1 || diff.h == 1 && diff.i > 30) {
 //				// Timestamp was today, but more than 90 minutes ago: say 'today' and the time.
-//				$format = this.getDateFormatString('time', $user->getDatePreference() ?: 'default');
-//				$ts = wfMessage('today-at')
-//					->inLanguage($this)
-//					->params(this.sprintfDate($format, $ts->getTimestamp(TS_MW)))
-//					->text();
+//				format = this.getDateFormatString('time', user.getDatePreference() ?: 'default');
+//				ts = wfMessage('today-at')
+//					.inLanguage(this)
+//					.params(this.sprintfDate(format, ts.getTimestamp(TS_MW)))
+//					.text();
 //
 //			// From here on in, the timestamp was soon enough ago so that we can simply say
 //			// XX units ago, e.g., "2 hours ago" or "5 minutes ago"
-//			} elseif ($diff->h == 1) {
+//			} elseif (diff.h == 1) {
 //				// Less than 90 minutes, but more than an hour ago.
-//				$ts = wfMessage('hours-ago')->inLanguage($this)->numParams(1)->text();
-//			} elseif ($diff->i >= 1) {
+//				ts = wfMessage('hours-ago').inLanguage(this).numParams(1).text();
+//			} elseif (diff.i >= 1) {
 //				// A few minutes ago.
-//				$ts = wfMessage('minutes-ago')->inLanguage($this)->numParams($diff->i)->text();
-//			} elseif ($diff->s >= 30) {
+//				ts = wfMessage('minutes-ago').inLanguage(this).numParams(diff.i).text();
+//			} elseif (diff.s >= 30) {
 //				// Less than a minute, but more than 30 sec ago.
-//				$ts = wfMessage('seconds-ago')->inLanguage($this)->numParams($diff->s)->text();
+//				ts = wfMessage('seconds-ago').inLanguage(this).numParams(diff.s).text();
 //			} else {
 //				// Less than 30 seconds ago.
-//				$ts = wfMessage('just-now')->text();
+//				ts = wfMessage('just-now').text();
 //			}
 //
-//			return $ts;
+//			return ts;
 //		}
 //
 //		/**
-//		* @param String $key
+//		* @param String key
 //		* @return String|null
 //		*/
-//		public function getMessage($key) {
-//			return self::$dataCache->getSubitem(this.mCode, 'messages', $key);
+//		public function getMessage(key) {
+//			return XomwLanguage.dataCache.getSubitem(this.mCode, 'messages', key);
 //		}
 //
 //		/**
 //		* @return array
 //		*/
 //		function getAllMessages() {
-//			return self::$dataCache->getItem(this.mCode, 'messages');
+//			return XomwLanguage.dataCache.getItem(this.mCode, 'messages');
 //		}
 //
 //		/**
-//		* @param String $in
-//		* @param String $out
-//		* @param String $String
+//		* @param String in
+//		* @param String out
+//		* @param String String
 //		* @return String
 //		*/
-//		public function iconv($in, $out, $String) {
+//		public function iconv(in, out, String) {
 //			# Even with //IGNORE iconv can whine about illegal characters in
 //			# *input* String. We just ignore those too.
 //			# REF: https://bugs.php.net/bug.php?id=37166
 //			# REF: https://phabricator.wikimedia.org/T18885
 //			MediaWiki\suppressWarnings();
-//			$text = iconv($in, $out . '//IGNORE', $String);
+//			text = iconv(in, out . '//IGNORE', String);
 //			MediaWiki\restoreWarnings();
-//			return $text;
+//			return text;
 //		}
 //
 //		// callback functions for ucwords(), ucwordbreaks()
 //
 //		/**
-//		* @param array $matches
+//		* @param array matches
 //		* @return mixed|String
 //		*/
-//		function ucwordbreaksCallbackAscii($matches) {
-//			return this.ucfirst($matches[1]);
+//		function ucwordbreaksCallbackAscii(matches) {
+//			return this.ucfirst(matches[1]);
 //		}
 //
 //		/**
-//		* @param array $matches
+//		* @param array matches
 //		* @return String
 //		*/
-//		function ucwordbreaksCallbackMB($matches) {
-//			return mb_strtoupper($matches[0]);
+//		function ucwordbreaksCallbackMB(matches) {
+//			return mb_strtoupper(matches[0]);
 //		}
 //
 //		/**
-//		* @param array $matches
+//		* @param array matches
 //		* @return String
 //		*/
-//		function ucwordsCallbackMB($matches) {
-//			return mb_strtoupper($matches[0]);
+//		function ucwordsCallbackMB(matches) {
+//			return mb_strtoupper(matches[0]);
 //		}
 //
 //		/**
 //		* Make a String's first character uppercase
 //		*
-//		* @param String $str
+//		* @param String str
 //		*
 //		* @return String
 //		*/
-//		public function ucfirst($str) {
-//			$o = ord($str);
-//			if ($o < 96) { // if already uppercase...
-//				return $str;
-//			} elseif ($o < 128) {
-//				return ucfirst($str); // use PHP's ucfirst()
+//		public function ucfirst(str) {
+//			o = ord(str);
+//			if (o < 96) { // if already uppercase...
+//				return str;
+//			} elseif (o < 128) {
+//				return ucfirst(str); // use PHP's ucfirst()
 //			} else {
 //				// fall back to more complex logic in case of multibyte strings
-//				return this.uc($str, true);
+//				return this.uc(str, true);
 //			}
 //		}
 //
 //		/**
 //		* Convert a String to uppercase
 //		*
-//		* @param String $str
-//		* @param boolean $first
+//		* @param String str
+//		* @param boolean first
 //		*
 //		* @return String
 //		*/
-//		public function uc($str, $first = false) {
-//			if ($first) {
-//				if (this.isMultibyte($str)) {
-//					return mb_strtoupper(mb_substr($str, 0, 1)) . mb_substr($str, 1);
+//		public function uc(str, first = false) {
+//			if (first) {
+//				if (this.isMultibyte(str)) {
+//					return mb_strtoupper(mb_substr(str, 0, 1)) . mb_substr(str, 1);
 //				} else {
-//					return ucfirst($str);
+//					return ucfirst(str);
 //				}
 //			} else {
-//				return this.isMultibyte($str) ? mb_strtoupper($str) : strtoupper($str);
+//				return this.isMultibyte(str) ? mb_strtoupper(str) : strtoupper(str);
 //			}
 //		}
 //
 //		/**
-//		* @param String $str
+//		* @param String str
 //		* @return mixed|String
 //		*/
-//		function lcfirst($str) {
-//			$o = ord($str);
-//			if (!$o) {
-//				return strval($str);
-//			} elseif ($o >= 128) {
-//				return this.lc($str, true);
-//			} elseif ($o > 96) {
-//				return $str;
+//		function lcfirst(str) {
+//			o = ord(str);
+//			if (!o) {
+//				return strval(str);
+//			} elseif (o >= 128) {
+//				return this.lc(str, true);
+//			} elseif (o > 96) {
+//				return str;
 //			} else {
-//				$str[0] = strtolower($str[0]);
-//				return $str;
+//				str[0] = strtolower(str[0]);
+//				return str;
 //			}
 //		}
 
 	/**
-	* @param String $str
-	* @param boolean $first
+	* @param String str
+	* @param boolean first
 	* @return mixed|String
 	*/
 	public byte[] lc(byte[] str) {return lc(str, false);}
@@ -2752,106 +2760,106 @@ public class XomwLanguage {
 	}
 
 //		/**
-//		* @param String $str
+//		* @param String str
 //		* @return boolean
 //		*/
-//		function isMultibyte($str) {
-//			return strlen($str) != mb_strlen($str);
+//		function isMultibyte(str) {
+//			return strlen(str) != mb_strlen(str);
 //		}
 //
 //		/**
-//		* @param String $str
+//		* @param String str
 //		* @return mixed|String
 //		*/
-//		function ucwords($str) {
-//			if (this.isMultibyte($str)) {
-//				$str = this.lc($str);
+//		function ucwords(str) {
+//			if (this.isMultibyte(str)) {
+//				str = this.lc(str);
 //
 //				// regexp to find first letter in each word (i.e. after each space)
-//				$replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
+//				replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
 //
 //				// function to use to capitalize a single char
 //				return preg_replace_callback(
-//					$replaceRegexp,
-//					[ $this, 'ucwordsCallbackMB' ],
-//					$str
+//					replaceRegexp,
+//					[ this, 'ucwordsCallbackMB' ],
+//					str
 //				);
 //			} else {
-//				return ucwords(strtolower($str));
+//				return ucwords(strtolower(str));
 //			}
 //		}
 //
 //		/**
 //		* capitalize words at word breaks
 //		*
-//		* @param String $str
+//		* @param String str
 //		* @return mixed
 //		*/
-//		function ucwordbreaks($str) {
-//			if (this.isMultibyte($str)) {
-//				$str = this.lc($str);
+//		function ucwordbreaks(str) {
+//			if (this.isMultibyte(str)) {
+//				str = this.lc(str);
 //
 //				// since \b doesn't work for UTF-8, we explicitely define word break chars
-//				$breaks = "[ \-\(\)\}\{\.,\?!]";
+//				breaks = "[ \-\(\)\}\{\.,\?!]";
 //
 //				// find first letter after word break
-//				$replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|" .
-//					"$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
+//				replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|" .
+//					"breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
 //
 //				return preg_replace_callback(
-//					$replaceRegexp,
-//					[ $this, 'ucwordbreaksCallbackMB' ],
-//					$str
+//					replaceRegexp,
+//					[ this, 'ucwordbreaksCallbackMB' ],
+//					str
 //				);
 //			} else {
 //				return preg_replace_callback(
 //					'/\b([\w\x80-\xff]+)\b/',
-//					[ $this, 'ucwordbreaksCallbackAscii' ],
-//					$str
+//					[ this, 'ucwordbreaksCallbackAscii' ],
+//					str
 //				);
 //			}
 //		}
 //
 //		/**
-//		* Return a case-folded representation of $s
+//		* Return a case-folded representation of s
 //		*
-//		* This is a representation such that caseFold($s1)==caseFold($s2) if $s1
-//		* and $s2 are the same except for the case of their characters. It is not
+//		* This is a representation such that caseFold(s1)==caseFold(s2) if s1
+//		* and s2 are the same except for the case of their characters. It is not
 //		* necessary for the value returned to make sense when displayed.
 //		*
 //		* Do *not* perform any other normalisation in this function. If a caller
 //		* uses this function when it should be using a more general normalisation
 //		* function, then fix the caller.
 //		*
-//		* @param String $s
+//		* @param String s
 //		*
 //		* @return String
 //		*/
-//		function caseFold($s) {
-//			return this.uc($s);
+//		function caseFold(s) {
+//			return this.uc(s);
 //		}
 //
 //		/**
-//		* @param String $s
+//		* @param String s
 //		* @return String
 //		* @throws MWException
 //		*/
-//		function checkTitleEncoding($s) {
-//			if (is_array($s)) {
+//		function checkTitleEncoding(s) {
+//			if (is_array(s)) {
 //				throw new MWException('Given array to checkTitleEncoding.');
 //			}
-//			if (StringUtils::isUtf8($s)) {
-//				return $s;
+//			if (StringUtils::isUtf8(s)) {
+//				return s;
 //			}
 //
-//			return this.iconv(this.fallback8bitEncoding(), 'utf-8', $s);
+//			return this.iconv(this.fallback8bitEncoding(), 'utf-8', s);
 //		}
 //
 //		/**
 //		* @return array
 //		*/
 //		function fallback8bitEncoding() {
-//			return self::$dataCache->getItem(this.mCode, 'fallback8bitEncoding');
+//			return XomwLanguage.dataCache.getItem(this.mCode, 'fallback8bitEncoding');
 //		}
 //
 //		/**
@@ -2870,117 +2878,117 @@ public class XomwLanguage {
 //		* Some languages such as Chinese require word segmentation,
 //		* Specify such segmentation when overridden in derived class.
 //		*
-//		* @param String $String
+//		* @param String String
 //		* @return String
 //		*/
-//		function segmentByWord($String) {
-//			return $String;
+//		function segmentByWord(String) {
+//			return String;
 //		}
 //
 //		/**
 //		* Some languages have special punctuation need to be normalized.
 //		* Make such changes here.
 //		*
-//		* @param String $String
+//		* @param String String
 //		* @return String
 //		*/
-//		function normalizeForSearch($String) {
-//			return self::convertDoubleWidth($String);
+//		function normalizeForSearch(String) {
+//			return XomwLanguage.convertDoubleWidth(String);
 //		}
 //
 //		/**
 //		* convert double-width roman characters to single-width.
 //		* range: ff00-ff5f ~= 0020-007f
 //		*
-//		* @param String $String
+//		* @param String String
 //		*
 //		* @return String
 //		*/
-//		protected static function convertDoubleWidth($String) {
-//			static $full = null;
-//			static $half = null;
+//		protected static function convertDoubleWidth(String) {
+//			static full = null;
+//			static half = null;
 //
-//			if ($full == null) {
-//				$fullWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
-//				$halfWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
-//				$full = str_split($fullWidth, 3);
-//				$half = str_split($halfWidth);
+//			if (full == null) {
+//				fullWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+//				halfWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+//				full = str_split(fullWidth, 3);
+//				half = str_split(halfWidth);
 //			}
 //
-//			$String = str_replace($full, $half, $String);
-//			return $String;
+//			String = str_replace(full, half, String);
+//			return String;
 //		}
 //
 //		/**
-//		* @param String $String
-//		* @param String $pattern
+//		* @param String String
+//		* @param String pattern
 //		* @return String
 //		*/
-//		protected static function insertSpace($String, $pattern) {
-//			$String = preg_replace($pattern, " $1 ", $String);
-//			$String = preg_replace('/ +/', ' ', $String);
-//			return $String;
+//		protected static function insertSpace(String, pattern) {
+//			String = preg_replace(pattern, " 1 ", String);
+//			String = preg_replace('/ +/', ' ', String);
+//			return String;
 //		}
 //
 //		/**
-//		* @param array $termsArray
+//		* @param array termsArray
 //		* @return array
 //		*/
-//		function convertForSearchResult($termsArray) {
+//		function convertForSearchResult(termsArray) {
 //			# some languages, e.g. Chinese, need to do a conversion
 //			# in order for search results to be displayed correctly
-//			return $termsArray;
+//			return termsArray;
 //		}
 //
 //		/**
 //		* Get the first character of a String.
 //		*
-//		* @param String $s
+//		* @param String s
 //		* @return String
 //		*/
-//		function firstChar($s) {
-//			$matches = [];
+//		function firstChar(s) {
+//			matches = [];
 //			preg_match(
 //				'/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
 //					'[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/',
-//				$s,
-//				$matches
+//				s,
+//				matches
 //			);
 //
-//			if (isset($matches[1])) {
-//				if (strlen($matches[1]) != 3) {
-//					return $matches[1];
+//			if (isset(matches[1])) {
+//				if (strlen(matches[1]) != 3) {
+//					return matches[1];
 //				}
 //
 //				// Break down Hangul syllables to grab the first jamo
-//				$code = UtfNormal\Utils::utf8ToCodepoint($matches[1]);
-//				if ($code < 0xac00 || 0xd7a4 <= $code) {
-//					return $matches[1];
-//				} elseif ($code < 0xb098) {
+//				code = UtfNormal\Utils::utf8ToCodepoint(matches[1]);
+//				if (code < 0xac00 || 0xd7a4 <= code) {
+//					return matches[1];
+//				} elseif (code < 0xb098) {
 //					return "\xe3\x84\xb1";
-//				} elseif ($code < 0xb2e4) {
+//				} elseif (code < 0xb2e4) {
 //					return "\xe3\x84\xb4";
-//				} elseif ($code < 0xb77c) {
+//				} elseif (code < 0xb77c) {
 //					return "\xe3\x84\xb7";
-//				} elseif ($code < 0xb9c8) {
+//				} elseif (code < 0xb9c8) {
 //					return "\xe3\x84\xb9";
-//				} elseif ($code < 0xbc14) {
+//				} elseif (code < 0xbc14) {
 //					return "\xe3\x85\x81";
-//				} elseif ($code < 0xc0ac) {
+//				} elseif (code < 0xc0ac) {
 //					return "\xe3\x85\x82";
-//				} elseif ($code < 0xc544) {
+//				} elseif (code < 0xc544) {
 //					return "\xe3\x85\x85";
-//				} elseif ($code < 0xc790) {
+//				} elseif (code < 0xc790) {
 //					return "\xe3\x85\x87";
-//				} elseif ($code < 0xcc28) {
+//				} elseif (code < 0xcc28) {
 //					return "\xe3\x85\x88";
-//				} elseif ($code < 0xce74) {
+//				} elseif (code < 0xce74) {
 //					return "\xe3\x85\x8a";
-//				} elseif ($code < 0xd0c0) {
+//				} elseif (code < 0xd0c0) {
 //					return "\xe3\x85\x8b";
-//				} elseif ($code < 0xd30c) {
+//				} elseif (code < 0xd30c) {
 //					return "\xe3\x85\x8c";
-//				} elseif ($code < 0xd558) {
+//				} elseif (code < 0xd558) {
 //					return "\xe3\x85\x8d";
 //				} else {
 //					return "\xe3\x85\x8e";
@@ -2998,21 +3006,21 @@ public class XomwLanguage {
 //		}
 //
 //		/**
-//		* @param String $s
+//		* @param String s
 //		* @return String
 //		* @deprecated No-op since 1.28
 //		*/
-//		function recodeForEdit($s) {
-//			return $s;
+//		function recodeForEdit(s) {
+//			return s;
 //		}
 //
 //		/**
-//		* @param String $s
+//		* @param String s
 //		* @return String
 //		* @deprecated No-op since 1.28
 //		*/
-//		function recodeInput($s) {
-//			return $s;
+//		function recodeInput(s) {
+//			return s;
 //		}
 //
 //		/**
@@ -3022,44 +3030,44 @@ public class XomwLanguage {
 //		*
 //		* This is language-specific for performance reasons only.
 //		*
-//		* @param String $s
+//		* @param String s
 //		*
 //		* @return String
 //		*/
-//		function normalize($s) {
-//			global $wgAllUnicodeFixes;
-//			$s = UtfNormal\Validator::cleanUp($s);
-//			if ($wgAllUnicodeFixes) {
-//				$s = this.transformUsingPairFile('normalize-ar.ser', $s);
-//				$s = this.transformUsingPairFile('normalize-ml.ser', $s);
+//		function normalize(s) {
+//			global wgAllUnicodeFixes;
+//			s = UtfNormal\Validator::cleanUp(s);
+//			if (wgAllUnicodeFixes) {
+//				s = this.transformUsingPairFile('normalize-ar.ser', s);
+//				s = this.transformUsingPairFile('normalize-ml.ser', s);
 //			}
 //
-//			return $s;
+//			return s;
 //		}
 //
 //		/**
 //		* Transform a String using serialized data stored in the given file (which
-//		* must be in the serialized subdirectory of $IP). The file contains pairs
+//		* must be in the serialized subdirectory of IP). The file contains pairs
 //		* mapping source characters to destination characters.
 //		*
 //		* The data is cached in process memory. This will go faster if you have the
 //		* FastStringSearch extension.
 //		*
-//		* @param String $file
-//		* @param String $String
+//		* @param String file
+//		* @param String String
 //		*
 //		* @throws MWException
 //		* @return String
 //		*/
-//		function transformUsingPairFile($file, $String) {
-//			if (!isset(this.transformData[$file])) {
-//				$data = wfGetPrecompiledData($file);
-//				if ($data == false) {
-//					throw new MWException(__METHOD__ . ": The transformation file $file is missing");
+//		function transformUsingPairFile(file, String) {
+//			if (!isset(this.transformData[file])) {
+//				data = wfGetPrecompiledData(file);
+//				if (data == false) {
+//					throw new MWException(__METHOD__ . ": The transformation file file is missing");
 //				}
-//				this.transformData[$file] = new ReplacementArray($data);
+//				this.transformData[file] = new ReplacementArray(data);
 //			}
-//			return this.transformData[$file]->replace($String);
+//			return this.transformData[file].replace(String);
 //		}
 //
 //		/**
@@ -3068,7 +3076,7 @@ public class XomwLanguage {
 //		* @return boolean
 //		*/
 //		function isRTL() {
-//			return self::$dataCache->getItem(this.mCode, 'rtl');
+//			return XomwLanguage.dataCache.getItem(this.mCode, 'rtl');
 //		}
 //
 //		/**
@@ -3110,12 +3118,12 @@ public class XomwLanguage {
 //		* because it makes the output HTML source code more readable. When
 //		* the output is plain text or can be escaped, getDirMark() should be used.
 //		*
-//		* @param boolean $opposite Get the direction mark opposite to your language
+//		* @param boolean opposite Get the direction mark opposite to your language
 //		* @return String
 //		* @since 1.20
 //		*/
-//		function getDirMarkEntity($opposite = false) {
-//			if ($opposite) {
+//		function getDirMarkEntity(opposite = false) {
+//			if (opposite) {
 //				return this.isRTL() ? '‎' : '‏';
 //			}
 //			return this.isRTL() ? '‏' : '‎';
@@ -3128,46 +3136,46 @@ public class XomwLanguage {
 //		* when the output is plain text or can be escaped. When the output is
 //		* HTML, use getDirMarkEntity() instead.
 //		*
-//		* @param boolean $opposite Get the direction mark opposite to your language
+//		* @param boolean opposite Get the direction mark opposite to your language
 //		* @return String
 //		*/
-//		function getDirMark($opposite = false) {
-//			$lrm = "\xE2\x80\x8E"; # LEFT-TO-RIGHT MARK, commonly abbreviated LRM
-//			$rlm = "\xE2\x80\x8F"; # RIGHT-TO-LEFT MARK, commonly abbreviated RLM
-//			if ($opposite) {
-//				return this.isRTL() ? $lrm : $rlm;
+//		function getDirMark(opposite = false) {
+//			lrm = "\xE2\x80\x8E"; # LEFT-TO-RIGHT MARK, commonly abbreviated LRM
+//			rlm = "\xE2\x80\x8F"; # RIGHT-TO-LEFT MARK, commonly abbreviated RLM
+//			if (opposite) {
+//				return this.isRTL() ? lrm : rlm;
 //			}
-//			return this.isRTL() ? $rlm : $lrm;
+//			return this.isRTL() ? rlm : lrm;
 //		}
 //
 //		/**
 //		* @return array
 //		*/
 //		function capitalizeAllNouns() {
-//			return self::$dataCache->getItem(this.mCode, 'capitalizeAllNouns');
+//			return XomwLanguage.dataCache.getItem(this.mCode, 'capitalizeAllNouns');
 //		}
 //
 //		/**
 //		* An arrow, depending on the language direction.
 //		*
-//		* @param String $direction The direction of the arrow: forwards (default),
+//		* @param String direction The direction of the arrow: forwards (default),
 //		*   backwards, left, right, up, down.
 //		* @return String
 //		*/
-//		function getArrow($direction = 'forwards') {
-//			switch ($direction) {
+//		function getArrow(direction = 'forwards') {
+//			switch (direction) {
 //			case 'forwards':
-//				return this.isRTL() ? '←' : '→';
+//				return this.isRTL() ? '?' : '?';
 //			case 'backwards':
-//				return this.isRTL() ? '→' : '←';
+//				return this.isRTL() ? '?' : '?';
 //			case 'left':
-//				return '←';
+//				return '?';
 //			case 'right':
-//				return '→';
+//				return '?';
 //			case 'up':
-//				return '↑';
+//				return '?';
 //			case 'down':
-//				return '↓';
+//				return '?';
 //			}
 //		}
 //
@@ -3177,7 +3185,7 @@ public class XomwLanguage {
 //		* @return boolean
 //		*/
 //		function linkPrefixExtension() {
-//			return self::$dataCache->getItem(this.mCode, 'linkPrefixExtension');
+//			return XomwLanguage.dataCache.getItem(this.mCode, 'linkPrefixExtension');
 //		}
 //
 //		/**
@@ -3185,7 +3193,7 @@ public class XomwLanguage {
 //		* @return array
 //		*/
 //		function getMagicWords() {
-//			return self::$dataCache->getItem(this.mCode, 'magicWords');
+//			return XomwLanguage.dataCache.getItem(this.mCode, 'magicWords');
 //		}
 //
 //		/**
@@ -3202,40 +3210,40 @@ public class XomwLanguage {
 //		/**
 //		* Fill a MagicWord Object with data from here
 //		*
-//		* @param MagicWord $mw
+//		* @param MagicWord mw
 //		*/
-//		function getMagic($mw) {
+//		function getMagic(mw) {
 //			// Saves a function call
 //			if (!this.mMagicHookDone) {
 //				this.doMagicHook();
 //			}
 //
-//			if (isset(this.mMagicExtensions[$mw->mId])) {
-//				$rawEntry = this.mMagicExtensions[$mw->mId];
+//			if (isset(this.mMagicExtensions[mw.mId])) {
+//				rawEntry = this.mMagicExtensions[mw.mId];
 //			} else {
-//				$rawEntry = self::$dataCache->getSubitem(
-//					this.mCode, 'magicWords', $mw->mId);
+//				rawEntry = XomwLanguage.dataCache.getSubitem(
+//					this.mCode, 'magicWords', mw.mId);
 //			}
 //
-//			if (!is_array($rawEntry)) {
-//				wfWarn("\"$rawEntry\" is not a valid magic word for \"$mw->mId\"");
+//			if (!is_array(rawEntry)) {
+//				wfWarn("\"rawEntry\" is not a valid magic word for \"mw.mId\"");
 //			} else {
-//				$mw->mCaseSensitive = $rawEntry[0];
-//				$mw->mSynonyms = array_slice($rawEntry, 1);
+//				mw.mCaseSensitive = rawEntry[0];
+//				mw.mSynonyms = array_slice(rawEntry, 1);
 //			}
 //		}
 //
 //		/**
 //		* Add magic words to the extension array
 //		*
-//		* @param array $newWords
+//		* @param array newWords
 //		*/
-//		function addMagicWordsByLang($newWords) {
-//			$fallbackChain = this.getFallbackLanguages();
-//			$fallbackChain = array_reverse($fallbackChain);
-//			foreach ($fallbackChain as $code) {
-//				if (isset($newWords[$code])) {
-//					this.mMagicExtensions = $newWords[$code] + this.mMagicExtensions;
+//		function addMagicWordsByLang(newWords) {
+//			fallbackChain = this.getFallbackLanguages();
+//			fallbackChain = array_reverse(fallbackChain);
+//			foreach (fallbackChain as code) {
+//				if (isset(newWords[code])) {
+//					this.mMagicExtensions = newWords[code] + this.mMagicExtensions;
 //				}
 //			}
 //		}
@@ -3250,7 +3258,7 @@ public class XomwLanguage {
 //			if (is_null(this.mExtendedSpecialPageAliases)) {
 //				// Initialise array
 //				this.mExtendedSpecialPageAliases =
-//					self::$dataCache->getItem(this.mCode, 'specialPageAliases');
+//					XomwLanguage.dataCache.getItem(this.mCode, 'specialPageAliases');
 //				Hooks::run('LanguageGetSpecialPageAliases',
 //					[ &this.mExtendedSpecialPageAliases, this.getCode() ]);
 //			}
@@ -3261,33 +3269,33 @@ public class XomwLanguage {
 //		/**
 //		* Italic is unsuitable for some languages
 //		*
-//		* @param String $text The text to be emphasized.
+//		* @param String text The text to be emphasized.
 //		* @return String
 //		*/
-//		function emphasize($text) {
-//			return "$text";
+//		function emphasize(text) {
+//			return "text";
 //		}
 
 	/**
 	* Normally we output all numbers in plain en_US style, that is
 	* 293,291.235 for twohundredninetythreethousand-twohundredninetyone
 	* point twohundredthirtyfive. However this is not suitable for all
-	* languages, some such as Bengali (bn) want ২,৯৩,২৯১.২৩৫ and others such as
+	* languages, some such as Bengali (bn) want ?,??,???.??? and others such as
 	* Icelandic just want to use commas instead of dots, and dots instead
 	* of commas like "293.291,235".
 	*
 	* An example of this function being called:
 	* 
-	* wfMessage('message')->numParams($num)->text()
+	* wfMessage('message').numParams(num).text()
 	* 
 	*
-	* See $separatorTransformTable on MessageIs.php for
+	* See separatorTransformTable on MessageIs.php for
 	* the , => . and . => , implementation.
 	*
 	* @todo check if it's viable to use localeconv() for the decimal separator thing.
-	* @param int|float $number The String to be formatted, should be an integer
+	* @param int|float number The String to be formatted, should be an integer
 	*   or a floating point number.
-	* @param boolean $nocommafy Set to true for special numbers like dates
+	* @param boolean nocommafy Set to true for special numbers like dates
 	* @return String
 	*/
 	// DFLT:nocommafy=false
@@ -3316,42 +3324,42 @@ public class XomwLanguage {
 //		/**
 //		* Front-end for non-commafied formatNum
 //		*
-//		* @param int|float $number The String to be formatted, should be an integer
+//		* @param int|float number The String to be formatted, should be an integer
 //		*        or a floating point number.
 //		* @since 1.21
 //		* @return String
 //		*/
-//		public function formatNumNoSeparators($number) {
-//			return this.formatNum($number, true);
+//		public function formatNumNoSeparators(number) {
+//			return this.formatNum(number, true);
 //		}
 //
 //		/**
-//		* @param String $number
+//		* @param String number
 //		* @return String
 //		*/
-//		public function parseFormattedNumber($number) {
-//			$s = this.digitTransformTable();
-//			if ($s) {
+//		public function parseFormattedNumber(number) {
+//			s = this.digitTransformTable();
+//			if (s) {
 //				// eliminate empty array values such as ''. (bug 64347)
-//				$s = array_filter($s);
-//				$number = strtr($number, array_flip($s));
+//				s = array_filter(s);
+//				number = strtr(number, array_flip(s));
 //			}
 //
-//			$s = this.separatorTransformTable();
-//			if ($s) {
+//			s = this.separatorTransformTable();
+//			if (s) {
 //				// eliminate empty array values such as ''. (bug 64347)
-//				$s = array_filter($s);
-//				$number = strtr($number, array_flip($s));
+//				s = array_filter(s);
+//				number = strtr(number, array_flip(s));
 //			}
 //
-//			$number = strtr($number, [ ',' => '' ]);
-//			return $number;
+//			number = strtr(number, [ ',' => '' ]);
+//			return number;
 //		}
 
 	/**
 	* Adds commas to a given number
 	* @since 1.19
-	* @param mixed $number
+	* @param mixed number
 	* @return String
 	*/
 	private static byte[] DIGIT_GROUPING_PATTERN_MILLION = Bry_.new_a7("###,###,###");
@@ -3398,7 +3406,7 @@ public class XomwLanguage {
 
 		if (digitGroupingPattern == null || Bry_.Eq(digitGroupingPattern, DIGIT_GROUPING_PATTERN_MILLION)) {
 			// default grouping is at thousands,  use the same for ###,###,### pattern too.
-			// return strrev((String)preg_replace('/(\d{3})(?=\d)(?!\d*\.)/', '$1,', strrev(number)));
+			// return strrev((String)preg_replace('/(\d{3})(?=\d)(?!\d*\.)/', '1,', strrev(number)));
 			if (negative)
 				tmp_commafy.Add_byte(Byte_ascii.Dash);
 
@@ -3408,7 +3416,7 @@ public class XomwLanguage {
 				&& integerLen > 0)      // ignore numbers with no integer portion; EX: ".123"
 				seg_0 = 3;				// set seg_0 to 3
 
-			// print digits before 1st comma; EX: 12345 -> "12"
+			// print digits before 1st comma; EX: 12345 . "12"
 			if (seg_0 > 0) {
 				tmp_commafy.Add_mid(number, integerBgn, integerBgn + seg_0);
 				integerBgn = integerBgn + seg_0;
@@ -3449,7 +3457,7 @@ public class XomwLanguage {
 			int digitGroupingPatternLen = digitGroupingPattern.length;
 			tmp_matches.Clear();
 
-			// parse digitGroupingPattern for groups of "#"; EX: "##,###" -> 0,2; 3,6
+			// parse digitGroupingPattern for groups of "#"; EX: "##,###" . 0,2; 3,6
 			Int_2_ref match = null;
 			boolean matchContinued = false;
 			for (int i = 0; i < digitGroupingPatternLen; i++) {
@@ -3513,14 +3521,14 @@ public class XomwLanguage {
 //		* @return array
 //		*/
 //		function digitTransformTable() {
-//			return self::$dataCache->getItem(this.mCode, 'digitTransformTable');
+//			return XomwLanguage.dataCache.getItem(this.mCode, 'digitTransformTable');
 //		}
 
 	/**
 	* @return array
 	*/
 	//	private byte[][] separatorTransformTable() {
-	//		return self::$dataCache->getItem(this.mCode, 'separatorTransformTable');
+	//		return XomwLanguage.dataCache.getItem(this.mCode, 'separatorTransformTable');
 	//	}
 
 //		/**
@@ -3529,67 +3537,67 @@ public class XomwLanguage {
 //		* The last two strings are chained with an "and".
 //		* NOTE: This function will only work with standard numeric array keys (0, 1, 2…)
 //		*
-//		* @param String[] $l
+//		* @param String[] l
 //		* @return String
 //		*/
-//		function listToText(array $l) {
-//			$m = count($l) - 1;
-//			if ($m < 0) {
+//		function listToText(array l) {
+//			m = count(l) - 1;
+//			if (m < 0) {
 //				return '';
 //			}
-//			if ($m > 0) {
-//				$and = this.msg('and')->escaped();
-//				$space = this.msg('word-separator')->escaped();
-//				if ($m > 1) {
-//					$comma = this.msg('comma-separator')->escaped();
+//			if (m > 0) {
+//				and = this.msg('and').escaped();
+//				space = this.msg('word-separator').escaped();
+//				if (m > 1) {
+//					comma = this.msg('comma-separator').escaped();
 //				}
 //			}
-//			$s = $l[$m];
-//			for ($i = $m - 1; $i >= 0; $i--) {
-//				if ($i == $m - 1) {
-//					$s = $l[$i] . $and . $space . $s;
+//			s = l[m];
+//			for (i = m - 1; i >= 0; i--) {
+//				if (i == m - 1) {
+//					s = l[i] . and . space . s;
 //				} else {
-//					$s = $l[$i] . $comma . $s;
+//					s = l[i] . comma . s;
 //				}
 //			}
-//			return $s;
+//			return s;
 //		}
 //
 //		/**
 //		* Take a list of strings and build a locale-friendly comma-separated
 //		* list, using the local comma-separator message.
-//		* @param String[] $list Array of strings to put in a comma list
+//		* @param String[] list Array of strings to put in a comma list
 //		* @return String
 //		*/
-//		function commaList(array $list) {
+//		function commaList(array list) {
 //			return implode(
-//				wfMessage('comma-separator')->inLanguage($this)->escaped(),
-//				$list
+//				wfMessage('comma-separator').inLanguage(this).escaped(),
+//				list
 //			);
 //		}
 //
 //		/**
 //		* Take a list of strings and build a locale-friendly semicolon-separated
 //		* list, using the local semicolon-separator message.
-//		* @param String[] $list Array of strings to put in a semicolon list
+//		* @param String[] list Array of strings to put in a semicolon list
 //		* @return String
 //		*/
-//		function semicolonList(array $list) {
+//		function semicolonList(array list) {
 //			return implode(
-//				wfMessage('semicolon-separator')->inLanguage($this)->escaped(),
-//				$list
+//				wfMessage('semicolon-separator').inLanguage(this).escaped(),
+//				list
 //			);
 //		}
 //
 //		/**
 //		* Same as commaList, but separate it with the pipe instead.
-//		* @param String[] $list Array of strings to put in a pipe list
+//		* @param String[] list Array of strings to put in a pipe list
 //		* @return String
 //		*/
-//		function pipeList(array $list) {
+//		function pipeList(array list) {
 //			return implode(
-//				wfMessage('pipe-separator')->inLanguage($this)->escaped(),
-//				$list
+//				wfMessage('pipe-separator').inLanguage(this).escaped(),
+//				list
 //			);
 //		}
 //
@@ -3601,54 +3609,54 @@ public class XomwLanguage {
 //		* multi-byte character sets mean we need to ensure that only whole characters
 //		* are included, otherwise broken characters can be passed to the user
 //		*
-//		* If $length is negative, the String will be truncated from the beginning
+//		* If length is negative, the String will be truncated from the beginning
 //		*
-//		* @param String $String String to truncate
-//		* @param int $length Maximum length (including ellipses)
-//		* @param String $ellipsis String to append to the truncated text
-//		* @param boolean $adjustLength Subtract length of ellipsis from $length.
-//		*	$adjustLength was introduced in 1.18, before that behaved as if false.
+//		* @param String String String to truncate
+//		* @param int length Maximum length (including ellipses)
+//		* @param String ellipsis String to append to the truncated text
+//		* @param boolean adjustLength Subtract length of ellipsis from length.
+//		*	adjustLength was introduced in 1.18, before that behaved as if false.
 //		* @return String
 //		*/
-//		function truncate($String, $length, $ellipsis = '...', $adjustLength = true) {
+//		function truncate(String, length, ellipsis = '...', adjustLength = true) {
 //			# Use the localized ellipsis character
-//			if ($ellipsis == '...') {
-//				$ellipsis = wfMessage('ellipsis')->inLanguage($this)->escaped();
+//			if (ellipsis == '...') {
+//				ellipsis = wfMessage('ellipsis').inLanguage(this).escaped();
 //			}
 //			# Check if there is no need to truncate
-//			if ($length == 0) {
-//				return $ellipsis; // convention
-//			} elseif (strlen($String) <= abs($length)) {
-//				return $String; // no need to truncate
+//			if (length == 0) {
+//				return ellipsis; // convention
+//			} elseif (strlen(String) <= abs(length)) {
+//				return String; // no need to truncate
 //			}
-//			$stringOriginal = $String;
-//			# If ellipsis length is >= $length then we can't apply $adjustLength
-//			if ($adjustLength && strlen($ellipsis) >= abs($length)) {
-//				$String = $ellipsis; // this can be slightly unexpected
+//			stringOriginal = String;
+//			# If ellipsis length is >= length then we can't apply adjustLength
+//			if (adjustLength && strlen(ellipsis) >= abs(length)) {
+//				String = ellipsis; // this can be slightly unexpected
 //			# Otherwise, truncate and add ellipsis...
 //			} else {
-//				$eLength = $adjustLength ? strlen($ellipsis) : 0;
-//				if ($length > 0) {
-//					$length -= $eLength;
-//					$String = substr($String, 0, $length); // xyz...
-//					$String = this.removeBadCharLast($String);
-//					$String = rtrim($String);
-//					$String = $String . $ellipsis;
+//				eLength = adjustLength ? strlen(ellipsis) : 0;
+//				if (length > 0) {
+//					length -= eLength;
+//					String = substr(String, 0, length); // xyz...
+//					String = this.removeBadCharLast(String);
+//					String = rtrim(String);
+//					String = String . ellipsis;
 //				} else {
-//					$length += $eLength;
-//					$String = substr($String, $length); // ...xyz
-//					$String = this.removeBadCharFirst($String);
-//					$String = ltrim($String);
-//					$String = $ellipsis . $String;
+//					length += eLength;
+//					String = substr(String, length); // ...xyz
+//					String = this.removeBadCharFirst(String);
+//					String = ltrim(String);
+//					String = ellipsis . String;
 //				}
 //			}
 //			# Do not truncate if the ellipsis makes the String longer/equal (bug 22181).
-//			# This check is *not* redundant if $adjustLength, due to the single case where
-//			# LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $String.
-//			if (strlen($String) < strlen($stringOriginal)) {
-//				return $String;
+//			# This check is *not* redundant if adjustLength, due to the single case where
+//			# LEN(ellipsis) > ABS(limit arg); stringOriginal could be shorter than String.
+//			if (strlen(String) < strlen(stringOriginal)) {
+//				return String;
 //			} else {
-//				return $stringOriginal;
+//				return stringOriginal;
 //			}
 //		}
 //
@@ -3656,44 +3664,44 @@ public class XomwLanguage {
 //		* Remove bytes that represent an incomplete Unicode character
 //		* at the end of String (e.g. bytes of the char are missing)
 //		*
-//		* @param String $String
+//		* @param String String
 //		* @return String
 //		*/
-//		protected function removeBadCharLast($String) {
-//			if ($String != '') {
-//				$char = ord($String[strlen($String) - 1]);
-//				$m = [];
-//				if ($char >= 0xc0) {
+//		protected function removeBadCharLast(String) {
+//			if (String != '') {
+//				char = ord(String[strlen(String) - 1]);
+//				m = [];
+//				if (char >= 0xc0) {
 //					# We got the first byte only of a multibyte char; remove it.
-//					$String = substr($String, 0, -1);
-//				} elseif ($char >= 0x80 &&
+//					String = substr(String, 0, -1);
+//				} elseif (char >= 0x80 &&
 //					// Use the /s modifier (PCRE_DOTALL) so (.*) also matches newlines
 //					preg_match('/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
-//						'[\xf0-\xf7][\x80-\xbf]{1,2})$/s', $String, $m)
+//						'[\xf0-\xf7][\x80-\xbf]{1,2})/s', String, m)
 //				) {
 //					# We chopped in the middle of a character; remove it
-//					$String = $m[1];
+//					String = m[1];
 //				}
 //			}
-//			return $String;
+//			return String;
 //		}
 //
 //		/**
 //		* Remove bytes that represent an incomplete Unicode character
 //		* at the start of String (e.g. bytes of the char are missing)
 //		*
-//		* @param String $String
+//		* @param String String
 //		* @return String
 //		*/
-//		protected function removeBadCharFirst($String) {
-//			if ($String != '') {
-//				$char = ord($String[0]);
-//				if ($char >= 0x80 && $char < 0xc0) {
+//		protected function removeBadCharFirst(String) {
+//			if (String != '') {
+//				char = ord(String[0]);
+//				if (char >= 0x80 && char < 0xc0) {
 //					# We chopped in the middle of a character; remove the whole thing
-//					$String = preg_replace('/^[\x80-\xbf]+/', '', $String);
+//					String = preg_replace('/^[\x80-\xbf]+/', '', String);
 //				}
 //			}
-//			return $String;
+//			return String;
 //		}
 //
 //		/**
@@ -3704,165 +3712,165 @@ public class XomwLanguage {
 //		* tags like  and , were the tags are self-contained (valid HTML).
 //		* Also, this will not detect things like "display:none" CSS.
 //		*
-//		* Note: since 1.18 you do not need to leave extra room in $length for ellipses.
+//		* Note: since 1.18 you do not need to leave extra room in length for ellipses.
 //		*
-//		* @param String $text HTML String to truncate
-//		* @param int $length (zero/positive) Maximum length (including ellipses)
-//		* @param String $ellipsis String to append to the truncated text
+//		* @param String text HTML String to truncate
+//		* @param int length (zero/positive) Maximum length (including ellipses)
+//		* @param String ellipsis String to append to the truncated text
 //		* @return String
 //		*/
-//		function truncateHtml($text, $length, $ellipsis = '...') {
+//		function truncateHtml(text, length, ellipsis = '...') {
 //			# Use the localized ellipsis character
-//			if ($ellipsis == '...') {
-//				$ellipsis = wfMessage('ellipsis')->inLanguage($this)->escaped();
+//			if (ellipsis == '...') {
+//				ellipsis = wfMessage('ellipsis').inLanguage(this).escaped();
 //			}
 //			# Check if there is clearly no need to truncate
-//			if ($length <= 0) {
-//				return $ellipsis; // no text shown, nothing to format (convention)
-//			} elseif (strlen($text) <= $length) {
-//				return $text; // String short enough even *with* HTML (short-circuit)
+//			if (length <= 0) {
+//				return ellipsis; // no text shown, nothing to format (convention)
+//			} elseif (strlen(text) <= length) {
+//				return text; // String short enough even *with* HTML (short-circuit)
 //			}
 //
-//			$dispLen = 0; // innerHTML legth so far
-//			$testingEllipsis = false; // checking if ellipses will make String longer/equal?
-//			$tagType = 0; // 0-open, 1-close
-//			$bracketState = 0; // 1-tag start, 2-tag name, 0-neither
-//			$entityState = 0; // 0-not entity, 1-entity
-//			$tag = $ret = ''; // accumulated tag name, accumulated result String
-//			$openTags = []; // open tag stack
-//			$maybeState = null; // possible truncation state
+//			dispLen = 0; // innerHTML legth so far
+//			testingEllipsis = false; // checking if ellipses will make String longer/equal?
+//			tagType = 0; // 0-open, 1-close
+//			bracketState = 0; // 1-tag start, 2-tag name, 0-neither
+//			entityState = 0; // 0-not entity, 1-entity
+//			tag = ret = ''; // accumulated tag name, accumulated result String
+//			openTags = []; // open tag stack
+//			maybeState = null; // possible truncation state
 //
-//			$textLen = strlen($text);
-//			$neLength = max(0, $length - strlen($ellipsis)); // non-ellipsis len if truncated
-//			for ($pos = 0; true; ++$pos) {
+//			textLen = strlen(text);
+//			neLength = max(0, length - strlen(ellipsis)); // non-ellipsis len if truncated
+//			for (pos = 0; true; ++pos) {
 //				# Consider truncation once the display length has reached the maximim.
-//				# We check if $dispLen > 0 to grab tags for the $neLength = 0 case.
+//				# We check if dispLen > 0 to grab tags for the neLength = 0 case.
 //				# Check that we're not in the middle of a bracket/entity...
-//				if ($dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState) {
-//					if (!$testingEllipsis) {
-//						$testingEllipsis = true;
+//				if (dispLen && dispLen >= neLength && bracketState == 0 && !entityState) {
+//					if (!testingEllipsis) {
+//						testingEllipsis = true;
 //						# Save where we are; we will truncate here unless there turn out to
 //						# be so few remaining characters that truncation is not necessary.
-//						if (!$maybeState) { // already saved? ($neLength = 0 case)
-//							$maybeState = [ $ret, $openTags ]; // save state
+//						if (!maybeState) { // already saved? (neLength = 0 case)
+//							maybeState = [ ret, openTags ]; // save state
 //						}
-//					} elseif ($dispLen > $length && $dispLen > strlen($ellipsis)) {
+//					} elseif (dispLen > length && dispLen > strlen(ellipsis)) {
 //						# String in fact does need truncation, the truncation point was OK.
-//						list($ret, $openTags) = $maybeState; // reload state
-//						$ret = this.removeBadCharLast($ret); // multi-byte char fix
-//						$ret .= $ellipsis; // add ellipsis
+//						list(ret, openTags) = maybeState; // reload state
+//						ret = this.removeBadCharLast(ret); // multi-byte char fix
+//						ret .= ellipsis; // add ellipsis
 //						break;
 //					}
 //				}
-//				if ($pos >= $textLen) {
+//				if (pos >= textLen) {
 //					break; // extra iteration just for above checks
 //				}
 //
 //				# Read the next char...
-//				$ch = $text[$pos];
-//				$lastCh = $pos ? $text[$pos - 1] : '';
-//				$ret .= $ch; // add to result String
-//				if ($ch == '<') {
-//					this.truncate_endBracket($tag, $tagType, $lastCh, $openTags); // for bad HTML
-//					$entityState = 0; // for bad HTML
-//					$bracketState = 1; // tag started (checking for backslash)
-//				} elseif ($ch == '>') {
-//					this.truncate_endBracket($tag, $tagType, $lastCh, $openTags);
-//					$entityState = 0; // for bad HTML
-//					$bracketState = 0; // out of brackets
-//				} elseif ($bracketState == 1) {
-//					if ($ch == '/') {
-//						$tagType = 1; // close tag (e.g. "")
+//				ch = text[pos];
+//				lastCh = pos ? text[pos - 1] : '';
+//				ret .= ch; // add to result String
+//				if (ch == '<') {
+//					this.truncate_endBracket(tag, tagType, lastCh, openTags); // for bad HTML
+//					entityState = 0; // for bad HTML
+//					bracketState = 1; // tag started (checking for backslash)
+//				} elseif (ch == '>') {
+//					this.truncate_endBracket(tag, tagType, lastCh, openTags);
+//					entityState = 0; // for bad HTML
+//					bracketState = 0; // out of brackets
+//				} elseif (bracketState == 1) {
+//					if (ch == '/') {
+//						tagType = 1; // close tag (e.g. "")
 //					} else {
-//						$tagType = 0; // open tag (e.g. "")
-//						$tag .= $ch;
+//						tagType = 0; // open tag (e.g. "")
+//						tag .= ch;
 //					}
-//					$bracketState = 2; // building tag name
-//				} elseif ($bracketState == 2) {
-//					if ($ch != ' ') {
-//						$tag .= $ch;
+//					bracketState = 2; // building tag name
+//				} elseif (bracketState == 2) {
+//					if (ch != ' ') {
+//						tag .= ch;
 //					} else {
 //						// Name found (e.g. "", $pos + 1);
+//						pos += this.truncate_skip(ret, text, "<>", pos + 1);
 //					}
-//				} elseif ($bracketState == 0) {
-//					if ($entityState) {
-//						if ($ch == ';') {
-//							$entityState = 0;
-//							$dispLen++; // entity is one displayed char
+//				} elseif (bracketState == 0) {
+//					if (entityState) {
+//						if (ch == ';') {
+//							entityState = 0;
+//							dispLen++; // entity is one displayed char
 //						}
 //					} else {
-//						if ($neLength == 0 && !$maybeState) {
-//							// Save state without $ch. We want to *hit* the first
+//						if (neLength == 0 && !maybeState) {
+//							// Save state without ch. We want to *hit* the first
 //							// display char (to get tags) but not *use* it if truncating.
-//							$maybeState = [ substr($ret, 0, -1), $openTags ];
+//							maybeState = [ substr(ret, 0, -1), openTags ];
 //						}
-//						if ($ch == '&') {
-//							$entityState = 1; // entity found, (e.g. " ")
+//						if (ch == '&') {
+//							entityState = 1; // entity found, (e.g. " ")
 //						} else {
-//							$dispLen++; // this char is displayed
-//							// Add the next $max display text chars after this in one swoop...
-//							$max = ($testingEllipsis ? $length : $neLength) - $dispLen;
-//							$skipped = this.truncate_skip($ret, $text, "<>&", $pos + 1, $max);
-//							$dispLen += $skipped;
-//							$pos += $skipped;
+//							dispLen++; // this char is displayed
+//							// Add the next max display text chars after this in one swoop...
+//							max = (testingEllipsis ? length : neLength) - dispLen;
+//							skipped = this.truncate_skip(ret, text, "<>&", pos + 1, max);
+//							dispLen += skipped;
+//							pos += skipped;
 //						}
 //					}
 //				}
 //			}
 //			// Close the last tag if left unclosed by bad HTML
-//			this.truncate_endBracket($tag, $text[$textLen - 1], $tagType, $openTags);
-//			while (count($openTags) > 0) {
-//				$ret .= ''; // close open tags
+//			this.truncate_endBracket(tag, text[textLen - 1], tagType, openTags);
+//			while (count(openTags) > 0) {
+//				ret .= ''; // close open tags
 //			}
-//			return $ret;
+//			return ret;
 //		}
 //
 //		/**
 //		* truncateHtml() helper function
-//		* like strcspn() but adds the skipped chars to $ret
+//		* like strcspn() but adds the skipped chars to ret
 //		*
-//		* @param String $ret
-//		* @param String $text
-//		* @param String $search
-//		* @param int $start
-//		* @param null|int $len
+//		* @param String ret
+//		* @param String text
+//		* @param String search
+//		* @param int start
+//		* @param null|int len
 //		* @return int
 //		*/
-//		private function truncate_skip(&$ret, $text, $search, $start, $len = null) {
-//			if ($len == null) {
-//				$len = -1; // -1 means "no limit" for strcspn
-//			} elseif ($len < 0) {
-//				$len = 0; // sanity
+//		private function truncate_skip(&ret, text, search, start, len = null) {
+//			if (len == null) {
+//				len = -1; // -1 means "no limit" for strcspn
+//			} elseif (len < 0) {
+//				len = 0; // sanity
 //			}
-//			$skipCount = 0;
-//			if ($start < strlen($text)) {
-//				$skipCount = strcspn($text, $search, $start, $len);
-//				$ret .= substr($text, $start, $skipCount);
+//			skipCount = 0;
+//			if (start < strlen(text)) {
+//				skipCount = strcspn(text, search, start, len);
+//				ret .= substr(text, start, skipCount);
 //			}
-//			return $skipCount;
+//			return skipCount;
 //		}
 //
 //		/**
 //		* truncateHtml() helper function
-//		* (a) push or pop $tag from $openTags as needed
-//		* (b) clear $tag value
-//		* @param String &$tag Current HTML tag name we are looking at
-//		* @param int $tagType (0-open tag, 1-close tag)
-//		* @param String $lastCh Character before the '>' that ended this tag
-//		* @param array &$openTags Open tag stack (not accounting for $tag)
+//		* (a) push or pop tag from openTags as needed
+//		* (b) clear tag value
+//		* @param String &tag Current HTML tag name we are looking at
+//		* @param int tagType (0-open tag, 1-close tag)
+//		* @param String lastCh Character before the '>' that ended this tag
+//		* @param array &openTags Open tag stack (not accounting for tag)
 //		*/
-//		private function truncate_endBracket(&$tag, $tagType, $lastCh, &$openTags) {
-//			$tag = ltrim($tag);
-//			if ($tag != '') {
-//				if ($tagType == 0 && $lastCh != '/') {
-//					$openTags[] = $tag; // tag opened (didn't close itself)
-//				} elseif ($tagType == 1) {
-//					if ($openTags && $tag == $openTags[count($openTags) - 1]) {
-//						array_pop($openTags); // tag closed
+//		private function truncate_endBracket(&tag, tagType, lastCh, &openTags) {
+//			tag = ltrim(tag);
+//			if (tag != '') {
+//				if (tagType == 0 && lastCh != '/') {
+//					openTags[] = tag; // tag opened (didn't close itself)
+//				} elseif (tagType == 1) {
+//					if (openTags && tag == openTags[count(openTags) - 1]) {
+//						array_pop(openTags); // tag closed
 //					}
 //				}
-//				$tag = '';
+//				tag = '';
 //			}
 //		}
 //
@@ -3870,54 +3878,54 @@ public class XomwLanguage {
 //		* Grammatical transformations, needed for inflected languages
 //		* Invoked by putting {{grammar:case|word}} in a message
 //		*
-//		* @param String $word
-//		* @param String $case
+//		* @param String word
+//		* @param String case
 //		* @return String
 //		*/
-//		function convertGrammar($word, $case) {
-//			global $wgGrammarForms;
-//			if (isset($wgGrammarForms[this.getCode()][$case][$word])) {
-//				return $wgGrammarForms[this.getCode()][$case][$word];
+//		function convertGrammar(word, case) {
+//			global wgGrammarForms;
+//			if (isset(wgGrammarForms[this.getCode()][case][word])) {
+//				return wgGrammarForms[this.getCode()][case][word];
 //			}
 //
-//			$grammarTransformations = this.getGrammarTransformations();
+//			grammarTransformations = this.getGrammarTransformations();
 //
-//			if (isset($grammarTransformations[$case])) {
-//				$forms = $grammarTransformations[$case];
+//			if (isset(grammarTransformations[case])) {
+//				forms = grammarTransformations[case];
 //
 //				// Some names of grammar rules are aliases for other rules.
 //				// In such cases the value is a String rather than Object,
 //				// so load the actual rules.
-//				if (is_string($forms)) {
-//					$forms = $grammarTransformations[$forms];
+//				if (is_string(forms)) {
+//					forms = grammarTransformations[forms];
 //				}
 //
-//				foreach (array_values($forms) as $rule) {
-//					$form = $rule[0];
+//				foreach (array_values(forms) as rule) {
+//					form = rule[0];
 //
-//					if ($form == '@metadata') {
+//					if (form == '@metadata') {
 //						continue;
 //					}
 //
-//					$replacement = $rule[1];
+//					replacement = rule[1];
 //
-//					$regex = '/' . addcslashes($form, '/') . '/u';
-//					$patternMatches = preg_match($regex, $word);
+//					regex = '/' . addcslashes(form, '/') . '/u';
+//					patternMatches = preg_match(regex, word);
 //
-//					if ($patternMatches == false) {
+//					if (patternMatches == false) {
 //						wfLogWarning(
 //							'An error occurred while processing grammar. ' .
-//							"Word: '$word'. Regex: /$form/."
+//							"Word: 'word'. Regex: /form/."
 //						);
-//					} elseif ($patternMatches == 1) {
-//						$word = preg_replace($regex, $replacement, $word);
+//					} elseif (patternMatches == 1) {
+//						word = preg_replace(regex, replacement, word);
 //
 //						break;
 //					}
 //				}
 //			}
 //
-//			return $word;
+//			return word;
 //		}
 //
 //		/**
@@ -3926,11 +3934,11 @@ public class XomwLanguage {
 //		* @since 1.20
 //		*/
 //		function getGrammarForms() {
-//			global $wgGrammarForms;
-//			if (isset($wgGrammarForms[this.getCode()])
-//				&& is_array($wgGrammarForms[this.getCode()])
+//			global wgGrammarForms;
+//			if (isset(wgGrammarForms[this.getCode()])
+//				&& is_array(wgGrammarForms[this.getCode()])
 //			) {
-//				return $wgGrammarForms[this.getCode()];
+//				return wgGrammarForms[this.getCode()];
 //			}
 //
 //			return [];
@@ -3946,33 +3954,33 @@ public class XomwLanguage {
 //		* @since 1.28
 //		*/
 //		public function getGrammarTransformations() {
-//			$languageCode = this.getCode();
+//			languageCode = this.getCode();
 //
-//			if (self::$grammarTransformations == null) {
-//				self::$grammarTransformations = new MapCacheLRU(10);
+//			if (XomwLanguage.grammarTransformations == null) {
+//				XomwLanguage.grammarTransformations = new MapCacheLRU(10);
 //			}
 //
-//			if (self::$grammarTransformations->has($languageCode)) {
-//				return self::$grammarTransformations->get($languageCode);
+//			if (XomwLanguage.grammarTransformations.has(languageCode)) {
+//				return XomwLanguage.grammarTransformations.get(languageCode);
 //			}
 //
-//			$data = [];
+//			data = [];
 //
-//			$grammarDataFile = __DIR__ . "/data/grammarTransformations/$languageCode.json";
-//			if (is_readable($grammarDataFile)) {
-//				$data = FormatJson::decode(
-//					file_get_contents($grammarDataFile),
+//			grammarDataFile = __DIR__ . "/data/grammarTransformations/languageCode.json";
+//			if (is_readable(grammarDataFile)) {
+//				data = FormatJson::decode(
+//					file_get_contents(grammarDataFile),
 //					true
 //				);
 //
-//				if ($data == null) {
-//					throw new MWException("Invalid grammar data for \"$languageCode\".");
+//				if (data == null) {
+//					throw new MWException("Invalid grammar data for \"languageCode\".");
 //				}
 //
-//				self::$grammarTransformations->set($languageCode, $data);
+//				XomwLanguage.grammarTransformations.set(languageCode, data);
 //			}
 //
-//			return $data;
+//			return data;
 //		}
 //
 //		/**
@@ -3989,96 +3997,104 @@ public class XomwLanguage {
 //		* If fewer than three forms are given, the default is to use the first (masculine) form.
 //		* These details can be overridden in subclasses.
 //		*
-//		* @param String $gender
-//		* @param array $forms
+//		* @param String gender
+//		* @param array forms
 //		*
 //		* @return String
 //		*/
-//		function gender($gender, $forms) {
-//			if (!count($forms)) {
+//		function gender(gender, forms) {
+//			if (!count(forms)) {
 //				return '';
 //			}
-//			$forms = this.preConvertPlural($forms, 2);
-//			if ($gender == 'male') {
-//				return $forms[0];
+//			forms = this.preConvertPlural(forms, 2);
+//			if (gender == 'male') {
+//				return forms[0];
 //			}
-//			if ($gender == 'female') {
-//				return $forms[1];
+//			if (gender == 'female') {
+//				return forms[1];
 //			}
-//			return isset($forms[2]) ? $forms[2] : $forms[0];
+//			return isset(forms[2]) ? forms[2] : forms[0];
 //		}
-//
-//		/**
-//		* Plural form transformations, needed for some languages.
-//		* For example, there are 3 form of plural in Russian and Polish,
-//		* depending on "count mod 10". See [[w:Plural]]
-//		* For English it is pretty simple.
-//		*
-//		* Invoked by putting {{plural:count|wordform1|wordform2}}
-//		* or {{plural:count|wordform1|wordform2|wordform3}}
-//		*
-//		* Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
-//		*
-//		* @param int $count Non-localized number
-//		* @param array $forms Different plural forms
-//		* @return String Correct form of plural for $count in this language
-//		*/
-//		function convertPlural($count, $forms) {
-//			// Handle explicit n=pluralform cases
-//			$forms = this.handleExplicitPluralForms($count, $forms);
-//			if (is_string($forms)) {
-//				return $forms;
-//			}
-//			if (!count($forms)) {
-//				return '';
-//			}
-//
-//			$pluralForm = this.getPluralRuleIndexNumber($count);
-//			$pluralForm = min($pluralForm, count($forms) - 1);
-//			return $forms[$pluralForm];
-//		}
-//
-//		/**
-//		* Handles explicit plural forms for Language::convertPlural()
-//		*
-//		* In {{PLURAL:$1|0=nothing|one|many}}, 0=nothing will be returned if $1 equals zero.
-//		* If an explicitly defined plural form matches the $count, then
-//		* String value returned, otherwise array returned for further consideration
-//		* by CLDR rules or overridden convertPlural().
-//		*
-//		* @since 1.23
-//		*
-//		* @param int $count Non-localized number
-//		* @param array $forms Different plural forms
-//		*
-//		* @return array|String
-//		*/
-//		protected function handleExplicitPluralForms($count, array $forms) {
-//			foreach ($forms as $index => $form) {
-//				if (preg_match('/\d+=/i', $form)) {
-//					$pos = strpos($form, '=');
-//					if (substr($form, 0, $pos) == (String)$count) {
-//						return substr($form, $pos + 1);
-//					}
-//					unset($forms[$index]);
-//				}
-//			}
-//			return array_values($forms);
-//		}
-//
+
+	/**
+	* Plural form transformations, needed for some languages.
+	* For example, there are 3 form of plural in Russian and Polish,
+	* depending on "count mod 10". See [[w:Plural]]
+	* For English it is pretty simple.
+	*
+	* Invoked by putting {{plural:count|wordform1|wordform2}}
+	* or {{plural:count|wordform1|wordform2|wordform3}}
+	*
+	* Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
+	*
+	* @param int count Non-localized number
+	* @param array forms Different plural forms
+	* @return String Correct form of plural for count in this language
+	*/
+	public String convertPlural(String count, XophpArray forms) {
+		// Handle explicit n=pluralform cases
+		Object formsObject = this.handleExplicitPluralForms(count, forms);
+		if (XophpString_.is_string(formsObject)) {
+			return (String)formsObject;
+		}
+		forms = (XophpArray)formsObject;
+		if (!forms.Count_bool()) {
+			return "";
+		}
+
+		int pluralForm = this.getPluralRuleIndexNumber(count);
+		pluralForm = XophpMath.min(pluralForm, forms.Count() - 1);
+		return forms.Get_at_str(pluralForm);
+	}
+
+	/**
+	* Handles explicit plural forms for XomwLanguage.convertPlural()
+	*
+	* In {{PLURAL:1|0=nothing|one|many}}, 0=nothing will be returned if 1 equals zero.
+	* If an explicitly defined plural form matches the count, then
+	* String value returned, otherwise array returned for further consideration
+	* by CLDR rules or overridden convertPlural().
+	*
+	* @since 1.23
+	*
+	* @param int count Non-localized number
+	* @param array forms Different plural forms
+	*
+	* @return array|String
+	*/
+	public Object handleExplicitPluralForms(String count, XophpArray forms) {
+		XophpArray mutable = forms.Clone();
+		int len = forms.Len();
+		for (int i = 0; i < len; i++) {
+			XophpArrayItm formItem = forms.Get_at_itm(i);
+			String index = formItem.Key();
+			String form = (String)formItem.Val();
+			if (XophpRegex_.preg_match_bool(handleExplicitPluralForms_digits, XophpRegex_.MODIFIER_i, form)) {
+				int pos = XophpString_.strpos(form, "=");
+				if (String_.Eq(XophpString_.substr(form, 0, pos), count)) {
+					return XophpString_.substr(form, pos + 1);
+				}
+				mutable.Unset(index);
+			}
+		}
+
+		return XophpArrayUtl.array_values(mutable);
+	}
+	private static final    Regx_adp handleExplicitPluralForms_digits = Regx_adp_.new_("\\d+=");
+
 //		/**
 //		* Checks that convertPlural was given an array and pads it to requested
 //		* amount of forms by copying the last one.
 //		*
-//		* @param array $forms Array of forms given to convertPlural
-//		* @param int $count How many forms should there be at least
+//		* @param array forms Array of forms given to convertPlural
+//		* @param int count How many forms should there be at least
 //		* @return array Padded array of forms or an exception if not an array
 //		*/
-//		protected function preConvertPlural(/* Array */ $forms, $count) {
-//			while (count($forms) < $count) {
-//				$forms[] = $forms[count($forms) - 1];
+//		protected function preConvertPlural(/* Array */ forms, count) {
+//			while (count(forms) < count) {
+//				forms[] = forms[count(forms) - 1];
 //			}
-//			return $forms;
+//			return forms;
 //		}
 //
 //		/**
@@ -4094,21 +4110,21 @@ public class XomwLanguage {
 //		* there is no embedding equivalent of U+2068 FSI (isolation with heuristic
 //		* direction inference). The latter is cleaner but still not widely supported.
 //		*
-//		* @param String $text Text to wrap
+//		* @param String text Text to wrap
 //		* @return String Text, wrapped in LRE...PDF or RLE...PDF or nothing
 //		*/
-//		public function embedBidi($text = '') {
-//			$dir = Language::strongDirFromContent($text);
-//			if ($dir == 'ltr') {
+//		public function embedBidi(text = '') {
+//			dir = XomwLanguage.strongDirFromContent(text);
+//			if (dir == 'ltr') {
 //				// Wrap in LEFT-TO-RIGHT EMBEDDING ... POP DIRECTIONAL FORMATTING
-//				return self::$lre . $text . self::$pdf;
+//				return XomwLanguage.lre . text . XomwLanguage.pdf;
 //			}
-//			if ($dir == 'rtl') {
+//			if (dir == 'rtl') {
 //				// Wrap in RIGHT-TO-LEFT EMBEDDING ... POP DIRECTIONAL FORMATTING
-//				return self::$rle . $text . self::$pdf;
+//				return XomwLanguage.rle . text . XomwLanguage.pdf;
 //			}
 //			// No strong directionality: do not wrap
-//			return $text;
+//			return text;
 //		}
 //
 //		/**
@@ -4118,44 +4134,44 @@ public class XomwLanguage {
 //		* on old expiry lengths recorded in log entries. You'd need to provide the start date to
 //		* match up with it.
 //		*
-//		* @param String $str The validated block duration in English
-//		* @param User $user User Object to use timezone from or null for $wgUser
-//		* @param int $now Current timestamp, for formatting relative block durations
+//		* @param String str The validated block duration in English
+//		* @param User user User Object to use timezone from or null for wgUser
+//		* @param int now Current timestamp, for formatting relative block durations
 //		* @return String Somehow translated block duration
 //		* @see LanguageFi.php for example implementation
 //		*/
-//		function translateBlockExpiry($str, User $user = null, $now = 0) {
-//			$duration = SpecialBlock::getSuggestedDurations($this);
-//			foreach ($duration as $show => $value) {
-//				if (strcmp($str, $value) == 0) {
-//					return htmlspecialchars(trim($show));
+//		function translateBlockExpiry(str, User user = null, now = 0) {
+//			duration = SpecialBlock::getSuggestedDurations(this);
+//			foreach (duration as show => value) {
+//				if (strcmp(str, value) == 0) {
+//					return htmlspecialchars(trim(show));
 //				}
 //			}
 //
-//			if (wfIsInfinity($str)) {
-//				foreach ($duration as $show => $value) {
-//					if (wfIsInfinity($value)) {
-//						return htmlspecialchars(trim($show));
+//			if (wfIsInfinity(str)) {
+//				foreach (duration as show => value) {
+//					if (wfIsInfinity(value)) {
+//						return htmlspecialchars(trim(show));
 //					}
 //				}
 //			}
 //
 //			// If all else fails, return a standard duration or timestamp description.
-//			$time = strtotime($str, $now);
-//			if ($time == false) { // Unknown format. Return it as-is in case.
-//				return $str;
-//			} elseif ($time != strtotime($str, $now + 1)) { // It's a relative timestamp.
+//			time = strtotime(str, now);
+//			if (time == false) { // Unknown format. Return it as-is in case.
+//				return str;
+//			} elseif (time != strtotime(str, now + 1)) { // It's a relative timestamp.
 //				// The result differs based on current time, so it's a duration length.
-//				return this.formatDuration($time);
+//				return this.formatDuration(time);
 //			} else { // It's an absolute timestamp.
-//				if ($time == 0) {
+//				if (time == 0) {
 //					// wfTimestamp() handles 0 as current time instead of epoch.
-//					$time = '19700101000000';
+//					time = '19700101000000';
 //				}
-//				if ($user) {
-//					return this.userTimeAndDate($time, $user);
+//				if (user) {
+//					return this.userTimeAndDate(time, user);
 //				}
-//				return this.timeanddate($time);
+//				return this.timeanddate(time);
 //			}
 //		}
 //
@@ -4163,21 +4179,21 @@ public class XomwLanguage {
 //		* languages like Chinese need to be segmented in order for the diff
 //		* to be of any use
 //		*
-//		* @param String $text
+//		* @param String text
 //		* @return String
 //		*/
-//		public function segmentForDiff($text) {
-//			return $text;
+//		public function segmentForDiff(text) {
+//			return text;
 //		}
 //
 //		/**
 //		* and unsegment to show the result
 //		*
-//		* @param String $text
+//		* @param String text
 //		* @return String
 //		*/
-//		public function unsegmentForDiff($text) {
-//			return $text;
+//		public function unsegmentForDiff(text) {
+//			return text;
 //		}
 //
 //		/**
@@ -4193,41 +4209,41 @@ public class XomwLanguage {
 //		/**
 //		* convert text to all supported variants
 //		*
-//		* @param String $text
+//		* @param String text
 //		* @return array
 //		*/
-//		public function autoConvertToAllVariants($text) {
-//			return this.mConverter->autoConvertToAllVariants($text);
+//		public function autoConvertToAllVariants(text) {
+//			return this.mConverter.autoConvertToAllVariants(text);
 //		}
 //
 //		/**
 //		* convert text to different variants of a language.
 //		*
-//		* @param String $text
+//		* @param String text
 //		* @return String
 //		*/
-//		public function convert($text) {
-//			return this.mConverter->convert($text);
+//		public function convert(text) {
+//			return this.mConverter.convert(text);
 //		}
 //
 //		/**
 //		* Convert a Title Object to a String in the preferred variant
 //		*
-//		* @param Title $title
+//		* @param Title title
 //		* @return String
 //		*/
-//		public function convertTitle($title) {
-//			return this.mConverter->convertTitle($title);
+//		public function convertTitle(title) {
+//			return this.mConverter.convertTitle(title);
 //		}
 //
 //		/**
 //		* Convert a namespace index to a String in the preferred variant
 //		*
-//		* @param int $ns
+//		* @param int ns
 //		* @return String
 //		*/
-//		public function convertNamespace($ns) {
-//			return this.mConverter->convertNamespace($ns);
+//		public function convertNamespace(ns) {
+//			return this.mConverter.convertNamespace(ns);
 //		}
 //
 //		/**
@@ -4243,30 +4259,30 @@ public class XomwLanguage {
 //		* Check if the language has the specific variant
 //		*
 //		* @since 1.19
-//		* @param String $variant
+//		* @param String variant
 //		* @return boolean
 //		*/
-//		public function hasVariant($variant) {
-//			return (boolean)this.mConverter->validateVariant($variant);
+//		public function hasVariant(variant) {
+//			return (boolean)this.mConverter.validateVariant(variant);
 //		}
 //
 //		/**
 //		* Perform output conversion on a String, and encode for safe HTML output.
-//		* @param String $text Text to be converted
-//		* @param boolean $isTitle Whether this conversion is for the article title
+//		* @param String text Text to be converted
+//		* @param boolean isTitle Whether this conversion is for the article title
 //		* @return String
 //		* @todo this should get integrated somewhere sane
 //		*/
-//		public function convertHtml($text, $isTitle = false) {
-//			return htmlspecialchars(this.convert($text, $isTitle));
+//		public function convertHtml(text, isTitle = false) {
+//			return htmlspecialchars(this.convert(text, isTitle));
 //		}
 //
 //		/**
-//		* @param String $key
+//		* @param String key
 //		* @return String
 //		*/
-//		public function convertCategoryKey($key) {
-//			return this.mConverter->convertCategoryKey($key);
+//		public function convertCategoryKey(key) {
+//			return this.mConverter.convertCategoryKey(key);
 //		}
 //
 //		/**
@@ -4276,28 +4292,28 @@ public class XomwLanguage {
 //		* @return array An array of language codes
 //		*/
 //		public function getVariants() {
-//			return this.mConverter->getVariants();
+//			return this.mConverter.getVariants();
 //		}
 //
 //		/**
 //		* @return String
 //		*/
 //		public function getPreferredVariant() {
-//			return this.mConverter->getPreferredVariant();
+//			return this.mConverter.getPreferredVariant();
 //		}
 //
 //		/**
 //		* @return String
 //		*/
 //		public function getDefaultVariant() {
-//			return this.mConverter->getDefaultVariant();
+//			return this.mConverter.getDefaultVariant();
 //		}
 //
 //		/**
 //		* @return String
 //		*/
 //		public function getURLVariant() {
-//			return this.mConverter->getURLVariant();
+//			return this.mConverter.getURLVariant();
 //		}
 //
 //		/**
@@ -4307,13 +4323,13 @@ public class XomwLanguage {
 //		* tries to find it. See e.g. LanguageZh.php
 //		* The input parameters may be modified upon return
 //		*
-//		* @param String &$link The name of the link
-//		* @param Title &$nt The title Object of the link
-//		* @param boolean $ignoreOtherCond To disable other conditions when
+//		* @param String &link The name of the link
+//		* @param Title &nt The title Object of the link
+//		* @param boolean ignoreOtherCond To disable other conditions when
 //		*   we need to transclude a template or update a category's link
 //		*/
-//		public function findVariantLink(&$link, &$nt, $ignoreOtherCond = false) {
-//			this.mConverter->findVariantLink($link, $nt, $ignoreOtherCond);
+//		public function findVariantLink(&link, &nt, ignoreOtherCond = false) {
+//			this.mConverter.findVariantLink(link, nt, ignoreOtherCond);
 //		}
 //
 //		/**
@@ -4323,7 +4339,7 @@ public class XomwLanguage {
 //		* @return String
 //		*/
 //		function getExtraHashOptions() {
-//			return this.mConverter->getExtraHashOptions();
+//			return this.mConverter.getExtraHashOptions();
 //		}
 //
 //		/**
@@ -4334,17 +4350,17 @@ public class XomwLanguage {
 //		* @return String
 //		*/
 //		public function getParsedTitle() {
-//			return this.mConverter->getParsedTitle();
+//			return this.mConverter.getParsedTitle();
 //		}
 //
 //		/**
 //		* Refresh the cache of conversion tables when
 //		* MediaWiki:Conversiontable* is updated.
 //		*
-//		* @param Title $title The Title of the page being updated
+//		* @param Title title The Title of the page being updated
 //		*/
-//		public function updateConversionTable(Title $title) {
-//			this.mConverter->updateConversionTable($title);
+//		public function updateConversionTable(Title title) {
+//			this.mConverter.updateConversionTable(title);
 //		}
 //
 //		/**
@@ -4355,16 +4371,16 @@ public class XomwLanguage {
 //		* This function is called "markNoConversion" for historical
 //		* reasons.
 //		*
-//		* @param String $text Text to be used for external link
-//		* @param boolean $noParse Wrap it without confirming it's a real URL first
+//		* @param String text Text to be used for external link
+//		* @param boolean noParse Wrap it without confirming it's a real URL first
 //		* @return String The tagged text
 //		*/
-//		public function markNoConversion($text, $noParse = false) {
+//		public function markNoConversion(text, noParse = false) {
 //			// Excluding protocal-relative URLs may avoid many false positives.
-//			if ($noParse || preg_match('/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text)) {
-//				return this.mConverter->markNoConversion($text);
+//			if (noParse || preg_match('/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', text)) {
+//				return this.mConverter.markNoConversion(text);
 //			} else {
-//				return $text;
+//				return text;
 //			}
 //		}
 //
@@ -4375,7 +4391,7 @@ public class XomwLanguage {
 //		* @return String
 //		*/
 //		public function linkTrail() {
-//			return self::$dataCache->getItem(this.mCode, 'linkTrail');
+//			return XomwLanguage.dataCache.getItem(this.mCode, 'linkTrail');
 //		}
 //
 //		/**
@@ -4385,7 +4401,7 @@ public class XomwLanguage {
 //		* @return String
 //		*/
 //		public function linkPrefixCharset() {
-//			return self::$dataCache->getItem(this.mCode, 'linkPrefixCharset');
+//			return XomwLanguage.dataCache.getItem(this.mCode, 'linkPrefixCharset');
 //		}
 //
 //		/**
@@ -4400,30 +4416,30 @@ public class XomwLanguage {
 //				return this.mParentLanguage;
 //			}
 //
-//			$code = explode('-', this.getCode())[0];
-//			if (!in_array($code, LanguageConverter::$languagesWithVariants)) {
+//			code = explode('-', this.getCode())[0];
+//			if (!in_array(code, LanguageConverter::languagesWithVariants)) {
 //				this.mParentLanguage = null;
 //				return null;
 //			}
-//			$lang = Language::factory($code);
-//			if (!$lang->hasVariant(this.getCode())) {
+//			lang = XomwLanguage.factory(code);
+//			if (!lang.hasVariant(this.getCode())) {
 //				this.mParentLanguage = null;
 //				return null;
 //			}
 //
-//			this.mParentLanguage = $lang;
-//			return $lang;
+//			this.mParentLanguage = lang;
+//			return lang;
 //		}
 //
 //		/**
 //		* Compare with an other language Object
 //		*
 //		* @since 1.28
-//		* @param Language $lang
+//		* @param Language lang
 //		* @return boolean
 //		*/
-//		public function equals(Language $lang) {
-//			return $lang->getCode() == this.mCode;
+//		public function equals(Language lang) {
+//			return lang.getCode() == this.mCode;
 //		}
 //
 //		/**
@@ -4456,10 +4472,10 @@ public class XomwLanguage {
 //		}
 //
 //		/**
-//		* @param String $code
+//		* @param String code
 //		*/
-//		public function setCode($code) {
-//			this.mCode = $code;
+//		public function setCode(code) {
+//			this.mCode = code;
 //			// Ensure we don't leave incorrect cached data lying around
 //			this.mHtmlCode = null;
 //			this.mParentLanguage = false;
@@ -4467,135 +4483,139 @@ public class XomwLanguage {
 //
 //		/**
 //		* Get the language code from a file name. Inverse of getFileName()
-//		* @param String $filename $prefix . $languageCode . $suffix
-//		* @param String $prefix Prefix before the language code
-//		* @param String $suffix Suffix after the language code
-//		* @return String Language code, or false if $prefix or $suffix isn't found
+//		* @param String filename prefix . languageCode . suffix
+//		* @param String prefix Prefix before the language code
+//		* @param String suffix Suffix after the language code
+//		* @return String Language code, or false if prefix or suffix isn't found
 //		*/
-//		public static function getCodeFromFileName($filename, $prefix = 'Language', $suffix = '.php') {
-//			$m = null;
-//			preg_match('/' . preg_quote($prefix, '/') . '([A-Z][a-z_]+)' .
-//				preg_quote($suffix, '/') . '/', $filename, $m);
-//			if (!count($m)) {
+//		public static function getCodeFromFileName(filename, prefix = 'Language', suffix = '.php') {
+//			m = null;
+//			preg_match('/' . preg_quote(prefix, '/') . '([A-Z][a-z_]+)' .
+//				preg_quote(suffix, '/') . '/', filename, m);
+//			if (!count(m)) {
 //				return false;
 //			}
-//			return str_replace('_', '-', strtolower($m[1]));
+//			return str_replace('_', '-', strtolower(m[1]));
 //		}
 //
 //		/**
-//		* @param String $code
+//		* @param String code
 //		* @return String Name of the language class
 //		*/
-//		public static function classFromCode($code) {
-//			if ($code == 'en') {
+//		public static function classFromCode(code) {
+//			if (code == 'en') {
 //				return 'Language';
 //			} else {
-//				return 'Language' . str_replace('-', '_', ucfirst($code));
+//				return 'Language' . str_replace('-', '_', ucfirst(code));
 //			}
 //		}
 //
 //		/**
 //		* Get the name of a file for a certain language code
-//		* @param String $prefix Prepend this to the filename
-//		* @param String $code Language code
-//		* @param String $suffix Append this to the filename
+//		* @param String prefix Prepend this to the filename
+//		* @param String code Language code
+//		* @param String suffix Append this to the filename
 //		* @throws MWException
-//		* @return String $prefix . $mangledCode . $suffix
+//		* @return String prefix . mangledCode . suffix
 //		*/
-//		public static function getFileName($prefix = 'Language', $code, $suffix = '.php') {
-//			if (!self::isValidBuiltInCode($code)) {
-//				throw new MWException("Invalid language code \"$code\"");
+//		public static function getFileName(prefix = 'Language', code, suffix = '.php') {
+//			if (!XomwLanguage.isValidBuiltInCode(code)) {
+//				throw new MWException("Invalid language code \"code\"");
 //			}
 //
-//			return $prefix . str_replace('-', '_', ucfirst($code)) . $suffix;
+//			return prefix . str_replace('-', '_', ucfirst(code)) . suffix;
 //		}
 //
 //		/**
-//		* @param String $code
+//		* @param String code
 //		* @return String
 //		*/
-//		public static function getMessagesFileName($code) {
-//			global $IP;
-//			$file = self::getFileName("$IP/languages/messages/Messages", $code, '.php');
-//			Hooks::run('Language::getMessagesFileName', [ $code, &$file ]);
-//			return $file;
+//		public static function getMessagesFileName(code) {
+//			global IP;
+//			file = XomwLanguage.getFileName("IP/languages/messages/Messages", code, '.php');
+//			Hooks::run('XomwLanguage.getMessagesFileName', [ code, &file ]);
+//			return file;
 //		}
 //
 //		/**
-//		* @param String $code
+//		* @param String code
 //		* @return String
 //		* @throws MWException
 //		* @since 1.23
 //		*/
-//		public static function getJsonMessagesFileName($code) {
-//			global $IP;
+//		public static function getJsonMessagesFileName(code) {
+//			global IP;
 //
-//			if (!self::isValidBuiltInCode($code)) {
-//				throw new MWException("Invalid language code \"$code\"");
+//			if (!XomwLanguage.isValidBuiltInCode(code)) {
+//				throw new MWException("Invalid language code \"code\"");
 //			}
 //
-//			return "$IP/languages/i18n/$code.json";
+//			return "IP/languages/i18n/code.json";
 //		}
-//
-//		/**
-//		* Get the first fallback for a given language.
-//		*
-//		* @param String $code
-//		*
-//		* @return boolean|String
-//		*/
-//		public static function getFallbackFor($code) {
-//			$fallbacks = self::getFallbacksFor($code);
-//			if ($fallbacks) {
-//				return $fallbacks[0];
-//			}
-//			return false;
-//		}
-//
-//		/**
-//		* Get the ordered list of fallback languages.
-//		*
-//		* @since 1.19
-//		* @param String $code Language code
-//		* @return array Non-empty array, ending in "en"
-//		*/
-//		public static function getFallbacksFor($code) {
-//			if ($code == 'en' || !Language::isValidBuiltInCode($code)) {
-//				return [];
-//			}
-//			// For unknown languages, fallbackSequence returns an empty array,
-//			// hardcode fallback to 'en' in that case.
-//			return self::getLocalisationCache()->getItem($code, 'fallbackSequence') ?: [ 'en' ];
-//		}
-//
+
+	/**
+	* Get the first fallback for a given language.
+	*
+	* @param String code
+	*
+	* @return boolean|String
+	*/
+	public static String getFallbackFor(String code) {
+		XophpArray fallbacks = XomwLanguage.getFallbacksFor(code);
+		if (XophpObject.is_true(fallbacks)) {
+			return fallbacks.Get_at_str(0);
+		}
+		return null;
+	}
+
+	/**
+	* Get the ordered list of fallback languages.
+	*
+	* @since 1.19
+	* @param String code Language code
+	* @return array Non-empty array, ending in "en"
+	*/
+	public static XophpArray getFallbacksFor(String code) {
+		if (code == "en" || !XomwLanguage.isValidBuiltInCode(code)) {
+			return XophpArray.New();
+		}
+		if (XomwLanguage.getLocalisationCache() == null) {
+			return null;
+		}
+		// For unknown languages, fallbackSequence returns an empty array,
+		// hardcode fallback to 'en' in that case.
+		Object rv = XomwLanguage.getLocalisationCache().getItem(code, "fallbackSequence");
+		return rv == null ? XophpArray.New().Add("en") : (XophpArray)rv;
+	}
+
 //		/**
 //		* Get the ordered list of fallback languages, ending with the fallback
 //		* language chain for the site language.
 //		*
 //		* @since 1.22
-//		* @param String $code Language code
+//		* @param String code Language code
 //		* @return array Array(fallbacks, site fallbacks)
 //		*/
-//		public static function getFallbacksIncludingSiteLanguage($code) {
-//			global $wgLanguageCode;
+//		public static function getFallbacksIncludingSiteLanguage(code) {
+//			global wgLanguageCode;
 //
 //			// Usually, we will only store a tiny number of fallback chains, so we
 //			// keep them in static memory.
-//			$cacheKey = "{$code}-{$wgLanguageCode}";
+//			cacheKey = "{code}-{wgLanguageCode}";
 //
-//			if (!array_key_exists($cacheKey, self::$fallbackLanguageCache)) {
-//				$fallbacks = self::getFallbacksFor($code);
+//			if (!array_key_exists(cacheKey, XomwLanguage.fallbackLanguageCache)) {
+//				fallbacks = XomwLanguage.getFallbacksFor(code);
 //
 //				// Append the site's fallback chain, including the site language itself
-//				$siteFallbacks = self::getFallbacksFor($wgLanguageCode);
-//				array_unshift($siteFallbacks, $wgLanguageCode);
+//				siteFallbacks = XomwLanguage.getFallbacksFor(wgLanguageCode);
+//				array_unshift(siteFallbacks, wgLanguageCode);
 //
 //				// Eliminate any languages already included in the chain
-//				$siteFallbacks = array_diff($siteFallbacks, $fallbacks);
+//				siteFallbacks = array_diff(siteFallbacks, fallbacks);
 //
-//				self::$fallbackLanguageCache[$cacheKey] = [ $fallbacks, $siteFallbacks ];
+//				XomwLanguage.fallbackLanguageCache[cacheKey] = [ fallbacks, siteFallbacks ];
 //			}
-//			return self::$fallbackLanguageCache[$cacheKey];
+//			return XomwLanguage.fallbackLanguageCache[cacheKey];
 //		}
 //
 //		/**
@@ -4603,194 +4623,194 @@ public class XomwLanguage {
 //		* WARNING: this may take a long time. If you just need all message *keys*
 //		* but need the *contents* of only a few messages, consider using getMessageKeysFor().
 //		*
-//		* @param String $code
+//		* @param String code
 //		*
 //		* @return array
 //		*/
-//		public static function getMessagesFor($code) {
-//			return self::getLocalisationCache()->getItem($code, 'messages');
+//		public static function getMessagesFor(code) {
+//			return XomwLanguage.getLocalisationCache().getItem(code, 'messages');
 //		}
 //
 //		/**
 //		* Get a message for a given language
 //		*
-//		* @param String $key
-//		* @param String $code
+//		* @param String key
+//		* @param String code
 //		*
 //		* @return String
 //		*/
-//		public static function getMessageFor($key, $code) {
-//			return self::getLocalisationCache()->getSubitem($code, 'messages', $key);
+//		public static function getMessageFor(key, code) {
+//			return XomwLanguage.getLocalisationCache().getSubitem(code, 'messages', key);
 //		}
 //
 //		/**
 //		* Get all message keys for a given language. This is a faster alternative to
-//		* array_keys(Language::getMessagesFor($code))
+//		* array_keys(XomwLanguage.getMessagesFor(code))
 //		*
 //		* @since 1.19
-//		* @param String $code Language code
+//		* @param String code Language code
 //		* @return array Array of message keys (strings)
 //		*/
-//		public static function getMessageKeysFor($code) {
-//			return self::getLocalisationCache()->getSubitemList($code, 'messages');
+//		public static function getMessageKeysFor(code) {
+//			return XomwLanguage.getLocalisationCache().getSubitemList(code, 'messages');
 //		}
 //
 //		/**
-//		* @param String $talk
+//		* @param String talk
 //		* @return mixed
 //		*/
-//		function fixVariableInNamespace($talk) {
-//			if (strpos($talk, '$1') == false) {
-//				return $talk;
+//		function fixVariableInNamespace(talk) {
+//			if (strpos(talk, '1') == false) {
+//				return talk;
 //			}
 //
-//			global $wgMetaNamespace;
-//			$talk = str_replace('$1', $wgMetaNamespace, $talk);
+//			global wgMetaNamespace;
+//			talk = str_replace('1', wgMetaNamespace, talk);
 //
 //			# Allow grammar transformations
 //			# Allowing full message-style parsing would make simple requests
 //			# such as action=raw much more expensive than they need to be.
 //			# This will hopefully cover most cases.
-//			$talk = preg_replace_callback('/{{grammar:(.*?)\|(.*?)}}/i',
-//				[ &$this, 'replaceGrammarInNamespace' ], $talk);
-//			return str_replace(' ', '_', $talk);
+//			talk = preg_replace_callback('/{{grammar:(.*?)\|(.*?)}}/i',
+//				[ &this, 'replaceGrammarInNamespace' ], talk);
+//			return str_replace(' ', '_', talk);
 //		}
 //
 //		/**
-//		* @param String $m
+//		* @param String m
 //		* @return String
 //		*/
-//		function replaceGrammarInNamespace($m) {
-//			return this.convertGrammar(trim($m[2]), trim($m[1]));
+//		function replaceGrammarInNamespace(m) {
+//			return this.convertGrammar(trim(m[2]), trim(m[1]));
 //		}
 //
 //		/**
 //		* Decode an expiry (block, protection, etc) which has come from the DB
 //		*
-//		* @param String $expiry Database expiry String
-//		* @param boolean|int $format True to process using language functions, or TS_ constant
+//		* @param String expiry Database expiry String
+//		* @param boolean|int format True to process using language functions, or TS_ constant
 //		*     to return the expiry in a given timestamp
-//		* @param String $infinity If $format is not true, use this String for infinite expiry
+//		* @param String infinity If format is not true, use this String for infinite expiry
 //		* @return String
 //		* @since 1.18
 //		*/
-//		public function formatExpiry($expiry, $format = true, $infinity = 'infinity') {
-//			static $dbInfinity;
-//			if ($dbInfinity == null) {
-//				$dbInfinity = wfGetDB(DB_SLAVE)->getInfinity();
+//		public function formatExpiry(expiry, format = true, infinity = 'infinity') {
+//			static dbInfinity;
+//			if (dbInfinity == null) {
+//				dbInfinity = wfGetDB(DB_SLAVE).getInfinity();
 //			}
 //
-//			if ($expiry == '' || $expiry == 'infinity' || $expiry == $dbInfinity) {
-//				return $format == true
+//			if (expiry == '' || expiry == 'infinity' || expiry == dbInfinity) {
+//				return format == true
 //					? this.getMessageFromDB('infiniteblock')
-//					: $infinity;
+//					: infinity;
 //			} else {
-//				return $format == true
-//					? this.timeanddate($expiry, /* User preference timezone */ true)
-//					: wfTimestamp($format, $expiry);
+//				return format == true
+//					? this.timeanddate(expiry, /* User preference timezone */ true)
+//					: wfTimestamp(format, expiry);
 //			}
 //		}
 //
 //		/**
 //		* Formats a time given in seconds into a String representation of that time.
 //		*
-//		* @param int|float $seconds
-//		* @param array $format An optional argument that formats the returned String in different ways:
-//		*   If $format['avoid'] == 'avoidseconds': don't show seconds if $seconds >= 1 hour,
-//		*   If $format['avoid'] == 'avoidminutes': don't show seconds/minutes if $seconds > 48 hours,
-//		*   If $format['noabbrevs'] is true: use 'seconds' and friends instead of 'seconds-abbrev'
+//		* @param int|float seconds
+//		* @param array format An optional argument that formats the returned String in different ways:
+//		*   If format['avoid'] == 'avoidseconds': don't show seconds if seconds >= 1 hour,
+//		*   If format['avoid'] == 'avoidminutes': don't show seconds/minutes if seconds > 48 hours,
+//		*   If format['noabbrevs'] is true: use 'seconds' and friends instead of 'seconds-abbrev'
 //		*     and friends.
-//		* @note For backwards compatibility, $format may also be one of the strings 'avoidseconds'
+//		* @note For backwards compatibility, format may also be one of the strings 'avoidseconds'
 //		*     or 'avoidminutes'.
 //		* @return String
 //		*/
-//		function formatTimePeriod($seconds, $format = []) {
-//			if (!is_array($format)) {
-//				$format = [ 'avoid' => $format ]; // For backwards compatibility
+//		function formatTimePeriod(seconds, format = []) {
+//			if (!is_array(format)) {
+//				format = [ 'avoid' => format ]; // For backwards compatibility
 //			}
-//			if (!isset($format['avoid'])) {
-//				$format['avoid'] = false;
+//			if (!isset(format['avoid'])) {
+//				format['avoid'] = false;
 //			}
-//			if (!isset($format['noabbrevs'])) {
-//				$format['noabbrevs'] = false;
+//			if (!isset(format['noabbrevs'])) {
+//				format['noabbrevs'] = false;
 //			}
-//			$secondsMsg = wfMessage(
-//				$format['noabbrevs'] ? 'seconds' : 'seconds-abbrev')->inLanguage($this);
-//			$minutesMsg = wfMessage(
-//				$format['noabbrevs'] ? 'minutes' : 'minutes-abbrev')->inLanguage($this);
-//			$hoursMsg = wfMessage(
-//				$format['noabbrevs'] ? 'hours' : 'hours-abbrev')->inLanguage($this);
-//			$daysMsg = wfMessage(
-//				$format['noabbrevs'] ? 'days' : 'days-abbrev')->inLanguage($this);
+//			secondsMsg = wfMessage(
+//				format['noabbrevs'] ? 'seconds' : 'seconds-abbrev').inLanguage(this);
+//			minutesMsg = wfMessage(
+//				format['noabbrevs'] ? 'minutes' : 'minutes-abbrev').inLanguage(this);
+//			hoursMsg = wfMessage(
+//				format['noabbrevs'] ? 'hours' : 'hours-abbrev').inLanguage(this);
+//			daysMsg = wfMessage(
+//				format['noabbrevs'] ? 'days' : 'days-abbrev').inLanguage(this);
 //
-//			if (round($seconds * 10) < 100) {
-//				$s = this.formatNum(sprintf("%.1f", round($seconds * 10) / 10));
-//				$s = $secondsMsg->params($s)->text();
-//			} elseif (round($seconds) < 60) {
-//				$s = this.formatNum(round($seconds));
-//				$s = $secondsMsg->params($s)->text();
-//			} elseif (round($seconds) < 3600) {
-//				$minutes = floor($seconds / 60);
-//				$secondsPart = round(fmod($seconds, 60));
-//				if ($secondsPart == 60) {
-//					$secondsPart = 0;
-//					$minutes++;
+//			if (round(seconds * 10) < 100) {
+//				s = this.formatNum(sprintf("%.1f", round(seconds * 10) / 10));
+//				s = secondsMsg.params(s).text();
+//			} elseif (round(seconds) < 60) {
+//				s = this.formatNum(round(seconds));
+//				s = secondsMsg.params(s).text();
+//			} elseif (round(seconds) < 3600) {
+//				minutes = floor(seconds / 60);
+//				secondsPart = round(fmod(seconds, 60));
+//				if (secondsPart == 60) {
+//					secondsPart = 0;
+//					minutes++;
 //				}
-//				$s = $minutesMsg->params(this.formatNum($minutes))->text();
-//				$s .= ' ';
-//				$s .= $secondsMsg->params(this.formatNum($secondsPart))->text();
-//			} elseif (round($seconds) <= 2 * 86400) {
-//				$hours = floor($seconds / 3600);
-//				$minutes = floor(($seconds - $hours * 3600) / 60);
-//				$secondsPart = round($seconds - $hours * 3600 - $minutes * 60);
-//				if ($secondsPart == 60) {
-//					$secondsPart = 0;
-//					$minutes++;
+//				s = minutesMsg.params(this.formatNum(minutes)).text();
+//				s .= ' ';
+//				s .= secondsMsg.params(this.formatNum(secondsPart)).text();
+//			} elseif (round(seconds) <= 2 * 86400) {
+//				hours = floor(seconds / 3600);
+//				minutes = floor((seconds - hours * 3600) / 60);
+//				secondsPart = round(seconds - hours * 3600 - minutes * 60);
+//				if (secondsPart == 60) {
+//					secondsPart = 0;
+//					minutes++;
 //				}
-//				if ($minutes == 60) {
-//					$minutes = 0;
-//					$hours++;
+//				if (minutes == 60) {
+//					minutes = 0;
+//					hours++;
 //				}
-//				$s = $hoursMsg->params(this.formatNum($hours))->text();
-//				$s .= ' ';
-//				$s .= $minutesMsg->params(this.formatNum($minutes))->text();
-//				if (!in_array($format['avoid'], [ 'avoidseconds', 'avoidminutes' ])) {
-//					$s .= ' ' . $secondsMsg->params(this.formatNum($secondsPart))->text();
+//				s = hoursMsg.params(this.formatNum(hours)).text();
+//				s .= ' ';
+//				s .= minutesMsg.params(this.formatNum(minutes)).text();
+//				if (!in_array(format['avoid'], [ 'avoidseconds', 'avoidminutes' ])) {
+//					s .= ' ' . secondsMsg.params(this.formatNum(secondsPart)).text();
 //				}
 //			} else {
-//				$days = floor($seconds / 86400);
-//				if ($format['avoid'] == 'avoidminutes') {
-//					$hours = round(($seconds - $days * 86400) / 3600);
-//					if ($hours == 24) {
-//						$hours = 0;
-//						$days++;
+//				days = floor(seconds / 86400);
+//				if (format['avoid'] == 'avoidminutes') {
+//					hours = round((seconds - days * 86400) / 3600);
+//					if (hours == 24) {
+//						hours = 0;
+//						days++;
 //					}
-//					$s = $daysMsg->params(this.formatNum($days))->text();
-//					$s .= ' ';
-//					$s .= $hoursMsg->params(this.formatNum($hours))->text();
-//				} elseif ($format['avoid'] == 'avoidseconds') {
-//					$hours = floor(($seconds - $days * 86400) / 3600);
-//					$minutes = round(($seconds - $days * 86400 - $hours * 3600) / 60);
-//					if ($minutes == 60) {
-//						$minutes = 0;
-//						$hours++;
+//					s = daysMsg.params(this.formatNum(days)).text();
+//					s .= ' ';
+//					s .= hoursMsg.params(this.formatNum(hours)).text();
+//				} elseif (format['avoid'] == 'avoidseconds') {
+//					hours = floor((seconds - days * 86400) / 3600);
+//					minutes = round((seconds - days * 86400 - hours * 3600) / 60);
+//					if (minutes == 60) {
+//						minutes = 0;
+//						hours++;
 //					}
-//					if ($hours == 24) {
-//						$hours = 0;
-//						$days++;
+//					if (hours == 24) {
+//						hours = 0;
+//						days++;
 //					}
-//					$s = $daysMsg->params(this.formatNum($days))->text();
-//					$s .= ' ';
-//					$s .= $hoursMsg->params(this.formatNum($hours))->text();
-//					$s .= ' ';
-//					$s .= $minutesMsg->params(this.formatNum($minutes))->text();
+//					s = daysMsg.params(this.formatNum(days)).text();
+//					s .= ' ';
+//					s .= hoursMsg.params(this.formatNum(hours)).text();
+//					s .= ' ';
+//					s .= minutesMsg.params(this.formatNum(minutes)).text();
 //				} else {
-//					$s = $daysMsg->params(this.formatNum($days))->text();
-//					$s .= ' ';
-//					$s .= this.formatTimePeriod($seconds - $days * 86400, $format);
+//					s = daysMsg.params(this.formatNum(days)).text();
+//					s .= ' ';
+//					s .= this.formatTimePeriod(seconds - days * 86400, format);
 //				}
 //			}
-//			return $s;
+//			return s;
 //		}
 //
 //		/**
@@ -4801,45 +4821,45 @@ public class XomwLanguage {
 //		* This use super 1000. For super 1024 use formatSize(), for another super
 //		* see formatComputingNumbers().
 //		*
-//		* @param int $bps
+//		* @param int bps
 //		* @return String
 //		*/
-//		function formatBitrate($bps) {
-//			return this.formatComputingNumbers($bps, 1000, "bitrate-$1bits");
+//		function formatBitrate(bps) {
+//			return this.formatComputingNumbers(bps, 1000, "bitrate-1bits");
 //		}
 //
 //		/**
-//		* @param int $size Size of the unit
-//		* @param int $boundary Size boundary (1000, or 1024 in most cases)
-//		* @param String $messageKey Message key to be uesd
+//		* @param int size Size of the unit
+//		* @param int boundary Size boundary (1000, or 1024 in most cases)
+//		* @param String messageKey Message key to be uesd
 //		* @return String
 //		*/
-//		function formatComputingNumbers($size, $boundary, $messageKey) {
-//			if ($size <= 0) {
-//				return str_replace('$1', this.formatNum($size),
-//					this.getMessageFromDB(str_replace('$1', '', $messageKey))
+//		function formatComputingNumbers(size, boundary, messageKey) {
+//			if (size <= 0) {
+//				return str_replace('1', this.formatNum(size),
+//					this.getMessageFromDB(str_replace('1', '', messageKey))
 //				);
 //			}
-//			$sizes = [ '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zeta', 'yotta' ];
-//			$index = 0;
+//			sizes = [ '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zeta', 'yotta' ];
+//			index = 0;
 //
-//			$maxIndex = count($sizes) - 1;
-//			while ($size >= $boundary && $index < $maxIndex) {
-//				$index++;
-//				$size /= $boundary;
+//			maxIndex = count(sizes) - 1;
+//			while (size >= boundary && index < maxIndex) {
+//				index++;
+//				size /= boundary;
 //			}
 //
 //			// For small sizes no decimal places necessary
-//			$round = 0;
-//			if ($index > 1) {
+//			round = 0;
+//			if (index > 1) {
 //				// For MB and bigger two decimal places are smarter
-//				$round = 2;
+//				round = 2;
 //			}
-//			$msg = str_replace('$1', $sizes[$index], $messageKey);
+//			msg = str_replace('1', sizes[index], messageKey);
 //
-//			$size = round($size, $round);
-//			$text = this.getMessageFromDB($msg);
-//			return str_replace('$1', this.formatNum($size), $text);
+//			size = round(size, round);
+//			text = this.getMessageFromDB(msg);
+//			return str_replace('1', this.formatNum(size), text);
 //		}
 //
 //		/**
@@ -4849,100 +4869,100 @@ public class XomwLanguage {
 //		* This method use super 1024. For super 1000 use formatBitrate(), for
 //		* another super see formatComputingNumbers()
 //		*
-//		* @param int $size Size to format
+//		* @param int size Size to format
 //		* @return String Plain text (not HTML)
 //		*/
-//		function formatSize($size) {
-//			return this.formatComputingNumbers($size, 1024, "size-$1bytes");
+//		function formatSize(size) {
+//			return this.formatComputingNumbers(size, 1024, "size-1bytes");
 //		}
 //
 //		/**
 //		* Make a list item, used by various special pages
 //		*
-//		* @param String $page Page link
-//		* @param String $details HTML safe text between brackets
-//		* @param boolean $oppositedm Add the direction mark opposite to your
+//		* @param String page Page link
+//		* @param String details HTML safe text between brackets
+//		* @param boolean oppositedm Add the direction mark opposite to your
 //		*   language, to display text properly
 //		* @return HTML escaped String
 //		*/
-//		function specialList($page, $details, $oppositedm = true) {
-//			if (!$details) {
-//				return $page;
+//		function specialList(page, details, oppositedm = true) {
+//			if (!details) {
+//				return page;
 //			}
 //
-//			$dirmark = ($oppositedm ? this.getDirMark(true) : '') . this.getDirMark();
+//			dirmark = (oppositedm ? this.getDirMark(true) : '') . this.getDirMark();
 //			return
-//				$page .
-//				$dirmark .
-//				this.msg('word-separator')->escaped() .
-//				this.msg('parentheses')->rawParams($details)->escaped();
+//				page .
+//				dirmark .
+//				this.msg('word-separator').escaped() .
+//				this.msg('parentheses').rawParams(details).escaped();
 //		}
 //
 //		/**
 //		* Generate (prev x| next x) (20|50|100...) type links for paging
 //		*
-//		* @param Title $title Title Object to link
-//		* @param int $offset
-//		* @param int $limit
-//		* @param array $query Optional URL query parameter String
-//		* @param boolean $atend Optional param for specified if this is the last page
+//		* @param Title title Title Object to link
+//		* @param int offset
+//		* @param int limit
+//		* @param array query Optional URL query parameter String
+//		* @param boolean atend Optional param for specified if this is the last page
 //		* @return String
 //		*/
-//		public function viewPrevNext(Title $title, $offset, $limit,
-//			array $query = [], $atend = false
+//		public function viewPrevNext(Title title, offset, limit,
+//			array query = [], atend = false
 //		) {
 //			// @todo FIXME: Why on earth this needs one message for the text and another one for tooltip?
 //
 //			# Make 'previous' link
-//			$prev = wfMessage('prevn')->inLanguage($this)->title($title)->numParams($limit)->text();
-//			if ($offset > 0) {
-//				$plink = this.numLink($title, max($offset - $limit, 0), $limit,
-//					$query, $prev, 'prevn-title', 'mw-prevlink');
+//			prev = wfMessage('prevn').inLanguage(this).title(title).numParams(limit).text();
+//			if (offset > 0) {
+//				plink = this.numLink(title, max(offset - limit, 0), limit,
+//					query, prev, 'prevn-title', 'mw-prevlink');
 //			} else {
-//				$plink = htmlspecialchars($prev);
+//				plink = htmlspecialchars(prev);
 //			}
 //
 //			# Make 'next' link
-//			$next = wfMessage('nextn')->inLanguage($this)->title($title)->numParams($limit)->text();
-//			if ($atend) {
-//				$nlink = htmlspecialchars($next);
+//			next = wfMessage('nextn').inLanguage(this).title(title).numParams(limit).text();
+//			if (atend) {
+//				nlink = htmlspecialchars(next);
 //			} else {
-//				$nlink = this.numLink($title, $offset + $limit, $limit,
-//					$query, $next, 'nextn-title', 'mw-nextlink');
+//				nlink = this.numLink(title, offset + limit, limit,
+//					query, next, 'nextn-title', 'mw-nextlink');
 //			}
 //
 //			# Make links to set number of items per page
-//			$numLinks = [];
-//			foreach ([ 20, 50, 100, 250, 500 ] as $num) {
-//				$numLinks[] = this.numLink($title, $offset, $num,
-//					$query, this.formatNum($num), 'shown-title', 'mw-numlink');
+//			numLinks = [];
+//			foreach ([ 20, 50, 100, 250, 500 ] as num) {
+//				numLinks[] = this.numLink(title, offset, num,
+//					query, this.formatNum(num), 'shown-title', 'mw-numlink');
 //			}
 //
-//			return wfMessage('viewprevnext')->inLanguage($this)->title($title
-//				)->rawParams($plink, $nlink, this.pipeList($numLinks))->escaped();
+//			return wfMessage('viewprevnext').inLanguage(this).title(title
+//				).rawParams(plink, nlink, this.pipeList(numLinks)).escaped();
 //		}
 //
 //		/**
 //		* Helper function for viewPrevNext() that generates links
 //		*
-//		* @param Title $title Title Object to link
-//		* @param int $offset
-//		* @param int $limit
-//		* @param array $query Extra query parameters
-//		* @param String $link Text to use for the link; will be escaped
-//		* @param String $tooltipMsg Name of the message to use as tooltip
-//		* @param String $class Value of the "class" attribute of the link
+//		* @param Title title Title Object to link
+//		* @param int offset
+//		* @param int limit
+//		* @param array query Extra query parameters
+//		* @param String link Text to use for the link; will be escaped
+//		* @param String tooltipMsg Name of the message to use as tooltip
+//		* @param String class Value of the "class" attribute of the link
 //		* @return String HTML fragment
 //		*/
-//		private function numLink(Title $title, $offset, $limit, array $query, $link,
-//			$tooltipMsg, $class
+//		private function numLink(Title title, offset, limit, array query, link,
+//			tooltipMsg, class
 //		) {
-//			$query = [ 'limit' => $limit, 'offset' => $offset ] + $query;
-//			$tooltip = wfMessage($tooltipMsg)->inLanguage($this)->title($title)
-//				->numParams($limit)->text();
+//			query = [ 'limit' => limit, 'offset' => offset ] + query;
+//			tooltip = wfMessage(tooltipMsg).inLanguage(this).title(title)
+//				.numParams(limit).text();
 //
-//			return Html::element('a', [ 'href' => $title->getLocalURL($query),
-//				'title' => $tooltip, 'class' => $class ], $link);
+//			return Html::element('a', [ 'href' => title.getLocalURL(query),
+//				'title' => tooltip, 'class' => class ], link);
 //		}
 //
 //		/**
@@ -4951,45 +4971,51 @@ public class XomwLanguage {
 //		* @return String
 //		*/
 //		public function getConvRuleTitle() {
-//			return this.mConverter->getConvRuleTitle();
+//			return this.mConverter.getConvRuleTitle();
 //		}
-//
-//		/**
-//		* Get the compiled plural rules for the language
-//		* @since 1.20
-//		* @return array Associative array with plural form, and plural rule as key-value pairs
-//		*/
-//		public function getCompiledPluralRules() {
-//			$pluralRules = self::$dataCache->getItem(strtolower(this.mCode), 'compiledPluralRules');
-//			$fallbacks = Language::getFallbacksFor(this.mCode);
-//			if (!$pluralRules) {
-//				foreach ($fallbacks as $fallbackCode) {
-//					$pluralRules = self::$dataCache->getItem(strtolower($fallbackCode), 'compiledPluralRules');
-//					if ($pluralRules) {
-//						break;
-//					}
-//				}
-//			}
-//			return $pluralRules;
-//		}
-//
+
+	public static XomwLocalisationCacheForXowa dataCacheXowa = new XomwLocalisationCacheForXowa();
+	private final    static XophpArray getCompiledPluralRulesEmpty = XophpArray.New();
+	// MW.SRC:1.33
+	/**
+	* Get the compiled plural rules for the language
+	* @since 1.20
+	* @return array Associative array with plural form, and plural rule as key-value pairs
+	*/
+	public XophpArray getCompiledPluralRules() {
+		XophpArray pluralRules = (XophpArray)XomwLanguage.dataCacheXowa.getItem_ary(XophpString_.strtolower(this.mCode), "compiledPluralRules");
+		if (pluralRules == null) return getCompiledPluralRulesEmpty;
+		XophpArray fallbacks = XomwLanguage.getFallbacksFor(this.mCode);
+		if (!XophpObject.is_true(pluralRules)) {
+			int fallbacks_len = fallbacks.Len();
+			for (int i = 0; i < fallbacks_len; i++) {
+				String fallbackCode = fallbacks.Get_at_str(i);
+				pluralRules = XomwLanguage.dataCacheXowa.getItem_ary(XophpString_.strtolower(fallbackCode), "compiledPluralRules");
+				if (XophpObject.is_true(pluralRules)) {
+					break;
+				}
+			}
+		}
+		return pluralRules;
+	}
+
 //		/**
 //		* Get the plural rules for the language
 //		* @since 1.20
 //		* @return array Associative array with plural form number and plural rule as key-value pairs
 //		*/
 //		public function getPluralRules() {
-//			$pluralRules = self::$dataCache->getItem(strtolower(this.mCode), 'pluralRules');
-//			$fallbacks = Language::getFallbacksFor(this.mCode);
-//			if (!$pluralRules) {
-//				foreach ($fallbacks as $fallbackCode) {
-//					$pluralRules = self::$dataCache->getItem(strtolower($fallbackCode), 'pluralRules');
-//					if ($pluralRules) {
+//			pluralRules = XomwLanguage.dataCache.getItem(strtolower(this.mCode), "pluralRules");
+//			fallbacks = XomwLanguage.getFallbacksFor(this.mCode);
+//			if (!pluralRules) {
+//				foreach (fallbacks as fallbackCode) {
+//					pluralRules = XomwLanguage.dataCache.getItem(strtolower(fallbackCode), "pluralRules");
+//					if (pluralRules) {
 //						break;
 //					}
 //				}
 //			}
-//			return $pluralRules;
+//			return pluralRules;
 //		}
 //
 //		/**
@@ -4998,45 +5024,41 @@ public class XomwLanguage {
 //		* @return array Associative array with plural form number and plural rule type as key-value pairs
 //		*/
 //		public function getPluralRuleTypes() {
-//			$pluralRuleTypes = self::$dataCache->getItem(strtolower(this.mCode), 'pluralRuleTypes');
-//			$fallbacks = Language::getFallbacksFor(this.mCode);
-//			if (!$pluralRuleTypes) {
-//				foreach ($fallbacks as $fallbackCode) {
-//					$pluralRuleTypes = self::$dataCache->getItem(strtolower($fallbackCode), 'pluralRuleTypes');
-//					if ($pluralRuleTypes) {
+//			pluralRuleTypes = XomwLanguage.dataCache.getItem(strtolower(this.mCode), "pluralRuleTypes");
+//			fallbacks = XomwLanguage.getFallbacksFor(this.mCode);
+//			if (!pluralRuleTypes) {
+//				foreach (fallbacks as fallbackCode) {
+//					pluralRuleTypes = XomwLanguage.dataCache.getItem(strtolower(fallbackCode), "pluralRuleTypes");
+//					if (pluralRuleTypes) {
 //						break;
 //					}
 //				}
 //			}
-//			return $pluralRuleTypes;
+//			return pluralRuleTypes;
 //		}
 //
 //		/**
 //		* Find the index number of the plural rule appropriate for the given number
-//		* @param int $number
+//		* @param int number
 //		* @return int The index number of the plural rule
 //		*/
-//		public function getPluralRuleIndexNumber($number) {
-//			$pluralRules = this.getCompiledPluralRules();
-//			$form = Evaluator::evaluateCompiled($number, $pluralRules);
-//			return $form;
-//		}
-//
+	public int getPluralRuleIndexNumber(String number) {
+		XophpArray pluralRules = this.getCompiledPluralRules();
+		int form = XomwEvaluator.evaluateCompiled(number, pluralRules);
+		return form;
+	}
+
 //		/**
 //		* Find the plural rule type appropriate for the given number
 //		* For example, if the language is set to Arabic, getPluralType(5) should
-//		* return 'few'.
+//		* return "few".
 //		* @since 1.22
-//		* @param int $number
+//		* @param int number
 //		* @return String The name of the plural rule type, e.g. one, two, few, many
 //		*/
-//		public function getPluralRuleType($number) {
-//			$index = this.getPluralRuleIndexNumber($number);
-//			$pluralRuleTypes = this.getPluralRuleTypes();
-//			if (isset($pluralRuleTypes[$index])) {
-//				return $pluralRuleTypes[$index];
-//			} else {
-//				return 'other';
-//			}
+//		public function getPluralRuleType(number) {
+//			index = this.getPluralRuleIndexNumber(number);
+//			pluralRuleTypes = this.getPluralRuleTypes();
+//			return pluralRuleTypes[index] ?? "other";
 //		}
 }
diff --git a/400_xowa/src/gplx/xowa/mediawiki/languages/XomwLanguage_fxt.java b/400_xowa/src/gplx/xowa/mediawiki/languages/XomwLanguage_fxt.java
new file mode 100644
index 000000000..452771c02
--- /dev/null
+++ b/400_xowa/src/gplx/xowa/mediawiki/languages/XomwLanguage_fxt.java
@@ -0,0 +1,70 @@
+/*
+XOWA: the XOWA Offline Wiki Application
+Copyright (C) 2012-2017 gnosygnu@gmail.com
+
+XOWA is licensed under the terms of the General Public License (GPL) Version 3,
+or alternatively under the terms of the Apache License Version 2.0.
+
+You may use XOWA according to either of these licenses as is most appropriate
+for your project on a case-by-case basis.
+
+The terms of each license can be found in the source code repository:
+
+GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt
+Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
+*/
+package gplx.xowa.mediawiki.languages; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*;
+import gplx.core.tests.*;
+import gplx.xowa.langs.*;
+import gplx.xowa.mediawiki.includes.cache.localisation.*;
+public class XomwLanguage_fxt {
+	private XomwLanguage lang;
+	private final    Xoae_app app;
+	public XomwLanguage_fxt() {
+		this.app = Xoa_app_fxt.Make__app__edit();
+		this.Init__lang("en");
+	}
+	public void Init_digitGroupingPattern(String digitGroupingPattern) {
+		lang.setDigitGroupingPattern(Bry_.new_u8(digitGroupingPattern));
+	}
+	public void Init__lang(String lang_code) {
+		Xol_lang_itm xoLang = app.Lang_mgr().Get_by_or_load(Bry_.new_a7(lang_code));
+		this.lang = new XomwLanguage(xoLang);
+	}
+	public void Test_commafy(String raw, String expd) {
+		Gftest.Eq__str(expd, lang.commafy(Bry_.new_u8(raw)));
+	}
+	public void Test__handleExplicitPluralForms__string(String count, XophpArray forms, String expd) {
+		Gftest.Eq__str(expd, (String)lang.handleExplicitPluralForms(count, forms));
+	}
+	public void Test__handleExplicitPluralForms__array(String count, XophpArray forms, XophpArray expd) {
+		Gftest.Eq__ary(expd.To_ary(), ((XophpArray)lang.handleExplicitPluralForms(count, forms)).To_ary());
+	}
+	public void Init__pluralRulesXml(String... ary) {
+		String xml = String_.Replace(String_.Concat_lines_nl
+		( ""
+		, "    "
+		, "    "
+		, "    "
+		, String_.Concat_lines_nl(ary)
+		, "    "
+		, ""
+		), "'", "\"");
+		XomwLocalisationCacheForXowa.Init_ip(app.Fsys_mgr().Root_dir());
+		Io_mgr.Instance.SaveFilStr(app.Fsys_mgr().Root_dir().GenSubFil_nest("languages", "data", "plurals.xml"), xml);
+	}
+	public void Init__pluralRulesXml__en() {
+		this.Init__pluralRulesXml
+		( ""
+		, "    i = 1 and v = 0 @integer 1"
+		, "     @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, � @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, �"
+		, ""
+		);
+	}
+
+	public void Test__getPluralRuleIndexNumber(int expd, String... ary) {
+		for (String itm : ary) {
+			Gftest.Eq__int(expd, lang.getPluralRuleIndexNumber(itm), itm);
+		}
+	}
+}
diff --git a/400_xowa/src/gplx/xowa/mediawiki/languages/XomwLanguage_tst.java b/400_xowa/src/gplx/xowa/mediawiki/languages/XomwLanguage_tst.java
index 874c683a1..beda9ac0b 100644
--- a/400_xowa/src/gplx/xowa/mediawiki/languages/XomwLanguage_tst.java
+++ b/400_xowa/src/gplx/xowa/mediawiki/languages/XomwLanguage_tst.java
@@ -16,6 +16,7 @@ Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
 package gplx.xowa.mediawiki.languages; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*;
 import org.junit.*; import gplx.core.tests.*;
 import gplx.xowa.langs.*;
+import gplx.xowa.mediawiki.includes.cache.localisation.*;
 public class XomwLanguage_tst {
 	private final    XomwLanguage_fxt fxt = new XomwLanguage_fxt();
 	@Test  public void Commafy_standard() {
@@ -112,18 +113,34 @@ public class XomwLanguage_tst {
 		fxt.Test_commafy("-123456789"     , "-12,34,56,789");
 		fxt.Test_commafy("-1234567890"    , "-1,23,45,67,890");
 	}
-}
-class XomwLanguage_fxt {
-	private final    XomwLanguage lang;
-	public XomwLanguage_fxt() {
-		Xoae_app app = Xoa_app_fxt.Make__app__edit();
-		Xol_lang_itm xoLang = app.Lang_mgr().Get_by_or_load(Bry_.new_a7("en"));
-		this.lang = new XomwLanguage(xoLang);
+	@Test   public void handleExplicitPluralForms() {
+		fxt.Test__handleExplicitPluralForms__string("1", XophpArray.New().Add("1=one"), "one");
+		fxt.Test__handleExplicitPluralForms__array("1", XophpArray.New().Add("no_match"), XophpArray.New().Add(0, "no_match"));
 	}
-	public void Init_digitGroupingPattern(String digitGroupingPattern) {
-		lang.setDigitGroupingPattern(Bry_.new_u8(digitGroupingPattern));
-	}
-	public void Test_commafy(String raw, String expd) {
-		Gftest.Eq__str(expd, lang.commafy(Bry_.new_u8(raw)));
+	@Test   public void getPluralRuleIndexNumber() {
+		fxt.Init__pluralRulesXml
+		( ""
+		, "    i = 1 and v = 0 @integer 1"
+		, "     @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
+		, ""
+		, ""
+		, "    v = 0 and i % 10 = 1 and i % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …"
+		, "    v = 0 and i % 10 = 2..4 and i % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …"
+		, "    v = 0 and i % 10 = 0 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 11..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …"
+		, "       @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
+		, ""
+		);
+		fxt.Init__lang("qqq");
+		fxt.Test__getPluralRuleIndexNumber(0, "0", "1", "2", "1.1");
+
+		fxt.Init__lang("en");
+		fxt.Test__getPluralRuleIndexNumber(0, "1");
+		fxt.Test__getPluralRuleIndexNumber(1, "2", "1.1", "3");
+
+		fxt.Init__lang("ru");
+		fxt.Test__getPluralRuleIndexNumber(0, "1");
+		fxt.Test__getPluralRuleIndexNumber(1, "2");
+		fxt.Test__getPluralRuleIndexNumber(2, "0", "5", "12", "19", "100");
+		fxt.Test__getPluralRuleIndexNumber(3, "0.0", "1.5", "10.0");
 	}
 }
diff --git a/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/Converter/XomwExpression.java b/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/Converter/XomwExpression.java
new file mode 100644
index 000000000..f57631524
--- /dev/null
+++ b/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/Converter/XomwExpression.java
@@ -0,0 +1,45 @@
+/*
+XOWA: the XOWA Offline Wiki Application
+Copyright (C) 2012-2017 gnosygnu@gmail.com
+
+XOWA is licensed under the terms of the General Public License (GPL) Version 3,
+or alternatively under the terms of the Apache License Version 2.0.
+
+You may use XOWA according to either of these licenses as is most appropriate
+for your project on a case-by-case basis.
+
+The terms of each license can be found in the source code repository:
+
+GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt
+Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
+*/
+package gplx.xowa.mediawiki.vendor.wikimedia.cldr_plural_rule_parser.src.Converter; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.vendor.*; import gplx.xowa.mediawiki.vendor.wikimedia.*; import gplx.xowa.mediawiki.vendor.wikimedia.cldr_plural_rule_parser.*; import gplx.xowa.mediawiki.vendor.wikimedia.cldr_plural_rule_parser.src.*;
+// MW.SRC:1.33.1
+/**
+* Helper for Converter.
+* An expression Object, representing a region of the input String (for error
+* messages), the RPN notation used to evaluate it, and the result type for
+* validation.
+*/
+public class XomwExpression extends XomwFragment { 	/** @var String */
+	public String type;
+
+	/** @var String */
+	public String rpn;
+
+	public XomwExpression(XomwConverter parser, String type, String rpn, int pos, int length) {super(parser, pos, length);
+		this.type = type;
+		this.rpn = rpn;
+	}
+
+	public boolean isType(String type) {
+		if (String_.Eq(type, "range") && (String_.Eq(this.type, "range") || String_.Eq(this.type, "number"))) {
+			return true;
+		}
+		if (String_.Eq(type, this.type)) {
+			return true;
+		}
+
+		return false;
+	}
+}
\ No newline at end of file
diff --git a/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/Converter/XomwFragment.java b/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/Converter/XomwFragment.java
new file mode 100644
index 000000000..91d3ebc63
--- /dev/null
+++ b/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/Converter/XomwFragment.java
@@ -0,0 +1,41 @@
+/*
+XOWA: the XOWA Offline Wiki Application
+Copyright (C) 2012-2017 gnosygnu@gmail.com
+
+XOWA is licensed under the terms of the General Public License (GPL) Version 3,
+or alternatively under the terms of the Apache License Version 2.0.
+
+You may use XOWA according to either of these licenses as is most appropriate
+for your project on a case-by-case basis.
+
+The terms of each license can be found in the source code repository:
+
+GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt
+Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
+*/
+package gplx.xowa.mediawiki.vendor.wikimedia.cldr_plural_rule_parser.src.Converter; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.vendor.*; import gplx.xowa.mediawiki.vendor.wikimedia.*; import gplx.xowa.mediawiki.vendor.wikimedia.cldr_plural_rule_parser.*; import gplx.xowa.mediawiki.vendor.wikimedia.cldr_plural_rule_parser.src.*;
+// MW.SRC:1.33.1
+/**
+* Helper for Converter.
+* The super class for operators and expressions, describing a region of the input String.
+*/
+public class XomwFragment {
+	public XomwConverter parser;
+	public int pos, length, end;
+
+	public XomwFragment(XomwConverter parser, int pos, int length) {
+		this.parser = parser;
+		this.pos = pos;
+		this.length = length;
+		this.end = pos + length;
+	}
+
+	public void error(String message) {
+		String text = this.getText();
+		throw XomwError.New__fmt("$message at position " + Int_.To_str(this.pos + 1) + ": \"$text\"", message, text);
+	}
+
+	public String getText() {
+		return XophpString_.substr(this.parser.rule, this.pos, this.length);
+	}
+}
\ No newline at end of file
diff --git a/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/Converter/XomwOperator.java b/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/Converter/XomwOperator.java
new file mode 100644
index 000000000..0f193550a
--- /dev/null
+++ b/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/Converter/XomwOperator.java
@@ -0,0 +1,118 @@
+/*
+XOWA: the XOWA Offline Wiki Application
+Copyright (C) 2012-2017 gnosygnu@gmail.com
+
+XOWA is licensed under the terms of the General Public License (GPL) Version 3,
+or alternatively under the terms of the Apache License Version 2.0.
+
+You may use XOWA according to either of these licenses as is most appropriate
+for your project on a case-by-case basis.
+
+The terms of each license can be found in the source code repository:
+
+GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt
+Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
+*/
+package gplx.xowa.mediawiki.vendor.wikimedia.cldr_plural_rule_parser.src.Converter; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.vendor.*; import gplx.xowa.mediawiki.vendor.wikimedia.*; import gplx.xowa.mediawiki.vendor.wikimedia.cldr_plural_rule_parser.*; import gplx.xowa.mediawiki.vendor.wikimedia.cldr_plural_rule_parser.src.*;
+// MW.SRC:1.33.1
+/**
+* Helper for Converter.
+* An operator Object, representing a region of the input String (for error
+* messages), and the binary operator at that location.
+*/
+public class XomwOperator extends XomwFragment { 	/** @var String The name */
+	public String name;
+
+	/**
+	* Each op type has three characters: left operand type, right operand type and result type
+	*
+	*   b = boolean
+	*   n = number
+	*   r = range
+	*
+	* A number is a kind of range.
+	*
+	* @var array
+	*/
+	private static XophpArray opTypes = XophpArray.New()
+		.Add("or", "bbb")
+		.Add("and", "bbb")
+		.Add("is", "nnb")
+		.Add("is-not", "nnb")
+		.Add("in", "nrb")
+		.Add("not-in", "nrb")
+		.Add("within", "nrb")
+		.Add("not-within", "nrb")
+		.Add("mod", "nnn")
+		.Add(",", "rrr")
+		.Add("..", "nnr")
+	;
+
+	/**
+	* Map converting from the abbrevation to the full form.
+	*
+	* @var array
+	*/
+	private static XophpArray typeSpecMap = XophpArray.New()
+		.Add("b", "boolean")
+		.Add("n", "number")
+		.Add("r", "range")
+	;
+
+	/**
+	* Map for converting the new operators introduced in Rev 33 to the old forms
+	*/
+	private static XophpArray aliasMap = XophpArray.New()
+		.Add("%", "mod")
+		.Add("!=", "not-in")
+		.Add("=", "in")
+	;
+
+	/**
+	* Initialize a new instance of a CLDRPluralRuleConverterOperator Object
+	*
+	* @param Converter parser The parser
+	* @param String name The operator name
+	* @param int pos The length
+	* @param int length
+	*/
+	public XomwOperator(XomwConverter parser, String name, int pos, int length) {super(parser, pos, length);
+		if (XomwOperator.aliasMap.isset(name)) {
+			name = XomwOperator.aliasMap.Get_by_str(name);
+		}
+		this.name = name;
+	}
+
+	/**
+	* Compute the operation
+	*
+	* @param Expression left The left part of the expression
+	* @param Expression right The right part of the expression
+	* @return Expression The result of the operation
+	*/
+	public XomwExpression operate(XomwExpression left, XomwExpression right) {
+		String typeSpec = XomwOperator.opTypes.Get_by_str(this.name);
+
+		String leftType = XomwOperator.typeSpecMap.Get_by_str(String_.CharAt(typeSpec, 0));
+		String rightType = XomwOperator.typeSpecMap.Get_by_str(String_.CharAt(typeSpec, 1));
+		String resultType = XomwOperator.typeSpecMap.Get_by_str(String_.CharAt(typeSpec, 2));
+
+		int start = XophpMath.min_many(this.pos, left.pos, right.pos);
+		int end = XophpMath.max_many(this.end, left.end, right.end);
+		int length = end - start;
+
+		XomwExpression newExpr = new XomwExpression(this.parser, resultType,
+			left.rpn + " " + right.rpn + " " + this.name,
+			start, length);
+
+		if (!left.isType(leftType)) {
+			newExpr.error("invalid type for left operand: expected leftType, got {left.type}");
+		}
+
+		if (!right.isType(rightType)) {
+			newExpr.error("invalid type for right operand: expected rightType, got {right.type}");
+		}
+
+		return newExpr;
+	}
+}
\ No newline at end of file
diff --git a/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/XomwConverter.java b/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/XomwConverter.java
new file mode 100644
index 000000000..36c439ad1
--- /dev/null
+++ b/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/XomwConverter.java
@@ -0,0 +1,335 @@
+/*
+XOWA: the XOWA Offline Wiki Application
+Copyright (C) 2012-2017 gnosygnu@gmail.com
+
+XOWA is licensed under the terms of the General Public License (GPL) Version 3,
+or alternatively under the terms of the Apache License Version 2.0.
+
+You may use XOWA according to either of these licenses as is most appropriate
+for your project on a case-by-case basis.
+
+The terms of each license can be found in the source code repository:
+
+GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt
+Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
+*/
+package gplx.xowa.mediawiki.vendor.wikimedia.cldr_plural_rule_parser.src; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.vendor.*; import gplx.xowa.mediawiki.vendor.wikimedia.*; import gplx.xowa.mediawiki.vendor.wikimedia.cldr_plural_rule_parser.*;
+import gplx.xowa.mediawiki.vendor.wikimedia.cldr_plural_rule_parser.src.Converter.*;
+import gplx.langs.regxs.*;
+// MW.SRC:1.33.1
+/**
+* Helper class for converting rules to reverse polish notation (RPN).
+*/
+public class XomwConverter {
+	/**
+	* The input String
+	*
+	* @var String
+	*/
+	public String rule;
+
+	/**
+	* The current position
+	*
+	* @var int
+	*/
+	public int pos;
+
+	/**
+	* The past-the-end position
+	*
+	* @var int
+	*/
+	public int end;
+
+	/**
+	* The operator stack
+	*
+	* @var array
+	*/
+	public XophpArray operators = XophpArray.New();
+
+	/**
+	* The operand stack
+	*
+	* @var array
+	*/
+	public XophpArray operands = XophpArray.New();
+
+	/**
+	* Precedence levels. Note that there's no need to worry about associativity
+	* for the level 4 operators, since they return boolean and don't accept
+	* boolean inputs.
+	*/
+	private static XophpArray precedence = XophpArray.New()
+		.Add("or", 2)
+		.Add("and", 3)
+		.Add("is", 4)
+		.Add("is-not", 4)
+		.Add("in", 4)
+		.Add("not-in", 4)
+		.Add("within", 4)
+		.Add("not-within", 4)
+		.Add("mod", 5)
+		.Add(",", 6)
+		.Add("..", 7)
+	;
+
+	/**
+	* A character list defining whitespace, for use in strspn() etc.
+	*/
+	private static final    Hash_adp WHITESPACE_CLASS = XophpString_.strspn_hash(" \t\r\n");
+
+	/**
+	* Same for digits. Note that the grammar given in UTS #35 doesn't allow
+	* negative numbers or decimal separators.
+	*/
+	private static final    Hash_adp NUMBER_CLASS = XophpString_.strspn_hash("0123456789");
+
+	/**
+	* A character list of symbolic operands.
+	*/
+	private static final String OPERAND_SYMBOLS = "nivwft";
+
+	/**
+	* An anchored regular expression which matches a word at the current offset.
+	*/
+	private static final    Regx_adp WORD_REGEX = Regx_adp_.new_("[a-zA-Z@]+");
+
+	/**
+	* Convert a rule to RPN. This is the only public entry point.
+	*
+	* @param String rule The rule to convert
+	* @return String The RPN representation of the rule
+	*/
+	public static String convert(String rule) {
+		XomwConverter parser = new XomwConverter(rule);
+
+		return parser.doConvert();
+	}
+
+	/**
+	* Private constructor.
+	* @param String rule
+	*/
+	protected XomwConverter(String rule) {
+		this.rule = rule;
+		this.pos = 0;
+		this.end = XophpString_.strlen(rule);
+	}
+
+	/**
+	* Do the operation.
+	*
+	* @return String The RPN representation of the rule (e.g. "5 3 mod n is")
+	*/
+	protected String doConvert() {
+		boolean expectOperator = true;
+
+		// Iterate through all tokens, saving the operators and operands to a
+		// stack per Dijkstra's shunting yard algorithm.
+		/** @var Operator token */
+		XomwFragment token;
+		while (null != (token = this.nextToken())) {
+			// In this grammar, there are only binary operators, so every valid
+			// rule String will alternate between operator and operand tokens.
+			expectOperator = !expectOperator;
+
+			if (Type_.Is_assignable_from_by_obj(token, XomwExpression.class)) {
+				// Operand
+				if (expectOperator) {
+					token.error("unexpected operand");
+				}
+				this.operands.Add(token);
+				continue;
+			} else {
+				// Operator
+				if (!expectOperator) {
+					token.error("unexpected operator");
+				}
+				// Resolve higher precedence levels
+				XomwOperator lastOp = (XomwOperator)this.operators.end();
+				while (lastOp != null && Int_.Cast(XomwConverter.precedence.Get_by(((XomwOperator)token).name)) <= Int_.Cast(XomwConverter.precedence.Get_by(((XomwOperator)lastOp).name))) {
+					this.doOperation(lastOp, this.operands);
+					this.operators.pop();
+					lastOp = (XomwOperator)this.operators.end();
+				}
+				this.operators.Add(token);
+			}
+		}
+
+		// Finish off the stack
+		XomwOperator op = null;
+		while (null != (op = (XomwOperator)this.operators.pop())) {
+			this.doOperation(op, this.operands);
+		}
+
+		// Make sure the result is sane. The first case is possible for an empty
+		// String input, the second should be unreachable.
+		if (!this.operands.Count_bool()) {
+			this.error("condition expected");
+		} else if (this.operands.Count() > 1) {
+			this.error("missing operator or too many operands");
+		}
+
+		XomwExpression value = (XomwExpression)this.operands.Get_at(0);
+		if (!String_.Eq(value.type, "boolean")) {
+			this.error("the result must have a boolean type");
+		}
+
+		return ((XomwExpression)this.operands.Get_at(0)).rpn;
+	}
+
+	/**
+	* Fetch the next token from the input String.
+	*
+	* @return Fragment The next token
+	*/
+	protected XomwFragment nextToken() {
+		if (this.pos >= this.end) {
+			return null;
+		}
+
+		// Whitespace
+		int length = XophpString_.strspn(this.rule, XomwConverter.WHITESPACE_CLASS, this.pos);
+		this.pos += length;
+
+		if (this.pos >= this.end) {
+			return null;
+		}
+
+		// Number
+		length = XophpString_.strspn(this.rule, XomwConverter.NUMBER_CLASS, this.pos);
+		if (length != 0) {
+			XomwFragment token = this.newNumber(XophpString_.substr(this.rule, this.pos, length), this.pos);
+			this.pos += length;
+
+			return token;
+		}
+
+		// Two-character operators
+		String op2 = XophpString_.substr(this.rule, this.pos, 2);
+		if (String_.Eq(op2, "..") || String_.Eq(op2, "!=")) {
+			XomwFragment token = this.newOperator(op2, this.pos, 2);
+			this.pos += 2;
+
+			return token;
+		}
+
+		// Single-character operators
+		String op1 = Char_.To_str(String_.CharAt(this.rule, this.pos));
+		if (String_.Eq(op1, ",") || String_.Eq(op1, "=") || String_.Eq(op1, "%")) {
+			XomwFragment token = this.newOperator(op1, this.pos, 1);
+			this.pos++;
+
+			return token;
+		}
+
+		// Word
+		XophpArray m = XophpArray.New();
+		if (!XophpRegex_.preg_match_bool(XomwConverter.WORD_REGEX, this.rule, m, 0, this.pos)) {
+			this.error("unexpected character \"" + String_.CharAt(this.rule, this.pos) + "\"");
+		}
+		String word1 = XophpString_.strtolower(m.Get_at_str(0));
+		String word2 = "";
+		int nextTokenPos = this.pos + XophpString_.strlen(word1);
+		if (String_.Eq(word1, "not") || String_.Eq(word1, "is")) {
+			// Look ahead one word
+			nextTokenPos += XophpString_.strspn(this.rule, XomwConverter.WHITESPACE_CLASS, nextTokenPos);
+			m = XophpArray.New();
+			if (nextTokenPos < this.end
+				&& XophpRegex_.preg_match_bool(XomwConverter.WORD_REGEX, this.rule, m, 0, nextTokenPos)
+			) {
+				word2 = XophpString_.strtolower(m.Get_at_str(0));
+				nextTokenPos += XophpString_.strlen(word2);
+			}
+		}
+
+		// Two-word operators like "is not" take precedence over single-word operators like "is"
+		if (String_.Eq(word2, "")) {
+			String bothWords = word1 + "-" + word2;
+			if (XomwConverter.precedence.isset(bothWords)) {
+				XomwFragment token = this.newOperator(bothWords, this.pos, nextTokenPos - this.pos);
+				this.pos = nextTokenPos;
+
+				return token;
+			}
+		}
+
+		// Single-word operators
+		if (XomwConverter.precedence.isset(word1)) {
+			XomwFragment token = this.newOperator(word1, this.pos, XophpString_.strlen(word1));
+			this.pos += XophpString_.strlen(word1);
+
+			return token;
+		}
+
+		// The single-character operand symbols
+		if (XophpString_.strpos(XomwConverter.OPERAND_SYMBOLS, word1) != String_.Pos_neg1) {
+			XomwFragment token = this.newNumber(word1, this.pos);
+			this.pos++;
+
+			return token;
+		}
+
+		// Samples
+		if (String_.Eq(word1, "@integer") || String_.Eq(word1, "@decimal")) {
+			// Samples are like comments, they have no effect on rule evaluation.
+			// They run from the first sample indicator to the end of the String.
+			this.pos = this.end;
+
+			return null;
+		}
+
+		this.error("unrecognised word");
+		return null;
+	}
+
+	/**
+	* For the binary operator op, pop its operands off the stack and push
+	* a fragment with rpn and type members describing the result of that
+	* operation.
+	*
+	* @param Operator op
+	*/
+	protected void doOperation(XomwOperator op, Object ignore) { // NOTE: MW passes 2 args, but method only has 1
+		if (this.operands.Count() < 2) {
+			op.error("missing operand");
+		}
+		XomwExpression right = (XomwExpression)this.operands.pop();
+		XomwExpression left = (XomwExpression)this.operands.pop();
+		XomwExpression result = op.operate(left, right);
+		this.operands.Add(result);
+	}
+
+	/**
+	* Create a numerical expression Object
+	*
+	* @param String text
+	* @param int pos
+	* @return Expression The numerical expression
+	*/
+	protected XomwExpression newNumber(String text, int pos) {
+		return new XomwExpression(this, "number", text, pos, XophpString_.strlen(text));
+	}
+
+	/**
+	* Create a binary operator
+	*
+	* @param String type
+	* @param int pos
+	* @param int length
+	* @return Operator The operator
+	*/
+	protected XomwOperator newOperator(String type, int pos, int length) {
+		return new XomwOperator(this, type, pos, length);
+	}
+
+	/**
+	* Throw an error
+	* @param String message
+	*/
+	private void error(String message) {
+		throw new XomwError(message);
+	}
+}
\ No newline at end of file
diff --git a/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/XomwError.java b/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/XomwError.java
new file mode 100644
index 000000000..de7ea5f31
--- /dev/null
+++ b/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/XomwError.java
@@ -0,0 +1,29 @@
+/*
+XOWA: the XOWA Offline Wiki Application
+Copyright (C) 2012-2017 gnosygnu@gmail.com
+
+XOWA is licensed under the terms of the General Public License (GPL) Version 3,
+or alternatively under the terms of the Apache License Version 2.0.
+
+You may use XOWA according to either of these licenses as is most appropriate
+for your project on a case-by-case basis.
+
+The terms of each license can be found in the source code repository:
+
+GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt
+Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
+*/
+package gplx.xowa.mediawiki.vendor.wikimedia.cldr_plural_rule_parser.src; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.vendor.*; import gplx.xowa.mediawiki.vendor.wikimedia.*; import gplx.xowa.mediawiki.vendor.wikimedia.cldr_plural_rule_parser.*;
+import gplx.xowa.mediawiki.includes.exception.*;
+// MW.SRC:1.33.1
+/**
+* The exception cl+ass for all the cl+asses in this file. This will be thrown
+* back to the caller if there is any validation error.
+*/
+public class XomwError extends Err { 	public XomwError(String msg) {super(true, "", "", msg);
+	}
+	public static XomwError New(String msg) {return new XomwError("CLDR plural rule error: " + msg);}
+	public static XomwError New__fmt(String msg, Object... args) {
+		return new XomwError("CLDR plural rule error: " + XophpString_.Fmt(msg, args));
+	}
+}
diff --git a/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/XomwEvaluator.java b/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/XomwEvaluator.java
new file mode 100644
index 000000000..1da862a2e
--- /dev/null
+++ b/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/XomwEvaluator.java
@@ -0,0 +1,246 @@
+/*
+XOWA: the XOWA Offline Wiki Application
+Copyright (C) 2012-2017 gnosygnu@gmail.com
+
+XOWA is licensed under the terms of the General Public License (GPL) Version 3,
+or alternatively under the terms of the Apache License Version 2.0.
+
+You may use XOWA according to either of these licenses as is most appropriate
+for your project on a case-by-case basis.
+
+The terms of each license can be found in the source code repository:
+
+GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt
+Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
+*/
+package gplx.xowa.mediawiki.vendor.wikimedia.cldr_plural_rule_parser.src; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.vendor.*; import gplx.xowa.mediawiki.vendor.wikimedia.*; import gplx.xowa.mediawiki.vendor.wikimedia.cldr_plural_rule_parser.*;
+import gplx.xowa.mediawiki.vendor.wikimedia.cldr_plural_rule_parser.src.Converter.*;
+// MW.SRC:1.33.1
+public class XomwEvaluator {
+	/**
+	* Evaluate a number against a set of plural rules. If a rule passes,
+	* return the index of plural rule.
+	*
+	* @param int number The number to be evaluated against the rules
+	* @param array rules The associative array of plural rules in pluralform => rule format.
+	* @return int The index of the plural form which passed the evaluation
+	*/
+	public static int evaluate(String number, XophpArray rules) {
+		rules = XomwEvaluator.compile(rules);
+
+		return XomwEvaluator.evaluateCompiled(number, rules);
+	}
+
+	/**
+	* Convert a set of rules to a compiled form which is optimised for
+	* fast evaluation. The result will be an array of strings, and may be cached.
+	*
+	* @param array rules The rules to compile
+	* @return array An array of compile rules.
+	*/
+	public static XophpArray compile(XophpArray rules) {
+		XophpArray rv = XophpArray.New();
+		// We can't use array_map() for this because it generates a warning if
+		// there is an exception.
+		int rules_len = rules.Len();
+		for (int i = 0; i < rules_len; i++) {
+			String rule = rules.Get_at_str(i);
+			rule = XomwConverter.convert(rule);
+			rv.Add(rule);
+		}
+
+		return rv;
+	}
+
+	/**
+	* Evaluate a compiled set of rules returned by compile(). Do not allow
+	* the user to edit the compiled form, or else PHP errors may result.
+	*
+	* @param String number The number to be evaluated against the rules, in English, or it
+	*   may be a type convertible to String.
+	* @param array rules The associative array of plural rules in pluralform => rule format.
+	* @return int The index of the plural form which passed the evaluation
+	*/
+	public static int evaluateCompiled(String number_str, XophpArray rules) {
+		// Calculate the values of the operand symbols
+		// String number_str = XophpInt_.strval(number);
+
+		// NOTE: '/^ -? ( ([0-9]+) (?: \. ([0-9]+) )? )$/x'
+		XophpArray m = XophpArray.New();
+		if (!XophpRegex_.preg_match_bool(gplx.langs.regxs.Regx_adp_.new_("^-?(([0-9]+)(?:\\.([0-9]+))?)"), number_str, m, 0, 0)) {
+			XomwLog_.wfDebug_by_method("evaluateCompiled", ": invalid number input, returning \"other\"\n");
+			return rules.Count();
+		}
+
+		XophpArray operandSymbols = null;			
+		if (!m.isset(3)) {
+			operandSymbols = XophpArray.New()
+				.Add("n", Decimal_adp_.int_(XophpInt_.intval(m.Get_at_str(1))))
+				.Add("i", Decimal_adp_.int_(XophpInt_.intval(m.Get_at_str(1))))
+				.Add("v", Decimal_adp_.Zero)
+				.Add("w", Decimal_adp_.Zero)
+				.Add("f", Decimal_adp_.Zero)
+				.Add("t", Decimal_adp_.Zero)
+			;
+		} else {
+			String absValStr = m.Get_at_str(1);
+			String intStr = m.Get_at_str(2);
+			String fracStr = m.Get_at_str(3);
+			operandSymbols = XophpArray.New()
+				.Add("n", Decimal_adp_.double_(XophpFloat_.floatval(absValStr)))
+				.Add("i", Decimal_adp_.int_(XophpInt_.intval(intStr)))
+				.Add("v", Decimal_adp_.int_(XophpString_.strlen(fracStr)))
+				.Add("w", Decimal_adp_.int_(XophpString_.strlen(XophpString_.rtrim(fracStr, "0"))))
+				.Add("f", Decimal_adp_.int_(XophpInt_.intval(fracStr)))
+				.Add("t", Decimal_adp_.int_(XophpInt_.intval(XophpString_.rtrim(fracStr, "0"))))
+			;
+		}
+
+		// The compiled form is RPN, with tokens strictly delimited by
+		// spaces, so this is a simple RPN evaluator.
+		int rules_len = rules.Len();
+		for (int i = 0; i < rules_len; i++) {
+			String rule = rules.Get_at_str(i);
+			XophpArray stack = XophpArray.New();
+			int zero = XophpString_.ord("0");
+			int nine = XophpString_.ord("9");
+
+			String[] tokens = XophpString_.explode(" ", rule);
+			for (String token : tokens) {
+				int ord = XophpString_.ord(token);
+				if (operandSymbols.isset(token)) {
+					stack.Add(XomwStackItem.New__number((Decimal_adp)operandSymbols.Get_by(token)));
+				} else if (ord >= zero && ord <= nine) {
+					stack.Add(XomwStackItem.New__number(Decimal_adp_.int_(XophpInt_.intval(token))));
+				} else {
+					XomwStackItem right = (XomwStackItem)stack.pop();
+					XomwStackItem left = (XomwStackItem)stack.pop();
+					XomwStackItem result = XomwEvaluator.doOperation(token, left, right);
+					stack.Add(result);
+				}
+			}
+			if (((XomwStackItem)stack.Get_at(0)).Tid() == XomwStackItem.Tid__bool && ((XomwStackItem)stack.Get_at(0)).As_bool()) {
+				return i;
+			}
+		}
+		// None of the provided rules match. The number belongs to category
+		// "other", which comes last.
+		return rules.Count();
+	}
+
+	/**
+	* Do a single operation
+	*
+	* @param String token The token String
+	* @param mixed left The left operand. If it is an Object, its state may be destroyed.
+	* @param mixed right The right operand
+	* @throws Error
+	* @return mixed The operation result
+	*/
+	// XO: left / right can be boolean, Decimal, Range
+	private static final    XophpArray doOperationTokens = XophpArray.New().Add_as_key_and_val_many("in", "not-in", "within", "not-within");
+	private static XomwStackItem doOperation(String token, XomwStackItem left, XomwStackItem right) {
+		if (doOperationTokens.Has(token)) {
+			if (right.Tid() != XomwStackItem.Tid__range) {
+				right = XomwStackItem.New__range(new XomwRange(right.As_num(), null));
+			}
+		}
+		if (String_.Eq(token, "or")) {
+			return XomwStackItem.New__bool(left.As_bool() || right.As_bool());
+		}
+		else if (String_.Eq(token, "and")) {
+			return XomwStackItem.New__bool(left.As_bool() && right.As_bool());
+		}
+		else if (String_.Eq(token, "is")) {
+			return XomwStackItem.New__bool(left.As_bool() == right.As_bool());
+		}
+		else if (String_.Eq(token, "is-not")) {
+			return XomwStackItem.New__bool(left.As_bool() != right.As_bool());
+		}
+		else if (String_.Eq(token, "in")) {
+			return XomwStackItem.New__bool(right.As_range().isNumberIn(left.As_num()));
+		}
+		else if (String_.Eq(token, "not-in")) {
+			return XomwStackItem.New__bool(!right.As_range().isNumberIn(left.As_num()));
+		}
+		else if (String_.Eq(token, "within")) {
+			return XomwStackItem.New__bool(right.As_range().isNumberWithin(left.As_num()));
+		}
+		else if (String_.Eq(token, "not-within")) {
+			return XomwStackItem.New__bool(!right.As_range().isNumberWithin(left.As_num()));
+		}
+		else if (String_.Eq(token, "mod")) {
+			if (left.Tid() == XomwStackItem.Tid__number) {
+				return XomwStackItem.New__number(XophpMath.fmod_decimal(left.As_num(), right.As_num()));
+			}
+
+			return XomwStackItem.New__number(XophpMath.fmod_decimal(left.As_num(), right.As_num()));
+		}
+		else if (String_.Eq(token, ",")) {
+			XomwRange range = null;
+			if (left.Tid() == XomwStackItem.Tid__range) {
+				range = left.As_range();
+			} else {
+				range = new XomwRange(left.As_num(), null);
+			}
+			range.add(right.As_obj());
+
+			return XomwStackItem.New__range(range);
+		}
+		else if (String_.Eq(token, "..")) {
+			return XomwStackItem.New__range(new XomwRange(left.As_num(), right.As_num()));
+		}
+		else {
+			throw new XomwError("Invalid RPN token");
+		}
+	}
+}
+class XomwStackItem {
+	XomwStackItem(int tid, boolean val__bool, Decimal_adp val__number, XomwRange val__range) {
+		this.tid = tid;
+		this.val__bool = val__bool;
+		this.val__number = val__number;
+		this.val__range = val__range;
+	}
+	public int Tid() {return tid;} private final    int tid;
+	public boolean As_bool() {
+		if (tid != Tid__bool) Fail_bc_wrong_type(Tid__bool);
+		return val__bool;
+	} private final    boolean val__bool;
+	public Decimal_adp As_num() {
+		if (tid != Tid__number) Fail_bc_wrong_type(Tid__number);
+		return val__number;
+	} private final    Decimal_adp val__number;
+	public XomwRange As_range() {
+		if (tid != Tid__range) Fail_bc_wrong_type(Tid__range);
+		return val__range;
+	} private final    XomwRange val__range;
+	public Object As_obj() {
+		switch (tid) {
+			case Tid__bool: return val__bool;
+			case Tid__number: return val__number;
+			case Tid__range: return val__range;
+			default: throw Err_.new_unhandled_default(tid);
+		}
+	}
+	
+	private void Fail_bc_wrong_type(int expd) {
+		throw new XomwError("wrong type; expd=" + Tid_to_str(expd) + "; actl=" + Tid_to_str(tid));
+	}
+	private String Tid_to_str(int v) {
+		switch (tid) {
+			case Tid__bool: return "boolean";
+			case Tid__number: return "number";
+			case Tid__range: return "range";
+			default: throw Err_.new_unhandled_default(tid);
+		}
+	}
+
+	public static XomwStackItem New__bool(boolean v)          {return new XomwStackItem(Tid__bool, v, null, null);}
+	public static XomwStackItem New__number(Decimal_adp v) {return new XomwStackItem(Tid__number, false, v, null);}
+	public static XomwStackItem New__range(XomwRange v)    {return new XomwStackItem(Tid__range, false, null, v);}
+	public static final int
+	  Tid__bool = 0
+	, Tid__number = 1
+	, Tid__range = 2;
+}
\ No newline at end of file
diff --git a/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/XomwEvaluator_tst.java b/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/XomwEvaluator_tst.java
new file mode 100644
index 000000000..85fd6901f
--- /dev/null
+++ b/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/XomwEvaluator_tst.java
@@ -0,0 +1,178 @@
+/*
+XOWA: the XOWA Offline Wiki Application
+Copyright (C) 2012-2017 gnosygnu@gmail.com
+
+XOWA is licensed under the terms of the General Public License (GPL) Version 3,
+or alternatively under the terms of the Apache License Version 2.0.
+
+You may use XOWA according to either of these licenses as is most appropriate
+for your project on a case-by-case basis.
+
+The terms of each license can be found in the source code repository:
+
+GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt
+Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
+*/
+package gplx.xowa.mediawiki.vendor.wikimedia.cldr_plural_rule_parser.src; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.vendor.*; import gplx.xowa.mediawiki.vendor.wikimedia.*; import gplx.xowa.mediawiki.vendor.wikimedia.cldr_plural_rule_parser.*;
+import org.junit.*; import gplx.core.tests.*;
+public class XomwEvaluator_tst {
+	// REF: https://unicode.org/reports/tr35/tr35-numbers.html#Language_Plural_Rules
+	private final    XomwEvaluator_fxt fxt = new XomwEvaluator_fxt();
+	@Test  public void Rule__n() { // "absolute value of the source number (integer and decimals)."
+		fxt.Init__rule("n = 1");
+		fxt.Test__match__y("1", "-1");
+		fxt.Test__match__y("1.0", "-1.0"); // not sure if this is correct, but "'n' => floatval( $absValStr )", and "echo(floatval("1.00"));" -> "1"
+		fxt.Test__match__n("2", "1.1");
+	}
+	@Test  public void Rule__i() { // "integer digits of n."
+		fxt.Init__rule("i = 1");
+		fxt.Test__match__y("1");
+		fxt.Test__match__n("0", "2");
+	}
+	@Test  public void Rule__v() { // "number of visible fraction digits in n, with trailing zeros."
+		fxt.Init__rule("v = 1");
+		fxt.Test__match__y("2.3");
+		fxt.Test__match__n("2", "2.30");
+	}
+	@Test  public void Rule__w() { // "number of visible fraction digits in n, without trailing zeros."
+		fxt.Init__rule("w = 1");
+		fxt.Test__match__y("2.30", "2.3");
+		fxt.Test__match__n("2");
+	}
+	@Test  public void Rule__f() { // "visible fractional digits in n, with trailing zeros."
+		fxt.Init__rule("f = 1");
+		fxt.Test__match__y("2.1");
+		fxt.Test__match__n("2", "2.10");
+	}
+	@Test  public void Rule__t() { // "visible fractional digits in n, without trailing zeros."
+		fxt.Init__rule("t = 1");
+		fxt.Test__match__y("2.1", "2.10");
+		fxt.Test__match__n("2");
+	}
+	@Test  public void Rule__sample() { // MW ignores samples
+		fxt.Init__rule("n = 1 @integer f = 1"); // embed fake rule for "1.1" after @integer
+		fxt.Test__match__y("1");
+	}
+	@Test   public void Lang__0() {
+		// NOTE: never parse "other" rule; "// Don't record "other" rules, which have an empty condition"; REF.MW:/includes/cache/localization/LocalizationCache.php
+		// fxt.Init__rule("@integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …");
+		fxt.Init__rule();
+		fxt.Test__match(0, "1", "1.2", "-1"); // basically, anything
+	}
+	@Test   public void Lang__1__am() {
+		fxt.Init__rule("i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04");
+		fxt.Test__match(0, "0", "0.1", "-0.1", "1", "-1");
+		fxt.Test__match(1, "2", "1.1", "-1.1");
+	}
+	@Test   public void Lang__1__ff__fr() {
+		fxt.Init__rule("i = 0,1 @integer 0, 1 @decimal 0.0~1.5");
+		fxt.Test__match(0, "0", "0.1", "1", "1.0", "1.9");
+		fxt.Test__match(1, "2");
+	}
+	@Test   public void Lang__1__ast__de__fr() {
+		fxt.Init__rule("i = 1 and v = 0 @integer 1");
+		fxt.Test__match(0, "1", "-1");
+		fxt.Test__match(1, "1.1", "-1.1", "2");
+	}
+	@Test   public void Lang__1__si() {
+		fxt.Init__rule("n = 0,1 or i = 0 and f = 1 @integer 0, 1 @decimal 0.0, 0.1, 1.0, 0.00, 0.01, 1.00, 0.000, 0.001, 1.000, 0.0000, 0.0001, 1.0000");
+		fxt.Test__match(0, "0", "1", "-1", "0.1", "-0.1");
+		fxt.Test__match(1, "1.1", "0.11", "2");
+	}
+	@Test   public void Lang__1__ak__bh() {
+		fxt.Init__rule("n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000");
+		fxt.Test__match(0, "0", "1", "-1");
+		fxt.Test__match(1, "0.123", "-0.123", "1.1", "2");
+	}
+	@Test   public void Lang__1__tzm() {
+		fxt.Init__rule("n = 0..1 or n = 11..99 @integer 0, 1, 11~24 @decimal 0.0, 1.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0");
+		fxt.Test__match(0, "0", "1", "11", "21", "99");
+		fxt.Test__match(1, "0.123", "-0.123", "1.1", "2", "11.1", "99.1");
+	}
+	@Test   public void Lang__1__pt() {
+		fxt.Init__rule("n = 0..2 and n != 2 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000");
+		fxt.Test__match(0, "0", "1", "0.0", "1.0", "-1");
+		fxt.Test__match(1, "2", "1.1", "-2");
+	}
+	@Test   public void Lang__1__da() {
+		fxt.Init__rule("n = 1 or t != 0 and i = 0,1 @integer 1 @decimal 0.1~1.6");
+		fxt.Test__match(0, "0.2", "1", "-1", "1.2");
+		fxt.Test__match(1, "0", "2");
+	}
+	@Test   public void Lang__1__is() {
+		fxt.Init__rule("t = 0 and i % 10 = 1 and i % 100 != 11 or t != 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1~1.6, 10.1, 100.1, 1000.1, …");
+		fxt.Test__match(0, "1", "21", "101", "0.1", "1.1", "10.1");
+		fxt.Test__match(1, "0", "2", "11", "100", "0.0", "10", "10.0");
+	}
+	@Test   public void Lang__3__he__iw() {
+		fxt.Init__rule
+			( "i = 1 and v = 0 @integer 1"
+			, "i = 2 and v = 0 @integer 2"
+			, "v = 0 and n != 0..10 and n % 10 = 0 @integer 20, 30, 40, 50, 60, 70, 80, 90, 100, 1000, 10000, 100000, 1000000, …"
+			);
+		fxt.Test__match(0, "1", "-1");
+		fxt.Test__match(1, "2", "-2");
+		fxt.Test__match(2, "20", "30", "100", "110", "1000");
+		fxt.Test__match(3
+			, "1.2", "-1.2"
+			, "2.3", "-2.3"
+			, "3", "9", "-3", "11", "19", "101"
+			);
+	}
+	@Test   public void Lang__4__br() {
+		fxt.Init__rule
+			( "n % 10 = 1 and n % 100 != 11,71,91 @integer 1, 21, 31, 41, 51, 61, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 81.0, 101.0, 1001.0, …"
+			, "n % 10 = 2 and n % 100 != 12,72,92 @integer 2, 22, 32, 42, 52, 62, 82, 102, 1002, … @decimal 2.0, 22.0, 32.0, 42.0, 52.0, 62.0, 82.0, 102.0, 1002.0, …"
+			, "n % 10 = 3..4,9 and n % 100 != 10..19,70..79,90..99 @integer 3, 4, 9, 23, 24, 29, 33, 34, 39, 43, 44, 49, 103, 1003, … @decimal 3.0, 4.0, 9.0, 23.0, 24.0, 29.0, 33.0, 34.0, 103.0, 1003.0, …"
+			, "n != 0 and n % 1000000 = 0 @integer 1000000, … @decimal 1000000.0, 1000000.00, 1000000.000, …"
+			);
+		fxt.Test__match(0, "1", "21", "1.0", "21.0", "-1");
+		fxt.Test__match(1, "2", "22", "2.0", "22.0", "-2");
+		fxt.Test__match(2, "3", "4", "9", "23", "103", "3.0", "103.0", "-3");
+		fxt.Test__match(3, "1000000", "1000000.0");
+		fxt.Test__match(4
+			, "1.1", "11"
+			, "2.1"
+			, "3.1", "5", "6", "7", "8", "10", "19", "70", "79"
+			, "1000000.1" // NOTE: fails in C#
+			, "60"
+			);
+	}
+	@Test   public void Lang__5__ar() {
+		fxt.Init__rule
+			( "n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000"
+			, "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000"
+			, "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000"
+			, "n % 100 = 3..10 @integer 3~10, 103~110, 1003, … @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 103.0, 1003.0, …"
+			, "n % 100 = 11..99 @integer 11~26, 111, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, …"
+			);
+		fxt.Test__match(0, "0", "0.00");
+		fxt.Test__match(1, "1", "-1", "1.0");
+		fxt.Test__match(2, "2", "-2", "2.0");
+		fxt.Test__match(3, "3", "4", "10", "103", "1003", "-3", "3.0");
+		fxt.Test__match(4, "11", "99", "111", "1011", "-11", "11.0");
+		fxt.Test__match(5
+			, "0.1", "-0.1"
+			, "1.1", "-1.1"
+			, "2.1", "-2.1"
+			, "3.1", "10.1"
+			, "100", "102", "200", "1000"
+			);
+	}
+}
+class XomwEvaluator_fxt {
+	private final    XophpArray rules = XophpArray.New();
+	public void Init__rule(String... ary) {
+		rules.Clear();
+		for (String itm : ary)
+			rules.Add(itm);
+	}
+	public void Test__match__y(String... ary) {Test__match(0, ary);}
+	public void Test__match__n(String... ary) {Test__match(1, ary);}
+	public void Test__match(int expd, String... ary) {
+		for (String itm : ary) {
+			int actl = XomwEvaluator.evaluate(itm, rules);
+			Gftest.Eq__int(expd, actl, itm);
+		}
+	}
+}
diff --git a/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/XomwRange.java b/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/XomwRange.java
new file mode 100644
index 000000000..94652c519
--- /dev/null
+++ b/400_xowa/src/gplx/xowa/mediawiki/vendor/wikimedia/cldr_plural_rule_parser/src/XomwRange.java
@@ -0,0 +1,125 @@
+/*
+XOWA: the XOWA Offline Wiki Application
+Copyright (C) 2012-2017 gnosygnu@gmail.com
+
+XOWA is licensed under the terms of the General Public License (GPL) Version 3,
+or alternatively under the terms of the Apache License Version 2.0.
+
+You may use XOWA according to either of these licenses as is most appropriate
+for your project on a case-by-case basis.
+
+The terms of each license can be found in the source code repository:
+
+GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt
+Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
+*/
+package gplx.xowa.mediawiki.vendor.wikimedia.cldr_plural_rule_parser.src; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.vendor.*; import gplx.xowa.mediawiki.vendor.wikimedia.*; import gplx.xowa.mediawiki.vendor.wikimedia.cldr_plural_rule_parser.*;
+// MW.SRC:1.33.1
+/**
+* Evaluator helper class representing a range list.
+*/
+class XomwRange {
+	/**
+	* The parts
+	*
+	* @var array
+	*/
+	public XophpArray parts = XophpArray.New();
+
+	/**
+	* Initialize a new instance of Range
+	*
+	* @param int start The start of the range
+	* @param int|boolean end The end of the range, or false if the range is not bounded.
+	*/
+	public XomwRange(Decimal_adp start, Decimal_adp end) {
+		if (end == null) {
+			this.parts.Add(start);
+		} else {
+			this.parts.Add(XophpArray.New().Add(start).Add(end));
+		}
+	}
+
+	/**
+	* Determine if the given number is inside the range.
+	*
+	* @param int number The number to check
+	* @param boolean integerConstraint If true, also asserts the number is an integer;
+	*   otherwise, number simply has to be inside the range.
+	* @return boolean True if the number is inside the range; otherwise, false.
+	*/
+	public boolean isNumberIn(Decimal_adp number) {return isNumberIn(number, true);}
+	public boolean isNumberIn(Decimal_adp number, boolean integerConstraint) {
+		int parts_len = parts.Len();
+		for (int i = 0; i < parts_len; i++) {
+			Object part_obj = this.parts.Get_at(i);
+			if (XophpArray.is_array(part_obj)) {
+				XophpArray part = (XophpArray)part_obj;
+				if ((!integerConstraint || number.Floor().Eq(number))
+					&& number.Comp_gte((Decimal_adp)part.Get_at(0)) && number.Comp_lte((Decimal_adp)part.Get_at(1))
+				) {
+					return true;
+				}
+			} else {
+				Decimal_adp part_decimal = (Decimal_adp)part_obj;
+				if (part_decimal == null) part_decimal = number; // if "new XomwRange(start, null)", then range is just "start, start"
+				if (number.Eq(part_decimal)) {
+					return true;
+				}
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	* Readable alias for isNumberIn(number, false), and the implementation
+	* of the "within" operator.
+	*
+	* @param int number The number to check
+	* @return boolean True if the number is inside the range; otherwise, false.
+	*/
+	public boolean isNumberWithin(Decimal_adp number) {
+		return this.isNumberIn(number, false);
+	}
+
+	/**
+	* Add another part to this range.
+	*
+	* @param Range|int other The part to add, either
+	*   a range Object itself or a single number.
+	*/
+	public void add(Object otherObj) {
+		if (Type_.Eq_by_obj(otherObj, XomwRange.class)) {
+			this.parts = XophpArrayUtl.array_merge(this.parts, ((XomwRange)otherObj).parts);
+		} else {
+			this.parts.Add(otherObj);
+		}
+	}
+
+	/**
+	* Returns the String representation of the rule evaluator range.
+	* The purpose of this method is to help debugging.
+	*
+	* @return String The String representation of the rule evaluator range
+	*/
+	@Override public String toString() {
+		String s = "Range(";
+		int parts_len = this.parts.Len();
+		for (int i = 0; i < parts_len; i++) {
+			Object part_obj = this.parts.Get_at(i);
+			if (i > 0) {
+				s += ", ";
+			}
+			if (XophpArray.is_array(part_obj)) {
+				XophpArray part = (XophpArray)part_obj;
+				s += Int_.To_str(part.Get_at_int(0)) + ".." + Int_.To_str(part.Get_at_int(1));
+			} else {
+				s += Int_.To_str(Int_.Cast(part_obj));
+			}
+		}
+		s += ")";
+
+		return s;
+	}
+}
diff --git a/400_xowa/src/gplx/xowa/mediawiki/xml/XophpDOMDocument.java b/400_xowa/src/gplx/xowa/mediawiki/xml/XophpDOMDocument.java
new file mode 100644
index 000000000..d26810bbb
--- /dev/null
+++ b/400_xowa/src/gplx/xowa/mediawiki/xml/XophpDOMDocument.java
@@ -0,0 +1,27 @@
+/*
+XOWA: the XOWA Offline Wiki Application
+Copyright (C) 2012-2017 gnosygnu@gmail.com
+
+XOWA is licensed under the terms of the General Public License (GPL) Version 3,
+or alternatively under the terms of the Apache License Version 2.0.
+
+You may use XOWA according to either of these licenses as is most appropriate
+for your project on a case-by-case basis.
+
+The terms of each license can be found in the source code repository:
+
+GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt
+Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
+*/
+package gplx.xowa.mediawiki.xml; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*;
+import gplx.langs.xmls.*;
+public class XophpDOMDocument {
+	private XmlDoc xdoc;
+	public void loadXML(String xml) {
+		this.xdoc = XmlDoc_.parse(xml);
+	}
+	public XophpDOMNodeList getElementsByTagName(String tagName) {
+		XmlNdeList list = XmlDoc_.Select_tags(xdoc.Root(), tagName);
+		return new XophpDOMNodeList(list);
+	}
+}
\ No newline at end of file
diff --git a/400_xowa/src/gplx/xowa/mediawiki/xml/XophpDOMNode.java b/400_xowa/src/gplx/xowa/mediawiki/xml/XophpDOMNode.java
new file mode 100644
index 000000000..6bf3790bc
--- /dev/null
+++ b/400_xowa/src/gplx/xowa/mediawiki/xml/XophpDOMNode.java
@@ -0,0 +1,32 @@
+/*
+XOWA: the XOWA Offline Wiki Application
+Copyright (C) 2012-2017 gnosygnu@gmail.com
+
+XOWA is licensed under the terms of the General Public License (GPL) Version 3,
+or alternatively under the terms of the Apache License Version 2.0.
+
+You may use XOWA according to either of these licenses as is most appropriate
+for your project on a case-by-case basis.
+
+The terms of each license can be found in the source code repository:
+
+GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt
+Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
+*/
+package gplx.xowa.mediawiki.xml; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*;
+import gplx.langs.xmls.*;
+public class XophpDOMNode {
+	private final    XmlNde xnde;
+	public String nodeValue = null;
+	public XophpDOMNode(XmlNde xnde) {
+		this.xnde = xnde;
+		// TODO.PHP:implement edge cases for nodeValue; https://stackoverflow.com/questions/12380919/php-dom-textcontent-vs-nodevalue
+		this.nodeValue = xnde.Text_inner();
+	}
+	public String getAttribute(String attribName) {
+		return xnde.Atrs().Get_by(attribName).Value();
+	}
+	public XophpDOMNodeList getElementsByTagName(String tagName) {
+		return new XophpDOMNodeList(XmlDoc_.Select_tags(xnde, tagName));
+	}
+}
diff --git a/400_xowa/src/gplx/xowa/mediawiki/xml/XophpDOMNodeList.java b/400_xowa/src/gplx/xowa/mediawiki/xml/XophpDOMNodeList.java
new file mode 100644
index 000000000..60fda9a3c
--- /dev/null
+++ b/400_xowa/src/gplx/xowa/mediawiki/xml/XophpDOMNodeList.java
@@ -0,0 +1,29 @@
+/*
+XOWA: the XOWA Offline Wiki Application
+Copyright (C) 2012-2017 gnosygnu@gmail.com
+
+XOWA is licensed under the terms of the General Public License (GPL) Version 3,
+or alternatively under the terms of the Apache License Version 2.0.
+
+You may use XOWA according to either of these licenses as is most appropriate
+for your project on a case-by-case basis.
+
+The terms of each license can be found in the source code repository:
+
+GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt
+Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
+*/
+package gplx.xowa.mediawiki.xml; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*;
+import gplx.langs.xmls.*;
+public class XophpDOMNodeList {
+	private final    List_adp list = List_adp_.New();
+	public XophpDOMNodeList(XmlNdeList nde_list) {
+		int len = nde_list.Count();
+		for (int i = 0; i < len; i++) {
+			XmlNde nde = nde_list.Get_at(i);
+			list.Add(new XophpDOMNode(nde));
+		}
+	}
+	public int count() {return list.Count();}
+	public XophpDOMNode item(int i) {return (XophpDOMNode)list.Get_at(i);}
+}
diff --git a/400_xowa/src/gplx/xowa/parsers/tmpls/Xot_fmtr.java b/400_xowa/src/gplx/xowa/parsers/tmpls/Xot_fmtr.java
index 5a7fb9189..ef0e89bf2 100644
--- a/400_xowa/src/gplx/xowa/parsers/tmpls/Xot_fmtr.java
+++ b/400_xowa/src/gplx/xowa/parsers/tmpls/Xot_fmtr.java
@@ -20,54 +20,3 @@ public interface Xot_fmtr {
 	void Reg_tmpl(Xop_ctx ctx, byte[] src, Xop_tkn_itm name_tkn, int args_len, Arg_nde_tkn[] args);
 	void Reg_arg(Xop_ctx ctx, byte[] src, int arg_idx, Arg_nde_tkn self_tkn);
 }
-class Xot_fmtr_prm implements Xot_fmtr {
-	public Xot_fmtr_prm Caller_(Xot_invk v)		{this.caller = v; return this;} private Xot_invk caller;
-	public Xot_fmtr_prm NewLineArgs_(boolean v)	{this.newLineArgs = v; return this;} private boolean newLineArgs = false;
-	public void Reg_ary(Xop_ctx ctx, byte[] src, boolean tmpl_static, int src_bgn, int src_end, int subs_len, Xop_tkn_itm[] subs) {
-		if (tmpl_static && src_bgn != -1) trg.Add_mid(src, src_bgn, src_end);	// HACK: fails for {{IPA-de|l|lang|De-Ludwig_van_Beethoven.ogg}}
-		for (int i = 0; i < subs_len; i++)
-			subs[i].Tmpl_fmt(ctx, src, this);
-	}
-	public void Reg_prm(Xop_ctx ctx, byte[] src, Xot_prm_tkn self, int prm_idx, byte[] prm_key, Xop_tkn_itm dflt_tkn) {
-		if (caller == null) {	// raw mode
-			trg.Add(Bry_bgn);
-			if (prm_idx == -1)	{if (prm_key != null) trg.Add(prm_key);}
-			else				trg.Add_int_variable(prm_idx);
-			if (dflt_tkn != null) {
-				trg.Add_byte(Byte_ascii.Pipe);
-				dflt_tkn.Tmpl_fmt(ctx, src, this);
-			}
-			trg.Add(Bry_end);
-		}
-		else					// invk mode
-			self.Tmpl_evaluate(ctx, src, caller, trg);
-	}	private static final    byte[] Bry_bgn = new byte[] {Byte_ascii.Curly_bgn, Byte_ascii.Curly_bgn, Byte_ascii.Curly_bgn}, Bry_end = new byte[] {Byte_ascii.Curly_end, Byte_ascii.Curly_end, Byte_ascii.Curly_end};
-	public void Reg_tmpl(Xop_ctx ctx, byte[] src, Xop_tkn_itm name_tkn, int args_len, Arg_nde_tkn[] args) {
-		trg.Add(Xop_curly_bgn_lxr.Hook);
-		++depth;
-		name_tkn.Tmpl_fmt(ctx, src, this);
-		for (int i = 0; i < args_len; i++) {
-			if (depth == 1 && newLineArgs) trg.Add_byte_nl();
-			trg.Add_byte(Byte_ascii.Pipe);
-			args[i].Tmpl_fmt(ctx, src, this);
-		}
-		--depth;
-		trg.Add(Xop_curly_end_lxr.Hook);
-	}
-	public void Write(byte b) {trg.Add_byte(b);}
-	public void Reg_arg(Xop_ctx ctx, byte[] src, int arg_idx, Arg_nde_tkn self_tkn) {
-		self_tkn.Key_tkn().Tmpl_fmt(ctx, src, this);
-		if (self_tkn.KeyTkn_exists()) {
-			if (arg_idx == 0) {
-				if (self_tkn.Eq_tkn().Tkn_tid() == Xop_tkn_itm_.Tid_colon)
-					trg.Add_byte(Byte_ascii.Colon);
-			}
-			else
-				trg.Add_byte(Byte_ascii.Eq);
-		}
-		self_tkn.Val_tkn().Tmpl_fmt(ctx, src, this);
-	}
-	public void Print(Bry_bfr bb) {bb.Add_bfr_and_preserve(trg); trg.Clear(); depth = 0;}
-	Bry_bfr trg = Bry_bfr_.New(); int depth = 0;
-	public static final    Xot_fmtr_prm Instance = new Xot_fmtr_prm();
-}
diff --git a/400_xowa/src/gplx/xowa/parsers/tmpls/Xot_fmtr_prm.java b/400_xowa/src/gplx/xowa/parsers/tmpls/Xot_fmtr_prm.java
new file mode 100644
index 000000000..8beabf46c
--- /dev/null
+++ b/400_xowa/src/gplx/xowa/parsers/tmpls/Xot_fmtr_prm.java
@@ -0,0 +1,67 @@
+/*
+XOWA: the XOWA Offline Wiki Application
+Copyright (C) 2012-2017 gnosygnu@gmail.com
+
+XOWA is licensed under the terms of the General Public License (GPL) Version 3,
+or alternatively under the terms of the Apache License Version 2.0.
+
+You may use XOWA according to either of these licenses as is most appropriate
+for your project on a case-by-case basis.
+
+The terms of each license can be found in the source code repository:
+
+GPLv3 License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-GPLv3.txt
+Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
+*/
+package gplx.xowa.parsers.tmpls; import gplx.*; import gplx.xowa.*; import gplx.xowa.parsers.*;
+public class Xot_fmtr_prm implements Xot_fmtr {
+	public Xot_fmtr_prm Caller_(Xot_invk v)		{this.caller = v; return this;} private Xot_invk caller;
+	public Xot_fmtr_prm NewLineArgs_(boolean v)	{this.newLineArgs = v; return this;} private boolean newLineArgs = false;
+	public void Reg_ary(Xop_ctx ctx, byte[] src, boolean tmpl_static, int src_bgn, int src_end, int subs_len, Xop_tkn_itm[] subs) {
+		if (tmpl_static && src_bgn != -1) trg.Add_mid(src, src_bgn, src_end);	// HACK: fails for {{IPA-de|l|lang|De-Ludwig_van_Beethoven.ogg}}
+		for (int i = 0; i < subs_len; i++)
+			subs[i].Tmpl_fmt(ctx, src, this);
+	}
+	public void Reg_prm(Xop_ctx ctx, byte[] src, Xot_prm_tkn self, int prm_idx, byte[] prm_key, Xop_tkn_itm dflt_tkn) {
+		if (caller == null) {	// raw mode
+			trg.Add(Bry_bgn);
+			if (prm_idx == -1)	{if (prm_key != null) trg.Add(prm_key);}
+			else				trg.Add_int_variable(prm_idx);
+			if (dflt_tkn != null) {
+				trg.Add_byte(Byte_ascii.Pipe);
+				dflt_tkn.Tmpl_fmt(ctx, src, this);
+			}
+			trg.Add(Bry_end);
+		}
+		else					// invk mode
+			self.Tmpl_evaluate(ctx, src, caller, trg);
+	}	private static final    byte[] Bry_bgn = new byte[] {Byte_ascii.Curly_bgn, Byte_ascii.Curly_bgn, Byte_ascii.Curly_bgn}, Bry_end = new byte[] {Byte_ascii.Curly_end, Byte_ascii.Curly_end, Byte_ascii.Curly_end};
+	public void Reg_tmpl(Xop_ctx ctx, byte[] src, Xop_tkn_itm name_tkn, int args_len, Arg_nde_tkn[] args) {
+		trg.Add(Xop_curly_bgn_lxr.Hook);
+		++depth;
+		name_tkn.Tmpl_fmt(ctx, src, this);
+		for (int i = 0; i < args_len; i++) {
+			if (depth == 1 && newLineArgs) trg.Add_byte_nl();
+			trg.Add_byte(Byte_ascii.Pipe);
+			args[i].Tmpl_fmt(ctx, src, this);
+		}
+		--depth;
+		trg.Add(Xop_curly_end_lxr.Hook);
+	}
+	public void Write(byte b) {trg.Add_byte(b);}
+	public void Reg_arg(Xop_ctx ctx, byte[] src, int arg_idx, Arg_nde_tkn self_tkn) {
+		self_tkn.Key_tkn().Tmpl_fmt(ctx, src, this);
+		if (self_tkn.KeyTkn_exists()) {
+			if (arg_idx == 0) {
+				if (self_tkn.Eq_tkn().Tkn_tid() == Xop_tkn_itm_.Tid_colon)
+					trg.Add_byte(Byte_ascii.Colon);
+			}
+			else
+				trg.Add_byte(Byte_ascii.Eq);
+		}
+		self_tkn.Val_tkn().Tmpl_fmt(ctx, src, this);
+	}
+	public void Print(Bry_bfr bb) {bb.Add_bfr_and_preserve(trg); trg.Clear(); depth = 0;}
+	Bry_bfr trg = Bry_bfr_.New(); int depth = 0;
+	public static final    Xot_fmtr_prm Instance = new Xot_fmtr_prm();
+}
diff --git a/400_xowa/src/gplx/xowa/xtns/pfuncs/Pf_func_.java b/400_xowa/src/gplx/xowa/xtns/pfuncs/Pf_func_.java
index 90dc5ca19..a5b08c684 100644
--- a/400_xowa/src/gplx/xowa/xtns/pfuncs/Pf_func_.java
+++ b/400_xowa/src/gplx/xowa/xtns/pfuncs/Pf_func_.java
@@ -19,6 +19,7 @@ import gplx.xowa.langs.*; import gplx.xowa.langs.msgs.*; import gplx.xowa.langs.
 import gplx.xowa.xtns.pfuncs.ifs.*; import gplx.xowa.xtns.pfuncs.times.*; import gplx.xowa.xtns.pfuncs.numbers.*; import gplx.xowa.xtns.pfuncs.ttls.*; import gplx.xowa.xtns.pfuncs.langs.*; import gplx.xowa.xtns.pfuncs.strings.*; import gplx.xowa.xtns.pfuncs.tags.*; import gplx.xowa.xtns.pfuncs.stringutils.*; import gplx.xowa.xtns.pfuncs.pages.*; import gplx.xowa.xtns.pfuncs.wikis.*;
 import gplx.xowa.parsers.*; import gplx.xowa.parsers.tmpls.*;
 import gplx.xowa.wikis.domains.*;
+import gplx.xowa.mediawiki.*;
 public class Pf_func_ {
 	public static final byte Name_dlm = Byte_ascii.Colon;
 	public static boolean Eval_arg_to_kvp(byte[][] rslt, Xop_ctx ctx, byte[] src, Xot_invk caller, Xot_invk self, int self_args_len, Bry_bfr tmp_bfr, int i) {
@@ -106,6 +107,42 @@ public class Pf_func_ {
 		}
 		return Ary_nonwmf;
 	}
+
+	// convert XO template to MW array; EX: {{name:argx|key1=val1|key2=val2}} comes in as a Xot_invk obj; convert it to an array of [(key1, val1), (key2, val2)]
+	public static XophpArray Convert_xo_tmpl_to_mw_ary(Xop_ctx ctx, Xot_invk caller, Xot_invk tmpl, byte[] src) {
+		// init
+		XophpArray rv = XophpArray.New();
+		Bry_bfr rv_bfr = Bry_bfr_.New();
+		Bry_bfr tmp_bfr = Bry_bfr_.New();
+		int args_len = tmpl.Args_len();
+
+		// loop
+		for (int i = 0; i < args_len; i++) {
+			// clear bfrs
+			rv_bfr.Clear();
+			tmp_bfr.Clear();
+
+			// get arg
+			Arg_nde_tkn arg = tmpl.Args_get_by_idx(i);
+
+			// eval key; NOTE: could be recursive
+			arg.Key_tkn().Tmpl_evaluate(ctx, src, caller, tmp_bfr);
+			rv_bfr.Add_bfr_and_clear(tmp_bfr);
+
+			// add eq
+			if (rv_bfr.Len_gt_0())
+				rv_bfr.Add_byte_eq();
+
+			// eval val; NOTE: could be recursive
+			arg.Val_tkn().Tmpl_evaluate(ctx, src, caller, tmp_bfr);
+			rv_bfr.Add_bfr_and_clear(tmp_bfr);
+
+			// add to rv
+			rv.Add(rv_bfr.To_str_and_clear());
+		}
+		return rv;
+	}
+
 	private static int[] Ary_nonwmf = null;
 	private static final    int[] Ary_wmf = new int[]
 	{ Xol_kwd_grp_.Id_utc_year
diff --git a/400_xowa/src/gplx/xowa/xtns/pfuncs/langs/Pfunc_plural.java b/400_xowa/src/gplx/xowa/xtns/pfuncs/langs/Pfunc_plural.java
index 437c09c70..dc7a4c948 100644
--- a/400_xowa/src/gplx/xowa/xtns/pfuncs/langs/Pfunc_plural.java
+++ b/400_xowa/src/gplx/xowa/xtns/pfuncs/langs/Pfunc_plural.java
@@ -16,16 +16,31 @@ Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
 package gplx.xowa.xtns.pfuncs.langs; import gplx.*; import gplx.xowa.*; import gplx.xowa.xtns.*; import gplx.xowa.xtns.pfuncs.*;
 import gplx.xowa.langs.*; import gplx.xowa.langs.kwds.*;
 import gplx.xowa.parsers.*; import gplx.xowa.parsers.tmpls.*;
+import gplx.xowa.mediawiki.*;
 public class Pfunc_plural extends Pf_func_base {
-	@Override public boolean Func_require_colon_arg() {return true;}
-	@Override public void Func_evaluate(Bry_bfr bfr, Xop_ctx ctx, Xot_invk caller, Xot_invk self, byte[] src) {// REF.MW: CoreParserFunctions.php
-		byte[] number = Eval_argx(ctx, src, caller, self);
-		int self_args_len = self.Args_len();
-		int arg_idx = Pf_func_.Eq(ctx, number, Ary_Num_1) ? 0 : 1;
-		if (arg_idx == 1 && self_args_len == 1) arg_idx = 0;	// number is plural, but plural_arg not present; use singular; see test
-		byte[] word = Pf_func_.Eval_arg_or_empty(ctx, src, caller, self, self_args_len, arg_idx);
-		bfr.Add(word);
-	}	private static final    byte[] Ary_Num_1 = new byte[] {Byte_ascii.Num_1};
 	@Override public int Id() {return Xol_kwd_grp_.Id_i18n_plural;}
 	@Override public Pf_func New(int id, byte[] name) {return new Pfunc_plural().Name_(name);}
-}	
+	@Override public boolean Func_require_colon_arg() {return true;}
+	// EX: {{plural:1|one|many}}
+	@Override public void Func_evaluate(Bry_bfr bfr, Xop_ctx ctx, Xot_invk caller, Xot_invk self, byte[] src) {// REF.MW: CoreParserFunctions.php
+		// convert xo_tmpl to mw_ary
+		XophpArray forms = Pf_func_.Convert_xo_tmpl_to_mw_ary(ctx, caller, self, src);
+
+		// get number from argx (EX: 1)
+		String number_str = String_.new_u8(Eval_argx(ctx, src, caller, self));
+
+		// if number matches explicit key, use it; EX: {{plural:3|2=two|3=three|one|many}} -> three
+		Object result = ctx.Lang().Mw_lang().handleExplicitPluralForms(number_str, forms);
+		if (Type_.Eq_by_obj(result, String.class)) {
+			bfr.Add_str_u8((String)result);
+			return;
+		}
+
+		// no match for explicit key; take results (which has removed all explicit keys) and get plural rule index; EX: {{plural:1|2=two|3=three|one|many}} -> {{plural:?|one|many}}
+		XophpArray resultArray = (XophpArray)result;
+		int idx = ctx.Lang().Mw_lang().getPluralRuleIndexNumber(number_str);
+		if (idx >= resultArray.Count()) // bound-check; EX: {{plural:2|wiki}} -> idx = 1 -> idx = 0
+			idx = resultArray.Count() - 1;
+		bfr.Add_str_u8(resultArray.Get_at_str(idx));
+	}
+}
diff --git a/400_xowa/src/gplx/xowa/xtns/pfuncs/langs/Pfunc_plural_tst.java b/400_xowa/src/gplx/xowa/xtns/pfuncs/langs/Pfunc_plural_tst.java
index 21423637b..96955e323 100644
--- a/400_xowa/src/gplx/xowa/xtns/pfuncs/langs/Pfunc_plural_tst.java
+++ b/400_xowa/src/gplx/xowa/xtns/pfuncs/langs/Pfunc_plural_tst.java
@@ -15,11 +15,22 @@ Apache License: https://github.com/gnosygnu/xowa/blob/master/LICENSE-APACHE2.txt
 */
 package gplx.xowa.xtns.pfuncs.langs; import gplx.*; import gplx.xowa.*; import gplx.xowa.xtns.*; import gplx.xowa.xtns.pfuncs.*;
 import org.junit.*;
+import gplx.xowa.mediawiki.languages.*;
 public class Pfunc_plural_tst {
-	private final Xop_fxt fxt = new Xop_fxt();
-	@Before public void init()					{fxt.Reset();}
-	@Test  public void Singular()				{fxt.Test_parse_tmpl_str_test("{{plural:1|wiki|wikis}}"				, "{{test}}"	, "wiki");}
-	@Test  public void Plural()					{fxt.Test_parse_tmpl_str_test("{{plural:2|wiki|wikis}}"				, "{{test}}"	, "wikis");}
-	@Test  public void Plural_but_one_arg()		{fxt.Test_parse_tmpl_str_test("{{plural:2|wiki}}"					, "{{test}}"	, "wiki");}
-	@Test  public void Null()					{fxt.Test_parse_tmpl_str_test("{{plural:|wiki|wikis}}"				, "{{test}}"	, "wikis");}
+	private final    Xop_fxt fxt = new Xop_fxt();
+	@Before public void init() {
+		fxt.Reset();
+		XomwLanguage_fxt lang_fxt = new XomwLanguage_fxt();
+		lang_fxt.Init__pluralRulesXml__en();
+	}
+	@Test  public void Singular()				{fxt.Test__parse__tmpl_to_html("{{plural:1|wiki|wikis}}" , "wiki");}
+	@Test  public void Plural()					{fxt.Test__parse__tmpl_to_html("{{plural:2|wiki|wikis}}" , "wikis");}
+	@Test  public void Plural_but_one_arg()		{fxt.Test__parse__tmpl_to_html("{{plural:2|wiki}}"       , "wiki");}
+	@Test  public void Null()					{fxt.Test__parse__tmpl_to_html("{{plural:|wiki|wikis}}"  , "wikis");}
+	@Test  public void handleExplicitPluralForms() {
+		fxt.Test__parse__tmpl_to_html("{{plural:1|2=two|3=three|one|many}}", "one");
+		fxt.Test__parse__tmpl_to_html("{{plural:2|2=two|3=three|one|many}}", "two");
+		fxt.Test__parse__tmpl_to_html("{{plural:3|2=two|3=three|one|many}}", "three");
+		fxt.Test__parse__tmpl_to_html("{{plural:9|2=two|3=three|one|many}}", "many");
+	}
 }
diff --git a/res/bin/any/mediawiki/languages/data/plurals-mediawiki.xml b/res/bin/any/mediawiki/languages/data/plurals-mediawiki.xml
new file mode 100644
index 000000000..1ed6a5187
--- /dev/null
+++ b/res/bin/any/mediawiki/languages/data/plurals-mediawiki.xml
@@ -0,0 +1,37 @@
+
+
+
+	
+		
+		
+			n % 10 = 1 and n % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 71.0, 81.0, 101.0, 1001.0, …
+			n % 10 = 2..4 and n % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 2.0, 3.0, 4.0, 22.0, 23.0, 24.0, 32.0, 33.0, 102.0, 1002.0, …
+			n % 10 = 0 or n % 10 = 5..9 or n % 100 = 11..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+			   @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, …
+		
+		
+		
+			n % 10 = 1 @integer 1, 11, 21, 31, …
+			n % 10 = 2 @integer 2, 12, 22, 32, …
+			n % 10 = 3..4 @integer 3~4, 13~14, 23~24, …
+			 @integer 5, 6, 7, 8, 9, 10, 15, 105, 206, 307, …
+		
+		
+		
+			n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000
+			 @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+		
+		
+		
+			n % 10 = 1 and n % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 91, 101, 121, …
+			n % 10 = 2 and n % 100 != 12 @integer 2, 22, 32, 42, 52, 62, 72, 82, 92, 102, 122, …
+			n = 0 or n % 100 = 0 or n % 100 = 10..19 @integer 0, 11, 12, 13, 14, 15, 16, 17, 18, 19, 100, 111,112, …
+			 @integer 3, 4, 5, 6, 7, 8, 9, 20, 103, 104, …
+		
+		
+			v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …
+			v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 0.2~0.4, 1.2~1.4, 2.2~2.4, 3.2~3.4, 4.2~4.4, 5.2, 10.2, 100.2, 1000.2, …
+			 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+		
+	
+
diff --git a/res/bin/any/mediawiki/languages/data/plurals.xml b/res/bin/any/mediawiki/languages/data/plurals.xml
new file mode 100644
index 000000000..78bfa831a
--- /dev/null
+++ b/res/bin/any/mediawiki/languages/data/plurals.xml
@@ -0,0 +1,231 @@
+
+
+
+
+    
+    
+    
+        
+
+        
+
+        
+             @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+
+        
+
+        
+            i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04
+             @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+        
+            i = 0,1 @integer 0, 1 @decimal 0.0~1.5
+             @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+        
+            i = 1 and v = 0 @integer 1
+             @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+        
+            n = 0,1 or i = 0 and f = 1 @integer 0, 1 @decimal 0.0, 0.1, 1.0, 0.00, 0.01, 1.00, 0.000, 0.001, 1.000, 0.0000, 0.0001, 1.0000
+             @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.2~0.9, 1.1~1.8, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+        
+            n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000
+             @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+        
+            n = 0..1 or n = 11..99 @integer 0, 1, 11~24 @decimal 0.0, 1.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0
+             @integer 2~10, 100~106, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+        
+            n = 0..2 and n != 2 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000
+             @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+        
+            n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
+             @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+        
+            n = 1 and v = 0 @integer 1
+             @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+        
+            n = 1 or t != 0 and i = 0,1 @integer 1 @decimal 0.1~1.6
+             @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 2.0~3.4, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+        
+            t = 0 and i % 10 = 1 and i % 100 != 11 or t != 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1~1.6, 10.1, 100.1, 1000.1, …
+             @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+        
+            v = 0 and i % 10 = 1 or f % 10 = 1 @integer 1, 11, 21, 31, 41, 51, 61, 71, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …
+             @integer 0, 2~10, 12~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.2~1.0, 1.2~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+        
+            v = 0 and i = 1,2,3 or v = 0 and i % 10 != 4,6,9 or v != 0 and f % 10 != 4,6,9 @integer 0~3, 5, 7, 8, 10~13, 15, 17, 18, 20, 21, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.3, 0.5, 0.7, 0.8, 1.0~1.3, 1.5, 1.7, 1.8, 2.0, 2.1, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+             @integer 4, 6, 9, 14, 16, 19, 24, 26, 104, 1004, … @decimal 0.4, 0.6, 0.9, 1.4, 1.6, 1.9, 2.4, 2.6, 10.4, 100.4, 1000.4, …
+        
+
+        
+
+        
+            n % 10 = 0 or n % 100 = 11..19 or v = 2 and f % 100 = 11..19 @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+            n % 10 = 1 and n % 100 != 11 or v = 2 and f % 10 = 1 and f % 100 != 11 or v != 2 and f % 10 = 1 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.0, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …
+             @integer 2~9, 22~29, 102, 1002, … @decimal 0.2~0.9, 1.2~1.9, 10.2, 100.2, 1000.2, …
+        
+        
+            n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000
+            i = 0,1 and n != 0 @integer 1 @decimal 0.1~1.6
+             @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+        
+            n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000
+            n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
+             @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+
+        
+
+        
+            n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
+            n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000
+             @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+
+        
+
+        
+            i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04
+            n = 2..10 @integer 2~10 @decimal 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 2.00, 3.00, 4.00, 5.00, 6.00, 7.00, 8.00
+             @integer 11~26, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~1.9, 2.1~2.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+        
+            i = 1 and v = 0 @integer 1
+            v != 0 or n = 0 or n != 1 and n % 100 = 1..19 @integer 0, 2~16, 101, 1001, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+             @integer 20~35, 100, 1000, 10000, 100000, 1000000, …
+        
+        
+            v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …
+            v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 0.2~0.4, 1.2~1.4, 2.2~2.4, 3.2~3.4, 4.2~4.4, 5.2, 10.2, 100.2, 1000.2, …
+             @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+
+        
+
+        
+            n = 1,11 @integer 1, 11 @decimal 1.0, 11.0, 1.00, 11.00, 1.000, 11.000, 1.0000
+            n = 2,12 @integer 2, 12 @decimal 2.0, 12.0, 2.00, 12.00, 2.000, 12.000, 2.0000
+            n = 3..10,13..19 @integer 3~10, 13~19 @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 3.00
+             @integer 0, 20~34, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+        
+            v = 0 and i % 100 = 1 @integer 1, 101, 201, 301, 401, 501, 601, 701, 1001, …
+            v = 0 and i % 100 = 2 @integer 2, 102, 202, 302, 402, 502, 602, 702, 1002, …
+            v = 0 and i % 100 = 3..4 or v != 0 @integer 3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+             @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …
+        
+        
+            v = 0 and i % 100 = 1 or f % 100 = 1 @integer 1, 101, 201, 301, 401, 501, 601, 701, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …
+            v = 0 and i % 100 = 2 or f % 100 = 2 @integer 2, 102, 202, 302, 402, 502, 602, 702, 1002, … @decimal 0.2, 1.2, 2.2, 3.2, 4.2, 5.2, 6.2, 7.2, 10.2, 100.2, 1000.2, …
+            v = 0 and i % 100 = 3..4 or f % 100 = 3..4 @integer 3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, … @decimal 0.3, 0.4, 1.3, 1.4, 2.3, 2.4, 3.3, 3.4, 4.3, 4.4, 5.3, 5.4, 6.3, 6.4, 7.3, 7.4, 10.3, 100.3, 1000.3, …
+             @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+
+        
+
+        
+            i = 1 and v = 0 @integer 1
+            i = 2 and v = 0 @integer 2
+            v = 0 and n != 0..10 and n % 10 = 0 @integer 20, 30, 40, 50, 60, 70, 80, 90, 100, 1000, 10000, 100000, 1000000, …
+             @integer 0, 3~17, 101, 1001, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+
+        
+
+        
+            i = 1 and v = 0 @integer 1
+            i = 2..4 and v = 0 @integer 2~4
+            v != 0   @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+             @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …
+        
+        
+            i = 1 and v = 0 @integer 1
+            v = 0 and i % 10 = 2..4 and i % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …
+            v = 0 and i != 1 and i % 10 = 0..1 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 12..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …
+               @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+        
+            n % 10 = 1 and n % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 71.0, 81.0, 101.0, 1001.0, …
+            n % 10 = 2..4 and n % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 2.0, 3.0, 4.0, 22.0, 23.0, 24.0, 32.0, 33.0, 102.0, 1002.0, …
+            n % 10 = 0 or n % 10 = 5..9 or n % 100 = 11..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+               @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, …
+        
+        
+            n % 10 = 1 and n % 100 != 11..19 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 71.0, 81.0, 101.0, 1001.0, …
+            n % 10 = 2..9 and n % 100 != 11..19 @integer 2~9, 22~29, 102, 1002, … @decimal 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 22.0, 102.0, 1002.0, …
+            f != 0   @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, …
+             @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+        
+            n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
+            n = 0 or n % 100 = 2..10 @integer 0, 2~10, 102~107, 1002, … @decimal 0.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0, 102.0, 1002.0, …
+            n % 100 = 11..19 @integer 11~19, 111~117, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, …
+             @integer 20~35, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+        
+            v = 0 and i % 10 = 1 and i % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …
+            v = 0 and i % 10 = 2..4 and i % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …
+            v = 0 and i % 10 = 0 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 11..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …
+               @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+
+        
+
+        
+            n % 10 = 1 and n % 100 != 11,71,91 @integer 1, 21, 31, 41, 51, 61, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 81.0, 101.0, 1001.0, …
+            n % 10 = 2 and n % 100 != 12,72,92 @integer 2, 22, 32, 42, 52, 62, 82, 102, 1002, … @decimal 2.0, 22.0, 32.0, 42.0, 52.0, 62.0, 82.0, 102.0, 1002.0, …
+            n % 10 = 3..4,9 and n % 100 != 10..19,70..79,90..99 @integer 3, 4, 9, 23, 24, 29, 33, 34, 39, 43, 44, 49, 103, 1003, … @decimal 3.0, 4.0, 9.0, 23.0, 24.0, 29.0, 33.0, 34.0, 103.0, 1003.0, …
+            n != 0 and n % 1000000 = 0 @integer 1000000, … @decimal 1000000.0, 1000000.00, 1000000.000, …
+             @integer 0, 5~8, 10~20, 100, 1000, 10000, 100000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, …
+        
+        
+            n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
+            n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000
+            n = 3..6 @integer 3~6 @decimal 3.0, 4.0, 5.0, 6.0, 3.00, 4.00, 5.00, 6.00, 3.000, 4.000, 5.000, 6.000, 3.0000, 4.0000, 5.0000, 6.0000
+            n = 7..10 @integer 7~10 @decimal 7.0, 8.0, 9.0, 10.0, 7.00, 8.00, 9.00, 10.00, 7.000, 8.000, 9.000, 10.000, 7.0000, 8.0000, 9.0000, 10.0000
+             @integer 0, 11~25, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+        
+            v = 0 and i % 10 = 1 @integer 1, 11, 21, 31, 41, 51, 61, 71, 101, 1001, …
+            v = 0 and i % 10 = 2 @integer 2, 12, 22, 32, 42, 52, 62, 72, 102, 1002, …
+            v = 0 and i % 100 = 0,20,40,60,80 @integer 0, 20, 40, 60, 80, 100, 120, 140, 1000, 10000, 100000, 1000000, …
+            v != 0   @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+             @integer 3~10, 13~19, 23, 103, 1003, …
+        
+
+        
+
+        
+            n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000
+            n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
+            n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000
+            n % 100 = 3..10 @integer 3~10, 103~110, 1003, … @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 103.0, 1003.0, …
+            n % 100 = 11..99 @integer 11~26, 111, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, …
+             @integer 100~102, 200~202, 300~302, 400~402, 500~502, 600, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+        
+            n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000
+            n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000
+            n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000
+            n = 3 @integer 3 @decimal 3.0, 3.00, 3.000, 3.0000
+            n = 6 @integer 6 @decimal 6.0, 6.00, 6.000, 6.0000
+             @integer 4, 5, 7~20, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+        
+    
+