Skip to content

Commit

Permalink
Internationalisation (i18) (#2)
Browse files Browse the repository at this point in the history
* i18n APIs

* finished i18n

* readme update
  • Loading branch information
Crafter-Y authored Jul 11, 2023
1 parent 9d4db1e commit 411c66f
Show file tree
Hide file tree
Showing 24 changed files with 377 additions and 20 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ plugin (for now) that has the basic commands `/home`, `/homes`,
Several things can be configured in the `config.yml` file.

- maxHomes: The maximum amount of homes a player can have, default: 3
- language: The language to use, default: en

You can add your own language by creating a new file your_language.lang.yml

## Roadmap
- [x] Persistent Data Storage
- [x] Configurable Homes per player
- [x] reflection for command discovery
- [x] abstract command definition syntax (with annotations)
- [ ] i18n
- [x] configurable i18n
- [ ] other implementations like fabric or forge
3 changes: 3 additions & 0 deletions annotations/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
plugins {
id("craftinghomes.java")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package de.craftery.craftinghomes.annotation;

import de.craftery.craftinghomes.annotation.annotations.I18nDef;
import de.craftery.craftinghomes.annotation.annotations.I18nSource;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;

@SupportedAnnotationTypes("de.craftery.craftinghomes.annotation.annotations.*")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
public class CraftingAnnotationProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return processI18nAnnotations(roundEnv);
}

private boolean processI18nAnnotations(RoundEnvironment roundEnv) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(I18nSource.class);
if (elements.size() > 1) {
error("There can only be one I18nSource per project for now!");
return false;
}
if (elements.isEmpty()) {
return false;
}

Element providerElement = elements.iterator().next();
if (!(providerElement instanceof TypeElement providerClass)) {
error("Entry element must be a interface!");
return false;
}

if (providerClass.getKind() != ElementKind.INTERFACE) {
error("Entry element must be a interface!");
return false;
}

Map<String, String> translations = new HashMap<>();
Map<String, Map<String, String>> parameters = new HashMap<>();

for (Element type : providerClass.getEnclosedElements()) {
if (type.getKind() != ElementKind.METHOD) {
error("Each entry in this interface must be a method!");
return false;
}
ExecutableElement method = (ExecutableElement) type;

if (!method.getReturnType().toString().equals("java.lang.String")) {
error("Each entry in this interface must return a String! Got: " + method.getReturnType().toString());
return false;
}
if (type.getAnnotation(I18nDef.class) == null) {
error("Each entry in this interface must be annotated with @I18nDef!");
return false;
}

Map<String, String> methodParameters = new HashMap<>();
for (VariableElement parameter : method.getParameters()) {
String paramType;
switch (parameter.asType().toString()) {
case "java.lang.String" -> paramType = "String";
case "java.lang.Integer" -> paramType = "Integer";
default -> {
error("Unimplemented parameter type: " + parameter.asType().toString() + " in method " + method.getSimpleName().toString());
return false;
}
}
methodParameters.put(parameter.getSimpleName().toString(), paramType);
}

String def = type.getAnnotation(I18nDef.class).def();
translations.put(type.getSimpleName().toString(), def);
parameters.put(type.getSimpleName().toString(), methodParameters);
}

String providerPath = providerClass.getQualifiedName().toString();
String providerName = providerClass.getAnnotation(I18nSource.class).name();

String packageName = null;
int lastDot = providerPath.lastIndexOf('.');
if (lastDot > 0) {
packageName = providerPath.substring(0, lastDot);
}
String generatedClassName = packageName + "." + providerName;

try {
JavaFileObject builderFile = processingEnv.getFiler()
.createSourceFile(generatedClassName);

try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
// class header
if (packageName != null) {
out.print("package ");
out.print(packageName);
out.println(";");
out.println();
}

out.print("public class ");
out.print(providerName);
out.print(" extends BaseI18nProvider implements ");
out.print(providerPath);
out.println(" {");

// adding all translations to outer object
out.println(" @Override");
out.println(" protected void addAllTranslations() {");

for (Map.Entry<String, String> implementation : translations.entrySet()) {
out.print(" this.defTranslations.put(\"");
out.print(implementation.getKey());
out.print("\", \"");
out.print(implementation.getValue());
out.println("\");");
}

out.println(" }");

// implement all methods of interface
for (Map.Entry<String, String> implementation : translations.entrySet()) {
out.println();
out.println(" @Override");
out.print(" public String ");
out.print(implementation.getKey());
out.print("(");

List<String> paramTypes = parameters.get(implementation.getKey()).entrySet().stream()
.map(el -> el.getValue() + " " + el.getKey()).toList();
out.print(String.join(", ", paramTypes));

out.println(") {");
out.print(" String translation = this.getTranslation(\"");
out.print(implementation.getKey());
out.println("\");");

for (Map.Entry<String, String> parameter : parameters.get(implementation.getKey()).entrySet()) {
String replaceMethod;
switch (parameter.getValue()) {
case "String" -> replaceMethod = "replaceString";
case "Integer" -> replaceMethod = "replaceInteger";
default -> {
error("Unimplemented parameter type: " + parameter.getValue());
return false;
}
}

out.print(" translation = this.");
out.print(replaceMethod);
out.print("(translation, \"${");
out.print(parameter.getKey());
out.print("}\", ");
out.print(parameter.getKey());
out.println(");");

}

out.println(" return translation;");

out.println(" }");
}

out.println();
out.println("}");
}
} catch (IOException e) {
error("Failed to create file: " + e.getMessage());
return false;
}

return true;
}

private void error(String message) {
this.processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package de.craftery.craftinghomes.annotation.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface I18nDef {
String def();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package de.craftery.craftinghomes.annotation.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface I18nSource {
String name();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
de.craftery.craftinghomes.annotation.CraftingAnnotationProcessor
3 changes: 3 additions & 0 deletions build-logic/src/main/groovy/craftinghomes.java.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ plugins {
group = 'de.craftery'
version = findProperty('version')

compileJava.options.encoding = "UTF-8"
compileTestJava.options.encoding = "UTF-8"

repositories {
mavenCentral()
maven {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,15 @@ public void registerCommand(AbstractCommand command) {

@Override
public ConfigurationI getConfiguration() {
return new ConfigrationImpl(this.getConfig());
return new ConfigrationImpl(this.getDataFolder());
}

public static void saveConfiguration() {
instance.saveConfig();
@Override
public ConfigurationI getConfiguration(String configFileName) {
return new ConfigrationImpl(this.getDataFolder(), configFileName);
}

public static BukkitPlatform getInstance() {
return instance;
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,39 @@
package de.craftery.craftinghomes.impl;

import com.google.common.base.Charsets;
import de.craftery.craftinghomes.BukkitPlatform;
import de.craftery.craftinghomes.common.api.ConfigurationI;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;

import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashSet;
import java.util.Set;

public class ConfigrationImpl implements ConfigurationI {
private final FileConfiguration config;
private final File configFile;

public ConfigrationImpl(FileConfiguration config) {
this.config = config;
public ConfigrationImpl(File configDirectory) {
this(configDirectory, "config.yml");
}

public ConfigrationImpl (File configDirectory, String configName) {
this.configFile = new File(configDirectory, configName);
this.config = YamlConfiguration.loadConfiguration(configFile);
this.tryLoadDefaults(this.config, configName);
}

private void tryLoadDefaults(FileConfiguration config, String configName) {
InputStream defConfigStream = BukkitPlatform.getInstance().getResource(configName);
if (defConfigStream == null) {
return;
}

config.setDefaults(YamlConfiguration.loadConfiguration(new InputStreamReader(defConfigStream, Charsets.UTF_8)));
}

@Override
Expand Down Expand Up @@ -49,6 +70,22 @@ public Set<String> getKeys(String path) {

@Override
public void saveConfig() {
BukkitPlatform.saveConfiguration();
try {
this.config.save(this.configFile);
} catch (Exception e) {
throw new RuntimeException("Could not save config file!", e);
}

}

@Override
public void addDefault(String path, Object value) {
this.config.addDefault(path, value);
}

@Override
public void applyDefaults() {
this.config.options().copyDefaults(true);
this.saveConfig();
}
}
3 changes: 3 additions & 0 deletions bukkit/src/main/resources/config.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# The maximum amount of homes a player can have
maxHomes: 3

# The language to use. All language files must be in format language.lang.yml e.g. en.lang.yml
language: en

# This is the location where homes get saved by default
homes:
3 changes: 3 additions & 0 deletions common/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ plugins {
dependencies {
shadow 'org.apache.commons:commons-io:1.3.2'
implementation 'org.jetbrains:annotations:24.0.1'

implementation(project(":annotations"))
annotationProcessor(project(":annotations"))
}

tasks {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package de.craftery.craftinghomes.common;

import de.craftery.craftinghomes.common.api.CommandSenderI;
import de.craftery.craftinghomes.common.i18n.I18n;

import java.util.List;

public abstract class AbstractCommand {
private final String name;
protected I18n i18n;

public AbstractCommand(String name) {
this.name = name;
Expand All @@ -15,6 +17,10 @@ public String getName() {
return this.name;
}

public void setI18n(I18n i18n) {
this.i18n = i18n;
}

public abstract boolean onCommand(CommandSenderI sender, String[] args);
public abstract List<String> onTabComplete(CommandSenderI sender, String[] args);
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
package de.craftery.craftinghomes.common;

import de.craftery.craftinghomes.common.i18n.I18n;

public class Platform {
private static ServerEntry server;
private static final I18n i18n = new I18n();

public static void onEnable(ServerEntry entry) {
server = entry;
server.log("CraftingHomes is starting up!");

server.log("Registering i18n!");
i18n.register();

for (AbstractCommand command : ReflectionUtil.getCommands()) {
server.log("Registering command " + command.getName());
command.setI18n(i18n);
server.registerCommand(command);
}
}
Expand Down
Loading

0 comments on commit 411c66f

Please sign in to comment.