Skip to content

FOS Localization

Version 1.0.0 · requires FOS Essentials Core 1.0.0 or later

Translates the text and the images of a VRChat world into as many languages as you want, edited in a real table instead of two disconnected arrays, with CSV round-trip for translators and the player’s choice remembered between visits. Requires FOS Essentials Core 1.0.0.


  1. Create an empty GameObject and add Add Component > FOS Essentials > Localization > Locale Manager.
  2. Fill Language codes (en, fr, es, de) and Language names (English, Français, Español, Deutsch). The first one is the pivot every other language falls back to, so put the language you write the world in first.
  3. Open FOS Essentials > FOS Localization > Translation Table and add your keys.
  4. On every text of your interface, add Add Component > FOS Essentials > Localization > Localized Text, point it at the manager and give it its key.
  5. For the language switcher, add Language Button to each button, one per language. Its inspector wires the OnClick for you.

Component Role
FOSLocaleManager One per world. Holds the languages, the table and the current language.
FOSLocalizedText Binds a key to a TMP_Text or a legacy Text.
FOSLocalizedImage Swaps a sprite per language.
FOSLanguageButton Switches the world to one language, with an optional selected/unselected highlight.

Everything here is local. No synced variable, no network traffic, no ownership. Two players standing next to each other have no reason to read the same language, and synchronising the choice would mean one of them loses it every time the other clicks.


FOS Essentials > FOS Localization > Translation Table, or the button in the manager’s inspector. One row per key, one column per language, and the empty cells are coloured so a gap is visible instead of having to be looked for.

Colour Meaning
Red The first language is empty. Nothing to fall back on: the world will show #key.
Amber Another language is empty. The world falls back to the first language.

The first declared language is the pivot: it is what every other language falls back to. Put the language you actually write the world in first.

The toolbar filters by text — the search looks inside the translations too, so a sentence spotted in game is enough to find its key — and Untranslated only narrows the table down to the rows that still need work.

The keys and the translations are stored as two arrays, and they are hidden from the inspector on purpose. The grid is flat: translation j of key i lives at i × languages + j. A key added by hand in the array, without its row of cells, shifts every text after it by one — with no error, nothing visible on screen, and no way to reconstruct the original. The window is therefore the only write path, and it keeps the two arrays in step.

Adding or removing a language in the inspector is fine: the manager reports the mismatch and repairs the grid, moving every translation to its new position.


Export CSV writes the whole table, one row per key, one column per language, with the language codes as headers. Import CSV reads it back.

The import is a merge, never a replacement:

  • A key present in the file but not in the table is added.
  • A key in the table but absent from the file is left alone.
  • A column whose header matches no declared language is ignored — a comments column costs nothing.
  • An empty cell never overwrites an existing translation.

So a single language can be sent out for review and merged back without any risk to the others, and a translator who returns a half-finished column destroys nothing.

The file is written as UTF-8 with a BOM. Without it Excel opens the file in the local code page and mangles every accent, which is the single most common way a translation round-trip goes wrong.


A key is an identifier, not a sentence: menu.title, rules.adult.warning, teleport.button.spawn. Prefix by area so that a family stays together once the table is sorted — the Sort button orders keys alphabetically, taking their translations with them, and that is what makes a missing member of a family visible.

A missing key shows as #the.key in game. That is deliberate: a translation hole has to be noticed, and a blank label never is.

Line breaks are written \n in the table. An inspector field cannot take a real line break, and a CSV round-trip would not survive one.


VRChat does not tell a world which language a player’s client is in. There is no API for it, so no tool can detect it — this one included. A player arrives in the default language and switches by hand.

What can be done is remembering their choice. With Remember the choice on, the language is written to VRChat PlayerData and restored on the next visit to this world.

  • Nothing leaves the world, and nothing is shared with other players.
  • The choice is stored under the language code, never its index, so reordering or inserting a language does not silently move everyone somewhere else.
  • A language removed from the world since a player’s last visit leaves them on the default one instead of on nothing.

PlayerData is not readable at Start. The default language is applied straight away and the saved one replaces it on OnPlayerRestored, which is why a returning player may see one frame of the default language.


The lookup is resolved once, at Start: each FOSLocalizedText remembers the index of its key and never searches again. A language switch is then one indexed read per text.

Searching by name on every switch — which is the obvious way to write this — costs texts × keys string comparisons each time somebody clicks a flag. On a tablet UI with a hundred labels and two hundred keys, that is twenty thousand string comparisons per click, in Udon.

Components register themselves with the manager, so there is no list to fill by hand and nothing to update when a page gains a label.


Call On Effect
FOSSetLanguage(int) FOSLocaleManager Switches language and saves the choice
FOSCurrentLanguage() FOSLocaleManager Index of the language being displayed
FOSIndexOfLanguage(string) FOSLocaleManager Index of a language code, -1 if unknown
FOSIndexOfKey(string) FOSLocaleManager Index of a key, -1 if unknown. Call it once
FOSTextAt(int) FOSLocaleManager Translation of an already-resolved key
FOSText(string) FOSLocaleManager Translation of a key, lookup included
FOSRegister(UdonSharpBehaviour) FOSLocaleManager Follow language changes
SelectLanguage() FOSLanguageButton The control’s action

Any UdonSharpBehaviour can call FOSRegister(this) and expose a public FOSApplyLanguage() — it will then be notified on every switch, exactly like the components shipped here. That is the extension point for anything this pack does not cover: an audio source per language, a video player, a sign texture.


  • One manager per world is the intended setup. Several work, but each has its own table and its own current language, and a player switching one does not switch the other.
  • Texts created at runtime are not picked up. Call FOSRegister on them yourself.
  • The language is not synchronised, by design. See above.
  • Quest and PC behave identically: there is no platform-specific code, and there could not be — Udon is compiled once for both.