diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d534149..06289b9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ We follow semantic versioning: you can tell if a version contains breaking chang Changes marked with :warning: are **breaking changes**. +## QuartzLib 0.0.4 + +_Published on April 12th, 2021_ + +### Changed + +#### Internationalization (i18n) + +- Fixed a bug where plural scripts were loaded once per translation instead of once per file. +- Optimised the plural script manager by adding many known scripts. This reduces the likelihood of + having to start a JavaScript engine, improving performance by several orders of magnitude. + ## QuartzLib 0.0.3 _Published on April 11th, 2021_ diff --git a/src/main/java/fr/zcraft/quartzlib/components/i18n/translators/gettext/GettextPOTranslator.java b/src/main/java/fr/zcraft/quartzlib/components/i18n/translators/gettext/GettextPOTranslator.java index 46369c87..fddd9897 100644 --- a/src/main/java/fr/zcraft/quartzlib/components/i18n/translators/gettext/GettextPOTranslator.java +++ b/src/main/java/fr/zcraft/quartzlib/components/i18n/translators/gettext/GettextPOTranslator.java @@ -1,5 +1,12 @@ /* - * Copyright or © or Copr. QuartzLib contributors (2015 - 2020) + * This file is part of QuartzLib. + * + * Copyright or © or Copr. ProkopyL (2015 - 2021) + * Copyright or © or Copr. Amaury Carrade (2015 – 2021) + * Copyright or © or Copr. Vlammar (2019 – 2021) + * + * This software is a computer program whose purpose is to create Minecraft mods + * with the Bukkit API easily. * * This software is governed by the CeCILL-B license under French law and * abiding by the rules of distribution of free software. You can use, diff --git a/src/main/java/fr/zcraft/quartzlib/components/i18n/translators/gettext/POFile.java b/src/main/java/fr/zcraft/quartzlib/components/i18n/translators/gettext/POFile.java index 0d8a81e5..012667ae 100644 --- a/src/main/java/fr/zcraft/quartzlib/components/i18n/translators/gettext/POFile.java +++ b/src/main/java/fr/zcraft/quartzlib/components/i18n/translators/gettext/POFile.java @@ -1,5 +1,12 @@ /* - * Copyright or © or Copr. QuartzLib contributors (2015 - 2020) + * This file is part of QuartzLib. + * + * Copyright or © or Copr. ProkopyL (2015 - 2021) + * Copyright or © or Copr. Amaury Carrade (2015 – 2021) + * Copyright or © or Copr. Vlammar (2019 – 2021) + * + * This software is a computer program whose purpose is to create Minecraft mods + * with the Bukkit API easily. * * This software is governed by the CeCILL-B license under French law and * abiding by the rules of distribution of free software. You can use, @@ -118,7 +125,7 @@ public void parse() throws CannotParsePOException { return; } - try { + try (final BufferedReader reader = rawReader) { String line; Integer lineNumber = 0; @@ -129,7 +136,7 @@ public void parse() throws CannotParsePOException { Map tokens = new HashMap<>(); String lastToken = null; - while ((line = rawReader.readLine()) != null) { + while ((line = reader.readLine()) != null) { lineNumber++; // We don't care about trailing whitespaces @@ -174,17 +181,14 @@ public void parse() throws CannotParsePOException { if (!tokens.isEmpty()) { analyseEntry(tokens); } + + // At the end we compute plural rules + pluralForms = new PluralForms(pluralCount, pluralFormScript); } catch (IOException e) { throw new CannotParsePOException("An IO exception occurred while parsing the file", e); - } finally { - try { - if (rawReader != null) { - rawReader.close(); - rawReader = null; - } - } catch (IOException ignored) { - } } + + rawReader = null; } /** @@ -314,8 +318,6 @@ private void analyseEntry(Map tokens) { } } } - - pluralForms = new PluralForms(pluralCount, pluralFormScript); } /** @@ -374,7 +376,8 @@ public String getPluralFormScript() { * If you can use them, it's always better. * *

This method can only work correctly with Plural-Forms listed at: - * http://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html#Plural-forms + * http://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html#Plural-forms, + * as well as POEdit-generated plural forms. * * @param count The count to compute plural for. * @return The plural index. diff --git a/src/main/java/fr/zcraft/quartzlib/components/i18n/translators/gettext/PluralForms.java b/src/main/java/fr/zcraft/quartzlib/components/i18n/translators/gettext/PluralForms.java index 1e9d668a..5d1cb8f0 100644 --- a/src/main/java/fr/zcraft/quartzlib/components/i18n/translators/gettext/PluralForms.java +++ b/src/main/java/fr/zcraft/quartzlib/components/i18n/translators/gettext/PluralForms.java @@ -1,35 +1,38 @@ /* - * Plugin UHCReloaded : Alliances + * This file is part of QuartzLib. * - * Copyright ou © ou Copr. Amaury Carrade (2016) - * Idées et réflexions : Alexandre Prokopowicz, Amaury Carrade, "Vayan". + * Copyright or © or Copr. ProkopyL (2015 - 2021) + * Copyright or © or Copr. Amaury Carrade (2015 – 2021) + * Copyright or © or Copr. Vlammar (2019 – 2021) * - * Ce logiciel est régi par la licence CeCILL soumise au droit français et - * respectant les principes de diffusion des logiciels libres. Vous pouvez - * utiliser, modifier et/ou redistribuer ce programme sous les conditions - * de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA - * sur le site "http://www.cecill.info". + * This software is a computer program whose purpose is to create Minecraft mods + * with the Bukkit API easily. * - * En contrepartie de l'accessibilité au code source et des droits de copie, - * de modification et de redistribution accordés par cette licence, il n'est - * offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, - * seule une responsabilité restreinte pèse sur l'auteur du programme, le - * titulaire des droits patrimoniaux et les concédants successifs. + * This software is governed by the CeCILL-B license under French law and + * abiding by the rules of distribution of free software. You can use, + * modify and/ or redistribute the software under the terms of the CeCILL-B + * license as circulated by CEA, CNRS and INRIA at the following URL + * "http://www.cecill.info". * - * A cet égard l'attention de l'utilisateur est attirée sur les risques - * associés au chargement, à l'utilisation, à la modification et/ou au - * développement et à la reproduction du logiciel par l'utilisateur étant - * donné sa spécificité de logiciel libre, qui peut le rendre complexe à - * manipuler et qui le réserve donc à des développeurs et des professionnels - * avertis possédant des connaissances informatiques approfondies. Les - * utilisateurs sont donc invités à charger et tester l'adéquation du - * logiciel à leurs besoins dans des conditions permettant d'assurer la - * sécurité de leurs systèmes et ou de leurs données et, plus généralement, - * à l'utiliser et l'exploiter dans les mêmes conditions de sécurité. + * As a counterpart to the access to the source code and rights to copy, + * modify and redistribute granted by the license, users are provided only + * with a limited warranty and the software's author, the holder of the + * economic rights, and the successive licensors have only limited + * liability. * - * Le fait que vous puissiez accéder à cet en-tête signifie que vous avez - * pris connaissance de la licence CeCILL, et que vous en avez accepté les - * termes. + * In this respect, the user's attention is drawn to the risks associated + * with loading, using, modifying and/or developing or reproducing the + * software by the user in light of its specific status of free software, + * that may mean that it is complicated to manipulate, and that also + * therefore means that it is reserved for developers and experienced + * professionals having in-depth computer knowledge. Users are therefore + * encouraged to load and test the software's suitability as regards their + * requirements in conditions enabling the security of their systems and/or + * data to be ensured and, more generally, to use and operate it in the + * same conditions as regards security. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-B license and that you accept its terms. */ package fr.zcraft.quartzlib.components.i18n.translators.gettext; @@ -83,6 +86,20 @@ public PluralForms(int formsCount, @Nullable String formsScript) { this.formsFunction = computeFormsFunction(); } + /** + * For a given number, compute the plural index to use for the locale of this file. + * + *

Some plural scripts are very commons. For them, we hardcode native functions. + * We then do not depend on a JavaScript engine, and it's order of magnitude faster. + * If you can use them, it's always better. + * + *

This method can only work correctly with Plural-Forms listed at: + * http://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html#Plural-forms, + * as well as POEdit-generated plural forms. + * + * @param count The count to compute plural for. + * @return The plural index. + */ public int computePluralForm(long count) { final int index = this.formsFunction.apply(count); @@ -93,6 +110,16 @@ public int computePluralForm(long count) { } } + /** + * Computes the function to use to compute the plural form to use for a given count. + * + *

We first try to load a pure-Java implementation from a list of known plural scripts. + * If this fail, we try two JavaScript engines to compute the form. If this fail too, we + * fallback to English rules. + * + * @return a function that can be used to compute the plural index from a count. + * @see #computePluralForm(long) Method to use the computed function. + */ private Function computeFormsFunction() { if (formsScript == null || formsScript.isEmpty()) { return formsFunctionFallback(); @@ -101,22 +128,41 @@ private Function computeFormsFunction() { // We first try if this script is known Function function = formsFunctionFromKnownScripts(); - // Else we try two JS engines, and fallback to English rules. + // Else we try two JS engines, and fallback to English rules if none of them work. if (function == null) { function = formsFunctionFromNashorn(); - } - if (function == null) { - function = formsFunctionFromGraalVM(); - } - if (function == null) { - function = formsFunctionFallback(); + + if (function == null) { + function = formsFunctionFromGraalVM(); + + if (function == null) { + function = formsFunctionFallback(); + } + } } return function; } + /** + * Normalizes a forms script so we can compare them to a list of known scripts. + * + *

The normalized version is lowercase, without spaces, and without unnecessary + * surounding parenthesis. + * + * @param script The script. + * @return The normalized version. + */ private String normalizeFormsScript(final String script) { - return script.toLowerCase(Locale.ROOT).replace(" ", "").trim(); + final String normalized = script.toLowerCase(Locale.ROOT).replace(" ", "").trim(); + + // POEdit tends to add unnecessary parenthesis around the plural scripts. For them to correctly + // match our known scripts, we strip them. + if (normalized.startsWith("(") && normalized.endsWith(")")) { + return normalized.substring(1, normalized.length() - 1); + } else { + return normalized; + } } /** @@ -124,7 +170,8 @@ private String normalizeFormsScript(final String script) { * We then do not depend on a JavaScript engine, and it's order of magnitude faster. * *

This evaluate method can only work correctly with Plural-Forms listed at: - * http://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html#Plural-forms + * http://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html#Plural-forms as + * well as POEdit-generated plural forms. * * @return a Function to compute the plural index from the given count. */ @@ -136,29 +183,46 @@ private Function formsFunctionFromKnownScripts() { return n -> 0; // Two forms, singular used for one only - // English, German, Dutch, Swedish, Danish, Norwegian, Faroese, Spanish, Portuguese, Italian, - // Greek, Bulgarian, Finnish, Estonian, Hebrew, ahasa Indonesian, Esperanto, Hungarian, Turkish + // Bahasa Indonesian, Bulgarian, Danish, Dutch, English, Esperanto, Estonian, Faroese, Finnish, German, + // Greek, Hebrew, Hungarian, Italian, Norwegian, Portuguese, Spanish, Swedish, Turkish case "n!=1": + case "n<1||n>1": // Yup. POEdit can generate this. + case "n>1||n<1": return n -> n != 1 ? 1 : 0; case "n!=0": return n -> n != 0 ? 1 : 0; - // Two forms, singular used for zero and one - // Brazilian Portuguese, French + case "n==0||n==1": + case "n==1||n==0": + return n -> n == 0 || n == 1 ? 1 : 0; + case "n>0": return n -> n > 0 ? 1 : 0; + // Two forms, singular used for zero and one + // Brazilian Portuguese, French case "n>1": return n -> n > 1 ? 1 : 0; + // Icelandic, Macedonian + case "n%10==1&&n%100!=11": + return n -> n % 10 == 1 && n % 100 != 11 ? 1 : 0; + + case "n<=1||(n>=11&&n<=99)": + return n -> n <= 1 || (n >= 11 && n <= 99) ? 1 : 0; + // Three forms, special case for zero // Latvian case "n%10==1&&n%100!=11?0:n!=0?1:2": return n -> n % 10 == 1 && n % 100 != 11 ? 0 : (n != 0 ? 1 : 2); + // Filipino, Tagalog + case "n==1||n==2||n==3||(n%10!=4&&n%10!=6&&n%10!=9)": + return n -> n == 1 || n == 2 || n == 3 || (n % 10 != 4 && n % 10 != 6 && n % 10 != 9) ? 1 : 0; + // Three forms, special cases for one and two - // Gaeilge (Irish) + // Gaeilge (Irish), Cornish, Nama, Northern Sami case "n==1?0:n==2?1:2": return n -> n == 1 ? 0 : (n == 2 ? 1 : 2); @@ -173,11 +237,17 @@ private Function formsFunctionFromKnownScripts() { return n -> n % 10 == 1 && n % 100 != 11 ? 0 : (n % 10 >= 2 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2); // Three forms, special cases for numbers ending in 1 and 2, 3, 4, except those ending in 1[1-4] - // Russian, Ukrainian, Belarusian, Serbian, Croatian + // Belarusian, Bosnian, Croatian, Russian, Serbian, Ukrainian case "n%10==1&&n%100!=11?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2": return n -> n % 10 == 1 && n % 100 != 11 ? 0 : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2); + // Alternative for the above, sometime encountered + // Belarusian, Bosnian, Croatian, Russian, Serbian, Ukrainian + case "n%10==1&&n%100!=11?0:n%10>=2&&n%10<=4&&(n%100<12||n%100>14)?1:2": + return n -> n % 10 == 1 && n % 100 != 11 ? 0 : + (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 >= 14) ? 1 : 2); + // Three forms, special cases for 1 and 2, 3, 4 // Czech, Slovak case "(n==1)?0:(n>=2&&n<=4)?1:2": @@ -190,11 +260,76 @@ private Function formsFunctionFromKnownScripts() { case "n==1?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2": return n -> n == 1 ? 0 : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2); + // Polish (alternative) + case "n==1?0:n%10>=2&&n%10<=4&&(n%100<12||n%100>14)?1:2": + return n -> n == 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : 2; + + // Colognian + case "n==0?0:n==1?1:2": + return n -> n == 0 ? 0 : n == 1 ? 1 : 2; + + // Langi + case "n==0?0:(n==0||n==1)&&n!=0?1:2": + return n -> n == 0 ? 0 : (n == 0 || n == 1) && n != 0 ? 1 : 2; + + // Lithuanian + case "n%10==1&&(n%100<11||n%100>19)?0:n%10>=2&&n%10<=9&&(n%100<11||n%100>19)?1:2": + return n -> n % 10 == 1 && (n % 100 < 11 || n % 100 > 19) ? 0 : + n % 10 >= 2 && n % 10 <= 9 && (n % 100 < 11 || n % 100 > 19) ? 1 : 2; + + // Latvian + case "n%10==0||(n%100>=11&&n%100<=19)?0:n%10==1&&n%100!=11?1:2": + return n -> n % 10 == 0 || (n % 100 >= 11 && n % 100 <= 19) ? 0 : n % 10 == 1 && n % 100 != 11 ? 1 : 2; + + // Romanian, Moldavian + case "n==1?0:n==0||(n!=1&&n%100>=1&&n%100<=19)?1:2": + return n -> n == 1 ? 0 : n == 0 || (n != 1 && n % 100 >= 1 && n % 100 <= 19) ? 1 : 2; + + case "n==0||n==1?0:n>=2&&n<=10?1:2": + return n -> n == 0 || n == 1 ? 0 : n >= 2 && n <= 10 ? 1 : 2; + // Four forms, special case for one and all numbers ending in 02, 03, or 04 // Slovenian case "n%100==1?0:n%100==2?1:n%100==3||n%100==4?2:3": return n -> n % 100 == 1 ? 0 : (n % 100 == 2 ? 1 : (n % 100 == 3 || n % 100 == 4 ? 2 : 3)); + // Slovenian (alternative), Upper Sorbian + case "n%100==1?0:n%100==2?1:n%100>=3&&n%100<=4?2:3": + return n -> n % 100 == 1 ? 0 : n % 100 == 2 ? 1 : n % 100 >= 3 && n % 100 <= 4 ? 2 : 3; + + // Manx + case "n%10==1?0:n%10==2?1:n%100==0||n%100==20||n%100==40||n%100==60||n%100==80?2:3": + return n -> n % 10 == 1 ? 0 : n % 10 == 2 ? 1 : + n % 100 == 0 || n % 100 == 20 || n % 100 == 40 || n % 100 == 60 || n % 100 == 80 ? 2 : 3; + + // Hebrew + case "n==1?0:n==2?1:n>10&&n%10==0?2:3": + return n -> n == 1 ? 0 : n == 2 ? 1 : n > 10 && n % 10 == 0 ? 2 : 3; + + // Scottish Gaelic + case "n==1||n==11?0:n==2||n==12?1:(n>=3&&n<=10)||(n>=13&&n<=19)?2:3": + return n -> n == 1 || n == 11 ? 0 : + n == 2 || n == 12 ? 1 : (n >= 3 && n <= 10) || (n >= 13 && n <= 19) ? 2 : 3; + + // Maltese + case "n==1?0:n==0||(n%100>=2&&n%100<=10)?1:n%100>=11&&n%100<=19?2:3": + return n -> n == 1 ? 0 : + n == 0 || (n % 100 >= 2 && n % 100 <= 10) ? 1 : n % 100 >= 11 && n % 100 <= 19 ? 2 : 3; + + // Breton (are you serious??) + case "n%10==1&&n%100!=11&&n%100!=71&&n%100!=91?0:n%10==2&&n%100!=12&&n%100!=72&&n%100!=92?1:((n%10>=3&&n%10" + + "<=4)||n%10==9)&&(n%100<10||n%100>19)&&(n%100<70||n%100>79)&&(n%100<90||n%100>99)?2:n!=0&&n%10000" + + "00==0?3:4": + return n -> n % 10 == 1 && n % 100 != 11 && n % 100 != 71 && n % 100 != 91 ? 0 : + n % 10 == 2 && n % 100 != 12 && n % 100 != 72 && n % 100 != 92 ? 1 : + ((n % 10 >= 3 && n % 10 <= 4) || n % 10 == 9) && (n % 100 < 10 || n % 100 > 19) + && (n % 100 < 70 || n % 100 > 79) && (n % 100 < 90 || n % 100 > 99) ? 2 : + n != 0 && n % 1000000 == 0 ? 3 : 4; + + // Irish + case "n==1?0:n==2?1:n>=3&&n<=6?2:n>=7&&n<=10?3:4": + return n -> n == 1 ? 0 : n == 2 ? 1 : n >= 3 && n <= 6 ? 2 : n >= 7 && n <= 10 ? 3 : 4; + // Six forms, special cases for one, two, all numbers ending in 02, 03, … 10, all numbers ending in 11 … 99, // and others // Arabic @@ -202,6 +337,14 @@ private Function formsFunctionFromKnownScripts() { return n -> n == 0 ? 0 : (n == 1 ? 1 : (n == 2 ? 2 : (n % 100 >= 3 && n % 100 <= 10 ? 3 : (n % 100 >= 11 ? 4 : 5)))); + case "n==0?0:n==1?1:n==2?2:n%100>=3&&n%100<=10?3:n%100>=11&&n%100<=99?4:5": + return n -> n == 0 ? 0 : + (n == 1 ? 1 : (n == 2 ? 2 : + (n % 100 >= 3 && n % 100 <= 10 ? 3 : (n % 100 >= 11 && n % 100 <= 99 ? 4 : 5)))); + + case "n==0?0:n==1?1:n==2?2:n==3?3:n==6?4:5": + return n -> n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n == 3 ? 3 : n == 6 ? 4 : 5; + default: return null; } @@ -265,10 +408,10 @@ private Function formsFunctionFromJSEngine(final String engine) { */ private Function formsFunctionFallback() { PluginLogger.warning( - "Unknown plural rule “{0}”; without JavaScript engine available, we'll fallback to English " - + "pluralization rules. If you want your language's plural rules supported without JavaScript " - + "engine, please open an issue with your language and its plural rules at " - + "https://github.com/zDevelopers/QuartzLib/issues.", + "Unknown plural rule “{0}”; without JavaScript engine available, we'll fallback to English " + + "pluralization rules. If you want your language's plural rules supported without JavaScript " + + "engine, please open an issue with your language and its plural rules at " + + "https://github.com/zDevelopers/QuartzLib/issues.", formsScript );