mirror of
https://github.com/Ed94/Odin.git
synced 2026-08-05 15:18:49 +00:00
[i18n] QT Linguist TS reader.
This commit is contained in:
@@ -87,7 +87,6 @@ Option_Flag :: enum {
|
|||||||
If a tag body has a comment, it will be stripped unless this option is given.
|
If a tag body has a comment, it will be stripped unless this option is given.
|
||||||
*/
|
*/
|
||||||
Keep_Tag_Body_Comments,
|
Keep_Tag_Body_Comments,
|
||||||
|
|
||||||
}
|
}
|
||||||
Option_Flags :: bit_set[Option_Flag; u16]
|
Option_Flags :: bit_set[Option_Flag; u16]
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import "core:mem"
|
|||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:i18n"
|
import "core:i18n"
|
||||||
|
|
||||||
LOC :: i18n.get
|
_T :: i18n.get
|
||||||
|
|
||||||
_main :: proc() {
|
mo :: proc() {
|
||||||
using fmt
|
using fmt
|
||||||
|
|
||||||
err: i18n.Error
|
err: i18n.Error
|
||||||
@@ -23,27 +23,60 @@ _main :: proc() {
|
|||||||
These are in the .MO catalog.
|
These are in the .MO catalog.
|
||||||
*/
|
*/
|
||||||
println("-----")
|
println("-----")
|
||||||
println(LOC(""))
|
println(_T(""))
|
||||||
println("-----")
|
println("-----")
|
||||||
println(LOC("There are 69,105 leaves here."))
|
println(_T("There are 69,105 leaves here."))
|
||||||
println("-----")
|
println("-----")
|
||||||
println(LOC("Hellope, World!"))
|
println(_T("Hellope, World!"))
|
||||||
|
|
||||||
/*
|
/*
|
||||||
For ease of use, pluralized lookup can use both singular and plural form as key for the same translation.
|
For ease of use, pluralized lookup can use both singular and plural form as key for the same translation.
|
||||||
*/
|
*/
|
||||||
println("-----")
|
println("-----")
|
||||||
printf(LOC("There is %d leaf.\n", 1), 1)
|
printf(_T("There is %d leaf.\n", 1), 1)
|
||||||
printf(LOC("There is %d leaf.\n", 42), 42)
|
printf(_T("There is %d leaf.\n", 42), 42)
|
||||||
|
|
||||||
printf(LOC("There are %d leaves.\n", 1), 1)
|
printf(_T("There are %d leaves.\n", 1), 1)
|
||||||
printf(LOC("There are %d leaves.\n", 42), 42)
|
printf(_T("There are %d leaves.\n", 42), 42)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
This isn't.
|
This isn't.
|
||||||
*/
|
*/
|
||||||
println("-----")
|
println("-----")
|
||||||
println(LOC("Come visit us on Discord!"))
|
println(_T("Come visit us on Discord!"))
|
||||||
|
}
|
||||||
|
|
||||||
|
qt :: proc() {
|
||||||
|
using fmt
|
||||||
|
|
||||||
|
err: i18n.Error
|
||||||
|
|
||||||
|
/*
|
||||||
|
Parse QT file and set it as the active translation so we can omit `get`'s "catalog" parameter.
|
||||||
|
*/
|
||||||
|
i18n.ACTIVE, err = i18n.parse_qt(#load("../../../tests/core/assets/XML/nl_NL-qt-ts.ts"))
|
||||||
|
defer i18n.destroy()
|
||||||
|
|
||||||
|
fmt.printf("parse_qt returned %v\n", err)
|
||||||
|
if err != .None {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
These are in the .TS catalog.
|
||||||
|
*/
|
||||||
|
println("--- Page section ---")
|
||||||
|
println("Page:Text for translation =", _T("Page", "Text for translation"))
|
||||||
|
println("-----")
|
||||||
|
println("Page:Also text to translate =", _T("Page", "Also text to translate"))
|
||||||
|
println("-----")
|
||||||
|
println("--- installscript section ---")
|
||||||
|
println("installscript:99 bottles of beer on the wall =", _T("installscript", "99 bottles of beer on the wall"))
|
||||||
|
println("-----")
|
||||||
|
println("--- apple_count section ---")
|
||||||
|
println("apple_count:%d apple(s) =")
|
||||||
|
println("\t 1 =", _T("apple_count", "%d apple(s)", 1))
|
||||||
|
println("\t 42 =", _T("apple_count", "%d apple(s)", 42))
|
||||||
}
|
}
|
||||||
|
|
||||||
main :: proc() {
|
main :: proc() {
|
||||||
@@ -53,7 +86,8 @@ main :: proc() {
|
|||||||
mem.tracking_allocator_init(&track, context.allocator)
|
mem.tracking_allocator_init(&track, context.allocator)
|
||||||
context.allocator = mem.tracking_allocator(&track)
|
context.allocator = mem.tracking_allocator(&track)
|
||||||
|
|
||||||
_main()
|
// mo()
|
||||||
|
qt()
|
||||||
|
|
||||||
if len(track.allocation_map) > 0 {
|
if len(track.allocation_map) > 0 {
|
||||||
println()
|
println()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package i18n
|
|||||||
/*
|
/*
|
||||||
A parser for GNU GetText .MO files.
|
A parser for GNU GetText .MO files.
|
||||||
|
|
||||||
Copyright 2021 Jeroen van Rijn <nom@duclavier.com>.
|
Copyright 2021-2022 Jeroen van Rijn <nom@duclavier.com>.
|
||||||
Made available under Odin's BSD-3 license.
|
Made available under Odin's BSD-3 license.
|
||||||
|
|
||||||
A from-scratch implementation based after the specification found here:
|
A from-scratch implementation based after the specification found here:
|
||||||
|
|||||||
+18
-3
@@ -2,7 +2,7 @@ package i18n
|
|||||||
/*
|
/*
|
||||||
Internationalization helpers.
|
Internationalization helpers.
|
||||||
|
|
||||||
Copyright 2021 Jeroen van Rijn <nom@duclavier.com>.
|
Copyright 2021-2022 Jeroen van Rijn <nom@duclavier.com>.
|
||||||
Made available under Odin's BSD-3 license.
|
Made available under Odin's BSD-3 license.
|
||||||
|
|
||||||
List of contributors:
|
List of contributors:
|
||||||
@@ -26,8 +26,11 @@ MAX_PLURALS :: min(max(#config(ODIN_i18N_MAX_PLURAL_FORMS, 10), 1), 255)
|
|||||||
/*
|
/*
|
||||||
The main data structure. This can be generated from various different file formats, as long as we have a parser for them.
|
The main data structure. This can be generated from various different file formats, as long as we have a parser for them.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
Section :: map[string][]string
|
||||||
|
|
||||||
Translation :: struct {
|
Translation :: struct {
|
||||||
k_v: map[string]map[string][]string,
|
k_v: map[string]Section, // k_v[section][key][plural_form] = ...
|
||||||
intern: strings.Intern,
|
intern: strings.Intern,
|
||||||
|
|
||||||
pluralize: proc(number: int) -> int,
|
pluralize: proc(number: int) -> int,
|
||||||
@@ -39,6 +42,7 @@ Error :: enum {
|
|||||||
*/
|
*/
|
||||||
None = 0,
|
None = 0,
|
||||||
Empty_Translation_Catalog,
|
Empty_Translation_Catalog,
|
||||||
|
Duplicate_Key,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Couldn't find, open or read file.
|
Couldn't find, open or read file.
|
||||||
@@ -57,6 +61,17 @@ Error :: enum {
|
|||||||
MO_File_Unsupported_Version,
|
MO_File_Unsupported_Version,
|
||||||
MO_File_Invalid,
|
MO_File_Invalid,
|
||||||
MO_File_Incorrect_Plural_Count,
|
MO_File_Incorrect_Plural_Count,
|
||||||
|
|
||||||
|
/*
|
||||||
|
Qt Linguist *.TS file errors.
|
||||||
|
*/
|
||||||
|
TS_File_Parse_Error,
|
||||||
|
TS_File_Expected_Context,
|
||||||
|
TS_File_Expected_Context_Name,
|
||||||
|
TS_File_Expected_Source,
|
||||||
|
TS_File_Expected_Translation,
|
||||||
|
TS_File_Expected_NumerusForm,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -92,7 +107,7 @@ get_by_section :: proc(section, key: string, number := 0, catalog: ^Translation
|
|||||||
if catalog.pluralize != nil {
|
if catalog.pluralize != nil {
|
||||||
plural = catalog.pluralize(number)
|
plural = catalog.pluralize(number)
|
||||||
}
|
}
|
||||||
return get_by_slot(key, plural, catalog)
|
return get_by_slot(section, key, plural, catalog)
|
||||||
}
|
}
|
||||||
get :: proc{get_single_section, get_by_section}
|
get :: proc{get_single_section, get_by_section}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package i18n
|
||||||
|
/*
|
||||||
|
A parser for Qt Linguist TS files.
|
||||||
|
|
||||||
|
Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
|
||||||
|
Made available under Odin's BSD-3 license.
|
||||||
|
|
||||||
|
A from-scratch implementation based after the specification found here:
|
||||||
|
https://doc.qt.io/qt-5/linguist-ts-file-format.html
|
||||||
|
|
||||||
|
List of contributors:
|
||||||
|
Jeroen van Rijn: Initial implementation.
|
||||||
|
*/
|
||||||
|
import "core:os"
|
||||||
|
import "core:encoding/xml"
|
||||||
|
import "core:strings"
|
||||||
|
|
||||||
|
TS_XML_Options := xml.Options{
|
||||||
|
flags = {
|
||||||
|
.Input_May_Be_Modified,
|
||||||
|
.Must_Have_Prolog,
|
||||||
|
.Must_Have_DocType,
|
||||||
|
.Ignore_Unsupported,
|
||||||
|
.Unbox_CDATA,
|
||||||
|
.Decode_SGML_Entities,
|
||||||
|
},
|
||||||
|
expected_doctype = "TS",
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_qt_linguist_from_slice :: proc(data: []u8, pluralizer: proc(int) -> int = nil, allocator := context.allocator) -> (translation: ^Translation, err: Error) {
|
||||||
|
context.allocator = allocator
|
||||||
|
|
||||||
|
ts, xml_err := xml.parse(data, TS_XML_Options)
|
||||||
|
defer xml.destroy(ts)
|
||||||
|
|
||||||
|
if xml_err != .None || ts.element_count < 1 || ts.elements[0].ident != "TS" || len(ts.elements[0].children) == 0 {
|
||||||
|
return nil, .TS_File_Parse_Error
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Initalize Translation, interner and optional pluralizer.
|
||||||
|
*/
|
||||||
|
translation = new(Translation)
|
||||||
|
translation.pluralize = pluralizer
|
||||||
|
strings.intern_init(&translation.intern, allocator, allocator)
|
||||||
|
|
||||||
|
section: ^Section
|
||||||
|
|
||||||
|
for child_id in ts.elements[0].children {
|
||||||
|
// These should be <context>s.
|
||||||
|
child := ts.elements[child_id]
|
||||||
|
if child.ident != "context" {
|
||||||
|
return translation, .TS_File_Expected_Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find section name.
|
||||||
|
section_name_id, section_name_found := xml.find_child_by_ident(ts, child_id, "name")
|
||||||
|
if !section_name_found {
|
||||||
|
return translation, .TS_File_Expected_Context_Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
section_name := ts.elements[section_name_id].value
|
||||||
|
|
||||||
|
if section_name not_in translation.k_v {
|
||||||
|
translation.k_v[section_name] = {}
|
||||||
|
}
|
||||||
|
section = &translation.k_v[section_name]
|
||||||
|
|
||||||
|
// Find messages in section.
|
||||||
|
nth: int
|
||||||
|
for {
|
||||||
|
message_id, message_found := xml.find_child_by_ident(ts, child_id, "message", nth)
|
||||||
|
if !message_found {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
numerus_tag, _ := xml.find_attribute_val_by_key(ts, message_id, "numerus")
|
||||||
|
has_plurals := numerus_tag == "yes"
|
||||||
|
|
||||||
|
// We must have a <source> = key
|
||||||
|
source_id, source_found := xml.find_child_by_ident(ts, message_id, "source")
|
||||||
|
if !source_found {
|
||||||
|
return translation, .TS_File_Expected_Source
|
||||||
|
}
|
||||||
|
|
||||||
|
// We must have a <translation>
|
||||||
|
translation_id, translation_found := xml.find_child_by_ident(ts, message_id, "translation")
|
||||||
|
if !translation_found {
|
||||||
|
return translation, .TS_File_Expected_Translation
|
||||||
|
}
|
||||||
|
|
||||||
|
source := ts.elements[source_id]
|
||||||
|
xlat := ts.elements[translation_id]
|
||||||
|
|
||||||
|
if source.value in section {
|
||||||
|
return translation, .Duplicate_Key
|
||||||
|
}
|
||||||
|
|
||||||
|
if has_plurals {
|
||||||
|
if xlat.value != "" {
|
||||||
|
return translation, .TS_File_Expected_NumerusForm
|
||||||
|
}
|
||||||
|
|
||||||
|
num_plurals: int
|
||||||
|
for {
|
||||||
|
numerus_id, numerus_found := xml.find_child_by_ident(ts, translation_id, "numerusform", num_plurals)
|
||||||
|
if !numerus_found {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
num_plurals += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if num_plurals < 2 {
|
||||||
|
return translation, .TS_File_Expected_NumerusForm
|
||||||
|
}
|
||||||
|
section[source.value] = make([]string, num_plurals)
|
||||||
|
|
||||||
|
num_plurals = 0
|
||||||
|
for {
|
||||||
|
numerus_id, numerus_found := xml.find_child_by_ident(ts, translation_id, "numerusform", num_plurals)
|
||||||
|
if !numerus_found {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
numerus := ts.elements[numerus_id]
|
||||||
|
section[source.value][num_plurals] = strings.intern_get(&translation.intern, numerus.value)
|
||||||
|
|
||||||
|
num_plurals += 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Single translation
|
||||||
|
section[source.value] = make([]string, 1)
|
||||||
|
section[source.value][0] = strings.intern_get(&translation.intern, xlat.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
nth += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_qt_linguist_file :: proc(filename: string, pluralizer: proc(int) -> int = nil, allocator := context.allocator) -> (translation: ^Translation, err: Error) {
|
||||||
|
context.allocator = allocator
|
||||||
|
|
||||||
|
data, data_ok := os.read_entire_file(filename)
|
||||||
|
defer delete(data)
|
||||||
|
|
||||||
|
if !data_ok { return {}, .File_Error }
|
||||||
|
|
||||||
|
return parse_qt_linguist_from_slice(data, pluralizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_qt :: proc { parse_qt_linguist_file, parse_qt_linguist_from_slice }
|
||||||
@@ -2,34 +2,34 @@
|
|||||||
<!DOCTYPE TS>
|
<!DOCTYPE TS>
|
||||||
<TS version="2.1" language="nl" sourcelanguage="en">
|
<TS version="2.1" language="nl" sourcelanguage="en">
|
||||||
<context>
|
<context>
|
||||||
<name>Page</name>
|
<name>Page</name>
|
||||||
<message>
|
<message>
|
||||||
<source>Text for translation</source>
|
<source>Text for translation</source>
|
||||||
<comment>commenting</comment>
|
<comment>commenting</comment>
|
||||||
<translation type="obsolete">Tekst om te vertalen</translation>
|
<translation type="obsolete">Tekst om te vertalen</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>Also text to translate</source>
|
<source>Also text to translate</source>
|
||||||
<extracomment>some text</extracomment>
|
<extracomment>some text</extracomment>
|
||||||
<translation>Ook tekst om te vertalen</translation>
|
<translation>Ook tekst om te vertalen</translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>installscript</name>
|
<name>installscript</name>
|
||||||
<message>
|
<message>
|
||||||
<source>99 bottles of beer on the wall</source>
|
<source>99 bottles of beer on the wall</source>
|
||||||
<oldcomment>some new comments here</oldcomment>
|
<oldcomment>some new comments here</oldcomment>
|
||||||
<translation>99 flessen bier op de muur</translation>
|
<translation>99 flessen bier op de muur</translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>apple_count</name>
|
<name>apple_count</name>
|
||||||
<message numerus="yes">
|
<message numerus="yes">
|
||||||
<source>%d apple(s)</source>
|
<source>%d apple(s)</source>
|
||||||
<translation>
|
<translation>
|
||||||
<numerusform>%d appel</numerusform>
|
<numerusform>%d appel</numerusform>
|
||||||
<numerusform>%d appels</numerusform>
|
<numerusform>%d appels</numerusform>
|
||||||
</translation>
|
</translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
</TS>
|
</TS>
|
||||||
|
|||||||
Reference in New Issue
Block a user