Skip to content

Commit

Permalink
Move some translations to FML (#128)
Browse files Browse the repository at this point in the history
  • Loading branch information
Matyrobbrt authored May 5, 2024
1 parent a051ab9 commit 454a412
Show file tree
Hide file tree
Showing 9 changed files with 320 additions and 22 deletions.
12 changes: 12 additions & 0 deletions crowdin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
project_id_env: NEOFORGE_CROWDIN_PROJECT_ID
api_token_env: NEOFORGE_CROWDIN_API_TOKEN
base_path: .
base_url: 'https://neoforged.api.crowdin.com'
preserve_hierarchy: true
commit_message: '[ci skip]'
pull_request_labels:
- l10n
files:
- source: /loader/src/main/resources/lang/en_us.json
translation: /loader/src/main/resources/lang/%locale_with_underscore%.%file_extension%
dest: /fml_messages.%file_extension%
8 changes: 0 additions & 8 deletions loader/src/main/java/net/neoforged/fml/Bindings.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,6 @@ public static IEventBus getGameBus() {
return provider.getGameBus();
}

public static String parseMessage(String i18nMessage, Object... args) {
return provider.parseMessage(i18nMessage, args);
}

public static String stripControlCodes(String toStrip) {
return provider.stripControlCodes(toStrip);
}

public static IConfigEvent.ConfigConfig getConfigConfiguration() {
return provider.getConfigConfiguration();
}
Expand Down
4 changes: 0 additions & 4 deletions loader/src/main/java/net/neoforged/fml/IBindingsProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,5 @@
public interface IBindingsProvider {
IEventBus getGameBus();

String parseMessage(String i18nMessage, Object... args);

String stripControlCodes(String toStrip);

IConfigEvent.ConfigConfig getConfigConfiguration();
}
13 changes: 4 additions & 9 deletions loader/src/main/java/net/neoforged/fml/ModLoadingIssue.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;
import net.neoforged.fml.i18n.FMLTranslations;
import net.neoforged.neoforgespi.language.IModInfo;
import net.neoforged.neoforgespi.locating.IModFile;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -63,18 +64,12 @@ public String getTranslatedMessage() {
formattingArgs = Streams.concat(Stream.of(affectedMod, null), translationArgs.stream()).toArray();
}

return Bindings.parseMessage(translationKey, formattingArgs);
return FMLTranslations.parseEnglishMessage(translationKey, formattingArgs);
}

@Override
public String toString() {
var result = new StringBuilder(severity + ": " + translationKey);

for (var arg : translationArgs) {
result.append(", ");
result.append(arg);
}

return result.toString();
return severity + ": " + getTranslatedMessage();
}
public enum Severity {
WARNING,
Expand Down
134 changes: 134 additions & 0 deletions loader/src/main/java/net/neoforged/fml/i18n/FMLTranslations.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright (c) NeoForged and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

package net.neoforged.fml.i18n;

import com.google.common.base.CharMatcher;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParsePosition;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import net.neoforged.fml.Logging;
import net.neoforged.fml.loading.StringUtils;
import net.neoforged.neoforgespi.language.IModInfo;
import org.apache.commons.lang3.text.ExtendedMessageFormat;
import org.apache.commons.lang3.text.FormatFactory;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.ApiStatus;

@ApiStatus.Internal
public class FMLTranslations {
private static final Logger LOGGER = LogManager.getLogger();
private static final String ALLOWED_CHARS = "\u00c0\u00c1\u00c2\u00c8\u00ca\u00cb\u00cd\u00d3\u00d4\u00d5\u00da\u00df\u00e3\u00f5\u011f\u0130\u0131\u0152\u0153\u015e\u015f\u0174\u0175\u017e\u0207\u0000\u0000\u0000\u0000\u0000\u0000\u0000 !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u0000\u00c7\u00fc\u00e9\u00e2\u00e4\u00e0\u00e5\u00e7\u00ea\u00eb\u00e8\u00ef\u00ee\u00ec\u00c4\u00c5\u00c9\u00e6\u00c6\u00f4\u00f6\u00f2\u00fb\u00f9\u00ff\u00d6\u00dc\u00f8\u00a3\u00d8\u00d7\u0192\u00e1\u00ed\u00f3\u00fa\u00f1\u00d1\u00aa\u00ba\u00bf\u00ae\u00ac\u00bd\u00bc\u00a1\u00ab\u00bb\u2591\u2592\u2593\u2502\u2524\u2561\u2562\u2556\u2555\u2563\u2551\u2557\u255d\u255c\u255b\u2510\u2514\u2534\u252c\u251c\u2500\u253c\u255e\u255f\u255a\u2554\u2569\u2566\u2560\u2550\u256c\u2567\u2568\u2564\u2565\u2559\u2558\u2552\u2553\u256b\u256a\u2518\u250c\u2588\u2584\u258c\u2590\u2580\u03b1\u03b2\u0393\u03c0\u03a3\u03c3\u03bc\u03c4\u03a6\u0398\u03a9\u03b4\u221e\u2205\u2208\u2229\u2261\u00b1\u2265\u2264\u2320\u2321\u00f7\u2248\u00b0\u2219\u00b7\u221a\u207f\u00b2\u25a0\u0000";
private static final CharMatcher DISALLOWED_CHAR_MATCHER = CharMatcher.anyOf(ALLOWED_CHARS).negate();
private static final Map<String, FormatFactory> CUSTOM_FACTORIES;
private static final Pattern PATTERN_CONTROL_CODE = Pattern.compile("(?i)\\u00A7[0-9A-FK-OR]");

static {
CUSTOM_FACTORIES = new HashMap<>();
// {0,modinfo,id} -> modid from ModInfo object; {0,modinfo,name} -> displayname from ModInfo object
CUSTOM_FACTORIES.put("modinfo", (name, formatString, locale) -> new CustomReadOnlyFormat((stringBuffer, objectToParse) -> parseModInfo(formatString, stringBuffer, objectToParse)));
// {0,lower} -> lowercase supplied string
CUSTOM_FACTORIES.put("lower", (name, formatString, locale) -> new CustomReadOnlyFormat((stringBuffer, objectToParse) -> stringBuffer.append(StringUtils.toLowerCase(String.valueOf(objectToParse)))));
// {0,upper> -> uppercase supplied string
CUSTOM_FACTORIES.put("upper", (name, formatString, locale) -> new CustomReadOnlyFormat((stringBuffer, objectToParse) -> stringBuffer.append(StringUtils.toUpperCase(String.valueOf(objectToParse)))));
// {0,exc,cls} -> class of exception; {0,exc,msg} -> message from exception
CUSTOM_FACTORIES.put("exc", (name, formatString, locale) -> new CustomReadOnlyFormat((stringBuffer, objectToParse) -> parseException(formatString, stringBuffer, objectToParse)));
// {0,vr} -> transform VersionRange into cleartext string using fml.messages.version.restriction.* strings
CUSTOM_FACTORIES.put("vr", (name, formatString, locale) -> new CustomReadOnlyFormat(MavenVersionTranslator::parseVersionRange));
// {0,featurebound} -> transform feature bound to cleartext string
CUSTOM_FACTORIES.put("featurebound", (name, formatString, locale) -> new CustomReadOnlyFormat(MavenVersionTranslator::parseFeatureBoundValue));
// {0,i18n,fml.message} -> pass object to i18n string 'fml.message'
CUSTOM_FACTORIES.put("i18n", (name, formatString, locale) -> new CustomReadOnlyFormat((stringBuffer, o) -> stringBuffer.append(parseMessage(formatString, o))));
// {0,i18ntranslate} -> attempt to use the argument as a translation key
CUSTOM_FACTORIES.put("i18ntranslate", (name, formatString, locale) -> new CustomReadOnlyFormat((stringBuffer, o) -> stringBuffer.append(parseMessage((String) o))));
// {0,ornull,fml.absent} -> append String value of o, or i18n string 'fml.absent' (message format transforms nulls into the string literal "null")
CUSTOM_FACTORIES.put("ornull", ((name, formatString, locale) -> new CustomReadOnlyFormat((stringBuffer, o) -> stringBuffer.append(Objects.equals(String.valueOf(o), "null") ? parseMessage(formatString) : String.valueOf(o)))));
// {0,optional,[prefix]} -> append String value of o if the optional is present, with an optional prefix at the start
CUSTOM_FACTORIES.put("optional", (name, formatString, locale) -> new CustomReadOnlyFormat((stringBuffer, o) -> ((Optional<?>) o).ifPresent(val -> stringBuffer.append(formatString == null ? "" : formatString).append(val))));
}

private static void parseException(final String formatString, final StringBuffer stringBuffer, final Object objectToParse) {
Throwable t = (Throwable) objectToParse;
if (Objects.equals(formatString, "msg")) {
stringBuffer.append(t.getClass().getName()).append(": ").append(t.getMessage());
} else if (Objects.equals(formatString, "cls")) {
stringBuffer.append(t.getClass().getName());
}
}

private static void parseModInfo(final String formatString, final StringBuffer stringBuffer, final Object modInfo) {
final IModInfo info = (IModInfo) modInfo;
if (Objects.equals(formatString, "id")) {
stringBuffer.append(info.getModId());
} else if (Objects.equals(formatString, "name")) {
stringBuffer.append(info.getDisplayName());
}
}

public static String getPattern(final String patternName, final Supplier<String> fallback) {
final var translated = I18nManager.currentLocale.get(patternName);
return translated == null ? fallback.get() : translated;
}

public static String parseMessage(final String i18nMessage, Object... args) {
return parseMessageWithFallback(i18nMessage, () -> i18nMessage, args);
}

public static String parseMessageWithFallback(final String i18nMessage, final Supplier<String> fallback, Object... args) {
final String pattern = getPattern(i18nMessage, fallback);
try {
return parseFormat(pattern, args);
} catch (IllegalArgumentException e) {
LOGGER.error(Logging.CORE, "Illegal format found `{}`", pattern);
return pattern;
}
}

public static String parseEnglishMessage(final String i18n, Object... args) {
var translated = I18nManager.DEFAULT_TRANSLATIONS.getOrDefault(i18n, i18n);
try {
return parseFormat(translated, args);
} catch (IllegalArgumentException e) {
LOGGER.error(Logging.CORE, "Illegal format found `{}`", translated);
return translated;
}
}

public static String parseFormat(final String format, final Object... args) {
final ExtendedMessageFormat extendedMessageFormat = new ExtendedMessageFormat(format, CUSTOM_FACTORIES);
return extendedMessageFormat.format(args);
}

public static String stripControlCodes(String text) {
return PATTERN_CONTROL_CODE.matcher(text).replaceAll("");
}

public static class CustomReadOnlyFormat extends Format {
private final BiConsumer<StringBuffer, Object> formatter;

CustomReadOnlyFormat(final BiConsumer<StringBuffer, Object> formatter) {
this.formatter = formatter;
}

@Override
public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) {
formatter.accept(toAppendTo, obj);
return toAppendTo;
}

@Override
public Object parseObject(final String source, final ParsePosition pos) {
throw new UnsupportedOperationException("Parsing is not supported");
}
}
}
42 changes: 42 additions & 0 deletions loader/src/main/java/net/neoforged/fml/i18n/I18nManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (c) NeoForged and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

package net.neoforged.fml.i18n;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Collections;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class I18nManager {
private static final Logger LOGGER = LoggerFactory.getLogger(FMLTranslations.class);
private static final Gson GSON = new Gson();
private static final String DEFAULT_LOCALE = "en_us";

static final Map<String, String> DEFAULT_TRANSLATIONS = Collections.unmodifiableMap(loadTranslations(DEFAULT_LOCALE));

static Map<String, String> currentLocale = DEFAULT_TRANSLATIONS;

public static void injectTranslations(Map<String, String> translations) {
currentLocale = Collections.unmodifiableMap(translations);
}

public static Map<String, String> loadTranslations(String language) {
var stream = FMLTranslations.class.getResourceAsStream("/lang/" + language + ".json");
if (stream != null) {
try (var reader = new InputStreamReader(stream)) {
return GSON.fromJson(reader, new TypeToken<>() {});
} catch (IOException e) {
LOGGER.error("Failed to load translations for locale {}", language, e);
return Map.of();
}
}
return Map.of();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright (c) NeoForged and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

package net.neoforged.fml.i18n;

import java.util.Objects;
import java.util.stream.Collectors;
import net.neoforged.neoforgespi.locating.ForgeFeature;
import org.apache.maven.artifact.versioning.ArtifactVersion;
import org.apache.maven.artifact.versioning.Restriction;
import org.apache.maven.artifact.versioning.VersionRange;

public class MavenVersionTranslator {
public static String artifactVersionToString(final ArtifactVersion artifactVersion) {
return artifactVersion.toString();
}

public static String versionRangeToString(final VersionRange range) {
return range.getRestrictions().stream().map(MavenVersionTranslator::restrictionToString).collect(Collectors.joining(", "));
}

public static String restrictionToString(final Restriction restriction) {
if (restriction.getLowerBound() == null && restriction.getUpperBound() == null) {
return FMLTranslations.parseMessage("fml.messages.version.restriction.any");
} else if (restriction.getLowerBound() != null && restriction.getUpperBound() != null) {
if (Objects.equals(artifactVersionToString(restriction.getLowerBound()), artifactVersionToString(restriction.getUpperBound()))) {
return artifactVersionToString(restriction.getLowerBound());
} else {
if (restriction.isLowerBoundInclusive() && restriction.isUpperBoundInclusive()) {
return FMLTranslations.parseMessage("fml.messages.version.restriction.bounded.inclusive", restriction.getLowerBound(), restriction.getUpperBound());
} else if (restriction.isLowerBoundInclusive()) {
return FMLTranslations.parseMessage("fml.messages.version.restriction.bounded.upperexclusive", restriction.getLowerBound(), restriction.getUpperBound());
} else if (restriction.isUpperBoundInclusive()) {
return FMLTranslations.parseMessage("fml.messages.version.restriction.bounded.lowerexclusive", restriction.getLowerBound(), restriction.getUpperBound());
} else {
return FMLTranslations.parseMessage("fml.messages.version.restriction.bounded.exclusive", restriction.getLowerBound(), restriction.getUpperBound());
}
}
} else if (restriction.getLowerBound() != null) {
if (restriction.isLowerBoundInclusive()) {
return FMLTranslations.parseMessage("fml.messages.version.restriction.lower.inclusive", restriction.getLowerBound());
} else {
return FMLTranslations.parseMessage("fml.messages.version.restriction.lower.exclusive", restriction.getLowerBound());
}
} else {
if (restriction.isUpperBoundInclusive()) {
return FMLTranslations.parseMessage("fml.messages.version.restriction.upper.inclusive", restriction.getUpperBound());
} else {
return FMLTranslations.parseMessage("fml.messages.version.restriction.upper.exclusive", restriction.getUpperBound());
}
}
}

public static void parseVersionRange(final StringBuffer stringBuffer, final Object range) {
stringBuffer.append(versionRangeToString((VersionRange) range));
}

public static void parseFeatureBoundValue(final StringBuffer stringBuffer, final Object range) {
if (range instanceof ForgeFeature.Bound bound) {
stringBuffer.append(bound.featureName());
if (bound.bound() instanceof Boolean b) {
stringBuffer.append("=").append(b);
} else if (bound.bound() instanceof VersionRange vr) {
stringBuffer.append(" ").append(versionRangeToString(vr));
} else {
stringBuffer.append("=\"").append(bound.featureBound()).append("\"");
}
}
}
}
Loading

0 comments on commit 454a412

Please sign in to comment.