Data formats

XLIFF translation files

XLIFF 1.2 and 2.0 read and written, groups and segments included, inline tags kept exactly as they stand with plain text beside them. States, what is left to translate, words done, targets whose tags or numbers do not match the source, and translations put in with the rest of the file left byte for byte.

Version 1.0.0 · 7 functions · 2 crates

#translation#i18n#xml#localisation

Functions

What use xliff.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use xliff_read, xliff_read_file from xliff (Quillo 0.6).

xliff_read(text: str) → anyor an error

Reads a file: { version, files: [{ original, source_language, target_language, units }] }. A unit keeps source and target with their inline tags, and gives source_text and target_text without.

xliff_read_file(path: str) → anyor an error

Reads a file from a path, as xliff_read does.

xliff_untranslated(text: str) → listor an error

What still needs a translator: units with no target or in a state that says so, and translate="no" left out.

xliff_progress(text: str) → anyor an error

Progress: units and words done, by state, and a percentage.

xliff_check(text: str) → listor an error

Inline tags in a target that do not match its source, the error that breaks a build: missing, extra, or out of place.

xliff_fill(text: str, translations: any) → stror an error

Puts translations into a file: translations maps a unit's id to text, or to { target, state }. Text may hold inline tags as the source has them; anything else in the file stays byte for byte, and the state goes to translated unless told.

xliff_write(doc: any) → stror an error

Writes a file from { version, source_language, target_language, original, units: [{ id, source, target, state, note }] }, version being 1.2 or 2.0. source and target may hold inline tags; source_text and target_text are escaped for you.

Get it

$ quillo install xliff               # xliff.qwer into this folder$ quillo install xliff --example     # and its example, xliff.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/xliff.qwer$ curl -O https://quillolang.org/plugins/files/xliff.qwe$ curl -O https://quillolang.org/plugins/files/xliff_sample_12.xlf$ curl -O https://quillolang.org/plugins/files/xliff_sample_20.xlf

Then, in your program, all of it or only what you call:

main.qwe
use xliff.qweruse xliff_read, xliff_read_file from xliff      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
roxmltree = "0.20"serde_json = { version = "1", features = ["preserve_order"] }

Example

xliff.qwe
// A translation round: the files that came back from translators, how far along they are, what// breaks a build, and translations put in with the rest of the file left alone.////   quillo run xliff.qwe      then open http://localhost:8080/read, /v2, /todo, /progress, /check, /fill, /write and /brokenuse "xliff.qwer"let V1 = read_file("xliff_sample_12.xlf")let V2 = read_file("xliff_sample_20.xlf")route GET /read {    let d = xliff_read_file("xliff_sample_12.xlf")?    let f = d.files[0]    { version: d.version, file: [f.original, f.datatype, f.source_language, f.target_language], ids: f.units | map((u) => u.id), first: f.units[0], cart: [f.units[1].source, f.units[1].source_text, f.units[1].approved] }}route GET /v2 {    let f = xliff_read(V2)?.files[0]    { langs: [f.source_language, f.target_language], ids: f.units | map((u) => [u.id, u.state, u.translated]), first: f.units[0], placeholder: f.units[1].target, tags: f.units[1].tags }}route GET /todo = {    v1: xliff_untranslated(V1)? | map((u) => [u.id, u.source_text, u.state]),    v2: xliff_untranslated(V2)? | map((u) => [u.id, u.source_text, u.state]),}route GET /progress = { v1: xliff_progress(V1)?, v2: xliff_progress(V2)? }route GET /check = { v1: xliff_check(V1)?, v2: xliff_check(V2)? }route GET /fill {    let one = xliff_fill(V1, { save: "Enregistrer", open: { target: "Ouvrir <bpt id=\"1\">&lt;b&gt;</bpt>maintenant<ept id=\"1\">&lt;/b&gt;</ept>", state: "needs-review-translation" } })?    let two = xliff_fill(V2, { "u2/s2": "Zweiter.", u4: { target: "Tom &amp; Jerry", state: "final" } })?    {        left: xliff_untranslated(one)? | map((u) => u.id),        open: (xliff_read(one)?.files[0].units | filter((u) => u.id == "open"))[0],        kept: contains(one, "<header><tool tool-id=\"sample\" tool-name=\"Sample\"/></header>"),        added: contains(one, "<source>Save</source>\n          <target state=\"translated\">Enregistrer</target>"),        v2: xliff_read(two)?.files[0].units | map((u) => [u.id, u.state, u.target_text]),        nothing: xliff_fill(V1, {})? == V1,        no_id: xliff_fill(V1, { nope: "x" }).error,        bad_markup: xliff_fill(V1, { save: "a <b" }).error != null,        bad_state: xliff_fill(V2, { u4: { target: "x", state: "needs-review" } }).error,    }}let DOC = {    source_language: "en", target_language: "es", original: "app.json",    units: [        { id: "a", source_text: "Fish & chips <hot>", target_text: "Pescado & patatas", state: "translated", note: "A <dish>" },        { id: "b", source: "Hi <ph id=\"1\"/>", },    ],}route GET /write {    let two = xliff_write(merge(DOC, { version: "2.0" }))?    let one = xliff_write(merge(DOC, { version: "1.2", units: [merge(DOC.units[0], { state: "signed-off" }), { id: "b", source: "Hi <x id=\"1\"/>" }] }))?    let a = xliff_read(two)?    let b = xliff_read(one)?    {        two: two,        a: a.files[0].units | map((u) => [u.id, u.source_text, u.target_text, u.state]),        b: [b.files[0].source_language, b.files[0].units | map((u) => [u.id, u.source, u.state, u.notes])],        twice: xliff_write({ source_language: "en", units: [{ id: "a", source: "x" }, { id: "a", source: "y" }] }).error,        no_source: xliff_write({ source_language: "en", units: [{ id: "a" }] }).error,        bad_markup: xliff_write({ source_language: "en", units: [{ id: "a", source: "<b>" }] }).error != null,        bad_version: xliff_write({ version: "1.1", source_language: "en", units: [] }).error,        bad_state: xliff_write({ source_language: "en", units: [{ id: "a", source: "x", state: "signed-off" }] }).error,    }}route GET /broken = {    not_xml: xliff_read("<xliff").error != null,    wrong: xliff_read("<html/>").error,    no_version: xliff_read("<xliff/>").error,    old: xliff_read("<xliff version=\"3.0\"/>").error,    empty: xliff_read("<xliff version=\"1.2\"/>").error,    entity: xliff_read("<!DOCTYPE x [<!ENTITY a 'b'>]><xliff version=\"1.2\"/>").error != null,    no_file: xliff_read_file("nothing.xlf").error != null,}
$ quillo run xliff.qwe$ curl 'localhost:8080/read'$ curl 'localhost:8080/v2'$ curl 'localhost:8080/todo'$ curl 'localhost:8080/progress'$ curl 'localhost:8080/check'$ curl 'localhost:8080/fill'$ curl 'localhost:8080/write'$ curl 'localhost:8080/broken'

Source

xliff.qwer
//! XLIFF 1.2 and 2.0 translation files read and written, inline tags kept, states, what is untranslated, translations filled in and tags checked against the source.//! version: 1.0.0//!//! [dependencies]//! roxmltree = "0.20"//! serde_json = { version = "1", features = ["preserve_order"] }use roxmltree::{Document, Node, ParsingOptions};use serde_json::{json, Map, Value};use std::collections::HashMap;const INLINE_12: [&str; 9] = ["g", "x", "bx", "ex", "bpt", "ept", "ph", "it", "mrk"];const INLINE_20: [&str; 8] = ["ph", "pc", "sc", "ec", "mrk", "sm", "em", "cp"];const STATES_12: [&str; 10] = ["new", "needs-translation", "needs-adaptation", "needs-l10n", "needs-review-translation", "needs-review-adaptation", "needs-review-l10n", "translated", "signed-off", "final"];const STATES_20: [&str; 4] = ["initial", "translated", "reviewed", "final"];fn esc(s: &str) -> String {    s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;")}fn attr(s: &str) -> String {    esc(s).replace('"', "&quot;")}fn kids<'a, 'i>(n: Node<'a, 'i>, name: &'static str) -> impl Iterator<Item = Node<'a, 'i>> {    n.children().filter(move |c| c.is_element() && c.tag_name().name() == name)}/// The text of a segment as it stands in the file, with its inline tags, namespace prefixes and all.fn inner(n: Node, src: &str) -> String {    let (Some(first), Some(last)) = (n.first_child(), n.last_child()) else { return String::new() };    let raw = &src[first.range().start..last.range().end];    raw.to_string()}fn plain(n: Node) -> String {    let mut out = String::new();    for c in n.children() {        if c.is_text() {            out.push_str(c.text().unwrap_or(""));        } else if c.is_element() {            match c.tag_name().name() {                // Their text is markup of the original format, not words.                "bpt" | "ept" | "ph" | "it" | "sub" | "x" | "bx" | "ex" | "sc" | "ec" | "cp" | "sm" | "em" => {}                _ => out.push_str(&plain(c)),            }        }    }    out}/// The inline tags inside a segment: `[{ tag, id }]`, in order.fn tags_in(n: Node, list: &[&str], out: &mut Vec<Value>) {    for c in n.children().filter(|c| c.is_element()) {        let name = c.tag_name().name();        if list.contains(&name) {            out.push(json!({ "tag": name, "id": c.attribute("id").or(c.attribute("rid")).or(c.attribute("startRef")) }));        }        tags_in(c, list, out);    }}fn notes(u: Node) -> Vec<Value> {    let list: Vec<Node> = kids(u, "note").chain(kids(u, "notes").flat_map(|x| kids(x, "note"))).collect();    list.into_iter().map(|n| json!({ "text": n.text().unwrap_or("").trim(), "from": n.attribute("from").or(n.attribute("category")), "priority": n.attribute("priority").and_then(|p| p.parse::<i64>().ok()) })).collect()}fn unit_12(u: Node, src: &str) -> Value {    let source = kids(u, "source").next();    let target = kids(u, "target").next();    let mut tags = vec![];    if let Some(s) = source {        tags_in(s, &INLINE_12, &mut tags);    }    let text = target.map(|t| plain(t)).filter(|t| !t.trim().is_empty());    let state = target.and_then(|t| t.attribute("state"));    let done = text.is_some() && !matches!(state, Some("new") | Some("needs-translation") | Some("needs-adaptation") | Some("needs-l10n"));    json!({        "id": u.attribute("id"), "resname": u.attribute("resname"), "translate": u.attribute("translate") != Some("no"),        "source": source.map(|s| inner(s, src)), "source_text": source.map(plain),        "target": target.map(|t| inner(t, src)), "target_text": text,        "state": state, "approved": u.attribute("approved") == Some("yes"),        "translated": done, "tags": tags, "notes": notes(u),    })}fn walk_12(n: Node, src: &str, out: &mut Vec<Value>) {    for c in n.children().filter(|c| c.is_element()) {        match c.tag_name().name() {            "trans-unit" => out.push(unit_12(c, src)),            "group" | "body" => walk_12(c, src, out),            _ => {}        }    }}fn unit_20(u: Node, src: &str, out: &mut Vec<Value>) {    let segs: Vec<Node> = kids(u, "segment").collect();    let many = segs.len() > 1;    for (i, s) in segs.iter().enumerate() {        let source = kids(*s, "source").next();        let target = kids(*s, "target").next();        let mut tags = vec![];        if let Some(x) = source {            tags_in(x, &INLINE_20, &mut tags);        }        let text = target.map(|t| plain(t)).filter(|t| !t.trim().is_empty());        let state = s.attribute("state");        let done = text.is_some() && !matches!(state, Some("initial"));        let id = u.attribute("id").unwrap_or("");        let seg_id = s.attribute("id").map(str::to_string).unwrap_or_else(|| (i + 1).to_string());        out.push(json!({            "id": if many { format!("{}/{}", id, seg_id) } else { id.to_string() },            "resname": u.attribute("name"), "translate": u.attribute("translate") != Some("no"),            "source": source.map(|x| inner(x, src)), "source_text": source.map(plain),            "target": target.map(|t| inner(t, src)), "target_text": text,            "state": state, "approved": matches!(state, Some("final")),            "translated": done, "tags": tags, "notes": notes(u),        }));    }}fn walk_20(n: Node, src: &str, out: &mut Vec<Value>) {    for c in n.children().filter(|c| c.is_element()) {        match c.tag_name().name() {            "unit" => unit_20(c, src, out),            "group" => walk_20(c, src, out),            _ => {}        }    }}fn read_doc(text: &str) -> Result<Value, String> {    let text = text.trim_start_matches('\u{feff}');    let doc = Document::parse_with_options(text, ParsingOptions { allow_dtd: false, ..Default::default() }).map_err(|e| format!("not XML: {}", e))?;    let root = doc.root_element();    if root.tag_name().name() != "xliff" {        return Err(format!("not XLIFF: the root element is <{}>", root.tag_name().name()));    }    let version = root.attribute("version").ok_or("the <xliff> element has no version")?;    let v2 = if version.starts_with("2.") {        true    } else if version.starts_with("1.") {        false    } else {        return Err(format!("XLIFF {} is not read here: 1.2 or 2.0", version));    };    let mut files = vec![];    for f in kids(root, "file") {        let mut units = vec![];        if v2 {            walk_20(f, text, &mut units);        } else {            for b in kids(f, "body") {                walk_12(b, text, &mut units);            }        }        files.push(json!({            "id": f.attribute("id"), "original": f.attribute("original"), "datatype": f.attribute("datatype"),            "source_language": if v2 { root.attribute("srcLang") } else { f.attribute("source-language") },            "target_language": if v2 { root.attribute("trgLang") } else { f.attribute("target-language") },            "units": units,        }));    }    if files.is_empty() {        return Err("an XLIFF with no <file>".to_string());    }    Ok(json!({ "version": version, "files": files }))}fn units_of(doc: &Value) -> Vec<&Value> {    doc["files"].as_array().into_iter().flatten().flat_map(|f| f["units"].as_array().into_iter().flatten()).collect()}/// Reads a file: `{ version, files: [{ original, source_language, target_language, units }] }`. A unit keeps `source` and `target` with their inline tags, and gives `source_text` and `target_text` without.pub fn xliff_read(text: &str) -> Result<Value, String> {    read_doc(text)}/// Reads a file from a path, as `xliff_read` does.pub fn xliff_read_file(path: &str) -> Result<Value, String> {    let t = std::fs::read_to_string(path).map_err(|e| format!("cannot read {}: {}", path, e))?;    read_doc(&t)}/// What still needs a translator: units with no target or in a state that says so, and `translate="no"` left out.pub fn xliff_untranslated(text: &str) -> Result<Vec<Value>, String> {    let d = read_doc(text)?;    Ok(units_of(&d).into_iter().filter(|u| u["translate"] == json!(true) && u["translated"] == json!(false)).map(|u| json!({ "id": u["id"], "source": u["source"], "source_text": u["source_text"], "state": u["state"], "notes": u["notes"] })).collect())}fn word_count(t: &str) -> usize {    t.split_whitespace().filter(|w| w.chars().any(char::is_alphanumeric)).count()}/// Progress: units and words done, by state, and a percentage.pub fn xliff_progress(text: &str) -> Result<Value, String> {    let d = read_doc(text)?;    let all: Vec<&Value> = units_of(&d).into_iter().filter(|u| u["translate"] == json!(true)).collect();    let mut states: HashMap<String, i64> = HashMap::new();    let (mut words, mut done_words, mut done) = (0, 0, 0);    for u in &all {        let w = word_count(u["source_text"].as_str().unwrap_or(""));        words += w;        if u["translated"] == json!(true) {            done += 1;            done_words += w;        }        *states.entry(u["state"].as_str().unwrap_or(if u["target_text"].is_null() { "none" } else { "unstated" }).to_string()).or_default() += 1;    }    let mut sorted: Vec<(String, i64)> = states.into_iter().collect();    sorted.sort();    let m: Map<String, Value> = sorted.into_iter().map(|(k, v)| (k, json!(v))).collect();    let pct = if words == 0 { 100.0 } else { (done_words as f64 * 1000.0 / words as f64).round() / 10.0 };    Ok(json!({ "units": all.len(), "translated": done, "words": words, "translated_words": done_words, "percent": pct, "states": m }))}/// Inline tags in a target that do not match its source, the error that breaks a build: missing, extra, or out of place.pub fn xliff_check(text: &str) -> Result<Vec<Value>, String> {    let d = read_doc(text)?;    let version = d["version"].as_str().unwrap_or("");    let list: &[&str] = if version.starts_with('2') { &INLINE_20 } else { &INLINE_12 };    let doc = Document::parse_with_options(text.trim_start_matches('\u{feff}'), ParsingOptions { allow_dtd: false, ..Default::default() }).map_err(|e| e.to_string())?;    let mut out = vec![];    for u in doc.descendants().filter(|n| n.is_element() && matches!(n.tag_name().name(), "trans-unit" | "segment")) {        let (Some(s), Some(t)) = (kids(u, "source").next(), kids(u, "target").next()) else { continue };        let id = if u.tag_name().name() == "segment" { u.parent().and_then(|p| p.attribute("id")).unwrap_or("") } else { u.attribute("id").unwrap_or("") };        if plain(t).trim().is_empty() {            continue;        }        let (mut a, mut b) = (vec![], vec![]);        tags_in(s, list, &mut a);        tags_in(t, list, &mut b);        let key = |v: &Value| format!("{}#{}", v["tag"].as_str().unwrap_or(""), v["id"].as_str().unwrap_or(""));        let (ka, kb): (Vec<String>, Vec<String>) = (a.iter().map(key).collect(), b.iter().map(key).collect());        for k in ka.iter().filter(|k| !kb.contains(k)) {            out.push(json!({ "id": id, "problem": "missing tag", "tag": k }));        }        for k in kb.iter().filter(|k| !ka.contains(k)) {            out.push(json!({ "id": id, "problem": "extra tag", "tag": k }));        }        let (a_num, b_num): (Vec<String>, Vec<String>) = (numbers(&plain(s)), numbers(&plain(t)));        if a_num != b_num {            out.push(json!({ "id": id, "problem": "numbers differ", "tag": format!("{} / {}", a_num.join(" "), b_num.join(" ")) }));        }        let (x, y) = (plain(s), plain(t));        if x.ends_with(' ') != y.ends_with(' ') || x.starts_with(' ') != y.starts_with(' ') {            out.push(json!({ "id": id, "problem": "spaces at the ends differ", "tag": null }));        }    }    Ok(out)}fn numbers(t: &str) -> Vec<String> {    let mut out: Vec<String> = t.split(|c: char| !c.is_ascii_digit()).filter(|x| !x.is_empty()).map(str::to_string).collect();    out.sort();    out}fn well_formed(fragment: &str, id: &str, v2: bool) -> Result<(), String> {    let wrapper = format!("<t xmlns=\"urn:oasis:names:tc:xliff:{}\">{}</t>", if v2 { "document:2.0" } else { "document:1.2" }, fragment);    Document::parse(&wrapper).map(|_| ()).map_err(|e| format!("the translation of {} is not well formed: {}", id, e))}/// Finds where an element's own text sits in the source: `(start, end)` of its contents, and of a whole element.fn splice_target(src: &str, u: Node, content: &str, state: Option<&str>, v2: bool) -> Vec<(usize, usize, String)> {    let mut edits = vec![];    let t = kids(u, "target").next();    let state_attr = |s: &str| format!(" state=\"{}\"", attr(s));    match t {        Some(t) => {            let r = t.range();            let tag_end = src[r.start..r.end].find('>').map_or(r.start, |i| r.start + i);            let self_closed = src[..r.end].ends_with("/>");            let head = src[r.start..if self_closed { tag_end - 1 } else { tag_end }].to_string();            let head = match (state, v2) {                (Some(s), false) => {                    match head.find(" state=\"") {                        Some(i) => {                            let after = head[i + 8..].find('"').map_or(head.len(), |e| i + 8 + e + 1);                            format!("{}{}{}", &head[..i], state_attr(s), &head[after..])                        }                        None => format!("{}{}", head, state_attr(s)),                    }                }                _ => head,            };            edits.push((r.start, r.end, format!("{}>{}</target>", head, content)));        }        None => {            if let Some(s) = kids(u, "source").next() {                let at = s.range().end;                let line_start = src[..s.range().start].rfind('\n').map_or(0, |i| i + 1);                let pad: String = src[line_start..s.range().start].chars().take_while(|c| c.is_whitespace()).collect();                let st = match (state, v2) {                    (Some(s), false) => state_attr(s),                    _ => String::new(),                };                edits.push((at, at, format!("\n{}<target{}>{}</target>", pad, st, content)));            }        }    }    if let (Some(s), true) = (state, v2) {        // In 2.0 the state sits on <segment>.        let r = u.range();        let tag_end = src[r.start..].find('>').map_or(r.start, |i| r.start + i);        let head = &src[r.start..tag_end];        match head.find(" state=\"") {            Some(i) => {                let start = r.start + i;                let end = start + 8 + head[i + 8..].find('"').unwrap_or(0) + 1;                edits.push((start, end, state_attr(s)));            }            None => edits.push((tag_end, tag_end, state_attr(s))),        }    }    edits}/// Puts translations into a file: `translations` maps a unit's id to text, or to `{ target, state }`. Text may hold inline tags as the source has them; anything else in the file stays byte for byte, and the state goes to `translated` unless told.pub fn xliff_fill(text: &str, translations: serde_json::Value) -> Result<String, String> {    let src = text.trim_start_matches('\u{feff}');    let doc = Document::parse_with_options(src, ParsingOptions { allow_dtd: false, ..Default::default() }).map_err(|e| format!("not XML: {}", e))?;    let root = doc.root_element();    let v2 = root.attribute("version").map_or(false, |v| v.starts_with("2."));    let map = translations.as_object().ok_or("`translations` must be a map of id to text")?;    let mut edits: Vec<(usize, usize, String)> = vec![];    let mut used: Vec<&str> = vec![];    for u in doc.descendants().filter(|n| n.is_element() && matches!(n.tag_name().name(), "trans-unit" | "unit")) {        let id = u.attribute("id").unwrap_or("");        let segs: Vec<Node> = if v2 { kids(u, "segment").collect() } else { vec![u] };        let many = segs.len() > 1;        for (i, s) in segs.iter().enumerate() {            let key = if many { format!("{}/{}", id, s.attribute("id").map(str::to_string).unwrap_or_else(|| (i + 1).to_string())) } else { id.to_string() };            let Some((k, v)) = map.get_key_value(&key) else { continue };            used.push(k);            let (content, state) = match v {                Value::String(t) => (t.clone(), None),                Value::Object(o) => (o.get("target").and_then(Value::as_str).ok_or(format!("{}: needs `target`", key))?.to_string(), o.get("state").and_then(Value::as_str).map(str::to_string)),                _ => return Err(format!("{}: a translation must be text or a map", key)),            };            well_formed(&content, &key, v2)?;            let state = state.unwrap_or_else(|| "translated".to_string());            let allowed: &[&str] = if v2 { &STATES_20 } else { &STATES_12 };            if !allowed.contains(&state.as_str()) && !state.starts_with("x-") {                return Err(format!("{}: `{}` is not a state in XLIFF {}", key, state, if v2 { "2.0" } else { "1.2" }));            }            edits.extend(splice_target(src, *s, &content, Some(&state), v2));        }    }    if let Some(k) = map.keys().find(|k| !used.contains(&k.as_str())) {        return Err(format!("no unit has the id {}", k));    }    edits.sort_by_key(|e| std::cmp::Reverse(e.0));    let mut out = src.to_string();    for (a, b, t) in edits {        out.replace_range(a..b, &t);    }    Ok(out)}fn inline_of(u: &Value, key: &str, plain_key: &str) -> Option<String> {    u.get(key).and_then(Value::as_str).map(str::to_string).or(u.get(plain_key).and_then(Value::as_str).map(esc))}/// Writes a file from `{ version, source_language, target_language, original, units: [{ id, source, target, state, note }] }`, `version` being `1.2` or `2.0`. `source` and `target` may hold inline tags; `source_text` and `target_text` are escaped for you.pub fn xliff_write(doc: serde_json::Value) -> Result<String, String> {    let version = doc.get("version").and_then(Value::as_str).unwrap_or("2.0");    let v2 = match version {        "2.0" | "2.1" => true,        "1.2" => false,        o => return Err(format!("XLIFF {} is not written here: 1.2 or 2.0", o)),    };    let lang = doc.get("source_language").and_then(Value::as_str).ok_or("needs a `source_language`")?;    let trg = doc.get("target_language").and_then(Value::as_str);    let original = doc.get("original").and_then(Value::as_str).unwrap_or("messages");    let units = doc.get("units").and_then(Value::as_array).ok_or("needs `units`")?;    let mut seen: Vec<&str> = vec![];    let mut out = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");    if v2 {        out.push_str(&format!("<xliff xmlns=\"urn:oasis:names:tc:xliff:document:{}\" version=\"{}\" srcLang=\"{}\"", version, version, attr(lang)));        if let Some(t) = trg {            out.push_str(&format!(" trgLang=\"{}\"", attr(t)));        }        out.push_str(&format!(">\n  <file id=\"f1\" original=\"{}\">\n", attr(original)));    } else {        out.push_str(&format!("<xliff xmlns=\"urn:oasis:names:tc:xliff:document:1.2\" version=\"1.2\">\n  <file original=\"{}\" datatype=\"plaintext\" source-language=\"{}\"", attr(original), attr(lang)));        if let Some(t) = trg {            out.push_str(&format!(" target-language=\"{}\"", attr(t)));        }        out.push_str(">\n    <body>\n");    }    for (i, u) in units.iter().enumerate() {        let id = u.get("id").and_then(Value::as_str).filter(|x| !x.is_empty()).ok_or(format!("unit {}: needs an `id`", i))?;        if seen.contains(&id) {            return Err(format!("the id {} is used twice", id));        }        seen.push(id);        let src = inline_of(u, "source", "source_text").ok_or(format!("{}: needs a `source`", id))?;        well_formed(&src, id, v2)?;        let target = inline_of(u, "target", "target_text");        if let Some(t) = &target {            well_formed(t, id, v2)?;        }        let state = u.get("state").and_then(Value::as_str);        if let Some(s) = state {            let allowed: &[&str] = if v2 { &STATES_20 } else { &STATES_12 };            if !allowed.contains(&s) && !s.starts_with("x-") {                return Err(format!("{}: `{}` is not a state in XLIFF {}", id, s, version));            }        }        let note = u.get("note").and_then(Value::as_str);        if v2 {            out.push_str(&format!("    <unit id=\"{}\">\n", attr(id)));            if let Some(n) = note {                out.push_str(&format!("      <notes><note>{}</note></notes>\n", esc(n)));            }            match state {                Some(s) => out.push_str(&format!("      <segment state=\"{}\">\n", s)),                None => out.push_str("      <segment>\n"),            }            out.push_str(&format!("        <source>{}</source>\n", src));            if let Some(t) = &target {                out.push_str(&format!("        <target>{}</target>\n", t));            }            out.push_str("      </segment>\n    </unit>\n");        } else {            out.push_str(&format!("      <trans-unit id=\"{}\">\n        <source>{}</source>\n", attr(id), src));            if let Some(t) = &target {                match state {                    Some(s) => out.push_str(&format!("        <target state=\"{}\">{}</target>\n", s, t)),                    None => out.push_str(&format!("        <target>{}</target>\n", t)),                }            }            if let Some(n) = note {                out.push_str(&format!("        <note>{}</note>\n", esc(n)));            }            out.push_str("      </trans-unit>\n");        }    }    out.push_str(if v2 { "  </file>\n</xliff>\n" } else { "    </body>\n  </file>\n</xliff>\n" });    Ok(out)}
Thread
38Useful? Like it, or add to its thread below.

Thread 0 posts

On XLIFF translation files: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.