diff --git a/core/src/main/java/art/arcane/iris/localization/IrisLanguage.java b/core/src/main/java/art/arcane/iris/localization/IrisLanguage.java index 1a64b2079..f005fc552 100644 --- a/core/src/main/java/art/arcane/iris/localization/IrisLanguage.java +++ b/core/src/main/java/art/arcane/iris/localization/IrisLanguage.java @@ -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 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 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); } } diff --git a/core/src/main/resources/languages/de_DE.toml b/core/src/main/resources/languages/de_DE.toml index 0ddb8f08b..cda10dde4 100644 --- a/core/src/main/resources/languages/de_DE.toml +++ b/core/src/main/resources/languages/de_DE.toml @@ -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}'" diff --git a/core/src/main/resources/languages/es_ES.toml b/core/src/main/resources/languages/es_ES.toml index a1f716c5a..241a93324 100644 --- a/core/src/main/resources/languages/es_ES.toml +++ b/core/src/main/resources/languages/es_ES.toml @@ -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}'" diff --git a/core/src/main/resources/languages/fi_FI.toml b/core/src/main/resources/languages/fi_FI.toml index 24fa31627..f0a1981a3 100644 --- a/core/src/main/resources/languages/fi_FI.toml +++ b/core/src/main/resources/languages/fi_FI.toml @@ -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}'" diff --git a/core/src/main/resources/languages/fr_FR.toml b/core/src/main/resources/languages/fr_FR.toml index 09f246b59..6de0596ea 100644 --- a/core/src/main/resources/languages/fr_FR.toml +++ b/core/src/main/resources/languages/fr_FR.toml @@ -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}'" diff --git a/core/src/main/resources/languages/he_IL.toml b/core/src/main/resources/languages/he_IL.toml index a6d4646a3..e3218c83e 100644 --- a/core/src/main/resources/languages/he_IL.toml +++ b/core/src/main/resources/languages/he_IL.toml @@ -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}'" diff --git a/core/src/main/resources/languages/it_IT.toml b/core/src/main/resources/languages/it_IT.toml index 09e701923..136592a2e 100644 --- a/core/src/main/resources/languages/it_IT.toml +++ b/core/src/main/resources/languages/it_IT.toml @@ -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}'" diff --git a/core/src/main/resources/languages/ja-JP.toml b/core/src/main/resources/languages/ja-JP.toml index e07683bf3..af11d0285 100644 --- a/core/src/main/resources/languages/ja-JP.toml +++ b/core/src/main/resources/languages/ja-JP.toml @@ -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}' がありません" diff --git a/core/src/main/resources/languages/ko_KR.toml b/core/src/main/resources/languages/ko_KR.toml index 84e7aaed3..13f6b4d0c 100644 --- a/core/src/main/resources/languages/ko_KR.toml +++ b/core/src/main/resources/languages/ko_KR.toml @@ -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}' 권한이 없습니다" diff --git a/core/src/main/resources/languages/lt_LT.toml b/core/src/main/resources/languages/lt_LT.toml index d26e4eb95..383a045dd 100644 --- a/core/src/main/resources/languages/lt_LT.toml +++ b/core/src/main/resources/languages/lt_LT.toml @@ -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}'" diff --git a/core/src/main/resources/languages/nl_NL.toml b/core/src/main/resources/languages/nl_NL.toml index c6968a19c..81899f0ca 100644 --- a/core/src/main/resources/languages/nl_NL.toml +++ b/core/src/main/resources/languages/nl_NL.toml @@ -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}'" diff --git a/core/src/main/resources/languages/pl_PL.toml b/core/src/main/resources/languages/pl_PL.toml index d1590e8af..a0eefc734 100644 --- a/core/src/main/resources/languages/pl_PL.toml +++ b/core/src/main/resources/languages/pl_PL.toml @@ -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}'" diff --git a/core/src/main/resources/languages/pt_PT.toml b/core/src/main/resources/languages/pt_PT.toml index 7e070655f..f9e7fcba7 100644 --- a/core/src/main/resources/languages/pt_PT.toml +++ b/core/src/main/resources/languages/pt_PT.toml @@ -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}'" diff --git a/core/src/main/resources/languages/ru_RU.toml b/core/src/main/resources/languages/ru_RU.toml index 10695490f..0ca3028c9 100644 --- a/core/src/main/resources/languages/ru_RU.toml +++ b/core/src/main/resources/languages/ru_RU.toml @@ -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}'" diff --git a/core/src/main/resources/languages/tr_TR.toml b/core/src/main/resources/languages/tr_TR.toml index 00f7e8a7b..08cc296c5 100644 --- a/core/src/main/resources/languages/tr_TR.toml +++ b/core/src/main/resources/languages/tr_TR.toml @@ -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" diff --git a/core/src/main/resources/languages/vi_VI.toml b/core/src/main/resources/languages/vi_VI.toml index 4ca67848b..25e9a75d3 100644 --- a/core/src/main/resources/languages/vi_VI.toml +++ b/core/src/main/resources/languages/vi_VI.toml @@ -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}'" diff --git a/core/src/main/resources/languages/zh_CN.toml b/core/src/main/resources/languages/zh_CN.toml index cc5704227..96c03751b 100644 --- a/core/src/main/resources/languages/zh_CN.toml +++ b/core/src/main/resources/languages/zh_CN.toml @@ -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}'。" diff --git a/core/src/main/resources/languages/zh_TW.toml b/core/src/main/resources/languages/zh_TW.toml index 1b89cd69b..2ddb24a2d 100644 --- a/core/src/main/resources/languages/zh_TW.toml +++ b/core/src/main/resources/languages/zh_TW.toml @@ -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}'。" diff --git a/core/src/test/java/art/arcane/iris/configuration/SettingsHotloadWatchTest.java b/core/src/test/java/art/arcane/iris/configuration/SettingsHotloadWatchTest.java index 80ba0b46e..b3da94385 100644 --- a/core/src/test/java/art/arcane/iris/configuration/SettingsHotloadWatchTest.java +++ b/core/src/test/java/art/arcane/iris/configuration/SettingsHotloadWatchTest.java @@ -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) { diff --git a/core/src/test/java/art/arcane/iris/localization/IrisLanguageTest.java b/core/src/test/java/art/arcane/iris/localization/IrisLanguageTest.java index 14c981276..e90a72790 100644 --- a/core/src/test/java/art/arcane/iris/localization/IrisLanguageTest.java +++ b/core/src/test/java/art/arcane/iris/localization/IrisLanguageTest.java @@ -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";