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
- Support for language extensibility
- Scalability of loading methods
- Extensibility of text parsing
- Load localized resources dynamically
- Fast way to update resources
implementation
-
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
-
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
-
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
-
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
-
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
- ILocalizationLoader
- 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