This is an example of a single content directory with Localized images.
It is an example of a simple pattern for "variants" when using a content directory build.
The example is based on Unity 6.6.
The scene shows four greeting cards. A dropdown selects English, French or Spanish, and every card swaps to the image for that language. All twelve images are built into one content directory, and the player loads only the ones for the selected language.
It shows a method to ship the same content in several variants and pick one at runtime. This is similar to the AssetBundle variant functionality. There is no build-time or file-name magic
involved: the variants are ordinary data on a ScriptableObject, referenced through Loadable<T>.
| English | French | Spanish |
|---|---|---|
![]() |
![]() |
![]() |
Open Assets/Scenes/Phrasebook.unity and press Play. Pick a language from the dropdown.
To build a player, select Example > Build Player. The player is built for the active platform into
Build/Player, and the content directory is built and copied into its StreamingAssets folder as
part of that build.
flowchart LR
ROOT["PhrasebookCatalog<br/>(root asset)"] -. "Loadable<GameObject>" .-> P["HelloCard prefab<br/>Image + LocalizedImage"]
P -->|direct reference| LS["Hello.asset<br/>(LocalizedSprite)<br/>en / fr / es"]
LS -. "Loadable<Sprite>" .-> EN["Hello.en.png"]
LS -. "Loadable<Sprite>" .-> FR["Hello.fr.png"]
LS -. "Loadable<Sprite>" .-> ES["Hello.es.png"]
LS ==>|"Resolve(fr).Load()"| FR
LocalizedSpriteis aScriptableObjectwith a dictionary from language code toLoadable<Sprite>. One asset exists per phrase. Because the entries are loadable references, the build includes every language's sprite, but loading the asset loads none of them.LocalizedImagesits next to anImageon each card prefab and holds a direct reference to the phrase'sLocalizedSprite. When it is enabled, and whenever the language changes, it resolves the entry for the current language, loads that one sprite, shows it, and releases the previous one.PhrasebookCatalogis the root asset passed toBuildContentDirectory. It lists the card prefabs asLoadable<GameObject>. The build follows the references from there, so the whole phrasebook is built from this one asset.
Dotted lines are on-demand references and solid lines are direct references. A direct reference loads
with the object that holds it, which is fine for the small LocalizedSprite asset. A loadable
reference is built but only loaded when code asks for it, which is what keeps the other languages out
of memory.
The example does not need a content build to run in the Editor. In Play mode, CatalogProvider loads
PhrasebookCatalog.asset from the project, and the Loadable fields reached from it resolve to the
project's assets. In a player it registers the content directory from StreamingAssets and reads the
catalog from there with ContentLoadManager.GetRootAssets. Nothing else in the code knows the
difference.
The AudioExample shows the other approach, where Play mode registers a content directory that was built beforehand, so that the Editor and the player exercise the same code path.
Assets/Cards/<Phrase>/holds the images for one phrase, named<Phrase>.<lang>.png, and the phrase'sLocalizedSpriteasset,<Phrase>.asset. The language is a lower-case two-letter code.Assets/Prefabs/holds one card prefab per phrase: anImagewith aLocalizedImage.Assets/RootAssets/PhrasebookCatalog.assetis the root asset of the content build.Assets/Scenes/Phrasebook.unityis the only scene in the player. It holds the UI and thePhrasebookController.Assets/Scripts/LanguageSetting.cs: the current language, the list of languages, and a change event.LocalizedSprite.cs,LocalizedImage.cs,PhrasebookCatalog.cs: the three types described above.CatalogProvider.cs: returns the catalog, from the project in Play mode or from the content directory in a player.PhrasebookController.cs: fills the dropdown and instantiates the cards.
Assets/Editor/LocalizedSpriteSync.cs: anAssetPostprocessorthat fills eachLocalizedSpritefrom the images in its folder whenever they change, and warns when a language is missing. It only runs when an image is imported, so Example > Sync Localized Sprites exists for the one change it cannot see: editing the language list inLanguageSetting.CardTextureImporter.cs: imports the card images as sprites.BuildAll.cs: the Example menu items.ContentDirectoryDeployment.cs: aBuildPlayerProcessorthat builds the content directory during the player build and adds it toStreamingAssets.
Tools/New-PhraseCards.ps1generated the card images. It is only needed to change a word or add a language; the images are committed.
To add a phrase, create Assets/Cards/<Phrase>/ with a <Phrase>.<lang>.png for each language.
The sync creates <Phrase>.asset. Then make a card prefab for it and add the prefab to
PhrasebookCatalog.
To add a language, add its code and name to LanguageSetting.Available, then select
Example > Sync Localized Sprites. The Console warns for every phrase that has no image for the new
language, and those cards fall back to English until you add a <Phrase>.<lang>.png to each folder.
Adding the images updates the LocalizedSprite assets automatically.
This example keys the data by phrase, then by language: one LocalizedSprite per phrase holds every
language. That suits a prefab that shows one piece of content, because the prefab needs a single
reference regardless of the language. There are other valid approaches. Content directories place
no requirement on how variants are organized; anything reachable from a root asset through direct or
loadable references is built, and any code you write can pick which reference to load.
Some alternatives, each a reasonable fit for a different situation:
- Language first, then phrase. A
LanguagePackasset per language holds a dictionary from phrase key toLoadable<Sprite>. A translation team can deliver a whole language as one folder, and a postprocessor can fill the pack from it. Code looks up the current language's pack, then the phrase. A prefab then refers to the phrase by key rather than by a direct reference, so a missing key shows up at runtime rather than in the Inspector. - One content directory per language. With the language-first layout, each
LanguagePackcan be the root asset of its own content directory. The player ships with one language and registers others when they are present, andContentLoadManager.GetRootAssets<LanguagePack>()returns whichever are registered. The AudioExample shows several content directories working together. - Variants of the same asset rather than different assets. When the variants are the same source
asset at different import settings, for example texture quality tiers, the loadable id is the same in
every build. A high quality content directory registered after the base one takes precedence for
that id, so
Loadable<T>fields resolve to the higher quality version without any table. This is similar to how AssetBundle variants work, but at the scale of an entire content directory build. - A single lookup table. A root asset with a dictionary from a composite key such as
"Hello/fr"toLoadable<Sprite>is the smallest possible structure, and is close to loading by string from an AssetBundle. It works, but the Inspector cannot show which languages a phrase has, and nothing warns when one is missing.
Whichever layout you choose, the parts that matter are the same: every variant is behind a
Loadable<T> so that only the chosen one is loaded, and the choice is driven by a single place in the code.
Loadable<T>values inside a serializedDictionary, so one asset describes every variant of a piece of content.- A prefab that references a small
ScriptableObjectinstead of a sprite, so the prefab is not tied to one language and loading it does not load any image. - Resolving a variant at runtime and releasing the previous one, with the choice kept in one place.
- A single root asset that pulls the whole phrasebook into the content directory build through references, with no list of files to maintain.
- An
AssetPostprocessorthat keeps the data assets in step with a folder of images and reports gaps. BuildPlayerProcessor.PrepareForBuildbuilding the content directory for the active platform inside the player build, andAddAdditionalPathToStreamingAssetsshipping it with the player.- Running in Play mode from the project's assets, with no content build required.


