Add /lp translations command

This commit is contained in:
Luck 2020-10-15 23:02:11 +01:00
parent c5841b58a7
commit 6e7c49488d
No known key found for this signature in database
GPG Key ID: EFA9B3EC5FD90F8B
10 changed files with 357 additions and 12 deletions

View File

@ -60,6 +60,9 @@ luckperms {
migration { migration {
plugin brigadier:string single_word; plugin brigadier:string single_word;
} }
translations {
install;
}
creategroup { creategroup {
name brigadier:string single_word; name brigadier:string single_word;
} }

View File

@ -50,6 +50,7 @@ import me.lucko.luckperms.common.commands.misc.NetworkSyncCommand;
import me.lucko.luckperms.common.commands.misc.ReloadConfigCommand; import me.lucko.luckperms.common.commands.misc.ReloadConfigCommand;
import me.lucko.luckperms.common.commands.misc.SearchCommand; import me.lucko.luckperms.common.commands.misc.SearchCommand;
import me.lucko.luckperms.common.commands.misc.SyncCommand; import me.lucko.luckperms.common.commands.misc.SyncCommand;
import me.lucko.luckperms.common.commands.misc.TranslationsCommand;
import me.lucko.luckperms.common.commands.misc.TreeCommand; import me.lucko.luckperms.common.commands.misc.TreeCommand;
import me.lucko.luckperms.common.commands.misc.VerboseCommand; import me.lucko.luckperms.common.commands.misc.VerboseCommand;
import me.lucko.luckperms.common.commands.track.CreateTrack; import me.lucko.luckperms.common.commands.track.CreateTrack;
@ -114,6 +115,7 @@ public class CommandManager {
.add(new ReloadConfigCommand()) .add(new ReloadConfigCommand())
.add(new BulkUpdateCommand()) .add(new BulkUpdateCommand())
.add(new MigrationParentCommand()) .add(new MigrationParentCommand())
.add(new TranslationsCommand())
.add(new ApplyEditsCommand()) .add(new ApplyEditsCommand())
.add(new CreateGroup()) .add(new CreateGroup())
.add(new DeleteGroup()) .add(new DeleteGroup())

View File

@ -47,6 +47,7 @@ public enum CommandPermission {
BULK_UPDATE("bulkupdate", Type.NONE), BULK_UPDATE("bulkupdate", Type.NONE),
APPLY_EDITS("applyedits", Type.NONE), APPLY_EDITS("applyedits", Type.NONE),
MIGRATION("migration", Type.NONE), MIGRATION("migration", Type.NONE),
TRANSLATIONS("translations", Type.NONE),
CREATE_GROUP("creategroup", Type.NONE), CREATE_GROUP("creategroup", Type.NONE),
DELETE_GROUP("deletegroup", Type.NONE), DELETE_GROUP("deletegroup", Type.NONE),

View File

@ -85,6 +85,9 @@ public enum CommandSpec {
arg("constraint...", false) arg("constraint...", false)
), ),
MIGRATION("/%s migration"), MIGRATION("/%s migration"),
TRANSLATIONS("/%s translations",
arg("install", false)
),
APPLY_EDITS("/%s applyedits <code> [target]", APPLY_EDITS("/%s applyedits <code> [target]",
arg("code", true), arg("code", true),
arg("target", false) arg("target", false)

View File

@ -0,0 +1,189 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package me.lucko.luckperms.common.commands.misc;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import me.lucko.luckperms.common.command.CommandResult;
import me.lucko.luckperms.common.command.abstraction.SingleCommand;
import me.lucko.luckperms.common.command.access.CommandPermission;
import me.lucko.luckperms.common.command.spec.CommandSpec;
import me.lucko.luckperms.common.command.utils.ArgumentList;
import me.lucko.luckperms.common.http.UnsuccessfulRequestException;
import me.lucko.luckperms.common.locale.Message;
import me.lucko.luckperms.common.locale.TranslationManager;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.sender.Sender;
import me.lucko.luckperms.common.util.MoreFiles;
import me.lucko.luckperms.common.util.Predicates;
import me.lucko.luckperms.common.util.gson.GsonProvider;
import net.kyori.adventure.text.Component;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
public class TranslationsCommand extends SingleCommand {
private static final String TRANSLATIONS_INFO_ENDPOINT = "https://metadata.luckperms.net/data/translations";
private static final String TRANSLATIONS_DOWNLOAD_ENDPOINT = "https://metadata.luckperms.net/translation/";
public TranslationsCommand() {
super(CommandSpec.TRANSLATIONS, "Translations", CommandPermission.TRANSLATIONS, Predicates.notInRange(0, 1));
}
@Override
public CommandResult execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, String label) {
Message.TRANSLATIONS_SEARCHING.send(sender);
List<LanguageInfo> availableTranslations;
try {
availableTranslations = getAvailableTranslations(plugin);
} catch (IOException | UnsuccessfulRequestException e) {
Message.TRANSLATIONS_SEARCHING_ERROR.send(sender);
e.printStackTrace();
return CommandResult.FAILURE;
}
if (args.size() >= 1 && args.get(0).equalsIgnoreCase("install")) {
Message.TRANSLATIONS_DOWNLOADING.send(sender);
downloadTranslations(plugin, availableTranslations, sender);
plugin.getTranslationManager().reload();
Message.TRANSLATIONS_INSTALL_COMPLETE.send(sender);
return CommandResult.SUCCESS;
}
Message.INSTALLED_TRANSLATIONS.send(sender, plugin.getTranslationManager().getInstalledLocales().stream().map(Locale::toString).collect(Collectors.toList()));
Message.AVAILABLE_TRANSLATIONS_HEADER.send(sender);
for (LanguageInfo language : availableTranslations) {
Message.AVAILABLE_TRANSLATIONS_ENTRY.send(sender, language.locale.toString(), language.locale.getDisplayLanguage(language.locale), language.progress, language.contributors);
}
sender.sendMessage(Message.prefixed(Component.empty()));
Message.TRANSLATIONS_DOWNLOAD_PROMPT.send(sender, label);
return CommandResult.SUCCESS;
}
private static void downloadTranslations(LuckPermsPlugin plugin, List<LanguageInfo> languages, Sender sender) {
try {
MoreFiles.createDirectoriesIfNotExists(plugin.getTranslationManager().getTranslationsDirectory());
} catch (IOException e) {
// ignore
}
for (LanguageInfo language : languages) {
Message.TRANSLATIONS_INSTALLING.send(sender, language.locale.toString());
Request request = new Request.Builder()
.header("User-Agent", plugin.getBytebin().getUserAgent())
.url(TRANSLATIONS_DOWNLOAD_ENDPOINT + language.id)
.build();
Path file = plugin.getTranslationManager().getTranslationsDirectory().resolve(language.locale.toString() + ".properties");
try (Response response = plugin.getBytebin().makeHttpRequest(request)) {
try (ResponseBody responseBody = response.body()) {
if (responseBody == null) {
throw new RuntimeException("No response");
}
try (InputStream inputStream = responseBody.byteStream()) {
Files.copy(inputStream, file, StandardCopyOption.REPLACE_EXISTING);
}
}
} catch (UnsuccessfulRequestException | IOException e) {
Message.TRANSLATIONS_DOWNLOAD_ERROR.send(sender, language.locale.toString());
e.printStackTrace();
}
}
}
public static List<LanguageInfo> getAvailableTranslations(LuckPermsPlugin plugin) throws IOException, UnsuccessfulRequestException {
Request request = new Request.Builder()
.header("User-Agent", plugin.getBytebin().getUserAgent())
.url(TRANSLATIONS_INFO_ENDPOINT)
.build();
JsonObject jsonResponse;
try (Response response = plugin.getBytebin().makeHttpRequest(request)) {
try (ResponseBody responseBody = response.body()) {
if (responseBody == null) {
throw new RuntimeException("No response");
}
try (InputStream inputStream = responseBody.byteStream()) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
jsonResponse = GsonProvider.normal().fromJson(reader, JsonObject.class);
}
}
}
}
List<LanguageInfo> languages = new ArrayList<>();
for (Map.Entry<String, JsonElement> language : jsonResponse.get("languages").getAsJsonObject().entrySet()) {
languages.add(new LanguageInfo(language.getKey(), language.getValue().getAsJsonObject()));
}
languages.removeIf(language -> language.progress <= 0);
return languages;
}
private static final class LanguageInfo {
private final String id;
private final String name;
private final Locale locale;
private final int progress;
private final List<String> contributors;
LanguageInfo(String id, JsonObject data) {
this.id = id;
this.name = data.get("name").getAsString();
this.locale = Objects.requireNonNull(TranslationManager.parseLocale(data.get("localeTag").getAsString(), null));
this.progress = data.get("progress").getAsInt();
this.contributors = new ArrayList<>();
for (JsonElement contributor : data.get("contributors").getAsJsonArray()) {
this.contributors.add(contributor.getAsJsonObject().get("name").getAsString());
}
}
}
}

View File

@ -2570,6 +2570,104 @@ public interface Message {
.append(FULL_STOP) .append(FULL_STOP)
); );
Args0 TRANSLATIONS_SEARCHING = () -> prefixed(translatable()
// "&7Searching for available translations, please wait..."
.key("luckperms.command.translations.searching")
.color(GRAY)
);
Args0 TRANSLATIONS_SEARCHING_ERROR = () -> prefixed(text()
// "&cUnable to obtain a list of available translations. Check the console for errors."
.color(RED)
.append(translatable("luckperms.command.translations.searching-error"))
.append(FULL_STOP)
.append(space())
.append(translatable("luckperms.command.misc.check-console-for-errors"))
.append(FULL_STOP)
);
Args1<Collection<String>> INSTALLED_TRANSLATIONS = locales -> prefixed(translatable()
// "&aInstalled Translations:"
.key("luckperms.command.translations.installed-translations")
.color(GREEN)
.append(text(':'))
.append(space())
.append(formatStringList(locales))
);
Args0 AVAILABLE_TRANSLATIONS_HEADER = () -> prefixed(translatable()
// "&aAvailable Translations:"
.key("luckperms.command.translations.available-translations")
.color(GREEN)
.append(text(':'))
);
Args4<String, String, Integer, List<String>> AVAILABLE_TRANSLATIONS_ENTRY = (tag, name, percentComplete, contributors) -> prefixed(text()
// - {} ({}) - {}% translated - by {}
.color(GRAY)
.append(text('-'))
.append(space())
.append(text(tag, AQUA))
.append(space())
.append(OPEN_BRACKET)
.append(text(name, WHITE))
.append(CLOSE_BRACKET)
.append(text(" - "))
.append(translatable("luckperms.command.translations.percent-translated", text(percentComplete, GREEN)))
.apply(builder -> {
if (!contributors.isEmpty()) {
builder.append(text(" - "));
builder.append(translatable("luckperms.command.translations.translations-by"));
builder.append(space());
builder.append(formatStringList(contributors));
}
})
);
Args1<String> TRANSLATIONS_DOWNLOAD_PROMPT = label -> join(newline(),
// "Use /lp translations install to download and install up-to-date versions of these translations provided by the community."
// "Please note that this will override any changes you've made for these languages."
prefixed(translatable()
.key("luckperms.command.translations.download-prompt")
.color(AQUA)
.args(text("/" + label + " translations install", GREEN))
.append(FULL_STOP)),
prefixed(translatable()
.key("luckperms.command.translations.download-override-warning")
.color(GRAY)
.append(FULL_STOP))
);
Args0 TRANSLATIONS_DOWNLOADING = () -> prefixed(translatable()
// "&bDownloading translations, please wait..."
.key("luckperms.command.translations.downloading")
.color(AQUA)
);
Args1<String> TRANSLATIONS_INSTALLING = name -> prefixed(translatable()
// "&aInstalling language {}..."
.key("luckperms.command.translations.installing")
.color(GREEN)
.args(text((name)))
);
Args0 TRANSLATIONS_INSTALL_COMPLETE = () -> prefixed(translatable()
// "&bInstallation complete."
.key("luckperms.command.translations.install-complete")
.color(AQUA)
.append(FULL_STOP)
);
Args1<String> TRANSLATIONS_DOWNLOAD_ERROR = name -> prefixed(text()
// "&cUnable download translation for {}. Check the console for errors."
.color(RED)
.append(translatable("luckperms.command.translations.download-error", text(name, DARK_RED)))
.append(FULL_STOP)
.append(space())
.append(translatable("luckperms.command.misc.check-console-for-errors"))
.append(FULL_STOP)
);
Args4<String, String, Boolean, Boolean> USER_INFO_GENERAL = (username, uuid, mojang, online) -> join(newline(), Args4<String, String, Boolean, Boolean> USER_INFO_GENERAL = (username, uuid, mojang, online) -> join(newline(),
// "&b&l> &bUser Info: &f{}" // "&b&l> &bUser Info: &f{}"
// "&f- &3UUID: &f{}" // "&f- &3UUID: &f{}"

View File

@ -40,6 +40,8 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -48,23 +50,40 @@ public class TranslationManager {
public static final Locale DEFAULT_LOCALE = Locale.ENGLISH; public static final Locale DEFAULT_LOCALE = Locale.ENGLISH;
private final LuckPermsPlugin plugin; private final LuckPermsPlugin plugin;
private final TranslationRegistry registry; private final Path translationsDirectory;
private final Set<Locale> installed = ConcurrentHashMap.newKeySet();
private TranslationRegistry registry;
public TranslationManager(LuckPermsPlugin plugin) { public TranslationManager(LuckPermsPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
this.translationsDirectory = this.plugin.getBootstrap().getConfigDirectory().resolve("translations");
}
// create a translation registry for luckperms public Path getTranslationsDirectory() {
return this.translationsDirectory;
}
public Set<Locale> getInstalledLocales() {
return Collections.unmodifiableSet(this.installed);
}
public void reload() {
// remove any previous registry
if (this.registry != null) {
GlobalTranslator.get().removeSource(this.registry);
this.installed.clear();
}
// create a translation registry
this.registry = TranslationRegistry.create(Key.key("luckperms", "main")); this.registry = TranslationRegistry.create(Key.key("luckperms", "main"));
this.registry.defaultLocale(DEFAULT_LOCALE); this.registry.defaultLocale(DEFAULT_LOCALE);
// register it to the global source, so our translations can be picked up by adventure-platform
GlobalTranslator.get().addSource(this.registry);
}
public void load() {
// load custom translations first, then the base (built-in) translations after. // load custom translations first, then the base (built-in) translations after.
loadCustom(); loadCustom();
loadBase(); loadBase();
// register it to the global source, so our translations can be picked up by adventure-platform
GlobalTranslator.get().addSource(this.registry);
} }
/** /**
@ -80,7 +99,7 @@ public class TranslationManager {
*/ */
public void loadCustom() { public void loadCustom() {
List<Path> translationFiles; List<Path> translationFiles;
try (Stream<Path> stream = Files.list(this.plugin.getBootstrap().getConfigDirectory().resolve("translations"))) { try (Stream<Path> stream = Files.list(this.translationsDirectory)) {
translationFiles = stream.filter(path -> path.getFileName().toString().endsWith(".properties")).collect(Collectors.toList()); translationFiles = stream.filter(path -> path.getFileName().toString().endsWith(".properties")).collect(Collectors.toList());
} catch (IOException e) { } catch (IOException e) {
translationFiles = Collections.emptyList(); translationFiles = Collections.emptyList();
@ -106,7 +125,8 @@ public class TranslationManager {
} }
this.registry.registerAll(locale, translationFile, true); this.registry.registerAll(locale, translationFile, true);
this.plugin.getLogger().info("Registered additional translations for " + locale.toLanguageTag()); this.plugin.getLogger().info("Registered additional translations for " + locale.toString());
this.installed.add(locale);
} }
public static Locale parseLocale(String locale, Locale defaultLocale) { public static Locale parseLocale(String locale, Locale defaultLocale) {

View File

@ -78,6 +78,7 @@ public abstract class AbstractLuckPermsPlugin implements LuckPermsPlugin {
private PermissionRegistry permissionRegistry; private PermissionRegistry permissionRegistry;
private LogDispatcher logDispatcher; private LogDispatcher logDispatcher;
private LuckPermsConfiguration configuration; private LuckPermsConfiguration configuration;
private OkHttpClient httpClient;
private BytebinClient bytebin; private BytebinClient bytebin;
private FileWatcher fileWatcher = null; private FileWatcher fileWatcher = null;
private Storage storage; private Storage storage;
@ -98,7 +99,7 @@ public abstract class AbstractLuckPermsPlugin implements LuckPermsPlugin {
this.dependencyManager.loadDependencies(getGlobalDependencies()); this.dependencyManager.loadDependencies(getGlobalDependencies());
this.translationManager = new TranslationManager(this); this.translationManager = new TranslationManager(this);
this.translationManager.load(); this.translationManager.reload();
} }
public final void enable() { public final void enable() {
@ -118,11 +119,11 @@ public abstract class AbstractLuckPermsPlugin implements LuckPermsPlugin {
this.configuration = new LuckPermsConfiguration(this, provideConfigurationAdapter()); this.configuration = new LuckPermsConfiguration(this, provideConfigurationAdapter());
// setup a bytebin instance // setup a bytebin instance
OkHttpClient httpClient = new OkHttpClient.Builder() this.httpClient = new OkHttpClient.Builder()
.callTimeout(15, TimeUnit.SECONDS) .callTimeout(15, TimeUnit.SECONDS)
.build(); .build();
this.bytebin = new BytebinClient(httpClient, getConfiguration().get(ConfigKeys.BYTEBIN_URL), "luckperms"); this.bytebin = new BytebinClient(this.httpClient, getConfiguration().get(ConfigKeys.BYTEBIN_URL), "luckperms");
// now the configuration is loaded, we can create a storage factory and load initial dependencies // now the configuration is loaded, we can create a storage factory and load initial dependencies
StorageFactory storageFactory = new StorageFactory(this); StorageFactory storageFactory = new StorageFactory(this);
@ -316,6 +317,11 @@ public abstract class AbstractLuckPermsPlugin implements LuckPermsPlugin {
return this.configuration; return this.configuration;
} }
@Override
public OkHttpClient getHttpClient() {
return this.httpClient;
}
@Override @Override
public BytebinClient getBytebin() { public BytebinClient getBytebin() {
return this.bytebin; return this.bytebin;

View File

@ -57,6 +57,8 @@ import me.lucko.luckperms.common.verbose.VerboseHandler;
import net.luckperms.api.query.QueryOptions; import net.luckperms.api.query.QueryOptions;
import okhttp3.OkHttpClient;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -232,6 +234,13 @@ public interface LuckPermsPlugin {
*/ */
Optional<FileWatcher> getFileWatcher(); Optional<FileWatcher> getFileWatcher();
/**
* Gets the http client used by the plugin.
*
* @return the http client
*/
OkHttpClient getHttpClient();
/** /**
* Gets the bytebin instance in use by platform. * Gets the bytebin instance in use by platform.
* *

View File

@ -302,6 +302,18 @@ luckperms.command.update-task.push.error=Error whilst pushing changes to other s
luckperms.command.update-task.push.error-not-setup=Cannot push changes to other servers as a messaging service has not been configured luckperms.command.update-task.push.error-not-setup=Cannot push changes to other servers as a messaging service has not been configured
luckperms.command.reload-config.success=The configuration file was reloaded luckperms.command.reload-config.success=The configuration file was reloaded
luckperms.command.reload-config.restart-note=some options will only apply after the server has restarted luckperms.command.reload-config.restart-note=some options will only apply after the server has restarted
luckperms.command.translations.searching=Searching for available translations, please wait...
luckperms.command.translations.searching-error=Unable to obtain a list of available translations
luckperms.command.translations.installed-translations=Installed Translations
luckperms.command.translations.available-translations=Available Translations
luckperms.command.translations.percent-translated={0}% translated
luckperms.command.translations.translations-by=by
luckperms.command.translations.downloading=Downloading translations, please wait...
luckperms.command.translations.download-error=Unable download translation for {0}
luckperms.command.translations.installing=Installing language {0}...
luckperms.command.translations.install-complete=Installation complete
luckperms.command.translations.download-prompt=Use {0} to download and install up-to-date versions of these translations provided by the community
luckperms.command.translations.download-override-warning=Please note that this will override any changes you''ve made for these languages
luckperms.usage.user.description=A set of commands for managing users within LuckPerms. (A ''user'' in LuckPerms is just a player, and can refer to a UUID or username) luckperms.usage.user.description=A set of commands for managing users within LuckPerms. (A ''user'' in LuckPerms is just a player, and can refer to a UUID or username)
luckperms.usage.group.description=A set of commands for managing groups within LuckPerms. Groups are just collections of permission assignments that can be given to users. New groups are made using the ''creategroup'' command. luckperms.usage.group.description=A set of commands for managing groups within LuckPerms. Groups are just collections of permission assignments that can be given to users. New groups are made using the ''creategroup'' command.
luckperms.usage.track.description=A set of commands for managing tracks within LuckPerms. Tracks are a ordered collection of groups which can be used for defining promotions and demotions. luckperms.usage.track.description=A set of commands for managing tracks within LuckPerms. Tracks are a ordered collection of groups which can be used for defining promotions and demotions.
@ -340,6 +352,8 @@ luckperms.usage.bulk-update.argument.action-field=the field to act upon. only re
luckperms.usage.bulk-update.argument.action-value=the value to replace with. only required for ''update''. luckperms.usage.bulk-update.argument.action-value=the value to replace with. only required for ''update''.
luckperms.usage.bulk-update.argument.constraint=the constraints required for the update luckperms.usage.bulk-update.argument.constraint=the constraints required for the update
luckperms.usage.migration.description=Migration commands luckperms.usage.migration.description=Migration commands
luckperms.usage.translations.description=Manage translations
luckperms.usage.translations.argument.install=subcommand to install translations
luckperms.usage.apply-edits.description=Applies permission changes made from the web editor luckperms.usage.apply-edits.description=Applies permission changes made from the web editor
luckperms.usage.apply-edits.argument.code=the unique code for the data luckperms.usage.apply-edits.argument.code=the unique code for the data
luckperms.usage.apply-edits.argument.target=who to apply the data to luckperms.usage.apply-edits.argument.target=who to apply the data to