Engineeringこの記事を日本語で読む

Unity Localization package: a CSV workflow that survives updates

Unity's Localization package can export a String Table Collection to CSV and import a translated CSV back into it. The mechanism is a handful of clicks, and every tutorial stops there — which is why so many teams get through their first translation batch smoothly and then discover, three updates later, that they cannot tell what changed, that renaming a key orphaned two hundred translations, and that half the Smart Strings came back broken.

The part worth designing is not the export button. It is the operational shape around it: what identifies a row over the life of the project, which columns a translator actually needs, how you send only the lines that changed, and how you prove nothing was lost before you import. Get those right and the CSV round trip is boring for years. Get them wrong and every batch is an archaeology exercise.

This article works through those decisions in the order you have to make them. The exact column configuration and importer options live in the package documentation for the version you are on, and they do move between versions — what follows is about the choices those options are there to serve.

Decide what identifies a row before you export anything

Entries in a String Table have two identities: a numeric id that the package assigns and keeps stable, and a key, which is the human-readable name your code looks up and which anybody can rename at any time. This distinction is the single most important thing to understand about the CSV round trip.

If your exported CSV carries only the key, then renaming a key in Unity — something that happens naturally as a project is refactored — turns into a new, untranslated entry on the next import, while the old translations sit orphaned under a key nothing references. Include the id column, and let the key ride along as human-facing information. The importer can then match on the stable identity while a translator still sees a name that means something.

Decide the file granularity at the same time. One CSV per table collection is almost always right: the files stay small enough to diff usefully, you can send one system out for translation without touching the rest, and a mistake in one import is contained. A single monolithic export across every collection feels tidier and is much worse to operate.

Column design: what a translator actually needs in the sheet

The minimum for a working sheet is four things: the stable id, the key, the source-language value, and an empty column for the target language. That is enough to translate and import, and it is not enough to translate well.

Add comment columns and use them. A shared comment applies to the entry regardless of language and is where context belongs — what screen this appears on, who says it, whether the words after a colon are a variable, and any hard limit on length. A per-locale comment column gives the translator a channel to send questions and notes back to you inside the same file, instead of in a chat message that gets lost. Both of these cost you nothing at export time and remove most of the back-and-forth that otherwise happens by email.

You can also carry columns the importer never reads — a status column, a batch number, a screenshot reference — as long as your column mapping is explicit about which columns matter. Confirm that behaviour on a throwaway file first rather than assuming it, because it is a mapping question and the mapping is configurable.

Id,Key,Shared Comments,Japanese(ja),English(en),English(en) Comments,Status
1042,menu.settings.title,Settings screen title. 12 chars max.,設定,Settings,,done
1043,shop.buy_confirm,{0} is the item name.,{0}を購入しますか?,Buy {0}?,,done
1044,quest.reward_gold,{0} is a number. Smart String.,{0}ゴールドを手に入れた,,Plural for 1?,question

Smart Strings and the placeholder rules

Smart Strings are where the useful power and the breakage both live. Braces in the string name a variable and can carry formatting, plural selection, and conditional logic, which means the contents of the braces are code that happens to be sitting inside a sentence. A translator is expected to move a placeholder to wherever their language's grammar wants it — that is exactly the point — and must never rename it, delete it, duplicate it, or add a space inside the braces.

The failures are boringly consistent. A Japanese IME inserts full-width braces that look almost identical and match nothing. A translator helpfully translates the variable name inside the braces. A plural or conditional branch gets flattened to one form because it looked like duplicated text. Any of these produces either a runtime error or a string that renders with visible braces in the shipped game, and none of them are catchable by reading the sheet at normal speed.

Two defences, both needed. Put the rules in a one-page sheet that goes out with every batch, with a correct and an incorrect example rather than a description. And check the placeholder sets mechanically before import: for every row, the set of placeholders in the translation should match the set in the source, allowing for order. It is a small script and it will pay for itself in the first batch.

One more thing to verify early: whether an entry is a Smart String is a property of the entry rather than of the text, so confirm on a test file whether that property survives your export and import at all. If it does not travel through the CSV, then toggling it is something you do in Unity and never through the sheet — which is fine, as long as you know it before a translator hands you back a file that silently reverted a dozen entries to plain strings.

Send diffs, not the whole table

After the first batch, exporting everything every time is the default and it is wrong. It makes a translator re-read thousands of unchanged rows to find the forty that matter, it makes review impossible, and it multiplies the chance of an accidental edit to a line that was already approved and shipped.

There are two patterns that work. The first is to commit the exported CSV to version control after every batch, so that the next export can be diffed against it and the changed rows extracted by a script you own. The diff is authoritative, costs nothing to produce, and doubles as your record of what was sent when. The second is to keep a status column in your own tracking sheet and export a filtered subset — more manual, but workable for a small project where one person owns the whole pipeline.

Before you rely on partial files, resolve one question on a throwaway branch: what does your import do with entries that are present in the table but absent from the CSV? Left untouched is what you want for a diff workflow; removed is a reasonable behaviour for a full-replacement workflow and a catastrophe if you assumed the other one. Test it deliberately rather than finding out on a real table.

The file itself: encoding, spreadsheets, and quoting

CSV is a fragile container for game text, and most of the damage happens between your export and the translator's return file rather than inside Unity. Agree the encoding explicitly and check the returned file rather than trusting it, because a spreadsheet application opening a UTF-8 file with the wrong assumption will happily rewrite every Japanese character into garbage and save it back without a warning.

Quoting is the other fragile part. Game text contains commas, quotation marks, and line breaks inside a single string, all of which CSV handles through quoting rules that a hand-edited file breaks easily. If a translator's editor converts straight quotes to typographic ones, normalizes line endings, or strips a trailing space that was doing work, the file can come back structurally valid and semantically wrong. Handing out a shared spreadsheet and exporting from it yourself removes an entire class of these problems, at the cost of one more step.

Keep every exported and returned CSV in version control, named by batch and date. When someone asks in six months whether a line was ever sent for translation, that archive is the answer, and it takes no effort to maintain.

Checks before you import, and after

Before importing, run mechanical checks on the returned file. Row count matches what you sent. Ids are present, unique, and unchanged. No row that had a translation before now has an empty cell. Placeholder sets match per row. No cell contains the source text copied verbatim into the target column, which is the signature of a row that was skipped rather than translated.

Import onto a branch, never straight onto the mainline. The table assets are files like any other, so the import produces a diff you can actually review: the number of changed entries should match the number of rows you sent, and anything beyond that is a signal to stop and look before merging.

Then run the game. Automated checks catch mechanical breakage — missing placeholders, empty entries, encoding damage — but no script can tell you that a menu label is now technically correct and completely wrong for the screen it sits on. Switch locale, walk the screens the batch touched, and look at the text where players will see it. The CSV round trip is the plumbing; reading the result in context is the part that decides whether the translation is any good.

  • Row count and ids match the file you sent
  • No previously translated entry came back empty
  • Placeholder sets match the source row for row, order aside
  • No target cell is a verbatim copy of the source
  • Import happens on a branch and the asset diff is reviewed before merge
  • The affected screens are checked in the running game, in the target locale

Related articles