Ship localization tables as plain text files with your own format.
The built-in data-file source ships tables as JSON. This example replaces the format with plain key=value text files that translators can edit in any text editor. Build-time file generation, runtime loading, and language drop-in still work.
A file format has two halves: a runtime reader that turns a file into table data, and a writer in the Unity Editor that generates the files. For how the generated files ship and load, refer to Data file tables. The example’s file looks like this:
@collection=Menus
@guid=1f4a09e21b6c44dd8f2eab27d3a54c10
@locale=fr
# key=value, one entry per line
greeting=Bonjour
quit=Quitter
Before you start, make sure you have the following:
The reader script parses a file’s stream into a ResourceTableData snapshot, which consists of the collection header plus one EntryData per row. The reader implements ITableFileReader:
public class TextTableReader : ITableFileReader
{
public const string Extension = "txt";
public static readonly TextTableReader Instance = new();
public string FileExtension => Extension;
public ResourceTableData Read(Stream stream)
{
var data = new ResourceTableData();
using var reader = new StreamReader(stream, Encoding.UTF8, true, 1024, leaveOpen: true);
while (reader.ReadLine() is { } line)
{
// Lines starting with '#' are comments; every other line is "key=value".
if (line.StartsWith("#", StringComparison.Ordinal))
continue;
var split = line.IndexOf('=');
if (split <= 0)
continue;
var key = line[..split].Trim();
var value = line[(split + 1)..].Trim();
switch (key)
{
case "@collection":
data.CollectionName = value;
break;
case "@guid":
data.CollectionGuid = value;
break;
case "@locale":
data.LocaleCode = value;
break;
default:
// The file carries no entry ids, so the shared table data matches rows
// by key and assigns an id to any key it has not seen before.
data.Entries.Add(new EntryData { Key = key, Value = value });
break;
}
}
return data;
}
}
The provider script is the content source itself. The class derives from FileTableProvider and returns the reader. The base class handles file lookup, locale discovery, and rebuilding tables.
[Serializable]
public class TextResourceProvider : FileTableProvider
{
protected override ITableFileReader Reader => TextTableReader.Instance;
}
The writer script performs the opposite operation to the reader script. It turns a ResourceTableData snapshot into file content. The writer script runs in the Editor, so put it in an Editor folder.
public class TextTableWriter : ITableFileWriter
{
public static readonly TextTableWriter Instance = new();
public void Write(ResourceTableData data, Stream stream)
{
using var writer = new StreamWriter(stream, new UTF8Encoding(false), 1024, leaveOpen: true);
writer.WriteLine($"@collection={data.CollectionName}");
writer.WriteLine($"@guid={data.CollectionGuid}");
writer.WriteLine($"@locale={data.LocaleCode}");
foreach (var entry in data.Entries)
{
if (entry != null && !string.IsNullOrEmpty(entry.Key))
writer.WriteLine($"{entry.Key}={entry.Value}");
}
}
}
Connect the writer to the provider type with an Editor class. Without a registered Editor, Unity never generates the provider’s files. This script runs in the Editor, so put it in an Editor folder.
// Registering an editor for the provider type is what makes Unity generate its files at build time.
[AssetProviderEditor(typeof(TextResourceProvider))]
public class TextResourceProviderEditor : FileTableProviderEditor
{
public override ITableFileWriter Writer => TextTableWriter.Instance;
}
To use your provider in a project:
.txt files.StreamingAssets/LocalizationTables contains one .txt file per locale.Because FileTableProvider discovers locales from files, the format supports post-release languages without extra work. To add a new language, copy an existing locale’s .txt file, rename it to the new locale code, translate it, and relaunch. Refer to Add a language to a built Player.