Based on the Unity existing file management system implementation of the localization framework, the code has been uploaded to Git project GRUnityTools, can be directly downloaded source or through UPM use

Resources, AssetBundle, and Addressable resource management are now implemented, and can be expanded freely on demand

Provides line-by-line parsing, CSV parsing and JSON parsing, three localized text parsing methods, can also be expanded according to demand

Original address of this article: Unity Practice – Localization framework implementation

Achieve the goal

  1. Support for language extensibility
  2. Scalability of loading methods
  3. Extensibility of text parsing
  4. Load localized resources dynamically
  5. Fast way to update resources

implementation

  1. SystemLanguage

    To ensure uniform specifications for localized content across different projects and development objects, SystemLanguage enumerations are directly used as unique ids for various languages

  2. ILocalizationLoader

    In the form of a custom interface, the loading method is open to developers to implement, so that different project implementations can be customized as freely as possible

  3. ILocalizationParser

    Through the form of custom interface, the text parsing method is open to developers to implement, so that different project implementations can be customized to the maximum freedom

  4. Resources, AssetBundle and Addressable

    The ILocalizationLoader interface is implemented using the three methods of dynamically loading resources provided by Unity, providing three default dynamic loaders for localized resources

  5. LocalizationComponent

    To further simplify the development process, this script is provided to realize automatic batch update of localized controls when switching languages, supporting Text, TextMesh, Image and SpriteRender

The project structure

  • LocalizationManager
    • ILocalizationLoader
      • LocalizationResourcesLoader
      • LocalizationAssetBundleLoader
      • LocalizationAddressableLoader
      • . LocalizationCustomLoader
    • IlocalizationParser
      • LocalizationDefaultParser
      • . LocalizationCustomParser
  • LocalizationComponent
    • LocalizationComponentItem

The implementation process

In order to save space, part of the sample code has been modified, the specific code can be seen in the project GRUnityTools, and includes the use of the example

LocalizationManager

Originally designed for simple functions of loading and switching localized text, all loading and parsing is done by the LocalizationManager singleton

First thought is how to make different projects with the same sets of words, from my personal supplement will have gaps and cannot cover all the needs, finally decided to use the system to provide SystemLanguage enumerated type, basic covers all application degree is relatively wide language, also confirmed by the enumeration name as localized text file name used to load, Ordinal Numbers can be added as language order

Build file data structures that match language load files

public struct LocalizationFile { public int Index { get; } public SystemLanguage Type { get; } public string Name { get; } public string FileName { get; } public LocalizationFile(string fileName = null) { if (! string.IsNullOrEmpty(fileName)) { Name = fileName; string[] lan = fileName.Split('.'); Name = lan[1]; bool success; int index; SystemLanguage lanuageType; success = Int32.TryParse(lan[0], out index); Index = success ? index : -1; success = Enum.TryParse(Name, true, out lanuageType); Type = success ? lanuageType : SystemLanguage.Unknown; }}}Copy the code

LocalizatioManager singleton initialization uses Resources to get all text files in the specified directory to get a list of supported languages

TextAsset[] res = Resources.LoadAll<TextAsset>("Localization");
List<LocalizationFile> fileList = new List<LocalizationFile>();

for (int i = 0; i < res.Length; i++)
{
    TextAsset asset = res[i];
    LocalizationFile data = new LocalizationFile(asset.name);
    fileList.Add(data);
    Resources.UnloadAsset(asset);
}
Copy the code

Use the interface provided by Resources to load additional localized Resources under the specified subdirectory

Resources.LoadAsync<T>("Localization/Assets/" + assetPath);
Copy the code

The original use of custom text rules TXT files, a key and value pair per line, equal sign to separate the key and value, using the regex.unescape method to escape the text, LocalizationManager holds the parsed dictionary data for use by external controls

public Dictionary<string, string> ParseTxt(string txt) { if (string.IsNullOrEmpty(txt)) { return null; } string[] lines = txt.Split('\n'); Dictionary<string, string> localDict = new Dictionary<string, string>(); foreach (string line in lines) { if (! string.IsNullOrEmpty(line)) { string[] keyAndValue = line.Split(new[] {'='}, 2); if (keyAndValue.Length == 2) { string value = Regex.Unescape(keyAndValue[1]); localDict.Add(keyAndValue[0], value); } } } return localDict; }Copy the code

ILocalizationLoader

Due to the limitations and limitations of Resources, a more flexible method of dynamically loading Resources is needed, so support for AssetBundle and Addressable has been added. For information on both methods, see The Article Unity – Resource Management Overview

When designing these two loading methods, we realized that providing ready-made loading methods would have a lot of constraints and could not be universal, so we decided to isolate the two fixed methods required by LocalizationManager as interfaces to achieve external customization

Public interface ILocalizationLoader {// LoadAllFileListAsync(Action<LocalizationFile[]> complete); Void LoadAssetAsync<T>(string localizationFileName, string assetPath, bool defaultAsset, Action<T> complete) where T : Object; }Copy the code

Will first before loading ways of the Resources of rewriting in order to realize the interface LocalizationResourcesLoader, And extend the LocalizationAssetBundleLoader and LocalizationAddressableLoader two loaders

LocalizationAssetBundleLoader

Will different language AssetBundle packaging to Assets/StreamingAssets/Localization directory, and use the naming names similar to the Resources of Resources, Load the Localization manifest to obtain the Bundle list of the supported language

string bundlePath = Path.Combine(Application.streamingAssetsPath, FilesPath, FilesPath); // Load the automatically generated Localization Bundle var loadRequest = AssetBundle.LoadFromFileAsync("Assets/StreamingAssets/Localization/Localization"); Loadrequest.com pleted += operation => {// Obtain the Manifest file information of the Localization Bundle AssetBundleManifest Manifest = loadrequest.assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest"); var files = manifest.GetAllAssetBundles(); List<LocalizationFile> fileList = new List<LocalizationFile>(); for (int i = 0; i < files.Length; i++) { if (! files[i].Equals(CommonAssetsPath.ToLower())) { LocalizationFile data = new LocalizationFile(files[i]); fileList.Add(data); } } loadrequest.assetBundle.Unload(manifest); }Copy the code

Load resources

AssetBundleCreateRequest loadrequest = AssetBundle.LoadFromFileAsync(Path.Combine(Application.streamingAssetsPath, FilesPath,
                    localizationFileName));
loadrequest.completed += operation =>
{
    var request = loadrequest.assetBundle.LoadAssetAsync<T>(assetPath);
    request.completed += asyncOperation => 
    { 
      	//get request.asset
    };
};  
Copy the code

Also added shortcuts to make it easier to package localized assetBundles

string[] paths = Directory.GetDirectories(localizationFilePath); AssetBundleBuild[] buildMap = new AssetBundleBuild[paths.Length]; for (int i = 0; i < paths.Length; i++) { string bundleName = Path.GetFileName(paths[i]); buildMap[i].assetBundleName = bundleName; var files = Directory.GetFiles(paths[i], "*", SearchOption.AllDirectories); List<string> assets = new List<string>(); foreach (var file in files) { if (! file.EndsWith(".meta")) { var filePath = file.Replace(Application.dataPath, "Assets"); assets.Add(filePath); } } buildMap[i].assetNames = assets.ToArray(); } var parent = Path.GetFileName(localizationFilePath); BuildPipeline.BuildAssetBundles("Assets/StreamingAssets/" + parent, buildMap, BuildAssetBundleOptions.ChunkBasedCompression, target);Copy the code

LocalizationAddressableLoader

Due to Addressable, it is impossible to obtain files and supported languages through a fixed resource path. Therefore, Addressable’s Label function is used to Label localized text files of each language with Localization to achieve unified access. The text file address must be the same as the corresponding SystemLanguage

Addressables.LoadResourceLocationsAsync(KLocalizeFolder).Completed += handle => { List<LocalizationFile> fileList = new List<LocalizationFile>(); if (handle.Status == AsyncOperationStatus.Succeeded) { foreach (var file in handle.Result) { LocalizationFile data = new  LocalizationFile(file.PrimaryKey); fileList.Add(data); }}};Copy the code

Load resources using Addressables

Addressables.LoadAssetAsync<T>(assetAddress).Completed += handle =>
{
    //get request.asset
};
Copy the code

ILocalizationParser

Since the text parsing format is too personalized and not universal, the extension supports CSV and JSON parsing, and extracts parsing methods to ILocalizationParser in the same way as ILocalizationLoader to increase scalability

public interface ILocalizationParser
{
    Dictionary<string, string> Parse(string text);
}
Copy the code

LocalizationDefaultParser

Project will the TXT, CSV and json parsing methods summary to LocalizationDefaultParser LocalizationFileType decision analytical way

LocalizationComponent

LocalizationComponent is a mount in the objects of the scene for the script, used to automatically update text localization and resources to use LocalizationComponentItem key Component matching localization in the Inspector

[Serializable] public class LocalizationComponentItem { [SerializeField] internal Component component; public string localizationKey; public string defaultValue; public bool setNativeSize; internal Vector2 originalImageSize = Vector2.zero; } public class LocalizationComponent : MonoBehaviour { public LocalizationComponentItem[] items; private void LocalizationChanged(LocalizationFile localizationFile) { foreach (var item in items) { if (! string.IsNullOrEmpty(item.localizationKey) && item.component ! = null) { string value = LocalizationManager.Singleton.GetLocalizedText(item.localizationKey); if (value == null) { value = item.defaultValue; } if (item.component is Text text) { text.text = value; } else if (item.component is TextMesh mesh) { mesh.text = value; } else { Image image = null; SpriteRenderer spriteRenderer = null; if (item.component is Image imageComponent) { image = imageComponent; if (item.originalImageSize == Vector2.zero) { item.originalImageSize = image.rectTransform.sizeDelta; } } if (item.component is SpriteRenderer rendererComponent) { spriteRenderer = rendererComponent; } if (image ! = null || spriteRenderer ! = null) {/ / replace pictures LocalizationManager. Singleton. LoadLocalizationAssetAsync (the value of the item. The defaultValue, delegate(Sprite sprite) { if (sprite ! = null) { if (image ! = null) { Image img = (Image) item.component; img.sprite = sprite; if (item.setNativeSize) { img.SetNativeSize(); } else { image.rectTransform.sizeDelta = item.originalImageSize; } } if (spriteRenderer ! = null) { spriteRenderer.sprite = sprite; }}}); } } } } } }Copy the code