Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial localization support #37

Open
wants to merge 14 commits into
base: dev
Choose a base branch
from
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Made `OuterClassNamePropagator` configurable
- Made Enigma writer always output destination names if visited explicitly, establishing consistency across all writers
- Added a simplified `MappingNsCompleter` constructor for completing all destination names with the source names
- Added `MappingFormat#translatableName()`

## [0.7.1] - 2025-01-07
- Restored the ability to read source-namespace-only mapping files, even if not spec-compliant
Expand Down
47 changes: 31 additions & 16 deletions src/main/java/net/fabricmc/mappingio/format/MappingFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@

package net.fabricmc.mappingio.format;

import java.util.Locale;

import org.jetbrains.annotations.Nullable;

import net.fabricmc.mappingio.format.FeatureSet.ElementCommentSupport;
import net.fabricmc.mappingio.format.FeatureSet.FeaturePresence;
import net.fabricmc.mappingio.format.FeatureSet.MetadataSupport;
import net.fabricmc.mappingio.i18n.Translatable;

/**
* Represents a supported mapping format. Every format can be assumed to have an associated reader available.
Expand All @@ -34,7 +37,7 @@ public enum MappingFormat {
*
* @implNote File metadata only has limited support as of now, and is hardcoded to intermediary counters.
*/
TINY_FILE("Tiny file", "tiny", true, FeatureSetBuilder.create()
TINY_FILE("tiny", true, FeatureSetBuilder.create()
.withNamespaces(true)
.withFileMetadata(MetadataSupport.FIXED) // TODO: change this to ARBITRARY once https://github.com/FabricMC/mapping-io/pull/29 is merged
.withClasses(c -> c
Expand All @@ -54,7 +57,7 @@ public enum MappingFormat {
/**
* The {@code Tiny v2} mapping format, as specified <a href="https://fabricmc.net/wiki/documentation:tiny2">here</a>.
*/
TINY_2_FILE("Tiny v2 file", "tiny", true, FeatureSetBuilder.create()
TINY_2_FILE("tiny", true, FeatureSetBuilder.create()
.withNamespaces(true)
.withFileMetadata(MetadataSupport.ARBITRARY)
.withClasses(c -> c
Expand Down Expand Up @@ -87,7 +90,7 @@ public enum MappingFormat {
*
* @implNote Access modifiers are currently not supported.
*/
ENIGMA_FILE("Enigma file", "mapping", true, FeatureSetBuilder.create()
ENIGMA_FILE("mapping", true, FeatureSetBuilder.create()
.withElementMetadata(MetadataSupport.FIXED) // access modifiers
.withClasses(c -> c
.withSrcNames(FeaturePresence.REQUIRED)
Expand All @@ -112,14 +115,14 @@ public enum MappingFormat {
*
* @implNote Access modifiers are currently not supported.
*/
ENIGMA_DIR("Enigma directory", null, true, FeatureSetBuilder.createFrom(ENIGMA_FILE.features)),
ENIGMA_DIR(null, true, FeatureSetBuilder.createFrom(ENIGMA_FILE.features)),

/**
* ProGuard's mapping format, as specified <a href="https://www.guardsquare.com/manual/tools/retrace">here</a>.
*
* @implNote Line numbers are currently not supported.
*/
PROGUARD_FILE("ProGuard file", "txt", true, FeatureSetBuilder.create()
PROGUARD_FILE("txt", true, FeatureSetBuilder.create()
.withElementMetadata(MetadataSupport.FIXED) // line numbers
.withClasses(c -> c
.withSrcNames(FeaturePresence.REQUIRED)
Expand All @@ -140,7 +143,7 @@ public enum MappingFormat {
*
* @implNote Package mappings are currently not supported.
*/
SRG_FILE("SRG file", "srg", true, FeatureSetBuilder.create()
SRG_FILE("srg", true, FeatureSetBuilder.create()
.withPackages(p -> p
.withSrcNames(FeaturePresence.REQUIRED)
.withDstNames(FeaturePresence.REQUIRED))
Expand All @@ -165,15 +168,15 @@ public enum MappingFormat {
*
* @implNote Package mappings are currently not supported.
*/
XSRG_FILE("XSRG file", "xsrg", true, FeatureSetBuilder.createFrom(SRG_FILE.features)
XSRG_FILE("xsrg", true, FeatureSetBuilder.createFrom(SRG_FILE.features)
.withFields(f -> f
.withSrcDescs(FeaturePresence.REQUIRED)
.withDstDescs(FeaturePresence.REQUIRED))),

/**
* The {@code JAM} ("Java Associated Mapping"; formerly {@code SRGX}) mapping format, as specified <a href="https://github.com/caseif/JAM">here</a>.
*/
JAM_FILE("JAM file", "jam", true, FeatureSetBuilder.createFrom(SRG_FILE.features)
JAM_FILE("jam", true, FeatureSetBuilder.createFrom(SRG_FILE.features)
.withPackages(p -> p
.withSrcNames(FeaturePresence.ABSENT)
.withDstNames(FeaturePresence.ABSENT))
Expand All @@ -191,7 +194,7 @@ public enum MappingFormat {
*
* @implNote Package mappings are currently not supported.
*/
CSRG_FILE("CSRG file", "csrg", true, FeatureSetBuilder.createFrom(SRG_FILE.features)
CSRG_FILE("csrg", true, FeatureSetBuilder.createFrom(SRG_FILE.features)
.withMethods(m -> m
.withDstDescs(FeaturePresence.ABSENT))),

Expand All @@ -202,14 +205,14 @@ public enum MappingFormat {
*
* @implNote Package mappings are currently not supported.
*/
TSRG_FILE("TSRG file", "tsrg", true, FeatureSetBuilder.createFrom(CSRG_FILE.features)),
TSRG_FILE("tsrg", true, FeatureSetBuilder.createFrom(CSRG_FILE.features)),

/**
* The {@code TSRG v2} mapping format, as specified <a href="https://github.com/MinecraftForge/SrgUtils/blob/67f30647ece29f18256ca89a23cda6216d6bd21e/src/main/java/net/minecraftforge/srgutils/InternalUtils.java#L262-L285">here</a>.
*
* @implNote Package mappings and static markers for methods are currently not supported.
*/
TSRG_2_FILE("TSRG v2 file", "tsrg", true, FeatureSetBuilder.createFrom(TSRG_FILE.features)
TSRG_2_FILE("tsrg", true, FeatureSetBuilder.createFrom(TSRG_FILE.features)
.withNamespaces(true)
.withElementMetadata(MetadataSupport.FIXED) // static info for methods
.withFields(f -> f
Expand All @@ -224,7 +227,7 @@ public enum MappingFormat {
*
* @implNote Package mappings and file metadata are currently not supported.
*/
INTELLIJ_MIGRATION_MAP_FILE("IntelliJ migration map file", "xml", true, FeatureSetBuilder.create()
INTELLIJ_MIGRATION_MAP_FILE("xml", true, FeatureSetBuilder.create()
.withFileMetadata(MetadataSupport.FIXED) // migration map name and description
.withPackages(p -> p
.withSrcNames(FeaturePresence.REQUIRED)
Expand All @@ -238,7 +241,7 @@ public enum MappingFormat {
/**
* Recaf's {@code Simple} mapping format, as specified <a href="https://github.com/Col-E/Recaf/blob/e9765d4e02991a9dd48e67c9572a063c14552e7c/src/main/java/me/coley/recaf/mapping/SimpleMappings.java#L14-L23">here</a>.
*/
RECAF_SIMPLE_FILE("Recaf Simple file", "txt", true, FeatureSetBuilder.create()
RECAF_SIMPLE_FILE("txt", true, FeatureSetBuilder.create()
.withClasses(c -> c
.withSrcNames(FeaturePresence.REQUIRED)
.withDstNames(FeaturePresence.REQUIRED)
Expand All @@ -258,7 +261,7 @@ public enum MappingFormat {
*
* @implNote Package mappings are currently not supported.
*/
JOBF_FILE("JOBF file", "jobf", true, FeatureSetBuilder.create()
JOBF_FILE("jobf", true, FeatureSetBuilder.create()
.withPackages(p -> p
.withSrcNames(FeaturePresence.REQUIRED)
.withDstNames(FeaturePresence.REQUIRED))
Expand All @@ -275,8 +278,9 @@ public enum MappingFormat {
.withSrcDescs(FeaturePresence.REQUIRED))
.withFileComments(true));

MappingFormat(String name, @Nullable String fileExt, boolean hasWriter, FeatureSetBuilder featureBuilder) {
this.name = name;
MappingFormat(@Nullable String fileExt, boolean hasWriter, FeatureSetBuilder featureBuilder) {
this.translationKey = "format." + name().toLowerCase(Locale.ROOT);
this.name = translatableName().translate(Locale.US);
this.fileExt = fileExt;
this.hasWriter = hasWriter;
this.features = featureBuilder.build();
Expand All @@ -287,6 +291,10 @@ public enum MappingFormat {
this.supportsLocals = features.supportsVars();
}

public Translatable translatableName() {
return Translatable.of(translationKey);
}

public FeatureSet features() {
return features;
}
Expand All @@ -301,8 +309,15 @@ public String getGlobPattern() {
return "*."+fileExt;
}

private final String translationKey;
private final FeatureSet features;

/**
* @deprecated Use {@link #translatableName()} instead.
*/
@Deprecated
public final String name;

public final boolean hasWriter;
@Nullable
public final String fileExt;
Expand Down
111 changes: 111 additions & 0 deletions src/main/java/net/fabricmc/mappingio/i18n/I18n.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Copyright (c) 2023 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.mappingio.i18n;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.jetbrains.annotations.ApiStatus;

@ApiStatus.Internal
public class I18n {
private I18n() {
}

public static String translate(String key, Locale locale, Object... args) {
return String.format(translate(key, locale), args);
}

public static String translate(String key, Locale locale) {
try {
return load(locale).getString(key);
} catch (Exception e) {
System.err.println("Exception while translating key " + key + " to locale " + locale.toLanguageTag() + ": " + getStackTrace(e));
if (locale == fallbackLocale) return key;

try {
return load(fallbackLocale).getString(key);
} catch (Exception e2) {
System.err.println("Exception while translating key " + key + " to fallback locale: " + getStackTrace(e2));
return key;
}
}
}

private static ResourceBundle load(Locale locale) {
ResourceBundle bundle = messageBundles.get(locale);

if (bundle != null) {
return bundle;
}

bundlesLock.lock();

try {
if ((bundle = messageBundles.get(locale)) != null) {
return bundle;
}

return load0(locale);
} finally {
bundlesLock.unlock();
}
}

private static ResourceBundle load0(Locale locale) {
ResourceBundle resBundle;
String resName = String.format("/mappingio/lang/%s.properties", locale.toLanguageTag().replace('-', '_').toLowerCase(Locale.ROOT));
URL resUrl = I18n.class.getResource(resName);

if (resUrl == null) {
throw new RuntimeException("Locale resource not found: " + resName);
}

try (Reader reader = new InputStreamReader(resUrl.openStream(), StandardCharsets.UTF_8)) {
resBundle = new PropertyResourceBundle(reader);
messageBundles.put(locale, resBundle);
return resBundle;
} catch (IOException e) {
throw new RuntimeException("Failed to load " + resName, e);
}
}

private static String getStackTrace(Throwable t) {
if (t == null) return null;

StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
t.printStackTrace(pw);
return sw.toString();
}

private static final Lock bundlesLock = new ReentrantLock();
private static final Locale fallbackLocale = Locale.US;
private static final Map<Locale, ResourceBundle> messageBundles = new HashMap<>();
}
40 changes: 40 additions & 0 deletions src/main/java/net/fabricmc/mappingio/i18n/Translatable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2024 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.mappingio.i18n;

import java.util.Locale;

import org.jetbrains.annotations.ApiStatus;

@ApiStatus.NonExtendable
public interface Translatable {
@ApiStatus.Internal
static Translatable of(String translationKey) {
return new TranslatableImpl(translationKey);
}

/**
* Translates this translatable to the specified locale, with a fallback to en_US.
*/
String translate(Locale locale);

/**
* Returns the translation key of this translatable, allowing consumers to provide their own translations
* via custom localization facilities.
*/
String translationKey();
}
37 changes: 37 additions & 0 deletions src/main/java/net/fabricmc/mappingio/i18n/TranslatableImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2024 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.mappingio.i18n;

import java.util.Locale;

final class TranslatableImpl implements Translatable {
TranslatableImpl(String translationKey) {
this.translationKey = translationKey;
}

@Override
public String translate(Locale locale) {
return I18n.translate(translationKey, locale);
}

@Override
public String translationKey() {
return translationKey;
}

private final String translationKey;
}
14 changes: 14 additions & 0 deletions src/main/resources/mappingio/lang/en_us.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
format.tiny_file = Tiny File
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of this shouldnt even be translated, other than maybe "File". Im not really sure this should be handled in mapping-io.

Copy link
Member Author

@NebelNidas NebelNidas Jan 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some languages will need to hyphenate individual words, switch their order around, or even outright translate the format names. For example in German, you'd have Tiny-v2-Datei and IntelliJ-Migrationszuordnungsdatei (at least when going with the German language pack's translation).

I think this makes sense being part of Mapping-IO natively, as I don't want to duplicate the translation files across a dozen different applications like Enigma, Matcher and JADX, just to name a few. And you're not forced to use MIO's translations either, you can always delegate to a custom localization system via Translatable#translationKey(). Not to mention that these facilities are required for #88 and #94 to provide user-friendly warnings and error messages e.g. in Enigma.

format.tiny_2_file = Tiny v2 File
format.enigma_file = Enigma File
format.enigma_dir = Enigma Directory
format.proguard_file = ProGuard File
format.srg_file = SRG File
format.xsrg_file = XSRG File
format.jam_file = JAM File
format.csrg_file = CSRG File
format.tsrg_file = TSRG File
format.tsrg_2_file = TSRG v2 File
format.intellij_migration_map_file = IntelliJ Migration Map File
format.recaf_simple_file = Recaf Simple File
format.jobf_file = JOBF File
Loading