mirror of
https://github.com/Melledy/Grasscutter.git
synced 2024-11-22 06:31:26 +00:00
Language linting (#1382)
This commit is contained in:
parent
bb84432b4c
commit
8843276c41
27
.github/workflows/language_lint.yml
vendored
Normal file
27
.github/workflows/language_lint.yml
vendored
Normal file
@ -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
|
321
manage_languages.py
Normal file
321
manage_languages.py
Normal file
@ -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)
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
StringBuilder builder = new StringBuilder(translate(player, "commands.help.available_commands"));
|
||||
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));
|
||||
}
|
||||
|
||||
this.createCommand(builder, player, annotation);
|
||||
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());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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" -> {
|
||||
|
@ -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.",
|
||||
"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": "Przeteleportowano %s do %s, %s, %s w scenie %s"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user