<commit_before>package com.tisawesomeness.minecord.lang;

import com.tisawesomeness.minecord.util.URLs;

import lombok.Getter;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.Permission;

import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.text.Collator;
import java.text.MessageFormat;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.util.*;

/**
 * An enum of all the registered bot languages.
 */
@Slf4j
public enum Lang {
    EN_US("en_US", new Locale("en", "US")),
    DE_DE("de_DE", new Locale("de", "DE")),
    PT_BR("pt_BR", new Locale("pt", "BR"));

    // Setting a variable is atomic, reloading lang is rare and
    // changes do not need to be seen immediately across all threads
    private @NonNull ResourceBundle resource;
    private final @NonNull Collator collator2;
    private final @NonNull Collator collator3;

    @Getter private final @NonNull String code;
    @Getter private final @NonNull Locale locale;
    @Getter private final @NonNull Lang.Features features;

    /**
     * The flag emote ({@code :flag_xx:}) for the country this lang is based in.
     */
    @Getter private final @NonNull String flagEmote;

    Lang(@NonNull String code, @NonNull Locale locale) {
        this.code = code;
        this.locale = locale;
        resource = ResourceBundle.getBundle("lang/lang", locale);
        features = new Features();
        collator2 = newCollator(Collator.SECONDARY);
        collator3 = newCollator(Collator.TERTIARY);
        flagEmote = i18n("lang.flagEmote");
    }
    private Collator newCollator(int strength) {
        Collator collator = Collator.getInstance(locale);
        collator.setStrength(strength);
        return collator;
    }


    /**
     * Gets the default lang for the bot without configuration.
     */
    public static Lang getDefault() {
        return EN_US;
    }

    /**
     * Gets the lang associated with a language code
     * @param code The case-insensitive language code, formatted like {@code aa_BB} where
     *             {@code aa} is the language and {@code bb} is the country
     * @return The lang if found, otherwise empty
     */
    public static Optional<Lang> from(@NonNull String code) {
        return Arrays.stream(values())
                .filter(l -> l.code.equalsIgnoreCase(code))
                .findFirst();
    }

    /**
     * Updates the loaded translations by reading from an external file.
     * If the provided directory does not exist, the files will not load.
     * @param path The path to the lang folder containing {@code lang.properties}
     */
    public static void reloadFromFile(@NonNull Path path) {
        if (!path.toFile().isDirectory()) {
            log.warn(String.format("There is no directory at \"%s\", aborting...", path));
            return;
        }
        URL[] urls = {URLs.createUrl(path.toUri())};
        ResourceBundle.clearCache();
        for (Lang lang : values()) {
            lang.setResource(path, urls);
        }
    }
    private void setResource(Path path, URL[] urls) {
        String fileName = Lang.getDefault() == this ? "lang.properties" : String.format("lang_%s.properties", code);
        if (!path.resolve(fileName).toFile().isFile()) {
            log.warn(String.format("File \"%s\" does not exist in the lang folder, " +
                    "using default translations instead...", fileName));
            return;
        }
        try (URLClassLoader loader = new URLClassLoader(urls)) {
            resource = ResourceBundle.getBundle("lang", locale, loader);
        } catch (IOException ex) {
            log.error("There was an error closing the file URL", ex);
        }
    }

    /**
     * Updates the loaded translations by reading from resources.
     */
    public static void reloadFromResources() {
        ResourceBundle.clearCache();
        for (Lang lang : values()) {
            lang.resource = ResourceBundle.getBundle("lang/lang", lang.locale);
        }
    }


    /**
     * Gets the localization string for this lang.
     * <br>If not found, {@link #getDefault()} is used instead.
     * <br>Keys are in the format {@code category.optionalSubCategory.name}, where categories often follow package names.
     * <br>Lang config keys are in the {@code lang} category.
     * @param key The <b>case-sensitive</b> localization key. For example, {@code command.server.embedTitle}.
     * @return The localized string
     * @throws java.util.MissingResourceException If the given key could not be found
     */
    public @NonNull String i18n(@NonNull String key) {
        return resource.getString(key);
    }
    /**
     * Gets a localized string for this lang if the key exists.
     * @param key The <b>case-sensitive</b> localization key used in {@link #i18n(String)}
     * @return The localized string, or empty if not found
     */
    public Optional<String> i18nOpt(@NonNull String key) {
        if (resource.keySet().contains(key)) {
            return Optional.of(i18n(key));
        }
        return Optional.empty();
    }
    /**
     * Gets a formatted, localized string for this lang.
     * @param key The <b>case-sensitive</b> localization key used in {@link #i18n(String)}
     * @param args An ordered list of arguments to place into the string
     * @return The localized string
     * @throws java.util.MissingResourceException If the given key could not be found
     * @see MessageFormat
     * @see Locale
     */
    public @NonNull String i18nf(@NonNull String key, Object... args) {
        return new MessageFormat(i18n(key), locale).format(args);
    }
    /**
     * Gets a builder for a formatted, localized markdown string for this lang.
     * @param key The <b>case-sensitive</b> localization key used in {@link #i18n(String)}
     * @param args An ordered list of arguments to place into the string
     * @return A new localized markdown builder
     * @throws java.util.MissingResourceException If the given key could not be found
     * @see LocalizedMarkdownBuilder
     * @see MessageFormat
     * @see Locale
     */
    public @NonNull LocalizedMarkdownBuilder i18nm(@NonNull String key, Object... args) {
        return new LocalizedMarkdownBuilder(new MessageFormat(i18n(key), locale), args);
    }
    /**
     * Gets a formatted, localized string for this lang if the key exists.
     * @param key The <b>case-sensitive</b> localization key used in {@link #i18n(String)}
     * @param args An ordered list of arguments to place into the string
     * @return The localized string, or empty if not found
     * @see MessageFormat
     * @see Locale
     */
    public Optional<String> i18nfOpt(@NonNull String key, Object... args) {
        if (resource.keySet().contains(key)) {
            return Optional.of(i18nf(key, args));
        }
        return Optional.empty();
    }
    /**
     * Takes a localized string and splits it by comma, ignoring empty strings.
     * @param key The <b>case-sensitive</b> localization key used in {@link #i18n(String)}
     * @return A possibly-empty, immutable list of strings
     * @see MessageFormat
     * @see Locale
     */
    public List<String> i18nList(@NonNull String key) {
        if (!resource.keySet().contains(key)) {
            return Collections.emptyList();
        }
        String str = i18n(key);
        if (str.isEmpty()) {
            return Collections.emptyList();
        }
        return Arrays.asList(str.split(","));
    }


    /**
     * Localizes an object as a user-readable string.
     * @param obj The localizable object
     * @return The object's description in this language
     * @throws java.util.MissingResourceException If the localization key could not be found
     */<commit_msg>Make IntelliJ recognize lang property keys<commit_after>package com.tisawesomeness.minecord.lang;

import com.tisawesomeness.minecord.util.URLs;

import lombok.Getter;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.Permission;
import org.jetbrains.annotations.PropertyKey;

import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.text.Collator;
import java.text.MessageFormat;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.util.*;

/**
 * An enum of all the registered bot languages.
 */
@Slf4j
public enum Lang {
    EN_US("en_US", new Locale("en", "US")),
    DE_DE("de_DE", new Locale("de", "DE")),
    PT_BR("pt_BR", new Locale("pt", "BR"));

    private static final String RB = "lang.lang";

    // Setting a variable is atomic, reloading lang is rare and
    // changes do not need to be seen immediately across all threads
    private @NonNull ResourceBundle resource;
    private final @NonNull Collator collator2;
    private final @NonNull Collator collator3;

    @Getter private final @NonNull String code;
    @Getter private final @NonNull Locale locale;
    @Getter private final @NonNull Lang.Features features;

    /**
     * The flag emote ({@code :flag_xx:}) for the country this lang is based in.
     */
    @Getter private final @NonNull String flagEmote;

    Lang(@NonNull String code, @NonNull Locale locale) {
        this.code = code;
        this.locale = locale;
        resource = ResourceBundle.getBundle("lang/lang", locale);
        features = new Features();
        collator2 = newCollator(Collator.SECONDARY);
        collator3 = newCollator(Collator.TERTIARY);
        flagEmote = i18n("lang.flagEmote");
    }
    private Collator newCollator(int strength) {
        Collator collator = Collator.getInstance(locale);
        collator.setStrength(strength);
        return collator;
    }


    /**
     * Gets the default lang for the bot without configuration.
     */
    public static Lang getDefault() {
        return EN_US;
    }

    /**
     * Gets the lang associated with a language code
     * @param code The case-insensitive language code, formatted like {@code aa_BB} where
     *             {@code aa} is the language and {@code bb} is the country
     * @return The lang if found, otherwise empty
     */
    public static Optional<Lang> from(@NonNull String code) {
        return Arrays.stream(values())
                .filter(l -> l.code.equalsIgnoreCase(code))
                .findFirst();
    }

    /**
     * Updates the loaded translations by reading from an external file.
     * If the provided directory does not exist, the files will not load.
     * @param path The path to the lang folder containing {@code lang.properties}
     */
    public static void reloadFromFile(@NonNull Path path) {
        if (!path.toFile().isDirectory()) {
            log.warn(String.format("There is no directory at \"%s\", aborting...", path));
            return;
        }
        URL[] urls = {URLs.createUrl(path.toUri())};
        ResourceBundle.clearCache();
        for (Lang lang : values()) {
            lang.setResource(path, urls);
        }
    }
    private void setResource(Path path, URL[] urls) {
        String fileName = Lang.getDefault() == this ? "lang.properties" : String.format("lang_%s.properties", code);
        if (!path.resolve(fileName).toFile().isFile()) {
            log.warn(String.format("File \"%s\" does not exist in the lang folder, " +
                    "using default translations instead...", fileName));
            return;
        }
        try (URLClassLoader loader = new URLClassLoader(urls)) {
            resource = ResourceBundle.getBundle("lang", locale, loader);
        } catch (IOException ex) {
            log.error("There was an error closing the file URL", ex);
        }
    }

    /**
     * Updates the loaded translations by reading from resources.
     */
    public static void reloadFromResources() {
        ResourceBundle.clearCache();
        for (Lang lang : values()) {
            lang.resource = ResourceBundle.getBundle("lang/lang", lang.locale);
        }
    }


    /**
     * Gets the localization string for this lang.
     * <br>If not found, {@link #getDefault()} is used instead.
     * <br>Keys are in the format {@code category.optionalSubCategory.name}, where categories often follow package names.
     * <br>Lang config keys are in the {@code lang} category.
     * @param key The <b>case-sensitive</b> localization key. For example, {@code command.server.embedTitle}.
     * @return The localized string
     * @throws java.util.MissingResourceException If the given key could not be found
     */
    public @NonNull String i18n(@NonNull @PropertyKey(resourceBundle = RB) String key) {
        return resource.getString(key);
    }
    /**
     * Gets a localized string for this lang if the key exists.
     * @param key The <b>case-sensitive</b> localization key used in {@link #i18n(String)}
     * @return The localized string, or empty if not found
     */
    public Optional<String> i18nOpt(@NonNull @PropertyKey(resourceBundle = RB) String key) {
        if (resource.keySet().contains(key)) {
            return Optional.of(i18n(key));
        }
        return Optional.empty();
    }
    /**
     * Gets a formatted, localized string for this lang.
     * @param key The <b>case-sensitive</b> localization key used in {@link #i18n(String)}
     * @param args An ordered list of arguments to place into the string
     * @return The localized string
     * @throws java.util.MissingResourceException If the given key could not be found
     * @see MessageFormat
     * @see Locale
     */
    public @NonNull String i18nf(@NonNull @PropertyKey(resourceBundle = RB) String key, Object... args) {
        return new MessageFormat(i18n(key), locale).format(args);
    }
    /**
     * Gets a builder for a formatted, localized markdown string for this lang.
     * @param key The <b>case-sensitive</b> localization key used in {@link #i18n(String)}
     * @param args An ordered list of arguments to place into the string
     * @return A new localized markdown builder
     * @throws java.util.MissingResourceException If the given key could not be found
     * @see LocalizedMarkdownBuilder
     * @see MessageFormat
     * @see Locale
     */
    public @NonNull LocalizedMarkdownBuilder i18nm(@NonNull @PropertyKey(resourceBundle = RB) String key,
                                                   Object... args) {
        return new LocalizedMarkdownBuilder(new MessageFormat(i18n(key), locale), args);
    }
    /**
     * Gets a formatted, localized string for this lang if the key exists.
     * @param key The <b>case-sensitive</b> localization key used in {@link #i18n(String)}
     * @param args An ordered list of arguments to place into the string
     * @return The localized string, or empty if not found
     * @see MessageFormat
     * @see Locale
     */
    public Optional<String> i18nfOpt(@NonNull @PropertyKey(resourceBundle = RB) String key, Object... args) {
        if (resource.keySet().contains(key)) {
            return Optional.of(i18nf(key, args));
        }
        return Optional.empty();
    }
    /**
     * Takes a localized string and splits it by comma, ignoring empty strings.
     * @param key The <b>case-sensitive</b> localization key used in {@link #i18n(String)}
     * @return A possibly-empty, immutable list of strings
     * @see MessageFormat
     * @see Locale
     */
    public List<String> i18nList(@NonNull @PropertyKey(resourceBundle = RB) String key) {
        if (!resource.keySet().contains(key)) {
            return Collections.emptyList();
        }
        String str = i18n(key);
        if (str.isEmpty()) {
            return Collections.emptyList();
        }
        return Arrays.asList(str.split(","));
    }


    /**
     * Localizes an object as a user-readable string.
     * @param obj The localizable object
     * @return The object's description in this language
     * @throws java.util.MissingResourceException If the localization key could not be found
     */
{'fraction_non_alphanumeric': 0.1070962935369715, 'fraction_numerical': 0.005215123859191656, 'mean_word_length': 2.678465403060059, 'pattern_counts': {'":': 0, '<': 41, '<?xml version=': 0, '>': 43, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2365938', 'n_tokens_mistral': 4535, 'n_tokens_neox': 4441, 'n_words': 1495}