Table of Contents

Writers

Overview

While parsers read translation files in various formats and load them into memory, writers do the opposite — they serialize translations from the TranslationManager back to files in different formats. Writers enable you to export translations, convert between formats, implement translation workflows, and support round-trip editing (read → modify → write).

Tlumach provides writers for all supported file formats: JSON, ARB, INI, TOML, CSV, TSV, ResX, XLIFF, and Apple String Catalog. Like parsers, writers follow a hierarchical architecture with format-specific base classes and concrete implementations.

Writer Architecture

All writers inherit from a common base class hierarchy that organizes implementations by file format family:

BaseWriter (abstract)
├── BaseJsonWriter (abstract)
│   ├── JsonWriter
│   ├── ArbWriter
│   └── StringCatWriter
├── BaseKeyValueWriter (abstract)
│   ├── IniWriter
│   └── TomlWriter
├── BaseTableWriter (abstract)
│   ├── CsvWriter
│   └── TsvWriter
└── BaseXmlWriter (abstract)
    ├── ResxWriter
    └── XliffWriter

BaseWriter

The root BaseWriter class defines the writer contract:

  • FormatName — Display name of the format (e.g., "JSON", "INI")
  • ConfigExtension — File extension for configuration files (e.g., ".jsoncfg")
  • TranslationExtension — File extension for translation files (e.g., ".json")
  • WriteConfiguration() — Serializes the translation configuration
  • WriteTranslation(culture) — Writes a single culture's translations to a file
  • WriteTranslations(cultures) — Writes multiple cultures' translations in one file (table formats only)

Format-Specific Base Classes

Format families share common behavior through specialized base classes:

  • BaseJsonWriter — Handles JSON and ARB formats; supports indentation control via the IndentationStep property (default: 2 spaces)
  • BaseKeyValueWriter — Handles INI and TOML formats; organizes translations into sections and key-value pairs
  • BaseTableWriter — Handles CSV and TSV formats; writes translations as rows in a tabular structure
  • BaseXmlWriter — Handles ResX and XLIFF formats; works with XML DOM structures

Basic Usage

Creating and Using a Writer

To write translations, instantiate the appropriate writer, then call one of its methods:

using Tlumach;
using Tlumach.Writers;
using System.Globalization;

// Assume you have a TranslationManager instance
var translationManager = new TranslationManager();
// ... load translations ...

// Write a single culture to JSON
var jsonWriter = new JsonWriter();
using (var fileStream = File.Create("translations.en.json"))
{
    jsonWriter.WriteTranslation(translationManager, new CultureInfo("en"), fileStream);
}

// Write the configuration file
using (var fileStream = File.Create("translations.jsoncfg"))
{
    jsonWriter.WriteConfiguration(translationManager, fileStream);
}

Single-Culture vs. Multi-Culture Formats

Most formats (JSON, ARB, INI, TOML, ResX, XLIFF) store one culture per file. For these formats, call WriteTranslation(TranslationManager, CultureInfo, Stream) once per culture:

var cultures = new[] { new CultureInfo("en"), new CultureInfo("de") };
foreach (var culture in cultures)
{
    var filename = culture.Name == "en" 
        ? "strings.json" 
        : $"strings_{culture.Name}.json";
    using (var stream = File.Create(filename))
    {
        jsonWriter.WriteTranslation(translationManager, culture, stream);
    }
}

Table formats (CSV, TSV) and Apple String Catalog (.xcstrings) support multiple cultures in one file. Use WriteTranslations(TranslationManager, IReadOnlyCollection<CultureInfo>, Stream):

var csvWriter = new CsvWriter();
var cultures = new[] { new CultureInfo("en"), new CultureInfo("de"), new CultureInfo("fr") };
using (var stream = File.Create("all_translations.csv"))
{
    csvWriter.WriteTranslations(translationManager, cultures, stream);
}

Writing the Invariant Culture

To write the default translation (the one without a specific locale), pass InvariantCulture:

using (var stream = File.Create("default.json"))
{
    jsonWriter.WriteTranslation(translationManager, CultureInfo.InvariantCulture, stream);
}

Format-Specific Notes

JSON

JsonWriter serializes translations as a hierarchical JSON object. Dot-separated keys create nested structures:

{
  "App": {
    "Title": "My Application",
    "Settings": {
      "Language": "Language"
    }
  },
  "Menu": {
    "File": "File",
    "Edit": "Edit"
  }
}

Control indentation with the IndentationStep property:

var writer = new JsonWriter { IndentationStep = 4 };

The IndentationStep property also applies to configuration files written by WriteConfiguration().

ARB

ArbWriter writes ARB (Application Resource Bundle) format, which extends JSON with metadata support. ARB is commonly used in translation workflows, especially with tools like Google Translator Toolkit.

{
  "appTitle": "My Application",
  "@@locale": "en",
  "@appTitle": {
    "context": "application title"
  }
}

Like JsonWriter, the IndentationStep property controls JSON indentation.

INI

IniWriter writes Windows INI format. Dot-separated keys become section headers:

[App.Settings]
Language=Language
Theme=Theme

[Menu]
File=File
Edit=Edit

TOML

TomlWriter writes TOML format, which is human-friendly and supports advanced string features:

[App]
Title = "My Application"

[App.Settings]
Language = "Language"
Theme = "Theme"

[Menu]
File = "File"
Edit = "Edit"

The writer automatically handles string quoting and escaping according to TOML syntax rules.

CSV

CsvWriter writes comma-separated values with proper escaping. Each row represents one translation unit, and columns hold the key and one or more culture translations.

Control the separator character via the SeparatorChar property (default: comma):

var csvWriter = new CsvWriter { SeparatorChar = ';' };

Note: Excel typically exports CSV with a semicolon as separator.

TSV

TsvWriter writes tab-separated values, similar to CsvWriter but using tabs as separators. TSV is often preferred over CSV for translation workflows because tabs are rarely used within text content and minimal escaping is needed.

ResX

ResxWriter writes .NET ResX format (XML). This format is native to the .NET ecosystem and is useful when integrating translations with .NET resource systems:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="AppTitle" xml:space="preserve">
    <value>My Application</value>
  </data>
  <data name="Menu.File" xml:space="preserve">
    <value>File</value>
  </data>
</root>

Each culture's translations are written to a separate file with the naming pattern filename.culture.resx (e.g., strings.en.resx, strings.de.resx).

Apple String Catalog

StringCatWriter writes Apple String Catalog format (.xcstrings). Unlike most other writers, a single output file holds translations for all provided locales — both WriteTranslation() (single culture) and WriteTranslations() (multiple cultures) are supported.

The output is Xcode-compatible JSON with 2-space indentation and : key-value separators:

{
  "sourceLanguage" : "en",
  "strings" : {
    "Hello World" : {
      "comment" : "A greeting shown on launch",
      "localizations" : {
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Hello World"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Bonjour le monde"
          }
        }
      }
    }
  },
  "version" : "1.0"
}

The sourceLanguage field is derived from TranslationConfiguration.DefaultFileLocale when set; otherwise it defaults to the name of the first supplied culture. The per-entry comment from Comment is included when present. Locales that have no translation for a given key are omitted from that key's localizations block.

StringCatWriter inherits from BaseJsonWriter, so the IndentationStep property is available and controls JSON indentation (default: 2). The configuration file extension is .jsoncfg (shared with the JSON and ARB writers).

XLIFF

XliffWriter writes XLIFF 2.2 (XML Localization Interchange File Format), a standardized bitext format used in professional translation workflows. XLIFF files contain both source and target language text side-by-side, making them ideal for translation services and translation memory systems.

<xliff version="2.0" srcLang="en" trgLang="de">
  <file id="strings">
    <unit id="AppTitle">
      <segment>
        <source>My Application</source>
        <target>Meine Anwendung</target>
      </segment>
    </unit>
  </file>
</xliff>

The SourceFile and TargetFile properties of the writer control additional metadata in the XLIFF output. For more detailed information, see the XLIFF Guide.

Common Scenarios

Format Conversion

Convert translations from one format to another:

// Load from INI
IniParser.Use();
var translationManager = new TranslationManager();
translationManager.LoadFromDisk = true;
translationManager.LoadDefaultTranslation("translations.ini");

// Write to JSON
var jsonWriter = new JsonWriter();
var cultureInfo = new CultureInfo("en");
using (var stream = File.Create("translations.json"))
{
    jsonWriter.WriteTranslation(translationManager, cultureInfo, stream);
}

Batch Export

Export translations for multiple cultures and formats:

var cultures = new[] { new CultureInfo("en"), new CultureInfo("de"), new CultureInfo("fr") };
var exportDirectory = "exports";
Directory.CreateDirectory(exportDirectory);

var jsonWriter = new JsonWriter();
var csvWriter = new CsvWriter();

// Export to JSON (one file per culture)
foreach (var culture in cultures)
{
    var filename = Path.Combine(exportDirectory, $"strings_{culture.Name}.json");
    using (var stream = File.Create(filename))
    {
        jsonWriter.WriteTranslation(translationManager, culture, stream);
    }
}

// Also export all at once to CSV
using (var stream = File.Create(Path.Combine(exportDirectory, "all_strings.csv")))
{
    csvWriter.WriteTranslations(translationManager, cultures, stream);
}

Round-Trip Editing

Read translations, modify them, and write them back:

// Load
JsonParser.Use();
var translationManager = new TranslationManager();
translationManager.LoadFromDisk = true;
translationManager.LoadDefaultTranslation("strings.json");

// Modify (e.g., via UI or programmatically)
var translation = translationManager.GetTranslation(new CultureInfo("en"));
if (translation.TryGetValue("AppTitle", out var entry))
{
    // Modify the translation text
    // ...
}

// Write back
var jsonWriter = new JsonWriter();
using (var stream = File.Create("strings_updated.json"))
{
    jsonWriter.WriteTranslation(translationManager, new CultureInfo("en"), stream);
}

Exporting for Translation Services

Prepare translations for external translation services (e.g., converting to XLIFF for professional translation):

// Load current translations
JsonParser.Use();
var translationManager = new TranslationManager();
translationManager.LoadFromDisk = true;
translationManager.LoadDefaultTranslation("strings.json");

// Export to XLIFF for translation
var xliffWriter = new XliffWriter();
var sourceLanguage = new CultureInfo("en");
var targetLanguage = new CultureInfo("de");

using (var stream = File.Create("strings_de.xliff"))
{
    xliffWriter.WriteTranslation(translationManager, targetLanguage, stream);
}

Configuration File Export

Export the translation configuration:

var jsonWriter = new JsonWriter();
using (var stream = File.Create("translations.jsoncfg"))
{
    jsonWriter.WriteConfiguration(translationManager, stream);
}

The configuration file includes settings like the default file, default locale, and generated class namespace.

Advanced Topics

Implementing a Custom Writer

To create a writer for a custom or unsupported format, extend the appropriate base class:

using Tlumach.Base;
using Tlumach.Writers;

public class CustomYamlWriter : BaseWriter
{
    public override string FormatName => "YAML";
    public override string ConfigExtension => ".yamlcfg";
    public override string TranslationExtension => ".yaml";

    public override void WriteConfiguration(TranslationManager translationManager, Stream stream)
    {
        // Implement configuration serialization
    }

    public override void WriteTranslations(TranslationManager translationManager, 
        IReadOnlyCollection<CultureInfo> cultures, Stream stream)
    {
        throw new NotSupportedException("YAML format does not support multiple cultures per file.");
    }

    public override void WriteTranslation(TranslationManager translationManager, 
        CultureInfo culture, Stream stream)
    {
        var translation = translationManager.GetTranslation(culture)
            ?? throw new TlumachException($"No translation found for culture {culture.Name}");

        // Implement translation serialization to YAML
    }

    protected override void InternalWriteTranslations(TranslationManager translationManager,
        IReadOnlyCollection<CultureInfo> cultures, Stream stream)
    {
        throw new NotSupportedException("YAML format does not support multiple cultures per file.");
    }
}

Format-Specific Configuration

Many writers expose properties to customize output:

  • JsonWriter, ArbWriter, and StringCatWriterIndentationStep (default: 2)
  • CsvWriterSeparatorChar (default: ',')
  • XliffWriterSourceFile, TargetFile (metadata properties)

Example:

var csvWriter = new CsvWriter { SeparatorChar = ';' };
var jsonWriter = new JsonWriter { IndentationStep = 4 };

Error Handling

Writers throw exceptions if the operation is not supported for the format. For example, calling WriteTranslations() on a JsonWriter (which supports only one culture per file) throws a TlumachException:

var jsonWriter = new JsonWriter();
var cultures = new[] { new CultureInfo("en"), new CultureInfo("de") };

try
{
    using (var stream = File.Create("translations.json"))
    {
        // This throws an exception because JSON does not support multiple cultures per file
        jsonWriter.WriteTranslations(translationManager, cultures, stream);
    }
}
catch (TlumachException ex)
{
    Console.WriteLine($"Error: {ex.Message}");
}

Preserving Entry Order

By default, writers sort entries hierarchically by key. To preserve the original entry order from parsing, set the KeepEntryOrder property to true before parsing:

BaseParser.KeepEntryOrder = true;
JsonParser.Use();
// ... load translations ...