Initial commit (WIP WIP WIP)
This commit is contained in:
commit
68e0be1741
20 changed files with 1993 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
*.beam
|
||||||
|
*.ez
|
||||||
|
/build
|
||||||
|
erl_crash.dump
|
25
README.md
Normal file
25
README.md
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# kielet
|
||||||
|
|
||||||
|
[![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/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gleam add kielet
|
||||||
|
```
|
||||||
|
```gleam
|
||||||
|
import kielet
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
// TODO: An example of the project in use
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Further documentation can be found at <https://hexdocs.pm/kielet>.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gleam run # Run the project
|
||||||
|
gleam test # Run the tests
|
||||||
|
gleam shell # Run an Erlang shell
|
||||||
|
```
|
1098
expo_plural_forms_parser.erl
Normal file
1098
expo_plural_forms_parser.erl
Normal file
File diff suppressed because it is too large
Load diff
BIN
fi.mo
Normal file
BIN
fi.mo
Normal file
Binary file not shown.
34
fi.po
Normal file
34
fi.po
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# 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"
|
21
gleam.toml
Normal file
21
gleam.toml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
name = "kielet"
|
||||||
|
version = "1.0.0"
|
||||||
|
target = "javascript"
|
||||||
|
|
||||||
|
# Fill out these fields if you intend to generate HTML documentation or publish
|
||||||
|
# your project to the Hex package manager.
|
||||||
|
#
|
||||||
|
# description = ""
|
||||||
|
# licences = ["Apache-2.0"]
|
||||||
|
# repository = { type = "github", user = "username", repo = "project" }
|
||||||
|
# links = [{ title = "Website", href = "https://gleam.run" }]
|
||||||
|
#
|
||||||
|
# For a full reference of all the available options, you can have a look at
|
||||||
|
# https://gleam.run/writing-gleam/gleam-toml/.
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gleam_stdlib = ">= 0.34.0 and < 2.0.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
gleeunit = ">= 1.0.0 and < 2.0.0"
|
||||||
|
simplifile = ">= 1.7.0 and < 2.0.0"
|
14
manifest.toml
Normal file
14
manifest.toml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# This file was generated by Gleam
|
||||||
|
# You typically do not need to edit this file
|
||||||
|
|
||||||
|
packages = [
|
||||||
|
{ name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
|
||||||
|
{ name = "gleam_stdlib", version = "0.37.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "5398BD6C2ABA17338F676F42F404B9B7BABE1C8DC7380031ACB05BBE1BCF3742" },
|
||||||
|
{ name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" },
|
||||||
|
{ name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[requirements]
|
||||||
|
gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" }
|
||||||
|
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
|
||||||
|
simplifile = { version = ">= 1.7.0 and < 2.0.0" }
|
22
messages.po
Normal file
22
messages.po
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# 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 ""
|
30
messages.pot
Normal file
30
messages.pot
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# 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-09 18:44+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"
|
||||||
|
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
|
||||||
|
|
||||||
|
#: src/kielet/tester.gleam:15
|
||||||
|
msgid "Press to activate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/kielet/tester.gleam:18
|
||||||
|
#, c-format
|
||||||
|
msgid "Press to activate %s button"
|
||||||
|
msgid_plural "Press to activate %s buttons"
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
20
src/kielet.gleam
Normal file
20
src/kielet.gleam
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import gleam/io
|
||||||
|
import kielet/database.{type Database}
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
io.println("Hello from kielet!")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gettext(db: Database, msgid: String, language_code: String) -> String {
|
||||||
|
database.translate_singular(db, msgid, language_code)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ngettext(
|
||||||
|
db: Database,
|
||||||
|
singular: String,
|
||||||
|
plural: String,
|
||||||
|
count: Int,
|
||||||
|
language_code: String,
|
||||||
|
) -> String {
|
||||||
|
database.translate_plural(db, singular, plural, count, language_code)
|
||||||
|
}
|
49
src/kielet/database.gleam
Normal file
49
src/kielet/database.gleam
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import gleam/dict.{type Dict}
|
||||||
|
import kielet/language.{type Language}
|
||||||
|
|
||||||
|
pub opaque type Database {
|
||||||
|
Database(languages: Dict(String, Language))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new() {
|
||||||
|
Database(languages: dict.new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_language(db: Database, lang: Language) {
|
||||||
|
Database(languages: dict.insert(db.languages, language.get_code(lang), lang))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn translate_singular(db: Database, msgid: String, language_code: String) {
|
||||||
|
case dict.get(db.languages, language_code) {
|
||||||
|
Ok(lang) ->
|
||||||
|
case language.get_singular_translation(lang, msgid) {
|
||||||
|
Ok(translation) -> translation
|
||||||
|
Error(_) -> msgid
|
||||||
|
}
|
||||||
|
Error(_) -> msgid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn translate_plural(
|
||||||
|
db: Database,
|
||||||
|
msgid: String,
|
||||||
|
plural: String,
|
||||||
|
count: Int,
|
||||||
|
language_code: String,
|
||||||
|
) {
|
||||||
|
let translations = case dict.get(db.languages, language_code) {
|
||||||
|
Ok(lang) ->
|
||||||
|
case language.get_plural_translations(lang, msgid) {
|
||||||
|
Ok(translations) -> translations
|
||||||
|
Error(_) -> [msgid, plural]
|
||||||
|
}
|
||||||
|
Error(_) -> [msgid, plural]
|
||||||
|
}
|
||||||
|
|
||||||
|
let assert [singular, plural] = translations
|
||||||
|
|
||||||
|
case count {
|
||||||
|
1 -> singular
|
||||||
|
_ -> plural
|
||||||
|
}
|
||||||
|
}
|
52
src/kielet/language.gleam
Normal file
52
src/kielet/language.gleam
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import gleam/dict
|
||||||
|
import gleam/result
|
||||||
|
import kielet/mo
|
||||||
|
|
||||||
|
pub type LanguageError {
|
||||||
|
MoParseError(err: mo.ParseError)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type TranslateError {
|
||||||
|
MsgIsSingular(String)
|
||||||
|
MsgIsPlural(String)
|
||||||
|
MsgNotFound(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub opaque type Language {
|
||||||
|
Language(code: String, translations: mo.Translations)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(code: String, mo_file: BitArray) {
|
||||||
|
use mo <- result.try(
|
||||||
|
mo.parse(mo_file)
|
||||||
|
|> result.map_error(MoParseError),
|
||||||
|
)
|
||||||
|
|
||||||
|
Ok(Language(code, mo.translations))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_code(lang: Language) {
|
||||||
|
lang.code
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_singular_translation(lang: Language, msgid: String) {
|
||||||
|
case dict.get(lang.translations, msgid) {
|
||||||
|
Ok(mostring) ->
|
||||||
|
case mostring {
|
||||||
|
mo.Singular(content: c, ..) -> Ok(c)
|
||||||
|
_ -> Error(MsgIsPlural(msgid))
|
||||||
|
}
|
||||||
|
_ -> Error(MsgNotFound(msgid))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_plural_translations(lang: Language, msgid: String) {
|
||||||
|
case dict.get(lang.translations, msgid) {
|
||||||
|
Ok(mostring) ->
|
||||||
|
case mostring {
|
||||||
|
mo.Plural(content: c, ..) -> Ok(c)
|
||||||
|
_ -> Error(MsgIsSingular(msgid))
|
||||||
|
}
|
||||||
|
_ -> Error(MsgNotFound(msgid))
|
||||||
|
}
|
||||||
|
}
|
253
src/kielet/mo.gleam
Normal file
253
src/kielet/mo.gleam
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
import gleam/bit_array
|
||||||
|
import gleam/bool
|
||||||
|
import gleam/dict.{type Dict}
|
||||||
|
import gleam/int
|
||||||
|
import gleam/list
|
||||||
|
import gleam/result
|
||||||
|
import gleam/string
|
||||||
|
|
||||||
|
const max_supported_major = 0
|
||||||
|
|
||||||
|
const eot = ""
|
||||||
|
|
||||||
|
const nul = "\u{0}"
|
||||||
|
|
||||||
|
pub type MoString {
|
||||||
|
Singular(context: String, content: String)
|
||||||
|
Plural(context: String, content: List(String))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Translations =
|
||||||
|
Dict(String, MoString)
|
||||||
|
|
||||||
|
pub type Endianness {
|
||||||
|
BigEndian
|
||||||
|
LittleEndian
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type ParseError {
|
||||||
|
MagicNumberNotFound
|
||||||
|
MalformedHeader
|
||||||
|
UnknownRevision(Revision)
|
||||||
|
OffsetPastEnd(Int)
|
||||||
|
MalformedOffsetTableEntry(BitArray)
|
||||||
|
StringNotUTF8(BitArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Revision {
|
||||||
|
Revision(major: Int, minor: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Header {
|
||||||
|
Header(
|
||||||
|
revision: Revision,
|
||||||
|
string_count: Int,
|
||||||
|
og_table_offset: Int,
|
||||||
|
trans_table_offset: Int,
|
||||||
|
ht_size: Int,
|
||||||
|
ht_offset: Int,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Mo {
|
||||||
|
Mo(endianness: Endianness, header: Header, translations: Translations)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(mo: BitArray) {
|
||||||
|
use #(endianness, rest) <- result.try(parse_magic(mo))
|
||||||
|
use header <- result.try(parse_header(endianness, rest))
|
||||||
|
use <- bool.guard(
|
||||||
|
header.revision.major > max_supported_major,
|
||||||
|
Error(UnknownRevision(header.revision)),
|
||||||
|
)
|
||||||
|
use <- bool.guard(
|
||||||
|
header.string_count == 0,
|
||||||
|
Ok(Mo(endianness: endianness, header: header, translations: dict.new())),
|
||||||
|
)
|
||||||
|
|
||||||
|
let total_size = bit_array.byte_size(mo)
|
||||||
|
|
||||||
|
use <- bool.guard(
|
||||||
|
header.og_table_offset >= total_size,
|
||||||
|
Error(OffsetPastEnd(header.og_table_offset)),
|
||||||
|
)
|
||||||
|
use <- bool.guard(
|
||||||
|
header.trans_table_offset >= total_size,
|
||||||
|
Error(OffsetPastEnd(header.trans_table_offset)),
|
||||||
|
)
|
||||||
|
use <- bool.guard(
|
||||||
|
header.ht_offset >= total_size,
|
||||||
|
Error(OffsetPastEnd(header.ht_offset)),
|
||||||
|
)
|
||||||
|
|
||||||
|
use translations <- result.try(parse_translations(endianness, header, mo))
|
||||||
|
|
||||||
|
Ok(Mo(endianness: endianness, header: header, translations: translations))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_magic(body: BitArray) {
|
||||||
|
case body {
|
||||||
|
<<0xde120495:size(32), rest:bytes>> -> Ok(#(LittleEndian, rest))
|
||||||
|
<<0x950412de:size(32), rest:bytes>> -> Ok(#(BigEndian, rest))
|
||||||
|
_ -> Error(MagicNumberNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_header(endianness: Endianness, body: BitArray) {
|
||||||
|
case endianness {
|
||||||
|
LittleEndian -> {
|
||||||
|
case body {
|
||||||
|
<<
|
||||||
|
major_bytes:bytes-size(2),
|
||||||
|
minor_bytes:bytes-size(2),
|
||||||
|
string_count_bytes:bytes-size(4),
|
||||||
|
og_table_offset_bytes:bytes-size(4),
|
||||||
|
trans_table_offset_bytes:bytes-size(4),
|
||||||
|
ht_size_bytes:bytes-size(4),
|
||||||
|
ht_offset_bytes:bytes-size(4),
|
||||||
|
_rest:bytes,
|
||||||
|
>> -> {
|
||||||
|
let assert Ok(major) = le_int_8(major_bytes)
|
||||||
|
let assert Ok(minor) = le_int_8(minor_bytes)
|
||||||
|
let assert Ok(string_count) = le_int_32(string_count_bytes)
|
||||||
|
let assert Ok(og_table_offset) = le_int_32(og_table_offset_bytes)
|
||||||
|
let assert Ok(trans_table_offset) =
|
||||||
|
le_int_32(trans_table_offset_bytes)
|
||||||
|
let assert Ok(ht_size) = le_int_32(ht_size_bytes)
|
||||||
|
let assert Ok(ht_offset) = le_int_32(ht_offset_bytes)
|
||||||
|
Ok(Header(
|
||||||
|
Revision(major, minor),
|
||||||
|
string_count,
|
||||||
|
og_table_offset,
|
||||||
|
trans_table_offset,
|
||||||
|
ht_size,
|
||||||
|
ht_offset,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
_ -> Error(MalformedHeader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BigEndian -> panic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_translations(endianness: Endianness, header: Header, mo: BitArray) {
|
||||||
|
let strings = list.range(0, header.string_count - 1)
|
||||||
|
use translations <- result.try(
|
||||||
|
list.try_fold(strings, dict.new(), fn(translations, i) {
|
||||||
|
let new_offset = i * 8
|
||||||
|
let og_offset = header.og_table_offset + new_offset
|
||||||
|
let trans_offset = header.trans_table_offset + new_offset
|
||||||
|
use #(og, translation) <- result.try(parse_translation(
|
||||||
|
endianness,
|
||||||
|
mo,
|
||||||
|
og_offset,
|
||||||
|
trans_offset,
|
||||||
|
))
|
||||||
|
|
||||||
|
let key = case og {
|
||||||
|
Singular(content: c, ..) -> c
|
||||||
|
Plural(content: [c, ..], ..) -> c
|
||||||
|
Plural(..) -> panic as "Got plural form with zero entries"
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(dict.insert(translations, key, translation))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
Ok(translations)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_translation(
|
||||||
|
endianness: Endianness,
|
||||||
|
mo: BitArray,
|
||||||
|
og_offset: Int,
|
||||||
|
trans_offset: Int,
|
||||||
|
) {
|
||||||
|
use #(og_str_length, og_str_offset) <- result.try(parse_offset_table_entry(
|
||||||
|
endianness,
|
||||||
|
mo,
|
||||||
|
og_offset,
|
||||||
|
))
|
||||||
|
use #(trans_str_length, trans_str_offset) <- result.try(
|
||||||
|
parse_offset_table_entry(endianness, mo, trans_offset),
|
||||||
|
)
|
||||||
|
|
||||||
|
use og_string <- result.try(parse_mo_string(mo, og_str_length, og_str_offset))
|
||||||
|
use trans_string <- result.try(parse_mo_string(
|
||||||
|
mo,
|
||||||
|
trans_str_length,
|
||||||
|
trans_str_offset,
|
||||||
|
))
|
||||||
|
|
||||||
|
Ok(#(og_string, trans_string))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_offset_table_entry(endianness: Endianness, mo: BitArray, offset: Int) {
|
||||||
|
use data <- result.try(
|
||||||
|
bit_array.slice(mo, offset, 8)
|
||||||
|
|> result.replace_error(OffsetPastEnd(offset)),
|
||||||
|
)
|
||||||
|
case endianness {
|
||||||
|
LittleEndian ->
|
||||||
|
case data {
|
||||||
|
<<target_length:bytes-size(4), target_offset:bytes-size(4)>> -> {
|
||||||
|
let assert Ok(target_length) = le_int_32(target_length)
|
||||||
|
let assert Ok(target_offset) = le_int_32(target_offset)
|
||||||
|
Ok(#(target_length, target_offset))
|
||||||
|
}
|
||||||
|
_ -> Error(MalformedOffsetTableEntry(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
BigEndian -> panic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_mo_string(mo: BitArray, length: Int, offset: Int) {
|
||||||
|
use data <- result.try(
|
||||||
|
bit_array.slice(mo, offset, length)
|
||||||
|
|> result.replace_error(OffsetPastEnd(offset)),
|
||||||
|
)
|
||||||
|
|
||||||
|
use str <- result.try(
|
||||||
|
bit_array.to_string(data)
|
||||||
|
|> result.replace_error(StringNotUTF8(data)),
|
||||||
|
)
|
||||||
|
|
||||||
|
let #(context, str) = case string.split_once(str, eot) {
|
||||||
|
Ok(#(c, s)) -> #(c, s)
|
||||||
|
Error(_) -> #("", str)
|
||||||
|
}
|
||||||
|
|
||||||
|
case string.split(str, nul) {
|
||||||
|
[_] -> Ok(Singular(context: context, content: str))
|
||||||
|
plurals -> Ok(Plural(context: context, content: plurals))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn le_int_8(int8: BitArray) {
|
||||||
|
case int8 {
|
||||||
|
<<l:size(8), h:size(8)>> -> Ok(reconstruct_ui8(h, l))
|
||||||
|
_ -> Error(Nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn le_int_32(int32: BitArray) {
|
||||||
|
case int32 {
|
||||||
|
<<ll:size(8), lh:size(8), hl:size(8), hh:size(8)>> ->
|
||||||
|
Ok(reconstruct_ui32(hh, hl, lh, ll))
|
||||||
|
_ -> Error(Nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reconstruct_ui8(h: Int, l: Int) {
|
||||||
|
int.bitwise_shift_left(h, 8) + l
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reconstruct_ui32(hh: Int, hl: Int, lh: Int, ll: Int) {
|
||||||
|
int.bitwise_shift_left(hh, 8 * 3)
|
||||||
|
+ int.bitwise_shift_left(hl, 8 * 2)
|
||||||
|
+ int.bitwise_shift_left(lh, 8)
|
||||||
|
+ ll
|
||||||
|
}
|
13
src/kielet/plurals/LICENSE.txt
Normal file
13
src/kielet/plurals/LICENSE.txt
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
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.
|
148
src/kielet/plurals/parser.notgleam
Normal file
148
src/kielet/plurals/parser.notgleam
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
import gleam/result
|
||||||
|
import kielet/plurals/syntax_error
|
||||||
|
import kielet/plurals/tokenizer
|
||||||
|
|
||||||
|
pub type ParseError {
|
||||||
|
SyntaxError(err: syntax_error.SyntaxError)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type PluralForms {
|
||||||
|
PluralForms(nplurals: Int, ast: Ast)
|
||||||
|
}
|
||||||
|
|
||||||
|
type BinOp {
|
||||||
|
Equal
|
||||||
|
NotEqual
|
||||||
|
GreaterThan
|
||||||
|
GreaterThanOrEqual
|
||||||
|
LowerThan
|
||||||
|
LowerThanOrEqual
|
||||||
|
Remainder
|
||||||
|
And
|
||||||
|
Or
|
||||||
|
}
|
||||||
|
|
||||||
|
pub opaque type Ast {
|
||||||
|
N
|
||||||
|
Integer(Int)
|
||||||
|
BinaryOperation(operator: BinOp, lvalue: Ast, rvalue: Ast)
|
||||||
|
If(condition: Ast, truthy: Ast, falsy: Ast)
|
||||||
|
Paren(Ast)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(rules: String) {
|
||||||
|
use tokens <- result.try(
|
||||||
|
tokenizer.tokenize(rules)
|
||||||
|
|> result.map_error(SyntaxError),
|
||||||
|
)
|
||||||
|
|
||||||
|
do_parse(tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval_ast(ast: Ast, input: Int) {
|
||||||
|
case ast {
|
||||||
|
N -> input
|
||||||
|
Integer(i) -> i
|
||||||
|
If(condition, truthy, falsy) -> {
|
||||||
|
let ast = case eval_ast(condition, input) {
|
||||||
|
1 -> truthy
|
||||||
|
_ -> falsy
|
||||||
|
}
|
||||||
|
eval_ast(ast, input)
|
||||||
|
}
|
||||||
|
Paren(content) -> eval_ast(content, input)
|
||||||
|
BinaryOperation(operator, lvalue, rvalue) -> {
|
||||||
|
let lvalue = eval_ast(lvalue, input)
|
||||||
|
let rvalue = eval_ast(rvalue, input)
|
||||||
|
case operator {
|
||||||
|
Equal -> bool_to_int(lvalue == rvalue)
|
||||||
|
NotEqual -> bool_to_int(lvalue != rvalue)
|
||||||
|
GreaterThan -> bool_to_int(lvalue > rvalue)
|
||||||
|
GreaterThanOrEqual -> bool_to_int(lvalue >= rvalue)
|
||||||
|
LowerThan -> bool_to_int(lvalue < rvalue)
|
||||||
|
LowerThanOrEqual -> bool_to_int(lvalue <= rvalue)
|
||||||
|
Remainder -> lvalue % rvalue
|
||||||
|
And -> bool_to_int(lvalue == 1 && rvalue == 1)
|
||||||
|
Or -> bool_to_int(lvalue == 1 || rvalue == 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bool_to_int(bool: Bool) {
|
||||||
|
case bool {
|
||||||
|
True -> 1
|
||||||
|
False -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Location {
|
||||||
|
NoLocation
|
||||||
|
Line(Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_parse(tokens: List(tokenizer.Token)) {
|
||||||
|
yeccpars0(tokens, NoLocation, 0, [], [])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn yeccpars0(
|
||||||
|
tokens: List(tokenizer.Token),
|
||||||
|
location: Location,
|
||||||
|
state: Int,
|
||||||
|
states: List(Int),
|
||||||
|
vstack: List(tokenizer.Token),
|
||||||
|
) {
|
||||||
|
yeccpars1(tokens, location, state, states, vstack)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn yeccpars1(
|
||||||
|
tokens: List(tokenizer.Token),
|
||||||
|
location: Location,
|
||||||
|
state: Int,
|
||||||
|
states: List(Int),
|
||||||
|
vstack: List(tokenizer.Token),
|
||||||
|
) {
|
||||||
|
case tokens {
|
||||||
|
[token, ..rest] ->
|
||||||
|
yeccpars2(state, token, states, vstack, token, rest, location)
|
||||||
|
[] ->
|
||||||
|
case location {
|
||||||
|
NoLocation ->
|
||||||
|
yeccpars2(
|
||||||
|
state,
|
||||||
|
tokenizer.End,
|
||||||
|
states,
|
||||||
|
vstack,
|
||||||
|
#(tokenizer.End, 999_999),
|
||||||
|
[],
|
||||||
|
999_999,
|
||||||
|
)
|
||||||
|
Line(line) ->
|
||||||
|
yeccpars2(
|
||||||
|
state,
|
||||||
|
tokenizer.End,
|
||||||
|
states,
|
||||||
|
vstac,
|
||||||
|
#(tokenizer.End, line),
|
||||||
|
[],
|
||||||
|
line,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn yeccpars1_2(state1, state, states, vstack, token0, tokens, location) {
|
||||||
|
case tokens {
|
||||||
|
[token, ..rest] ->
|
||||||
|
yeccpars2(
|
||||||
|
state,
|
||||||
|
token,
|
||||||
|
[state1, ..states],
|
||||||
|
[token0, ..vstack],
|
||||||
|
token,
|
||||||
|
rest,
|
||||||
|
location,
|
||||||
|
)
|
||||||
|
[] -> yeccpars2()
|
||||||
|
}
|
||||||
|
}
|
6
src/kielet/plurals/syntax_error.gleam
Normal file
6
src/kielet/plurals/syntax_error.gleam
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import gleam/option
|
||||||
|
|
||||||
|
/// Error returned when the syntax in a plural forms string is invalid.
|
||||||
|
pub type SyntaxError {
|
||||||
|
SyntaxError(line: Int, column: option.Option(Int), reason: String)
|
||||||
|
}
|
140
src/kielet/plurals/tokenizer.gleam
Normal file
140
src/kielet/plurals/tokenizer.gleam
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
import gleam/int
|
||||||
|
import gleam/list
|
||||||
|
import gleam/option
|
||||||
|
import gleam/string
|
||||||
|
import kielet/plurals/syntax_error.{SyntaxError}
|
||||||
|
|
||||||
|
pub type Token {
|
||||||
|
Token(type_: TokenType, line: Int)
|
||||||
|
Int(value: Int, line: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type TokenType {
|
||||||
|
N
|
||||||
|
NPlurals
|
||||||
|
Plural
|
||||||
|
Equals
|
||||||
|
NotEquals
|
||||||
|
GreaterThanOrEquals
|
||||||
|
LowerThanOrEquals
|
||||||
|
GreaterThan
|
||||||
|
LowerThan
|
||||||
|
Assignment
|
||||||
|
Ternary
|
||||||
|
TernaryElse
|
||||||
|
Remainder
|
||||||
|
Or
|
||||||
|
And
|
||||||
|
Semicolon
|
||||||
|
LParen
|
||||||
|
RParen
|
||||||
|
End
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tokenize(str: String) {
|
||||||
|
do_tokenize(string.to_graphemes(str), [], 1, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_tokenize(str: List(String), acc: List(Token), line: Int, col: Int) {
|
||||||
|
case str {
|
||||||
|
[] -> Ok(list.reverse([Token(End, line), ..acc]))
|
||||||
|
["n", "p", "l", "u", "r", "a", "l", "s", ..rest] ->
|
||||||
|
do_tokenize(rest, [Token(NPlurals, line), ..acc], line, col + 8)
|
||||||
|
["p", "l", "u", "r", "a", "l", ..rest] ->
|
||||||
|
do_tokenize(rest, [Token(Plural, line), ..acc], line, col + 6)
|
||||||
|
["n", ..rest] -> do_tokenize(rest, [Token(N, line), ..acc], line, col + 1)
|
||||||
|
["\\", "\n", ..rest] -> do_tokenize(rest, acc, line + 1, 1)
|
||||||
|
["\n", ..rest] -> do_tokenize(rest, acc, line + 1, 1)
|
||||||
|
[" ", ..rest] -> do_tokenize(rest, acc, line, col + 1)
|
||||||
|
["=", "=", ..rest] ->
|
||||||
|
do_tokenize(rest, [Token(Equals, line), ..acc], line, col + 2)
|
||||||
|
["!", "=", ..rest] ->
|
||||||
|
do_tokenize(rest, [Token(NotEquals, line), ..acc], line, col + 2)
|
||||||
|
[">", "=", ..rest] ->
|
||||||
|
do_tokenize(
|
||||||
|
rest,
|
||||||
|
[Token(GreaterThanOrEquals, line), ..acc],
|
||||||
|
line,
|
||||||
|
col + 2,
|
||||||
|
)
|
||||||
|
["<", "=", ..rest] ->
|
||||||
|
do_tokenize(rest, [Token(LowerThanOrEquals, line), ..acc], line, col + 2)
|
||||||
|
[">", ..rest] ->
|
||||||
|
do_tokenize(rest, [Token(GreaterThan, line), ..acc], line, col + 1)
|
||||||
|
["<", ..rest] ->
|
||||||
|
do_tokenize(rest, [Token(LowerThan, line), ..acc], line, col + 1)
|
||||||
|
["=", ..rest] ->
|
||||||
|
do_tokenize(rest, [Token(Assignment, line), ..acc], line, col + 1)
|
||||||
|
["?", ..rest] ->
|
||||||
|
do_tokenize(rest, [Token(Ternary, line), ..acc], line, col + 1)
|
||||||
|
[":", ..rest] ->
|
||||||
|
do_tokenize(rest, [Token(TernaryElse, line), ..acc], line, col + 1)
|
||||||
|
["%", ..rest] ->
|
||||||
|
do_tokenize(rest, [Token(Remainder, line), ..acc], line, col + 1)
|
||||||
|
["|", "|", ..rest] ->
|
||||||
|
do_tokenize(rest, [Token(Or, line), ..acc], line, col + 2)
|
||||||
|
["&", "&", ..rest] ->
|
||||||
|
do_tokenize(rest, [Token(And, line), ..acc], line, col + 2)
|
||||||
|
[";", ..rest] ->
|
||||||
|
do_tokenize(rest, [Token(Semicolon, line), ..acc], line, col + 1)
|
||||||
|
[")", ..rest] ->
|
||||||
|
do_tokenize(rest, [Token(RParen, line), ..acc], line, col + 1)
|
||||||
|
["(", ..rest] ->
|
||||||
|
do_tokenize(rest, [Token(LParen, line), ..acc], line, col + 1)
|
||||||
|
[digit, ..rest] if digit == "0"
|
||||||
|
|| digit == "1"
|
||||||
|
|| digit == "2"
|
||||||
|
|| digit == "3"
|
||||||
|
|| digit == "4"
|
||||||
|
|| digit == "5"
|
||||||
|
|| digit == "6"
|
||||||
|
|| digit == "7"
|
||||||
|
|| digit == "8"
|
||||||
|
|| digit == "9" -> read_digits(rest, acc, line, col + 1, digit)
|
||||||
|
[grapheme, ..] ->
|
||||||
|
Error(SyntaxError(
|
||||||
|
reason: "Unexpected grapheme " <> grapheme,
|
||||||
|
line: line,
|
||||||
|
column: option.Some(col),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_digits(
|
||||||
|
str: List(String),
|
||||||
|
acc: List(Token),
|
||||||
|
line: Int,
|
||||||
|
col: Int,
|
||||||
|
digit_acc: String,
|
||||||
|
) {
|
||||||
|
case str {
|
||||||
|
[digit, ..rest]
|
||||||
|
if digit == "0"
|
||||||
|
|| digit == "1"
|
||||||
|
|| digit == "2"
|
||||||
|
|| digit == "3"
|
||||||
|
|| digit == "4"
|
||||||
|
|| digit == "5"
|
||||||
|
|| digit == "6"
|
||||||
|
|| digit == "7"
|
||||||
|
|| digit == "8"
|
||||||
|
|| digit == "9"
|
||||||
|
-> read_digits(rest, acc, line, col + 1, digit_acc <> digit)
|
||||||
|
other ->
|
||||||
|
case int.parse(digit_acc) {
|
||||||
|
Ok(int) ->
|
||||||
|
do_tokenize(
|
||||||
|
other,
|
||||||
|
[Int(int, line), ..acc],
|
||||||
|
line,
|
||||||
|
col + string.length(digit_acc),
|
||||||
|
)
|
||||||
|
Error(_) ->
|
||||||
|
Error(SyntaxError(
|
||||||
|
reason: "Unparseable integer " <> digit_acc,
|
||||||
|
line: line,
|
||||||
|
column: option.Some(col),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
src/kielet/tester.gleam
Normal file
44
src/kielet/tester.gleam
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
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",
|
||||||
|
))
|
||||||
|
}
|
12
test/kielet_test.gleam
Normal file
12
test/kielet_test.gleam
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import gleeunit
|
||||||
|
import gleeunit/should
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
gleeunit.main()
|
||||||
|
}
|
||||||
|
|
||||||
|
// gleeunit test functions end in `_test`
|
||||||
|
pub fn hello_world_test() {
|
||||||
|
1
|
||||||
|
|> should.equal(1)
|
||||||
|
}
|
8
test/mo_test.gleam
Normal file
8
test/mo_test.gleam
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
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))
|
||||||
|
}
|
Loading…
Reference in a new issue