Improve type safety of Plural.ts
authorTim Düsterhus <duesterhus@woltlab.com>
Thu, 7 Jan 2021 14:26:07 +0000 (15:26 +0100)
committerTim Düsterhus <duesterhus@woltlab.com>
Fri, 8 Jan 2021 09:33:00 +0000 (10:33 +0100)
wcfsetup/install/files/js/WoltLabSuite/Core/I18n/Plural.js
wcfsetup/install/files/ts/WoltLabSuite/Core/I18n/Plural.ts

index 042ba9f01a2386888a5193468ad6abbda631cc97..f52ac4e716d0a8629ba295f6ea317a04d65e0126 100644 (file)
 define(["require", "exports", "tslib", "../StringUtil"], function (require, exports, tslib_1, StringUtil) {
     "use strict";
     StringUtil = tslib_1.__importStar(StringUtil);
-    const PLURAL_FEW = "few";
-    const PLURAL_MANY = "many";
-    const PLURAL_ONE = "one";
-    const PLURAL_OTHER = "other";
-    const PLURAL_TWO = "two";
-    const PLURAL_ZERO = "zero";
-    const Plural = {
-        /**
-         * Returns the plural category for the given value.
-         */
-        getCategory(value, languageCode) {
-            if (!languageCode) {
-                languageCode = document.documentElement.lang;
-            }
-            // Fallback: handle unknown languages as English
-            if (typeof Plural[languageCode] !== "function") {
-                languageCode = "en";
-            }
-            const category = Plural[languageCode](value);
-            if (category) {
-                return category;
-            }
-            return PLURAL_OTHER;
-        },
-        /**
-         * Returns the value for a `plural` element used in the template.
-         *
-         * @see    wcf\system\template\plugin\PluralFunctionTemplatePlugin::execute()
-         */
-        getCategoryFromTemplateParameters(parameters) {
-            if (!parameters["value"]) {
-                throw new Error("Missing parameter value");
-            }
-            if (!parameters["other"]) {
-                throw new Error("Missing parameter other");
-            }
-            let value = parameters["value"];
-            if (Array.isArray(value)) {
-                value = value.length;
-            }
-            // handle numeric attributes
-            const numericAttribute = Object.keys(parameters).find((key) => {
-                return key.toString() === (~~key).toString() && key == value;
-            });
-            if (numericAttribute) {
-                return numericAttribute;
-            }
-            let category = Plural.getCategory(value);
-            if (!parameters[category]) {
-                category = PLURAL_OTHER;
-            }
-            const string = parameters[category];
-            if (string.indexOf("#") !== -1) {
-                return string.replace("#", StringUtil.formatNumeric(value));
-            }
-            return string;
-        },
-        /**
-         * `f` is the fractional number as a whole number (1.234 yields 234)
-         */
-        getF(n) {
-            const tmp = n.toString();
-            const pos = tmp.indexOf(".");
-            if (pos === -1) {
-                return 0;
-            }
-            return parseInt(tmp.substr(pos + 1), 10);
-        },
-        /**
-         * `v` represents the number of digits of the fractional part (1.234 yields 3)
-         */
-        getV(n) {
-            return n.toString().replace(/^[^.]*\.?/, "").length;
-        },
+    const Languages = {
         // Afrikaans
         af(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Amharic
         am(n) {
             const i = Math.floor(Math.abs(n));
             if (n == 1 || i === 0) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Arabic
         ar(n) {
             if (n == 0) {
-                return PLURAL_ZERO;
+                return "zero" /* Zero */;
             }
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
             if (n == 2) {
-                return PLURAL_TWO;
+                return "two" /* Two */;
             }
             const mod100 = n % 100;
             if (mod100 >= 3 && mod100 <= 10) {
-                return PLURAL_FEW;
+                return "few" /* Few */;
             }
             if (mod100 >= 11 && mod100 <= 99) {
-                return PLURAL_MANY;
+                return "many" /* Many */;
             }
         },
         // Assamese
         as(n) {
             const i = Math.floor(Math.abs(n));
             if (n == 1 || i === 0) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Azerbaijani
         az(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Belarusian
@@ -133,26 +60,26 @@ define(["require", "exports", "tslib", "../StringUtil"], function (require, expo
             const mod10 = n % 10;
             const mod100 = n % 100;
             if (mod10 == 1 && mod100 != 11) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
             if (mod10 >= 2 && mod10 <= 4 && !(mod100 >= 12 && mod100 <= 14)) {
-                return PLURAL_FEW;
+                return "few" /* Few */;
             }
             if (mod10 == 0 || (mod10 >= 5 && mod10 <= 9) || (mod100 >= 11 && mod100 <= 14)) {
-                return PLURAL_MANY;
+                return "many" /* Many */;
             }
         },
         // Bulgarian
         bg(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Bengali
         bn(n) {
             const i = Math.floor(Math.abs(n));
             if (n == 1 || i === 0) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Tibetan
@@ -168,54 +95,54 @@ define(["require", "exports", "tslib", "../StringUtil"], function (require, expo
             const fMod10 = f % 10;
             const fMod100 = f % 100;
             if ((v == 0 && mod10 == 1 && mod100 != 11) || (fMod10 == 1 && fMod100 != 11)) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
             if ((v == 0 && mod10 >= 2 && mod10 <= 4 && mod100 >= 12 && mod100 <= 14) ||
                 (fMod10 >= 2 && fMod10 <= 4 && fMod100 >= 12 && fMod100 <= 14)) {
-                return PLURAL_FEW;
+                return "few" /* Few */;
             }
         },
         // Czech
         cs(n) {
             const v = Plural.getV(n);
             if (n == 1 && v === 0) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
             if (n >= 2 && n <= 4 && v === 0) {
-                return PLURAL_FEW;
+                return "few" /* Few */;
             }
             if (v === 0) {
-                return PLURAL_MANY;
+                return "many" /* Many */;
             }
         },
         // Welsh
         cy(n) {
             if (n == 0) {
-                return PLURAL_ZERO;
+                return "zero" /* Zero */;
             }
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
             if (n == 2) {
-                return PLURAL_TWO;
+                return "two" /* Two */;
             }
             if (n == 3) {
-                return PLURAL_FEW;
+                return "few" /* Few */;
             }
             if (n == 6) {
-                return PLURAL_MANY;
+                return "many" /* Many */;
             }
         },
         // Danish
         da(n) {
             if (n > 0 && n < 2) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Greek
         el(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Catalan (ca)
@@ -230,71 +157,71 @@ define(["require", "exports", "tslib", "../StringUtil"], function (require, expo
         // Urdu (ur)
         en(n) {
             if (n == 1 && Plural.getV(n) === 0) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Spanish
         es(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Basque
         eu(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Persian
         fa(n) {
             if (n >= 0 && n <= 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // French
         fr(n) {
             if (n >= 0 && n < 2) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Irish
         ga(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
             if (n == 2) {
-                return PLURAL_TWO;
+                return "two" /* Two */;
             }
             if (n == 3 || n == 4 || n == 5 || n == 6) {
-                return PLURAL_FEW;
+                return "few" /* Few */;
             }
             if (n == 7 || n == 8 || n == 9 || n == 10) {
-                return PLURAL_MANY;
+                return "many" /* Many */;
             }
         },
         // Gujarati
         gu(n) {
             if (n >= 0 && n <= 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Hebrew
         he(n) {
             const v = Plural.getV(n);
             if (n == 1 && v === 0) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
             if (n == 2 && v === 0) {
-                return PLURAL_TWO;
+                return "two" /* Two */;
             }
             if (n > 10 && v === 0 && n % 10 == 0) {
-                return PLURAL_MANY;
+                return "many" /* Many */;
             }
         },
         // Hindi
         hi(n) {
             if (n >= 0 && n <= 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Croatian
@@ -305,13 +232,13 @@ define(["require", "exports", "tslib", "../StringUtil"], function (require, expo
         // Hungarian
         hu(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Armenian
         hy(n) {
             if (n >= 0 && n < 2) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Indonesian
@@ -322,7 +249,7 @@ define(["require", "exports", "tslib", "../StringUtil"], function (require, expo
         is(n) {
             const f = Plural.getF(n);
             if ((f === 0 && n % 10 === 1 && !(n % 100 === 11)) || !(f === 0)) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Japanese
@@ -336,13 +263,13 @@ define(["require", "exports", "tslib", "../StringUtil"], function (require, expo
         // Georgian
         ka(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Kazakh
         kk(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Khmer
@@ -352,7 +279,7 @@ define(["require", "exports", "tslib", "../StringUtil"], function (require, expo
         // Kannada
         kn(n) {
             if (n >= 0 && n <= 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Korean
@@ -362,19 +289,19 @@ define(["require", "exports", "tslib", "../StringUtil"], function (require, expo
         // Kurdish
         ku(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Kyrgyz
         ky(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Luxembourgish
         lb(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Lao
@@ -386,13 +313,13 @@ define(["require", "exports", "tslib", "../StringUtil"], function (require, expo
             const mod10 = n % 10;
             const mod100 = n % 100;
             if (mod10 == 1 && !(mod100 >= 11 && mod100 <= 19)) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
             if (mod10 >= 2 && mod10 <= 9 && !(mod100 >= 11 && mod100 <= 19)) {
-                return PLURAL_FEW;
+                return "few" /* Few */;
             }
             if (Plural.getF(n) != 0) {
-                return PLURAL_MANY;
+                return "many" /* Many */;
             }
         },
         // Latvian
@@ -404,10 +331,10 @@ define(["require", "exports", "tslib", "../StringUtil"], function (require, expo
             const fMod10 = f % 10;
             const fMod100 = f % 100;
             if (mod10 == 0 || (mod100 >= 11 && mod100 <= 19) || (v == 2 && fMod100 >= 11 && fMod100 <= 19)) {
-                return PLURAL_ZERO;
+                return "zero" /* Zero */;
             }
             if ((mod10 == 1 && mod100 != 11) || (v == 2 && fMod10 == 1 && fMod100 != 11) || (v != 2 && fMod10 == 1)) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Macedonian
@@ -417,19 +344,19 @@ define(["require", "exports", "tslib", "../StringUtil"], function (require, expo
         // Malayalam
         ml(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Mongolian
         mn(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Marathi
         mr(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Malay
@@ -440,13 +367,13 @@ define(["require", "exports", "tslib", "../StringUtil"], function (require, expo
         mt(n) {
             const mod100 = n % 100;
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
             if (n == 0 || (mod100 >= 2 && mod100 <= 10)) {
-                return PLURAL_FEW;
+                return "few" /* Few */;
             }
             if (mod100 >= 11 && mod100 <= 19) {
-                return PLURAL_MANY;
+                return "many" /* Many */;
             }
         },
         // Burmese
@@ -456,25 +383,25 @@ define(["require", "exports", "tslib", "../StringUtil"], function (require, expo
         // Norwegian
         no(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Nepali
         ne(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Odia
         or(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Punjabi
         pa(n) {
             if (n == 1 || n == 0) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Polish
@@ -483,26 +410,26 @@ define(["require", "exports", "tslib", "../StringUtil"], function (require, expo
             const mod10 = n % 10;
             const mod100 = n % 100;
             if (n == 1 && v == 0) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
             if (v == 0 && mod10 >= 2 && mod10 <= 4 && !(mod100 >= 12 && mod100 <= 14)) {
-                return PLURAL_FEW;
+                return "few" /* Few */;
             }
             if (v == 0 &&
                 ((n != 1 && mod10 >= 0 && mod10 <= 1) || (mod10 >= 5 && mod10 <= 9) || (mod100 >= 12 && mod100 <= 14))) {
-                return PLURAL_MANY;
+                return "many" /* Many */;
             }
         },
         // Pashto
         ps(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Portuguese
         pt(n) {
             if (n >= 0 && n < 2) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Romanian
@@ -510,10 +437,10 @@ define(["require", "exports", "tslib", "../StringUtil"], function (require, expo
             const v = Plural.getV(n);
             const mod100 = n % 100;
             if (n == 1 && v === 0) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
             if (v != 0 || n == 0 || (mod100 >= 2 && mod100 <= 19)) {
-                return PLURAL_FEW;
+                return "few" /* Few */;
             }
         },
         // Russian
@@ -522,26 +449,26 @@ define(["require", "exports", "tslib", "../StringUtil"], function (require, expo
             const mod100 = n % 100;
             if (Plural.getV(n) == 0) {
                 if (mod10 == 1 && mod100 != 11) {
-                    return PLURAL_ONE;
+                    return "one" /* One */;
                 }
                 if (mod10 >= 2 && mod10 <= 4 && !(mod100 >= 12 && mod100 <= 14)) {
-                    return PLURAL_FEW;
+                    return "few" /* Few */;
                 }
                 if (mod10 == 0 || (mod10 >= 5 && mod10 <= 9) || (mod100 >= 11 && mod100 <= 14)) {
-                    return PLURAL_MANY;
+                    return "many" /* Many */;
                 }
             }
         },
         // Sindhi
         sd(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Sinhala
         si(n) {
             if (n == 0 || n == 1 || (Math.floor(n) == 0 && Plural.getF(n) == 1)) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Slovak
@@ -554,19 +481,19 @@ define(["require", "exports", "tslib", "../StringUtil"], function (require, expo
             const v = Plural.getV(n);
             const mod100 = n % 100;
             if (v == 0 && mod100 == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
             if (v == 0 && mod100 == 2) {
-                return PLURAL_TWO;
+                return "two" /* Two */;
             }
             if ((v == 0 && (mod100 == 3 || mod100 == 4)) || v != 0) {
-                return PLURAL_FEW;
+                return "few" /* Few */;
             }
         },
         // Albanian
         sq(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Serbian
@@ -577,13 +504,13 @@ define(["require", "exports", "tslib", "../StringUtil"], function (require, expo
         // Tamil
         ta(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Telugu
         te(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Tajik
@@ -597,19 +524,19 @@ define(["require", "exports", "tslib", "../StringUtil"], function (require, expo
         // Turkmen
         tk(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Turkish
         tr(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Uyghur
         ug(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Ukrainian
@@ -620,7 +547,7 @@ define(["require", "exports", "tslib", "../StringUtil"], function (require, expo
         // Uzbek
         uz(n) {
             if (n == 1) {
-                return PLURAL_ONE;
+                return "one" /* One */;
             }
         },
         // Vietnamese
@@ -632,5 +559,73 @@ define(["require", "exports", "tslib", "../StringUtil"], function (require, expo
             return undefined;
         },
     };
+    const Plural = Object.assign({ 
+        /**
+         * Returns the plural category for the given value.
+         */
+        getCategory(value, languageCode) {
+            if (!languageCode) {
+                languageCode = document.documentElement.lang;
+            }
+            // Fallback: handle unknown languages as English
+            if (typeof Plural[languageCode] !== "function") {
+                languageCode = "en";
+            }
+            const category = Plural[languageCode](value);
+            if (category) {
+                return category;
+            }
+            return "other" /* Other */;
+        },
+        /**
+         * Returns the value for a `plural` element used in the template.
+         *
+         * @see    wcf\system\template\plugin\PluralFunctionTemplatePlugin::execute()
+         */
+        getCategoryFromTemplateParameters(parameters) {
+            if (!parameters["value"]) {
+                throw new Error("Missing parameter value");
+            }
+            if (!parameters["other"]) {
+                throw new Error("Missing parameter other");
+            }
+            let value = parameters["value"];
+            if (Array.isArray(value)) {
+                value = value.length;
+            }
+            // handle numeric attributes
+            const numericAttribute = Object.keys(parameters).find((key) => {
+                return key.toString() === (~~key).toString() && key.toString() === value.toString();
+            });
+            if (numericAttribute) {
+                return numericAttribute;
+            }
+            let category = Plural.getCategory(value);
+            if (!parameters[category]) {
+                category = "other" /* Other */;
+            }
+            const string = parameters[category];
+            if (string.indexOf("#") !== -1) {
+                return string.replace("#", StringUtil.formatNumeric(value));
+            }
+            return string;
+        },
+        /**
+         * `f` is the fractional number as a whole number (1.234 yields 234)
+         */
+        getF(n) {
+            const tmp = n.toString();
+            const pos = tmp.indexOf(".");
+            if (pos === -1) {
+                return 0;
+            }
+            return parseInt(tmp.substr(pos + 1), 10);
+        },
+        /**
+         * `v` represents the number of digits of the fractional part (1.234 yields 3)
+         */
+        getV(n) {
+            return n.toString().replace(/^[^.]*\.?/, "").length;
+        } }, Languages);
     return Plural;
 });
index b79bd944aba9730ff25bb1685c38e27366a6ff5e..2f104d889b33826f4824be3fe3c5cefd3df4d267 100644 (file)
 
 import * as StringUtil from "../StringUtil";
 
-const PLURAL_FEW = "few";
-const PLURAL_MANY = "many";
-const PLURAL_ONE = "one";
-const PLURAL_OTHER = "other";
-const PLURAL_TWO = "two";
-const PLURAL_ZERO = "zero";
-
-const Plural = {
-  /**
-   * Returns the plural category for the given value.
-   */
-  getCategory(value: number, languageCode?: string): string {
-    if (!languageCode) {
-      languageCode = document.documentElement.lang;
-    }
-
-    // Fallback: handle unknown languages as English
-    if (typeof Plural[languageCode] !== "function") {
-      languageCode = "en";
-    }
-
-    const category = Plural[languageCode](value);
-    if (category) {
-      return category;
-    }
-
-    return PLURAL_OTHER;
-  },
-
-  /**
-   * Returns the value for a `plural` element used in the template.
-   *
-   * @see    wcf\system\template\plugin\PluralFunctionTemplatePlugin::execute()
-   */
-  getCategoryFromTemplateParameters(parameters: object): string {
-    if (!parameters["value"]) {
-      throw new Error("Missing parameter value");
-    }
-    if (!parameters["other"]) {
-      throw new Error("Missing parameter other");
-    }
-
-    let value = parameters["value"];
-    if (Array.isArray(value)) {
-      value = value.length;
-    }
-
-    // handle numeric attributes
-    const numericAttribute = Object.keys(parameters).find((key) => {
-      return key.toString() === (~~key).toString() && key == value;
-    });
-
-    if (numericAttribute) {
-      return numericAttribute;
-    }
-
-    let category = Plural.getCategory(value);
-    if (!parameters[category]) {
-      category = PLURAL_OTHER;
-    }
-
-    const string = parameters[category];
-    if (string.indexOf("#") !== -1) {
-      return string.replace("#", StringUtil.formatNumeric(value));
-    }
-
-    return string;
-  },
-
-  /**
-   * `f` is the fractional number as a whole number (1.234 yields 234)
-   */
-  getF(n: number): number {
-    const tmp = n.toString();
-    const pos = tmp.indexOf(".");
-    if (pos === -1) {
-      return 0;
-    }
-
-    return parseInt(tmp.substr(pos + 1), 10);
-  },
-
-  /**
-   * `v` represents the number of digits of the fractional part (1.234 yields 3)
-   */
-  getV(n: number): number {
-    return n.toString().replace(/^[^.]*\.?/, "").length;
-  },
-
+const enum Category {
+  Few = "few",
+  Many = "many",
+  One = "one",
+  Other = "other",
+  Two = "two",
+  Zero = "zero",
+}
+
+const Languages = {
   // Afrikaans
-  af(n: number): string | undefined {
+  af(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Amharic
-  am(n: number): string | undefined {
+  am(n: number): Category | undefined {
     const i = Math.floor(Math.abs(n));
     if (n == 1 || i === 0) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Arabic
-  ar(n: number): string | undefined {
+  ar(n: number): Category | undefined {
     if (n == 0) {
-      return PLURAL_ZERO;
+      return Category.Zero;
     }
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
     if (n == 2) {
-      return PLURAL_TWO;
+      return Category.Two;
     }
 
     const mod100 = n % 100;
     if (mod100 >= 3 && mod100 <= 10) {
-      return PLURAL_FEW;
+      return Category.Few;
     }
     if (mod100 >= 11 && mod100 <= 99) {
-      return PLURAL_MANY;
+      return Category.Many;
     }
   },
 
   // Assamese
-  as(n: number): string | undefined {
+  as(n: number): Category | undefined {
     const i = Math.floor(Math.abs(n));
     if (n == 1 || i === 0) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Azerbaijani
-  az(n: number): string | undefined {
+  az(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Belarusian
-  be(n: number): string | undefined {
+  be(n: number): Category | undefined {
     const mod10 = n % 10;
     const mod100 = n % 100;
 
     if (mod10 == 1 && mod100 != 11) {
-      return PLURAL_ONE;
+      return Category.One;
     }
     if (mod10 >= 2 && mod10 <= 4 && !(mod100 >= 12 && mod100 <= 14)) {
-      return PLURAL_FEW;
+      return Category.Few;
     }
     if (mod10 == 0 || (mod10 >= 5 && mod10 <= 9) || (mod100 >= 11 && mod100 <= 14)) {
-      return PLURAL_MANY;
+      return Category.Many;
     }
   },
 
   // Bulgarian
-  bg(n: number): string | undefined {
+  bg(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Bengali
-  bn(n: number): string | undefined {
+  bn(n: number): Category | undefined {
     const i = Math.floor(Math.abs(n));
     if (n == 1 || i === 0) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Tibetan
-  bo(_n: number): string | undefined {
+  bo(_n: number): Category | undefined {
     return undefined;
   },
 
   // Bosnian
-  bs(n: number): string | undefined {
+  bs(n: number): Category | undefined {
     const v = Plural.getV(n);
     const f = Plural.getF(n);
     const mod10 = n % 10;
@@ -195,61 +116,61 @@ const Plural = {
     const fMod100 = f % 100;
 
     if ((v == 0 && mod10 == 1 && mod100 != 11) || (fMod10 == 1 && fMod100 != 11)) {
-      return PLURAL_ONE;
+      return Category.One;
     }
     if (
       (v == 0 && mod10 >= 2 && mod10 <= 4 && mod100 >= 12 && mod100 <= 14) ||
       (fMod10 >= 2 && fMod10 <= 4 && fMod100 >= 12 && fMod100 <= 14)
     ) {
-      return PLURAL_FEW;
+      return Category.Few;
     }
   },
 
   // Czech
-  cs(n: number): string | undefined {
+  cs(n: number): Category | undefined {
     const v = Plural.getV(n);
 
     if (n == 1 && v === 0) {
-      return PLURAL_ONE;
+      return Category.One;
     }
     if (n >= 2 && n <= 4 && v === 0) {
-      return PLURAL_FEW;
+      return Category.Few;
     }
     if (v === 0) {
-      return PLURAL_MANY;
+      return Category.Many;
     }
   },
 
   // Welsh
-  cy(n: number): string | undefined {
+  cy(n: number): Category | undefined {
     if (n == 0) {
-      return PLURAL_ZERO;
+      return Category.Zero;
     }
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
     if (n == 2) {
-      return PLURAL_TWO;
+      return Category.Two;
     }
     if (n == 3) {
-      return PLURAL_FEW;
+      return Category.Few;
     }
     if (n == 6) {
-      return PLURAL_MANY;
+      return Category.Many;
     }
   },
 
   // Danish
-  da(n: number): string | undefined {
+  da(n: number): Category | undefined {
     if (n > 0 && n < 2) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Greek
-  el(n: number): string | undefined {
+  el(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
@@ -263,204 +184,204 @@ const Plural = {
   // Swedish (sv)
   // Swahili (sw)
   // Urdu (ur)
-  en(n: number): string | undefined {
+  en(n: number): Category | undefined {
     if (n == 1 && Plural.getV(n) === 0) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Spanish
-  es(n: number): string | undefined {
+  es(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Basque
-  eu(n: number): string | undefined {
+  eu(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Persian
-  fa(n: number): string | undefined {
+  fa(n: number): Category | undefined {
     if (n >= 0 && n <= 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // French
-  fr(n: number): string | undefined {
+  fr(n: number): Category | undefined {
     if (n >= 0 && n < 2) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Irish
-  ga(n: number): string | undefined {
+  ga(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
     if (n == 2) {
-      return PLURAL_TWO;
+      return Category.Two;
     }
     if (n == 3 || n == 4 || n == 5 || n == 6) {
-      return PLURAL_FEW;
+      return Category.Few;
     }
     if (n == 7 || n == 8 || n == 9 || n == 10) {
-      return PLURAL_MANY;
+      return Category.Many;
     }
   },
 
   // Gujarati
-  gu(n: number): string | undefined {
+  gu(n: number): Category | undefined {
     if (n >= 0 && n <= 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Hebrew
-  he(n: number): string | undefined {
+  he(n: number): Category | undefined {
     const v = Plural.getV(n);
 
     if (n == 1 && v === 0) {
-      return PLURAL_ONE;
+      return Category.One;
     }
     if (n == 2 && v === 0) {
-      return PLURAL_TWO;
+      return Category.Two;
     }
     if (n > 10 && v === 0 && n % 10 == 0) {
-      return PLURAL_MANY;
+      return Category.Many;
     }
   },
 
   // Hindi
-  hi(n: number): string | undefined {
+  hi(n: number): Category | undefined {
     if (n >= 0 && n <= 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Croatian
-  hr(n: number): string | undefined {
+  hr(n: number): Category | undefined {
     // same as Bosnian
     return Plural.bs(n);
   },
 
   // Hungarian
-  hu(n: number): string | undefined {
+  hu(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Armenian
-  hy(n: number): string | undefined {
+  hy(n: number): Category | undefined {
     if (n >= 0 && n < 2) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Indonesian
-  id(_n: number): string | undefined {
+  id(_n: number): Category | undefined {
     return undefined;
   },
 
   // Icelandic
-  is(n: number): string | undefined {
+  is(n: number): Category | undefined {
     const f = Plural.getF(n);
 
     if ((f === 0 && n % 10 === 1 && !(n % 100 === 11)) || !(f === 0)) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Japanese
-  ja(_n: number): string | undefined {
+  ja(_n: number): Category | undefined {
     return undefined;
   },
 
   // Javanese
-  jv(_n: number): string | undefined {
+  jv(_n: number): Category | undefined {
     return undefined;
   },
 
   // Georgian
-  ka(n: number): string | undefined {
+  ka(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Kazakh
-  kk(n: number): string | undefined {
+  kk(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Khmer
-  km(_n: number): string | undefined {
+  km(_n: number): Category | undefined {
     return undefined;
   },
 
   // Kannada
-  kn(n: number): string | undefined {
+  kn(n: number): Category | undefined {
     if (n >= 0 && n <= 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Korean
-  ko(_n: number): string | undefined {
+  ko(_n: number): Category | undefined {
     return undefined;
   },
 
   // Kurdish
-  ku(n: number): string | undefined {
+  ku(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Kyrgyz
-  ky(n: number): string | undefined {
+  ky(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Luxembourgish
-  lb(n: number): string | undefined {
+  lb(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Lao
-  lo(_n: number): string | undefined {
+  lo(_n: number): Category | undefined {
     return undefined;
   },
 
   // Lithuanian
-  lt(n: number): string | undefined {
+  lt(n: number): Category | undefined {
     const mod10 = n % 10;
     const mod100 = n % 100;
 
     if (mod10 == 1 && !(mod100 >= 11 && mod100 <= 19)) {
-      return PLURAL_ONE;
+      return Category.One;
     }
     if (mod10 >= 2 && mod10 <= 9 && !(mod100 >= 11 && mod100 <= 19)) {
-      return PLURAL_FEW;
+      return Category.Few;
     }
     if (Plural.getF(n) != 0) {
-      return PLURAL_MANY;
+      return Category.Many;
     }
   },
 
   // Latvian
-  lv(n: number): string | undefined {
+  lv(n: number): Category | undefined {
     const mod10 = n % 10;
     const mod100 = n % 100;
     const v = Plural.getV(n);
@@ -469,273 +390,370 @@ const Plural = {
     const fMod100 = f % 100;
 
     if (mod10 == 0 || (mod100 >= 11 && mod100 <= 19) || (v == 2 && fMod100 >= 11 && fMod100 <= 19)) {
-      return PLURAL_ZERO;
+      return Category.Zero;
     }
     if ((mod10 == 1 && mod100 != 11) || (v == 2 && fMod10 == 1 && fMod100 != 11) || (v != 2 && fMod10 == 1)) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Macedonian
-  mk(n: number): string | undefined {
+  mk(n: number): Category | undefined {
     return Plural.bs(n);
   },
 
   // Malayalam
-  ml(n: number): string | undefined {
+  ml(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Mongolian
-  mn(n: number): string | undefined {
+  mn(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Marathi
-  mr(n: number): string | undefined {
+  mr(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Malay
-  ms(_n: number): string | undefined {
+  ms(_n: number): Category | undefined {
     return undefined;
   },
 
   // Maltese
-  mt(n: number): string | undefined {
+  mt(n: number): Category | undefined {
     const mod100 = n % 100;
 
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
     if (n == 0 || (mod100 >= 2 && mod100 <= 10)) {
-      return PLURAL_FEW;
+      return Category.Few;
     }
     if (mod100 >= 11 && mod100 <= 19) {
-      return PLURAL_MANY;
+      return Category.Many;
     }
   },
 
   // Burmese
-  my(_n: number): string | undefined {
+  my(_n: number): Category | undefined {
     return undefined;
   },
 
   // Norwegian
-  no(n: number): string | undefined {
+  no(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Nepali
-  ne(n: number): string | undefined {
+  ne(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Odia
-  or(n: number): string | undefined {
+  or(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Punjabi
-  pa(n: number): string | undefined {
+  pa(n: number): Category | undefined {
     if (n == 1 || n == 0) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Polish
-  pl(n: number): string | undefined {
+  pl(n: number): Category | undefined {
     const v = Plural.getV(n);
     const mod10 = n % 10;
     const mod100 = n % 100;
 
     if (n == 1 && v == 0) {
-      return PLURAL_ONE;
+      return Category.One;
     }
     if (v == 0 && mod10 >= 2 && mod10 <= 4 && !(mod100 >= 12 && mod100 <= 14)) {
-      return PLURAL_FEW;
+      return Category.Few;
     }
     if (
       v == 0 &&
       ((n != 1 && mod10 >= 0 && mod10 <= 1) || (mod10 >= 5 && mod10 <= 9) || (mod100 >= 12 && mod100 <= 14))
     ) {
-      return PLURAL_MANY;
+      return Category.Many;
     }
   },
 
   // Pashto
-  ps(n: number): string | undefined {
+  ps(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Portuguese
-  pt(n: number): string | undefined {
+  pt(n: number): Category | undefined {
     if (n >= 0 && n < 2) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Romanian
-  ro(n: number): string | undefined {
+  ro(n: number): Category | undefined {
     const v = Plural.getV(n);
     const mod100 = n % 100;
 
     if (n == 1 && v === 0) {
-      return PLURAL_ONE;
+      return Category.One;
     }
     if (v != 0 || n == 0 || (mod100 >= 2 && mod100 <= 19)) {
-      return PLURAL_FEW;
+      return Category.Few;
     }
   },
 
   // Russian
-  ru(n: number): string | undefined {
+  ru(n: number): Category | undefined {
     const mod10 = n % 10;
     const mod100 = n % 100;
 
     if (Plural.getV(n) == 0) {
       if (mod10 == 1 && mod100 != 11) {
-        return PLURAL_ONE;
+        return Category.One;
       }
       if (mod10 >= 2 && mod10 <= 4 && !(mod100 >= 12 && mod100 <= 14)) {
-        return PLURAL_FEW;
+        return Category.Few;
       }
       if (mod10 == 0 || (mod10 >= 5 && mod10 <= 9) || (mod100 >= 11 && mod100 <= 14)) {
-        return PLURAL_MANY;
+        return Category.Many;
       }
     }
   },
 
   // Sindhi
-  sd(n: number): string | undefined {
+  sd(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Sinhala
-  si(n: number): string | undefined {
+  si(n: number): Category | undefined {
     if (n == 0 || n == 1 || (Math.floor(n) == 0 && Plural.getF(n) == 1)) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Slovak
-  sk(n: number): string | undefined {
+  sk(n: number): Category | undefined {
     // same as Czech
     return Plural.cs(n);
   },
 
   // Slovenian
-  sl(n: number): string | undefined {
+  sl(n: number): Category | undefined {
     const v = Plural.getV(n);
     const mod100 = n % 100;
 
     if (v == 0 && mod100 == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
     if (v == 0 && mod100 == 2) {
-      return PLURAL_TWO;
+      return Category.Two;
     }
     if ((v == 0 && (mod100 == 3 || mod100 == 4)) || v != 0) {
-      return PLURAL_FEW;
+      return Category.Few;
     }
   },
 
   // Albanian
-  sq(n: number): string | undefined {
+  sq(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Serbian
-  sr(n: number): string | undefined {
+  sr(n: number): Category | undefined {
     // same as Bosnian
     return Plural.bs(n);
   },
 
   // Tamil
-  ta(n: number): string | undefined {
+  ta(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Telugu
-  te(n: number): string | undefined {
+  te(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Tajik
-  tg(_n: number): string | undefined {
+  tg(_n: number): Category | undefined {
     return undefined;
   },
 
   // Thai
-  th(_n: number): string | undefined {
+  th(_n: number): Category | undefined {
     return undefined;
   },
 
   // Turkmen
-  tk(n: number): string | undefined {
+  tk(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Turkish
-  tr(n: number): string | undefined {
+  tr(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Uyghur
-  ug(n: number): string | undefined {
+  ug(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Ukrainian
-  uk(n: number): string | undefined {
+  uk(n: number): Category | undefined {
     // same as Russian
     return Plural.ru(n);
   },
 
   // Uzbek
-  uz(n: number): string | undefined {
+  uz(n: number): Category | undefined {
     if (n == 1) {
-      return PLURAL_ONE;
+      return Category.One;
     }
   },
 
   // Vietnamese
-  vi(_n: number): string | undefined {
+  vi(_n: number): Category | undefined {
     return undefined;
   },
 
   // Chinese
-  zh(_n: number): string | undefined {
+  zh(_n: number): Category | undefined {
     return undefined;
   },
 };
 
+type ValidLanguage = keyof typeof Languages;
+
+// Note: This cannot be an interface due to the computed property.
+type Parameters = {
+  value: number;
+  other: string;
+} & {
+  [category in Category]?: string;
+} & {
+    [number: number]: string;
+  };
+
+const Plural = {
+  /**
+   * Returns the plural category for the given value.
+   */
+  getCategory(value: number, languageCode?: ValidLanguage): Category {
+    if (!languageCode) {
+      languageCode = document.documentElement.lang as ValidLanguage;
+    }
+
+    // Fallback: handle unknown languages as English
+    if (typeof Plural[languageCode] !== "function") {
+      languageCode = "en";
+    }
+
+    const category = Plural[languageCode](value);
+    if (category) {
+      return category;
+    }
+
+    return Category.Other;
+  },
+
+  /**
+   * Returns the value for a `plural` element used in the template.
+   *
+   * @see    wcf\system\template\plugin\PluralFunctionTemplatePlugin::execute()
+   */
+  getCategoryFromTemplateParameters(parameters: Parameters): string {
+    if (!parameters["value"]) {
+      throw new Error("Missing parameter value");
+    }
+    if (!parameters["other"]) {
+      throw new Error("Missing parameter other");
+    }
+
+    let value = parameters["value"];
+    if (Array.isArray(value)) {
+      value = value.length;
+    }
+
+    // handle numeric attributes
+    const numericAttribute = Object.keys(parameters).find((key) => {
+      return key.toString() === (~~key).toString() && key.toString() === value.toString();
+    });
+
+    if (numericAttribute) {
+      return numericAttribute;
+    }
+
+    let category = Plural.getCategory(value);
+    if (!parameters[category]) {
+      category = Category.Other;
+    }
+
+    const string = parameters[category]!;
+    if (string.indexOf("#") !== -1) {
+      return string.replace("#", StringUtil.formatNumeric(value));
+    }
+
+    return string;
+  },
+
+  /**
+   * `f` is the fractional number as a whole number (1.234 yields 234)
+   */
+  getF(n: number): number {
+    const tmp = n.toString();
+    const pos = tmp.indexOf(".");
+    if (pos === -1) {
+      return 0;
+    }
+
+    return parseInt(tmp.substr(pos + 1), 10);
+  },
+
+  /**
+   * `v` represents the number of digits of the fractional part (1.234 yields 3)
+   */
+  getV(n: number): number {
+    return n.toString().replace(/^[^.]*\.?/, "").length;
+  },
+
+  ...Languages,
+};
+
 export = Plural;