From fbb7233b6a1fcd5be64418f437788c469dd72472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81rmin=20Scipiades?= Date: Fri, 9 Dec 2016 20:47:10 +0100 Subject: [PATCH] Add some localisation Implemented with a big bad singleton. It features namespaces, though. --- build.gradle | 20 ++++--- .../java/ppke/itk/xplang/common/Messages.java | 7 +++ .../ppke/itk/xplang/common/Translator.java | 55 +++++++++++++++++++ .../ppke/itk/xplang/lang/PlangGrammar.java | 17 ++++-- .../ppke/itk/xplang/parser/LexerError.java | 6 +- .../itk/xplang/parser/NameClashError.java | 6 +- .../ppke/itk/xplang/parser/NameError.java | 6 +- .../ppke/itk/xplang/parser/SyntaxError.java | 12 ++-- src/main/resources/messages/parser.properties | 6 ++ .../resources/messages/parser_hu.properties | 6 ++ src/main/resources/messages/plang.properties | 5 ++ .../resources/messages/plang_hu.properties | 5 ++ .../itk/xplang/common/TranslatorTest.java | 28 ++++++++++ src/test/resources/messages/test.properties | 3 + .../resources/messages/test_hu.properties | 2 + 15 files changed, 163 insertions(+), 21 deletions(-) create mode 100644 src/main/java/ppke/itk/xplang/common/Messages.java create mode 100644 src/main/java/ppke/itk/xplang/common/Translator.java create mode 100644 src/main/resources/messages/parser.properties create mode 100644 src/main/resources/messages/parser_hu.properties create mode 100644 src/main/resources/messages/plang.properties create mode 100644 src/main/resources/messages/plang_hu.properties create mode 100644 src/test/java/ppke/itk/xplang/common/TranslatorTest.java create mode 100644 src/test/resources/messages/test.properties create mode 100644 src/test/resources/messages/test_hu.properties diff --git a/build.gradle b/build.gradle index 907fb06..f766e38 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,7 @@ dependencies { compile group: 'com.github.stefanbirkner', name: 'fishbowl', version: '1.4.0' compile group: 'net.sourceforge.argparse4j', name: 'argparse4j', version: '0.7.0' compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.5' + compile group: 'com.google.guava', name: 'guava', version: '20.0' compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.21' runtime group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.21' @@ -72,13 +73,18 @@ javadoc { } import org.apache.tools.ant.filters.ReplaceTokens -processResources { - filter(ReplaceTokens, tokens: [ - "version": version, - "built.at": (new Date()).format("yyyy-MM-dd HH:mm:ssZ"), - "built.by": System.getProperty('user.name'), - "build.JDK": System.getProperty('java.version') - ]) +import org.apache.tools.ant.filters.EscapeUnicode +tasks.withType(ProcessResources).each { task -> + task.from(task.getSource()) { + include '**/*.properties' + filter(EscapeUnicode) + filter(ReplaceTokens, tokens: [ + "version": version, + "built.at": (new Date()).format("yyyy-MM-dd HH:mm:ssZ"), + "built.by": System.getProperty('user.name'), + "build.JDK": System.getProperty('java.version') + ]) + } } run { diff --git a/src/main/java/ppke/itk/xplang/common/Messages.java b/src/main/java/ppke/itk/xplang/common/Messages.java new file mode 100644 index 0000000..96c786d --- /dev/null +++ b/src/main/java/ppke/itk/xplang/common/Messages.java @@ -0,0 +1,7 @@ +package ppke.itk.xplang.common; + +/** + * A big bad singleton encapsulating a resource bundle. + */ +public class Messages { +} diff --git a/src/main/java/ppke/itk/xplang/common/Translator.java b/src/main/java/ppke/itk/xplang/common/Translator.java new file mode 100644 index 0000000..01c12e5 --- /dev/null +++ b/src/main/java/ppke/itk/xplang/common/Translator.java @@ -0,0 +1,55 @@ +package ppke.itk.xplang.common; + +import java.util.*; + +import static java.util.Arrays.asList; + +public final class Translator { + private final static Set LANGUAGES = new HashSet<>(asList("", "hu", "en")); + private final static String DEFAULT_LANGUAGE = ""; + + private static final Map instances = new HashMap<>(); + private static String language = DEFAULT_LANGUAGE; + + private ResourceBundle messages; + private Translator(String namespace) { + reset(namespace); + } + + private void reset(String namespace) { + Locale locale = new Locale(language); + messages = ResourceBundle.getBundle( + String.format("messages.%s", namespace), + locale, + ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_PROPERTIES) + ); + } + + public String translate(String key) { + return messages.getString(key); + } + + public String translate(String key, Object... args) { + return String.format(messages.getString(key), args); + } + + public static Translator getInstance(String namespace) { + namespace = namespace.toLowerCase(); + if(!instances.containsKey(namespace)) { + instances.put(namespace, new Translator(namespace)); + } + + return instances.get(namespace); + } + + public static void setLanguage(String newLanguage) { + if(!LANGUAGES.contains(newLanguage)) { + throw new IllegalStateException(String.format("Language '%s' is not supported", language)); + } + language = newLanguage; + + for(Map.Entry instance : instances.entrySet()) { + instance.getValue().reset(instance.getKey()); + } + } +} diff --git a/src/main/java/ppke/itk/xplang/lang/PlangGrammar.java b/src/main/java/ppke/itk/xplang/lang/PlangGrammar.java index f407ad7..41dca7d 100644 --- a/src/main/java/ppke/itk/xplang/lang/PlangGrammar.java +++ b/src/main/java/ppke/itk/xplang/lang/PlangGrammar.java @@ -3,6 +3,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ppke.itk.xplang.ast.*; +import ppke.itk.xplang.common.Translator; import ppke.itk.xplang.parser.*; import java.util.ArrayList; @@ -11,6 +12,7 @@ import java.util.stream.Stream; public class PlangGrammar extends Grammar { + private final static Translator translator = Translator.getInstance("Plang"); private final static Logger log = LoggerFactory.getLogger("Root.Parser.Grammar"); public PlangGrammar() { @@ -60,8 +62,10 @@ public void setup(Context ctx) { */ protected Program program(Parser parser) throws ParseError { log.debug("Program"); - parser.accept("PROGRAM", "A programnak a PROGRAM kulcsszóval kell keződnie!"); - Token nameToken = parser.accept("IDENTIFIER", "Hiányzik a program neve (egy azonosító)."); + parser.accept("PROGRAM", + translator.translate("plang.program_keyword_missing", "PROGRAM")); + Token nameToken = parser.accept("IDENTIFIER", + translator.translate("plang.missing_program_name")); if(parser.actual().symbol().equals(parser.context().lookup("DECLARE"))) { declarations(parser); @@ -80,7 +84,8 @@ protected Program program(Parser parser) throws ParseError { } } while(!stoppers.contains(parser.actual().symbol())); - parser.accept("END_PROGRAM", "A programot a PROGRAM_VÉGE kulcsszóval kell lezárni."); + parser.accept("END_PROGRAM", + translator.translate("plang.missing_end_program", "PROGRAM_VÉGE")); Scope scope = parser.context().closeScope(); Sequence sequence = new Sequence(statementList); @@ -92,8 +97,10 @@ protected Program program(Parser parser) throws ParseError { */ protected void declarations(Parser parser) throws ParseError { log.debug("Declarations"); - parser.accept("DECLARE", "Hiányzik a VÁLTOZÓK kulcsszó"); - parser.accept("COLON", "Hiányzik a VÁLTOZÓK kulcsszó után a kettőspont."); + parser.accept("DECLARE", + translator.translate("plang.missing_declarations_keyword", "VÁLTOZÓK")); + parser.accept("COLON", + translator.translate("plang.missing_colon_after_declarations_keyword", "VÁLTOZÓK")); variableDeclaration(parser); while(parser.actual().symbol().equals(parser.context().lookup("COMMA"))) { diff --git a/src/main/java/ppke/itk/xplang/parser/LexerError.java b/src/main/java/ppke/itk/xplang/parser/LexerError.java index 9f4cd49..3a5b327 100644 --- a/src/main/java/ppke/itk/xplang/parser/LexerError.java +++ b/src/main/java/ppke/itk/xplang/parser/LexerError.java @@ -1,15 +1,19 @@ package ppke.itk.xplang.parser; +import ppke.itk.xplang.common.Translator; + /** * An error of the {@link Lexer}. Thrown when the Lexer encounters a piece of text it cannot match to any of the * {@link Symbol}s it knows. */ public class LexerError extends ParseError { + private final static Translator translator = Translator.getInstance("parser"); + /** * Signal a lexing error. * @param token Lexer is supposed to return the rest of the line, starting from the point of error. */ LexerError(Token token) { - super(String.format("Could not tokenize '%s'", token.lexeme()), token.location()); + super(translator.translate("parser.LexerError.message", token.lexeme()), token.location()); } } diff --git a/src/main/java/ppke/itk/xplang/parser/NameClashError.java b/src/main/java/ppke/itk/xplang/parser/NameClashError.java index 49ec3c4..47ec15b 100644 --- a/src/main/java/ppke/itk/xplang/parser/NameClashError.java +++ b/src/main/java/ppke/itk/xplang/parser/NameClashError.java @@ -1,13 +1,15 @@ package ppke.itk.xplang.parser; +import ppke.itk.xplang.common.Translator; + /** * Thrown when you try to declare something by a name, but that name is already taken in that scope. */ public class NameClashError extends SemanticError { - private final static String NAME_CLASH_MESSAGE = "Could not declare '%s': name already taken."; + private final static Translator translator = Translator.getInstance("parser"); NameClashError(Token token) { - this(NAME_CLASH_MESSAGE, token); + this(translator.translate("parser.NameClashError.message"), token); } NameClashError(String message, Token token) { diff --git a/src/main/java/ppke/itk/xplang/parser/NameError.java b/src/main/java/ppke/itk/xplang/parser/NameError.java index 532800f..f836881 100644 --- a/src/main/java/ppke/itk/xplang/parser/NameError.java +++ b/src/main/java/ppke/itk/xplang/parser/NameError.java @@ -1,10 +1,12 @@ package ppke.itk.xplang.parser; +import ppke.itk.xplang.common.Translator; + public class NameError extends SemanticError { - private final static String NAME_ERROR_MESSAGE = "'%s' does not exist."; + private final static Translator translator = Translator.getInstance("parser"); NameError(Token token) { - this(NAME_ERROR_MESSAGE, token); + this(translator.translate("parser.NameError.message"), token); } NameError(String message, Token token) { diff --git a/src/main/java/ppke/itk/xplang/parser/SyntaxError.java b/src/main/java/ppke/itk/xplang/parser/SyntaxError.java index 1ab1644..4183cc5 100644 --- a/src/main/java/ppke/itk/xplang/parser/SyntaxError.java +++ b/src/main/java/ppke/itk/xplang/parser/SyntaxError.java @@ -1,6 +1,8 @@ package ppke.itk.xplang.parser; +import ppke.itk.xplang.common.Translator; + import java.util.Collection; import java.util.Collections; @@ -8,8 +10,7 @@ * Thrown when the source code does not conform the expected structure of the language. */ public class SyntaxError extends ParseError { - private static final String EXPECT_MANY_MSG = "Expected any symbol of %s, encountered %s"; - private static final String EXPECT_ONE_MSG = "Expected symbol %s, encountered %s"; + private final static Translator translator = Translator.getInstance("parser"); /** * Signal a syntax error with the given message. @@ -33,7 +34,10 @@ public SyntaxError(String message, Collection expected, Symbol actual, T * @param token The token causing the error. */ public SyntaxError(Collection expected, Symbol actual, Token token) { - this(expected.size() > 1? EXPECT_MANY_MSG : EXPECT_ONE_MSG, expected, actual, token); + this(translator.translate(expected.size() > 1? + "parser.SyntaxError.message.expectMany" : + "parser.SyntaxError.message.expectOne" + ), expected, actual, token); } /** @@ -43,7 +47,7 @@ public SyntaxError(Collection expected, Symbol actual, Token token) { * @param token The token causing the error. */ public SyntaxError(Symbol expected, Symbol actual, Token token) { - this(EXPECT_ONE_MSG, expected, actual, token); + this(translator.translate("parser.SyntaxError.message.expectOne"), expected, actual, token); } /** diff --git a/src/main/resources/messages/parser.properties b/src/main/resources/messages/parser.properties new file mode 100644 index 0000000..f64234b --- /dev/null +++ b/src/main/resources/messages/parser.properties @@ -0,0 +1,6 @@ +parser.NameClashError.message=Could not declare '%s': name already taken. +parser.LexerError.message=Could not tokenize '%s' +parser.NameError.message='%s' does not exist. +parser.SyntaxError.message.expectMany=Expected any symbol of %s, encountered %s +parser.SyntaxError.message.expectOne=Expected symbol %s, encountered %s + diff --git a/src/main/resources/messages/parser_hu.properties b/src/main/resources/messages/parser_hu.properties new file mode 100644 index 0000000..45d5043 --- /dev/null +++ b/src/main/resources/messages/parser_hu.properties @@ -0,0 +1,6 @@ +parser.NameClashError.message=A '%s' név már használatban van. +parser.LexerError.message=Ismeretlen szimbólum: '%s' +parser.NameError.message='%s' nevű objektum nem létezik. +parser.SyntaxError.message.expectMany=A következő szimbólumok valamelyikére számítottam: %s, helyette azt kaptam, hogy %s +parser.SyntaxError.message.expectOne=Arra a szimbólura számítottam, hogy %s, de azt kaptam, hogy %s + diff --git a/src/main/resources/messages/plang.properties b/src/main/resources/messages/plang.properties new file mode 100644 index 0000000..96ff3ba --- /dev/null +++ b/src/main/resources/messages/plang.properties @@ -0,0 +1,5 @@ +plang.program_keyword_missing=The program must start with the %s keyword +plang.missing_program_name=The name of the program (an identifier) is missing. +plang.missing_end_program=The program must end with the %s keyword +plang.missing_declarations_keyword=The %s keyword is missing +plang.missing_colon_after_declarations_keyword=Missing colon after the %s keyword diff --git a/src/main/resources/messages/plang_hu.properties b/src/main/resources/messages/plang_hu.properties new file mode 100644 index 0000000..ee8d603 --- /dev/null +++ b/src/main/resources/messages/plang_hu.properties @@ -0,0 +1,5 @@ +plang.program_keyword_missing=A programnak a %s kulcsszóval kell keződnie! +plang.missing_program_name=Hiányzik a program neve (egy azonosító). +plang.missing_end_program=A programot a %s kulcsszóval kell lezárni. +plang.missing_declarations_keyword=Hiányzik a %s kulcsszó +plang.missing_colon_after_declarations_keyword=Hiányzik a %s kulcsszó után a kettőspont. diff --git a/src/test/java/ppke/itk/xplang/common/TranslatorTest.java b/src/test/java/ppke/itk/xplang/common/TranslatorTest.java new file mode 100644 index 0000000..adb0d89 --- /dev/null +++ b/src/test/java/ppke/itk/xplang/common/TranslatorTest.java @@ -0,0 +1,28 @@ +package ppke.itk.xplang.common; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class TranslatorTest { + @Test public void translatorShouldParametrizeMessage() { + Translator.setLanguage(""); + Translator translator = Translator.getInstance("test"); + assertEquals("TEST 1", translator.translate("parser.parametrized.message", 1)); + } + + @Test public void translatorShouldFallBackToDefault() { + Translator.setLanguage("hu"); + Translator translator = Translator.getInstance("test"); + + assertEquals("TEST_FALLBACK", translator.translate("parser.missing.message")); + } + + @Test public void switchingLanguagesShouldChangeInstances() { + Translator.setLanguage(""); + Translator translator = Translator.getInstance("test"); + + Translator.setLanguage("hu"); + assertEquals("DIFO_HU", translator.translate("parser.simple.message")); + } +} diff --git a/src/test/resources/messages/test.properties b/src/test/resources/messages/test.properties new file mode 100644 index 0000000..c1a61a8 --- /dev/null +++ b/src/test/resources/messages/test.properties @@ -0,0 +1,3 @@ +parser.simple.message=DIFO +parser.parametrized.message=TEST %s +parser.missing.message=TEST_FALLBACK diff --git a/src/test/resources/messages/test_hu.properties b/src/test/resources/messages/test_hu.properties new file mode 100644 index 0000000..8b613f2 --- /dev/null +++ b/src/test/resources/messages/test_hu.properties @@ -0,0 +1,2 @@ +parser.simple.message=DIFO_HU +parser.parametrized.message=TEST HU %s