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
|
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.
|
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
|
## Gettext limitations
|
||||||
|
|
||||||
Due to how Gettext is built, the source language (the language used in your source files) can only be a
|
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"
|
name = "kielet"
|
||||||
version = "1.0.0"
|
version = "2.0.0"
|
||||||
|
|
||||||
# Fill out these fields if you intend to generate HTML documentation or publish
|
# Fill out these fields if you intend to generate HTML documentation or publish
|
||||||
# your project to the Hex package manager.
|
# your project to the Hex package manager.
|
||||||
|
|
|
@ -2,6 +2,22 @@ import kielet/context
|
||||||
import kielet/database
|
import kielet/database
|
||||||
|
|
||||||
/// Translate the given singular message.
|
/// 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 {
|
pub fn gettext(context: context.Context, msgid: String) -> String {
|
||||||
database.translate_singular(context.database, msgid, context.language)
|
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
|
/// 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
|
/// 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.
|
/// `"%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(
|
pub fn ngettext(
|
||||||
context: context.Context,
|
context: context.Context,
|
||||||
singular: String,
|
singular: String,
|
||||||
|
|
|
@ -35,8 +35,9 @@ pub fn translate_singular(db: Database, msgid: String, language_code: String) {
|
||||||
/// Translate a plural message using the database.
|
/// Translate a plural message using the database.
|
||||||
///
|
///
|
||||||
/// If the language is not found, does not have a translation for the message,
|
/// 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
|
/// does not have the correct plural for the given `n`, or does not have a
|
||||||
/// message is returned.
|
/// plural forms header at all, the plural message given as the argument is
|
||||||
|
/// returned instead.
|
||||||
pub fn translate_plural(
|
pub fn translate_plural(
|
||||||
db: Database,
|
db: Database,
|
||||||
msgid: String,
|
msgid: String,
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
//// used to translate messages.
|
//// used to translate messages.
|
||||||
|
|
||||||
import gleam/dict
|
import gleam/dict
|
||||||
|
import gleam/option
|
||||||
import gleam/result
|
import gleam/result
|
||||||
import kielet/mo
|
import kielet/mo
|
||||||
import kielet/plurals
|
import kielet/plurals
|
||||||
|
@ -29,13 +30,19 @@ pub type TranslateError {
|
||||||
/// The plural algorithm returned a form that is out of bounds for the amount
|
/// The plural algorithm returned a form that is out of bounds for the amount
|
||||||
/// of plural forms.
|
/// of plural forms.
|
||||||
PluralOutOfBounds(requested: Int, but_max_is: Int)
|
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 {
|
pub opaque type Language {
|
||||||
Language(
|
Language(
|
||||||
code: String,
|
code: String,
|
||||||
translations: mo.Translations,
|
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)
|
mo.parse(mo_file)
|
||||||
|> result.map_error(MoParseError),
|
|> 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.
|
/// Get the language's language code.
|
||||||
|
@ -72,11 +80,12 @@ pub fn get_singular_translation(lang: Language, msgid: String) {
|
||||||
|
|
||||||
/// Translate a plural message.
|
/// Translate a plural message.
|
||||||
pub fn get_plural_translation(lang: Language, msgid: String, n: Int) {
|
pub fn get_plural_translation(lang: Language, msgid: String, n: Int) {
|
||||||
case dict.get(lang.translations, msgid) {
|
case lang.plurals, dict.get(lang.translations, msgid) {
|
||||||
Ok(mostring) ->
|
option.None, _ -> Error(LanguageHasNoPlurals)
|
||||||
|
option.Some(p), Ok(mostring) ->
|
||||||
case mostring {
|
case mostring {
|
||||||
mo.Plural(content: c, ..) -> {
|
mo.Plural(content: c, ..) -> {
|
||||||
let index = plurals.evaluate(lang.plurals, n)
|
let index = plurals.evaluate(p, n)
|
||||||
case dict.get(c, index) {
|
case dict.get(c, index) {
|
||||||
Ok(msg) -> Ok(msg)
|
Ok(msg) -> Ok(msg)
|
||||||
Error(_) ->
|
Error(_) ->
|
||||||
|
@ -88,6 +97,6 @@ pub fn get_plural_translation(lang: Language, msgid: String, n: Int) {
|
||||||
}
|
}
|
||||||
_ -> Error(MsgIsSingular(msgid))
|
_ -> 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() {
|
fn load_languages() {
|
||||||
database.new()
|
database.new()
|
||||||
|> database.add_language(load_language("fi"))
|
|> 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