using Lua; namespace AIStudio.Tools.PluginSystem; public sealed class PluginLanguage : PluginBase, ILanguagePlugin { private static readonly ILogger<PluginLanguage> LOGGER = Program.LOGGER_FACTORY.CreateLogger<PluginLanguage>(); private readonly Dictionary<string, string> content = []; private readonly List<ILanguagePlugin> otherLanguagePlugins = []; private readonly string langCultureTag; private readonly string langName; private ILanguagePlugin? baseLanguage; public PluginLanguage(bool isInternal, LuaState state, PluginType type) : base(isInternal, state, type) { if(!this.TryInitIETFTag(out var issue, out this.langCultureTag)) this.pluginIssues.Add(issue); if(!this.TryInitLangName(out issue, out this.langName)) this.pluginIssues.Add(issue); if (this.TryInitUITextContent(out issue, out var readContent)) this.content = readContent; else this.pluginIssues.Add(issue); } /// <summary> /// Sets the base language plugin. This plugin will be used to fill in missing keys. /// </summary> /// <param name="baseLanguagePlugin">The base language plugin to use.</param> public void SetBaseLanguage(ILanguagePlugin baseLanguagePlugin) => this.baseLanguage = baseLanguagePlugin; /// <summary> /// Add another language plugin. This plugin will be used to fill in missing keys. /// </summary> /// <remarks> /// Use this method to add (i.e., register) an assistant plugin as a language plugin. /// This is necessary because the assistant plugins need to serve their own texts. /// </remarks> /// <param name="languagePlugin">The language plugin to add.</param> public void AddOtherLanguagePlugin(ILanguagePlugin languagePlugin) => this.otherLanguagePlugins.Add(languagePlugin); /// <summary> /// Tries to initialize the IETF tag. /// </summary> /// <param name="message">The error message, when the IETF tag could not be read.</param> /// <param name="readLangCultureTag">The read IETF tag.</param> /// <returns>True, when the IETF tag could be read, false otherwise.</returns> private bool TryInitIETFTag(out string message, out string readLangCultureTag) { if (!this.state.Environment["IETF_TAG"].TryRead(out readLangCultureTag)) { message = "The field IETF_TAG does not exist or is not a valid string."; readLangCultureTag = string.Empty; return false; } if (string.IsNullOrWhiteSpace(readLangCultureTag)) { message = "The field IETF_TAG is empty. Use a valid IETF tag like 'en-US'. The first part is the language, the second part is the country code."; readLangCultureTag = string.Empty; return false; } if (readLangCultureTag.Length != 5) { message = "The field IETF_TAG is not a valid IETF tag. Use a valid IETF tag like 'en-US'. The first part is the language, the second part is the country code."; readLangCultureTag = string.Empty; return false; } if (readLangCultureTag[2] != '-') { message = "The field IETF_TAG is not a valid IETF tag. Use a valid IETF tag like 'en-US'. The first part is the language, the second part is the country code."; readLangCultureTag = string.Empty; return false; } // Check the first part consists of only lower case letters: for (var i = 0; i < 2; i++) if (!char.IsLower(readLangCultureTag[i])) { message = "The field IETF_TAG is not a valid IETF tag. Use a valid IETF tag like 'en-US'. The first part is the language, the second part is the country code."; readLangCultureTag = string.Empty; return false; } // Check the second part consists of only upper case letters: for (var i = 3; i < 5; i++) if (!char.IsUpper(readLangCultureTag[i])) { message = "The field IETF_TAG is not a valid IETF tag. Use a valid IETF tag like 'en-US'. The first part is the language, the second part is the country code."; readLangCultureTag = string.Empty; return false; } message = string.Empty; return true; } private bool TryInitLangName(out string message, out string readLangName) { if (!this.state.Environment["LANG_NAME"].TryRead(out readLangName)) { message = "The field LANG_NAME does not exist or is not a valid string."; readLangName = string.Empty; return false; } if (string.IsNullOrWhiteSpace(readLangName)) { message = "The field LANG_NAME is empty. Use a valid language name."; readLangName = string.Empty; return false; } message = string.Empty; return true; } #region Implementation of ILanguagePlugin /// <summary> /// Tries to get a text from the language plugin. /// </summary> /// <remarks> /// When the key neither in the base language nor in this language exist, /// the value will be an empty string. Please note that the key is case-sensitive. /// Furthermore, the keys are in the format "root::key". That means that /// the keys are hierarchical and separated by "::". /// </remarks> /// <param name="key">The key to use to get the text.</param> /// <param name="value">The desired text.</param> /// <param name="logWarning">When true, a warning will be logged if the key does not exist.</param> /// <returns>True if the key exists, false otherwise.</returns> public bool TryGetText(string key, out string value, bool logWarning = false) { // First, we check if the key is part of the main language pack: if (this.content.TryGetValue(key, out value!)) return true; // Second, we check if the key is part of the other language packs, such as the assistant plugins: foreach (var otherLanguagePlugin in this.otherLanguagePlugins) if(otherLanguagePlugin.TryGetText(key, out value)) return true; // Finally, we check if the key is part of the base language pack. This is the case, // when a language plugin does not cover all keys. In this case, the base language plugin // will be used to fill in the missing keys: if(this.baseLanguage is not null && this.baseLanguage.TryGetText(key, out value)) return true; if(logWarning) LOGGER.LogWarning($"Missing translation key '{key}'."); value = string.Empty; return false; } /// <inheritdoc /> public string IETFTag => this.langCultureTag; /// <inheritdoc /> public string LangName => this.langName; /// <inheritdoc /> public IReadOnlyDictionary<string, string> Content => this.content.AsReadOnly(); #endregion }