How the External Language Pack System Works in Undertale Changer Template
The external language pack system allows the Undertale Changer template to discover, enumerate, and load unlimited user-created language packs from the Assets/LanguagePacks directory at runtime without requiring code changes or recompilation.
The Undertale Changer template ships with three built-in language packs—Simplified Chinese (CN), Traditional Chinese (TCN), and English (US)—but the external language pack system extends this capability by scanning a designated folder for additional packs. This architecture enables modders and translators to distribute language packs as simple file directories rather than Unity asset bundles.
Architecture Overview
The system relies on three core components that handle discovery, data loading, and UI presentation:
| Component | Responsibility | Key Source Location |
|---|---|---|
| MainControl | Stores the global pack ID (languagePackId), distinguishes internal from external counts, and loads the LanguagePackControl asset. |
[MainControl.cs lines 113‑116](https://github.com/arch-aik/undertale-changer-template/blob/main/Assets/Scripts/UCT/Core/MainControl.cs#L113) |
| DataHandlerService | Provides helper methods for ID conversion (GetLanguageInsideId), file loading (LoadLanguageData), directory scanning (LanguagePackDetection), and full‑width text handling. |
[DataHandlerService.cs lines 75‑110](https://github.com/arch-aik/undertale-changer-template/blob/main/Assets/Scripts/UCT/Service/DataHandlerService.cs#L75) |
| SettingsController | Renders the language selection UI, traverses internal and external directories to build the option list, and persists the chosen ID to PlayerPrefs. |
[SettingsController.cs lines 857‑889](https://github.com/arch-aik/undertale-changer-template/blob/main/Assets/Scripts/UCT/Settings/SettingsController.cs#L857) |
How External Language Pack Discovery Works
The system distinguishes between internal packs (indices 0‑2) and external packs (indices 3 and above). This mapping ensures that built‑in assets remain accessible via Unity’s Resources system while external packs are loaded from the file system.
Directory Scanning
During initialization, DataHandlerService scans the Assets/LanguagePacks directory to count available external packs:
private string TraverseLanguagePackages(string pathStringSaver, bool isExternal)
{
var basePath = isExternal ? Application.dataPath + "/LanguagePacks"
: "TextAssets/LanguagePacks/";
var languagePackCount = isExternal
? Directory.GetDirectories(basePath).Length
: MainControl.LanguagePackageInternalNumber;
// ... enumeration logic
}
- Internal path:
TextAssets/LanguagePacks/(inside Unity’s Resources). - External path:
Application.dataPath + "/LanguagePacks"(physical folder next to theAssetsdirectory).
Global ID Mapping
The GetLanguagePackagesOptionFrom method converts local indices to global IDs:
private static int GetLanguagePackagesOptionFrom(int i, bool isExternal)
{
if (!isExternal) return i; // internal: 0, 1, 2
return MainControl.LanguagePackageInternalNumber + i; // external: 3, 4, ...
}
MainControl.LanguagePackageInternalNumber is hardcoded to 3, meaning external packs start at index 3.
Loading Text Assets at Runtime
When the game requests localized text, DataHandlerService.LoadLanguageData resolves the source based on the current languagePackId:
public static string LoadLanguageData(string path, int id)
{
return id < MainControl.LanguagePackageInternalNumber
? Resources.Load<TextAsset>($"TextAssets/LanguagePacks/{GetLanguageInsideId(id)}/{path}").text
: File.ReadAllText(
$"{Directory.GetDirectories(Application.dataPath + "/LanguagePacks")[id - MainControl.LanguagePackageInternalNumber]}\\{path}.txt");
}
- Internal packs use
Resources.Load<TextAsset>to pull from compiled Unity assets. - External packs use
File.ReadAllTextto read loose.txtfiles from the discovered directory.
UI Integration and Selection
The SettingsController handles user interaction:
- Building the list:
UpdateLanguagePackOptions(lines 800‑845) iterates through internal and external directories, creating UI entries for each pack. - Storing selection: When a user selects a pack, the controller sets
MainControl.Instance.languagePackId = _settingSelectedOption;and saves it toPlayerPrefs. - Applying changes: Upon exiting the settings menu,
ReturnToPreviousLayerdetects if the language changed. If so, it triggersGameUtilityService.RefreshTheScene()to reload the current scene with the new localization.
Adding a Custom Language Pack
To create and deploy an external language pack:
-
Create the directory structure:
<UnityProjectRoot>/Assets/LanguagePacks/MyCustomPack/ -
Add the metadata file
LanguagePackInformation.txt:LanguagePackName=My Custom Pack LanguagePackAuthor=Your Name LanguageBack=Back Open=Open Close=Close CultureInfo=en-US LanguagePackFullWidth=true -
Add content files matching the expected paths (e.g.,
Battle/EnemyNames.txt,Ink/Story.ink). -
Launch the game – the new pack appears in the Settings menu immediately after the built‑in options.
Key Implementation Files
| File | Purpose | Direct Link |
|---|---|---|
Assets/Scripts/UCT/Core/MainControl.cs |
Stores global language pack state and internal/external counters. | View Source |
Assets/Scripts/UCT/Service/DataHandlerService.cs |
Handles directory scanning, ID mapping, and runtime text loading. | View Source |
Assets/Scripts/UCT/Settings/SettingsController.cs |
Implements the language selection UI and persistence logic. | View Source |
Summary
- The external language pack system scans the
Assets/LanguagePacksdirectory at runtime to discover user-created localizations. - Global IDs are continuous: built-in packs occupy indices 0‑2, while external packs start at index 3.
- Loading logic branches based on the ID: internal packs use
Resources.Load<TextAsset>, while external packs useFile.ReadAllTextfrom the physical directory. - UI integration in
SettingsControllerautomatically enumerates available packs and persists the selection toPlayerPrefs. - Validation via
LanguagePackDetectionensures corrupted or out-of-range IDs default to the US pack (index 2).
Frequently Asked Questions
How does the game distinguish between built-in and external language packs?
The game uses a hardcoded threshold defined in MainControl.LanguagePackageInternalNumber (set to 3). When DataHandlerService.LoadLanguageData receives an ID, it checks if id < 3. If true, it treats the pack as internal and loads from Unity’s Resources folder; otherwise, it calculates the external directory index by subtracting 3 and reads from the file system.
What file structure is required for an external language pack?
An external pack must reside in its own subdirectory under Assets/LanguagePacks/ (e.g., Assets/LanguagePacks/MyPack/). It must contain a LanguagePackInformation.txt metadata file defining LanguagePackName, LanguagePackAuthor, and other UI strings. Content files (such as Battle/EnemyNames.txt or Ink/Story.ink) must match the paths expected by the game’s text loading logic.
Where does the system store the player’s selected language?
The selection is stored in PlayerPrefs via the SettingsController class. When a user selects a pack in the settings menu, the controller assigns the chosen index to MainControl.Instance.languagePackId and persists it. On subsequent launches, MainControl initializes the ID from PlayerPrefs, and DataHandlerService uses this value to resolve text paths.
Can external language packs override specific files without replacing the entire pack?
Yes. Because external packs are loaded via direct file system access (File.ReadAllText) rather than Unity’s asset database, you can modify individual .txt files within your external pack directory (e.g., editing Battle/EnemyNames.txt) and see changes immediately upon scene reload without rebuilding the game. The system reads files on demand, so updates are reflected as soon as the file is saved and the relevant game text is reloaded.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →