Add docs and tests

This commit is contained in:
Mikko Ahlroth 2024-05-21 23:39:19 +03:00
parent 44154d063d
commit ff65578d58
25 changed files with 587 additions and 184 deletions

26
LICENSE.txt Normal file
View file

@ -0,0 +1,26 @@
Copyright (c) 2024 Mikko Ahlroth
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
The plural forms tokenizer and evaluator are translated from the Expo package, used under the following
licence:
Copyright 2015 Plataformatec Copyright 2020 Dashbit 2022 JOSHMARTIN GmbH
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -1,25 +1,95 @@
# kielet
# Kielet - GNU Gettext for Gleam
[![Package Version](https://img.shields.io/hexpm/v/kielet)](https://hex.pm/packages/kielet)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/kielet/)
Kielet is a [GNU Gettext](https://www.gnu.org/software/gettext/) implementation for Gleam. With Kielet,
you can translate your Gleam or BEAM application without needing to change its source code or recompile
it. Kielet implements translation functions for singular and plural forms and an MO file parser to read
compiled translations. Gleam source code can be processed with the `xgettext` program to automatically
generate the translation templates (POT files).
## Usage
First add Kielet to your project:
```sh
gleam add kielet
```
Then you can start annotating your source code to prepare it for translation (skip the file reading and
language loading when you don't have languages yet):
```gleam
import kielet
import gleam/io
import kielet.{gettext as g_, ngettext as n_}
import kielet/context.{Context}
import kielet/database
import kielet/language
import simplifile
pub fn main() {
// TODO: An example of the project in use
// This example uses simplifile to read the MO data
let assert Ok(mo_data) = simplifile.read_bits("./path/to/fi.mo")
// Load language from MO file
let assert Ok(finnish) = language.load("fi", mo_data)
// Create language database
let db = database.new() |> database.add_language(finnish)
// Create translation context to choose active language
let ctx = Context(db, "fi")
// "Morjens, maailma!"
io.println(
g_(ctx, "Hello, world!")
)
// "Ou jee, mulla on %s euroa" -- The correct plural form is chosen based on the amount provided
io.println(
n_(ctx, "Nice, I have %s euro", "Nice, I have %s euros", 15)
)
}
```
Further documentation can be found at <https://hexdocs.pm/kielet>.
When you have annotated your code, but have no translations, the original messages will be returned.
Thus it's safe to add new strings to your program, the worst that can happen is that the original
strings will be shown.
To start translating your program, first extract the strings from your source code. You should read
the documentation of the `xgettext` program to do this, but here is an example command that works for
Gleam source code, when you have imported the functions as `g_` and `n_`:
```sh
xgettext src/**/*.gleam --keyword=g_:2 --keyword=n_:2,3 -o - --copyright-holder='Your Name' --package-name='Your App' --package-version='X.Y.Z' --msgid-bugs-address='email@example.com'
```
Store this output into a template file with the `.pot` extension and then use a translation app such as
[Poedit](https://poedit.net) to create language specific translation files. The output should be human
readable PO files that you can add to your version tracking if wanted, and binary MO files that Kielet
can read.
### String replacement
Note that Kielet does not do any string replacement. It's convention to use `%s` to denote the number
of items in a translatable string, but it's not enforced and has no special meaning. This means that
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.
## Gettext limitations
Due to how Gettext is built, the source language (the language used in your source files) can only be a
language with two plural forms. This means that there is a singular form, used when there is one item,
and a plural form that is used for every other amount of items. Unfortunately Gettext does not support
languages with more plural forms such as Arabic as the source language. They are supported as
translation targets, though.
If you do not have any usage of `ngettext` in your code, your source language can be any language. This
may be a risky choice, though, since it's quite likely you will need plural forms in the future.
## Development
```sh
gleam run # Run the project
gleam test # Run the tests
gleam shell # Run an Erlang shell
```

BIN
fi.mo

Binary file not shown.

34
fi.po
View file

@ -1,34 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: mikko@ahlroth.fi\n"
"POT-Creation-Date: 2024-05-09 14:33+0300\n"
"PO-Revision-Date: 2024-05-09 14:36+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"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.4.2\n"
#: src/kielet/tester.gleam:6
msgid "Press to activate"
msgstr "Paina aktivoidaksesi"
#: src/kielet/tester.gleam:9
#, c-format
msgid "Press to activate %s button"
msgid_plural "Press to activate %s buttons"
msgstr[0] "Paina aktivoidaksesi %s nappi"
msgstr[1] "Paina aktivoidaksesi %s nappia"
#~ msgid "foo"
#~ msgstr "Juu"

View file

@ -1,22 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-05-01 09:06+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/kielet/tester.gleam:5
msgid "foo"
msgstr ""

View file

@ -1,37 +1,25 @@
import gleam/io
import gleam/result
import kielet/database.{type Database}
import kielet/plurals/parser
import kielet/plurals/tokenizer
import nibble
import kielet/context
import kielet/database
pub fn main() {
let _ =
"nplurals=2; plural=n != 1;"
|> tokenizer.tokenize()
|> result.unwrap([])
|> nibble.run(parser.main())
|> io.debug()
"nplurals=6;
plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3
: n%100>=11 ? 4 : 5;"
|> tokenizer.tokenize()
|> result.unwrap([])
|> nibble.run(parser.main())
|> io.debug()
}
pub fn gettext(db: Database, msgid: String, language_code: String) -> String {
database.translate_singular(db, msgid, language_code)
/// Translate the given singular message.
pub fn gettext(context: context.Context, msgid: String) -> String {
database.translate_singular(context.database, msgid, context.language)
}
/// 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.
pub fn ngettext(
db: Database,
context: context.Context,
singular: String,
plural: String,
count: Int,
language_code: String,
n: Int,
) -> String {
database.translate_plural(db, singular, plural, count, language_code)
database.translate_plural(
context.database,
singular,
plural,
n,
context.language,
)
}

7
src/kielet/context.gleam Normal file
View file

@ -0,0 +1,7 @@
import kielet/database
/// Translation context, containing both the database and the active language
/// in one type for convenience.
pub type Context {
Context(database: database.Database, language: String)
}

View file

@ -1,3 +1,5 @@
//// The database contains all the loaded languages.
import gleam/dict.{type Dict}
import kielet/language.{type Language}
@ -5,14 +7,20 @@ pub opaque type Database {
Database(languages: Dict(String, Language))
}
/// Create a new empty database.
pub fn new() {
Database(languages: dict.new())
}
/// Add a language to the database.
pub fn add_language(db: Database, lang: Language) {
Database(languages: dict.insert(db.languages, language.get_code(lang), lang))
}
/// Translate a singular message using the database.
///
/// If the language is not found or does not have a translation for the message,
/// the message is returned as-is.
pub fn translate_singular(db: Database, msgid: String, language_code: String) {
case dict.get(db.languages, language_code) {
Ok(lang) ->
@ -24,26 +32,31 @@ 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.
pub fn translate_plural(
db: Database,
msgid: String,
plural: String,
count: Int,
n: Int,
language_code: String,
) {
let translations = case dict.get(db.languages, language_code) {
case dict.get(db.languages, language_code) {
Ok(lang) ->
case language.get_plural_translations(lang, msgid) {
Ok(translations) -> translations
Error(_) -> [msgid, plural]
case language.get_plural_translation(lang, msgid, n) {
Ok(translation) -> translation
Error(_) -> default_pick_plural(msgid, plural, n)
}
Error(_) -> [msgid, plural]
Error(_) -> default_pick_plural(msgid, plural, n)
}
}
let assert [singular, plural] = translations
case count {
1 -> singular
fn default_pick_plural(msgid: String, plural: String, n: Int) {
case n {
1 -> msgid
_ -> plural
}
}

View file

@ -1,34 +1,64 @@
//// A language contains translations and plural form information, and can be
//// used to translate messages.
import gleam/dict
import gleam/result
import kielet/mo
import kielet/plurals
pub type LanguageError {
/// Error returned when an MO file is loaded.
pub type LanguageLoadError {
/// The MO file could not be parsed.
MoParseError(err: mo.ParseError)
/// The plural forms in the MO file were invalid or missing.
PluralFormsLoadError(err: plurals.LoadError)
}
/// Error when trying to translate a message.
pub type TranslateError {
/// Tried to translate a plural message, but the translation is singular.
MsgIsSingular(String)
/// Tried to translate a singular message, but the translation is plural.
MsgIsPlural(String)
/// There was no translation for the message.
MsgNotFound(String)
/// The plural algorithm returned a form that is out of bounds for the amount
/// of plural forms.
PluralOutOfBounds(requested: Int, but_max_is: Int)
}
pub opaque type Language {
Language(code: String, translations: mo.Translations)
Language(
code: String,
translations: mo.Translations,
plurals: plurals.Plurals,
)
}
/// Load a language from the given MO file.
pub fn load(code: String, mo_file: BitArray) {
use mo <- result.try(
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))
Ok(Language(code, mo.translations, plurals))
}
/// Get the language's language code.
pub fn get_code(lang: Language) {
lang.code
}
/// Translate a singular message.
pub fn get_singular_translation(lang: Language, msgid: String) {
case dict.get(lang.translations, msgid) {
Ok(mostring) ->
@ -40,11 +70,22 @@ pub fn get_singular_translation(lang: Language, msgid: String) {
}
}
pub fn get_plural_translations(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 mostring {
mo.Plural(content: c, ..) -> Ok(c)
mo.Plural(content: c, ..) -> {
let index = plurals.evaluate(lang.plurals, n)
case dict.get(c, index) {
Ok(msg) -> Ok(msg)
Error(_) ->
Error(PluralOutOfBounds(
requested: index,
but_max_is: dict.size(c) - 1,
))
}
}
_ -> Error(MsgIsSingular(msgid))
}
_ -> Error(MsgNotFound(msgid))

View file

@ -1,3 +1,9 @@
//// An MO file parser.
////
//// Both little and big endian files are supported.
////
//// All strings must be UTF-8.
import gleam/bit_array
import gleam/bool
import gleam/dict.{type Dict}
@ -12,11 +18,14 @@ const eot = ""
const nul = "\u{0}"
/// A translation in the file. The context is a free-form text that can be
/// added as extra context for the translators.
pub type MoString {
Singular(context: String, content: String)
Plural(context: String, content: List(String))
Plural(context: String, content: Dict(Int, String))
}
/// Dictionary of translations keyed by the original msgid.
pub type Translations =
Dict(String, MoString)
@ -29,10 +38,18 @@ pub type ParseError {
MagicNumberNotFound
MalformedHeader
UnknownRevision(Revision)
/// An offset was given that either pointed directly out of bounds or the data
/// it pointed to would have been too big for the file.
OffsetPastEnd(Int)
MalformedOffsetTableEntry(BitArray)
StringNotUTF8(BitArray)
/// Metadata is contained as a "translation" for the msgid "" (empty string).
/// If this translation is missing, this error is returned.
MetaItemMissing
/// The metadata item is a plural.
MetaItemIsNotSingular
}
@ -43,14 +60,21 @@ pub type Revision {
pub type Header {
Header(
revision: Revision,
/// Amount of strings in file.
string_count: Int,
/// Offset to table of strings in original language.
og_table_offset: Int,
/// Offset to translations table.
trans_table_offset: Int,
/// Hash table size (not used by this parser).
ht_size: Int,
/// Hash table offset.
ht_offset: Int,
)
}
/// An MO file can contain metadata that looks quite like HTTP headers. The most
/// important data here are the plural forms with the key `Plural-Forms`.
pub type MetaData =
Dict(String, String)
@ -63,6 +87,7 @@ pub type Mo {
)
}
/// Parse given MO file data.
pub fn parse(mo: BitArray) {
use #(endianness, rest) <- result.try(parse_magic(mo))
use header <- result.try(parse_header(endianness, rest))
@ -169,8 +194,11 @@ fn parse_translations(endianness: Endianness, header: Header, mo: BitArray) {
let key = case og {
Singular(content: c, ..) -> c
Plural(content: [c, ..], ..) -> c
Plural(..) -> panic as "Got plural form with zero entries"
Plural(content: c, ..) ->
case dict.get(c, 0) {
Ok(c) -> c
Error(_) -> panic as "Got plural form with zero entries"
}
}
Ok(dict.insert(translations, key, translation))
@ -243,7 +271,13 @@ fn parse_mo_string(mo: BitArray, length: Int, offset: Int) {
case string.split(str, nul) {
[_] -> Ok(Singular(context: context, content: str))
plurals -> Ok(Plural(context: context, content: plurals))
plurals ->
Ok(Plural(
context: context,
content: plurals
|> list.index_map(fn(msg, i) { #(i, msg) })
|> dict.from_list(),
))
}
}

View file

@ -1,19 +1,44 @@
//// Utilities for plural forms.
import gleam/dict
import gleam/result
import kielet/mo
import kielet/plurals/ast
import kielet/plurals/evaluator
import kielet/plurals/parser
import kielet/plurals/syntax_error
import kielet/plurals/tokenizer
import nibble
const plural_forms_header = "Plural-Forms"
/// Error returned when tokenizing or parsing the plural forms.
pub type ParseError {
TokenizerError(err: syntax_error.SyntaxError)
ParserError(err: List(nibble.DeadEnd(tokenizer.Token, Nil)))
}
/// Error returned when loading the plural forms from an MO file.
pub type LoadError {
ParsingFailed(err: ParseError)
NoPluralFormsHeader
}
pub type Plurals {
Plurals(total: Int, algorithm: ast.Ast)
}
/// Load plural forms from a parsed MO file.
pub fn load_from_mo(mo: mo.Mo) {
use plural_header <- result.try(
dict.get(mo.metadata, plural_forms_header)
|> result.replace_error(NoPluralFormsHeader),
)
parse(plural_header)
|> result.map_error(ParsingFailed)
}
/// Tokenize and parse given plural forms string.
pub fn parse(input: String) {
use tokens <- result.try(
tokenizer.tokenize(input)
@ -25,3 +50,8 @@ pub fn parse(input: String) {
)
Ok(Plurals(total: total, algorithm: ast))
}
/// Evaluate given plurals, returning the index of the plural form to choose.
pub fn evaluate(plurals: Plurals, n: Int) {
evaluator.eval(plurals.algorithm, n)
}

View file

@ -1,13 +0,0 @@
Copyright 2015 Plataformatec Copyright 2020 Dashbit 2022 JOSHMARTIN GmbH
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -1,3 +1,6 @@
//// Plural forms abstract syntax tree
/// A binary operation (operation with two arguments)
pub type BinOp {
Equal
NotEqual
@ -11,9 +14,12 @@ pub type BinOp {
}
pub type Ast {
/// Represents the input value
N
/// Integer literal
Integer(Int)
BinaryOperation(operator: BinOp, lvalue: Ast, rvalue: Ast)
If(condition: Ast, truthy: Ast, falsy: Ast)
/// Parenthesised expression
Paren(Ast)
}

View file

@ -1,5 +1,11 @@
//// Evaluation engine for the syntax tree
// Translated from Expo evaluator
import kielet/plurals/ast
/// Evaluate the given syntax tree with the input, returning an integer value
/// representing the index of the plural form to choose.
pub fn eval(ast: ast.Ast, input: Int) {
case ast {
ast.N -> input

View file

@ -1,3 +1,7 @@
//// Parser for the plural form token stream.
////
//// The trailing semicolon in the format is optional.
import gleam/option
import kielet/plurals/ast
import kielet/plurals/tokenizer
@ -5,11 +9,12 @@ import nibble
import nibble/lexer
import nibble/pratt
/// Parse the given list of tokens into an abstract syntax tree.
pub fn parse(input: List(lexer.Token(tokenizer.Token))) {
nibble.run(input, main())
nibble.run(input, main_parser())
}
pub fn main() {
fn main_parser() {
use _ <- nibble.do(nibble.token(tokenizer.NPlurals))
use _ <- nibble.do(nibble.token(tokenizer.Assignment))
use nplurals <- nibble.do(int_parser())

View file

@ -1,5 +1,7 @@
import gleam/option
// Translated from Expo syntax error type
/// Error returned when the syntax in a plural forms string is invalid.
pub type SyntaxError {
SyntaxError(line: Int, column: option.Option(Int), reason: String)

View file

@ -1,3 +1,8 @@
//// The tokenizer converts the plural forms syntax into a list of tokens for
//// later parsing.
// Translated from Expo tokenizer
import gleam/int
import gleam/list
import gleam/option
@ -28,8 +33,9 @@ pub type Token {
Int(value: Int)
}
pub type TokenType
/// Tokenize the given plural forms syntax.
///
/// Whitespace is ignored and backslashes at the end of lines are removed.
pub fn tokenize(str: String) {
do_tokenize(string.to_graphemes(str), [], 1, 1)
}

View file

@ -1,44 +0,0 @@
import gleam/io
import gleam/string
import kielet.{gettext as g_, ngettext as n_}
import kielet/database
import kielet/language
import simplifile
pub fn main() {
let db = database.new()
let lang_code = "fi"
let assert Ok(fi) = simplifile.read_bits("fi.mo")
let assert Ok(lang) = language.load(lang_code, fi)
let db = database.add_language(db, lang)
print(db, "en")
print(db, "fi")
}
fn print(db: database.Database, lang_code: String) {
io.debug(g_(db, "Press to activate", lang_code))
io.debug(string.replace(
n_(
db,
"Press to activate %s button",
"Press to activate %s buttons",
1,
lang_code,
),
"%s",
"1",
))
io.debug(string.replace(
n_(
db,
"Press to activate %s button",
"Press to activate %s buttons",
5,
lang_code,
),
"%s",
"5",
))
}

View file

@ -1,12 +1,121 @@
import gleeunit
import gleeunit/should
import kielet.{gettext as g_, ngettext as n_}
import kielet/context
import kielet/database
import kielet/language
import simplifile
pub fn main() {
gleeunit.main()
}
// gleeunit test functions end in `_test`
pub fn hello_world_test() {
1
|> should.equal(1)
pub fn noop_singular_test() {
let context = context.Context(database: database.new(), language: "en")
should.equal(g_(context, "Hello, world!"), "Hello, world!")
}
pub fn noop_plural_singular_test() {
let context = context.Context(database: database.new(), language: "en")
should.equal(n_(context, "%s person", "%s people", 1), "%s person")
}
pub fn noop_plural_plural_test() {
let context = context.Context(database: database.new(), language: "en")
should.equal(n_(context, "%s person", "%s people", 2), "%s people")
}
pub fn wrong_language_test() {
let ctx = load_languages()
should.equal(
g_(
ctx,
"Gleam is a friendly language for building type-safe systems that scale!",
),
"Gleam is a friendly language for building type-safe systems that scale!",
)
}
pub fn fi_singular_test() {
let ctx = load_languages()
should.equal(
g_(
context.Context(..ctx, language: "fi"),
"Gleam is a friendly language for building type-safe systems that scale!",
),
"Gleam on ystävällinen kieli skaalautuvien ja tyyppiturvallisten järjestelmien rakentamiseen!",
)
}
pub fn fi_plural_test() {
let ctx = load_languages()
let ctx = context.Context(..ctx, language: "fi")
should.equal(
n_(ctx, "Oh look, it's a duck", "Oh look, it's %s ducks", 1),
"Oho kato, ankka",
)
should.equal(
n_(ctx, "Oh look, it's a duck", "Oh look, it's %s ducks", 2),
"Oho kato, %s ankkaa",
)
}
pub fn uk_plural_test() {
let ctx = load_languages()
let ctx = context.Context(..ctx, language: "uk")
should.equal(
n_(ctx, "Oh look, it's a duck", "Oh look, it's %s ducks", 1),
"там %s качка",
)
should.equal(
n_(ctx, "Oh look, it's a duck", "Oh look, it's %s ducks", 2),
"там %s качки",
)
should.equal(
n_(ctx, "Oh look, it's a duck", "Oh look, it's %s ducks", 11),
"там %s качок",
)
should.equal(
n_(ctx, "Oh look, it's a duck", "Oh look, it's %s ducks", 12),
"там %s качок",
)
should.equal(
n_(ctx, "Oh look, it's a duck", "Oh look, it's %s ducks", 5),
"там %s качок",
)
should.equal(
n_(ctx, "Oh look, it's a duck", "Oh look, it's %s ducks", 21),
"там %s качка",
)
should.equal(
n_(ctx, "Oh look, it's a duck", "Oh look, it's %s ducks", 22),
"там %s качки",
)
should.equal(
n_(ctx, "Oh look, it's a duck", "Oh look, it's %s ducks", 25),
"там %s качок",
)
}
fn load_languages() {
database.new()
|> database.add_language(load_language("fi"))
|> database.add_language(load_language("uk"))
|> context.Context("en")
}
fn load_language(lang_code: String) {
let mo_file =
"./test/locale/" <> lang_code <> "/LC_MESSAGES/" <> lang_code <> ".mo"
let assert Ok(mo_data) = simplifile.read_bits(mo_file)
let assert Ok(lang) = language.load(lang_code, mo_data)
lang
}

Binary file not shown.

View file

@ -0,0 +1,44 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR Mikko Ahlroth
# This file is distributed under the same license as the Kielet package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Kielet 1.2.3\n"
"Report-Msgid-Bugs-To: mikko@ahlroth.fi\n"
"POT-Creation-Date: 2024-05-21 22:06+0300\n"
"PO-Revision-Date: 2024-05-21 22:09+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"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.4.4\n"
#: test/kielet_test.gleam:15
msgid "Hello, world!"
msgstr "Morjens, maailma!"
#: test/kielet_test.gleam:20 test/kielet_test.gleam:25
#, c-format
msgid "%s person"
msgid_plural "%s people"
msgstr[0] "%s henkilö"
msgstr[1] "%s henkilöä"
#: test/kielet_test.gleam:33 test/kielet_test.gleam:44
#: test/kielet_test.gleam:55
msgid "Gleam is a friendly language for building type-safe systems that scale!"
msgstr "Gleam on ystävällinen kieli skaalautuvien ja tyyppiturvallisten järjestelmien rakentamiseen!"
# Muista “oho”
#: test/kielet_test.gleam:66 test/kielet_test.gleam:76
#, c-format
msgid "Oh look, it's a duck"
msgid_plural "Oh look, it's %s ducks"
msgstr[0] "Oho kato, ankka"
msgstr[1] "Oho kato, %s ankkaa"

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Kielet 1.2.3\n"
"Report-Msgid-Bugs-To: mikko@ahlroth.fi\n"
"POT-Creation-Date: 2024-05-09 18:44+0300\n"
"POT-Creation-Date: 2024-05-21 22:06+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,13 +18,25 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
#: src/kielet/tester.gleam:15
msgid "Press to activate"
#: test/kielet_test.gleam:15
msgid "Hello, world!"
msgstr ""
#: src/kielet/tester.gleam:18
#: test/kielet_test.gleam:20 test/kielet_test.gleam:25
#, c-format
msgid "Press to activate %s button"
msgid_plural "Press to activate %s buttons"
msgid "%s person"
msgid_plural "%s people"
msgstr[0] ""
msgstr[1] ""
#: test/kielet_test.gleam:33 test/kielet_test.gleam:44
#: test/kielet_test.gleam:55
msgid "Gleam is a friendly language for building type-safe systems that scale!"
msgstr ""
#: test/kielet_test.gleam:66 test/kielet_test.gleam:76
#, c-format
msgid "Oh look, it's a duck"
msgid_plural "Oh look, it's %s ducks"
msgstr[0] ""
msgstr[1] ""

Binary file not shown.

View file

@ -0,0 +1,45 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR Mikko Ahlroth
# This file is distributed under the same license as the Kielet package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Kielet 1.2.3\n"
"Report-Msgid-Bugs-To: mikko@ahlroth.fi\n"
"POT-Creation-Date: 2024-05-21 22:06+0300\n"
"PO-Revision-Date: 2024-05-21 22:13+0300\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: uk\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n"
"X-Generator: Poedit 3.4.4\n"
#: test/kielet_test.gleam:15
msgid "Hello, world!"
msgstr ""
#: test/kielet_test.gleam:20 test/kielet_test.gleam:25
#, c-format
msgid "%s person"
msgid_plural "%s people"
msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
#: test/kielet_test.gleam:33 test/kielet_test.gleam:44
#: test/kielet_test.gleam:55
msgid "Gleam is a friendly language for building type-safe systems that scale!"
msgstr ""
#: test/kielet_test.gleam:66 test/kielet_test.gleam:76
#, c-format
msgid "Oh look, it's a duck"
msgid_plural "Oh look, it's %s ducks"
msgstr[0] "там %s качка"
msgstr[1] "там %s качки"
msgstr[2] "там %s качок"

View file

@ -1,8 +1,80 @@
import gleam/dict
import gleeunit/should
import kielet/mo
import simplifile
pub fn parse_test() {
let assert Ok(data) = simplifile.read_bits("./fi.mo")
should.be_ok(mo.parse(data))
pub fn header_test() {
let mo = open()
should.equal(mo.endianness, mo.LittleEndian)
should.equal(mo.header.revision, mo.Revision(0, 0))
should.equal(mo.header.string_count, 5)
}
pub fn translations_test() {
let mo = open()
should.equal(
mo.translations,
dict.from_list([
#(
"",
mo.Singular(
"",
"Project-Id-Version: Kielet 1.2.3\nReport-Msgid-Bugs-To: mikko@ahlroth.fi\nPO-Revision-Date: 2024-05-21 22:09+0300\nLast-Translator: \nLanguage-Team: \nLanguage: fi\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\nPlural-Forms: nplurals=2; plural=(n != 1);\nX-Generator: Poedit 3.4.4\n",
),
),
#(
"%s person",
mo.Plural(
"",
dict.from_list([#(0, "%s henkilö"), #(1, "%s henkilöä")]),
),
),
#(
"Gleam is a friendly language for building type-safe systems that scale!",
mo.Singular(
"",
"Gleam on ystävällinen kieli skaalautuvien ja tyyppiturvallisten järjestelmien rakentamiseen!",
),
),
#("Hello, world!", mo.Singular("", "Morjens, maailma!")),
#(
"Oh look, it's a duck",
mo.Plural(
"",
dict.from_list([#(0, "Oho kato, ankka"), #(1, "Oho kato, %s ankkaa")]),
),
),
]),
)
}
pub fn metadata_test() {
let mo = open()
should.equal(
mo.metadata,
dict.from_list([
#("", ""),
#("Content-Transfer-Encoding", "8bit"),
#("Content-Type", "text/plain; charset=UTF-8"),
#("Language", "fi"),
#("Language-Team", ""),
#("Last-Translator", ""),
#("MIME-Version", "1.0"),
#("PO-Revision-Date", "2024-05-21 22:09+0300"),
#("Plural-Forms", "nplurals=2; plural=(n != 1);"),
#("Project-Id-Version", "Kielet 1.2.3"),
#("Report-Msgid-Bugs-To", "mikko@ahlroth.fi"),
#("X-Generator", "Poedit 3.4.4"),
]),
)
}
fn open() {
let assert Ok(mo_data) =
simplifile.read_bits("./test/locale/fi/LC_MESSAGES/fi.mo")
let mo = mo.parse(mo_data)
should.be_ok(mo)
let assert Ok(mo) = mo
mo
}