Add support for files with no plural forms
This commit is contained in:
parent
5827bbda39
commit
3c9736f8c3
9 changed files with 376 additions and 13 deletions
11
CHANGELOG.txt
Normal file
11
CHANGELOG.txt
Normal 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.
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"))
|
||||
|
|
BIN
test/locale/no-plural-forms.mo
Normal file
BIN
test/locale/no-plural-forms.mo
Normal file
Binary file not shown.
255
test/locale/no-plural-forms.po
Normal file
255
test/locale/no-plural-forms.po
Normal 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 ""
|
Loading…
Reference in a new issue