Add support for files with no plural forms

This commit is contained in:
Mikko Ahlroth 2024-05-24 11:21:29 +03:00
parent 5827bbda39
commit 3c9736f8c3
9 changed files with 376 additions and 13 deletions

11
CHANGELOG.txt Normal file
View file

@ -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.

View file

@ -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

View file

@ -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.

View file

@ -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,

View file

@ -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,

View file

@ -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))
}
}

View file

@ -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"))

Binary file not shown.

View file

@ -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 <EMAIL@ADDRESS>, 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 ""