mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 21:40:05 +00:00
Download translations on demand instead of bundling them
Locales are fetched from the Iris repository at runtime into languages/downloaded and cached against the revision the jar was built from; the artifact ships only a manifest of the available locales. IrisLanguage gains a player language service, per-audience snapshots and an in-game message editor that writes locale overrides. The Bukkit artifact verifier now fails when any locale file is embedded, and the modded shadow jars drop the Bukkit-only switcher and editor classes.
This commit is contained in:
10 files changed
+472
-191
No files matched your search
@@ -260,6 +260,9 @@ tasks.named('shadowJar', ShadowJar).configure {
|
|||||||
// archive as versioned and re-scan it per release directory for nothing.
|
// archive as versioned and re-scan it per release directory for nothing.
|
||||||
exclude('META-INF/versions/**')
|
exclude('META-INF/versions/**')
|
||||||
addMultiReleaseAttribute.set(false)
|
addMultiReleaseAttribute.set(false)
|
||||||
|
exclude('art/arcane/volmlib/util/localization/BukkitLanguageSwitcher*.class')
|
||||||
|
exclude('art/arcane/volmlib/util/localization/BukkitLanguageEditor*.class')
|
||||||
|
exclude('art/arcane/volmlib/util/localization/BukkitVolmitCommand*.class')
|
||||||
// Invalid package name on Java 9+ ('enum' is a keyword); a module-path scan chokes on it.
|
// Invalid package name on Java 9+ ('enum' is a keyword); a module-path scan chokes on it.
|
||||||
exclude('org/apache/commons/lang/enum/**')
|
exclude('org/apache/commons/lang/enum/**')
|
||||||
// Compile-only annotation carriers. Shipping them duplicates packages other mods also ship.
|
// Compile-only annotation carriers. Shipping them duplicates packages other mods also ship.
|
||||||
|
|||||||
@@ -252,6 +252,9 @@ tasks.named('shadowJar', ShadowJar).configure {
|
|||||||
// archive as versioned and re-scan it per release directory for nothing.
|
// archive as versioned and re-scan it per release directory for nothing.
|
||||||
exclude('META-INF/versions/**')
|
exclude('META-INF/versions/**')
|
||||||
addMultiReleaseAttribute.set(false)
|
addMultiReleaseAttribute.set(false)
|
||||||
|
exclude('art/arcane/volmlib/util/localization/BukkitLanguageSwitcher*.class')
|
||||||
|
exclude('art/arcane/volmlib/util/localization/BukkitLanguageEditor*.class')
|
||||||
|
exclude('art/arcane/volmlib/util/localization/BukkitVolmitCommand*.class')
|
||||||
// Invalid package name on Java 9+ ('enum' is a keyword); a module-path scan chokes on it.
|
// Invalid package name on Java 9+ ('enum' is a keyword); a module-path scan chokes on it.
|
||||||
exclude('org/apache/commons/lang/enum/**')
|
exclude('org/apache/commons/lang/enum/**')
|
||||||
// Compile-only annotation carriers. Shipping them duplicates packages other mods also ship.
|
// Compile-only annotation carriers. Shipping them duplicates packages other mods also ship.
|
||||||
|
|||||||
@@ -228,6 +228,9 @@ tasks.named('shadowJar', ShadowJar).configure {
|
|||||||
// archive as versioned and re-scan it per release directory for nothing.
|
// archive as versioned and re-scan it per release directory for nothing.
|
||||||
exclude('META-INF/versions/**')
|
exclude('META-INF/versions/**')
|
||||||
addMultiReleaseAttribute.set(false)
|
addMultiReleaseAttribute.set(false)
|
||||||
|
exclude('art/arcane/volmlib/util/localization/BukkitLanguageSwitcher*.class')
|
||||||
|
exclude('art/arcane/volmlib/util/localization/BukkitLanguageEditor*.class')
|
||||||
|
exclude('art/arcane/volmlib/util/localization/BukkitVolmitCommand*.class')
|
||||||
// Invalid package name on Java 9+ ('enum' is a keyword); a module-path scan chokes on it.
|
// Invalid package name on Java 9+ ('enum' is a keyword); a module-path scan chokes on it.
|
||||||
exclude('org/apache/commons/lang/enum/**')
|
exclude('org/apache/commons/lang/enum/**')
|
||||||
// Compile-only annotation carriers. Shipping them duplicates packages other mods also ship.
|
// Compile-only annotation carriers. Shipping them duplicates packages other mods also ship.
|
||||||
|
|||||||
+1
-58
@@ -3,8 +3,6 @@ import org.gradle.api.tasks.Copy
|
|||||||
import org.gradle.api.tasks.compile.JavaCompile
|
import org.gradle.api.tasks.compile.JavaCompile
|
||||||
import org.gradle.jvm.tasks.Jar
|
import org.gradle.jvm.tasks.Jar
|
||||||
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||||
import groovy.json.JsonOutput
|
|
||||||
import groovy.json.JsonSlurper
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||||
@@ -147,16 +145,6 @@ nmsBindings.each { key, value ->
|
|||||||
|
|
||||||
def included = configurations.create('included')
|
def included = configurations.create('included')
|
||||||
def jarJar = configurations.create('jarJar')
|
def jarJar = configurations.create('jarJar')
|
||||||
def bukkitLanguagesDirectory = layout.buildDirectory.dir('generated/bukkit-languages')
|
|
||||||
Set<String> sharedNonEnglishBukkitMessages = [
|
|
||||||
'iris.desktop.vision.fps',
|
|
||||||
'iris.desktop.vision.tiles',
|
|
||||||
'iris.bukkit.runtime.commandpack.message_2',
|
|
||||||
'iris.bukkit.commanddatapack.message_2',
|
|
||||||
'iris.runtime.chunk_job.bossbar.progress',
|
|
||||||
'iris.runtime.studio.action.progress',
|
|
||||||
'iris.runtime.chunk_job.action.progress'
|
|
||||||
] as Set
|
|
||||||
dependencies {
|
dependencies {
|
||||||
nmsBindings.keySet().each { key ->
|
nmsBindings.keySet().each { key ->
|
||||||
add('included', project(path: ":adapters:bukkit:nms:${key}", configuration: 'runtimeElements'))
|
add('included', project(path: ":adapters:bukkit:nms:${key}", configuration: 'runtimeElements'))
|
||||||
@@ -166,48 +154,6 @@ dependencies {
|
|||||||
add('jarJar', project(':core:agent'))
|
add('jarJar', project(':core:agent'))
|
||||||
}
|
}
|
||||||
|
|
||||||
def prepareBukkitLanguages = tasks.register('prepareBukkitLanguages') {
|
|
||||||
inputs.files(fileTree(project(':core').file('src/main/resources/languages')) {
|
|
||||||
include('*.json')
|
|
||||||
})
|
|
||||||
outputs.dir(bukkitLanguagesDirectory)
|
|
||||||
doLast {
|
|
||||||
File outputDirectory = bukkitLanguagesDirectory.get().asFile
|
|
||||||
delete(outputDirectory)
|
|
||||||
outputDirectory.mkdirs()
|
|
||||||
List<File> sources = fileTree(project(':core').file('src/main/resources/languages')) {
|
|
||||||
include('*.json')
|
|
||||||
}.files.sort { File left, File right -> left.name <=> right.name }
|
|
||||||
List<Map<String, Object>> catalogs = sources.collect { File source ->
|
|
||||||
(Map<String, Object>) new JsonSlurper().parse(source)
|
|
||||||
}
|
|
||||||
Map<String, Object> referenceMessages = (Map<String, Object>) catalogs.first().get('messages')
|
|
||||||
List<String> messageIds = referenceMessages.keySet()
|
|
||||||
.findAll { String key -> !key.startsWith('iris.modded.') }
|
|
||||||
.sort()
|
|
||||||
Set<String> sharedFallbackIds = new LinkedHashSet<>(messageIds)
|
|
||||||
catalogs.each { Map<String, Object> catalog ->
|
|
||||||
Map<String, Object> messages = (Map<String, Object>) catalog.get('messages')
|
|
||||||
if (!messages.keySet().containsAll(messageIds)) {
|
|
||||||
throw new GradleException("Bundled locale ${catalog.get('locale')} is missing Bukkit messages")
|
|
||||||
}
|
|
||||||
sharedFallbackIds.removeIf { String key -> messages.get(key) != referenceMessages.get(key) }
|
|
||||||
}
|
|
||||||
sharedFallbackIds.removeAll(sharedNonEnglishBukkitMessages)
|
|
||||||
|
|
||||||
sources.eachWithIndex { File source, int index ->
|
|
||||||
Map<String, Object> catalog = catalogs.get(index)
|
|
||||||
Map<String, Object> messages = (Map<String, Object>) catalog.get('messages')
|
|
||||||
List<Object> compactMessages = messageIds.collect { String key ->
|
|
||||||
sharedFallbackIds.contains(key) ? null : messages.get(key)
|
|
||||||
}
|
|
||||||
new File(outputDirectory, source.name).setText(
|
|
||||||
JsonOutput.toJson([catalog.get('locale'), compactMessages]),
|
|
||||||
'UTF-8')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named('jar', Jar).configure {
|
tasks.named('jar', Jar).configure {
|
||||||
inputs.files(included)
|
inputs.files(included)
|
||||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
@@ -216,9 +162,6 @@ tasks.named('jar', Jar).configure {
|
|||||||
from(provider { included.resolve().collect { zipTree(it) } }) {
|
from(provider { included.resolve().collect { zipTree(it) } }) {
|
||||||
exclude('languages/**')
|
exclude('languages/**')
|
||||||
}
|
}
|
||||||
from(prepareBukkitLanguages) {
|
|
||||||
into('languages')
|
|
||||||
}
|
|
||||||
doFirst {
|
doFirst {
|
||||||
delete(layout.buildDirectory.file("libs/Iris-${project.version}.jar"))
|
delete(layout.buildDirectory.file("libs/Iris-${project.version}.jar"))
|
||||||
}
|
}
|
||||||
@@ -591,6 +534,7 @@ List<String> requiredBukkitArtifactEntries = [
|
|||||||
'paper-plugin.yml',
|
'paper-plugin.yml',
|
||||||
'slimjar.dat',
|
'slimjar.dat',
|
||||||
'slimjar-resolutions.dat',
|
'slimjar-resolutions.dat',
|
||||||
|
'iris-language-source.properties',
|
||||||
'agent.jar',
|
'agent.jar',
|
||||||
'art/arcane/iris/Iris.class',
|
'art/arcane/iris/Iris.class',
|
||||||
'art/arcane/iris/IrisBootstrap.class',
|
'art/arcane/iris/IrisBootstrap.class',
|
||||||
@@ -618,7 +562,6 @@ tasks.register('verifyBukkitArtifact') {
|
|||||||
BukkitArtifactVerifier.verify(
|
BukkitArtifactVerifier.verify(
|
||||||
artifact,
|
artifact,
|
||||||
requiredBukkitArtifactEntries,
|
requiredBukkitArtifactEntries,
|
||||||
17,
|
|
||||||
16,
|
16,
|
||||||
maximumBukkitArtifactBytes)
|
maximumBukkitArtifactBytes)
|
||||||
logger.lifecycle("Verified ${artifact.name} packaging and class reference graph")
|
logger.lifecycle("Verified ${artifact.name} packaging and class reference graph")
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ public final class BukkitArtifactVerifier {
|
|||||||
private BukkitArtifactVerifier() {
|
private BukkitArtifactVerifier() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void verify(File artifact, List<String> requiredEntries, int minimumLocales,
|
public static void verify(File artifact, List<String> requiredEntries,
|
||||||
int minimumMatterSlices, long maximumArtifactBytes) {
|
int minimumMatterSlices, long maximumArtifactBytes) {
|
||||||
if (!artifact.isFile()) {
|
if (!artifact.isFile()) {
|
||||||
throw new GradleException("Missing Bukkit Iris artifact: " + artifact.getAbsolutePath());
|
throw new GradleException("Missing Bukkit Iris artifact: " + artifact.getAbsolutePath());
|
||||||
@@ -92,9 +92,8 @@ public final class BukkitArtifactVerifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (locales < minimumLocales) {
|
if (locales != 0) {
|
||||||
throw new GradleException(artifact.getName() + " ships " + locales + " locale files; expected at least "
|
throw new GradleException(artifact.getName() + " ships " + locales + " locale files; translations must download on demand");
|
||||||
+ minimumLocales);
|
|
||||||
}
|
}
|
||||||
// Matter.read() resolves slice types from the canonical name stored in the payload.
|
// Matter.read() resolves slice types from the canonical name stored in the payload.
|
||||||
if (matterSlices < minimumMatterSlices) {
|
if (matterSlices < minimumMatterSlices) {
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ public class BukkitArtifactVerifierTest {
|
|||||||
SLIMJAR_RESOLUTIONS,
|
SLIMJAR_RESOLUTIONS,
|
||||||
NMS_BINDING + ".class"
|
NMS_BINDING + ".class"
|
||||||
);
|
);
|
||||||
private static final int LOCALES = 2;
|
|
||||||
private static final int MATTER_SLICES = 1;
|
private static final int MATTER_SLICES = 1;
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
@@ -39,7 +38,7 @@ public class BukkitArtifactVerifierTest {
|
|||||||
public void acceptsCompleteArtifact() throws Exception {
|
public void acceptsCompleteArtifact() throws Exception {
|
||||||
File artifact = createArtifact(validEntries());
|
File artifact = createArtifact(validEntries());
|
||||||
|
|
||||||
BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE);
|
BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, MATTER_SLICES, Long.MAX_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -49,7 +48,7 @@ public class BukkitArtifactVerifierTest {
|
|||||||
File artifact = createArtifact(entries);
|
File artifact = createArtifact(entries);
|
||||||
|
|
||||||
GradleException failure = assertThrows(GradleException.class,
|
GradleException failure = assertThrows(GradleException.class,
|
||||||
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE));
|
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, MATTER_SLICES, Long.MAX_VALUE));
|
||||||
assertTrue(failure.getMessage().contains(PLUGIN_DESCRIPTOR));
|
assertTrue(failure.getMessage().contains(PLUGIN_DESCRIPTOR));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +59,7 @@ public class BukkitArtifactVerifierTest {
|
|||||||
File artifact = createArtifact(entries);
|
File artifact = createArtifact(entries);
|
||||||
|
|
||||||
GradleException failure = assertThrows(GradleException.class,
|
GradleException failure = assertThrows(GradleException.class,
|
||||||
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE));
|
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, MATTER_SLICES, Long.MAX_VALUE));
|
||||||
assertTrue(failure.getMessage().contains(SLIMJAR_DEPENDENCIES));
|
assertTrue(failure.getMessage().contains(SLIMJAR_DEPENDENCIES));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +70,7 @@ public class BukkitArtifactVerifierTest {
|
|||||||
File artifact = createArtifact(entries);
|
File artifact = createArtifact(entries);
|
||||||
|
|
||||||
GradleException failure = assertThrows(GradleException.class,
|
GradleException failure = assertThrows(GradleException.class,
|
||||||
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE));
|
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, MATTER_SLICES, Long.MAX_VALUE));
|
||||||
assertTrue(failure.getMessage().contains(SLIMJAR_RESOLUTIONS));
|
assertTrue(failure.getMessage().contains(SLIMJAR_RESOLUTIONS));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +81,7 @@ public class BukkitArtifactVerifierTest {
|
|||||||
File artifact = createArtifact(entries);
|
File artifact = createArtifact(entries);
|
||||||
|
|
||||||
GradleException failure = assertThrows(GradleException.class,
|
GradleException failure = assertThrows(GradleException.class,
|
||||||
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE));
|
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, MATTER_SLICES, Long.MAX_VALUE));
|
||||||
assertTrue(failure.getMessage().contains("art/arcane/volmlib/util/noise/CNG"));
|
assertTrue(failure.getMessage().contains("art/arcane/volmlib/util/noise/CNG"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +100,7 @@ public class BukkitArtifactVerifierTest {
|
|||||||
classReferencing("art/arcane/iris/ParalithicConsumer", "art/arcane/iris/util/paralithic/functions/Function"));
|
classReferencing("art/arcane/iris/ParalithicConsumer", "art/arcane/iris/util/paralithic/functions/Function"));
|
||||||
File artifact = createArtifact(entries);
|
File artifact = createArtifact(entries);
|
||||||
|
|
||||||
BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE);
|
BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, MATTER_SLICES, Long.MAX_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -112,20 +111,19 @@ public class BukkitArtifactVerifierTest {
|
|||||||
() -> BukkitArtifactVerifier.verify(
|
() -> BukkitArtifactVerifier.verify(
|
||||||
artifact,
|
artifact,
|
||||||
REQUIRED_ENTRIES,
|
REQUIRED_ENTRIES,
|
||||||
LOCALES,
|
|
||||||
MATTER_SLICES,
|
MATTER_SLICES,
|
||||||
artifact.length() - 1L));
|
artifact.length() - 1L));
|
||||||
assertTrue(failure.getMessage().contains("must not exceed"));
|
assertTrue(failure.getMessage().contains("must not exceed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void rejectsDroppedLocale() throws Exception {
|
public void rejectsEmbeddedLocale() throws Exception {
|
||||||
Map<String, byte[]> entries = validEntries();
|
Map<String, byte[]> entries = validEntries();
|
||||||
entries.remove("languages/de_DE.json");
|
entries.put("languages/de_DE.json", "{}".getBytes(StandardCharsets.UTF_8));
|
||||||
File artifact = createArtifact(entries);
|
File artifact = createArtifact(entries);
|
||||||
|
|
||||||
GradleException failure = assertThrows(GradleException.class,
|
GradleException failure = assertThrows(GradleException.class,
|
||||||
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE));
|
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, MATTER_SLICES, Long.MAX_VALUE));
|
||||||
assertTrue(failure.getMessage().contains("locale files"));
|
assertTrue(failure.getMessage().contains("locale files"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +134,7 @@ public class BukkitArtifactVerifierTest {
|
|||||||
File artifact = createArtifact(entries);
|
File artifact = createArtifact(entries);
|
||||||
|
|
||||||
GradleException failure = assertThrows(GradleException.class,
|
GradleException failure = assertThrows(GradleException.class,
|
||||||
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE));
|
() -> BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, MATTER_SLICES, Long.MAX_VALUE));
|
||||||
assertTrue(failure.getMessage().contains("Matter slice types"));
|
assertTrue(failure.getMessage().contains("Matter slice types"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +145,7 @@ public class BukkitArtifactVerifierTest {
|
|||||||
classAnnotatedWith("art/arcane/iris/Annotated", "com/google/errorprone/annotations/CanIgnoreReturnValue"));
|
classAnnotatedWith("art/arcane/iris/Annotated", "com/google/errorprone/annotations/CanIgnoreReturnValue"));
|
||||||
File artifact = createArtifact(entries);
|
File artifact = createArtifact(entries);
|
||||||
|
|
||||||
BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, LOCALES, MATTER_SLICES, Long.MAX_VALUE);
|
BukkitArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, MATTER_SLICES, Long.MAX_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, byte[]> validEntries() {
|
private Map<String, byte[]> validEntries() {
|
||||||
@@ -155,8 +153,7 @@ public class BukkitArtifactVerifierTest {
|
|||||||
entries.put(PLUGIN_DESCRIPTOR, "name: Iris\n".getBytes(StandardCharsets.UTF_8));
|
entries.put(PLUGIN_DESCRIPTOR, "name: Iris\n".getBytes(StandardCharsets.UTF_8));
|
||||||
entries.put(SLIMJAR_DEPENDENCIES, new byte[]{1});
|
entries.put(SLIMJAR_DEPENDENCIES, new byte[]{1});
|
||||||
entries.put(SLIMJAR_RESOLUTIONS, new byte[]{1});
|
entries.put(SLIMJAR_RESOLUTIONS, new byte[]{1});
|
||||||
entries.put("languages/de_DE.json", "{}".getBytes(StandardCharsets.UTF_8));
|
|
||||||
entries.put("languages/fr_FR.json", "{}".getBytes(StandardCharsets.UTF_8));
|
|
||||||
entries.put(NMS_BINDING + ".class",
|
entries.put(NMS_BINDING + ".class",
|
||||||
classReferencing(NMS_BINDING, "art/arcane/volmlib/util/noise/CNG"));
|
classReferencing(NMS_BINDING, "art/arcane/volmlib/util/noise/CNG"));
|
||||||
entries.put("art/arcane/volmlib/util/noise/CNG.class", emptyClass("art/arcane/volmlib/util/noise/CNG"));
|
entries.put("art/arcane/volmlib/util/noise/CNG.class", emptyClass("art/arcane/volmlib/util/noise/CNG"));
|
||||||
|
|||||||
+20
-1
@@ -38,6 +38,24 @@ String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
|
|||||||
.orElse('com.github.VolmitSoftware:VolmLib:e6574cf814b3dd670385a4d632a63fde2038e186')
|
.orElse('com.github.VolmitSoftware:VolmLib:e6574cf814b3dd670385a4d632a63fde2038e186')
|
||||||
.get()
|
.get()
|
||||||
|
|
||||||
|
String languageSourceReference = providers.gradleProperty('languageRevision').orElse('master').get()
|
||||||
|
List<File> languageSourceFiles = fileTree('src/main/resources/languages') {
|
||||||
|
include('*.json')
|
||||||
|
}.files.toList().sort { File left, File right -> left.name <=> right.name }
|
||||||
|
List<String> languageSourceLocales = languageSourceFiles.collect { File source ->
|
||||||
|
source.name.substring(0, source.name.length() - '.json'.length())
|
||||||
|
}
|
||||||
|
def languageSourceManifestTask = tasks.register('generateLanguageSourceManifest', WriteProperties) {
|
||||||
|
destinationFile.set(layout.buildDirectory.file('generated/language-source/iris-language-source.properties'))
|
||||||
|
property('revision', languageSourceReference)
|
||||||
|
property('locales', languageSourceLocales.join(','))
|
||||||
|
}
|
||||||
|
tasks.named('processResources').configure {
|
||||||
|
dependsOn(languageSourceManifestTask)
|
||||||
|
exclude('languages/**')
|
||||||
|
from(languageSourceManifestTask)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dependencies.
|
* Dependencies.
|
||||||
*
|
*
|
||||||
@@ -218,7 +236,8 @@ tasks.named('compileJava', JavaCompile).configure {
|
|||||||
|
|
||||||
tasks.named('test', Test).configure {
|
tasks.named('test', Test).configure {
|
||||||
jvmArgs('--add-modules', 'jdk.incubator.vector')
|
jvmArgs('--add-modules', 'jdk.incubator.vector')
|
||||||
classpath = files('src/main/resources') + classpath
|
dependsOn(languageSourceManifestTask)
|
||||||
|
classpath = files(layout.buildDirectory.dir('generated/language-source')) + files('src/main/resources') + classpath
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations.matching { it.name.startsWith('slim') }.all { }
|
configurations.matching { it.name.startsWith('slim') }.all { }
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ import art.arcane.iris.spi.IrisLogging;
|
|||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.volmlib.util.director.DirectorTextResolver;
|
import art.arcane.volmlib.util.director.DirectorTextResolver;
|
||||||
import art.arcane.volmlib.util.localization.LinesKey;
|
import art.arcane.volmlib.util.localization.LinesKey;
|
||||||
|
import art.arcane.volmlib.util.localization.LanguageAudience;
|
||||||
|
import art.arcane.volmlib.util.localization.LanguageFileEditor;
|
||||||
|
import art.arcane.volmlib.util.localization.PluginLanguageEditor;
|
||||||
|
import art.arcane.volmlib.util.localization.PluginLanguageService;
|
||||||
|
import art.arcane.volmlib.util.localization.RemoteLanguageCatalog;
|
||||||
|
import art.arcane.volmlib.util.localization.LocalizationValidator;
|
||||||
import art.arcane.volmlib.util.localization.LocaleOverlay;
|
import art.arcane.volmlib.util.localization.LocaleOverlay;
|
||||||
import art.arcane.volmlib.util.localization.LocalizationCandidate;
|
import art.arcane.volmlib.util.localization.LocalizationCandidate;
|
||||||
import art.arcane.volmlib.util.localization.LocalizationIssue;
|
import art.arcane.volmlib.util.localization.LocalizationIssue;
|
||||||
@@ -16,20 +22,32 @@ import art.arcane.volmlib.util.localization.MessageArgumentKind;
|
|||||||
import art.arcane.volmlib.util.localization.MessageArgs;
|
import art.arcane.volmlib.util.localization.MessageArgs;
|
||||||
import art.arcane.volmlib.util.localization.MessageCatalog;
|
import art.arcane.volmlib.util.localization.MessageCatalog;
|
||||||
import art.arcane.volmlib.util.localization.MessageKey;
|
import art.arcane.volmlib.util.localization.MessageKey;
|
||||||
|
import art.arcane.volmlib.util.localization.MessageValue;
|
||||||
|
import art.arcane.volmlib.util.localization.LinesValue;
|
||||||
|
import art.arcane.volmlib.util.localization.PluralValue;
|
||||||
|
import art.arcane.volmlib.util.localization.TextValue;
|
||||||
import art.arcane.volmlib.util.localization.PluralKey;
|
import art.arcane.volmlib.util.localization.PluralKey;
|
||||||
import art.arcane.volmlib.util.localization.PluralSelector;
|
import art.arcane.volmlib.util.localization.PluralSelector;
|
||||||
import art.arcane.volmlib.util.localization.ResolvedLines;
|
import art.arcane.volmlib.util.localization.ResolvedLines;
|
||||||
import art.arcane.volmlib.util.localization.ResolvedText;
|
import art.arcane.volmlib.util.localization.ResolvedText;
|
||||||
import art.arcane.volmlib.util.localization.TextKey;
|
import art.arcane.volmlib.util.localization.TextKey;
|
||||||
import art.arcane.volmlib.util.localization.VolmitLocales;
|
|
||||||
import com.google.gson.JsonArray;
|
import com.google.gson.JsonArray;
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import com.google.gson.JsonParser;
|
import com.google.gson.JsonParser;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import com.google.gson.Gson;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.charset.CharacterCodingException;
|
import java.nio.charset.CharacterCodingException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@@ -51,15 +69,12 @@ import java.util.function.BiConsumer;
|
|||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public final class IrisLanguage {
|
public final class IrisLanguage {
|
||||||
|
private static final Object SNAPSHOT_LOCK = new Object();
|
||||||
private static final long MAX_LOCALE_BYTES = 2L * 1024L * 1024L;
|
private static final long MAX_LOCALE_BYTES = 2L * 1024L * 1024L;
|
||||||
private static final int MAX_REPORTED_ISSUES = 12;
|
private static final int MAX_REPORTED_ISSUES = 12;
|
||||||
private static final Pattern LOCALE_NAME = Pattern.compile("[A-Za-z0-9_-]+");
|
private static final Pattern LOCALE_NAME = Pattern.compile("[A-Za-z0-9_-]+");
|
||||||
private static final Pattern LEGACY_COLOR = Pattern.compile("(?i)\\u00a7[0-9A-FK-ORX]");
|
private static final Pattern LEGACY_COLOR = Pattern.compile("(?i)\\u00a7[0-9A-FK-ORX]");
|
||||||
private static final MessageCatalog CATALOG = IrisMessages.catalog();
|
private static final MessageCatalog CATALOG = IrisMessages.catalog();
|
||||||
private static final List<String> BUKKIT_MESSAGE_IDS = CATALOG.ids().stream()
|
|
||||||
.filter(id -> !id.startsWith("iris.modded."))
|
|
||||||
.sorted()
|
|
||||||
.toList();
|
|
||||||
private static final LocalizationManager MANAGER = new LocalizationManager(
|
private static final LocalizationManager MANAGER = new LocalizationManager(
|
||||||
LocalizationCandidate.english(CATALOG, PluralSelector.oneOther())
|
LocalizationCandidate.english(CATALOG, PluralSelector.oneOther())
|
||||||
);
|
);
|
||||||
@@ -74,6 +89,9 @@ public final class IrisLanguage {
|
|||||||
new CopyOnWriteArrayList<>();
|
new CopyOnWriteArrayList<>();
|
||||||
private static volatile File dataFolder;
|
private static volatile File dataFolder;
|
||||||
private static volatile String activeLocale = CATALOG.englishLocale();
|
private static volatile String activeLocale = CATALOG.englishLocale();
|
||||||
|
private static volatile RemoteLanguageCatalog remote;
|
||||||
|
private static volatile Path remoteRoot;
|
||||||
|
private static volatile PluginLanguageService selections;
|
||||||
|
|
||||||
private IrisLanguage() {
|
private IrisLanguage() {
|
||||||
}
|
}
|
||||||
@@ -82,7 +100,9 @@ public final class IrisLanguage {
|
|||||||
if (!IrisPlatforms.isBound()) {
|
if (!IrisPlatforms.isBound()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return reload(IrisPlatforms.get().dataFolder(), configuredLocale());
|
boolean loaded = reload(IrisPlatforms.get().dataFolder(), configuredLocale());
|
||||||
|
start();
|
||||||
|
return loaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static synchronized boolean reload() {
|
public static synchronized boolean reload() {
|
||||||
@@ -201,20 +221,30 @@ public final class IrisLanguage {
|
|||||||
|
|
||||||
private static boolean applyReload(File root, String requestedLocale, SnapshotCapture capture) {
|
private static boolean applyReload(File root, String requestedLocale, SnapshotCapture capture) {
|
||||||
LocalizationReloadResult result;
|
LocalizationReloadResult result;
|
||||||
if (capture.failure() == null) {
|
synchronized (SNAPSHOT_LOCK) {
|
||||||
result = MANAGER.reload(() -> loadCandidate(root, requestedLocale, capture.snapshot()));
|
if (capture.failure() == null) {
|
||||||
} else {
|
result = MANAGER.reload(() -> loadCandidate(root, requestedLocale, capture.snapshot()));
|
||||||
result = MANAGER.reload(() -> {
|
} else {
|
||||||
throw capture.failure();
|
result = MANAGER.reload(() -> {
|
||||||
});
|
throw capture.failure();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
dataFolder = root;
|
||||||
|
if (result.applied()) {
|
||||||
|
activeLocale = requestedLocale;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dataFolder = root;
|
|
||||||
if (!result.applied()) {
|
if (!result.applied()) {
|
||||||
reportRejectedReload(requestedLocale, result);
|
reportRejectedReload(requestedLocale, result);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
activeLocale = requestedLocale;
|
PluginLanguageService current = selections;
|
||||||
|
if (current != null) {
|
||||||
|
current.invalidate();
|
||||||
|
current.cache(activeLocale, MANAGER.snapshot());
|
||||||
|
requestConfiguredLocale();
|
||||||
|
}
|
||||||
int warnings = result.validation().warnings().size();
|
int warnings = result.validation().warnings().size();
|
||||||
IrisLogging.debug("Loaded locale " + requestedLocale + " with " + warnings + " fallback "
|
IrisLogging.debug("Loaded locale " + requestedLocale + " with " + warnings + " fallback "
|
||||||
+ (warnings == 1 ? "entry" : "entries") + ".");
|
+ (warnings == 1 ? "entry" : "entries") + ".");
|
||||||
@@ -274,7 +304,7 @@ public final class IrisLanguage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String plain(MessageKey key) {
|
public static String plain(MessageKey key) {
|
||||||
LocalizationSnapshot snapshot = MANAGER.snapshot();
|
LocalizationSnapshot snapshot = snapshot();
|
||||||
PlainMemo memo = PLAIN_MEMO.get();
|
PlainMemo memo = PLAIN_MEMO.get();
|
||||||
if (memo == null || memo.snapshot() != snapshot) {
|
if (memo == null || memo.snapshot() != snapshot) {
|
||||||
memo = new PlainMemo(snapshot, new ConcurrentHashMap<>());
|
memo = new PlainMemo(snapshot, new ConcurrentHashMap<>());
|
||||||
@@ -284,7 +314,7 @@ public final class IrisLanguage {
|
|||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
String resolved = plain(key, MessageArgs.empty());
|
String resolved = IrisLogging.clean(render(resolve(snapshot, key, MessageArgs.empty())));
|
||||||
memo.values().put(key.id(), resolved);
|
memo.values().put(key.id(), resolved);
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
@@ -317,14 +347,18 @@ public final class IrisLanguage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static ResolvedText resolve(MessageKey key, MessageArgs arguments) {
|
private static ResolvedText resolve(MessageKey key, MessageArgs arguments) {
|
||||||
|
return resolve(snapshot(), key, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ResolvedText resolve(LocalizationSnapshot snapshot, MessageKey key, MessageArgs arguments) {
|
||||||
if (key instanceof TextKey textKey) {
|
if (key instanceof TextKey textKey) {
|
||||||
return MANAGER.snapshot().resolve(textKey, arguments);
|
return snapshot.resolve(textKey, arguments);
|
||||||
}
|
}
|
||||||
if (key instanceof PluralKey pluralKey) {
|
if (key instanceof PluralKey pluralKey) {
|
||||||
return MANAGER.snapshot().resolve(pluralKey, arguments);
|
return snapshot.resolve(pluralKey, arguments);
|
||||||
}
|
}
|
||||||
if (key instanceof LinesKey linesKey) {
|
if (key instanceof LinesKey linesKey) {
|
||||||
ResolvedLines lines = MANAGER.snapshot().resolve(linesKey, arguments);
|
ResolvedLines lines = snapshot.resolve(linesKey, arguments);
|
||||||
return new ResolvedText(lines.key(), lines.locale(), String.join("\n", lines.lines()), lines.arguments());
|
return new ResolvedText(lines.key(), lines.locale(), String.join("\n", lines.lines()), lines.arguments());
|
||||||
}
|
}
|
||||||
throw new IllegalArgumentException("Unsupported Iris message key: " + key.id());
|
throw new IllegalArgumentException("Unsupported Iris message key: " + key.id());
|
||||||
@@ -343,41 +377,216 @@ public final class IrisLanguage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!CATALOG.englishLocale().equals(locale)) {
|
if (!CATALOG.englishLocale().equals(locale)) {
|
||||||
LocaleOverlay bundled = loadBundledOverlay(locale);
|
LocaleOverlay downloaded = loadDownloadedOverlay(root, locale);
|
||||||
if (bundled != null) {
|
if (downloaded != null) {
|
||||||
overlays.add(bundled);
|
overlays.add(downloaded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new LocalizationCandidate(CATALOG, overlays, PluralSelector.oneOther());
|
return new LocalizationCandidate(CATALOG, overlays, PluralSelector.oneOther());
|
||||||
}
|
}
|
||||||
|
|
||||||
static LocaleOverlay loadBundledOverlay(String locale) throws Exception {
|
static RemoteLanguageCatalog remote(File root) {
|
||||||
String normalizedLocale = normalizeLocale(locale);
|
Path path = root.toPath().toAbsolutePath().normalize();
|
||||||
if (CATALOG.englishLocale().equals(normalizedLocale)) {
|
RemoteLanguageCatalog current = remote;
|
||||||
return null;
|
if (current != null && path.equals(remoteRoot)) {
|
||||||
|
return current;
|
||||||
}
|
}
|
||||||
String resourceName = "/languages/" + normalizedLocale + ".json";
|
synchronized (IrisLanguage.class) {
|
||||||
InputStream input = IrisLanguage.class.getResourceAsStream(resourceName);
|
if (remote != null && path.equals(remoteRoot)) {
|
||||||
if (input == null) {
|
return remote;
|
||||||
if (VolmitLocales.isBundled(normalizedLocale)) {
|
|
||||||
throw new IllegalStateException("Missing bundled Iris locale resource: " + resourceName);
|
|
||||||
}
|
}
|
||||||
return null;
|
if (remote != null) {
|
||||||
}
|
remote.close();
|
||||||
try (InputStream stream = input) {
|
|
||||||
byte[] bytes = stream.readNBytes((int) MAX_LOCALE_BYTES + 1);
|
|
||||||
if (bytes.length > MAX_LOCALE_BYTES) {
|
|
||||||
throw new IllegalArgumentException("Bundled locale is too large: " + resourceName);
|
|
||||||
}
|
}
|
||||||
return parseOverlay("bundled:" + resourceName, normalizedLocale, new String(bytes, StandardCharsets.UTF_8));
|
remote = RemoteLanguageCatalog.load(new RemoteLanguageCatalog.Options(
|
||||||
|
"Iris",
|
||||||
|
URI.create("https://raw.githubusercontent.com/VolmitSoftware/Iris/"),
|
||||||
|
"core/src/main/resources/languages",
|
||||||
|
".json",
|
||||||
|
"iris-language-source.properties",
|
||||||
|
path.resolve("languages/downloaded"),
|
||||||
|
IrisLanguage.class.getClassLoader()
|
||||||
|
));
|
||||||
|
remoteRoot = path;
|
||||||
|
return remote;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static synchronized void start() {
|
||||||
|
if (selections != null || dataFolder == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selections = new PluginLanguageService(new PluginLanguageService.Options(
|
||||||
|
dataFolder.toPath().resolve("languages/players.properties"),
|
||||||
|
IrisLanguage::availableLocales,
|
||||||
|
IrisLanguage::activeLocale,
|
||||||
|
MANAGER::snapshot,
|
||||||
|
IrisLanguage::prepareLocale,
|
||||||
|
IrisLanguage::selectDefault,
|
||||||
|
Logger.getLogger("Iris")
|
||||||
|
));
|
||||||
|
requestConfiguredLocale();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized void shutdown() {
|
||||||
|
PluginLanguageService current = selections;
|
||||||
|
selections = null;
|
||||||
|
if (current != null) {
|
||||||
|
current.close();
|
||||||
|
}
|
||||||
|
RemoteLanguageCatalog currentRemote = remote;
|
||||||
|
remote = null;
|
||||||
|
remoteRoot = null;
|
||||||
|
if (currentRemote != null) {
|
||||||
|
currentRemote.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PluginLanguageService selections() {
|
||||||
|
return selections;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<String> availableLocales() {
|
||||||
|
Set<String> locales = new LinkedHashSet<>();
|
||||||
|
locales.add(CATALOG.englishLocale());
|
||||||
|
locales.addAll(remote(dataFolder).availableLocales());
|
||||||
|
return Set.copyOf(locales);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String text(UUID player, MessageKey key, MessageArgs arguments) {
|
||||||
|
return LanguageAudience.call(player, () -> text(key, arguments));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String plain(UUID player, MessageKey key, MessageArgs arguments) {
|
||||||
|
return LanguageAudience.call(player, () -> plain(key, arguments));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocalizationSnapshot snapshot() {
|
||||||
|
PluginLanguageService current = selections;
|
||||||
|
return current == null ? MANAGER.snapshot() : current.snapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocalizationSnapshot prepareLocale(String locale) throws Exception {
|
||||||
|
File root = dataFolder;
|
||||||
|
if (!CATALOG.englishLocale().equals(locale) && remote(root).availableLocales().contains(locale)) {
|
||||||
|
remote(root).readOrDownload(locale, IrisLanguage::validateDownload);
|
||||||
|
}
|
||||||
|
SnapshotCapture capture = captureForReload(overrideFile(root, locale), locale);
|
||||||
|
if (capture.failure() != null) {
|
||||||
|
throw capture.failure();
|
||||||
|
}
|
||||||
|
return LocalizationSnapshot.create(loadCandidate(root, locale, capture.snapshot()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PluginLanguageEditor.Options editorOptions() {
|
||||||
|
return new PluginLanguageEditor.Options(IrisLanguage::prepareLocale, IrisLanguage::writeMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocalizationSnapshot writeMessage(PluginLanguageEditor.Edit edit) throws IOException {
|
||||||
|
File root = dataFolder;
|
||||||
|
File file = overrideFile(root, edit.locale());
|
||||||
|
LocalizationSnapshot prepared = LanguageFileEditor.update(file.toPath(), raw -> {
|
||||||
|
LocalizationSnapshot current = editorSnapshot(root, file, edit.locale(), raw);
|
||||||
|
if (!current.value(CATALOG.require(edit.key())).equals(edit.expected())) {
|
||||||
|
throw new IOException("Language message changed; reopen it before saving");
|
||||||
|
}
|
||||||
|
String updated = writeOverride(raw, edit);
|
||||||
|
return new LanguageFileEditor.Prepared<>(updated, editorSnapshot(root, file, edit.locale(), updated));
|
||||||
|
});
|
||||||
|
synchronized (SNAPSHOT_LOCK) {
|
||||||
|
if (root.equals(dataFolder) && edit.locale().equals(activeLocale)) {
|
||||||
|
MANAGER.install(prepared);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prepared;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocalizationSnapshot editorSnapshot(File root, File file, String locale, String raw) throws IOException {
|
||||||
|
try {
|
||||||
|
LocaleHotloadSnapshot source = LocaleHotloadSnapshot.present(file, locale, raw, sha256(raw.getBytes(StandardCharsets.UTF_8)));
|
||||||
|
return LocalizationSnapshot.create(loadCandidate(root, locale, source));
|
||||||
|
} catch (Exception failure) {
|
||||||
|
throw new IOException("Could not validate Iris language " + locale, failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String writeOverride(String raw, PluginLanguageEditor.Edit edit) {
|
||||||
|
Map<String, MessageValue> values = new LinkedHashMap<>(parseOverlay("editor", edit.locale(), raw).values());
|
||||||
|
values.put(edit.key(), edit.value());
|
||||||
|
JsonObject messages = new JsonObject();
|
||||||
|
Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
|
||||||
|
for (Map.Entry<String, MessageValue> entry : values.entrySet()) {
|
||||||
|
JsonElement value = switch (entry.getValue()) {
|
||||||
|
case TextValue text -> gson.toJsonTree(text.template());
|
||||||
|
case LinesValue lines -> gson.toJsonTree(lines.lines());
|
||||||
|
case PluralValue plural -> gson.toJsonTree(plural.forms());
|
||||||
|
};
|
||||||
|
messages.add(entry.getKey(), value);
|
||||||
|
}
|
||||||
|
JsonObject document = new JsonObject();
|
||||||
|
document.addProperty("locale", edit.locale());
|
||||||
|
document.add("messages", messages);
|
||||||
|
return gson.toJson(document) + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void selectDefault(String locale, LocalizationSnapshot prepared) throws Exception {
|
||||||
|
IrisSettings current = IrisSettings.get();
|
||||||
|
JsonObject serialized = new Gson().toJsonTree(current).getAsJsonObject();
|
||||||
|
serialized.getAsJsonObject("general").addProperty("language", locale);
|
||||||
|
Path target = dataFolder.toPath().resolve("iris.json");
|
||||||
|
Path temporary = Files.createTempFile(target.getParent(), "iris-", ".json.tmp");
|
||||||
|
try {
|
||||||
|
Files.writeString(temporary, serialized.toString());
|
||||||
|
Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
} finally {
|
||||||
|
Files.deleteIfExists(temporary);
|
||||||
|
}
|
||||||
|
current.getGeneral().setLanguage(locale);
|
||||||
|
synchronized (SNAPSHOT_LOCK) {
|
||||||
|
MANAGER.install(prepared);
|
||||||
|
activeLocale = locale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void requestConfiguredLocale() {
|
||||||
|
String locale = activeLocale;
|
||||||
|
if (CATALOG.englishLocale().equals(locale)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
File root = dataFolder;
|
||||||
|
remote(root).request(locale, IrisLanguage::validateDownload, result -> {
|
||||||
|
if (!result.successful()) {
|
||||||
|
IrisLogging.error("Failed to download Iris locale " + locale + ".");
|
||||||
|
IrisLogging.reportError(result.failure());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronized (IrisLanguage.class) {
|
||||||
|
if (root.equals(dataFolder) && locale.equals(activeLocale)) {
|
||||||
|
reloadResolved(root, locale, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateDownload(String locale, String raw) {
|
||||||
|
LocalizationValidator.validate(CATALOG, List.of(parseOverlay("download:" + locale, locale, raw)))
|
||||||
|
.throwIfInvalid();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocaleOverlay loadDownloadedOverlay(File root, String locale) {
|
||||||
|
RemoteLanguageCatalog.CacheResult cached = remote(root).read(locale, IrisLanguage::validateDownload);
|
||||||
|
if (cached.state() == RemoteLanguageCatalog.CacheState.VALID) {
|
||||||
|
return parseOverlay(cached.file().toString(), locale, cached.content());
|
||||||
|
}
|
||||||
|
if (cached.failure() != null) {
|
||||||
|
IrisLogging.error("Ignoring invalid downloaded Iris locale " + locale + ".");
|
||||||
|
IrisLogging.reportError(cached.failure());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
static LocaleOverlay parseOverlay(String source, String locale, String raw) {
|
static LocaleOverlay parseOverlay(String source, String locale, String raw) {
|
||||||
JsonElement parsed = JsonParser.parseString(raw == null || raw.isBlank() ? "{}" : raw);
|
JsonElement parsed = JsonParser.parseString(raw == null || raw.isBlank() ? "{}" : raw);
|
||||||
if (parsed.isJsonArray()) {
|
|
||||||
return parseCompactBukkitOverlay(source, locale, parsed.getAsJsonArray());
|
|
||||||
}
|
|
||||||
if (!parsed.isJsonObject()) {
|
if (!parsed.isJsonObject()) {
|
||||||
throw new IllegalArgumentException("Locale source is not a JSON object: " + source);
|
throw new IllegalArgumentException("Locale source is not a JSON object: " + source);
|
||||||
}
|
}
|
||||||
@@ -405,49 +614,6 @@ public final class IrisLanguage {
|
|||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LocaleOverlay parseCompactBukkitOverlay(
|
|
||||||
String source,
|
|
||||||
String locale,
|
|
||||||
JsonArray root
|
|
||||||
) {
|
|
||||||
if (root.size() != 2 || !root.get(0).isJsonPrimitive() || !root.get(1).isJsonArray()) {
|
|
||||||
throw new IllegalArgumentException("Compact locale source is invalid: " + source);
|
|
||||||
}
|
|
||||||
if (!locale.equals(normalizeLocale(root.get(0).getAsString()))) {
|
|
||||||
throw new IllegalArgumentException("Locale source declares a different locale than its file: " + source);
|
|
||||||
}
|
|
||||||
JsonArray messages = root.get(1).getAsJsonArray();
|
|
||||||
if (messages.size() != BUKKIT_MESSAGE_IDS.size()) {
|
|
||||||
throw new IllegalArgumentException("Compact locale message count is invalid: " + source);
|
|
||||||
}
|
|
||||||
LocaleOverlay.Builder builder = LocaleOverlay.builder(source, locale);
|
|
||||||
for (int i = 0; i < messages.size(); i++) {
|
|
||||||
JsonElement value = messages.get(i);
|
|
||||||
if (value == null || value.isJsonNull()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
appendCompactMessage(builder, BUKKIT_MESSAGE_IDS.get(i), value);
|
|
||||||
}
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void appendCompactMessage(
|
|
||||||
LocaleOverlay.Builder builder,
|
|
||||||
String key,
|
|
||||||
JsonElement value
|
|
||||||
) {
|
|
||||||
MessageKey definition = CATALOG.key(key);
|
|
||||||
if (value.isJsonObject() && definition instanceof PluralKey) {
|
|
||||||
builder.plural(key, readPlural(key, value.getAsJsonObject()));
|
|
||||||
} else if (value.isJsonArray()) {
|
|
||||||
builder.lines(key, readLines(key, value.getAsJsonArray()));
|
|
||||||
} else if (value.isJsonPrimitive() && value.getAsJsonPrimitive().isString()) {
|
|
||||||
builder.text(key, value.getAsString());
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("Compact locale value has an invalid shape: " + key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void appendMessages(LocaleOverlay.Builder builder, JsonObject object, String prefix) {
|
private static void appendMessages(LocaleOverlay.Builder builder, JsonObject object, String prefix) {
|
||||||
for (Map.Entry<String, JsonElement> entry : object.entrySet()) {
|
for (Map.Entry<String, JsonElement> entry : object.entrySet()) {
|
||||||
String key = prefix.isEmpty() ? entry.getKey() : prefix + "." + entry.getKey();
|
String key = prefix.isEmpty() ? entry.getKey() : prefix + "." + entry.getKey();
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package art.arcane.iris.core.localization;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.util.localization.LocalizationCandidate;
|
||||||
|
import art.arcane.volmlib.util.localization.LocalizationSnapshot;
|
||||||
|
import art.arcane.volmlib.util.localization.PluginLanguageEditor;
|
||||||
|
import art.arcane.volmlib.util.localization.PluginLanguageService;
|
||||||
|
import art.arcane.volmlib.util.localization.PluralSelector;
|
||||||
|
import art.arcane.volmlib.util.localization.TextValue;
|
||||||
|
import art.arcane.volmlib.util.localization.VolmitLocales;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
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;
|
||||||
|
|
||||||
|
public class IrisLanguageEditorTest {
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
private File directory;
|
||||||
|
private PluginLanguageService languages;
|
||||||
|
private PluginLanguageEditor editor;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
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(Path.of("src/main/resources/languages/fr_FR.json"), cache);
|
||||||
|
PluginLanguageEditor.Options options = IrisLanguage.editorOptions();
|
||||||
|
LocalizationSnapshot english = LocalizationSnapshot.create(
|
||||||
|
LocalizationCandidate.english(IrisLanguage.catalog(), PluralSelector.oneOther()));
|
||||||
|
languages = new PluginLanguageService(new PluginLanguageService.Options(
|
||||||
|
directory.toPath().resolve("players.properties"), VolmitLocales::all, () -> "en_US", () -> english,
|
||||||
|
options.loader()::load, (locale, snapshot) -> {
|
||||||
|
throw new AssertionError("Editing must not select a server language");
|
||||||
|
}, Logger.getLogger("IrisLanguageEditorTest")));
|
||||||
|
editor = new PluginLanguageEditor(languages, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void closeEditor() {
|
||||||
|
editor.close();
|
||||||
|
languages.close();
|
||||||
|
assertTrue(IrisLanguage.reload(directory, "en_US"));
|
||||||
|
IrisLanguage.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void savesOneLocaleAndRefreshesItsPersonalSnapshotWithoutSelectingIt() throws Exception {
|
||||||
|
UUID player = UUID.randomUUID();
|
||||||
|
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");
|
||||||
|
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")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void invalidMessageLeavesTheLocaleFileIntact() throws Exception {
|
||||||
|
PluginLanguageEditor.Document original = editor.load("fr_FR").get(5, TimeUnit.SECONDS);
|
||||||
|
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");
|
||||||
|
byte[] before = Files.readAllBytes(file);
|
||||||
|
PluginLanguageEditor.Document saved = editor.load("fr_FR").get(5, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
assertThrows(ExecutionException.class, () -> editor.save(new PluginLanguageEditor.Edit("fr_FR",
|
||||||
|
IrisMessages.COMMAND_RELOAD_SUCCESS.id(), saved.snapshot().value(IrisMessages.COMMAND_RELOAD_SUCCESS),
|
||||||
|
new TextValue("Missing placeholder"))).get(5, TimeUnit.SECONDS));
|
||||||
|
assertArrayEquals(before, Files.readAllBytes(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void incompleteDownloadedLocaleCanBeEditedWithoutBeingSelected() throws Exception {
|
||||||
|
Files.writeString(IrisLanguage.remote(directory).cacheFile("fr_FR"), "{\"locale\":\"fr_FR\",\"messages\":{}}");
|
||||||
|
PluginLanguageEditor.Document original = editor.load("fr_FR").get(5, TimeUnit.SECONDS);
|
||||||
|
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);
|
||||||
|
assertEquals(value, saved.snapshot().value(IrisMessages.COMMAND_RELOAD_SUCCESS));
|
||||||
|
assertEquals("en_US", languages.defaultLocale());
|
||||||
|
assertEquals("en_US", IrisLanguage.activeLocale());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,12 +23,17 @@ import java.net.URL;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import art.arcane.volmlib.util.localization.LanguageAudience;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -39,6 +44,7 @@ import static org.junit.Assert.assertFalse;
|
|||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static org.junit.Assert.assertSame;
|
import static org.junit.Assert.assertSame;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
|
||||||
public class IrisLanguageTest {
|
public class IrisLanguageTest {
|
||||||
private static final Pattern PLACEHOLDER = Pattern.compile("\\{[A-Za-z][A-Za-z0-9_]*}");
|
private static final Pattern PLACEHOLDER = Pattern.compile("\\{[A-Za-z][A-Za-z0-9_]*}");
|
||||||
@@ -108,6 +114,11 @@ public class IrisLanguageTest {
|
|||||||
public void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
dataFolder = temporaryFolder.newFolder();
|
dataFolder = temporaryFolder.newFolder();
|
||||||
assertTrue(IrisLanguage.reload(dataFolder, "en_US"));
|
assertTrue(IrisLanguage.reload(dataFolder, "en_US"));
|
||||||
|
for (String locale : VolmitLocales.nonEnglish()) {
|
||||||
|
Path target = IrisLanguage.remote(dataFolder).cacheFile(locale);
|
||||||
|
Files.createDirectories(target.getParent());
|
||||||
|
Files.copy(Path.of("src/main/resources/languages", locale + ".json"), target, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@@ -115,6 +126,56 @@ public class IrisLanguageTest {
|
|||||||
assertTrue(IrisLanguage.reload(dataFolder, "en_US"));
|
assertTrue(IrisLanguage.reload(dataFolder, "en_US"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private LocaleOverlay loadSourceOverlay(String locale) throws Exception {
|
||||||
|
Path source = Path.of("src/main/resources/languages", locale + ".json");
|
||||||
|
return IrisLanguage.parseOverlay(source.toString(), locale, Files.readString(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void missingDownloadUsesCodeOwnedEnglish() throws Exception {
|
||||||
|
Files.delete(IrisLanguage.remote(dataFolder).cacheFile("de_DE"));
|
||||||
|
assertTrue(IrisLanguage.reload(dataFolder, "de_DE"));
|
||||||
|
assertEquals("Unknown Iris command", IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failedServerSelectionRetainsSettingsAndRenderedLanguage() throws Exception {
|
||||||
|
IrisSettings previous = IrisSettings.settings;
|
||||||
|
IrisSettings current = new IrisSettings();
|
||||||
|
IrisSettings.settings = current;
|
||||||
|
Files.createDirectory(dataFolder.toPath().resolve("iris.json"));
|
||||||
|
IrisLanguage.start();
|
||||||
|
try {
|
||||||
|
assertThrows(ExecutionException.class,
|
||||||
|
() -> IrisLanguage.selections().selectDefault("de_DE").get(5, TimeUnit.SECONDS));
|
||||||
|
assertEquals("en_US", current.getGeneral().getLanguage());
|
||||||
|
assertEquals("en_US", IrisLanguage.activeLocale());
|
||||||
|
assertEquals("Unknown Iris command", IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN));
|
||||||
|
} finally {
|
||||||
|
IrisLanguage.shutdown();
|
||||||
|
IrisSettings.settings = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void personalSelectionDoesNotChangeAnotherPlayerOrTheServerDefault() throws Exception {
|
||||||
|
UUID germanPlayer = UUID.randomUUID();
|
||||||
|
UUID otherPlayer = UUID.randomUUID();
|
||||||
|
IrisLanguage.start();
|
||||||
|
try {
|
||||||
|
IrisLanguage.selections().selectPlayer(germanPlayer, "de_DE").get(5, TimeUnit.SECONDS);
|
||||||
|
String german = LanguageAudience.call(germanPlayer, () -> IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN));
|
||||||
|
String other = LanguageAudience.call(otherPlayer, () -> IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN));
|
||||||
|
assertFalse("Unknown Iris command".equals(german));
|
||||||
|
assertEquals("Unknown Iris command", other);
|
||||||
|
assertEquals("en_US", IrisLanguage.activeLocale());
|
||||||
|
IrisLanguage.selections().clearPlayer(germanPlayer).get(5, TimeUnit.SECONDS);
|
||||||
|
assertEquals("Unknown Iris command", LanguageAudience.call(germanPlayer, () -> IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN)));
|
||||||
|
} finally {
|
||||||
|
IrisLanguage.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void usesCodeOwnedEnglishWithoutLocaleFile() {
|
public void usesCodeOwnedEnglishWithoutLocaleFile() {
|
||||||
String rendered = IrisLanguage.plain(
|
String rendered = IrisLanguage.plain(
|
||||||
@@ -149,10 +210,10 @@ public class IrisLanguageTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void bundledLocalesMatchSharedManifestAndCoverEntireCatalog() throws Exception {
|
public void downloadableLocalesMatchSharedManifestAndCoverEntireCatalog() throws Exception {
|
||||||
assertEquals(17, VolmitLocales.nonEnglish().size());
|
assertEquals(17, VolmitLocales.nonEnglish().size());
|
||||||
for (String locale : VolmitLocales.nonEnglish()) {
|
for (String locale : VolmitLocales.nonEnglish()) {
|
||||||
LocaleOverlay overlay = IrisLanguage.loadBundledOverlay(locale);
|
LocaleOverlay overlay = loadSourceOverlay(locale);
|
||||||
assertEquals(locale, overlay.locale());
|
assertEquals(locale, overlay.locale());
|
||||||
assertEquals(IrisLanguage.catalog().ids(), overlay.values().keySet());
|
assertEquals(IrisLanguage.catalog().ids(), overlay.values().keySet());
|
||||||
|
|
||||||
@@ -175,30 +236,7 @@ public class IrisLanguageTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void compactBukkitLocaleUsesSortedCatalogPositionsAndEnglishFallbacks() {
|
public void sourceLocalesExactlyMatchNonEnglishManifest() throws Exception {
|
||||||
List<String> messageIds = IrisLanguage.catalog().ids().stream()
|
|
||||||
.filter(id -> !id.startsWith("iris.modded."))
|
|
||||||
.sorted()
|
|
||||||
.toList();
|
|
||||||
String translatedId = IrisMessages.COMMAND_UNKNOWN.id();
|
|
||||||
JsonArray values = new JsonArray(messageIds.size());
|
|
||||||
for (int i = 0; i < messageIds.size(); i++) {
|
|
||||||
values.add(messageIds.get(i).equals(translatedId) ? "Unbekannter Iris-Befehl" : null);
|
|
||||||
}
|
|
||||||
JsonArray compact = new JsonArray(2);
|
|
||||||
compact.add("de_DE");
|
|
||||||
compact.add(values);
|
|
||||||
|
|
||||||
LocaleOverlay overlay = IrisLanguage.parseOverlay("test", "de_DE", compact.toString());
|
|
||||||
|
|
||||||
assertEquals(Set.of(translatedId), overlay.values().keySet());
|
|
||||||
assertEquals(
|
|
||||||
"Unbekannter Iris-Befehl",
|
|
||||||
((TextValue) overlay.value(translatedId)).template());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void bundledServerResourcesExactlyMatchNonEnglishManifest() throws Exception {
|
|
||||||
Set<String> expected = VolmitLocales.nonEnglish().stream()
|
Set<String> expected = VolmitLocales.nonEnglish().stream()
|
||||||
.map(locale -> locale + ".json")
|
.map(locale -> locale + ".json")
|
||||||
.collect(Collectors.toUnmodifiableSet());
|
.collect(Collectors.toUnmodifiableSet());
|
||||||
@@ -208,9 +246,9 @@ public class IrisLanguageTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void bundledLocalesPreserveReservedWorldNames() throws Exception {
|
public void downloadableLocalesPreserveReservedWorldNames() throws Exception {
|
||||||
for (String locale : VolmitLocales.nonEnglish()) {
|
for (String locale : VolmitLocales.nonEnglish()) {
|
||||||
LocaleOverlay overlay = IrisLanguage.loadBundledOverlay(locale);
|
LocaleOverlay overlay = loadSourceOverlay(locale);
|
||||||
TextValue irisName = (TextValue) overlay.value(
|
TextValue irisName = (TextValue) overlay.value(
|
||||||
BukkitCommandMessagesExtended.COMMAND_IRIS_YOU_CANNOT_USE_WORLD_NAME_IRIS_CREATING_WORLDS_AS_IRIS.id()
|
BukkitCommandMessagesExtended.COMMAND_IRIS_YOU_CANNOT_USE_WORLD_NAME_IRIS_CREATING_WORLDS_AS_IRIS.id()
|
||||||
);
|
);
|
||||||
@@ -224,7 +262,7 @@ public class IrisLanguageTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void bundledLocaleLoadsWithoutUserOverride() {
|
public void downloadedLocaleLoadsWithoutUserOverride() {
|
||||||
assertTrue(IrisLanguage.reload(dataFolder, "de_DE"));
|
assertTrue(IrisLanguage.reload(dataFolder, "de_DE"));
|
||||||
assertEquals("de_DE", IrisLanguage.activeLocale());
|
assertEquals("de_DE", IrisLanguage.activeLocale());
|
||||||
assertFalse("Unknown Iris command".equals(IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN)));
|
assertFalse("Unknown Iris command".equals(IrisLanguage.plain(IrisMessages.COMMAND_UNKNOWN)));
|
||||||
|
|||||||
Reference in new issue
Block a user