Fixed language reload fallback

- Invalid language updates retain the last valid messages
- Malformed startup catalogs use English defaults
- Unrelated reloads preserve personal language caches
- Deleted locales use English without changing saved choices
- Restored locale files resume their translations
- Language catalogs include shared editor titles and remove obsolete usage messages
This commit is contained in:
Robert Stoneking committed 2026-09-12 13:53:11 -06:00
1 parent bc9b7e86d7
commit e832239730
20 files changed
+142 -55

No files matched your search

@@ -5,6 +5,7 @@ import art.arcane.iris.localization.IrisMessages;
import art.arcane.iris.testsupport.Await;
import art.arcane.volmlib.util.hotload.ConfigHotloadEngine;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.volmlib.util.localization.LanguageAudience;
import java.time.Duration;
import org.junit.After;
import org.junit.Before;
@@ -17,7 +18,9 @@ import java.io.File;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -68,22 +71,23 @@ public class SettingsHotloadWatchTest {
}
@Test
public void activeLocaleSnapshotAppliesAndInvalidSnapshotUsesEnglish() {
public void activeLocaleSnapshotAppliesAndInvalidSnapshotRetainsValidMessages() {
File override = override("en_US");
String valid = locale("en_US", "Active {permission}");
assertTrue(watch.applySnapshot(present(override, valid)));
assertEquals("Active " + PERMISSION, permissionMessage());
assertTrue(watch.applySnapshot(present(override, "{ invalid")));
assertEquals("You lack the permission '" + PERMISSION + "'", permissionMessage());
assertFalse(watch.applySnapshot(present(override, "{ invalid")));
assertEquals("Active " + PERMISSION, permissionMessage());
}
@Test
public void activeLocaleDeletionFallsBackToCodeOwnedEnglish() {
public void activeLocaleDeletionFallsBackToCodeOwnedEnglish() throws Exception {
File override = override("en_US");
assertTrue(watch.applySnapshot(present(override, locale("en_US", "Temporary {permission}"))));
assertEquals("Temporary " + PERMISSION, permissionMessage());
Files.delete(override.toPath());
assertTrue(watch.applySnapshot(missing(override)));
@@ -120,13 +124,13 @@ public class SettingsHotloadWatchTest {
}
@Test
public void invalidLocaleAllowsItsSettingsSwitchWithEnglishFallback() throws Exception {
public void invalidLocaleRejectsItsSettingsSwitchAndRetainsTheCurrentDefault() throws Exception {
Files.writeString(override("de_DE").toPath(), "{ invalid", StandardCharsets.UTF_8);
assertTrue(watch.applySnapshot(present(settingsFile, settings("de_DE"))));
assertFalse(watch.applySnapshot(present(settingsFile, settings("de_DE"))));
assertEquals("de_DE", IrisSettings.get().getGeneral().getLanguage());
assertEquals("de_DE", IrisLanguage.activeLocale());
assertEquals("en_US", IrisSettings.get().getGeneral().getLanguage());
assertEquals("en_US", IrisLanguage.activeLocale());
assertEquals("You lack the permission '" + PERMISSION + "'", permissionMessage());
}
@@ -226,7 +230,7 @@ public class SettingsHotloadWatchTest {
}
@Test(timeout = 8_000L)
public void oversizedActiveLanguageReportsOnceAndUsesEnglish() throws Exception {
public void oversizedActiveLanguageReportsOnceAndRetainsValidMessages() 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 +242,11 @@ public class SettingsHotloadWatchTest {
diagnostic,
"Failed to read watched Iris file " + override.getAbsolutePath()
));
assertEquals("You lack the permission '" + PERMISSION + "'", permissionMessage());
assertEquals("Active " + PERMISSION, permissionMessage());
}
@Test(timeout = 8_000L)
public void malformedUtf8FailureIsDeduplicatedAndUsesEnglish() throws Exception {
public void malformedUtf8FailureIsDeduplicatedAndRetainsValidMessages() 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 +257,74 @@ public class SettingsHotloadWatchTest {
diagnostic,
"Failed to read watched Iris file " + override.getAbsolutePath()
));
assertEquals("You lack the permission '" + PERMISSION + "'", permissionMessage());
assertEquals("Active " + PERMISSION, permissionMessage());
}
@Test(timeout = 30_000L)
public void personalLanguageWatcherRetainsMalformedFilesAndHandlesDeletionAndRestoration() throws Exception {
File personal = override("de_DE");
Files.writeString(personal.toPath(), locale("de_DE", "Personal {permission}"));
UUID player = UUID.randomUUID();
IrisLanguage.start();
try {
IrisLanguage.selections().selectPlayer(player, "de_DE").get(5L, TimeUnit.SECONDS);
Path preferences = dataFolder.toPath().resolve("languages/language-preferences.properties");
String savedPreferences = Files.readString(preferences);
Files.writeString(personal.toPath(), locale("de_DE", "Live {permission}"));
awaitPersonalMessage(player, "Live " + PERMISSION);
Files.writeString(personal.toPath(), "[unterminated");
captureErrorsWhilePolling("Could not reload Iris language de_DE", 150L);
assertEquals("Live " + PERMISSION, personalMessage(player));
Files.writeString(override("en_US").toPath(), locale("en_US", "Server {permission}"));
awaitPermissionMessage("Server " + PERMISSION);
assertEquals("Live " + PERMISSION, personalMessage(player));
assertEquals("[unterminated", Files.readString(personal.toPath()));
String partial = "[iris.command]\nunknown = 'Partial'\n";
Files.writeString(personal.toPath(), partial);
awaitPersonalUnknown(player, "Partial");
assertEquals("You lack the permission '" + PERMISSION + "'", personalMessage(player));
Files.delete(personal.toPath());
awaitPersonalUnknown(player, "Unknown Iris command");
assertFalse(personal.exists());
assertEquals("de_DE", IrisLanguage.selections().playerLocale(player).orElseThrow());
assertEquals(savedPreferences, Files.readString(preferences));
assertEquals("en_US", IrisLanguage.activeLocale());
assertTrue(IrisLanguage.reload(dataFolder, "en_US"));
assertEquals("Unknown Iris command", personalUnknown(player));
assertFalse(personal.exists());
Files.writeString(personal.toPath(), partial);
awaitPersonalUnknown(player, "Partial");
assertEquals(savedPreferences, Files.readString(preferences));
} finally {
IrisLanguage.shutdown();
}
}
private void awaitPersonalMessage(UUID player, String expected) {
assertTrue(Await.reached("the personal permission message '" + expected + "'", Duration.ofSeconds(5L), () -> {
watch.checkConfigHotload();
return expected.equals(personalMessage(player));
}));
}
private void awaitPersonalUnknown(UUID player, String expected) {
assertTrue(Await.reached("the personal unknown-command message '" + expected + "'", Duration.ofSeconds(5L), () -> {
watch.checkConfigHotload();
return expected.equals(personalUnknown(player));
}));
}
private String personalMessage(UUID player) {
return LanguageAudience.call(player, this::permissionMessage);
}
private String personalUnknown(UUID player) {
return LanguageAudience.call(player, () -> IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN));
}
private void awaitPermissionMessage(String expected) {
@@ -130,7 +130,7 @@ public class IrisLanguageTest {
private LocaleOverlay loadSourceOverlay(String locale) throws Exception {
Path source = ProjectPaths.moduleFile("src/main/resources/languages").resolve(locale + ".toml");
return IrisLanguage.parseDownloadedOverlay(source.toString(), locale, Files.readString(source));
return IrisLanguage.parseOverlay(source.toString(), locale, Files.readString(source));
}
@Test
@@ -139,7 +139,7 @@ public class IrisLanguageTest {
"iris.command.unknown" = "Unbekannter Iris-Befehl"
"iris.modded.help.entry.command.version" = "Version anzeigen"
""";
LocaleOverlay downloaded = IrisLanguage.parseDownloadedOverlay("download", "de_DE", raw);
LocaleOverlay downloaded = IrisLanguage.parseOverlay("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 local = IrisLanguage.parseOverlay("language", "de_DE", raw);
@@ -151,7 +151,7 @@ public class IrisLanguageTest {
@Test
public void invalidPlaceholdersAreExcludedWithoutDiscardingValidMessages() {
LocaleOverlay downloaded = IrisLanguage.parseDownloadedOverlay("download", "de_DE", """
LocaleOverlay downloaded = IrisLanguage.parseOverlay("download", "de_DE", """
"iris.command.permission_denied" = "Keine Erlaubnis"
"iris.command.unknown" = "Unbekannter Iris-Befehl"
""");
@@ -238,7 +238,7 @@ public class IrisLanguageTest {
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));
LocaleOverlay overlay = IrisLanguage.parseOverlay(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()));
@@ -475,16 +475,33 @@ public class IrisLanguageTest {
}
@Test
public void unreadableTomlUsesEnglishWithoutReplacingTheFile() throws Exception {
public void unreadableTomlRetainsTheLastValidMessagesWithoutReplacingTheFile() throws Exception {
assertTrue(IrisLanguage.reload(dataFolder, "de_DE"));
String previous = IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN);
String raw = "[unterminated";
File language = writeLanguage("de_DE", raw);
assertTrue(IrisLanguage.reload(dataFolder, "de_DE"));
assertEquals("Unknown Iris command", IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN));
assertFalse(IrisLanguage.reload(dataFolder, "de_DE"));
assertEquals(previous, IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN));
assertEquals(raw, Files.readString(language.toPath()));
}
@Test
public void malformedStartupLanguageUsesBuiltInEnglishAndKeepsSelectionsAvailable() throws Exception {
IrisLanguage.shutdown();
String raw = "[unterminated";
File language = writeLanguage("en_US", raw);
try {
assertTrue(IrisLanguage.reload(dataFolder, "en_US"));
IrisLanguage.start();
assertNotNull(IrisLanguage.selections());
assertEquals("Unknown Iris command", IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN));
assertEquals(raw, Files.readString(language.toPath()));
} finally {
IrisLanguage.shutdown();
}
}
@Test
public void englishEditsSurviveReloadAndMissingEnglishIsCreatedForAnotherLocale() throws Exception {
String edited = "\"iris.command.unknown\" = \"My Iris command\"\n";