Skip to content

Commit

Permalink
Merge pull request #39 from ChrisFeline/dev
Browse files Browse the repository at this point in the history
Add Localization
  • Loading branch information
ChrisFeline authored Jul 25, 2024
2 parents f6c3a3f + 93bd98f commit 9c9cf5e
Show file tree
Hide file tree
Showing 21 changed files with 858 additions and 171 deletions.
13 changes: 13 additions & 0 deletions Localization/CONTRIBUTE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
> ## How to contribute with translations?
> - Fork clone the [localization](https://github.com/ChrisFeline/ToNSaveManager/tree/localization) branch.
> - Create a copy of the `en-US.json` language file into `/Localization/Language`
> - Rename it to your local ISO language name. For example `ja-JP.json`
> - Translate the strings contained within this file into your target language.
> * Keep important string replacement tokens like: `{0}`, `{1}` or `$$MAIN.SETTINGS$$` etc...
> - Create a pull request.
> * Do **NOT** create a pull request into the `main` branch.
> * Make sure the only edited file is the new added language `.json` file, any other contribution in the source code unrelated to this translation will be rejected.
> ### OR
> - Download the file [`en-US.json`](#) from this repo.
> - Rename it to your local ISO language name. For example `ja-JP.json`
> - You can [contact me](#-contact) on discord and I'll review the changes.
169 changes: 169 additions & 0 deletions Localization/LANG.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
using Newtonsoft.Json;
using System.Diagnostics;
using System.Reflection;
using System.Text.RegularExpressions;
using ToNSaveManager.Windows;

namespace ToNSaveManager.Localization {
internal static class LANG {
public struct LangKey {
public string Key;
public string Name;
public string Chars;

public override string ToString() {
return Name;
}
}

const string PREF_DEFAULT_KEY = "en-US";

static Dictionary<string, Dictionary<string, string>> LanguageData = new Dictionary<string, Dictionary<string, string>>();

static Dictionary<string, string> SelectedLang = new Dictionary<string, string>();
internal static string SelectedKey { get; private set; } = PREF_DEFAULT_KEY;

static Dictionary<string, string> SelectedDefault = new Dictionary<string, string>();
static string SelectedDefaultKey = string.Empty;

internal static List<LangKey> AvailableLang { get; private set; } = new List<LangKey>();
static readonly Regex ReplacePattern = new Regex(@"\$\$(?<key>.*?)\$\$", RegexOptions.Compiled);
const string PatternSearch = "$$";

private static string? D(string key, params string[] args) {
#if DEBUG
// if (!key.EndsWith(".TT")) Debug.WriteLine($"Missing key '{key}' in language pack '{SelectedKey}'");
#endif

if (SelectedDefault.ContainsKey(key)) {
return args.Length > 0 ? string.Format(SelectedDefault[key], args) : SelectedDefault[key];
}

#if DEBUG
if (!key.EndsWith(".TT")) Debug.WriteLine($"Invalid language key '{key}'");
#endif
return null;
}

public static string? S(string key, params string[] args) {
string? result;
if (SelectedLang.ContainsKey(key)) {
result = args.Length > 0 ? string.Format(SelectedLang[key], args) : SelectedLang[key];
} else {
result = D(key, args);
}

if (!string.IsNullOrEmpty(result) && result.Contains(PatternSearch)) {
result = ReplacePattern.Replace(result, (v) => {
string k = v.Groups["key"].Value;
return S(k) ?? v.Value;
});
}

return result;
}

public static (string?, string?) T(string key, params string[] args) {
return (S(key, args), S(key + ".TT", args));
}

public static void C(Control control, string key, ToolTip? toolTip = null) {
(string? tx, string? tt) = T(key);
if (!string.IsNullOrEmpty(tx)) control.Text = tx;
if (!string.IsNullOrEmpty(tt)) {
if (toolTip == null) TooltipUtil.Set(control, tt);
else toolTip.SetToolTip(control, tt);
}
}
public static void C(ToolStripItem item, string key) {
(string? text, string? tooltip) = T(key);
if (!string.IsNullOrEmpty(text)) item.Text = text;
if (!string.IsNullOrEmpty(tooltip)) item.ToolTipText = tooltip;
}

internal static void Select(string key) {
Debug.WriteLine("Selecting language key: " + key);
SelectedLang = LanguageData.ContainsKey(key) ? LanguageData[key] : LanguageData[key = SelectedDefaultKey];
SelectedKey = key;
}

internal static string FindLanguageKey() {
var currentCulture = System.Globalization.CultureInfo.CurrentUICulture;
string langName = currentCulture.TwoLetterISOLanguageName;
string fullLangName = currentCulture.Name;

string[] languageKeys = LanguageData.Keys.ToArray();

string foundKey = SelectedDefaultKey;
for (int i = 0; i < 2; i++) {
foreach (string key in languageKeys) {
bool check = i == 0 ?
key.Equals(fullLangName, StringComparison.OrdinalIgnoreCase) :
key.StartsWith(langName);

if (check) {
foundKey = key;
i = 3;
break;
}
}
}

return foundKey;
}

internal static void ReloadAll() {
MainWindow.Instance?.LocalizeContent();
EditWindow.Instance?.LocalizeContent();
ObjectivesWindow.Instance?.LocalizeContent();
SettingsWindow.Instance?.LocalizeContent();
}

internal static void Initialize() {
Assembly assembly = Assembly.GetExecutingAssembly();
string[] streamNames = assembly.GetManifestResourceNames();

string? firstKey = null;
foreach (string name in streamNames) {
string[] split = name.Split('.');
if (name.EndsWith(".json") && Array.IndexOf(split, "Localization") > 0 && Array.IndexOf(split, "Language") > 0) {
using (Stream? stream = assembly.GetManifestResourceStream(name)) {
if (stream != null) {
using (StreamReader reader = new StreamReader(stream)) {
string json = reader.ReadToEnd();
var obj = JsonConvert.DeserializeObject<Dictionary<string, string>>(json);
if (obj != null) {
string key = split[split.Length - 2];
LanguageData.Add(key, obj);

if (key == PREF_DEFAULT_KEY) {
SelectedDefault = obj;
SelectedDefaultKey = key;
Select(key);

Debug.WriteLine("Found default language.");
}

if (string.IsNullOrEmpty(firstKey)) firstKey = key;

Debug.WriteLine("Added language with key: " + key);
AvailableLang.Add(new LangKey() { Key = key, Chars = obj["DISPLAY_INIT"], Name = obj["DISPLAY_NAME"] });
}
}
}
}
}
}

if (string.IsNullOrEmpty(SelectedDefaultKey) && !string.IsNullOrEmpty(firstKey)) {
Debug.WriteLine("Default prefered language not found, using " + firstKey);

SelectedDefault = LanguageData[firstKey];
SelectedDefaultKey = firstKey;
Select(SelectedDefaultKey);
} else if (string.IsNullOrEmpty(firstKey)) {
throw new Exception("Could not load any language pack.");
}
}
}
}
167 changes: 167 additions & 0 deletions Localization/Language/en-US.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
{
"DISPLAY_INIT": "EN",
"DISPLAY_NAME": "English",

"EDIT.SAVE": "Save",
"EDIT.OK": "OK",
"EDIT.CANCEL": "Cancel",

"MESSAGE.WRITE_SETTINGS_ERROR": "An error ocurred while trying to write your settings to a file.\n\nMake sure that the program contains permissions to write files in the current folder it's located at.",
"MESSAGE.COPY_FILES_ERROR": "An error ocurred while trying to copy your files to the selected location.\n\nMake sure that the program contains permissions to write files to the destination.\nPath: {0}",
"MESSAGE.IMPORT_SAVE_ERROR": "Error trying to import your save.",
"MESSAGE.WRITE_SAVE_ERROR": "An error ocurred while trying to write your saves to a file.\n\nMake sure that the program contains permissions to write files to the destination.\nPath: {0}",
"MESSAGE.SAVE_LOCATION_RESET": "Save data location has been reset to default.",
"MESSAGE.SAVE_LOCATION_RESET.TITLE": "Reset Custom Data Location",
"MESSAGE.UPDATE_AVAILABLE": "A new update have been released on GitHub.\n\nWould you like to automatically download and update to the new version?",
"MESSAGE.UPDATE_AVAILABLE.TITLE": "New update available",
"MESSAGE.UPDATE_UNAVAILABLE": "No updates are currently available.",
"MESSAGE.UPDATE_UNAVAILABLE.TITLE": "No updates available",

"MESSAGE.COPY_TO_CLIPBOARD": "Copied to clipboard!\n\nYou can now paste the code in game.",
"MESSAGE.COPY_TO_CLIPBOARD.TITLE": "Copied",

"MAIN.SETTINGS": "Settings",
"MAIN.SETTINGS.TT": "Configure other options under the settings menu.",
"MAIN.OBJECTIVES": "Objectives",
"MAIN.OBJECTIVES.TT": "Keep track of in game unlockables.",
"MAIN.WIKI": "Wiki",
"MAIN.WIKI.TT": "Visit the wiki \"terror.moe\" for more useful information.",
"MAIN.SUPPORT.TT": "Buy Me A Coffee ❤️",

"MAIN.CTX_IMPORT": "Import",
"MAIN.CTX_IMPORT.TITLE": "Import Save Code",

"MAIN.CTX_RENAME": "Rename",
"MAIN.CTX_RENAME.TITLE": "Set Collection Name",

"MAIN.CTX_DELETE": "Delete",

"MAIN.CTX_DELETE_ALL.TITLE": "Deleting Collection: {0}",
"MAIN.CTX_DELETE_ALL.SUBTITLE": "Are you SURE that you want to delete this collection?\n\nEvery save code from '{0}' will be permanently deleted.\n\nThis operation is not reversible!",

"MAIN.CTX_DELETE_ENTRY.TITLE": "Deleting Entry: {0}",
"MAIN.CTX_DELETE_ENTRY.SUBTITLE": "Are you SURE that you want to delete this entry?\n\nYour save '{0}' will be permanently deleted.\n\nThis operation is not reversible!",

"MAIN.CTX_ADD_TO": "Add to",
"MAIN.CTX_ADD_TO.NEW": "New Collection",
"MAIN.CTX_ADD_TO.NEW.TT": "Add this entry to a new collection.",
"MAIN.CTX_EDIT_NOTE": "Edit note",
"MAIN.CTX_EDIT_NOTE.TITLE": "Note Editor",
"MAIN.CTX_BACKUP": "Backup",
"MAIN.CTX_BACKUP.TT": "Force upload a backup of this code to Discord as a file... Requires \"$$SETTINGS.DISCORDWEBHOOKENABLED$$\" to be enabled under \"$$MAIN.SETTINGS$$\"",

"MAIN.ENTRY_NOTE": "Note:",
"MAIN.ENTRY_ROUND": "Round Type:",
"MAIN.ENTRY_TERRORS": "Terrors in round:",
"MAIN.ENTRY_PLAYERS": "Players in room:",

"SETTINGS.CHECK_UPDATE": "Check For Updates",
"SETTINGS.OPEN_DATA_BTN": "Data",
"SETTINGS.OPEN_DATA_BTN.TT": "Open save data folder.",
"SETTINGS.VERSION": "Current Version: {0}",
"SETTINGS.CUSTOM_DATA_FOLDER": "Custom Data Folder",
"SETTINGS.CUSTOM_DATA_PICK_FOLDER": "Pick Folder",
"SETTINGS.CUSTOM_DATA_RESET_DEFAULT": "Reset to Default",

"SETTINGS.GROUP.GENERAL": "General",
"SETTINGS.SKIPPARSEDLOGS": "Skip Parsed Logs (!)",
"SETTINGS.SKIPPARSEDLOGS.TT": "Skip old parsed log files that were already processed and saved.\nOnly disable this if you accidentally deleted a save code.",
"SETTINGS.AUTOCOPY": "Auto Clipboard Copy",
"SETTINGS.AUTOCOPY.TT": "Automatically copy new save codes to clipboard.",
"SETTINGS.SAVENAMES": "Collect Player Names",
"SETTINGS.SAVENAMES.TT": "Save codes will show players in the instance at the time of saving.",
"SETTINGS.SAVEROUNDINFO": "Save Round Info",
"SETTINGS.SAVEROUNDINFO.TT": "Save codes will display the last round type and terror names.",
"SETTINGS.SAVEROUNDNOTE": "Terror Name Notes",
"SETTINGS.SAVEROUNDNOTE.TT": "Automatically set survived terror names as note.",
"SETTINGS.SHOWWINLOSE": "Show [R][W][D] Tags",
"SETTINGS.SHOWWINLOSE.TT": "Entries will show a [R], [W] or [D] tag based on the source that triggered the save.",
"SETTINGS.OSCENABLED": "Send OSC Parameters",
"SETTINGS.OSCENABLED.TT": "Sends avatar parameters to VRChat using OSC.\nRight click this entry to open documentation about parameter names and types.",

"SETTINGS.DISCORDWEBHOOKENABLED": "Auto Discord Backup (Webhook)",
"SETTINGS.DISCORDWEBHOOKENABLED.TT": "Automatically saves your new codes to a Discord channel using a webhook integration.",
"SETTINGS.DISCORDWEBHOOK.TITLE": "Discord Webhook URL",
"SETTINGS.DISCORDWEBHOOKINVALID": "The URL your provided does not match a discord webhook url.\n\nMake sure you created your webhook and copied the url correctly.",
"SETTINGS.DISCORDWEBHOOKINVALID.TITLE": "Invalid Webhook URL",

"SETTINGS.DISCORDWEBHOOK.LABEL_PLAYER": "**Player**: `{0}`",
"SETTINGS.DISCORDWEBHOOK.LABEL_ROUND": "**Round Type**: `{0}`",
"SETTINGS.DISCORDWEBHOOK.LABEL_TERRORS": "**Terrors in Round**: `{0}`",
"SETTINGS.DISCORDWEBHOOK.LABEL_TERRORS_SPLIT": "`, `",
"SETTINGS.DISCORDWEBHOOK.LABEL_COUNT": "**Player Count**: `{0}`",
"SETTINGS.DISCORDWEBHOOK.LABEL_NOTE": "**Note**: `{0}`",

"SETTINGS.GROUP.NOTIFICATIONS": "Notifications",

"SETTINGS.XSOVERLAY": "XSOverlay Popup",
"SETTINGS.XSOVERLAY.TT": "XSOverlay popup notifications when saving.",
"SETTINGS.XSOVERLAY.MESSAGE": "<color=#ff9999><b>ToN</b></color><color=grey>:</color> <color=#adff2f>Save Data Stored</color>",
"SETTINGS.XSOVERLAY.TOGGLE": "<color=#ff9999><b>ToN</b></color><color=grey>:</color> <color=#adff2f>Notifications Enabled</color>",

"SETTINGS.PLAYAUDIO": "Play Audio ({0})",
"SETTINGS.PLAYAUDIO.TT": "Double click to select custom audio file.\nRight click to reset back to 'default.wav'",
"SETTINGS.PLAYAUDIO.TITLE": "Select Custom Audio",

"SETTINGS.GROUP.TIME_FORMAT": "Time Formatting",
"SETTINGS.USE24HOUR": "24 Hour Time",
"SETTINGS.SHOWSECONDS": "Show Seconds",
"SETTINGS.INVERTMD": "Invert Month/Day",
"SETTINGS.SHOWDATE": "Right Panel Date",
"SETTINGS.SHOWDATE.TT": "Entries on the right panel will display a full date.",

"SETTINGS.GROUP.STYLE": "Style",
"SETTINGS.COLORFULOBJECTIVES": "Colorful Objectives",
"SETTINGS.COLORFULOBJECTIVES.TT": "Items in the 'Objectives' window will show colors that correspond to those of the items in the game.",

"OBJECTIVES.TITLE": "ToN Objectives",
"OBJECTIVES.EVENT_ITEMS_UNLOCKS": "Event Items Unlocks",
"OBJECTIVES.SEALED_SWORD": "Sealed Sword",
"OBJECTIVES.SEALED_SWORD.TT": "Found in Museum. Break case with a stun tool.",
"OBJECTIVES.GRAN_FAUST": "Gran Faust",
"OBJECTIVES.GRAN_FAUST.TT": "Survive Arkus with '$$OBJECTIVES.SEALED_SWORD$$'.",
"OBJECTIVES.DIVINE_AVENGER": "Divine Avenger",
"OBJECTIVES.DIVINE_AVENGER.TT": "Survive Arkus with '$$OBJECTIVES.SEALED_SWORD$$' after hitting them at least two times.",
"OBJECTIVES.MAXWELL": "Maxwell",
"OBJECTIVES.MAXWELL.TT": "Found in Its Maze. (spawns once per round)",
"OBJECTIVES.ROCK": "Rock",
"OBJECTIVES.ROCK.TT": "Survive Fusion Pilot.",
"OBJECTIVES.ILLUMINA": "Illumina",
"OBJECTIVES.ILLUMINA.TT": "Survive Bliss.",
"OBJECTIVES.REDBULL": "Redbull",
"OBJECTIVES.REDBULL.TT": "Survive Roblander.",
"OBJECTIVES.OMORI_PLUSH": "Omori Plush",
"OBJECTIVES.OMORI_PLUSH.TT": "Survive Something.",
"OBJECTIVES.PARADISE_LOST": "Paradise Lost",
"OBJECTIVES.PARADISE_LOST.TT": "Beat the shit out of Apostles.",
"OBJECTIVES.ITEM_SKIN_UNLOCKS": "Item Skin Unlocks",
"OBJECTIVES.RED_MEDKIT": "Red Medkit",
"OBJECTIVES.RED_MEDKIT.TT": "Survive Virus with Medkit.",
"OBJECTIVES.PSYCHO_COIL": "Psycho Coil",
"OBJECTIVES.PSYCHO_COIL.TT": "Survive Psychosis with Glow Coil.",
"OBJECTIVES.BLOODY_TELEPORTER": "Bloody Teleporter",
"OBJECTIVES.BLOODY_TELEPORTER.TT": "Survive a Bloodbath round with Teleporter.",
"OBJECTIVES.PALE_SUITCASE": "Pale Suitcase",
"OBJECTIVES.PALE_SUITCASE.TT": "Survive an Alternate round with Teleporter.",
"OBJECTIVES.THORN_HACKER": "Thorn Hacker",
"OBJECTIVES.THORN_HACKER.TT": "Survive Pandora with Teleporter.",
"OBJECTIVES.BLOODY_COIL": "Bloody Coil",
"OBJECTIVES.BLOODY_COIL.TT": "Survive a Bloodbath round with Speed Coil.",
"OBJECTIVES.BLOODY_BAT": "Bloody Bat",
"OBJECTIVES.BLOODY_BAT.TT": "Survive a Bloodbath round with Metal Bat.",
"OBJECTIVES.METAL_PIPE": "Metal Pipe",
"OBJECTIVES.METAL_PIPE.TT": "Survive an Alternate round with Metal Bat.",
"OBJECTIVES.COLORABLE_BAT": "Colorable Bat",
"OBJECTIVES.COLORABLE_BAT.TT": "Survive a Cracked round with Metal Bat.",
"OBJECTIVES.JUSTITIA": "Justitia",
"OBJECTIVES.JUSTITIA.TT": "Survive a Midnight round with Metal Bat.",
"OBJECTIVES.TWILIGHT_COIL": "Twilight Coil",
"OBJECTIVES.TWILIGHT_COIL.TT": "Survive Apocalypse Bird with Chaos Coil.",
"OBJECTIVES.PALE_PISTOL": "Pale Pistol",
"OBJECTIVES.PALE_PISTOL.TT": "Survive an Alternate round with Antique Revolver.",
"OBJECTIVES.WINTERFEST_UNLOCKS": "Winterfest Unlocks",
"OBJECTIVES.SNOWY_SPEED_COIL": "Snowy Speed Coil",
"OBJECTIVES.SNOWY_SPEED_COIL.TT": "There's something in the snow for you.",
"OBJECTIVES.TORCH_OF_OBSESSION": "Torch Of Obsession",
"OBJECTIVES.TORCH_OF_OBSESSION.TT": "Survive the Cold Night."
}
Loading

0 comments on commit 9c9cf5e

Please sign in to comment.