diff --git a/src/main/java/aquality/selenium/core/application/AqualityModule.java b/src/main/java/aquality/selenium/core/application/AqualityModule.java index 2e44633..12a5d9d 100644 --- a/src/main/java/aquality/selenium/core/application/AqualityModule.java +++ b/src/main/java/aquality/selenium/core/application/AqualityModule.java @@ -1,5 +1,6 @@ package aquality.selenium.core.application; +import aquality.selenium.core.localization.*; import aquality.selenium.core.logging.Logger; import aquality.selenium.core.utilities.ISettingsFile; import aquality.selenium.core.utilities.JsonSettingsFile; @@ -11,7 +12,8 @@ /** * Describes all dependencies which is registered for the project. */ -public class AqualityModule extends AbstractModule { +public class AqualityModule extends AbstractModule + implements ILocalizationModule { private final Provider applicationProvider; @@ -31,6 +33,8 @@ protected void configure() { bind(ITimeoutConfiguration.class).to(TimeoutConfiguration.class).in(Singleton.class); bind(IRetryConfiguration.class).to(RetryConfiguration.class).in(Singleton.class); bind(IElementCacheConfiguration.class).to(ElementCacheConfiguration.class).in(Singleton.class); + bind(ILocalizationManager.class).to(getLocalizationManagerImplementation()).in(Singleton.class); + bind(ILocalizedLogger.class).to(getLocalizedLoggerImplementation()).in(Singleton.class); } /** diff --git a/src/main/java/aquality/selenium/core/localization/ILocalizationManager.java b/src/main/java/aquality/selenium/core/localization/ILocalizationManager.java new file mode 100644 index 0000000..776d3e0 --- /dev/null +++ b/src/main/java/aquality/selenium/core/localization/ILocalizationManager.java @@ -0,0 +1,14 @@ +package aquality.selenium.core.localization; + +/** + * This interface is used for translation messages to different languages. + */ +public interface ILocalizationManager { + /** + * Gets localized message from resources by its key. + * @param messageKey Key in resource file. + * @param args Arguments, which will be provided to template of localized message. + * @return Localized message. + */ + String getLocalizedMessage(String messageKey, Object... args); +} diff --git a/src/main/java/aquality/selenium/core/localization/ILocalizationModule.java b/src/main/java/aquality/selenium/core/localization/ILocalizationModule.java new file mode 100644 index 0000000..bce1f43 --- /dev/null +++ b/src/main/java/aquality/selenium/core/localization/ILocalizationModule.java @@ -0,0 +1,20 @@ +package aquality.selenium.core.localization; + +/** + * Describes implementations of localization services to be registered in DI container. + */ +public interface ILocalizationModule { + /** + * @return class which implements ILocalizationManager + */ + default Class getLocalizationManagerImplementation() { + return LocalizationManager.class; + } + + /** + * @return class which implements ILocalizedLogger + */ + default Class getLocalizedLoggerImplementation() { + return LocalizedLogger.class; + } +} diff --git a/src/main/java/aquality/selenium/core/localization/ILocalizedLogger.java b/src/main/java/aquality/selenium/core/localization/ILocalizedLogger.java new file mode 100644 index 0000000..d4cc70e --- /dev/null +++ b/src/main/java/aquality/selenium/core/localization/ILocalizedLogger.java @@ -0,0 +1,59 @@ +package aquality.selenium.core.localization; + +/** + * Log messages in current language. + */ +public interface ILocalizedLogger { + /** + * Logs localized message for action with INFO level which is applied for element, for example, click, send keys etc. + * @param elementType Type of the element. + * @param elementName Name of the element. + * @param messageKey Key in resource file. + * @param args Arguments, which will be provided to template of localized message. + */ + void infoElementAction(String elementType, String elementName, String messageKey, Object... args); + + /** + * Logs localized message with INFO level. + * @param messageKey Key in resource file. + * @param args Arguments, which will be provided to template of localized message. + */ + void info(String messageKey, Object... args); + + /** + * Logs localized message with DEBUG level. + * @param messageKey Key in resource file. + * @param args Arguments, which will be provided to template of localized message. + */ + void debug(String messageKey, Object... args); + + /** + * Logs localized message with DEBUG level. + * @param messageKey Key in resource file. + * @param throwable Throwable to log. + * @param args Arguments, which will be provided to template of localized message. + */ + void debug(String messageKey, Throwable throwable, Object... args); + + /** + * Logs localized message with WARN level. + * @param messageKey Key in resource file. + * @param args Arguments, which will be provided to template of localized message. + */ + void warn(String messageKey, Object... args); + + /** + * Logs localized message with ERROR level. + * @param messageKey Key in resource file. + * @param args Arguments, which will be provided to template of localized message. + */ + void error(String messageKey, Object... args); + + /** + * Logs localized message with FATAL level. + * @param messageKey Key in resource file. + * @param throwable Throwable to log. + * @param args Arguments, which will be provided to template of localized message. + */ + void fatal(String messageKey, Throwable throwable, Object... args); +} diff --git a/src/main/java/aquality/selenium/core/localization/LocalizationManager.java b/src/main/java/aquality/selenium/core/localization/LocalizationManager.java new file mode 100644 index 0000000..1395b50 --- /dev/null +++ b/src/main/java/aquality/selenium/core/localization/LocalizationManager.java @@ -0,0 +1,34 @@ +package aquality.selenium.core.localization; + +import aquality.selenium.core.configurations.ILoggerConfiguration; +import aquality.selenium.core.logging.Logger; +import aquality.selenium.core.utilities.ISettingsFile; +import aquality.selenium.core.utilities.JsonSettingsFile; +import com.google.inject.Inject; + +public class LocalizationManager implements ILocalizationManager { + private static final String LANG_RESOURCE_TEMPLATE = "localization/%1$s.json"; + private final ISettingsFile localizationFile; + private final Logger logger; + private final String locResourceName; + + @Inject + public LocalizationManager(ILoggerConfiguration loggerConfiguration, Logger logger) { + this.logger = logger; + String language = loggerConfiguration.getLanguage(); + locResourceName = String.format(LANG_RESOURCE_TEMPLATE, language.toLowerCase()); + localizationFile = new JsonSettingsFile(locResourceName); + } + + @Override + public String getLocalizedMessage(String messageKey, Object... args) { + String jsonKeyPath = "/".concat(messageKey); + if (localizationFile.isValuePresent(jsonKeyPath)) { + return String.format(localizationFile.getValue(jsonKeyPath).toString(), args); + } + + logger.warn(String.format("Cannot find localized message by key '%1$s' in resource file %2$s", + jsonKeyPath, locResourceName)); + return messageKey; + } +} diff --git a/src/main/java/aquality/selenium/core/localization/LocalizedLogger.java b/src/main/java/aquality/selenium/core/localization/LocalizedLogger.java new file mode 100644 index 0000000..d9e5bc9 --- /dev/null +++ b/src/main/java/aquality/selenium/core/localization/LocalizedLogger.java @@ -0,0 +1,56 @@ +package aquality.selenium.core.localization; + +import aquality.selenium.core.logging.Logger; +import com.google.inject.Inject; + +public class LocalizedLogger implements ILocalizedLogger { + + private final ILocalizationManager localizationManager; + private final Logger logger; + + @Inject + public LocalizedLogger(ILocalizationManager localizationManager, Logger logger) { + this.localizationManager = localizationManager; + this.logger = logger; + } + + private String localizeMessage(String messageKey, Object... args) { + return localizationManager.getLocalizedMessage(messageKey, args); + } + + @Override + public void infoElementAction(String elementType, String elementName, String messageKey, Object... args) { + String message = String.format("%1$s '%2$s' :: %3$s", elementType, elementName, localizeMessage(messageKey, args)); + logger.info(message); + } + + @Override + public void info(String messageKey, Object... args) { + logger.info(localizeMessage(messageKey, args)); + } + + @Override + public void debug(String messageKey, Object... args) { + logger.debug(localizeMessage(messageKey, args)); + } + + @Override + public void debug(String messageKey, Throwable throwable, Object... args) { + logger.debug(localizeMessage(messageKey, args), throwable); + } + + @Override + public void warn(String messageKey, Object... args) { + logger.warn(localizeMessage(messageKey, args)); + } + + @Override + public void error(String messageKey, Object... args) { + logger.error(localizeMessage(messageKey, args)); + } + + @Override + public void fatal(String messageKey, Throwable throwable, Object... args) { + logger.fatal(localizeMessage(messageKey, args), throwable); + } +} diff --git a/src/main/resources/localization/be.json b/src/main/resources/localization/be.json new file mode 100644 index 0000000..b364b8a --- /dev/null +++ b/src/main/resources/localization/be.json @@ -0,0 +1,11 @@ +{ + "loc.clicking": "Націскаем", + "loc.el.getattr": "Атрымліваем атрыбут '%1$s'", + "loc.get.text": "Атрымліваем тэкст элемента", + "loc.text.sending.keys": "Націскаем клавішы '%1$s'", + "loc.no.elements.found.in.state": "Не знайшлі элементаў па лакатару '%1$s' у %2$s стане", + "loc.no.elements.found.by.locator": "Не знайшлі элементаў па лакатару '%1$s'", + "loc.elements.were.found.but.not.in.state": "Знайшлі элементы па лакатару '%1$s', але яны не ў жаданым стане %2$s", + "loc.elements.found.but.should.not": "Не павінна быць знойдзена элементаў па лакатару '%1$s' у %2$s стане", + "loc.search.of.elements.failed": "Пошук элемента па лакатару '%1$s' прайшоў няўдала" +} \ No newline at end of file diff --git a/src/main/resources/localization/en.json b/src/main/resources/localization/en.json new file mode 100644 index 0000000..0021326 --- /dev/null +++ b/src/main/resources/localization/en.json @@ -0,0 +1,11 @@ +{ + "loc.clicking": "Clicking", + "loc.el.getattr": "Getting attribute '%1$s'", + "loc.get.text": "Getting text from element", + "loc.text.sending.keys": "Sending keys '%1$s'", + "loc.no.elements.found.in.state": "No elements with locator '%1$s' were found in %2$s state", + "loc.no.elements.found.by.locator": "No elements were found by locator '%1$s'", + "loc.elements.were.found.but.not.in.state": "Elements were found by locator '%1$s' but not in desired state %2$s", + "loc.elements.found.but.should.not": "No elements should be found by locator '%1$s' in %2$s state", + "loc.search.of.elements.failed": "Search of element by locator '%1$s' failed" +} \ No newline at end of file diff --git a/src/main/resources/localization/ru.json b/src/main/resources/localization/ru.json new file mode 100644 index 0000000..c1eba70 --- /dev/null +++ b/src/main/resources/localization/ru.json @@ -0,0 +1,11 @@ +{ + "loc.clicking": "Клик", + "loc.el.getattr": "Получение аттрибута '%1$s'", + "loc.get.text": "Получение текста элемента", + "loc.text.sending.keys": "Нажатие клавиши '%1$s'", + "loc.no.elements.found.in.state": "Не удалось найти элементов по локатору '%1$s' в %2$s состоянии", + "loc.no.elements.found.by.locator": "Не удалось найти элементов по локатору '%1$s'", + "loc.elements.were.found.but.not.in.state": "Удалось найти элементы по локатору '%1$s', но они не в желаемом состоянии %2$s", + "loc.elements.found.but.should.not": "Не должно быть найдено элементов по локатору '%1$s' в %2$s состоянии", + "loc.search.of.elements.failed": "Поиск элемента по локатору '%1$s' прошел неудачно" +} \ No newline at end of file diff --git a/src/main/resources/log4j.properties b/src/main/resources/log4j.properties index 0b181b8..f451a2e 100644 --- a/src/main/resources/log4j.properties +++ b/src/main/resources/log4j.properties @@ -1,13 +1,15 @@ -log4j.rootLogger=INFO, stdout, file +log4j.rootLogger=DEBUG, stdout, file log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.encoding=UTF-8 log4j.appender.stdout.target=System.out log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p - %m%n +log4j.appender.stdout.Threshold=INFO log4j.appender.file = org.apache.log4j.RollingFileAppender log4j.appender.file.File = target/log/log.log log4j.appender.file.MaxFileSize=10MB log4j.appender.file.layout=org.apache.log4j.PatternLayout -log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p - %m%n \ No newline at end of file +log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p - %m%n +log4j.appender.file.Threshold=DEBUG \ No newline at end of file diff --git a/src/test/java/tests/localization/LocalizationManagerTests.java b/src/test/java/tests/localization/LocalizationManagerTests.java new file mode 100644 index 0000000..a8e0d13 --- /dev/null +++ b/src/test/java/tests/localization/LocalizationManagerTests.java @@ -0,0 +1,114 @@ +package tests.localization; + +import aquality.selenium.core.configurations.ILoggerConfiguration; +import aquality.selenium.core.localization.ILocalizationManager; +import aquality.selenium.core.localization.LocalizationManager; +import aquality.selenium.core.logging.Logger; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import tests.application.CustomAqualityServices; +import tests.application.browser.AqualityServices; + +import java.util.MissingFormatArgumentException; + +import static org.testng.Assert.assertEquals; + +public class LocalizationManagerTests { + private static final String[] SUPPORTED_LANGUAGES = new String[]{"be", "en", "ru"}; + private static final String CLICKING_MESSAGE_KEY = "loc.clicking"; + + @DataProvider + private Object[] keysWithParams() { + return new String[]{ + "loc.el.getattr", + "loc.text.sending.keys", + "loc.no.elements.found.in.state", + "loc.no.elements.found.by.locator", + "loc.elements.were.found.but.not.in.state", + "loc.elements.found.but.should.not", + "loc.search.of.elements.failed"}; + } + + @DataProvider + private Object[] keysWithoutParams() { + return new String[]{ + CLICKING_MESSAGE_KEY, + "loc.get.text"}; + } + + + private LocalizationManager getLocalizationManager() { + return new LocalizationManager( + AqualityServices.getServiceProvider().getInstance(ILoggerConfiguration.class), + Logger.getInstance()); + } + + private LocalizationManager getLocalizationManager(String language) { + return new LocalizationManager(() -> language, Logger.getInstance()); + } + + @Test + public void testShouldReturnUnknownKey() { + String unknownKey = "loc.unknown.fake.key"; + Assert.assertEquals(unknownKey, getLocalizationManager().getLocalizedMessage(unknownKey)); + } + + @Test + public void testShouldBeRegisteredAsSingleton() { + assertEquals(CustomAqualityServices.getServiceProvider().getInstance(ILocalizationManager.class), + CustomAqualityServices.getServiceProvider().getInstance(ILocalizationManager.class)); + } + + @Test + public void testShouldBePossibleToUseForClicking() { + assertEquals(getLocalizationManager().getLocalizedMessage(CLICKING_MESSAGE_KEY), "Clicking", + "Logger should be configured in English by default and return valid value"); + } + + @Test + public void testShouldBePossibleToUseForClickingWithCustomLanguage() { + assertEquals(getLocalizationManager("be").getLocalizedMessage(CLICKING_MESSAGE_KEY), "Націскаем", + "Logger should be configured in custom language when use custom profile, and return valid value"); + } + + @Test(dataProvider = "keysWithParams") + public void testShouldThrowFormatExceptionWhenKeysRequireParams(String keyWithParams) { + for (String language: SUPPORTED_LANGUAGES) { + Assert.assertThrows(MissingFormatArgumentException.class, + () -> getLocalizationManager(language).getLocalizedMessage(keyWithParams)); + } + } + + @Test(dataProvider = "keysWithoutParams") + public void testShouldReturnNonKeyAndNonEmptyValuesForKeysWithoutParams(String keyWithoutParams) { + for (String language: SUPPORTED_LANGUAGES) { + String value = getLocalizationManager(language).getLocalizedMessage(keyWithoutParams); + Assert.assertFalse(value.isEmpty(), + String.format("value of key %1$s in language %2$s should not be empty", keyWithoutParams, language)); + Assert.assertNotEquals(value, keyWithoutParams, + String.format("value of key %1$s in language %2$s should be defined", keyWithoutParams, language)); + } + } + + @Test(dataProvider = "keysWithParams") + public void testShouldReturnNonKeyAndNonEmptyValuesForKeysWithParams(String keyWithParams) { + for (String language: SUPPORTED_LANGUAGES) { + Object[] params = new String[] { "a", "b", "c" }; + String value = getLocalizationManager(language).getLocalizedMessage(keyWithParams, params); + Assert.assertFalse(value.isEmpty(), + String.format("value of key %1$s in language %2$s should not be empty", keyWithParams, language)); + Assert.assertNotEquals(value, keyWithParams, + String.format("value of key %1$s in language %2$s should be defined", keyWithParams, language)); + Assert.assertTrue(value.contains(params[0].toString()), + String.format("value of key %1$s in language %2$s should contain at least first parameter", + keyWithParams, language)); + } + } + + @Test + public void testShouldThrowWhenInvalidLanguageSupplied() { + Assert.assertThrows(IllegalArgumentException.class, () -> + getLocalizationManager("invalid").getLocalizedMessage(CLICKING_MESSAGE_KEY)); + } +} diff --git a/src/test/resources/TestSuite.xml b/src/test/resources/TestSuite.xml index d464978..6cbacac 100644 --- a/src/test/resources/TestSuite.xml +++ b/src/test/resources/TestSuite.xml @@ -10,6 +10,7 @@ +