mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 05:50:39 +00:00
Updated Iris language files -
All 17 translations now use editable TOML files Language menus and debug messages include translations Headers explain all 243 variables, prefixes, colors and formatting Generated guides refresh without overwriting customized messages Missing or invalid messages fall back to English Language reloads preserve server and player selections Jars include localized guides and required TOML support
This commit is contained in:
1 parent
62e61970f8
commit
45407a7e3f
45 files changed
+37032
-27536
No files matched your search
@@ -46,7 +46,7 @@ public class SettingsHotloadWatchTest {
|
||||
previousSettings = IrisSettings.settings;
|
||||
dataFolder = temporaryFolder.newFolder("iris-hotload");
|
||||
settingsFile = new File(dataFolder, "iris.json");
|
||||
overrideFolder = new File(dataFolder, "languages/overrides");
|
||||
overrideFolder = new File(dataFolder, "languages");
|
||||
Files.createDirectories(overrideFolder.toPath());
|
||||
String settings = settings("en_US");
|
||||
Files.writeString(settingsFile.toPath(), settings, StandardCharsets.UTF_8);
|
||||
@@ -68,15 +68,15 @@ public class SettingsHotloadWatchTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void activeLocaleSnapshotAppliesAndInvalidSnapshotRetainsLastGood() {
|
||||
public void activeLocaleSnapshotAppliesAndInvalidSnapshotUsesEnglish() {
|
||||
File override = override("en_US");
|
||||
String valid = locale("en_US", "Active {permission}");
|
||||
|
||||
assertTrue(watch.applySnapshot(present(override, valid)));
|
||||
assertEquals("Active " + PERMISSION, permissionMessage());
|
||||
|
||||
assertFalse(watch.applySnapshot(present(override, "{ invalid")));
|
||||
assertEquals("Active " + PERMISSION, permissionMessage());
|
||||
assertTrue(watch.applySnapshot(present(override, "{ invalid")));
|
||||
assertEquals("You lack the permission '" + PERMISSION + "'", permissionMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -120,13 +120,13 @@ public class SettingsHotloadWatchTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidLocaleBlocksItsSettingsSwitchAndRetainsThePreviousRuntime() throws Exception {
|
||||
public void invalidLocaleAllowsItsSettingsSwitchWithEnglishFallback() throws Exception {
|
||||
Files.writeString(override("de_DE").toPath(), "{ invalid", StandardCharsets.UTF_8);
|
||||
|
||||
assertFalse(watch.applySnapshot(present(settingsFile, settings("de_DE"))));
|
||||
assertTrue(watch.applySnapshot(present(settingsFile, settings("de_DE"))));
|
||||
|
||||
assertEquals("en_US", IrisSettings.get().getGeneral().getLanguage());
|
||||
assertEquals("en_US", IrisLanguage.activeLocale());
|
||||
assertEquals("de_DE", IrisSettings.get().getGeneral().getLanguage());
|
||||
assertEquals("de_DE", IrisLanguage.activeLocale());
|
||||
assertEquals("You lack the permission '" + PERMISSION + "'", permissionMessage());
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ public class SettingsHotloadWatchTest {
|
||||
}
|
||||
|
||||
@Test(timeout = 8_000L)
|
||||
public void oversizedActiveOverrideReportsOnceAndRetainsLastGoodCatalog() throws Exception {
|
||||
public void oversizedActiveLanguageReportsOnceAndUsesEnglish() throws Exception {
|
||||
File override = override("en_US");
|
||||
assertTrue(watch.applySnapshot(present(override, locale("en_US", "Active {permission}"))));
|
||||
byte[] oversized = new byte[2 * 1024 * 1024 + 1];
|
||||
@@ -238,11 +238,11 @@ public class SettingsHotloadWatchTest {
|
||||
diagnostic,
|
||||
"Failed to read watched Iris file " + override.getAbsolutePath()
|
||||
));
|
||||
assertEquals("Active " + PERMISSION, permissionMessage());
|
||||
assertEquals("You lack the permission '" + PERMISSION + "'", permissionMessage());
|
||||
}
|
||||
|
||||
@Test(timeout = 8_000L)
|
||||
public void malformedUtf8FailureIsDeduplicatedAndRetainsLastGoodCatalog() throws Exception {
|
||||
public void malformedUtf8FailureIsDeduplicatedAndUsesEnglish() throws Exception {
|
||||
File override = override("en_US");
|
||||
assertTrue(watch.applySnapshot(present(override, locale("en_US", "Active {permission}"))));
|
||||
Files.write(override.toPath(), new byte[]{(byte) 0xC3, 0x28});
|
||||
@@ -253,7 +253,7 @@ public class SettingsHotloadWatchTest {
|
||||
diagnostic,
|
||||
"Failed to read watched Iris file " + override.getAbsolutePath()
|
||||
));
|
||||
assertEquals("Active " + PERMISSION, permissionMessage());
|
||||
assertEquals("You lack the permission '" + PERMISSION + "'", permissionMessage());
|
||||
}
|
||||
|
||||
private void awaitPermissionMessage(String expected) {
|
||||
@@ -326,15 +326,15 @@ public class SettingsHotloadWatchTest {
|
||||
}
|
||||
|
||||
private ConfigHotloadEngine.StableContentSnapshot present(File file, String content) {
|
||||
return new ConfigHotloadEngine.StableContentSnapshot(file, "present", content.trim());
|
||||
return new ConfigHotloadEngine.StableContentSnapshot(file, "present", content.trim(), 0L);
|
||||
}
|
||||
|
||||
private ConfigHotloadEngine.StableContentSnapshot missing(File file) {
|
||||
return new ConfigHotloadEngine.StableContentSnapshot(file, "missing", null);
|
||||
return new ConfigHotloadEngine.StableContentSnapshot(file, "missing", null, 0L);
|
||||
}
|
||||
|
||||
private File override(String locale) {
|
||||
return new File(overrideFolder, locale + ".json").getAbsoluteFile();
|
||||
return new File(overrideFolder, locale + ".toml").getAbsoluteFile();
|
||||
}
|
||||
|
||||
private String permissionMessage() {
|
||||
@@ -349,7 +349,6 @@ public class SettingsHotloadWatchTest {
|
||||
}
|
||||
|
||||
private String locale(String locale, String permissionMessage) {
|
||||
return "{\"locale\":\"" + locale + "\",\"messages\":{"
|
||||
+ "\"iris.command.permission_denied\":\"" + permissionMessage + "\"}}";
|
||||
return "[iris.command]\npermission_denied = \"" + permissionMessage + "\"\n";
|
||||
}
|
||||
}
|
||||
+62
-56
@@ -4,8 +4,7 @@ import art.arcane.iris.testsupport.ProjectPaths;
|
||||
import art.arcane.volmlib.util.localization.LocaleOverlay;
|
||||
import art.arcane.volmlib.util.localization.LocalizationValidator;
|
||||
import art.arcane.volmlib.util.localization.RemoteLanguageCatalog;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import art.arcane.volmlib.util.localization.TextValue;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import org.junit.Rule;
|
||||
@@ -30,34 +29,26 @@ import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisDownloadValidationTest {
|
||||
private static final String LOCALE = "de_DE";
|
||||
private static final String MISSING_KEY = "iris.director.commanddeveloper.director.stage_world_generation_update";
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void incompleteCacheDownloadsCompleteCatalogAndReusesIt() throws Exception {
|
||||
public void missingLanguageDownloadsOnceAndPreservesLaterEdits() throws Exception {
|
||||
String complete = completeCatalog();
|
||||
AtomicInteger requests = new AtomicInteger();
|
||||
HttpServer server = server(complete, requests);
|
||||
try (URLClassLoader resources = resources();
|
||||
RemoteLanguageCatalog catalog = catalog(server, resources)) {
|
||||
Path cache = catalog.cacheFile(LOCALE);
|
||||
Files.createDirectories(cache.getParent());
|
||||
Files.writeString(cache, incompleteCatalog(complete));
|
||||
|
||||
RemoteLanguageCatalog.CacheResult initial = catalog.read(LOCALE, IrisLanguage::validateDownload);
|
||||
assertEquals(RemoteLanguageCatalog.CacheState.INVALID, initial.state());
|
||||
assertTrue(initial.failure().getMessage().contains(MISSING_KEY));
|
||||
assertEquals(0, requests.get());
|
||||
|
||||
assertEquals(complete, catalog.readOrDownload(LOCALE, IrisLanguage::validateDownload));
|
||||
assertEquals(complete, Files.readString(cache));
|
||||
assertEquals(RemoteLanguageCatalog.CacheState.VALID,
|
||||
catalog.read(LOCALE, IrisLanguage::validateDownload).state());
|
||||
Path language = temporaryFolder.newFolder().toPath().resolve("languages/de_DE.toml");
|
||||
assertEquals(complete, catalog.readOrInstall(LOCALE, language, IrisLanguage::validateDownload));
|
||||
assertEquals(complete, Files.readString(language));
|
||||
assertEquals(1, requests.get());
|
||||
|
||||
assertEquals(complete, catalog.readOrDownload(LOCALE, IrisLanguage::validateDownload));
|
||||
String edited = "\"iris.command.unknown\" = \"Eigener Befehl\"\n";
|
||||
Files.writeString(language, edited);
|
||||
assertEquals(edited, catalog.readOrInstall(LOCALE, language, IrisLanguage::validateDownload));
|
||||
assertEquals(edited, Files.readString(language));
|
||||
assertEquals(1, requests.get());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
@@ -65,36 +56,34 @@ public class IrisDownloadValidationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void incompleteUpstreamDoesNotReplaceAnInvalidCache() throws Exception {
|
||||
String incomplete = incompleteCatalog(completeCatalog());
|
||||
public void existingUnreadableLanguageIsNeverReplacedByADownload() throws Exception {
|
||||
AtomicInteger requests = new AtomicInteger();
|
||||
HttpServer server = server(incomplete, requests);
|
||||
HttpServer server = server(completeCatalog(), requests);
|
||||
try (URLClassLoader resources = resources();
|
||||
RemoteLanguageCatalog catalog = catalog(server, resources)) {
|
||||
Path cache = catalog.cacheFile(LOCALE);
|
||||
Files.createDirectories(cache.getParent());
|
||||
String cached = "{\"locale\":\"de_DE\",\"messages\":{}}";
|
||||
Files.writeString(cache, cached);
|
||||
Path language = temporaryFolder.newFile("de_DE.toml").toPath();
|
||||
String unreadable = "[unterminated";
|
||||
Files.writeString(language, unreadable);
|
||||
|
||||
IllegalArgumentException failure = assertThrows(IllegalArgumentException.class,
|
||||
() -> catalog.readOrDownload(LOCALE, IrisLanguage::validateDownload));
|
||||
assertTrue(failure.getMessage().contains(MISSING_KEY));
|
||||
assertEquals(cached, Files.readString(cache));
|
||||
assertEquals(1, requests.get());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void incompleteUpstreamDoesNotCreateACacheFile() throws Exception {
|
||||
AtomicInteger requests = new AtomicInteger();
|
||||
HttpServer server = server(incompleteCatalog(completeCatalog()), requests);
|
||||
try (URLClassLoader resources = resources();
|
||||
RemoteLanguageCatalog catalog = catalog(server, resources)) {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> catalog.readOrDownload(LOCALE, IrisLanguage::validateDownload));
|
||||
assertFalse(Files.exists(catalog.cacheFile(LOCALE)));
|
||||
() -> catalog.readOrInstall(LOCALE, language, IrisLanguage::validateDownload));
|
||||
assertEquals(unreadable, Files.readString(language));
|
||||
assertEquals(0, requests.get());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void malformedDownloadDoesNotCreateALanguageFile() throws Exception {
|
||||
AtomicInteger requests = new AtomicInteger();
|
||||
HttpServer server = server("[unterminated", requests);
|
||||
try (URLClassLoader resources = resources();
|
||||
RemoteLanguageCatalog catalog = catalog(server, resources)) {
|
||||
Path language = temporaryFolder.newFolder().toPath().resolve("de_DE.toml");
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> catalog.readOrInstall(LOCALE, language, IrisLanguage::validateDownload));
|
||||
assertFalse(Files.exists(language));
|
||||
assertEquals(1, requests.get());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
@@ -102,9 +91,33 @@ public class IrisDownloadValidationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void partialOperatorOverridesRemainValid() {
|
||||
LocaleOverlay overlay = IrisLanguage.parseOverlay("override", LOCALE, """
|
||||
{"locale":"de_DE","messages":{"iris.command.unknown":"Unbekannter Iris-Befehl"}}
|
||||
public void partialDownloadsInstallTheirValidMessages() throws Exception {
|
||||
String partial = """
|
||||
"iris.command.unknown" = "Unbekannter Iris-Befehl"
|
||||
"iris.command.permission_denied" = "Missing placeholder"
|
||||
"future.message" = "Future value"
|
||||
""";
|
||||
AtomicInteger requests = new AtomicInteger();
|
||||
HttpServer server = server(partial, requests);
|
||||
try (URLClassLoader resources = resources();
|
||||
RemoteLanguageCatalog catalog = catalog(server, resources)) {
|
||||
Path language = temporaryFolder.newFolder().toPath().resolve("de_DE.toml");
|
||||
assertEquals(partial, catalog.readOrInstall(LOCALE, language, IrisLanguage::validateDownload));
|
||||
assertEquals(partial, Files.readString(language));
|
||||
LocaleOverlay overlay = IrisLanguage.parseOverlay(language.toString(), LOCALE, partial);
|
||||
assertEquals(1, overlay.values().size());
|
||||
assertEquals("Unbekannter Iris-Befehl", ((TextValue)
|
||||
overlay.value(IrisMessages.COMMAND_UNKNOWN.id())).template());
|
||||
assertEquals(1, requests.get());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void partialLanguageFilesRemainValid() {
|
||||
LocaleOverlay overlay = IrisLanguage.parseOverlay("language", LOCALE, """
|
||||
"iris.command.unknown" = "Unbekannter Iris-Befehl"
|
||||
""");
|
||||
|
||||
assertEquals(1, overlay.values().size());
|
||||
@@ -112,19 +125,13 @@ public class IrisDownloadValidationTest {
|
||||
}
|
||||
|
||||
private String completeCatalog() throws IOException {
|
||||
return Files.readString(ProjectPaths.moduleFile("src/main/resources/languages").resolve(LOCALE + ".json"));
|
||||
}
|
||||
|
||||
private String incompleteCatalog(String complete) {
|
||||
JsonObject document = JsonParser.parseString(complete).getAsJsonObject();
|
||||
assertTrue(document.getAsJsonObject("messages").remove(MISSING_KEY) != null);
|
||||
return document.toString();
|
||||
return Files.readString(ProjectPaths.moduleFile("src/main/resources/languages").resolve(LOCALE + ".toml"));
|
||||
}
|
||||
|
||||
private HttpServer server(String content, AtomicInteger requests) throws IOException {
|
||||
byte[] response = content.getBytes(StandardCharsets.UTF_8);
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
server.createContext("/master/languages/de_DE.json", exchange -> respond(exchange, response, requests));
|
||||
server.createContext("/master/languages/de_DE.toml", exchange -> respond(exchange, response, requests));
|
||||
server.start();
|
||||
return server;
|
||||
}
|
||||
@@ -150,9 +157,8 @@ public class IrisDownloadValidationTest {
|
||||
"Iris",
|
||||
URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/"),
|
||||
"languages",
|
||||
".json",
|
||||
".toml",
|
||||
"source.properties",
|
||||
temporaryFolder.newFolder().toPath(),
|
||||
resources
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.core.localization;
|
||||
|
||||
import art.arcane.iris.testsupport.ProjectPaths;
|
||||
import art.arcane.volmlib.util.config.TomlCodec;
|
||||
import art.arcane.volmlib.util.localization.LocalizationCandidate;
|
||||
import art.arcane.volmlib.util.localization.LocalizationSnapshot;
|
||||
import art.arcane.volmlib.util.localization.PluginLanguageEditor;
|
||||
@@ -8,7 +9,9 @@ import art.arcane.volmlib.util.localization.PluginLanguageService;
|
||||
import art.arcane.volmlib.util.localization.PluralSelector;
|
||||
import art.arcane.volmlib.util.localization.RemoteLanguageCatalog;
|
||||
import art.arcane.volmlib.util.localization.TextValue;
|
||||
import art.arcane.volmlib.util.localization.TomlLanguageEditor;
|
||||
import art.arcane.volmlib.util.localization.VolmitLocales;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import org.junit.After;
|
||||
@@ -32,7 +35,6 @@ import java.util.logging.Logger;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@@ -48,9 +50,9 @@ public class IrisLanguageEditorTest {
|
||||
public void prepareEditor() throws Exception {
|
||||
directory = temporaryFolder.newFolder();
|
||||
assertTrue(IrisLanguage.reload(directory, "en_US"));
|
||||
Path cache = IrisLanguage.remote(directory).cacheFile("fr_FR");
|
||||
Files.createDirectories(cache.getParent());
|
||||
Files.copy(ProjectPaths.moduleFile("src/main/resources/languages/fr_FR.json"), cache);
|
||||
Path language = directory.toPath().resolve("languages/fr_FR.toml");
|
||||
Files.createDirectories(language.getParent());
|
||||
Files.copy(ProjectPaths.moduleFile("src/main/resources/languages/fr_FR.toml"), language);
|
||||
PluginLanguageEditor.Options options = IrisLanguage.editorOptions();
|
||||
LocalizationSnapshot english = LocalizationSnapshot.create(
|
||||
LocalizationCandidate.english(IrisLanguage.catalog(), PluralSelector.oneOther()));
|
||||
@@ -73,20 +75,22 @@ public class IrisLanguageEditorTest {
|
||||
@Test
|
||||
public void savesOneLocaleAndRefreshesItsPersonalSnapshotWithoutSelectingIt() throws Exception {
|
||||
UUID player = UUID.randomUUID();
|
||||
Path english = directory.toPath().resolve("languages/en_US.toml");
|
||||
byte[] englishBefore = Files.readAllBytes(english);
|
||||
languages.selectPlayer(player, "fr_FR").get(5, TimeUnit.SECONDS);
|
||||
PluginLanguageEditor.Document original = editor.load("fr_FR").get(5, TimeUnit.SECONDS);
|
||||
TextValue value = new TextValue("Langue \"{locale}\" rechargee.\nSuite.");
|
||||
editor.save(new PluginLanguageEditor.Edit("fr_FR", IrisMessages.COMMAND_RELOAD_SUCCESS.id(),
|
||||
original.snapshot().value(IrisMessages.COMMAND_RELOAD_SUCCESS), value)).get(5, TimeUnit.SECONDS);
|
||||
|
||||
Path file = directory.toPath().resolve("languages/overrides/fr_FR.json");
|
||||
Path file = directory.toPath().resolve("languages/fr_FR.toml");
|
||||
assertTrue(Files.isRegularFile(file));
|
||||
assertEquals(value, IrisLanguage.editorOptions().loader().load("fr_FR").value(IrisMessages.COMMAND_RELOAD_SUCCESS));
|
||||
assertEquals(value, languages.snapshot(player).value(IrisMessages.COMMAND_RELOAD_SUCCESS));
|
||||
assertEquals("fr_FR", languages.playerLocale(player).orElseThrow());
|
||||
assertEquals("en_US", languages.defaultLocale());
|
||||
assertEquals("en_US", IrisLanguage.activeLocale());
|
||||
assertFalse(Files.exists(directory.toPath().resolve("languages/overrides/en_US.json")));
|
||||
assertArrayEquals(englishBefore, Files.readAllBytes(english));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -95,7 +99,7 @@ public class IrisLanguageEditorTest {
|
||||
editor.save(new PluginLanguageEditor.Edit("fr_FR", IrisMessages.COMMAND_RELOAD_SUCCESS.id(),
|
||||
original.snapshot().value(IrisMessages.COMMAND_RELOAD_SUCCESS), new TextValue("Langue {locale}")))
|
||||
.get(5, TimeUnit.SECONDS);
|
||||
Path file = directory.toPath().resolve("languages/overrides/fr_FR.json");
|
||||
Path file = directory.toPath().resolve("languages/fr_FR.toml");
|
||||
byte[] before = Files.readAllBytes(file);
|
||||
PluginLanguageEditor.Document saved = editor.load("fr_FR").get(5, TimeUnit.SECONDS);
|
||||
|
||||
@@ -106,8 +110,8 @@ public class IrisLanguageEditorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void incompleteDownloadedLocaleCanBeEditedWithoutBeingSelected() throws Exception {
|
||||
byte[] complete = Files.readAllBytes(ProjectPaths.moduleFile("src/main/resources/languages/fr_FR.json"));
|
||||
public void missingLocaleDownloadsAndCanBeEditedWithoutBeingSelected() throws Exception {
|
||||
byte[] complete = Files.readAllBytes(ProjectPaths.moduleFile("src/main/resources/languages/fr_FR.toml"));
|
||||
AtomicInteger requests = new AtomicInteger();
|
||||
Field remoteField = IrisLanguage.class.getDeclaredField("remote");
|
||||
remoteField.setAccessible(true);
|
||||
@@ -117,19 +121,19 @@ public class IrisLanguageEditorTest {
|
||||
"Iris",
|
||||
URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/"),
|
||||
"languages",
|
||||
".json",
|
||||
".toml",
|
||||
"iris-language-source.properties",
|
||||
directory.toPath().resolve("languages/downloaded"),
|
||||
IrisLanguage.class.getClassLoader()
|
||||
))) {
|
||||
server.createContext(catalog.sourceUri("fr_FR").getPath(),
|
||||
exchange -> respond(exchange, complete, requests));
|
||||
server.start();
|
||||
remoteField.set(null, catalog);
|
||||
Path cache = catalog.cacheFile("fr_FR");
|
||||
Files.writeString(cache, "{\"locale\":\"fr_FR\",\"messages\":{}}");
|
||||
Path language = directory.toPath().resolve("languages/fr_FR.toml");
|
||||
Files.delete(language);
|
||||
|
||||
PluginLanguageEditor.Document original = editor.load("fr_FR").get(5, TimeUnit.SECONDS);
|
||||
assertArrayEquals(complete, Files.readAllBytes(language));
|
||||
TextValue value = new TextValue("Langue {locale}");
|
||||
PluginLanguageEditor.Document saved = editor.save(new PluginLanguageEditor.Edit("fr_FR", IrisMessages.COMMAND_RELOAD_SUCCESS.id(),
|
||||
original.snapshot().value(IrisMessages.COMMAND_RELOAD_SUCCESS), value)).get(5, TimeUnit.SECONDS);
|
||||
@@ -137,7 +141,7 @@ public class IrisLanguageEditorTest {
|
||||
assertEquals(value, saved.snapshot().value(IrisMessages.COMMAND_RELOAD_SUCCESS));
|
||||
assertEquals("en_US", languages.defaultLocale());
|
||||
assertEquals("en_US", IrisLanguage.activeLocale());
|
||||
assertArrayEquals(complete, Files.readAllBytes(cache));
|
||||
assertEquals(value, IrisLanguage.editorOptions().loader().load("fr_FR").value(IrisMessages.COMMAND_RELOAD_SUCCESS));
|
||||
assertEquals(1, requests.get());
|
||||
} finally {
|
||||
remoteField.set(null, previous);
|
||||
@@ -145,6 +149,55 @@ public class IrisLanguageEditorTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void savingOneMessagePreservesUnknownValuesAndLeadingComments() throws Exception {
|
||||
Path file = directory.toPath().resolve("languages/fr_FR.toml");
|
||||
String header = "# Operator customization\n";
|
||||
String extension = "\n[future]\nvalue = \"Preserve this\"\nenabled = true\norder = [1, 2]\n";
|
||||
String source = header + Files.readString(file) + extension;
|
||||
Files.writeString(file, source);
|
||||
JsonElement unknown = TomlCodec.toJsonElement(source).getAsJsonObject().get("future");
|
||||
PluginLanguageEditor.Document original = editor.load("fr_FR").get(5, TimeUnit.SECONDS);
|
||||
TextValue value = new TextValue("Langue {locale}");
|
||||
|
||||
editor.save(new PluginLanguageEditor.Edit("fr_FR", IrisMessages.COMMAND_RELOAD_SUCCESS.id(),
|
||||
original.snapshot().value(IrisMessages.COMMAND_RELOAD_SUCCESS), value)).get(5, TimeUnit.SECONDS);
|
||||
|
||||
String saved = Files.readString(file);
|
||||
assertTrue(saved.startsWith(header));
|
||||
assertEquals(unknown, TomlCodec.toJsonElement(saved).getAsJsonObject().get("future"));
|
||||
assertEquals(value, IrisLanguage.editorOptions().loader().load("fr_FR").value(IrisMessages.COMMAND_RELOAD_SUCCESS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void staleEditorSaveDoesNotReplaceANewerLocalEdit() throws Exception {
|
||||
PluginLanguageEditor.Document original = editor.load("fr_FR").get(5, TimeUnit.SECONDS);
|
||||
Path file = directory.toPath().resolve("languages/fr_FR.toml");
|
||||
String external = TomlLanguageEditor.upsert(Files.readString(file), IrisMessages.COMMAND_RELOAD_SUCCESS.id(),
|
||||
new TextValue("Modification externe {locale}")).content();
|
||||
Files.writeString(file, external);
|
||||
|
||||
assertThrows(ExecutionException.class, () -> editor.save(new PluginLanguageEditor.Edit("fr_FR",
|
||||
IrisMessages.COMMAND_RELOAD_SUCCESS.id(), original.snapshot().value(IrisMessages.COMMAND_RELOAD_SUCCESS),
|
||||
new TextValue("Modification ancienne {locale}"))).get(5, TimeUnit.SECONDS));
|
||||
|
||||
assertEquals(external, Files.readString(file));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void editingDefaultEnglishRefreshesServerMessagesWithoutSelectingAnotherLocale() throws Exception {
|
||||
PluginLanguageEditor.Document original = editor.load("en_US").get(5, TimeUnit.SECONDS);
|
||||
TextValue value = new TextValue("Custom Iris command");
|
||||
|
||||
editor.save(new PluginLanguageEditor.Edit("en_US", IrisMessages.COMMAND_UNKNOWN.id(),
|
||||
original.snapshot().value(IrisMessages.COMMAND_UNKNOWN), value)).get(5, TimeUnit.SECONDS);
|
||||
|
||||
assertEquals("Custom Iris command", IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN));
|
||||
assertEquals("en_US", IrisLanguage.activeLocale());
|
||||
assertEquals("en_US", languages.defaultLocale());
|
||||
assertEquals(value, IrisLanguage.editorOptions().loader().load("en_US").value(IrisMessages.COMMAND_UNKNOWN));
|
||||
}
|
||||
|
||||
private static void respond(HttpExchange exchange, byte[] response, AtomicInteger requests) throws IOException {
|
||||
requests.incrementAndGet();
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package art.arcane.iris.core.localization;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisLanguageGuideTest {
|
||||
private static final String ENGLISH_GUIDE = """
|
||||
# Iris — en_US
|
||||
#
|
||||
# === File editing ===
|
||||
# plugins/Iris/languages/en_US.toml
|
||||
# Edit this file directly. Local changes are preserved; missing or invalid entries use built-in English.
|
||||
# /iris language server edit en_US
|
||||
#
|
||||
# === Prefix ===
|
||||
# Prefixes belong to individual messages or the platform display.
|
||||
#
|
||||
# === Formatting ===
|
||||
# Colors and styles: &0-&f, &k-&r.
|
||||
# RGB colors: &#RRGGBB, &xRRGGBB, &x&R&R&G&G&B&B, [RRGGBB].
|
||||
# Put a backslash before & or [ to display it literally.
|
||||
# Keep the placeholders already present in each message.
|
||||
#
|
||||
# === Variables ===
|
||||
# Keep variable names unchanged and use only the variables belonging to each message.
|
||||
# {permission}
|
||||
# {world}
|
||||
""";
|
||||
private static final String GERMAN_GUIDE = """
|
||||
# Iris - de_DE
|
||||
#
|
||||
# === Datei bearbeiten ===
|
||||
# languages/de_DE.toml
|
||||
# Diese Datei kann direkt bearbeitet werden. Lokale Änderungen bleiben erhalten; fehlende oder ungültige Einträge verwenden das eingebaute Englisch.
|
||||
# /iris language server edit de_DE
|
||||
#
|
||||
|
||||
# === Präfix ===
|
||||
# Präfixe stehen in einzelnen Nachrichten oder werden von der jeweiligen Server- oder Client-Oberfläche ergänzt.
|
||||
#
|
||||
# === Formatierung ===
|
||||
# Farben: &0-&f, &k-&r, &#RRGGBB, &xRRGGBB, &x&R&R&G&G&B&B und [RRGGBB].
|
||||
# Ein vorangestellter Rückwärtsschrägstrich zeigt & oder [ wörtlich an.
|
||||
#
|
||||
|
||||
# === Variablen ===
|
||||
# Variablennamen unverändert lassen und nur die Variablen der jeweiligen Nachricht verwenden.
|
||||
# {permission} {world}
|
||||
""";
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporary = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void refreshesKnownEnglishProseAcrossLineEndingsAndCatalogSizes() throws Exception {
|
||||
String note = "# Local operator note\r\n";
|
||||
String body = "\r\n# Keep this explanation\r\n[iris.command]\r\nunknown = 'Custom answer'\r\n"
|
||||
+ "[extra]\r\nenabled = true\r\n";
|
||||
File file = temporary.newFile("en_US.toml");
|
||||
Files.writeString(file.toPath(), note + ENGLISH_GUIDE.replace("\n", "\r\n") + body);
|
||||
|
||||
IrisLanguageGuide.refresh(file, "en_US");
|
||||
|
||||
assertArrayEquals((note + IrisLanguageGuide.englishHeader("en_US") + body).getBytes(StandardCharsets.UTF_8),
|
||||
Files.readAllBytes(file.toPath()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void refreshesKnownGermanProsePreservingMessageBytes() throws Exception {
|
||||
String body = "\n# Eigene Erklärung\n[iris.command]\nunknown = 'Eigene Nachricht'\n";
|
||||
File file = temporary.newFile("de_DE.toml");
|
||||
Files.writeString(file.toPath(), GERMAN_GUIDE + body);
|
||||
|
||||
IrisLanguageGuide.refresh(file, "de_DE");
|
||||
|
||||
assertArrayEquals((IrisLanguageGuide.header("de_DE") + body).getBytes(StandardCharsets.UTF_8),
|
||||
Files.readAllBytes(file.toPath()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void leavesCustomCommentsInsideAnUnmarkedGuideUntouched() throws Exception {
|
||||
String custom = ENGLISH_GUIDE.replace("# === Formatting ===", "# Operator's custom explanation\n# === Formatting ===")
|
||||
+ "\n[iris.command]\nunknown = 'Custom answer'\n";
|
||||
File file = temporary.newFile("en_US.toml");
|
||||
Files.writeString(file.toPath(), custom);
|
||||
FileTime originalTime = FileTime.fromMillis(123_456L);
|
||||
Files.setLastModifiedTime(file.toPath(), originalTime);
|
||||
|
||||
IrisLanguageGuide.refresh(file, "en_US");
|
||||
|
||||
assertArrayEquals(custom.getBytes(StandardCharsets.UTF_8), Files.readAllBytes(file.toPath()));
|
||||
assertEquals(originalTime, Files.getLastModifiedTime(file.toPath()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void leavesChangedGuideProseAndUnknownUnmarkedGuidesUntouched() {
|
||||
String changed = GERMAN_GUIDE.replace("# languages/de_DE.toml", "# My chosen file location")
|
||||
+ "\n[iris.command]\nunknown = 'Custom answer'\n";
|
||||
String unknown = "# Iris - custom\n# Custom instructions\n# {world}\n[iris.command]\nunknown = 'Custom answer'\n";
|
||||
|
||||
assertEquals(changed, IrisLanguageGuide.replaceHeader(changed, IrisLanguageGuide.englishHeader("de_DE"), "de_DE"));
|
||||
assertEquals(unknown, IrisLanguageGuide.replaceHeader(unknown, IrisLanguageGuide.englishHeader("custom"), "custom"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void englishLocationIsRelativeToThePlatformDataFolder() {
|
||||
String header = IrisLanguageGuide.englishHeader("en_US");
|
||||
|
||||
assertTrue(header.contains("# languages/en_US.toml\n"));
|
||||
assertFalse(header.contains("# plugins/Iris/languages/en_US.toml"));
|
||||
}
|
||||
}
|
||||
+38
-13
@@ -5,6 +5,7 @@ import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
@@ -13,16 +14,17 @@ import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public class IrisLanguageHotloadSnapshotTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void capturesMissingOverrideAsStableTombstone() throws Exception {
|
||||
File override = new File(temporaryFolder.getRoot(), "languages/overrides/en_US.json");
|
||||
public void capturesMissingLanguageAsStableTombstone() throws Exception {
|
||||
File language = new File(temporaryFolder.getRoot(), "languages/en_US.toml");
|
||||
|
||||
LocaleHotloadSnapshot snapshot = IrisLanguage.captureHotloadSnapshot(override, "en_US");
|
||||
LocaleHotloadSnapshot snapshot = IrisLanguage.captureHotloadSnapshot(language, "en_US");
|
||||
|
||||
assertTrue(snapshot.missing());
|
||||
assertEquals("missing", snapshot.sha256());
|
||||
@@ -30,18 +32,18 @@ public class IrisLanguageHotloadSnapshotTest {
|
||||
|
||||
@Test
|
||||
public void detectsSameMetadataContentReplacementBySha256() throws Exception {
|
||||
File override = new File(temporaryFolder.getRoot(), "languages/overrides/en_US.json");
|
||||
Files.createDirectories(override.toPath().getParent());
|
||||
String firstContent = "{\"messages\":{\"a\":\"1\"}}";
|
||||
String secondContent = "{\"messages\":{\"a\":\"2\"}}";
|
||||
Files.writeString(override.toPath(), firstContent, StandardCharsets.UTF_8);
|
||||
File language = new File(temporaryFolder.getRoot(), "languages/en_US.toml");
|
||||
Files.createDirectories(language.toPath().getParent());
|
||||
String firstContent = "a = \"1\"\n";
|
||||
String secondContent = "a = \"2\"\n";
|
||||
Files.writeString(language.toPath(), firstContent, StandardCharsets.UTF_8);
|
||||
FileTime fixedTime = FileTime.fromMillis(10_000L);
|
||||
Files.setLastModifiedTime(override.toPath(), fixedTime);
|
||||
LocaleHotloadSnapshot first = IrisLanguage.captureHotloadSnapshot(override, "en_US");
|
||||
Files.setLastModifiedTime(language.toPath(), fixedTime);
|
||||
LocaleHotloadSnapshot first = IrisLanguage.captureHotloadSnapshot(language, "en_US");
|
||||
|
||||
Files.writeString(override.toPath(), secondContent, StandardCharsets.UTF_8);
|
||||
Files.setLastModifiedTime(override.toPath(), fixedTime);
|
||||
LocaleHotloadSnapshot second = IrisLanguage.captureHotloadSnapshot(override, "en_US");
|
||||
Files.writeString(language.toPath(), secondContent, StandardCharsets.UTF_8);
|
||||
Files.setLastModifiedTime(language.toPath(), fixedTime);
|
||||
LocaleHotloadSnapshot second = IrisLanguage.captureHotloadSnapshot(language, "en_US");
|
||||
|
||||
assertFalse(first.missing());
|
||||
assertFalse(second.missing());
|
||||
@@ -52,4 +54,27 @@ public class IrisLanguageHotloadSnapshotTest {
|
||||
assertNotEquals(first.sha256(), second.sha256());
|
||||
assertNotEquals(first, second);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void snapshotsRejectFilesLargerThanTwoMebibytes() throws Exception {
|
||||
File language = temporaryFolder.newFile("en_US.toml");
|
||||
Files.write(language.toPath(), new byte[2 * 1024 * 1024 + 1]);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> IrisLanguage.captureHotloadSnapshot(language, "en_US"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void snapshotsRejectInvalidUtf8() throws Exception {
|
||||
File language = temporaryFolder.newFile("en_US.toml");
|
||||
Files.write(language.toPath(), new byte[]{(byte) 0xC3, (byte) 0x28});
|
||||
|
||||
assertThrows(IOException.class, () -> IrisLanguage.captureHotloadSnapshot(language, "en_US"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void snapshotsRejectDirectoriesAtTheLanguagePath() throws Exception {
|
||||
File language = temporaryFolder.newFolder("en_US.toml");
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> IrisLanguage.captureHotloadSnapshot(language, "en_US"));
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import art.arcane.volmlib.util.localization.MessageValue;
|
||||
import art.arcane.volmlib.util.localization.PluralValue;
|
||||
import art.arcane.volmlib.util.localization.TextValue;
|
||||
import art.arcane.volmlib.util.localization.VolmitLocales;
|
||||
import com.google.gson.JsonArray;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
@@ -25,6 +24,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -116,50 +116,52 @@ public class IrisLanguageTest {
|
||||
dataFolder = temporaryFolder.newFolder();
|
||||
assertTrue(IrisLanguage.reload(dataFolder, "en_US"));
|
||||
for (String locale : VolmitLocales.nonEnglish()) {
|
||||
Path target = IrisLanguage.remote(dataFolder).cacheFile(locale);
|
||||
Path target = dataFolder.toPath().resolve("languages/" + locale + ".toml");
|
||||
Files.createDirectories(target.getParent());
|
||||
Files.copy(ProjectPaths.moduleFile("src/main/resources/languages").resolve(locale + ".json"), target, StandardCopyOption.REPLACE_EXISTING);
|
||||
Files.copy(ProjectPaths.moduleFile("src/main/resources/languages").resolve(locale + ".toml"), target, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
IrisLanguage.shutdown();
|
||||
assertTrue(IrisLanguage.reload(dataFolder, "en_US"));
|
||||
}
|
||||
|
||||
private LocaleOverlay loadSourceOverlay(String locale) throws Exception {
|
||||
Path source = ProjectPaths.moduleFile("src/main/resources/languages").resolve(locale + ".json");
|
||||
Path source = ProjectPaths.moduleFile("src/main/resources/languages").resolve(locale + ".toml");
|
||||
return IrisLanguage.parseDownloadedOverlay(source.toString(), locale, Files.readString(source));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void downloadedCatalogSelectsBukkitKeysWithoutAcceptingUnknownOverrides() {
|
||||
public void languageCatalogSelectsKnownKeysWithoutDiscardingValidTranslations() {
|
||||
String raw = """
|
||||
{"locale":"de_DE","messages":{
|
||||
"iris.command.unknown":"Unbekannter Iris-Befehl",
|
||||
"iris.modded.help.entry.command.version":"Version anzeigen"
|
||||
}}
|
||||
"iris.command.unknown" = "Unbekannter Iris-Befehl"
|
||||
"iris.modded.help.entry.command.version" = "Version anzeigen"
|
||||
""";
|
||||
LocaleOverlay downloaded = IrisLanguage.parseDownloadedOverlay("download", "de_DE", raw);
|
||||
assertEquals(Set.of(IrisMessages.COMMAND_UNKNOWN.id()), downloaded.values().keySet());
|
||||
assertTrue(LocalizationValidator.validate(IrisLanguage.catalog(), List.of(downloaded)).errors().isEmpty());
|
||||
LocaleOverlay override = IrisLanguage.parseOverlay("override", "de_DE", raw);
|
||||
assertFalse(LocalizationValidator.validate(IrisLanguage.catalog(), List.of(override)).errors().isEmpty());
|
||||
LocaleOverlay local = IrisLanguage.parseOverlay("language", "de_DE", raw);
|
||||
assertEquals(downloaded.values(), local.values());
|
||||
assertTrue(LocalizationValidator.validate(IrisLanguage.catalog(), List.of(local)).errors().isEmpty());
|
||||
assertEquals("A pregeneration task is already running. Stop it first with /iris pregen stop.",
|
||||
IrisLanguage.plain(IrisMessages.PREGEN_ALREADY_RUNNING));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void downloadedCatalogStillRejectsInvalidActivePlaceholders() {
|
||||
public void invalidPlaceholdersAreExcludedWithoutDiscardingValidMessages() {
|
||||
LocaleOverlay downloaded = IrisLanguage.parseDownloadedOverlay("download", "de_DE", """
|
||||
{"locale":"de_DE","messages":{"iris.command.permission_denied":"Keine Erlaubnis"}}
|
||||
"iris.command.permission_denied" = "Keine Erlaubnis"
|
||||
"iris.command.unknown" = "Unbekannter Iris-Befehl"
|
||||
""");
|
||||
assertFalse(LocalizationValidator.validate(IrisLanguage.catalog(), List.of(downloaded)).errors().isEmpty());
|
||||
assertEquals(Set.of(IrisMessages.COMMAND_UNKNOWN.id()), downloaded.values().keySet());
|
||||
assertTrue(LocalizationValidator.validate(IrisLanguage.catalog(), List.of(downloaded)).errors().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingDownloadUsesCodeOwnedEnglish() throws Exception {
|
||||
Files.delete(IrisLanguage.remote(dataFolder).cacheFile("de_DE"));
|
||||
public void missingLanguageUsesBuiltInEnglish() throws Exception {
|
||||
Files.delete(dataFolder.toPath().resolve("languages/de_DE.toml"));
|
||||
assertTrue(IrisLanguage.reload(dataFolder, "de_DE"));
|
||||
assertEquals("Unknown Iris command", IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN));
|
||||
}
|
||||
@@ -203,7 +205,44 @@ public class IrisLanguageTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void usesCodeOwnedEnglishWithoutLocaleFile() {
|
||||
public void hotloadRefreshesDefaultAndPersonalLanguagesWithoutChangingSelections() throws Exception {
|
||||
IrisSettings previous = IrisSettings.settings;
|
||||
IrisSettings.settings = new IrisSettings();
|
||||
UUID frenchPlayer = UUID.randomUUID();
|
||||
IrisLanguage.start();
|
||||
try {
|
||||
IrisLanguage.selections().selectDefault("de_DE").get(5, TimeUnit.SECONDS);
|
||||
IrisLanguage.selections().selectPlayer(frenchPlayer, "fr_FR").get(5, TimeUnit.SECONDS);
|
||||
String german = "\"iris.command.unknown\" = \"Deutsch aktualisiert\"\n";
|
||||
File germanFile = writeLanguage("de_DE", german);
|
||||
assertTrue(IrisLanguage.reloadOverride(germanFile, german));
|
||||
assertEquals("Deutsch aktualisiert", IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN));
|
||||
|
||||
String french = "\"iris.command.unknown\" = \"Francais actualise\"\n";
|
||||
File frenchFile = writeLanguage("fr_FR", french);
|
||||
assertTrue(IrisLanguage.reloadOverride(frenchFile, french));
|
||||
|
||||
assertEquals("Francais actualise", LanguageAudience.call(frenchPlayer,
|
||||
() -> IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN)));
|
||||
assertEquals("Deutsch aktualisiert", IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN));
|
||||
assertEquals("fr_FR", IrisLanguage.selections().playerLocale(frenchPlayer).orElseThrow());
|
||||
assertEquals("de_DE", IrisLanguage.selections().defaultLocale());
|
||||
assertEquals("de_DE", IrisSettings.settings.getGeneral().getLanguage());
|
||||
} finally {
|
||||
IrisLanguage.shutdown();
|
||||
IrisSettings.settings = previous;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generatedEnglishLanguageRoundTripsTheEntireCatalog() throws Exception {
|
||||
Path english = dataFolder.toPath().resolve("languages/en_US.toml");
|
||||
assertTrue(Files.isRegularFile(english));
|
||||
LocaleOverlay overlay = IrisLanguage.parseDownloadedOverlay(english.toString(), "en_US", Files.readString(english));
|
||||
assertEquals(IrisLanguage.catalog().ids(), overlay.values().keySet());
|
||||
for (MessageKey key : IrisLanguage.catalog().keys()) {
|
||||
assertEquals(key.id(), key.englishValue(), overlay.value(key.id()));
|
||||
}
|
||||
String rendered = IrisLanguage.plain(
|
||||
IrisMessages.COMMAND_PERMISSION_DENIED,
|
||||
MessageArgument.untrusted("permission", "iris.all")
|
||||
@@ -214,14 +253,128 @@ public class IrisLanguageTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void appliesExternalOverlayAndFallsBackPerKey() throws Exception {
|
||||
writeOverride("de_DE", """
|
||||
{
|
||||
"locale": "de_DE",
|
||||
"messages": {
|
||||
"iris.command.permission_denied": "Fehlende Berechtigung: {permission}"
|
||||
}
|
||||
}
|
||||
public void guideDescribesEveryRequiredAndOptionalCatalogPlaceholder() {
|
||||
Map<String, String> descriptions = IrisLanguageGuide.variableDescriptions();
|
||||
for (MessageKey key : IrisLanguage.catalog().keys()) {
|
||||
for (String placeholder : key.placeholders()) {
|
||||
assertPlaceholderDescription(key.id(), placeholder, descriptions.get(placeholder));
|
||||
}
|
||||
for (String placeholder : key.optionalPlaceholders()) {
|
||||
assertPlaceholderDescription(key.id(), placeholder, descriptions.get(placeholder));
|
||||
}
|
||||
}
|
||||
for (Map.Entry<String, String> entry : descriptions.entrySet()) {
|
||||
assertPlaceholderDescription("language guide", entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generatedEnglishGuideIncludesMeaningsAndPrefixExamples() throws Exception {
|
||||
String header = Files.readString(dataFolder.toPath().resolve("languages/en_US.toml"));
|
||||
assertTrue(header.startsWith("# === Iris language guide ===\n"));
|
||||
for (Map.Entry<String, String> entry : IrisLanguageGuide.variableDescriptions().entrySet()) {
|
||||
assertTrue(entry.getKey(), header.contains("# {" + entry.getKey() + "} " + entry.getValue()));
|
||||
}
|
||||
assertTrue(header.contains("set_debug = \"&6[Worlds]&r &aSet debug to: {to}\""));
|
||||
assertTrue(header.contains("&6 gold"));
|
||||
assertTrue(header.contains("&l bold"));
|
||||
assertTrue(header.contains("&r reset"));
|
||||
assertTrue(header.contains("{PC}, {SC} and {TC} are not Iris theme variables"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyTranslatedGuideDescribesItsVariablesAndMatchesThePackagedGuide() throws Exception {
|
||||
Pattern definition = Pattern.compile("(?m)^# \\{([A-Za-z][A-Za-z0-9_]*)} (\\S[^\\r\\n]*)$");
|
||||
for (String locale : VolmitLocales.nonEnglish()) {
|
||||
String source = Files.readString(ProjectPaths.moduleFile("src/main/resources/languages/" + locale + ".toml"));
|
||||
int bodyStart = source.indexOf("\n[") + 1;
|
||||
assertTrue(locale, bodyStart > 0);
|
||||
String header = source.substring(0, bodyStart);
|
||||
String body = source.substring(bodyStart);
|
||||
Map<String, String> descriptions = new LinkedHashMap<>();
|
||||
Matcher defined = definition.matcher(header);
|
||||
while (defined.find()) {
|
||||
assertFalse(locale + ": duplicate " + defined.group(1), descriptions.containsKey(defined.group(1)));
|
||||
descriptions.put(defined.group(1), defined.group(2));
|
||||
}
|
||||
Matcher used = PLACEHOLDER.matcher(body);
|
||||
while (used.find()) {
|
||||
String placeholder = used.group().substring(1, used.group().length() - 1);
|
||||
assertPlaceholderDescription(locale, placeholder, descriptions.get(placeholder));
|
||||
}
|
||||
assertEquals(locale, IrisLanguageGuide.variableDescriptions().keySet(), descriptions.keySet());
|
||||
assertEquals(locale, header.stripTrailing(), IrisLanguageGuide.header(locale).stripTrailing());
|
||||
assertTrue(locale, header.contains("# === End Iris language guide ==="));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void refreshingAnExistingGuidePreservesCustomMessagesAndOtherComments() throws Exception {
|
||||
String note = "# Operator's own note\r\n";
|
||||
String oldGuide = "# === Iris language guide ===\r\n# Iris — en_US\r\n"
|
||||
+ "# === Variables ===\r\n# {permission} \r\n# {world} \r\n"
|
||||
+ "# === End Iris language guide ===\r\n";
|
||||
String body = "\r\n# Keep this explanation\r\n\"iris.command.unknown\" = 'My edited message'\r\n"
|
||||
+ "[future]\r\nenabled = true\r\nitems = [1, 2]\r\n";
|
||||
File english = writeLanguage("en_US", note + oldGuide + body);
|
||||
|
||||
assertTrue(IrisLanguage.reload(dataFolder, "en_US"));
|
||||
|
||||
String refreshed = Files.readString(english.toPath());
|
||||
assertEquals(note + IrisLanguageGuide.englishHeader("en_US") + body, refreshed);
|
||||
assertEquals("My edited message", IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN));
|
||||
FileTime unchangedTime = FileTime.fromMillis(123_456L);
|
||||
Files.setLastModifiedTime(english.toPath(), unchangedTime);
|
||||
assertTrue(IrisLanguage.reload(dataFolder, "en_US"));
|
||||
assertEquals(refreshed, Files.readString(english.toPath()));
|
||||
assertEquals(unchangedTime, Files.getLastModifiedTime(english.toPath()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void refreshingATranslatedGuidePreservesItsCustomMessages() throws Exception {
|
||||
String oldGuide = "# === Iris language guide ===\n# Iris - de_DE\n"
|
||||
+ "# === Variablen ===\n# {permission} {world}\n# === End Iris language guide ===\n";
|
||||
String body = "\n[iris.command]\nunknown = 'Eigener Text'\n";
|
||||
File german = writeLanguage("de_DE", oldGuide + body);
|
||||
|
||||
assertTrue(IrisLanguage.reload(dataFolder, "de_DE"));
|
||||
|
||||
assertEquals(IrisLanguageGuide.header("de_DE") + body, Files.readString(german.toPath()));
|
||||
assertEquals("Eigener Text", IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void documentedPrefixExampleUsesItsExistingDebugVariable() throws Exception {
|
||||
writeLanguage("en_US", "[iris.bukkit.commandiris]\nset_debug = \"&6[Worlds]&r &aSet debug to: {to}\"\n");
|
||||
assertTrue(IrisLanguage.reload(dataFolder, "en_US"));
|
||||
MessageKey debug = IrisLanguage.catalog().require("iris.bukkit.commandiris.set_debug");
|
||||
|
||||
assertEquals("\u00a76[Worlds]\u00a7r \u00a7aSet debug to: true",
|
||||
IrisLanguage.text(debug, MessageArgument.trusted("to", true)));
|
||||
assertEquals("[Worlds] Set debug to: false",
|
||||
IrisLanguage.plain(debug, MessageArgument.trusted("to", false)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unreadableEnglishGuideDoesNotBlockSelectingAnotherLanguage() throws Exception {
|
||||
Files.write(dataFolder.toPath().resolve("languages/en_US.toml"), new byte[]{(byte) 0xc3, 0x28});
|
||||
UUID player = UUID.randomUUID();
|
||||
IrisLanguage.start();
|
||||
try {
|
||||
IrisLanguage.selections().selectPlayer(player, "de_DE").get(5, TimeUnit.SECONDS);
|
||||
|
||||
assertEquals("de_DE", IrisLanguage.selections().playerLocale(player).orElseThrow());
|
||||
assertFalse("Unknown Iris command".equals(LanguageAudience.call(player,
|
||||
() -> IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN))));
|
||||
} finally {
|
||||
IrisLanguage.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void appliesLanguageFileAndFallsBackPerKey() throws Exception {
|
||||
writeLanguage("de_DE", """
|
||||
"iris.command.permission_denied" = "Fehlende Berechtigung: {permission}"
|
||||
""");
|
||||
|
||||
assertTrue(IrisLanguage.reload(dataFolder, "de_DE"));
|
||||
@@ -232,7 +385,7 @@ public class IrisLanguageTest {
|
||||
MessageArgument.untrusted("permission", "iris.all")
|
||||
)
|
||||
);
|
||||
assertFalse("Unknown Iris command".equals(IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN)));
|
||||
assertEquals("Unknown Iris command", IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -265,11 +418,11 @@ public class IrisLanguageTest {
|
||||
@Test
|
||||
public void sourceLocalesExactlyMatchNonEnglishManifest() throws Exception {
|
||||
Set<String> expected = VolmitLocales.nonEnglish().stream()
|
||||
.map(locale -> locale + ".json")
|
||||
.map(locale -> locale + ".toml")
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
|
||||
assertEquals(expected, resourceFiles("languages"));
|
||||
assertFalse(expected.contains(VolmitLocales.ENGLISH + ".json"));
|
||||
assertFalse(expected.contains(VolmitLocales.ENGLISH + ".toml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -289,42 +442,61 @@ public class IrisLanguageTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void downloadedLocaleLoadsWithoutUserOverride() {
|
||||
public void installedLocaleLoadsDirectly() {
|
||||
assertTrue(IrisLanguage.reload(dataFolder, "de_DE"));
|
||||
assertEquals("de_DE", IrisLanguage.activeLocale());
|
||||
assertFalse("Unknown Iris command".equals(IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsUnknownKeysAndPlaceholderShapeWhileRetainingLastGood() throws Exception {
|
||||
writeOverride("de_DE", """
|
||||
{
|
||||
"locale": "de_DE",
|
||||
"messages": {
|
||||
"iris.command.permission_denied": "Erlaubnis {permission} fehlt"
|
||||
}
|
||||
}
|
||||
public void invalidEntriesUseEnglishWhileValidAndUnknownFileContentIsPreserved() throws Exception {
|
||||
writeLanguage("de_DE", """
|
||||
"iris.command.permission_denied" = "Erlaubnis {permission} fehlt"
|
||||
""");
|
||||
assertTrue(IrisLanguage.reload(dataFolder, "de_DE"));
|
||||
|
||||
writeOverride("de_DE", """
|
||||
{
|
||||
"locale": "de_DE",
|
||||
"messages": {
|
||||
"iris.command.permission_denied": "Erlaubnis fehlt",
|
||||
"iris.command.not_real": "Unbekannt"
|
||||
}
|
||||
}
|
||||
""");
|
||||
String raw = """
|
||||
"iris.command.permission_denied" = "Erlaubnis fehlt"
|
||||
"iris.command.unknown" = "Unbekannter Iris-Befehl"
|
||||
"iris.command.not_real" = "Unbekannt"
|
||||
""";
|
||||
File language = writeLanguage("de_DE", raw);
|
||||
|
||||
assertFalse(IrisLanguage.reload(dataFolder, "de_DE"));
|
||||
assertTrue(IrisLanguage.reload(dataFolder, "de_DE"));
|
||||
assertEquals(
|
||||
"Erlaubnis iris.all fehlt",
|
||||
"You lack the permission 'iris.all'",
|
||||
IrisLanguage.plain(
|
||||
IrisMessages.COMMAND_PERMISSION_DENIED,
|
||||
MessageArgument.untrusted("permission", "iris.all")
|
||||
)
|
||||
);
|
||||
assertEquals("Unbekannter Iris-Befehl", IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN));
|
||||
assertEquals(raw, Files.readString(language.toPath()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unreadableTomlUsesEnglishWithoutReplacingTheFile() throws Exception {
|
||||
assertTrue(IrisLanguage.reload(dataFolder, "de_DE"));
|
||||
String raw = "[unterminated";
|
||||
File language = writeLanguage("de_DE", raw);
|
||||
|
||||
assertTrue(IrisLanguage.reload(dataFolder, "de_DE"));
|
||||
assertEquals("Unknown Iris command", IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN));
|
||||
assertEquals(raw, Files.readString(language.toPath()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void englishEditsSurviveReloadAndMissingEnglishIsCreatedForAnotherLocale() throws Exception {
|
||||
String edited = "\"iris.command.unknown\" = \"My Iris command\"\n";
|
||||
File english = writeLanguage("en_US", edited);
|
||||
assertTrue(IrisLanguage.reload(dataFolder, "en_US"));
|
||||
assertEquals("My Iris command", IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN));
|
||||
assertEquals(edited, Files.readString(english.toPath()));
|
||||
|
||||
Files.delete(english.toPath());
|
||||
assertTrue(IrisLanguage.reload(dataFolder, "de_DE"));
|
||||
assertTrue(Files.isRegularFile(english.toPath()));
|
||||
assertEquals("de_DE", IrisLanguage.activeLocale());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -349,13 +521,8 @@ public class IrisLanguageTest {
|
||||
|
||||
@Test
|
||||
public void untrustedArgumentsCannotInjectLegacyOrMiniMessageFormatting() throws Exception {
|
||||
writeOverride("de_DE", """
|
||||
{
|
||||
"locale": "de_DE",
|
||||
"messages": {
|
||||
"iris.command.permission_denied": "&aWert: {permission}"
|
||||
}
|
||||
}
|
||||
writeLanguage("de_DE", """
|
||||
"iris.command.permission_denied" = "&aWert: {permission}"
|
||||
""");
|
||||
assertTrue(IrisLanguage.reload(dataFolder, "de_DE"));
|
||||
|
||||
@@ -385,28 +552,20 @@ public class IrisLanguageTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lookupsUseImmutableSnapshotWithoutReadingTheOverrideAgain() throws Exception {
|
||||
File override = writeOverride("de_DE", """
|
||||
{
|
||||
"locale": "de_DE",
|
||||
"messages": {
|
||||
"iris.command.unknown": "Unbekannter Iris-Befehl"
|
||||
}
|
||||
}
|
||||
public void lookupsUseImmutableSnapshotWithoutReadingTheLanguageFileAgain() throws Exception {
|
||||
File language = writeLanguage("de_DE", """
|
||||
"iris.command.unknown" = "Unbekannter Iris-Befehl"
|
||||
""");
|
||||
assertTrue(IrisLanguage.reload(dataFolder, "de_DE"));
|
||||
Files.delete(override.toPath());
|
||||
Files.delete(language.toPath());
|
||||
|
||||
assertEquals("Unbekannter Iris-Befehl", IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void appliesLineAndPluralOverlayShapes() throws Exception {
|
||||
writeOverride("de_DE", """
|
||||
{
|
||||
"locale": "de_DE",
|
||||
"messages": {
|
||||
"iris.bukkit.runtime.commanddeveloper.stage_world_generation_update_warning": [
|
||||
writeLanguage("de_DE", """
|
||||
"iris.bukkit.runtime.commanddeveloper.stage_world_generation_update_warning" = [
|
||||
"Sicherung erstellen.",
|
||||
"Vorhandene Chunks bleiben erhalten.",
|
||||
"Neue Chunks verwenden das neue Paket.",
|
||||
@@ -414,13 +573,10 @@ public class IrisLanguageTest {
|
||||
"Den vollständigen Verlauf sichern.",
|
||||
"Neustart bestätigen.",
|
||||
"Welt {world}, Paket {pack}"
|
||||
],
|
||||
"iris.bukkit.runtime.commandpack.more_warning_s": {
|
||||
"one": "Noch {count} Warnung.",
|
||||
"other": "Noch {count} Warnungen."
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
["iris.bukkit.runtime.commandpack.more_warning_s"]
|
||||
one = "Noch {count} Warnung."
|
||||
other = "Noch {count} Warnungen."
|
||||
""");
|
||||
|
||||
assertTrue(IrisLanguage.reload(dataFolder, "de_DE"));
|
||||
@@ -454,13 +610,21 @@ public class IrisLanguageTest {
|
||||
);
|
||||
}
|
||||
|
||||
private File writeOverride(String locale, String json) throws Exception {
|
||||
File file = new File(dataFolder, "languages/overrides/" + locale + ".json");
|
||||
private File writeLanguage(String locale, String toml) throws Exception {
|
||||
File file = new File(dataFolder, "languages/" + locale + ".toml");
|
||||
Files.createDirectories(file.toPath().getParent());
|
||||
Files.writeString(file.toPath(), json, StandardCharsets.UTF_8);
|
||||
Files.writeString(file.toPath(), toml, StandardCharsets.UTF_8);
|
||||
return file;
|
||||
}
|
||||
|
||||
private void assertPlaceholderDescription(String key, String placeholder, String description) {
|
||||
String context = key + " placeholder {" + placeholder + "}";
|
||||
assertNotNull(context + " needs a definition", description);
|
||||
assertFalse(context + " definition must not be blank", description.isBlank());
|
||||
assertFalse(context + " definition must explain the variable", placeholder.equalsIgnoreCase(description.strip()));
|
||||
assertFalse(context + " definition must explain the variable", ("{" + placeholder + "}").equals(description.strip()));
|
||||
}
|
||||
|
||||
private void assertValueIntegrity(String locale, String key, MessageValue english, MessageValue translated) {
|
||||
if (english instanceof TextValue englishText && translated instanceof TextValue translatedText) {
|
||||
assertTemplateIntegrity(locale, key, englishText.template(), translatedText.template());
|
||||
|
||||
Reference in new issue
Block a user