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
2026-09-12 13:53:11 -06:00
parent bc9b7e86d7
commit e832239730
20 changed files with 142 additions and 55 deletions
@@ -167,7 +167,16 @@ public final class IrisLanguage {
refreshLanguageGuide(override, requestedLocale);
}
SnapshotCapture capture = captureForReload(override, requestedLocale);
boolean initialLoad = dataFolder == null;
boolean changedLocale = !requestedLocale.equals(activeLocale);
boolean applied = applyReload(resolvedRoot, requestedLocale, capture);
if (!applied && initialLoad) {
return applyReload(resolvedRoot, requestedLocale,
new SnapshotCapture(LocaleHotloadSnapshot.missing(override, requestedLocale), null));
}
if (applied && changedLocale && selections != null) {
requestConfiguredLocale();
}
if (applied && notifyManualReload) {
notifyManualReload(capture.snapshot());
}
@@ -199,6 +208,12 @@ public final class IrisLanguage {
if (!expected.equals(override.getAbsoluteFile())) {
return true;
}
if (rawContent == null && !Files.notExists(expected.toPath(), LinkOption.NOFOLLOW_LINKS)) {
IOException failure = new IOException("Language file could not be read: " + expected);
IrisLogging.error("Could not reload Iris language " + requestedLocale + "; keeping the last valid messages.");
IrisLogging.reportError(failure);
return false;
}
LocaleHotloadSnapshot snapshot = rawContent == null
? LocaleHotloadSnapshot.missing(expected, requestedLocale)
@@ -278,9 +293,6 @@ public final class IrisLanguage {
return false;
}
if (current != null) {
requestConfiguredLocale();
}
int warnings = result.validation().warnings().size();
IrisLogging.debug("Loaded locale " + requestedLocale + " with " + warnings + " fallback "
+ (warnings == 1 ? "entry" : "entries") + ".");
@@ -629,11 +641,11 @@ public final class IrisLanguage {
}
static void validateDownload(String locale, String raw) {
LocaleOverlay overlay = parseDownloadedOverlay("download:" + locale, locale, raw);
LocaleOverlay overlay = parseOverlay("download:" + locale, locale, raw);
LocalizationValidator.validate(CATALOG, List.of(overlay)).throwIfInvalid();
}
static LocaleOverlay parseDownloadedOverlay(String source, String locale, String raw) {
static LocaleOverlay parseOverlay(String source, String locale, String raw) {
LocaleOverlay.Builder builder = LocaleOverlay.builder(source, locale);
try {
for (Map.Entry<String, MessageValue> entry : TomlLanguageParser.parseValidValues(raw, CATALOG).entrySet()) {
@@ -645,16 +657,6 @@ public final class IrisLanguage {
return LocalizationValidator.validValues(CATALOG, builder.build());
}
static LocaleOverlay parseOverlay(String source, String locale, String raw) {
try {
return parseDownloadedOverlay(source, locale, raw);
} catch (IllegalArgumentException failure) {
IrisLogging.error("Using English for unreadable Iris language file " + source + ".");
IrisLogging.reportError(failure);
return LocaleOverlay.builder(source, locale).build();
}
}
static String englishReference() {
Map<String, MessageValue> values = new LinkedHashMap<>();
for (MessageKey key : CATALOG.keys()) {
@@ -924,10 +926,7 @@ public final class IrisLanguage {
}
return new SnapshotCapture(snapshot, null);
} catch (Exception failure) {
IrisLogging.error("Using English for unreadable Iris language file " + file.getPath() + ".");
IrisLogging.reportError(failure);
return new SnapshotCapture(LocaleHotloadSnapshot.present(
file, locale, "", sha256(new byte[0])), null);
return new SnapshotCapture(null, failure);
}
}
+1 -1
View File
@@ -1,3 +1,4 @@
"language.editor.title" = "{plugin} › {section}"
# === Iris language guide ===
# Iris - de_DE
#
@@ -333,7 +334,6 @@ parameters_hover = "Bewege den Mauszeiger über die Parameter, um mehr zu erfahr
"error.default_parse_failed" = "Der Standardwert für Parameter {parameter} kann nicht gelesen werden"
"error.missing_argument" = "Fehlendes Argument \"{parameter}\" ({type})"
"error.execution_failed" = "Befehl {command} konnte nicht ausgeführt werden: {reason}"
usage = "Verwendung: {usage}"
[iris.command]
permission_denied = "Dir fehlt die Berechtigung '{permission}'"
+1 -1
View File
@@ -1,3 +1,4 @@
"language.editor.title" = "{plugin} › {section}"
# === Iris language guide ===
# Iris - es_ES
#
@@ -333,7 +334,6 @@ parameters_hover = "Pasa el cursor sobre los parámetros para saber más."
"error.default_parse_failed" = "No se puede analizar el valor predeterminado del parámetro {parameter}"
"error.missing_argument" = "Falta el argumento \"{parameter}\" ({type})"
"error.execution_failed" = "No se pudo ejecutar el comando {command}: {reason}"
usage = "Uso: {usage}"
[iris.command]
permission_denied = "No tienes el permiso '{permission}'"
+1 -1
View File
@@ -1,3 +1,4 @@
"language.editor.title" = "{plugin} › {section}"
# === Iris language guide ===
# Iris - fi_FI
#
@@ -338,7 +339,6 @@ parameters_hover = "Vie hiiri parametrien päälle saadaksesi lisätietoja."
"error.default_parse_failed" = "Parametrin {parameter} oletusarvoa ei voi jäsentää"
"error.missing_argument" = "Puuttuva argumentti \"{parameter}\" ({type})"
"error.execution_failed" = "Komennon {command} suoritus epäonnistui: {reason}"
usage = "Käyttö: {usage}"
[iris.command]
permission_denied = "Sinulta puuttuu käyttöoikeus '{permission}'"
+1 -1
View File
@@ -1,3 +1,4 @@
"language.editor.title" = "{plugin} › {section}"
# === Iris language guide ===
# Iris - fr_FR
#
@@ -333,7 +334,6 @@ parameters_hover = "Survolez les paramètres pour en savoir plus."
"error.default_parse_failed" = "Impossible d'analyser la valeur par défaut du paramètre {parameter}"
"error.missing_argument" = "Argument manquant \"{parameter}\" ({type})"
"error.execution_failed" = "Échec de l'exécution de la commande {command} : {reason}"
usage = "Utilisation: {usage}"
[iris.command]
permission_denied = "Vous n'avez pas la permission '{permission}'"
+1 -1
View File
@@ -1,3 +1,4 @@
"language.editor.title" = "{plugin} › {section}"
# === Iris language guide ===
# Iris - he_IL
#
@@ -338,7 +339,6 @@ parameters_hover = "רחפו מעל הפרמטרים כדי ללמוד עוד."
"error.default_parse_failed" = "לא ניתן לנתח את ערך ברירת המחדל של הפרמטר {parameter}"
"error.missing_argument" = "חסר ארגומנט \"{parameter}\" ({type})"
"error.execution_failed" = "הפעלת הפקודה {command} נכשלה: {reason}"
usage = "שימוש: {usage}"
[iris.command]
permission_denied = "אין לך את ההרשאה '{permission}'"
+1 -1
View File
@@ -1,3 +1,4 @@
"language.editor.title" = "{plugin} › {section}"
# === Iris language guide ===
# Iris - it_IT
#
@@ -333,7 +334,6 @@ parameters_hover = "Passa il cursore sui parametri per saperne di più."
"error.default_parse_failed" = "Impossibile analizzare il valore predefinito del parametro {parameter}"
"error.missing_argument" = "Argomento mancante \"{parameter}\" ({type})"
"error.execution_failed" = "Impossibile eseguire il comando {command}: {reason}"
usage = "Utilizzo: {usage}"
[iris.command]
permission_denied = "Non disponi del permesso '{permission}'"
+1 -1
View File
@@ -1,3 +1,4 @@
"language.editor.title" = "{plugin} › {section}"
# === Iris language guide ===
# Iris - ja-JP
#
@@ -336,7 +337,6 @@ parameters_hover = "パラメータにカーソルを合わせると詳細が表
"error.default_parse_failed" = "パラメーター {parameter} のデフォルト値を解析できません"
"error.missing_argument" = "引数 \"{parameter}\" ({type}) が必要です"
"error.execution_failed" = "コマンド {command} の実行に失敗しました: {reason}"
usage = "使用法: {usage}"
[iris.command]
permission_denied = "権限 '{permission}' がありません"
+1 -1
View File
@@ -1,3 +1,4 @@
"language.editor.title" = "{plugin} › {section}"
# === Iris language guide ===
# Iris - ko_KR
#
@@ -336,7 +337,6 @@ parameters_hover = "매개변수 위에 마우스를 올리면 자세한 내용
"error.default_parse_failed" = "매개변수 {parameter}의 기본값을 분석할 수 없습니다"
"error.missing_argument" = "인수 \"{parameter}\" ({type})가 필요합니다"
"error.execution_failed" = "명령어 {command} 실행에 실패했습니다: {reason}"
usage = "사용법: {usage}"
[iris.command]
permission_denied = "'{permission}' 권한이 없습니다"
+1 -1
View File
@@ -1,3 +1,4 @@
"language.editor.title" = "{plugin} › {section}"
# === Iris language guide ===
# Iris - lt_LT
#
@@ -338,7 +339,6 @@ parameters_hover = "Užveskite pelės žymeklį ant parametrų, kad sužinotumė
"error.default_parse_failed" = "Nepavyko nuskaityti parametro {parameter} numatytosios reikšmės"
"error.missing_argument" = "Trūksta argumento \"{parameter}\" ({type})"
"error.execution_failed" = "Nepavyko įvykdyti komandos {command}: {reason}"
usage = "Naudojimas: {usage}"
[iris.command]
permission_denied = "Jums trūksta leidimo '{permission}'"
+1 -1
View File
@@ -1,3 +1,4 @@
"language.editor.title" = "{plugin} › {section}"
# === Iris language guide ===
# Iris - nl_NL
#
@@ -333,7 +334,6 @@ parameters_hover = "Beweeg de muis over de parameters voor meer informatie."
"error.default_parse_failed" = "Kan de standaardwaarde voor parameter {parameter} niet verwerken"
"error.missing_argument" = "Ontbrekend argument \"{parameter}\" ({type})"
"error.execution_failed" = "Opdracht {command} kon niet worden uitgevoerd: {reason}"
usage = "Gebruik: {usage}"
[iris.command]
permission_denied = "Je hebt niet de vereiste permissie '{permission}'"
+1 -1
View File
@@ -1,3 +1,4 @@
"language.editor.title" = "{plugin} › {section}"
# === Iris language guide ===
# Iris - pl_PL
#
@@ -338,7 +339,6 @@ parameters_hover = "Najedź kursorem na parametry, aby dowiedzieć się więcej.
"error.default_parse_failed" = "Nie można odczytać wartości domyślnej parametru {parameter}"
"error.missing_argument" = "Brak argumentu \"{parameter}\" ({type})"
"error.execution_failed" = "Nie udało się wykonać polecenia {command}: {reason}"
usage = "Użycie: {usage}"
[iris.command]
permission_denied = "Nie masz uprawnienia '{permission}'"
+1 -1
View File
@@ -1,3 +1,4 @@
"language.editor.title" = "{plugin} › {section}"
# === Iris language guide ===
# Iris - pt_PT
#
@@ -333,7 +334,6 @@ parameters_hover = "Passe o cursor sobre os parâmetros para saber mais."
"error.default_parse_failed" = "Não foi possível interpretar o valor predefinido do parâmetro {parameter}"
"error.missing_argument" = "Falta o argumento \"{parameter}\" ({type})"
"error.execution_failed" = "Falha ao executar o comando {command}: {reason}"
usage = "Utilização: {usage}"
[iris.command]
permission_denied = "Não tem a permissão '{permission}'"
+1 -1
View File
@@ -1,3 +1,4 @@
"language.editor.title" = "{plugin} › {section}"
# === Iris language guide ===
# Iris - ru_RU
#
@@ -338,7 +339,6 @@ parameters_hover = "Наведите курсор на параметры, чт
"error.default_parse_failed" = "Невозможно разобрать значение по умолчанию параметра {parameter}"
"error.missing_argument" = "Отсутствует аргумент \"{parameter}\" ({type})"
"error.execution_failed" = "Не удалось выполнить команду {command}: {reason}"
usage = "Использование: {usage}"
[iris.command]
permission_denied = "У вас нет разрешения '{permission}'"
+1 -1
View File
@@ -1,3 +1,4 @@
"language.editor.title" = "{plugin} › {section}"
# === Iris language guide ===
# Iris - tr_TR
#
@@ -338,7 +339,6 @@ parameters_hover = "Daha fazla bilgi için imleci parametrelerin üzerine getiri
"error.default_parse_failed" = "{parameter} parametresinin varsayılan değeri ayrıştırılamıyor"
"error.missing_argument" = "Eksik bağımsız değişken \"{parameter}\" ({type})"
"error.execution_failed" = "{command} komutu çalıştırılamadı: {reason}"
usage = "Kullanım: {usage}"
[iris.command]
permission_denied = "'{permission}' iznine sahip değilsiniz"
+1 -1
View File
@@ -1,3 +1,4 @@
"language.editor.title" = "{plugin} › {section}"
# === Iris language guide ===
# Iris - vi_VI
#
@@ -336,7 +337,6 @@ parameters_hover = "Di chuột qua các tham số để tìm hiểu thêm."
"error.default_parse_failed" = "Không thể phân tích giá trị mặc định của tham số {parameter}"
"error.missing_argument" = "Thiếu đối số \"{parameter}\" ({type})"
"error.execution_failed" = "Không thể thực thi lệnh {command}: {reason}"
usage = "Cách dùng: {usage}"
[iris.command]
permission_denied = "Bạn không có quyền '{permission}'"
+1 -1
View File
@@ -1,3 +1,4 @@
"language.editor.title" = "{plugin} › {section}"
# === Iris language guide ===
# Iris - zh_CN
#
@@ -336,7 +337,6 @@ parameters_hover = "将鼠标悬停在参数上以了解更多。"
"error.default_parse_failed" = "无法解析参数 {parameter} 的默认值"
"error.missing_argument" = "缺少参数 \"{parameter}\" ({type})"
"error.execution_failed" = "执行命令 {command} 失败: {reason}"
usage = "用法: {usage}"
[iris.command]
permission_denied = "你没有权限 '{permission}'。"
+1 -1
View File
@@ -1,3 +1,4 @@
"language.editor.title" = "{plugin} › {section}"
# === Iris language guide ===
# Iris - zh_TW
#
@@ -336,7 +337,6 @@ parameters_hover = "將滑鼠懸停在參數上以瞭解更多。"
"error.default_parse_failed" = "無法解析參數 {parameter} 的預設值"
"error.missing_argument" = "缺少參數 \"{parameter}\" ({type})"
"error.execution_failed" = "執行指令 {command} 失敗: {reason}"
usage = "用法: {usage}"
[iris.command]
permission_denied = "你沒有權限 '{permission}'。"
@@ -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";