From 3c9736f8c34e9194d7b113c5b1ac5b03cde0e425 Mon Sep 17 00:00:00 2001 From: Mikko Ahlroth Date: Fri, 24 May 2024 11:21:29 +0300 Subject: [PATCH] Add support for files with no plural forms --- CHANGELOG.txt | 11 ++ README.md | 6 + gleam.toml | 2 +- src/kielet.gleam | 53 +++++++ src/kielet/database.gleam | 5 +- src/kielet/language.gleam | 29 ++-- test/kielet_test.gleam | 28 ++++ test/locale/no-plural-forms.mo | Bin 0 -> 1793 bytes test/locale/no-plural-forms.po | 255 +++++++++++++++++++++++++++++++++ 9 files changed, 376 insertions(+), 13 deletions(-) create mode 100644 CHANGELOG.txt create mode 100644 test/locale/no-plural-forms.mo create mode 100644 test/locale/no-plural-forms.po diff --git a/CHANGELOG.txt b/CHANGELOG.txt new file mode 100644 index 0000000..11bcb8f --- /dev/null +++ b/CHANGELOG.txt @@ -0,0 +1,11 @@ +2.0.0 +----- + ++ Added support for translation files with no plural forms. These can be used for singular + translations. ++ Added more documentation and examples. + +1.0.0 +----- + +* Initial release. diff --git a/README.md b/README.md index 3834a7a..29a9fe7 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,12 @@ of items in a translatable string, but it's not enforced and has no special mean the translated message will contain the `%s` unchanged, and it is up to you to replace it with the appropriate number, taking into account the target locale's number format. +### Plural-Forms header + +Kielet requires a compiled translation file to contain a `Plural-Forms` header to use plurals. There is +no builtin database of plural form algorithms, so if such a header does not exist, all attempts at +translating plurals will fail. Such a file can be used to translate singular messages, however. + ## Gettext limitations Due to how Gettext is built, the source language (the language used in your source files) can only be a diff --git a/gleam.toml b/gleam.toml index 886a7fc..c6c5753 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,5 +1,5 @@ name = "kielet" -version = "1.0.0" +version = "2.0.0" # Fill out these fields if you intend to generate HTML documentation or publish # your project to the Hex package manager. diff --git a/src/kielet.gleam b/src/kielet.gleam index ac60108..d81d8d1 100644 --- a/src/kielet.gleam +++ b/src/kielet.gleam @@ -2,6 +2,22 @@ import kielet/context import kielet/database /// Translate the given singular message. +/// +/// If there is a failure to translate the message, the given message is +/// returned as-is. Causes for such a failure are: +/// +/// - if no translations have been loaded for the language, +/// - if there is no translation for this message for the language, or +/// - if the translation for this message is plural. +/// +/// Example: +/// +/// ```gleam +/// // Imported with kielet.{gettext as g_} +/// io.println( +/// g_(ctx, "Sleep tight in a new light through another warning call") +/// ) +/// ``` pub fn gettext(context: context.Context, msgid: String) -> String { database.translate_singular(context.database, msgid, context.language) } @@ -9,6 +25,43 @@ pub fn gettext(context: context.Context, msgid: String) -> String { /// Translate the given plural message. `n` is the amount of countable items /// in the message. For example for the English language, from `"%s bunny"` and /// `"%s bunnies"`, the latter would be returned when `n` is anything except 1. +/// +/// Note that this function does no replacing of any placeholder. It is only +/// convention to use `%s` in place of the amount in the message, and it will +/// not be altered by this function. Replacing of the amount is left to the +/// user. +/// +/// If there is a failure to translate the message, the given message is +/// returned, in singular or plural, using the English pluralisation rules. +/// Causes for such a failure are: +/// +/// - if no translations have been loaded for the language, +/// - if there is no translation for this message for the language, +/// - if the translation for this message is singular, +/// - if the plural form algorithm returned a form that does not exist in the +/// translation, or +/// - if the translation file did not have a `Plural-Forms` header. +/// +/// Example: +/// +/// ```gleam +/// // Imported with kielet.{ngettext as n_} +/// +/// let n = 100 +/// +/// io.println( +/// string.replace( +/// n_( +/// ctx, +/// "That's better than a rabbit", +/// "That's better than %s rabbits", +/// n +/// ), +/// "%s", +/// int.to_string(n) +/// ) +/// ) +/// ``` pub fn ngettext( context: context.Context, singular: String, diff --git a/src/kielet/database.gleam b/src/kielet/database.gleam index 4c26651..5e6e3e9 100644 --- a/src/kielet/database.gleam +++ b/src/kielet/database.gleam @@ -35,8 +35,9 @@ pub fn translate_singular(db: Database, msgid: String, language_code: String) { /// Translate a plural message using the database. /// /// If the language is not found, does not have a translation for the message, -/// or does not have the correct plural for the given `n`, the given plural -/// message is returned. +/// does not have the correct plural for the given `n`, or does not have a +/// plural forms header at all, the plural message given as the argument is +/// returned instead. pub fn translate_plural( db: Database, msgid: String, diff --git a/src/kielet/language.gleam b/src/kielet/language.gleam index 3ce67cf..565924b 100644 --- a/src/kielet/language.gleam +++ b/src/kielet/language.gleam @@ -2,6 +2,7 @@ //// used to translate messages. import gleam/dict +import gleam/option import gleam/result import kielet/mo import kielet/plurals @@ -29,13 +30,19 @@ pub type TranslateError { /// The plural algorithm returned a form that is out of bounds for the amount /// of plural forms. PluralOutOfBounds(requested: Int, but_max_is: Int) + + /// The translation file had no plural forms or the plural forms header was + /// missing. + LanguageHasNoPlurals } pub opaque type Language { Language( code: String, translations: mo.Translations, - plurals: plurals.Plurals, + /// A translation file may not have any plurals and no plural-forms header. + /// In that case this value is `None` and plural translations will fail. + plurals: option.Option(plurals.Plurals), ) } @@ -45,12 +52,13 @@ pub fn load(code: String, mo_file: BitArray) { mo.parse(mo_file) |> result.map_error(MoParseError), ) - use plurals <- result.try( - plurals.load_from_mo(mo) - |> result.map_error(PluralFormsLoadError), - ) - Ok(Language(code, mo.translations, plurals)) + case plurals.load_from_mo(mo) { + Ok(p) -> Ok(Language(code, mo.translations, option.Some(p))) + Error(plurals.NoPluralFormsHeader) -> + Ok(Language(code, mo.translations, option.None)) + Error(err) -> Error(PluralFormsLoadError(err)) + } } /// Get the language's language code. @@ -72,11 +80,12 @@ pub fn get_singular_translation(lang: Language, msgid: String) { /// Translate a plural message. pub fn get_plural_translation(lang: Language, msgid: String, n: Int) { - case dict.get(lang.translations, msgid) { - Ok(mostring) -> + case lang.plurals, dict.get(lang.translations, msgid) { + option.None, _ -> Error(LanguageHasNoPlurals) + option.Some(p), Ok(mostring) -> case mostring { mo.Plural(content: c, ..) -> { - let index = plurals.evaluate(lang.plurals, n) + let index = plurals.evaluate(p, n) case dict.get(c, index) { Ok(msg) -> Ok(msg) Error(_) -> @@ -88,6 +97,6 @@ pub fn get_plural_translation(lang: Language, msgid: String, n: Int) { } _ -> Error(MsgIsSingular(msgid)) } - _ -> Error(MsgNotFound(msgid)) + option.Some(_), _ -> Error(MsgNotFound(msgid)) } } diff --git a/test/kielet_test.gleam b/test/kielet_test.gleam index 2693b04..6cbaa7e 100644 --- a/test/kielet_test.gleam +++ b/test/kielet_test.gleam @@ -105,6 +105,34 @@ pub fn uk_plural_test() { ) } +pub fn no_plural_forms_test() { + let mo_file = "./test/locale/no-plural-forms.mo" + let assert Ok(mo_data) = simplifile.read_bits(mo_file) + let assert Ok(lang) = language.load("fi-no-plurals", mo_data) + let db = + database.new() + |> database.add_language(lang) + let ctx = context.Context(db, "fi-no-plurals") + + should.equal( + language.get_plural_translation(lang, "Wibble", 1), + Error(language.LanguageHasNoPlurals), + ) + + should.equal( + n_(ctx, "I biked one kilometre", "I biked %s kilometres", 1), + "I biked one kilometre", + ) + + should.equal( + n_(ctx, "I biked one kilometre", "I biked %s kilometres", 2), + "I biked %s kilometres", + ) + + // Singular should work + should.equal(g_(ctx, "Read more…"), "Lue lisää…") +} + fn load_languages() { database.new() |> database.add_language(load_language("fi")) diff --git a/test/locale/no-plural-forms.mo b/test/locale/no-plural-forms.mo new file mode 100644 index 0000000000000000000000000000000000000000..648d1e088bc10f4a88718eb5723a5db0d1eca64a GIT binary patch literal 1793 zcmaKrONd-W6oyM3qqeUkni!ucb#S5H_D*L8#*RWpGYJXlWSD8A3t_0~KGV0R@4eO3 zqp@v9ge(*UQBYjy!bL0S!hjoH=&ErcxO1U`h-9S;7g>l4H~y!)CW#6u?m1uo^*E>M zRP`@=XWkWPv#|SMPhBI#0{G&!aA@guLf{i~aMyzi;Jx5s@DA{K5TEG6?E+2Ieeg!; zm!m!x^=nbT1>S)1ccT6PoPqu@>W`xSBfb@m z^9RW5|BT~*gSSA>U@?A*J>X5?K9J)bgyZp9@HTJ`d;mNG-VUyV2T&^ma@^P9IPaU_ zUEq7*KJYxqb@?XpI}o4v9**b#6!nFue~bEJ)PF_IL3n?=Ky-=SAg|vK;u8nqc>Iyr zpMsAvIuAdH&US-cIAU#`~Cs?SVZE!;(pR2%cl#Kb2xXxEIFv&AIu$crO(g*ZNKv z-_3(CZgS#L6Oi91@9XZ^cP03K_-uU7T+7|CoyKpRTl^bL&U7nxEcGm#&c%{#Wx7vJ zZW=3xg(-cuEr$bAcKbhLu`hJQQs@Nd#ZhXpQL#Z*9HR|38vY+Xn@Z=@uFD}=d2!nG*``^}w7h^B&bXpFzk5nBJ!?yCY{kR-@UC=hm0Zu(R{kqGH z_3O^tOLZ7#wfXgQyFM&*2_mQ2qLxoiw3D-hjhnJA*IHKhysT)N|!6-sY~>foq*FMO;W`bx-Jp6aiXz&d3STz&$GB&6d-91dPw3?+b4Rt z(&b^<%20mtPe0c@mMmVL$GJAiCd*~Zv~fFixwxTy<%Q%pm1L)zSTWSrULLB>SLZ7t z3z^qb-wgrpr>mXEq5t=;Vt+pvr*~dkM!Fo21m%X;Zkdor&oZODKxFnTy(%Clo@JLD zR!##Fy#N3J literal 0 HcmV?d00001 diff --git a/test/locale/no-plural-forms.po b/test/locale/no-plural-forms.po new file mode 100644 index 0000000..ea0d764 --- /dev/null +++ b/test/locale/no-plural-forms.po @@ -0,0 +1,255 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR Mikko Ahlroth +# This file is distributed under the same license as the Scriptorium package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Scriptorium 2.0.0\n" +"Report-Msgid-Bugs-To: mikko@ahlroth.fi\n" +"POT-Creation-Date: 2024-05-23 23:58+0300\n" +"PO-Revision-Date: 2024-05-24 00:08+0300\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: fi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.4.4\n" + +#: src/scriptorium/parser.gleam:119 +msgid "Menu parsing failed: {err}" +msgstr "Valikon jäsentäminen epäonnistui: {err}" + +#: src/scriptorium/renderer.gleam:177 +msgid "Archives for {year}" +msgstr "Arkisto: vuosi {year}" + +#: src/scriptorium/renderer.gleam:206 +msgid "Archives for {month} {year}" +msgstr "Arkisto: {month} {year}" + +#: src/scriptorium/rendering/views/list_page.gleam:22 +msgid "Pages" +msgstr "Sivut" + +#: src/scriptorium/rendering/views/nav.gleam:20 +msgid "Previous" +msgstr "Edellinen" + +#: src/scriptorium/rendering/views/nav.gleam:38 +msgid "current page" +msgstr "tämä sivu" + +#: src/scriptorium/rendering/views/nav.gleam:64 +msgid "Next" +msgstr "Seuraava" + +#: src/scriptorium/rendering/views/single_post.gleam:51 +msgid "Tags" +msgstr "Tagit" + +#: src/scriptorium/rendering/views/single_post.gleam:74 +msgid "Read more…" +msgstr "Lue lisää…" + +#: src/scriptorium/rendering/views/single_post.gleam:92 +msgid "{date}, {time}" +msgstr "{date}, {time}" + +#: src/scriptorium/utils/date.gleam:126 +msgid "January" +msgstr "tammikuu" + +#: src/scriptorium/utils/date.gleam:127 +msgid "February" +msgstr "helmikuu" + +#: src/scriptorium/utils/date.gleam:128 +msgid "March" +msgstr "maaliskuu" + +#: src/scriptorium/utils/date.gleam:129 +msgid "April" +msgstr "huhtikuu" + +#: src/scriptorium/utils/date.gleam:130 src/scriptorium/utils/date.gleam:148 +msgid "May" +msgstr "toukokuu" + +#: src/scriptorium/utils/date.gleam:131 +msgid "June" +msgstr "kesäkuu" + +#: src/scriptorium/utils/date.gleam:132 +msgid "July" +msgstr "heinäkuu" + +#: src/scriptorium/utils/date.gleam:133 +msgid "August" +msgstr "elokuu" + +#: src/scriptorium/utils/date.gleam:134 +msgid "September" +msgstr "syyskuu" + +#: src/scriptorium/utils/date.gleam:135 +msgid "October" +msgstr "lokakuu" + +#: src/scriptorium/utils/date.gleam:136 +msgid "November" +msgstr "marraskuu" + +#: src/scriptorium/utils/date.gleam:137 +msgid "December" +msgstr "joulukuu" + +#: src/scriptorium/utils/date.gleam:144 +msgid "Jan" +msgstr "tammi" + +#: src/scriptorium/utils/date.gleam:145 +msgid "Feb" +msgstr "helmi" + +#: src/scriptorium/utils/date.gleam:146 +msgid "Mar" +msgstr "maalis" + +#: src/scriptorium/utils/date.gleam:147 +msgid "Apr" +msgstr "huhti" + +#: src/scriptorium/utils/date.gleam:149 +msgid "Jun" +msgstr "touko" + +#: src/scriptorium/utils/date.gleam:150 +msgid "Jul" +msgstr "kesä" + +#: src/scriptorium/utils/date.gleam:151 +msgid "Aug" +msgstr "heinä" + +#: src/scriptorium/utils/date.gleam:152 +msgid "Sep" +msgstr "syys" + +#: src/scriptorium/utils/date.gleam:153 +msgid "Oct" +msgstr "loka" + +#: src/scriptorium/utils/date.gleam:154 +msgid "Nov" +msgstr "marras" + +#: src/scriptorium/utils/date.gleam:155 +msgid "Dec" +msgstr "joulu" + +#: src/scriptorium/utils/date.gleam:161 +msgid "{day} {month_short_str} {year}" +msgstr "{day} {month_str} {year}" + +#: src/scriptorium/utils/time.gleam:44 +msgid "{h24_0}:{min_0}" +msgstr "{h24_0}:{min_0}" + +#: src/scriptorium/utils/time.gleam:77 +msgid "<00> am" +msgstr "" + +#: src/scriptorium/utils/time.gleam:78 +msgid "<01> am" +msgstr "" + +#: src/scriptorium/utils/time.gleam:79 +msgid "<02> am" +msgstr "" + +#: src/scriptorium/utils/time.gleam:80 +msgid "<03> am" +msgstr "" + +#: src/scriptorium/utils/time.gleam:81 +msgid "<04> am" +msgstr "" + +#: src/scriptorium/utils/time.gleam:82 +msgid "<05> am" +msgstr "" + +#: src/scriptorium/utils/time.gleam:83 +msgid "<06> am" +msgstr "" + +#: src/scriptorium/utils/time.gleam:84 +msgid "<07> am" +msgstr "" + +#: src/scriptorium/utils/time.gleam:85 +msgid "<08> am" +msgstr "" + +#: src/scriptorium/utils/time.gleam:86 +msgid "<09> am" +msgstr "" + +#: src/scriptorium/utils/time.gleam:87 +msgid "<10> am" +msgstr "" + +#: src/scriptorium/utils/time.gleam:88 +msgid "<11> am" +msgstr "" + +#: src/scriptorium/utils/time.gleam:89 +msgid "<12> pm" +msgstr "" + +#: src/scriptorium/utils/time.gleam:90 +msgid "<13> pm" +msgstr "" + +#: src/scriptorium/utils/time.gleam:91 +msgid "<14> pm" +msgstr "" + +#: src/scriptorium/utils/time.gleam:92 +msgid "<15> pm" +msgstr "" + +#: src/scriptorium/utils/time.gleam:93 +msgid "<16> pm" +msgstr "" + +#: src/scriptorium/utils/time.gleam:94 +msgid "<17> pm" +msgstr "" + +#: src/scriptorium/utils/time.gleam:95 +msgid "<18> pm" +msgstr "" + +#: src/scriptorium/utils/time.gleam:96 +msgid "<19> pm" +msgstr "" + +#: src/scriptorium/utils/time.gleam:97 +msgid "<20> pm" +msgstr "" + +#: src/scriptorium/utils/time.gleam:98 +msgid "<21> pm" +msgstr "" + +#: src/scriptorium/utils/time.gleam:99 +msgid "<22> pm" +msgstr "" + +#: src/scriptorium/utils/time.gleam:100 +msgid "<23> pm" +msgstr ""