package i18n /* A parser for Qt Linguist TS files. Copyright 2022 Jeroen van Rijn . 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_bytes :: proc(data: []byte, options := DEFAULT_PARSE_OPTIONS, 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 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, _ := strings.intern_get(&translation.intern, "") if !options.merge_sections { section_name, _ = strings.intern_get(&translation.intern, 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 = 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_id, translation_found := xml.find_child_by_ident(ts, message_id, "translation") if !translation_found { return translation, .TS_File_Expected_Translation } source, _ := strings.intern_get(&translation.intern, ts.elements[source_id].value) xlat, _ := strings.intern_get(&translation.intern, ts.elements[translation_id].value) if source in section { return translation, .Duplicate_Key } if has_plurals { if xlat != "" { 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] = 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, _ := strings.intern_get(&translation.intern, ts.elements[numerus_id].value) section[source][num_plurals] = numerus num_plurals += 1 } } else { // Single translation section[source] = make([]string, 1) section[source][0] = xlat } nth += 1 } } return } parse_qt_linguist_file :: proc(filename: string, options := DEFAULT_PARSE_OPTIONS, 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_bytes(data, options, pluralizer, allocator) } parse_qt :: proc { parse_qt_linguist_file, parse_qt_linguist_from_bytes }