From 8843276c41b1c6682cf9f9566bed5355829cbf18 Mon Sep 17 00:00:00 2001
From: Luke H-W <Birdulon@users.noreply.github.com>
Date: Mon, 11 Jul 2022 23:51:05 +0930
Subject: [PATCH] Language linting (#1382)

---
 .github/workflows/language_lint.yml           |  27 +
 manage_languages.py                           | 321 ++++++++++++
 .../auth/DefaultAuthentication.java           |   2 +-
 .../emu/grasscutter/command/CommandMap.java   |  18 +-
 .../command/commands/GiveCommand.java         |   1 -
 .../command/commands/HelpCommand.java         | 118 ++---
 .../command/commands/SendMailCommand.java     |   2 +-
 src/main/resources/languages/pl-PL.json       | 487 ++++++++++--------
 8 files changed, 676 insertions(+), 300 deletions(-)
 create mode 100644 .github/workflows/language_lint.yml
 create mode 100644 manage_languages.py

diff --git a/.github/workflows/language_lint.yml b/.github/workflows/language_lint.yml
new file mode 100644
index 00000000..17ee7d7e
--- /dev/null
+++ b/.github/workflows/language_lint.yml
@@ -0,0 +1,27 @@
+name: "Language Lint"
+on:
+  workflow_dispatch: ~
+  push:
+    paths:
+      - "**.java"
+      - "**.json"
+    branches:
+      - "stable"
+      - "development"
+  pull_request:
+    paths:
+      - "**.java"
+      - "**.json"
+    types:
+      - opened
+      - synchronize
+      - reopened
+jobs:
+  Lint-Language-Keys:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v4
+        with:
+          python-version: '3.10' # Version range or exact version of a Python version to use, using SemVer's version range syntax
+      - run: python3 manage_languages.py -l
diff --git a/manage_languages.py b/manage_languages.py
new file mode 100644
index 00000000..ee7d1a5c
--- /dev/null
+++ b/manage_languages.py
@@ -0,0 +1,321 @@
+# Written for Python 3.6+
+# Older versions don't retain insertion order of regular dicts
+import argparse
+import cmd
+import json
+import os
+import re
+from pprint import pprint
+
+INDENT = 2
+PRIMARY_LANGUAGE = 'en-US.json'
+PRIMARY_FALLBACK_PREFIX = '馃嚭馃嚫'  # This is invisible in-game, terminal emulators might render it
+LANGUAGE_FOLDER = 'src/main/resources/languages/'
+LANGUAGE_FILENAMES = sorted(os.listdir(LANGUAGE_FOLDER), key=lambda x: 'AAA' if x == PRIMARY_LANGUAGE else x)
+SOURCE_FOLDER = 'src/'
+SOURCE_EXTENSIONS = ('java')
+
+
+def ppprint(data):
+    pprint(data, width=130, sort_dicts=False, compact=True)
+
+
+class JsonHelpers:
+    @staticmethod
+    def load(filename: str) -> dict:
+        with open(filename, 'r') as file:
+            return json.load(file)
+
+    @staticmethod
+    def save(filename: str, data: dict) -> None:
+        with open(filename, 'w', encoding='utf-8', newline='\n') as file:
+            json.dump(data, file, ensure_ascii=False, indent=INDENT)
+
+    @staticmethod
+    def flatten(data: dict, prefix='') -> dict:
+        output = {}
+        for key, value in data.items():
+            if isinstance(value, dict):
+                for k,v in JsonHelpers.flatten(value, f'{prefix}{key}.').items():
+                    output[k] = v
+            else:
+                output[f'{prefix}{key}'] = value
+        return output
+
+    @staticmethod
+    def unflatten(data: dict) -> dict:
+        output = {}
+        def add_key(k: list, value, d: dict):
+            if len(k) == 1:
+                d[k[0]] = value
+            else:
+                d[k[0]] = d.get(k[0], {})
+                add_key(k[1:], value, d[k[0]])
+        for key, value in data.items():
+            add_key(key.split('.'), value, output)
+        return output
+
+    @staticmethod
+    def pprint_keys(keys, indent=4) -> str:
+        # Only strip down to one level
+        padding = ' ' * indent
+        roots = {}
+        for key in keys:
+            root, _, k = key.rpartition('.')
+            roots[root] = roots.get(root, [])
+            roots[root].append(k)
+        lines = []
+        for root, ks in roots.items():
+            if len(ks) > 1:
+                lines.append(f'{padding}{root}.[{", ".join(ks)}]')
+            else:
+                lines.append(f'{padding}{root}.{ks[0]}')
+        return ',\n'.join(lines)
+
+    @staticmethod
+    def deep_clone_and_fill(d1: dict, d2: dict, fallback_prefix=PRIMARY_FALLBACK_PREFIX) -> dict:
+        out = {}
+        for key, value in d1.items():
+            if isinstance(value, dict):
+                out[key] = JsonHelpers.deep_clone_and_fill(value, d2.get(key, {}), fallback_prefix)
+            else:
+                v2 = d2.get(key, value)
+                if type(value) == str and v2 == value:
+                    out[key] = fallback_prefix + value
+                else:
+                    out[key] = v2
+        return out
+
+
+class LanguageManager:
+    TRANSLATION_KEY = re.compile(r'[Tt]ranslate.*"(\w+\.[\w\.]+)"')
+    POTENTIAL_KEY = re.compile(r'"(\w+\.[\w\.]+)"')
+
+    def __init__(self):
+        self.load_jsons()
+
+    def load_jsons(self):
+        self.language_jsons = [JsonHelpers.load(LANGUAGE_FOLDER + filename) for filename in LANGUAGE_FILENAMES]
+        self.flattened_jsons = [JsonHelpers.flatten(j) for j in self.language_jsons]
+        self.update_keys()
+
+    def update_keys(self):
+        self.key_sets = [set(j.keys()) for j in self.flattened_jsons]
+        self.common_keys = set.intersection(*self.key_sets)
+        self.all_keys = set.union(*self.key_sets)
+        self.used_keys = self.find_all_used_keys(self.all_keys)
+        self.missing_keys = self.used_keys - self.common_keys
+        self.unused_keys = self.all_keys - self.used_keys
+
+    def find_all_used_keys(self, expected_keys=[]) -> set:
+        # Note that this will only find string literals passed to the translate() or sendTranslatedMessage() methods!
+        # String variables passed to them can be checked against expected_keys
+        used = set()
+        potential = set()
+        for root, dirs, files in os.walk(SOURCE_FOLDER):
+            for file in files:
+                if file.rpartition('.')[-1] in SOURCE_EXTENSIONS:
+                    filename = os.path.join(root, file)
+                    with open(filename, 'r') as f:
+                        data = f.read()  # Loads in entire file at once
+                        for k in self.TRANSLATION_KEY.findall(data):
+                            used.add(k)
+                        for k in self.POTENTIAL_KEY.findall(data):
+                            potential.add(k)
+        return used | (potential & expected_keys)
+
+    def _lint_report_language(self, lang: str, keys: set, flattened: dict, primary_language_flattened: dict) -> None:
+        missing = self.used_keys - keys
+        unused = keys - self.used_keys
+        identical_keys = set() if (lang == PRIMARY_LANGUAGE) else {key for key in keys if primary_language_flattened.get(key, None) == flattened.get(key)}
+        placeholder_keys = {key for key in keys if flattened.get(key).startswith(PRIMARY_FALLBACK_PREFIX)}
+        p1 = f'Language {lang} has {len(missing)} missing keys and {len(unused)} unused keys.'
+        p2 = 'This is the primary language.' if (lang == PRIMARY_LANGUAGE) else f'{len(identical_keys)} match {PRIMARY_LANGUAGE}, {len(placeholder_keys)} have the placeholder mark.'
+        print(f'{p1} {p2}')
+
+        lint_categories = {
+            'Missing': missing,
+            'Unused': unused,
+            f'Matches {PRIMARY_LANGUAGE}': identical_keys,
+            'Placeholder': placeholder_keys,
+        }
+        for name, category in lint_categories.items():
+            if len(category) > 0:
+                print(name + ':')
+                print(JsonHelpers.pprint_keys(sorted(category)))
+
+    def lint_report(self) -> None:
+        print(f'There are {len(self.missing_keys)} translation keys in use that are missing from one or more language files.')
+        print(f'There are {len(self.unused_keys)} translation keys in language files that are not used.')
+        primary_language_flattened = self.flattened_jsons[LANGUAGE_FILENAMES.index(PRIMARY_LANGUAGE)]
+        for lang, keys, flattened in zip(LANGUAGE_FILENAMES, self.key_sets, self.flattened_jsons):
+            print('')
+            self._lint_report_language(lang, keys, flattened, primary_language_flattened)
+
+    def rename_keys(self, key_remappings: dict) -> None:
+        # Unfortunately we can't rename keys in-place preserving insertion order, so we have to make new dicts
+        for i in range(len(self.flattened_jsons)):
+            self.flattened_jsons[i] = {key_remappings.get(k,k):v for k,v in self.flattened_jsons[i].items()}
+
+    def update_secondary_languages(self):
+        # Push en_US fallback
+        primary_language_json = self.language_jsons[LANGUAGE_FILENAMES.index(PRIMARY_LANGUAGE)]
+        for filename, lang in zip(LANGUAGE_FILENAMES, self.language_jsons):
+            if filename != PRIMARY_LANGUAGE:
+                js = JsonHelpers.deep_clone_and_fill(primary_language_json, lang)
+                JsonHelpers.save(LANGUAGE_FOLDER + filename, js)
+
+    def update_all_languages_from_flattened(self):
+        for filename, flat in zip(LANGUAGE_FILENAMES, self.flattened_jsons):
+            JsonHelpers.save(LANGUAGE_FOLDER + filename, JsonHelpers.unflatten(flat))
+
+    def save_flattened_languages(self, prefix='flat_'):
+        for filename, flat in zip(LANGUAGE_FILENAMES, self.flattened_jsons):
+            JsonHelpers.save(prefix + filename, flat)
+
+
+class InteractiveRename(cmd.Cmd):
+    intro = 'Welcome to the interactive rename shell.   Type help or ? to list commands.\n'
+    prompt = '(rename) '
+    file = None
+
+    def __init__(self, language_manager: LanguageManager) -> None:
+        super().__init__()
+        self.language_manager = language_manager
+        self.flat_keys = [key for key in language_manager.flattened_jsons[LANGUAGE_FILENAMES.index(PRIMARY_LANGUAGE)].keys()]
+        self.mappings = {}
+
+    def do_add(self, arg):
+        '''
+        Prepare to rename an existing translation key. Will not actually rename anything until you confirm all your pending changes with 'rename'.
+        e.g. a single string:  add commands.execution.argument_error commands.generic.invalid.argument
+        e.g. a group:          add commands.enter_dungeon commands.new_enter_dungeon
+        '''
+        args = arg.split()
+        if len(args) < 2:
+            self.do_help('add')
+            return
+        old, new = args[:2]
+        if old in self.flat_keys:
+            self.mappings[old] = new
+        else:
+            # Check if we are renaming a higher level
+            if not old.endswith('.'):
+                old = old + '.'
+            results = [key for key in self.flat_keys if key.startswith(old)]
+            if len(results) > 0:
+                if not new.endswith('.'):
+                    new = new + '.'
+                new_mappings = {key: key.replace(old, new) for key in results}
+                # Ask for confirmation
+                print('Will add the following mappings:')
+                ppprint(new_mappings)
+                print('Add these mappings? [y/N]')
+                if self.prompt_yn():
+                    for k,v in new_mappings.items():
+                        self.mappings[k] = v
+            else:
+                print('No translation keys matched!')
+    
+    def complete_add(self, text: str, line: str, begidx: int, endidx: int) -> list:
+        if text == '':
+            return [k for k in {key.partition('.')[0] for key in self.flat_keys}]
+        results = [key for key in self.flat_keys if key.startswith(text)]
+        if len(results) > 40:
+            # Collapse categories
+            if text[-1] != '.':
+                text = text + '.'
+            level = text.count('.') + 1
+            new_results = {'.'.join(key.split('.')[:level]) for key in results}
+            return list(new_results)
+        return results
+
+    def do_remove(self, arg):
+        '''
+        Remove a pending rename mapping. Takes the old name of the key, not the new one.
+        e.g. a single key:  remove commands.execution.argument_error
+        e.g. a group:       remove commands.enter_dungeon
+        '''
+        old = arg.split()[0]
+        if old in self.mappings:
+            self.mappings.pop(old)
+        else:
+            # Check if we are renaming a higher level
+            if not old.endswith('.'):
+                old = old + '.'
+            results = [key for key in self.mappings if key.startswith(old)]
+            if len(results) > 0:
+                # Ask for confirmation
+                print('Will remove the following pending mappings:')
+                print(JsonHelpers.pprint_keys(results))
+                print('Delete these mappings? [y/N]')
+                if self.prompt_yn():
+                    for key in results:
+                        self.mappings.pop(key)
+            else:
+                print('No pending rename mappings matched!')
+    
+    def complete_remove(self, text: str, line: str, begidx: int, endidx: int) -> list:
+        return [key for key in self.mappings if key.startswith(text)]
+
+    def do_rename(self, _arg):
+        'Applies pending renames and overwrites language jsons.'
+        # Ask for confirmation
+        print('Will perform the following mappings:')
+        ppprint(self.mappings)
+        print('Perform and save these rename mappings? [y/N]')
+        if self.prompt_yn():
+            self.language_manager.rename_keys(self.mappings)
+            self.language_manager.update_all_languages_from_flattened()
+            print('Renamed keys, closing')
+            return True
+        else:
+            print('Do you instead wish to quit without saving? [yes/N]')
+            if self.prompt_yn(True):
+                print('Left rename shell without renaming')
+                return True
+
+    def prompt_yn(self, strict_yes=False):
+        if strict_yes:
+            return input('(yes/N) ').lower() == 'yes'
+        return input('(y/N) ').lower()[0] == 'y'
+
+
+def main(args: argparse.Namespace):
+    # print(args)
+    language_manager = LanguageManager()
+    errors = None
+    if args.lint_report:
+        language_manager.lint_report()
+        missing = language_manager.used_keys - language_manager.key_sets[LANGUAGE_FILENAMES.index(PRIMARY_LANGUAGE)]
+        if len(missing) > 0:
+            errors = f'[ERROR] {len(missing)} keys missing from primary language json!\n{JsonHelpers.pprint_keys(missing)}'
+    if prefix := args.save_flattened:
+        language_manager.save_flattened_languages(prefix)
+    if args.update:
+        print('Updating secondary languages')
+        language_manager.update_secondary_languages()
+    if args.interactive_rename:
+        language_manager.load_jsons()  # Previous actions may have changed them on-disk
+        try:
+            InteractiveRename(language_manager).cmdloop()
+        except KeyboardInterrupt:
+            print('Left rename shell without renaming')
+    if errors:
+        print(errors)
+        exit(1)
+
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(description="Manage Grasscutter's language json files.")
+    parser.add_argument('-u', '--update', action='store_true',
+        help=f'Update secondary language files to conform to the layout of the primary language file ({PRIMARY_LANGUAGE}) and contain any new keys from it.')
+    parser.add_argument('-l', '--lint-report', action='store_true',
+        help='Prints a lint report, listing unused, missing, and untranslated keys among all language jsons.')
+    parser.add_argument('-f', '--save-flattened', const='./flat_', metavar='prefix', nargs='?',
+        help='Save copies of all the language jsons in a flattened key form.')
+    parser.add_argument('-i', '--interactive-rename', action='store_true',
+        help='Enter interactive rename mode, in which you can specify keys in flattened form to be renamed.')
+    args = parser.parse_args()
+    main(args)
\ No newline at end of file
diff --git a/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java b/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java
index ba77e7d6..efe63753 100644
--- a/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java
+++ b/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java
@@ -31,7 +31,7 @@ public final class DefaultAuthentication implements AuthenticationSystem {
 
     @Override
     public Account verifyUser(String details) {
-        Grasscutter.getLogger().info(translate("dispatch.authentication.default_unable_to_verify"));
+        Grasscutter.getLogger().info(translate("messages.dispatch.authentication.default_unable_to_verify"));
         return null;
     }
 
diff --git a/src/main/java/emu/grasscutter/command/CommandMap.java b/src/main/java/emu/grasscutter/command/CommandMap.java
index bc15ec9a..fe19075b 100644
--- a/src/main/java/emu/grasscutter/command/CommandMap.java
+++ b/src/main/java/emu/grasscutter/command/CommandMap.java
@@ -106,7 +106,12 @@ public final class CommandMap {
      * @return The command handler.
      */
     public CommandHandler getHandler(String label) {
-        return this.commands.get(label);
+        CommandHandler handler = this.commands.get(label);
+        if (handler == null) {
+            // Try getting by alias
+            handler = this.aliases.get(label);
+        }
+        return handler;
     }
 
     private Player getTargetPlayer(String playerId, Player player, Player targetPlayer, List<String> args) {
@@ -129,7 +134,7 @@ public final class CommandMap {
                     }
                     return targetPlayer;
                 } catch (NumberFormatException e) {
-                    CommandHandler.sendTranslatedMessage(player, "commands.execution.uid_error");
+                    CommandHandler.sendTranslatedMessage(player, "commands.generic.invalid.uid");
                     throw new IllegalArgumentException();
                 }
             }
@@ -177,7 +182,7 @@ public final class CommandMap {
             CommandHandler.sendTranslatedMessage(player, targetPlayer.isOnline()? "commands.execution.set_target_online" : "commands.execution.set_target_offline", targetUid);
             return true;
         } catch (NumberFormatException e) {
-            CommandHandler.sendTranslatedMessage(player, "commands.execution.uid_error");
+            CommandHandler.sendTranslatedMessage(player, "commands.generic.invalid.uid");
             return false;
         }
     }
@@ -220,12 +225,9 @@ public final class CommandMap {
         }
 
         // Get command handler.
-        CommandHandler handler = this.commands.get(label);
-        if(handler == null)
-            // Try to get the handler by alias.
-            handler = this.aliases.get(label);
+        CommandHandler handler = this.getHandler(label);
 
-        // Check if the handler is still null.
+        // Check if the handler is null.
         if (handler == null) {
             CommandHandler.sendTranslatedMessage(player, "commands.generic.unknown_command", label);
             return;
diff --git a/src/main/java/emu/grasscutter/command/commands/GiveCommand.java b/src/main/java/emu/grasscutter/command/commands/GiveCommand.java
index 74d36e93..dab45378 100644
--- a/src/main/java/emu/grasscutter/command/commands/GiveCommand.java
+++ b/src/main/java/emu/grasscutter/command/commands/GiveCommand.java
@@ -221,7 +221,6 @@ public final class GiveCommand implements CommandHandler {
                 case ITEM_RELIQUARY:
                     targetPlayer.getInventory().addItems(makeArtifacts(param), ActionReason.SubfieldDrop);
                     CommandHandler.sendTranslatedMessage(sender, "commands.give.given_level", Integer.toString(param.id), Integer.toString(param.lvl), Integer.toString(param.amount), Integer.toString(targetPlayer.getUid()));
-                    //CommandHandler.sendTranslatedMessage(sender, "commands.giveArtifact.success", Integer.toString(param.id), Integer.toString(targetPlayer.getUid()));
                     return;
                 default:
                     targetPlayer.getInventory().addItem(new GameItem(param.data, param.amount), ActionReason.SubfieldDrop);
diff --git a/src/main/java/emu/grasscutter/command/commands/HelpCommand.java b/src/main/java/emu/grasscutter/command/commands/HelpCommand.java
index 8a00d9cf..65e7228c 100644
--- a/src/main/java/emu/grasscutter/command/commands/HelpCommand.java
+++ b/src/main/java/emu/grasscutter/command/commands/HelpCommand.java
@@ -1,6 +1,5 @@
 package emu.grasscutter.command.commands;
 
-import emu.grasscutter.Grasscutter;
 import emu.grasscutter.command.Command;
 import emu.grasscutter.command.CommandHandler;
 import emu.grasscutter.command.CommandMap;
@@ -13,6 +12,28 @@ import static emu.grasscutter.utils.Language.translate;
 @Command(label = "help", usage = "help [command]", description = "commands.help.description", targetRequirement = Command.TargetRequirement.NONE)
 public final class HelpCommand implements CommandHandler {
 
+    private void createCommand(StringBuilder builder, Player player, Command annotation) {
+        builder.append("\n").append(annotation.label()).append(" - ").append(translate(player, annotation.description()));
+        builder.append("\n\t").append(translate(player, "commands.help.usage"));
+        if (annotation.aliases().length >= 1) {
+            builder.append("\n\t").append(translate(player, "commands.help.aliases"));
+            for (String alias : annotation.aliases()) {
+                builder.append(alias).append(" ");
+            }
+        }
+        builder.append("\n\t").append(translate(player, "commands.help.tip_need_permission"));
+        if(annotation.permission().isEmpty() || annotation.permission().isBlank()) {
+            builder.append(translate(player, "commands.help.tip_need_no_permission"));
+        } else {
+            builder.append(annotation.permission());
+        }
+
+        if(!annotation.permissionTargeted().isEmpty() && !annotation.permissionTargeted().isBlank()) {
+            String permissionTargeted = annotation.permissionTargeted();
+            builder.append(" ").append(translate(player, "commands.help.tip_permission_targeted", permissionTargeted));
+        }
+    }
+
     @Override
     public void execute(Player player, Player targetPlayer, List<String> args) {
         if (args.size() < 1) {
@@ -32,38 +53,16 @@ public final class HelpCommand implements CommandHandler {
         } else {
             String command = args.get(0);
             CommandHandler handler = CommandMap.getInstance().getHandler(command);
-            StringBuilder builder = new StringBuilder(player == null ? "\n" + translate(player, "commands.status.help") + " - " : translate(player, "commands.status.help") + " - ").append(command).append(": \n");
+            StringBuilder builder = new StringBuilder("");
             if (handler == null) {
                 builder.append(translate(player, "commands.generic.command_exist_error"));
             } else {
                 Command annotation = handler.getClass().getAnnotation(Command.class);
 
-                builder.append("   ").append(translate(player, annotation.description())).append("\n");
-                builder.append(translate(player, "commands.help.usage")).append(annotation.usage());
-                if (annotation.aliases().length >= 1) {
-                    builder.append("\n").append(translate(player, "commands.help.aliases"));
-                    for (String alias : annotation.aliases()) {
-                        builder.append(alias).append(" ");
-                    }
-                }
-
-                builder.append("\n").append(translate(player, "commands.help.tip_need_permission"));
-                if(annotation.permission().isEmpty() || annotation.permission().isBlank()) {
-                    builder.append(translate(player, "commands.help.tip_need_no_permission"));
-                }
-                else {
-                    builder.append(annotation.permission());
-                }
-                builder.append(" ");
-
-                if(!annotation.permissionTargeted().isEmpty() && !annotation.permissionTargeted().isBlank()) {
-                    String permissionTargeted = annotation.permissionTargeted();
-                    builder.append(translate(player, "commands.help.tip_permission_targeted", permissionTargeted));
-                }
+                this.createCommand(builder, player, annotation);
 
                 if (player != null && !Objects.equals(annotation.permission(), "") && !player.getAccount().hasPermission(annotation.permission())) {
-                    builder.append("\n ");
-                    builder.append(translate(player, "commands.help.warn_player_has_no_permission"));
+                    builder.append("\n\t").append(translate(player, "commands.help.warn_player_has_no_permission"));
                 }
             }
 
@@ -72,67 +71,12 @@ public final class HelpCommand implements CommandHandler {
     }
 
     void SendAllHelpMessage(Player player, List<Command> annotations) {
-        if (player == null) {
-            StringBuilder builder = new StringBuilder("\n" + translate(player, "commands.help.available_commands") + "\n");
-            annotations.forEach(annotation -> {
-                builder.append(annotation.label()).append("\n");
-                builder.append("   ").append(translate(player, annotation.description())).append("\n");
-                builder.append(translate(player, "commands.help.usage")).append(annotation.usage());
-                if (annotation.aliases().length >= 1) {
-                    builder.append("\n").append(translate(player, "commands.help.aliases"));
-                    for (String alias : annotation.aliases()) {
-                        builder.append(alias).append(" ");
-                    }
-                }
-                builder.append("\n").append(translate(player, "commands.help.tip_need_permission"));
-                if(annotation.permission().isEmpty() || annotation.permission().isBlank()) {
-                    builder.append(translate(player, "commands.help.tip_need_no_permission"));
-                }
-                else {
-                    builder.append(annotation.permission());
-                }
-
-                builder.append(" ");
-
-                if(!annotation.permissionTargeted().isEmpty() && !annotation.permissionTargeted().isBlank()) {
-                    String permissionTargeted = annotation.permissionTargeted();
-                    builder.append(translate(player, "commands.help.tip_permission_targeted", permissionTargeted));
-                }
+        StringBuilder builder = new StringBuilder(translate(player, "commands.help.available_commands"));
+        annotations.forEach(annotation -> {
+            this.createCommand(builder, player, annotation);
+            builder.append("\n");
+        });
 
-                builder.append("\n");
-            });
-
-            CommandHandler.sendMessage(null, builder.toString());
-        } else {
-            CommandHandler.sendMessage(player, translate(player, "commands.help.available_commands"));
-            annotations.forEach(annotation -> {
-                StringBuilder builder = new StringBuilder(annotation.label()).append("\n");
-                builder.append("   ").append(translate(player, annotation.description())).append("\n");
-                builder.append(translate(player, "commands.help.usage")).append(annotation.usage());
-                if (annotation.aliases().length >= 1) {
-                    builder.append("\n").append(translate(player, "commands.help.aliases"));
-                    for (String alias : annotation.aliases()) {
-                        builder.append(alias).append(" ");
-                    }
-                }
-                builder.append("\n").append(translate(player, "commands.help.tip_need_permission"));
-                if(annotation.permission().isEmpty() || annotation.permission().isBlank()) {
-                    builder.append(translate(player, "commands.help.tip_need_no_permission"));
-                }
-                else {
-                    builder.append(annotation.permission());
-                }
-
-                builder.append(" ");
-
-                if(!annotation.permissionTargeted().isEmpty() && !annotation.permissionTargeted().isBlank()) {
-                    String permissionTargeted = annotation.permissionTargeted();
-                    builder.append(translate(player, "commands.help.tip_permission_targeted", permissionTargeted));
-                }
-
-
-                CommandHandler.sendMessage(player, builder.toString());
-            });
-        }
+        CommandHandler.sendMessage(player, builder.toString());
     }
 }
diff --git a/src/main/java/emu/grasscutter/command/commands/SendMailCommand.java b/src/main/java/emu/grasscutter/command/commands/SendMailCommand.java
index c4369fa8..79f13e20 100644
--- a/src/main/java/emu/grasscutter/command/commands/SendMailCommand.java
+++ b/src/main/java/emu/grasscutter/command/commands/SendMailCommand.java
@@ -65,7 +65,7 @@ public final class SendMailCommand implements CommandHandler {
                 switch (args.get(0).toLowerCase()) {
                     case "stop" -> {
                         mailBeingConstructed.remove(senderId);
-                        CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.sendCancel"));
+                        CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.send_cancel"));
                         return;
                     }
                     case "finish" -> {
diff --git a/src/main/resources/languages/pl-PL.json b/src/main/resources/languages/pl-PL.json
index edc44465..41d63f70 100644
--- a/src/main/resources/languages/pl-PL.json
+++ b/src/main/resources/languages/pl-PL.json
@@ -1,99 +1,106 @@
 {
   "messages": {
     "game": {
-      "port_bind": "Serwer gry uruchomiony na porcie: %s",
-      "connect": "Klient po艂膮czy艂 si臋 z %s",
-      "disconnect": "Klient roz艂膮czy艂 si臋 z %s",
+      "port_bind": "Serwer gry zosta艂 uruchomiony na porcie %s.",
+      "connect": "Klient po艂膮czy艂 si臋 z %s.",
+      "disconnect": "Klient roz艂膮czy艂 si臋 z %s.",
       "game_update_error": "Wyst膮pi艂 b艂膮d podczas aktualizacji gry.",
-      "command_error": "B艂膮d komendy:"
+      "command_error": "B艂膮d komendy: "
     },
     "dispatch": {
-      "port_bind": "[Dispatch] Serwer dispatch wystartowa艂 na porcie %s",
-      "request": "[Dispatch] Klient %s %s zapytanie: %s",
+      "port_bind": "[Dispatch] Serwer Dispatch zosta艂 uruchomiony na porcie %s.",
+      "request": "[Dispatch] 呕膮danie klienta %s (metoda %s): %s",
       "keystore": {
-        "general_error": "[Dispatch] B艂膮d 艂膮dowania keystore!",
-        "password_error": "[Dispatch] Nie mo偶na za艂adowa膰 keystore. Pr贸ba z domy艣lnym has艂em keystore...",
-        "no_keystore_error": "[Dispatch] Brak certyfikatu SSL! Przej艣cie na serwer HTTP.",
-        "default_password": "[Dispatch] Domy艣lne has艂o keystore zadzia艂a艂o. Rozwa偶 ustawienie go na 123456 w pliku config.json."
+        "general_error": "[Keystore] Wyst膮pi艂 b艂膮d podczas 艂adowania keystore.",
+        "password_error": "[Keystore] Nie mo偶na za艂adowa膰 keystore. Spr贸buj臋 u偶y膰 domy艣lnego has艂a...",
+        "no_keystore_error": "[Keystore] Brak certyfikatu SSL. Przej艣cie na serwer HTTP.",
+        "default_password": "[Keystore] Domy艣lne has艂o keystore zosta艂o za艂adowane. Rozwa偶 ustawienie tego has艂a jako \"123456\" w pliku config.json."
+      },
+      "authentication": {
+        "default_unable_to_verify": "[Authentication] Co艣 wywo艂a艂o metod臋 \"verifyUser\", kt贸ra jest niedost臋pna w domy艣lnym kontrolerze uwierzytelniaj膮cym."
       },
       "no_commands_error": "Komendy nie s膮 wspierane w trybie DISPATCH_ONLY.",
-      "unhandled_request_error": "[Dispatch] Potencjalnie niepodtrzymane %s zapytanie: %s",
+      "unhandled_request_error": "[Dispatch] Potencjalnie nierozstrzygni臋te 偶膮danie (metoda %s): %s",
       "account": {
-        "login_attempt": "[Dispatch] Klient %s pr贸buje si臋 zalogowa膰",
-        "login_success": "[Dispatch] Klient %s zalogowa艂 si臋 jako %s",
-        "login_max_player_limit": "[Dispatch] Klient %s nie powiod艂o si臋: Liczba graczy online osi膮gn臋艂a limit",
-        "login_token_attempt": "[Dispatch] Klient %s pr贸buje si臋 zalogowa膰 poprzez token",
-        "login_token_error": "[Dispatch] Klient %s nie m贸g艂 si臋 zalogowa膰 poprzez token",
-        "login_token_success": "[Dispatch] Klient %s zalogowa艂 si臋 poprzez token jako %s",
-        "combo_token_success": "[Dispatch] Klient %s pomy艣lnie wymieni艂 combo token",
-        "combo_token_error": "[Dispatch] Klient %s nie wymieni艂 combo token'u",
-        "account_login_create_success": "[Dispatch] Klient %s nie m贸g艂 si臋 zalogowa膰: Konto %s stworzone",
-        "account_login_create_error": "[Dispatch] Klient %s nie m贸g艂 si臋 zalogowa膰: Tworzenie konta nie powiod艂o si臋",
-        "account_login_exist_error": "[Dispatch] Klient %s nie m贸g艂 si臋 zalogowa膰: Nie znaleziono konta",
-        "account_cache_error": "B艂膮d pami臋ci cache konta gry",
+        "login_attempt": "[Account] Klient %s pr贸buje si臋 zalogowa膰.",
+        "login_success": "[Account] Klient %s zalogowa艂 si臋 jako %s.",
+        "login_max_player_limit": "[Account] Logowanie klienta %s nie powiod艂o si臋: liczba graczy online osi膮gn臋艂a sw贸j limit.",
+        "login_token_attempt": "[Account] Klient %s pr贸buje si臋 zalogowa膰 poprzez token.",
+        "login_token_error": "[Account] Logowanie klienta %s poprzez token nie powiod艂o si臋.",
+        "login_token_success": "[Account] Klient %s zalogowa艂 si臋 poprzez token jako %s.",
+        "combo_token_success": "[Account] Klient %s pomy艣lnie wymieni艂 token combo.",
+        "combo_token_error": "[Account] Wymienienie tokena combo klienta %s nie powiod艂o si臋.",
+        "account_login_create_success": "[Account] Logowanie klienta %s powiod艂o si臋: konto %s zosta艂o stworzone.",
+        "account_login_create_error": "[Account] Logowanie klienta %s nie powiod艂o si臋: tworzenie konta nie powiod艂o si臋.",
+        "account_login_exist_error": "[Account] Logowanie klienta %s nie powiod艂o si臋: nie znaleziono konta.",
+        "account_cache_error": "B艂膮d pami臋ci cache konta gry.",
         "session_key_error": "B艂臋dny klucz sesji.",
-        "username_error": "Nazwa u偶ytkownika nie znaleziona.",
-        "username_create_error": "Nazwa u偶ytkownika nie znaleziona, tworzenie nie powiod艂o si臋.",
-        "server_max_player_limit": "Liczba graczy online osi膮gn臋艂a limit"
-      }
+        "username_error": "Podana nazwa u偶ytkownika nie istnieje.",
+        "username_create_error": "Podana nazwa u偶ytkownika nie istnieje. Automatyczne tworzenie nowego konta nie powiod艂o si臋.",
+        "server_max_player_limit": "Liczba graczy online osi膮gn臋艂a sw贸j limit."
+      },
+      "router_error": "[Dispatch] Wyst膮pi艂 b艂膮d podczas tworzenia routera."
     },
     "status": {
-      "free_software": "Grasscutter to DARMOWE oprogramowanie. Je偶eli kto艣 Ci je sprzeda艂, to zosta艂e艣 oscamowany. Strona domowa: https://github.com/Grasscutters/Grasscutter",
+      "free_software": "Grasscutter to DARMOWE oprogramowanie oparte na licencji AGPL-3.0. Je偶eli za nie zap艂aci艂e艣, zosta艂e艣 oszukany. Strona projektu: https://github.com/Grasscutters/Grasscutter",
       "starting": "Uruchamianie Grasscutter...",
-      "shutdown": "Wy艂膮czanie...",
-      "done": "Gotowe! Wpisz \"help\" aby uzyska膰 pomoc",
+      "shutdown": "Zatrzymywanie Grasscutter...",
+      "done": "Gotowe! Wpisz \"help\", aby uzyska膰 pomoc.",
       "error": "Wyst膮pi艂 b艂膮d.",
-      "welcome": "Witamy w Grasscutter",
+      "welcome": "Witamy w Grasscutter!",
       "run_mode_error": "B艂臋dny tryb pracy serwera: %s.",
-      "run_mode_help": "Tryb pracy serwera musi by膰 ustawiony na 'HYBRID', 'DISPATCH_ONLY', lub 'GAME_ONLY'. Nie mo偶na wystartowa膰 Grasscutter...",
-      "create_resources": "Tworzenie folderu resources...",
-      "resources_error": "Umie艣膰 kopi臋 'BinOutput' i 'ExcelBinOutput' w folderze resources.",
-      "version": "Grasscutter versi贸n: %s-%s",
-      "game_version": "Game versi贸n: %s",
+      "run_mode_help": "Tryb pracy serwera musi by膰 ustawiony na \"HYBRID\", \"DISPATCH_ONLY\", lub \"GAME_ONLY\".",
+      "create_resources": "Tworzenie folderu \"resources\"...",
+      "resources_error": "Umie艣膰 kopi臋 folder贸w \"BinOutput\" oraz \"ExcelBinOutput\" w folderze \"resources\" i spr贸buj ponownie.",
+      "version": "Wersja Grasscutter: %s-%s",
+      "game_version": "Wersja gry: %s",
       "resources": {
-        "loading": "Loading resources...",
-        "finish": "Finished loading resources."
+        "loading": "艁adowanie zasob贸w...",
+        "finish": "Za艂adowano zasoby."
       }
     }
   },
   "commands": {
     "generic": {
       "not_specified": "Nie podano komendy.",
-      "unknown_command": "Nieznana komenda: %s",
-      "permission_error": "Nie masz uprawnie艅 do tej komendy.",
-      "console_execute_error": "T膮 komende mo偶na wywo艂a膰 tylko z konsoli.",
-      "player_execute_error": "Wywo艂aj t膮 komend臋 w grze.",
+      "unknown_command": "Nieznana komenda: %s.",
+      "permission_error": "Nie masz wystarczaj膮cych uprawnie艅 do u偶ywania tej komendy.",
+      "console_execute_error": "Ta komenda mo偶e by膰 u偶yta tylko w konsoli.",
+      "player_execute_error": "Ta komenda mo偶e by膰 u偶yta tylko w grze.",
       "command_exist_error": "Nie znaleziono komendy.",
+      "no_usage_specified": "Brak przyk艂adu zastosowania.",
+      "no_description_specified": "Brak opisu.",
       "set_to": "%s ustawiono na %s.",
-      "set_for_to": "%s dla %s ustawiono na %s.",
+      "set_for_to": "%s (UID %s) ustawiono na %s.",
       "invalid": {
         "amount": "B艂臋dna ilo艣膰.",
         "artifactId": "B艂臋dne ID artefaktu.",
-        "avatarId": "B艂臋dne id postaci.",
+        "avatarId": "B艂臋dne ID postaci.",
         "avatarLevel": "B艂臋dny poziom postaci.",
-        "entityId": "B艂臋dne id obiektu.",
-        "id przedmiotu": "B艂臋dne id przedmiotu.",
+        "entityId": "B艂臋dne ID obiektu.",
+        "itemId": "B艂臋dne ID przedmiotu.",
         "itemLevel": "B艂臋dny poziom przedmiotu.",
         "itemRefinement": "B艂臋dne ulepszenie.",
-        "value_between": "Invalid value: %s must be between %s and %s.",
-        "playerId": "B艂臋dne playerId.",
+        "statValue": "B艂臋dna warto艣膰 atrybutu.",
+        "value_between": "B艂臋dna warto艣膰: %s musi by膰 pomi臋dzy %s a %s.",
+        "playerId": "B艂臋dne ID gracza.",
         "uid": "B艂臋dne UID.",
         "id": "B艂臋dne ID."
       }
     },
     "execution": {
-      "player_exist_error": "Gracz nie znaleziony.",
+      "player_exist_error": "Gracz nie istnieje.",
       "player_offline_error": "Gracz nie jest online.",
-      "item_player_exist_error": "B艂臋dny przedmiot lub UID.",
-      "player_exist_offline_error": "Gracz nie znaleziony lub jest offline.",
-      "argument_error": "B艂臋dne argumenty.",
-      "clear_target": "Cel wyczyszczony.",
-      "set_target": "Nast臋pne komendy b臋d膮 celowa膰 w @%s.",
-      "set_target_online": "@%s jest online. Niekt贸re polecenia mog膮 wymaga膰 celu offline.",
-      "set_target_offline": "@%s jest offline. Niekt贸re polecenia mog膮 wymaga膰 celu online.",
-      "need_target": "Ta komenda wymaga docelowego UID. Dodaj argument <@UID> lub ustaw sta艂y cel poleceniem /target @UID.",
-      "need_target_online": "To polecenie wymaga identyfikatora UID celu w trybie online, ale bie偶膮cy cel jest w trybie offline. Dodaj inny argument <@UID> lub ustaw trwa艂y cel za pomoc膮 /target @UID.",
-      "need_target_offline": "To polecenie wymaga identyfikatora UID celu offline, ale bie偶膮cy cel jest online. Dodaj inny argument <@UID> lub ustaw trwa艂y cel za pomoc膮 /target @UID."
+      "item_player_exist_error": "B艂臋dny przedmiot lub ID.",
+      "player_exist_offline_error": "Gracz nie istnieje lub nie jest online.",
+      "argument_error": "B艂臋dny argument lub argumenty.",
+      "clear_target": "Nast臋pne komendy nie b臋d膮 dotyczy艂y nikogo. B臋dziesz musia艂/a sam/a doda膰 ID gracza docelowego do ka偶dej kolejnej komendy.",
+      "set_target": "Nast臋pne komendy b臋d膮 dotyczy艂y gracza @%s.",
+      "set_target_online": "Gracz @%s jest online. Niekt贸re polecenia wymagaj膮 trybu offline i mog膮 nie dzia艂a膰.",
+      "set_target_offline": "Gracz @%s jest offline. Niekt贸re polecenia wymagaj膮 trybu online i mog膮 nie dzia艂a膰.",
+      "need_target": "Ta komenda wymaga ID gracza. Dodaj argument <@ID>, lub ustaw ID tego gracza na sta艂e dla ka偶dej kolejnej komendy poleceniem \"set_target @ID\".",
+      "need_target_online": "Ta komenda wymaga ID gracza, kt贸ry jest online, ale bie偶膮cy gracz docelowy jest w trybie offline. Dodaj inny argument <@ID> lub ustaw ID gracza na sta艂e dla ka偶dej kolejnej komendy poleceniem \"set_target @ID\".",
+      "need_target_offline": "Ta komenda wymaga ID gracza, kt贸ry jest offline, ale bie偶膮cy gracz docelowy jest w trybie online. Dodaj inny argument <@ID> lub ustaw ID gracza na sta艂e dla ka偶dej kolejnej komendy poleceniem \"set_target @ID\"."
     },
     "status": {
       "enabled": "W艂膮czone",
@@ -102,221 +109,297 @@
       "success": "Sukces"
     },
     "account": {
-      "modify": "Modyfikuj konta u偶ytkownik贸w",
-      "invalid": "B艂臋dne UID.",
-      "exists": "Konto ju偶 istnieje.",
+      "usage": "account <create|delete> <nazwa gracza> [UID]",
+      "invalid": "B艂臋dne UID gracza.",
+      "exists": "Konto o tej nazwie u偶ytkownika i/lub UID ju偶 istnieje.",
       "create": "Stworzono konto z UID %s.",
-      "delete": "Konto usuni臋te.",
+      "delete": "Konto zosta艂o usuni臋te.",
       "no_account": "Nie znaleziono konta.",
-      "command_usage": "U偶ycie: account <create|delete> <nazwa> [uid]"
+      "description": "Tw贸rz lub usu艅 konta."
+    },
+    "announce": {
+      "usage": "announce <tpl> <ID szablonu> LUB announce <refresh> LUB announce <wiadomo艣膰> LUB announce <revoke> <ID szablonu>",
+      "send_success": "Og艂oszenie zosta艂o pomy艣lnie wys艂ane. Mo偶esz je odwo艂a膰 u偶ywaj膮c \"announce revoke %s\".",
+      "refresh_success": "Od艣wie偶ono konfiguracj臋 og艂osze艅 (w sumie jest ich %s).",
+      "revoke_done": "Pomy艣lnie odwo艂ano og艂oszenie %s.",
+      "not_found": "Nie znaleziono og艂oszenia %s.",
+      "description": "Wysy艂aj i zarz膮dzaj og艂oszeniami."
     },
     "clear": {
-      "command_usage": "U偶ycie: clear <all|wp|art|mat> [lv<max level>] [r<max refinement>] [<max rarity>*]",
-      "weapons": "Wyczyszczono bronie dla %s.",
-      "artifacts": "Wyczyszczono artefakty dla %s.",
-      "materials": "Wyczyszczono materia艂y dla %s.",
-      "furniture": "Wyczyszczono meble dla %s.",
-      "displays": "Wyczyszczono displays dla %s.",
-      "virtuals": "Wyczyszczono virtuals dla %s.",
-      "everything": "Wyczyszczono wszystko dla %s."
+      "usage": "U偶ycie: clear <all|wp|art|mat> [lv<max level>] [r<max refinement>] [<max rarity>*]",
+      "weapons": "Usuni臋to bronie gracza %s.",
+      "artifacts": "Usuni臋to artefakty gracza %s.",
+      "materials": "Usuni臋to materia艂y gracza %s.",
+      "furniture": "Usuni臋to meble gracza %s.",
+      "displays": "Usuni臋to displaye gracza %s.",
+      "virtuals": "Usuni臋to virtuale gracza %s.",
+      "everything": "Usuni臋to wszystkie niewyposa偶one przedmioty gracza %s.",
+      "description": "Usu艅 niewyposa偶one przedmioty wskazanego gracza."
     },
     "coop": {
-      "usage": "U偶ycie: coop [host uid]",
-      "success": "Przyzwano %s do 艣wiata %s."
+      "usage": "coop @[ID gracza]",
+      "success": "Pomy艣lnie dodano gracza %s do 艣wiata gracza %s.",
+      "description": "Dodaj wskazanego gracza do swojego 艣wiata."
     },
     "enter_dungeon": {
-      "usage": "U偶ycie: enterdungeon <ID lochu>",
-      "changed": "Zmieniono loch na %s",
-      "not_found_error": "Ten loch nie istnieje",
-      "in_dungeon_error": "Ju偶 jeste艣 w tym lochu"
-    },
-    "giveAll": {
-      "usage": "U偶ycie: giveall [gracz] [ilo艣膰]",
-      "started": "Dodawanie wszystkich przedmiot贸w...",
-      "success": "Pomy艣lnie dodano wszystkie przedmioty dla %s.",
-      "invalid_amount_or_playerId": "B艂臋dna ilo艣膰 lub ID gracza."
-    },
-    "giveArtifact": {
-      "usage": "U偶ycie: giveart|gart [gracz] <id artefaktu> <mainPropId> [<appendPropId>[,<razy>]]... [poziom]",
-      "id_error": "B艂臋dne ID artefaktu.",
-      "success": "Dano %s dla %s."
+      "usage": "enterdungeon <ID lochu>",
+      "changed": "Pomy艣lnie zmieniono loch na %s.",
+      "not_found_error": "Podane ID lochu jest nieprawid艂owe.",
+      "in_dungeon_error": "Wskazany gracz ju偶 jest w tym lochu.",
+      "description": "Zmie艅 loch, w kt贸rym ma si臋 znajdowa膰 wskazany gracz."
     },
     "give": {
-            "usage": "U偶ycie: give <gracz> <id przedmiotu | avatarID> [ilo艣膰] [poziom] [refinement]",
-      "refinement_only_applicable_weapons": "Ulepszenie mo偶na zastosowa膰 tylko dla broni.",
-      "refinement_must_between_1_and_5": "Ulepszenie musi by膰 pomi臋dzy 1, a 5.",
-      "given": "Dano %s %s dla %s.",
-      "given_with_level_and_refinement": "Dano %s z poziomem %s, ulepszeniem %s %s razy dla %s",
-            "given_level": "Dano %s z poziomem %s %s razy dla %s",
-            "given_avatar": "Dano %s z poziomem %s dla %s."
-    },
-    "godmode": {
-      "success": "Godmode jest teraz %s dla %s."
+      "usage": "give <ID przedmiotu|ID awataru|all|weapons|mats|avatars> [x<ilo艣膰>] [lv<poziom>] [r<poziom ulepszenia>]",
+      "usage_relic": "give <ID reliktu> [ID pierwszego przedmiotu] [<ID drugiego przedmiotu>[, <ile razy je po艂膮czy膰>]]... [lv<poziom od 0 do 20>]",
+      "illegal_relic": "Ten ID reliktu znajduje si臋 na czarnej li艣cie i mo偶e by膰 nie tym, czego szukasz.",
+      "given": "Dodano %s przedmiot贸w o ID %s graczowi o ID %s.",
+      "given_with_level_and_refinement": "Dodano %s przedmiot贸w o poziomie %s oraz poziomie ulepszenia %s i ID %s graczowi o ID %s.",
+      "given_level": "Dodano %s artefakt贸w o poziomie %s oraz ID %s graczowi o ID %s.",
+      "given_avatar": "Dodano awatar o ID %s oraz poziomie %s graczowi o ID %s.",
+      "giveall_success": "Pomy艣lnie dodano wybrane przedmioty.",
+      "description": "Dodaj wybrane przedmioty do ekwipunku wybranego gracza."
     },
     "heal": {
-      "success": "Wszystkie postacie zosta艂y wyleczone."
+      "usage": "heal",
+      "success": "Wszystkie postacie zosta艂y uleczone.",
+      "description": "Ulecz wszystkie postacie w swoim zespole."
+    },
+    "help": {
+      "usage": "help [nazwa komendy]",
+      "usage_prefix": "U偶ycie: ",
+      "aliases": "Aliasy: ",
+      "available_commands": "Dost臋pne komendy: ",
+      "description": "Wy艣wietl wszystkie komendy lub informacje na temat danej komendy.",
+      "tip_need_permission": "Wymagane uprawnienie: ",
+      "tip_need_no_permission": "brak",
+      "tip_permission_targeted": "(u偶ycie tego polecenia na innych graczach r贸wnie偶 wymaga uprawnienia %s)",
+      "warn_player_has_no_permission": "Nie masz wystarczaj膮cych uprawnie艅 do u偶ywania tej komendy."
     },
     "kick": {
-      "player_kick_player": "Gracz [%s:%s] wyrzuci艂 gracza [%s:%s]",
-      "server_kick_player": "Wyrzucono gracza [%s:%s]"
+      "usage": "kick",
+      "player_kick_player": "Gracz [%s:%s] wyrzuci艂 gracza [%s:%s].",
+      "server_kick_player": "Wyrzucono gracza [%s:%s].",
+      "description": "Wyrzu膰 wskazanego gracza z gry."
     },
     "killall": {
-      "usage": "U偶ycie: killall [UID gracza] [ID sceny]",
-      "scene_not_found_in_player_world": "Scena nie znaleziona w 艣wiecie gracza",
-      "kill_monsters_in_scene": "Zabito %s potwor贸w w scenie %s"
+      "usage": "killall @[ID gracza] [ID sceny]",
+      "scene_not_found_in_player_world": "B艂臋dny ID sceny.",
+      "kill_monsters_in_scene": "Zabito %s potwor贸w w scenie %s.",
+      "description": "Zabij wszystkie potwory we wskazanej scenie."
     },
     "killCharacter": {
-      "usage": "U偶ycie: /killcharacter [ID gracza]",
-      "success": "Zabito aktualn膮 posta膰 gracza %s."
+      "usage": "killcharacter @[ID gracza]",
+      "success": "Pomy艣lnie zabito posta膰 gracza %s.",
+      "description": "Zabij posta膰 wskazanego gracza."
+    },
+    "language": {
+      "usage": "language [kod j臋zyka]",
+      "current_language": "Bie偶膮cy kod j臋zyka to %s.",
+      "language_changed": "Zmieniono j臋zyk na ten o kodzie %s.",
+      "language_not_found": "Nie znaleziono j臋zyka o kodzie \"%s\".",
+      "description": "Poka偶 lub zmie艅 bie偶膮cy kod j臋zyka."
     },
     "list": {
-      "success": "Teraz jest %s gracz(y) online:"
+      "usage": "list @[ID gracza]",
+      "success": "%s graczy online:",
+      "description": "Poka偶 ile jest graczy na serwerze."
     },
     "permission": {
-      "usage": "U偶ycie: permission <add|remove> <nazwa gracza> <uprawnienie>",
-      "add": "Dodano uprawnienie",
-      "has_error": "To konto ju偶 ma to uprawnienie!",
-      "remove": "Usuni臋to uprawnienie.",
-      "not_have_error": "To konto nie ma tych uprawnie艅!",
-      "account_error": "Konto nie mo偶e zosta膰 znalezione."
+      "usage": "permission <add|remove> <nazwa gracza> <uprawnienie>",
+      "add": "Pomy艣lnie dodano uprawnienie.",
+      "has_error": "Ten gracz ju偶 ma to uprawnienie.",
+      "remove": "Pomy艣lnie usuni臋to uprawnienie.",
+      "not_have_error": "Ten gracz nie ma tego uprawnienia.",
+      "account_error": "Podana nazwa gracza nie istnieje.",
+      "description": "Dodaj lub usu艅 uprawnienia podanego gracza."
     },
     "position": {
-      "success": "Koordynaty: %s, %s, %s\nID sceny: %s"
+      "usage": "position",
+      "success": "Koordynaty: (%s, %s, %s).\nID sceny: %s.",
+      "description": "Poka偶 gdzie znajduje si臋 dany gracz."
+    },
+    "quest": {
+      "usage": "quest <add|finish> [ID zadania]",
+      "added": "Zadanie %s zosta艂o dodane.",
+      "finished": "Zadanie %s zosta艂o zako艅czone.",
+      "not_found": "Nie ma zadania o podanym ID.",
+      "invalid_id": "B艂臋dny format ID zadania.",
+      "description": "Dodaj lub wykonaj wskazane zadanie."
     },
     "reload": {
-      "reload_start": "Ponowne 艂adowanie konfiguracji.",
-      "reload_done": "Ponowne 艂adowanie zako艅czone."
+      "usage": "reload",
+      "reload_start": "Ponowne 艂adowanie konfiguracji...",
+      "reload_done": "Ponowne 艂adowanie konfiguracji zako艅czone.",
+      "description": "Ponownie za艂aduj j臋zyk, konfiguracj臋 oraz inne dane gry."
     },
     "resetConst": {
-      "reset_all": "Resetuj konstelacje wszystkich postaci.",
-      "success": "Konstelacje dla %s zosta艂y zresetowane. Prosz臋 zalogowa膰 si臋 ponownie aby zobaczy膰 zmiany."
+      "usage": "resetconst [all]",
+      "reset_all": "Zresetowano konstelacje dla wszystkich postaci. Aby zobaczy膰 zmiany, zaloguj si臋 ponownie.",
+      "success": "Konstelacje awatara %s zosta艂y zresetowane. Aby zobaczy膰 zmiany, zaloguj si臋 ponownie.",
+      "description": "Resetuj konstelacje wszystkich lub wybranej postaci."
     },
     "resetShopLimit": {
-      "usage": "U偶ycie: /resetshop <ID gracza>",
-      "success": "Reset complete.",
-      "description": "Reset target player's shop refresh time"
+      "usage": "resetshop @<ID gracza>",
+      "success": "Zresetowano czas od艣wie偶ania sklepu podanego gracza.",
+      "description": "Resetuj czas od艣wie偶ania sklepu podanego gracza."
     },
     "sendMail": {
-      "usage": "U偶ycie: /sendmail <ID gracza | all | help> [id szablonu]",
-      "user_not_exist": "Gracz o ID '%s' nie istnieje",
-      "start_composition": "Komponowanie wiadomo艣ci.\nProsz臋 u偶yj '/sendmail <tytu艂>' aby kontynuowa膰.\nMo偶esz u偶y膰 '/sendmail stop' w dowolnym momencie",
-      "templates": "Szablony zostan膮 zaimplementowane nied艂ugo...",
+      "usage": "sendmail <@<ID gracza>|all|help> [ID szablonu]",
+      "user_not_exist": "Gracz o podanym ID %s nie istnieje.",
+      "start_composition": "Tworzenie wiadomo艣ci.\nU偶yj \"sendmail <tytu艂>\", aby kontynuowa膰.\nMo偶esz u偶y膰 \"sendmail stop\" w dowolnym momencie, aby przesta膰.",
+      "templates": "Szablony nie s膮 jeszcze gotowe do u偶ycia.",
       "invalid_arguments": "B艂臋dne argumenty.",
       "send_cancel": "Anulowano wysy艂anie wiadomo艣ci",
-      "send_done": "Wys艂ano wiadomo艣膰 do gracza %s!",
-      "send_all_done": "Wys艂ano wiadomo艣c do wszystkich graczy!",
-      "not_composition_end": "Komponowanie nie jest na ostatnim etapie.\nProsz臋 u偶yj '/sendmail %s' lub '/sendmail stop' aby anulowa膰",
-      "please_use": "Prosz臋 u偶yj '/sendmail %s'",
-      "set_title": "Tytu艂 wiadomo艣ci to teraz: '%s'.\nU偶yj '/sendmail <tre艣膰>' aby kontynuowa膰.",
-      "set_contents": "Tre艣膰 wiadomo艣ci to teraz '%s'.\nU偶yj '/sendmail <nadawca>' aby kontynuowa膰.",
-      "set_message_sender": "Nadawca wiadomo艣ci to teraz '%s'.\nU偶yj '/sendmail <id przedmiotu | nazwa przedmiotu | zako艅cz> [ilo艣膰] [poziom]' aby kontynuowa膰.",
-      "send": "Za艂膮czono %s %s (poziom %s) do wiadomo艣ci.\nDodaj wi臋cej przedmiot贸w lub u偶yj '/sendmail finish' aby wys艂a膰 wiadomo艣膰.",
-      "invalid_arguments_please_use": "B艂臋dne argumenty.\nProsz臋 u偶yj '/sendmail %s'",
+      "send_done": "Wys艂ano wiadomo艣膰 do gracza %s.",
+      "send_all_done": "Wys艂ano wiadomo艣膰 do wszystkich graczy.",
+      "not_composition_end": "Tworzenie wiadomo艣ci nie jest jeszcze na ostatnim etapie. Je偶eli naprawd臋 chcesz teraz przesta膰, u偶yj \"sendmail %s\" lub \"sendmail stop\".",
+      "please_use": "U偶ycie: \"sendmail %s\".",
+      "set_title": "Tytu艂 wiadomo艣ci to teraz:\n\n%s\n\nU偶yj \"sendmail <tre艣膰>\", aby kontynuowa膰.",
+      "set_contents": "Tre艣膰 wiadomo艣ci to teraz:\n\n%s\n\nU偶yj \"sendmail <nadawca>\", aby kontynuowa膰.",
+      "set_message_sender": "Nadawca wiadomo艣ci to teraz:\n\n%s\n\nU偶yj \"sendmail %s\", aby kontynuowa膰.",
+      "send": "Za艂膮czono %s przedmiot贸w %s o poziomie %s do wiadomo艣ci.\nMo偶esz doda膰 wi臋cej przedmiot贸w lub u偶y膰 \"sendmail finish\", aby wys艂a膰 wiadomo艣膰.",
+      "invalid_arguments_please_use": "B艂臋dne argumenty.\nProsz臋 u偶yj \"sendmail %s\"",
       "title": "<tytu艂>",
       "message": "<wiadomo艣膰>",
       "sender": "<nadawca>",
-      "arguments": "<id przedmiotu | nazwa przedmiotu | zako艅cz> [ilo艣膰] [poziom]",
-      "error": "B艁膭D: niepoprawny etap konstrukcji: %s. Sprawd藕 konsol臋 aby dowiedzie膰 si臋 wi臋cej."
+      "arguments": "<ID przedmiotu|nazwa przedmiotu|finish> [ilo艣膰] [poziom] [poziom ulepszenia]",
+      "error": "B艁膭D: niepoprawny etap konstrukcji: %s.",
+      "description": "Wy艣lij wiadomo艣膰 wraz z przedmiotami do wybranego lub wszystkich graczy."
     },
     "sendMessage": {
-      "usage": "U偶ycie: /sendmessage <player> <message>",
-      "success": "Wiadomo艣膰 wys艂ana."
+      "usage": "sendmessage @<ID gracza> <wiadomo艣膰>",
+      "success": "Wiadomo艣膰 wys艂ana.",
+      "description": "Wy艣lij wiadomo艣膰 do gracza jako serwer. Je艣li nie okre艣lono celu, wysy艂a do wszystkich graczy na serwerze."
     },
     "setFetterLevel": {
-      "usage": "U偶ycie: setfetterlevel <poziom>",
-      "range_error": "Poziom przyja藕ni musi by膰 pomi臋dzy 0,a 10.",
-      "success": "Poziom przyja藕ni ustawiono na: %s",
-      "level_error": "B艂臋dny poziom przyja藕ni."
+      "usage": "setfetterlevel <poziom przyja藕ni>",
+      "range_error": "Poziom przyja藕ni musi by膰 pomi臋dzy 0 a 10.",
+      "success": "Poziom przyja藕ni zosta艂 pomy艣lnie ustawiony na %s.",
+      "level_error": "B艂臋dny poziom przyja藕ni.",
+      "description": "Ustaw poziom przyja藕ni obecnej postaci."
     },
     "setProp": {
-      "usage": "Usage: setprop|prop <prop> <value>\n\tValues for <prop>: godmode | nostamina | unlimitedenergy | abyss | worldlevel | bplevel\n\t(cont.) see PlayerProperty enum for other possible values, of form PROP_MAX_SPRING_VOLUME -> max_spring_volume",
-      "description": "Sets accountwide properties. Things like godmode can be enabled this way, as well as changing things like unlocked abyss floor and battle pass progress."
+      "usage": "setprop <nazwa w艂asno艣ci> <warto艣膰>\n\tMo偶liwe nazwy w艂asno艣ci: godmode | nostamina | unlimitedenergy | abyss | worldlevel | bplevel | ...\n\tTa komenda ma wi臋cej nazw w艂asno艣ci, kt贸re mo偶e otrzyma膰. Mo偶esz je wszystkie zobaczy膰 w pliku \"game/props/PlayerProperty.java\".\n\tW tym pliku, przyjmuj膮 one form臋 \"PROP_XXX_YYY_ZZZ\", ale powiniene艣 je zapisywa膰 jako \"xxx_yyy_zzz\" je艣li chcesz je u偶y膰 w tej komendzie.",
+      "description": "Ustaw pewne w艂asno艣ci konta, takie jak tryb nie艣miertelno艣ci (godmode) czy te偶 zmiana post臋pu Battle Pass."
     },
     "setStats": {
-      "usage": "U偶ycie: setstats|stats <statystyka> <warto艣膰>\n\tWarto艣ci dla Statystyka: hp | maxhp | def | atk | em | er | crate | cdmg | cdr | heal | heali | shield | defi\n\t(cont.) Bonus DMG 偶ywio艂u: epyro | ecryo | ehydro | egeo | edendro | eelectro | ephys\n\t(cont.) RES na 偶ywio艂: respyro | rescryo | reshydro | resgeo | resdendro | reselectro | resphys\n",
-      "description": "Sets fight property for your current active character"
-    },
-    "setWorldLevel": {
-      "usage": "U偶ycie: setworldlevel <poziom>",
-      "value_error": "Poziom 艣wiata musi by膰 pomi臋dzy 0, a 8",
-      "success": "Ustawiono poziom 艣wiata na: %s.",
-      "invalid_world_level": "Invalid world level."
+      "usage": "setstats <nazwa statystyki> <warto艣膰>\n\tMo偶liwe nazwy statystyki: hp | maxhp | def | atk | em | er | crate | cdmg | cdr | heal | heali | shield | defi\n\tDodatkowe obra偶enia od 偶ywio艂u: epyro | ecryo | ehydro | egeo | edendro | eelectro | ephys\n\tOdporno艣膰 na 偶ywio艂: respyro | rescryo | reshydro | resgeo | resdendro | reselectro | resphys",
+      "description": "Ustaw statystyk臋 walki dla obecnie wybranej postaci wybranego gracza."
     },
     "spawn": {
-      "usage": "U偶ycie: /spawn <id obiektu> [ilo艣膰] [poziom(tylko potwory)]",
-      "success": "Stworzono %s %s."
+      "usage": "spawn <ID obiektu> [ilo艣膰] [poziom (tylko potwory)] [<x> <y> <z> (tylko potwory)]",
+      "success": "Stworzono %s obiekt贸w o ID %s.",
+      "limit_reached": "Osi膮gni臋to maksymaln膮 ilo艣膰 obiekt贸w w scenie. Dodane zostan膮 tylko %s.",
+      "description": "Dodaj wskazane obiekty do sceny wybranego gracza."
     },
     "stop": {
-      "success": "Serwer wy艂膮cza si臋..."
+      "usage": "stop",
+      "success": "Serwer zatrzymuje si臋...",
+      "description": "Zatrzymaj serwer."
     },
     "talent": {
-      "usage_1": "Aby ustawi膰 poziom talentu: /talent set <ID talentu> <warto艣膰>",
-      "usage_2": "Inny spos贸b na ustawienie poziomu talentu: /talent <n lub e lub q> <warto艣膰>",
-      "usage_3": "Aby uzyska膰 ID talentu: /talent getid",
-      "lower_16": "B艂臋dny poziom talentu. Poziom powinien by膰 mniejszy ni偶 16",
-      "set_id": "Ustawiono talent na %s.",
-      "set_atk": "Ustawiono talent Atak Podstawowy na poziom %s.",
+      "usage_1": "Aby ustawi膰 poziom talentu, u偶yj: \"talent set <ID talentu> <warto艣膰>\".",
+      "usage_2": "Mo偶esz te偶 u偶y膰: \"talent <n lub e lub q> <warto艣膰>\"",
+      "usage_3": "Aby uzyska膰 ID talentu, u偶yj: \"talent getid\"",
+      "lower_16": "B艂臋dny poziom talentu. Poziom ten powinien by膰 mniejszy ni偶 16.",
+      "set_id": "Ustawiono poziom talentu na %s.",
+      "set_atk": "Ustawiono poziom talentu Atak Podstawowy na %s.",
       "set_e": "Ustawiono poziom talentu E na %s.",
       "set_q": "Ustawiono poziom talentu Q na %s.",
       "invalid_skill_id": "B艂臋dne ID umiej臋tno艣ci.",
-      "set_this": "Ustawiono ten talent na poziom %s.",
+      "set_this": "Ustawiono obecny talent na poziom %s.",
       "invalid_level": "B艂臋dny poziom talentu.",
       "normal_attack_id": "ID podstawowego ataku: %s.",
       "e_skill_id": "ID umiej臋tno艣ci E: %s.",
-      "q_skill_id": "ID umiej臋tno艣ci Q: %s."
+      "q_skill_id": "ID umiej臋tno艣ci Q: %s.",
+      "description": "Ustaw poziomu talentu obecnie wybranej postaci wybranego gracza."
+    },
+    "team": {
+      "usage": "team <add|remove|set> [ID awatara, ...] [indeks|pierwszy|ostatni|pierwszy_indeks-ostatni_indeks, ...]",
+      "invalid_usage": "Nieprawid艂owe u偶ycie komendy.",
+      "add_usage": "team add <ID awatara, ...> [indeks]",
+      "invalid_index": "B艂臋dny indeks.",
+      "add_too_much": "Mo偶na mie膰 maksymalnie %d postaci w zespole.",
+      "failed_to_add_avatar": "B艂膮d podczas dodawania awatara o ID \"%s\".",
+      "remove_usage": "team remove <indeks|pierwszy|ostatni|pierwszy_indeks-ostatni_indeks, ...>",
+      "failed_to_parse_index": "B艂膮d podczas przetwarzania indeksu \"%s\".",
+      "remove_too_much": "Nie mo偶esz usun膮膰 wszystkich awatar贸w w zespole.",
+      "ignore_index": "Ignorowanie indeksu/贸w %s.",
+      "set_usage": "team set <indeks> <ID awatara>",
+      "index_out_of_range": "Podany indeks nie mie艣ci si臋 w swoim zakresie.",
+      "failed_parse_avatar_id": "B艂臋dny ID awatara \"%s\".",
+      "avatar_already_in_team": "Podany awatar jest ju偶 w zespole wybranego gracza.",
+      "avatar_not_found": "Awatar o ID \"%d\" nie istnieje.",
+      "description": "Modyfikuj zesp贸艂 wybranego gracza."
     },
     "teleportAll": {
-      "success": "Przyzwano wszystkich graczy do Ciebie.",
-      "error": "Mo偶esz u偶y膰 tej komendy wy艂膮cznie w trybie MP."
+      "usage": "tpall",
+      "success": "Przyzwano wszystkich graczy do wybranego gracza.",
+      "error": "Mo偶esz u偶y膰 tej komendy wy艂膮cznie w trybie MP.",
+      "description": "Przyzwij wszystkich graczy do wybranego gracza."
     },
     "teleport": {
-      "usage_server": "U偶ycie: /tp @<ID gracza> <x> <y> <z> [ID sceny]",
-      "usage": "U偶ycie: /tp [@<ID gracza>] <x> <y> <z> [ID sceny]",
-      "specify_player_id": "Musisz okre艣li膰 ID gracza.",
-      "invalid_position": "B艂臋dna pozycja.",
-            "exists_error": "Ta scena nie istenieje.",
-      "success": "Przeteleportowano %s do %s, %s, %s w scenie %s"
+      "usage_server": "tp @<ID gracza> <x> <y> <z> [ID sceny]",
+      "usage": "tp [@<ID gracza>] <x> <y> <z> [ID sceny]",
+      "specify_player_id": "Musisz poda膰 ID gracza.",
+      "invalid_position": "B艂臋dna pozycja xyz.",
+      "exists_error": "Ta scena nie istenieje.",
+      "success": "Gracz %s zosta艂 przeniesiony do pozycji (%s, %s, %s) w scenie o ID %s.",
+      "description": "Przemie艣膰 wybranego gracza do podanej pozycji w podanej scenie."
     },
     "weather": {
-      "description": "Changes the weather.Weather IDs can be found in WeatherExcelConfigData.json.\nClimate types: sunny, cloudy, rain, thunderstorm, snow, mist.",
-      "usage": "Usage: weather [weatherId] [climateType]\nWeather IDs can be found in WeatherExcelConfigData.json.\nClimate types: sunny, cloudy, rain, thunderstorm, snow, mist.",
-      "success": "Set weather ID to %s with climate type %s.",
-      "status": "Current weather ID is %s with climate type %s."
-    },
-    "help": {
-      "usage": "U偶ycie: ",
-      "aliases": "Aliasy: ",
-      "available_commands": "Dost臋pne komendy: "
-    },
-    "unlocktower": {
-      "success": "odblokowa膰 gotowe",
-      "description": "Odblokuj g艂臋bok膮 spiral臋"
+      "usage": "weather [ID pogody] [typ klimatu]\n\tID pogody mo偶na znale藕膰 w pliku \"WeatherExcelConfigData.json\".\n\tMo偶liwe typy klimatu: sunny (s艂oneczny), cloudy (pochmurny), rain (deszcz), thunderstorm (burza), snow (艣nieg), mist (mg艂a)",
+      "success": "ID pogody zosta艂 ustawiony na %s, a typ klimatu na %s.",
+      "status": "Bie偶膮ce ID pogody to %s, a typ klimatu to %s.",
+      "description": "Zmie艅 ID pogody i typ klimatu."
     },
     "ban": {
-      "command_usage": "Usage: ban <@playerId> [timestamp] [reason]",
-      "success": "Successful.",
-      "failure": "Failed, player not found.",
-      "invalid_time": "Unable to parse timestamp.",
-      "description": "Ban a player"
+      "usage": "ban @<ID gracza> [na ile czasu] [pow贸d]",
+      "success": "Pomy艣lnie zbanowano podanego gracza.",
+      "failure": "Gracz o podanym ID nie istnieje.",
+      "invalid_time": "Nieprawid艂owy czas bana.",
+      "description": "Zbanuj podanego gracza."
     },
     "unban": {
-      "command_usage": "Usage: unban <@playerId>",
-      "success": "Successful.",
-      "failure": "Failed, player not found.",
-      "description": "Unban a player"
+      "usage": "unban @<ID gracza>",
+      "success": "Pomy艣lnie odbanowano podanego gracza.",
+      "failure": "Gracz o podanym ID nie istnieje.",
+      "description": "Odbanuj podanego gracza."
     }
   },
   "gacha": {
     "details": {
-      "title": "Banner Details",
-      "available_five_stars": "Available 5-star Items",
-      "available_four_stars": "Available 4-star Items",
-      "available_three_stars": "Available 3-star Items"
+      "title": "Szczeg贸艂y losowania",
+      "available_five_stars": "Dost臋pne 5-gwiazdkowe przedmioty",
+      "available_four_stars": "Dost臋pne 4-gwiazdkowe przedmioty",
+      "available_three_stars": "Dost臋pne 3-gwiazdkowe przedmioty"
     },
     "records": {
-      "title": "Gacha Records",
-      "date": "Date",
-      "item": "Item"
+      "title": "Rekordy gracza",
+      "date": "Data",
+      "item": "Przedmiot"
+    }
+  },
+  "documentation": {
+    "handbook": {
+      "title": "GM Handbook",
+      "title_commands": "Komendy",
+      "title_avatars": "Awatary",
+      "title_items": "Przedmioty",
+      "title_scenes": "Sceny",
+      "title_monsters": "Potwory",
+      "header_id": "ID",
+      "header_command": "Komenda",
+      "header_description": "Opis",
+      "header_avatar": "Awatar",
+      "header_item": "Przedmiot",
+      "header_scene": "Scena",
+      "header_monster": "Potw贸r"
+    },
+    "index": {
+      "title": "Dokumentacja",
+      "handbook": "GM Handbook",
+      "gacha_mapping": "Losowanie w formacie JSON"
     }
   }
-}
\ No newline at end of file
+}
-- 
GitLab