Browse Source

[i18n] QT Linguist TS reader.

Jeroen van Rijn 3 years ago
parent
commit
1289c96e2c

+ 0 - 1
core/encoding/xml/xml_reader.odin

@@ -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]
 
 

+ 45 - 11
core/i18n/example/i18n_example.odin

@@ -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(LOC("There is %d leaf.\n", 42), 42)
+	printf(_T("There is %d leaf.\n", 1), 1)
+	printf(_T("There is %d leaf.\n", 42), 42)
 
 
-	printf(LOC("There are %d leaves.\n", 1), 1)
-	printf(LOC("There are %d leaves.\n", 42), 42)
+	printf(_T("There are %d leaves.\n", 1), 1)
+	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()

+ 1 - 1
core/i18n/gettext.odin

@@ -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 <[email protected]>.
+	Copyright 2021-2022 Jeroen van Rijn <[email protected]>.
 	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
core/i18n/i18n.odin

@@ -2,7 +2,7 @@ package i18n
 /*
 /*
 	Internationalization helpers.
 	Internationalization helpers.
 
 
-	Copyright 2021 Jeroen van Rijn <[email protected]>.
+	Copyright 2021-2022 Jeroen van Rijn <[email protected]>.
 	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}
 
 

+ 153 - 0
core/i18n/qt_linguist.odin

@@ -0,0 +1,153 @@
+package i18n
+/*
+	A parser for Qt Linguist TS files.
+
+	Copyright 2022 Jeroen van Rijn <[email protected]>.
+	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 }

+ 26 - 26
tests/core/assets/XML/nl_NL-qt-ts.ts

@@ -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>
-  <message>
-    <source>Text for translation</source>
-    <comment>commenting</comment>
-    <translation type="obsolete">Tekst om te vertalen</translation>
-  </message>
-  <message>
-     <source>Also text to translate</source>
-     <extracomment>some text</extracomment>
-    <translation>Ook tekst om te vertalen</translation>
-  </message>
+	<name>Page</name>
+	<message>
+		<source>Text for translation</source>
+		<comment>commenting</comment>
+		<translation type="obsolete">Tekst om te vertalen</translation>
+	</message>
+	<message>
+		 <source>Also text to translate</source>
+		 <extracomment>some text</extracomment>
+		<translation>Ook tekst om te vertalen</translation>
+	</message>
 </context>
 </context>
 <context>
 <context>
-  <name>installscript</name>
-  <message>
-    <source>99 bottles of beer on the wall</source>
-    <oldcomment>some new comments here</oldcomment>
-    <translation>99 flessen bier op de muur</translation>
-  </message>
+	<name>installscript</name>
+	<message>
+		<source>99 bottles of beer on the wall</source>
+		<oldcomment>some new comments here</oldcomment>
+		<translation>99 flessen bier op de muur</translation>
+	</message>
 </context>
 </context>
 <context>
 <context>
-    <name>apple_count</name>
-    <message numerus="yes">
-      <source>%d apple(s)</source>
-      <translation>
-        <numerusform>%d appel</numerusform>
-        <numerusform>%d appels</numerusform>
-      </translation>
-    </message>
-  </context>
+		<name>apple_count</name>
+		<message numerus="yes">
+			<source>%d apple(s)</source>
+			<translation>
+				<numerusform>%d appel</numerusform>
+				<numerusform>%d appels</numerusform>
+			</translation>
+		</message>
+	</context>
 </TS>
 </TS>