diff --git a/.github/scripts/check-diff.py b/.github/scripts/check-diff.py new file mode 100755 index 00000000..51296319 --- /dev/null +++ b/.github/scripts/check-diff.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: 2019 Corentin Noël +# +# SPDX-License-Identifier: GPL-3.0-only + +# Taken from https://github.com/elementary/actions/blob/master/gettext-template/check-diff.py + +import git +import io + +def commit_to_repo(repo): + print('There are translation changes, committing changes to repository!') + files = repo.git.diff(None, name_only=True) + for f in files.split('\n'): + if f.endswith ('.po') or f.endswith ('.pot'): + repo.git.add(f) + repo.git.commit('-m', 'Update translation template') + infos = repo.remotes.origin.push() + has_error=False + error_msg='' + for info in infos: + if info.flags & git.remote.PushInfo.ERROR == git.remote.PushInfo.ERROR: + has_error=True + error_msg += info.summary + if has_error: + raise NameError('Unable to push to repository: ' + error_msg) + +print('Checking the repository for new translations...') +repo = git.Repo('.') +t = repo.head.commit.tree +files = repo.git.diff(None, name_only=True) +needs_commit=False + +for f in files.split('\n'): + if f.endswith ('.pot'): + raw_diff = repo.git.diff(t, f) + output = io.StringIO() + for line in raw_diff.splitlines(): + if line.startswith ('+++'): + continue + if line.startswith ('---'): + continue + if line.startswith ('diff'): + continue + if line.startswith ('index'): + continue + if line.startswith ('@@'): + continue + if line.startswith (' '): + continue + if line.startswith ('+#:'): + continue + if line.startswith ('-#:'): + continue + if line.startswith ('-"'): + continue + if line.startswith ('+"'): + continue + if not line.strip(): + continue + print(line, file=output) + if output.getvalue().strip(): + print(f + " has changed!") + needs_commit = True + output.close() + +if needs_commit: + commit_to_repo(repo) +else: + print('The translations are up-to-date!') diff --git a/.github/scripts/i18n.sh b/.github/scripts/i18n.sh new file mode 100755 index 00000000..0bd9a0de --- /dev/null +++ b/.github/scripts/i18n.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: 2019 Corentin Noël +# SPDX-FileCopyrightText: 2022 Janet Blackquill +# +# SPDX-License-Identifier: GPL-3.0-only + +# adapted from https://github.com/elementary/actions/blob/master/gettext-template/entrypoint.sh + +set -e + +export DEBIAN_FRONTEND="noninteractive" + +# if a custom token is provided, use it instead of the default github token. +if [ -n "$GIT_USER_TOKEN" ]; then + GITHUB_TOKEN="$GIT_USER_TOKEN" +fi + +if [ -z "${GITHUB_TOKEN}" ]; then + echo "\033[0;31mERROR: The GITHUB_TOKEN environment variable is not defined.\033[0m" && exit 1 +fi + +# Git repository is owned by another user, mark it as safe +git config --global --add safe.directory /github/workspace + +# get default branch, see: https://davidwalsh.name/get-default-branch-name +DEFAULT_BRANCH="$(git remote show origin | grep 'HEAD branch' | cut -d' ' -f5)" + +if [ -z "${INPUT_TRANSLATION_BRANCH}" ]; then + TRANSLATION_BRANCH="${DEFAULT_BRANCH}" +else + TRANSLATION_BRANCH="${INPUT_TRANSLATION_BRANCH}" +fi + +# default email and username to github actions user +if [ -z "$GIT_USER_EMAIL" ]; then + GIT_USER_EMAIL="action@github.com" +fi +if [ -z "$GIT_USER_NAME" ]; then + GIT_USER_NAME="GitHub Action" +fi + +# make sure branches are up-to-date +git fetch +echo "Setting up git credentials..." +git remote set-url origin https://x-access-token:"$GITHUB_TOKEN"@github.com/"$GITHUB_REPOSITORY".git +git config --global user.email "$GIT_USER_EMAIL" +git config --global user.name "$GIT_USER_NAME" +echo "Git credentials configured." + +# get the project's name: +PROJECT="$(basename "$GITHUB_REPOSITORY")" +echo "Project: $PROJECT" + +sudo apt-get -qq update +sudo apt-get -qq install python3-git + +dotnet tool install --global GetText.NET.Extractor --version 1.6.6 + +GetText.Extractor --order -s TShock.sln -t i18n/template.pot + +python3 .github/scripts/check-diff.py diff --git a/.github/workflows/ci-otapi3.yml b/.github/workflows/ci-otapi3.yml index 10031473..4f0365aa 100644 --- a/.github/workflows/ci-otapi3.yml +++ b/.github/workflows/ci-otapi3.yml @@ -33,6 +33,9 @@ jobs: with: dotnet-version: '6.0.100' + - name: Install msgfmt + run: sudo apt-get install -y gettext + - name: Produce build run: | cd TShockLauncher diff --git a/.github/workflows/i18n-extract.yml b/.github/workflows/i18n-extract.yml new file mode 100644 index 00000000..7ed3cdab --- /dev/null +++ b/.github/workflows/i18n-extract.yml @@ -0,0 +1,23 @@ +name: i18n extraction +on: + push: + branches: [ general-devel ] +jobs: + extract: + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + steps: + - uses: actions/checkout@v3 + with: + submodules: 'recursive' + + - uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 3.1.x + 6.0.100 + + - name: Run i18n checking/extraction script + run: ./.github/scripts/i18n.sh + shell: bash diff --git a/TShockAPI/Commands.cs b/TShockAPI/Commands.cs index 632642fb..5b9aaf52 100644 --- a/TShockAPI/Commands.cs +++ b/TShockAPI/Commands.cs @@ -649,7 +649,7 @@ namespace TShockAPI string cmdName; if (index == 0) // Space after the command specifier should not be supported { - player.SendErrorMessage("Invalid command entered. Type {0}help for a list of valid commands.", Specifier); + player.SendErrorMessage(I18n.C.GetString("Invalid command entered. Type {0}help for a list of valid commands.", Specifier)); return true; } else if (index < 0) @@ -677,28 +677,28 @@ namespace TShockAPI call(new CommandArgs(cmdText, player, args)); return true; } - player.SendErrorMessage("Invalid command entered. Type {0}help for a list of valid commands.", Specifier); + player.SendErrorMessage(I18n.C.GetString("Invalid command entered. Type {0}help for a list of valid commands.", Specifier)); return true; } foreach (Command cmd in cmds) { if (!cmd.CanRun(player)) { - TShock.Utils.SendLogs(string.Format("{0} tried to execute {1}{2}.", player.Name, Specifier, cmdText), Color.PaleVioletRed, player); - player.SendErrorMessage("You do not have access to this command."); + TShock.Utils.SendLogs(I18n.C.GetString("{0} tried to execute {1}{2}.", player.Name, Specifier, cmdText), Color.PaleVioletRed, player); + player.SendErrorMessage(I18n.C.GetString("You do not have access to this command.")); if (player.HasPermission(Permissions.su)) { - player.SendInfoMessage("You can use '{0}sudo {0}{1}' to override this check.", Specifier, cmdText); + player.SendInfoMessage(I18n.C.GetString("You can use '{0}sudo {0}{1}' to override this check.", Specifier, cmdText)); } } else if (!cmd.AllowServer && !player.RealPlayer) { - player.SendErrorMessage("You must use this command in-game."); + player.SendErrorMessage(I18n.C.GetString("You must use this command in-game.")); } else { if (cmd.DoLog) - TShock.Utils.SendLogs(string.Format("{0} executed: {1}{2}.", player.Name, silent ? SilentSpecifier : Specifier, cmdText), Color.PaleVioletRed, player); + TShock.Utils.SendLogs(I18n.C.GetString("{0} executed: {1}{2}.", player.Name, silent ? SilentSpecifier : Specifier, cmdText), Color.PaleVioletRed, player); cmd.Run(cmdText, silent, player, args); } } @@ -1011,7 +1011,7 @@ namespace TShockAPI { args.Player.SendSuccessMessage("Account \"{0}\" has been registered.", account.Name); args.Player.SendSuccessMessage("Your password is {0}.", echoPassword); - + if (!TShock.Config.Settings.DisableUUIDLogin) args.Player.SendMessage($"Type {Specifier}login to sign in to your account using your UUID.", Color.White); @@ -1019,7 +1019,7 @@ namespace TShockAPI args.Player.SendMessage($"Type {Specifier}login \"{account.Name.Color(Utils.GreenHighlight)}\" {echoPassword.Color(Utils.BoldHighlight)} to sign in to your account.", Color.White); else args.Player.SendMessage($"Type {Specifier}login {echoPassword.Color(Utils.BoldHighlight)} to sign in to your account.", Color.White); - + TShock.UserAccounts.AddUserAccount(account); TShock.Log.ConsoleInfo("{0} registered an account: \"{1}\".", args.Player.Name, account.Name); } @@ -1145,7 +1145,7 @@ namespace TShockAPI TShock.UserAccounts.SetUserGroup(account, args.Parameters[2]); TShock.Log.ConsoleInfo(args.Player.Name + " changed account " + account.Name + " to group " + args.Parameters[2] + "."); args.Player.SendSuccessMessage("Account " + account.Name + " has been changed to group " + args.Parameters[2] + "!"); - + //send message to player with matching account name var player = TShock.Players.FirstOrDefault(p => p != null && p.Account?.Name == account.Name); if (player != null && !args.Silent) @@ -1403,7 +1403,7 @@ namespace TShockAPI LineTextColor = Color.White }); break; - + case "examples": args.Player.SendMessage("", Color.White); args.Player.SendMessage("Ban Usage Examples", Color.White); @@ -1562,7 +1562,7 @@ namespace TShockAPI if (banUuid) { - banResult = DoBan($"{Identifier.UUID}{player.UUID}", reason, expiration); + banResult = DoBan($"{Identifier.UUID}{player.UUID}", reason, expiration); } if (banName) @@ -1625,7 +1625,7 @@ namespace TShockAPI args.Player.SendMessage($"Invalid Ban List syntax. Refer to {"ban help list".Color(Utils.BoldHighlight)} for details on how to use the {"ban list".Color(Utils.BoldHighlight)} command", Color.White); return; } - + var bans = from ban in TShock.Bans.Bans where ban.Value.ExpirationDateTime > DateTime.UtcNow orderby ban.Value.ExpirationDateTime ascending @@ -1664,7 +1664,7 @@ namespace TShockAPI DisplayBanDetails(ban); } - + string subcmd = args.Parameters.Count == 0 ? "help" : args.Parameters[0].ToLower(); switch (subcmd) { @@ -2698,7 +2698,7 @@ namespace TShockAPI TSPlayer.Server.SpawnNPC(npc.type, npc.FullName, amount, args.Player.TileX, args.Player.TileY); spawnName = "a Solar Pillar"; break; - case "nebula pillar": + case "nebula pillar": npc.SetDefaults(507); TSPlayer.Server.SpawnNPC(npc.type, npc.FullName, amount, args.Player.TileX, args.Player.TileY); spawnName = "a Nebula Pillar"; @@ -5827,7 +5827,7 @@ namespace TShockAPI target.SendErrorMessage($"{user.Name} just killed you!"); } } - + private static void Respawn(CommandArgs args) { if (!args.Player.RealPlayer && args.Parameters.Count == 0) @@ -5857,7 +5857,7 @@ namespace TShockAPI } playerToRespawn = players[0]; } - else + else playerToRespawn = args.Player; if (!playerToRespawn.Dead) diff --git a/TShockAPI/I18n.cs b/TShockAPI/I18n.cs new file mode 100644 index 00000000..1ec38c42 --- /dev/null +++ b/TShockAPI/I18n.cs @@ -0,0 +1,46 @@ +/* +TShock, a server mod for Terraria +Copyright (C) 2022 Janet Blackquill + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +using System; +using System.Globalization; +using System.IO; +using GetText; + +namespace TShockAPI +{ + static class I18n { + static string TranslationsDirectory => Path.Combine(AppContext.BaseDirectory, "i18n"); + static CultureInfo TranslationCultureInfo + { + get + { + // cross-platform mapping of cultureinfos can be a bit screwy, so give our users + // the chance to explicitly spell out which translation they would like to use. + // this is an environment variable instead of a flag because this needs to be + // valid whether the passed flags are in a sane state or not. + if (Environment.GetEnvironmentVariable("TSHOCK_LANGUAGE") is string overrideLang) + { + return new CultureInfo(overrideLang); + } + return CultureInfo.CurrentUICulture; + } + } + /// Instance of a GetText.Catalog loaded with TShockAPI translations for user's specified language + public static Catalog C = new Catalog("TShockAPI", TranslationsDirectory, TranslationCultureInfo); + } +} diff --git a/TShockAPI/TShockAPI.csproj b/TShockAPI/TShockAPI.csproj index 60fcd20f..999ed882 100644 --- a/TShockAPI/TShockAPI.csproj +++ b/TShockAPI/TShockAPI.csproj @@ -33,6 +33,7 @@ + diff --git a/TShockLauncher/TShockLauncher.csproj b/TShockLauncher/TShockLauncher.csproj index 3589d158..53a30187 100644 --- a/TShockLauncher/TShockLauncher.csproj +++ b/TShockLauncher/TShockLauncher.csproj @@ -24,6 +24,7 @@ + @@ -41,6 +42,31 @@ + + + + + + + + + + + + + + + + + + + + @@ -50,6 +76,12 @@ + + + + + + diff --git a/i18n/.gitkeep b/i18n/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/i18n/template.pot b/i18n/template.pot new file mode 100644 index 00000000..a7d63daa --- /dev/null +++ b/i18n/template.pot @@ -0,0 +1,41 @@ +msgid "" +msgstr "" +"Project-Id-Version: TShock\n" +"POT-Creation-Date: 2022-10-20 00:16:51-0400\n" +"PO-Revision-Date: 2022-10-20 00:16:51-0400\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: GetText.NET Extractor\n" + +#: ../../TShockAPI/Commands.cs:701 +#, csharp-format +msgid "{0} executed: {1}{2}." +msgstr "" + +#: ../../TShockAPI/Commands.cs:687 +#, csharp-format +msgid "{0} tried to execute {1}{2}." +msgstr "" + +#: ../../TShockAPI/Commands.cs:652 +#: ../../TShockAPI/Commands.cs:680 +#, csharp-format +msgid "Invalid command entered. Type {0}help for a list of valid commands." +msgstr "" + +#: ../../TShockAPI/Commands.cs:691 +#, csharp-format +msgid "You can use '{0}sudo {0}{1}' to override this check." +msgstr "" + +#: ../../TShockAPI/Commands.cs:688 +msgid "You do not have access to this command." +msgstr "" + +#: ../../TShockAPI/Commands.cs:696 +msgid "You must use this command in-game." +msgstr "" + diff --git a/i18n/tok/TShockAPI.po b/i18n/tok/TShockAPI.po new file mode 100644 index 00000000..dc52dc2b --- /dev/null +++ b/i18n/tok/TShockAPI.po @@ -0,0 +1,44 @@ +# Janet Blackquill , 2022. +msgid "" +msgstr "" +"Project-Id-Version: TShock\n" +"POT-Creation-Date: 2022-10-20 00:16:51-0400\n" +"PO-Revision-Date: 2022-10-19 23:20-0400\n" +"Last-Translator: Janet Blackquill \n" +"Language-Team: none>\n" +"Language: tok\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || n%10>=5 && n%10<=9 || " +"n%100>=11 && n%100<=14 ? 2 : 3;\n" +"X-Generator: Lokalize 22.11.70\n" + +#: ../../TShockAPI/Commands.cs:701 +#, csharp-format +msgid "{0} executed: {1}{2}." +msgstr "jan {0} li kepeken: {1}{2}." + +#: ../../TShockAPI/Commands.cs:687 +#, csharp-format +msgid "{0} tried to execute {1}{2}." +msgstr "jan {0} li wile kepeken {1}{2}. taso ona li ken ala." + +#: ../../TShockAPI/Commands.cs:652 ../../TShockAPI/Commands.cs:680 +#, csharp-format +msgid "Invalid command entered. Type {0}help for a list of valid commands." +msgstr "ilo ni li lon ala. sina wile sona e ilo lon la o toki e {0}help." + +#: ../../TShockAPI/Commands.cs:691 +#, csharp-format +msgid "You can use '{0}sudo {0}{1}' to override this check." +msgstr "sina wile weka e awen pona ni la o kepeken '{0}sudo {0}{1}'." + +#: ../../TShockAPI/Commands.cs:688 +msgid "You do not have access to this command." +msgstr "sina ken ala kepeken ilo ni." + +#: ../../TShockAPI/Commands.cs:696 +msgid "You must use this command in-game." +msgstr "ilo ni li pali lon musi taso."