diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4b38e6f8..0772569b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/src/main/java/net/fabricmc/mappingio/format/MappingFormat.java b/src/main/java/net/fabricmc/mappingio/format/MappingFormat.java
index b281ce36..9b7c0682 100644
--- a/src/main/java/net/fabricmc/mappingio/format/MappingFormat.java
+++ b/src/main/java/net/fabricmc/mappingio/format/MappingFormat.java
@@ -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.
@@ -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
@@ -54,7 +57,7 @@ public enum MappingFormat {
/**
* The {@code Tiny v2} mapping format, as specified here.
*/
- 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
@@ -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)
@@ -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 here.
*
* @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)
@@ -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))
@@ -165,7 +168,7 @@ 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))),
@@ -173,7 +176,7 @@ public enum MappingFormat {
/**
* The {@code JAM} ("Java Associated Mapping"; formerly {@code SRGX}) mapping format, as specified here.
*/
- 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))
@@ -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))),
@@ -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 here.
*
* @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
@@ -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)
@@ -238,7 +241,7 @@ public enum MappingFormat {
/**
* Recaf's {@code Simple} mapping format, as specified here.
*/
- 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)
@@ -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))
@@ -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();
@@ -287,6 +291,10 @@ public enum MappingFormat {
this.supportsLocals = features.supportsVars();
}
+ public Translatable translatableName() {
+ return Translatable.of(translationKey);
+ }
+
public FeatureSet features() {
return features;
}
@@ -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;
diff --git a/src/main/java/net/fabricmc/mappingio/i18n/I18n.java b/src/main/java/net/fabricmc/mappingio/i18n/I18n.java
new file mode 100644
index 00000000..45f137d6
--- /dev/null
+++ b/src/main/java/net/fabricmc/mappingio/i18n/I18n.java
@@ -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 messageBundles = new HashMap<>();
+}
diff --git a/src/main/java/net/fabricmc/mappingio/i18n/Translatable.java b/src/main/java/net/fabricmc/mappingio/i18n/Translatable.java
new file mode 100644
index 00000000..e1029734
--- /dev/null
+++ b/src/main/java/net/fabricmc/mappingio/i18n/Translatable.java
@@ -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();
+}
diff --git a/src/main/java/net/fabricmc/mappingio/i18n/TranslatableImpl.java b/src/main/java/net/fabricmc/mappingio/i18n/TranslatableImpl.java
new file mode 100644
index 00000000..78d0e2e2
--- /dev/null
+++ b/src/main/java/net/fabricmc/mappingio/i18n/TranslatableImpl.java
@@ -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;
+}
diff --git a/src/main/resources/mappingio/lang/en_us.properties b/src/main/resources/mappingio/lang/en_us.properties
new file mode 100644
index 00000000..c6b10743
--- /dev/null
+++ b/src/main/resources/mappingio/lang/en_us.properties
@@ -0,0 +1,14 @@
+format.tiny_file = Tiny File
+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
diff --git a/src/test/java/net/fabricmc/mappingio/test/tests/i18n/I18nTest.java b/src/test/java/net/fabricmc/mappingio/test/tests/i18n/I18nTest.java
new file mode 100644
index 00000000..9b837532
--- /dev/null
+++ b/src/test/java/net/fabricmc/mappingio/test/tests/i18n/I18nTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.test.tests.i18n;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import java.io.File;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import net.fabricmc.mappingio.format.MappingFormat;
+
+public class I18nTest {
+ private static List supportedLocales;
+
+ @BeforeAll
+ public static void init() throws Exception {
+ URL i18nUrl = I18nTest.class.getResource("/mappingio/lang/");
+ Path path = Paths.get(i18nUrl.toURI());
+
+ supportedLocales = Files.walk(path)
+ .map(Path::toAbsolutePath)
+ .map(Path::toString)
+ .filter(name -> name.endsWith(".properties"))
+ .map(name -> name.substring(Math.max(0, name.lastIndexOf(File.separatorChar) + 1), name.length() - ".properties".length()))
+ .map(tag -> Locale.forLanguageTag(tag))
+ .collect(Collectors.toList());
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void mappingFormatNames() {
+ for (MappingFormat format : MappingFormat.values()) {
+ String enUsName = format.translatableName().translate(Locale.US);
+ assertEquals(enUsName, format.name);
+
+ for (Locale locale : supportedLocales) {
+ String translatedName = format.translatableName().translate(locale);
+ assertFalse(translatedName.startsWith("format."), "Translated name for " + format + " in " + locale + " is missing");
+ }
+ }
+ }
+}