langcodec 0.14.1

Universal localization file toolkit for Rust. Supports Apple, Android, and CSV formats.
Documentation

langcodec (Library)

Universal localization file toolkit for Rust. Parse, write, convert, merge.

  • Formats: Apple .strings, .stringsdict, .xcstrings, .xliff, Android strings.xml, CSV, TSV
  • Unified model: Resource with Entry, Translation::Singular|Plural
  • Robust error type, utilities to infer format/language, merge, cache

Install

[dependencies]
langcodec = "0.14.1"

Docs: https://docs.rs/langcodec

Quick Start

use langcodec::{Codec, convert_auto};

// Convert between formats automatically
convert_auto("Localizable.strings", "strings.xml")?;

// Build an XLIFF exchange file (target language must be selected explicitly)
langcodec::convert_resources_to_format(
    vec![langcodec::Resource {
        metadata: langcodec::Metadata {
            language: "en".into(),
            domain: "Localizable".into(),
            custom: std::collections::HashMap::from([(
                "source_language".into(),
                "en".into(),
            )]),
        },
        entries: vec![],
    }],
    "Localizable.xliff",
    langcodec::FormatType::Xliff(Some("fr".into())),
)?;

// Load, inspect, and write
let mut codec = Codec::new();
codec.read_file_by_extension("en.lproj/Localizable.strings", None)?;
codec.write_to_file()?;
# Ok::<(), langcodec::Error>(())

Builder Pattern

use langcodec::Codec;

let codec = Codec::builder()
  .add_file("en.lproj/Localizable.strings")?
  .add_file("values/strings.xml")?
  .build();
# Ok::<(), langcodec::Error>(())

Work with Entries

use langcodec::{Codec, types::{Translation, EntryStatus}};
let mut codec = Codec::new();
codec.add_entry("welcome", "en", Translation::Singular("Hello".into()), None, None)?;
codec.update_translation("welcome", "en", Translation::Singular("Hello!".into()), Some(EntryStatus::Translated))?;
# Ok::<(), langcodec::Error>(())

Conversion Helpers

  • convert(input, input_format, output, output_format)
  • convert_auto(input, output)
  • infer_format_from_path, infer_language_from_path

Plurals

  • .xcstrings: plural variations supported via CLDR categories
  • .stringsdict: XML and binary plist input with canonical XML output, limited to one bare NSStringPluralRuleType variable per key
  • .xliff: Apple/Xcode XLIFF 1.2 bilingual exchange files
  • Android strings.xml: <plurals> supported (one/two/few/many/other/zero)

The .stringsdict codec implements the lossless intersection with Resource: plural-only entries, one variable, and a bare %#@variable@ or %n$#@variable@ selector. It rejects wrapper text, select/gender or nested rules, multiple variables, non-representable entry metadata, and unsupported plist value kinds. A language is inferred from an *.lproj directory or must be supplied by the caller. Binary input is accepted, while all writes use canonical XML.

Format::try_from(Resource) requires every plural entry to provide stringsdict.localized_format, stringsdict.variable_name, and stringsdict.value_type in Entry.custom. These fields explicitly identify the Apple selector. They are mandatory because Resource plural forms do not otherwise say which printf argument controls quantity; inferring from a numeric placeholder could select an unrelated value. Resources parsed from .stringsdict receive the three fields automatically.

Resource.metadata.domain is container/path metadata and is not encoded in a .stringsdict file. Known operational resource metadata (source_language, version, format, and langcodec provenance keys) is also container-only; unknown resource metadata and all non-structural entry custom metadata are rejected.

Binary input is materialized through plist::Value. Duplicate dictionary keys in an adversarial binary plist therefore cannot be detected after the plist library has materialized the dictionary.

CSV and TSV Schemas

CSV and TSV support two schemas. The conventional wide key,<language>... schema remains the default for simple singular catalogs. Conversion automatically selects the versioned __langcodec_extended_v1 schema when the wide form would invent or discard Resource data.

The extended schema round-trips resource and entry order, languages, domains, plural identifiers and forms, comments, statuses, custom metadata, and the difference between Translation::Empty and an empty singular string. Serialization is deterministic, including map-valued metadata. This preserves the langcodec data model, not the original whitespace or quoting of an imported file.

CSVFormat and TSVFormat now carry private schema state. Construct basic values with new or with_records, inspect parsed values with is_extended, and convert through TryFrom<Vec<Resource>> when automatic schema selection is required.

Error Handling

Fallible APIs return langcodec::Error with stable error codes and optional structured context. Path-based Parser::read_from and Parser::write_to operations attach the caller-visible path without changing the underlying error category.

Parser::write_to creates missing parent directories, serializes to a temporary file in the destination directory, flushes and syncs it, and then atomically replaces the destination. On Unix, temporary content remains owner-only until serialization succeeds, the destination mode is then applied, and the parent directory is synced after replacement. Existing symlinks remain in place while their referent is replaced; dangling symlinks and symlink loops are rejected. Unix hard-linked destinations are also rejected rather than having their identity silently broken.

These guarantees assume the destination directory is trusted and is not being concurrently mutated by an adversary. A parent-directory sync failure can be reported after replacement is already visible; in that case the new contents are committed but crash durability is uncertain. Newly created ancestor directories are not individually synced. Directory syncing and hard-link protection are Unix-only.

Only file permissions are retained. Ownership, ACLs, extended attributes, and other platform metadata are not copied. Callers that exhaustively match the public Error enum must handle the new Error::WithPath variant.

License

MIT