# How the External Language Pack System Works in Undertale Changer Template

> Discover how Undertale Changer template's external language pack system loads unlimited user-created packs at runtime without recompilation. Enhance your game's localization easily.

- Repository: [Archived AIk/undertale-changer-template](https://github.com/arch-aik/undertale-changer-template)
- Tags: internals
- Published: 2026-02-25

---

**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`](https://github.com/arch-aik/undertale-changer-template/blob/main/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`](https://github.com/arch-aik/undertale-changer-template/blob/main/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`](https://github.com/arch-aik/undertale-changer-template/blob/main/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:

```csharp
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 the `Assets` directory).

### Global ID Mapping

The `GetLanguagePackagesOptionFrom` method converts local indices to global IDs:

```csharp
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`:

```csharp
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.ReadAllText` to read loose `.txt` files from the discovered directory.

## UI Integration and Selection

The `SettingsController` handles user interaction:

1.  **Building the list**: `UpdateLanguagePackOptions` (lines 800‑845) iterates through internal and external directories, creating UI entries for each pack.
2.  **Storing selection**: When a user selects a pack, the controller sets `MainControl.Instance.languagePackId = _settingSelectedOption;` and saves it to `PlayerPrefs`.
3.  **Applying changes**: Upon exiting the settings menu, `ReturnToPreviousLayer` detects if the language changed. If so, it triggers `GameUtilityService.RefreshTheScene()` to reload the current scene with the new localization.

## Adding a Custom Language Pack

To create and deploy an external language pack:

1.  **Create the directory structure**:
    ```

    <UnityProjectRoot>/Assets/LanguagePacks/MyCustomPack/
    ```

2.  **Add the metadata file** [`LanguagePackInformation.txt`](https://github.com/arch-aik/undertale-changer-template/blob/main/LanguagePackInformation.txt):
    ```txt
    LanguagePackName=My Custom Pack
    LanguagePackAuthor=Your Name
    LanguageBack=Back
    Open=Open
    Close=Close
    CultureInfo=en-US
    LanguagePackFullWidth=true
    ```

3.  **Add content files** matching the expected paths (e.g., [`Battle/EnemyNames.txt`](https://github.com/arch-aik/undertale-changer-template/blob/main/Battle/EnemyNames.txt), `Ink/Story.ink`).

4.  **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`](https://github.com/arch-aik/undertale-changer-template/blob/main/Assets/Scripts/UCT/Core/MainControl.cs) | Stores global language pack state and internal/external counters. | [View Source](https://github.com/arch-aik/undertale-changer-template/blob/main/Assets/Scripts/UCT/Core/MainControl.cs) |
| [`Assets/Scripts/UCT/Service/DataHandlerService.cs`](https://github.com/arch-aik/undertale-changer-template/blob/main/Assets/Scripts/UCT/Service/DataHandlerService.cs) | Handles directory scanning, ID mapping, and runtime text loading. | [View Source](https://github.com/arch-aik/undertale-changer-template/blob/main/Assets/Scripts/UCT/Service/DataHandlerService.cs) |
| [`Assets/Scripts/UCT/Settings/SettingsController.cs`](https://github.com/arch-aik/undertale-changer-template/blob/main/Assets/Scripts/UCT/Settings/SettingsController.cs) | Implements the language selection UI and persistence logic. | [View Source](https://github.com/arch-aik/undertale-changer-template/blob/main/Assets/Scripts/UCT/Settings/SettingsController.cs) |

## Summary

- The **external language pack system** scans the `Assets/LanguagePacks` directory 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 use `File.ReadAllText` from the physical directory.
- **UI integration** in `SettingsController` automatically enumerates available packs and persists the selection to `PlayerPrefs`.
- **Validation** via `LanguagePackDetection` ensures 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`](https://github.com/arch-aik/undertale-changer-template/blob/main/LanguagePackInformation.txt) metadata file defining `LanguagePackName`, `LanguagePackAuthor`, and other UI strings. Content files (such as [`Battle/EnemyNames.txt`](https://github.com/arch-aik/undertale-changer-template/blob/main/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`](https://github.com/arch-aik/undertale-changer-template/blob/main/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.