diff --git a/README.md b/README.md index 5a70853..2c26144 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,8 @@ import org.densy.polyglot.api.util.LanguageStrategy; import org.densy.polyglot.common.language.SimpleLanguage; import org.densy.polyglot.core.context.BaseTranslationContext; import org.densy.polyglot.core.language.SimpleLanguageStandard; -import org.densy.polyglot.core.parameter.KeyedTrParameters; +import org.densy.polyglot.core.parameter.KeyedTranslationParameters; +import org.densy.polyglot.core.parameter.TrParameters; import org.densy.polyglot.core.provider.YamlFileProvider; import java.io.File; @@ -46,7 +47,7 @@ translation.addTranslation(SimpleLanguage.ENG, "local.translation", "Local messa // Translating the messages String local = translation.translate(SimpleLanguage.RUS, "local.translation", 1); -String global = translation.translate(SimpleLanguage.RUS, "global.translation", new KeyedTrParameters().put("local", "Parameter")); +String global = translation.translate(SimpleLanguage.RUS, "global.translation", TrParameters.keyed().put("local", "Parameter")); System.out.println("Translated local message: " + local); System.out.println("Translated global message: " + global); @@ -68,7 +69,7 @@ Adding a library api: org.densy.polyglot api - 1.0.5-SNAPSHOT + 1.0.5-SNAPSHOT ``` diff --git a/api/src/main/java/org/densy/polyglot/api/Translation.java b/api/src/main/java/org/densy/polyglot/api/Translation.java index 33d737f..033713a 100644 --- a/api/src/main/java/org/densy/polyglot/api/Translation.java +++ b/api/src/main/java/org/densy/polyglot/api/Translation.java @@ -1,32 +1,83 @@ package org.densy.polyglot.api; +import org.densy.polyglot.api.formatter.TranslationFormatterAware; import org.densy.polyglot.api.language.Language; -import org.densy.polyglot.api.parameter.TrParameters; -import org.densy.polyglot.api.parameter.formatter.TrParameterFormatter; +import org.densy.polyglot.api.parameter.TranslationParameters; import org.densy.polyglot.api.util.FallbackStrategy; import org.densy.polyglot.api.util.LanguageStrategy; import java.util.Set; -public interface Translation { +/** + * Translation interface. Provides methods for translating keys into localized strings + * with support for parameters, formatters, and fallback strategies. + */ +public interface Translation extends TranslationFormatterAware, TranslationsAware { + /** + * Translates a key into the target language. + * + * @param language the target language + * @param key the translation key + * @return translated string, or the key itself if translation not found + */ String translate(Language language, String key); - String translate(Language language, String key, TrParameters parameters); + /** + * Translates a key into the target language with parameters. + * + * @param language the target language + * @param key the translation key + * @param parameters the translation parameters + * @return translated and formatted string + */ + String translate(Language language, String key, TranslationParameters parameters); + /** + * Translates a key into the target language with array parameters. + * Parameters are accessible as {0}, {1}, {2}, etc. + * + * @param language the target language + * @param key the translation key + * @param parameters the array parameters + * @return translated and formatted string + */ String translate(Language language, String key, Object... parameters); + /** + * Gets the default language for this translation. + * + * @return default language + */ Language getDefaultLanguage(); + /** + * Sets the default language for this translation. + * + * @param language the language to set as default + */ void setDefaultLanguage(Language language); + /** + * Sets the language strategy that determines which language to use + * when the requested language is not available. + * + * @param languageStrategy the language strategy + */ void setLanguageStrategy(LanguageStrategy languageStrategy); + /** + * Sets the fallback strategy that determines what to return + * when a translation key is not found. + * + * @param fallbackStrategy the fallback strategy + */ void setFallbackStrategy(FallbackStrategy fallbackStrategy); - void addTranslation(Language language, String key, String value); - - void setParameterFormatter(Class parameterType, TrParameterFormatter formatter); - + /** + * Gets all languages that have at least one translation in this instance. + * + * @return set of available languages + */ Set getAvailableLanguages(); -} +} \ No newline at end of file diff --git a/api/src/main/java/org/densy/polyglot/api/TranslationsAware.java b/api/src/main/java/org/densy/polyglot/api/TranslationsAware.java new file mode 100644 index 0000000..fede89e --- /dev/null +++ b/api/src/main/java/org/densy/polyglot/api/TranslationsAware.java @@ -0,0 +1,45 @@ +package org.densy.polyglot.api; + +import org.densy.polyglot.api.language.Language; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.Map; + +/** + * Interface for managing translation storage. + */ +public interface TranslationsAware { + + /** + * Gets all translations organized by language and key. + * + * @return unmodifiable map of translations (Language -> Key -> Translation) + */ + @UnmodifiableView + Map> getTranslations(); + + /** + * Adds a single translation for a specific language and key. + * + * @param language the target language + * @param key the translation key + * @param value the translation value + */ + void addTranslation(Language language, String key, String value); + + /** + * Adds multiple translations for a specific language. + * + * @param language the target language + * @param translations map of translation keys to values + */ + void addTranslations(Language language, Map translations); + + /** + * Removes a translation for a specific language and key. + * + * @param language the target language + * @param key the translation key to remove + */ + void removeTranslation(Language language, String key); +} \ No newline at end of file diff --git a/api/src/main/java/org/densy/polyglot/api/context/TranslationContext.java b/api/src/main/java/org/densy/polyglot/api/context/TranslationContext.java index a590cc2..49d5c33 100644 --- a/api/src/main/java/org/densy/polyglot/api/context/TranslationContext.java +++ b/api/src/main/java/org/densy/polyglot/api/context/TranslationContext.java @@ -45,7 +45,7 @@ public interface TranslationContext { /** * Adds global parameter available to all translations. * - * @param key the parameter key + * @param key the parameter key * @param value the parameter value to add */ void addGlobalParameter(String key, Object value); @@ -67,7 +67,7 @@ public interface TranslationContext { /** * Adds global translations for a language. * - * @param language the target language + * @param language the target language * @param translations the translations to add */ void addGlobalTranslations(Language language, Map translations); @@ -75,8 +75,8 @@ public interface TranslationContext { /** * Adds global translation for a language. * - * @param language the target language - * @param key the message key + * @param language the target language + * @param key the message key * @param translation the translation */ void addGlobalTranslation(Language language, String key, String translation); diff --git a/api/src/main/java/org/densy/polyglot/api/formatter/TranslationFormatter.java b/api/src/main/java/org/densy/polyglot/api/formatter/TranslationFormatter.java new file mode 100644 index 0000000..3133f40 --- /dev/null +++ b/api/src/main/java/org/densy/polyglot/api/formatter/TranslationFormatter.java @@ -0,0 +1,18 @@ +package org.densy.polyglot.api.formatter; + +import org.densy.polyglot.api.formatter.context.TranslationFormatContext; + +/** + * Translation post formatter interface. + */ +public interface TranslationFormatter { + + /** + * Formats translation text and by applies parameters if it needs. + * + * @param text the text to format + * @param context the translation format context + * @return formatted text + */ + String format(String text, TranslationFormatContext context); +} diff --git a/api/src/main/java/org/densy/polyglot/api/formatter/TranslationFormatterAware.java b/api/src/main/java/org/densy/polyglot/api/formatter/TranslationFormatterAware.java new file mode 100644 index 0000000..b991f0b --- /dev/null +++ b/api/src/main/java/org/densy/polyglot/api/formatter/TranslationFormatterAware.java @@ -0,0 +1,34 @@ +package org.densy.polyglot.api.formatter; + +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.List; + +/** + * Interface for managing translation formatters. + */ +public interface TranslationFormatterAware { + + /** + * Gets the list of registered formatters. + * Formatters are applied in the order they appear in this list. + * + * @return unmodifiable list of formatters + */ + @UnmodifiableView + List getFormatters(); + + /** + * Adds a formatter to the end of the formatter chain. + * + * @param formatter the formatter to add + */ + void addFormatter(TranslationFormatter formatter); + + /** + * Removes a formatter from the formatter chain. + * + * @param formatter the formatter to remove + */ + void removeFormatter(TranslationFormatter formatter); +} \ No newline at end of file diff --git a/api/src/main/java/org/densy/polyglot/api/formatter/context/TranslationFormatContext.java b/api/src/main/java/org/densy/polyglot/api/formatter/context/TranslationFormatContext.java new file mode 100644 index 0000000..65ab2ca --- /dev/null +++ b/api/src/main/java/org/densy/polyglot/api/formatter/context/TranslationFormatContext.java @@ -0,0 +1,39 @@ +package org.densy.polyglot.api.formatter.context; + +import org.densy.polyglot.api.Translation; +import org.densy.polyglot.api.language.Language; +import org.densy.polyglot.api.parameter.TranslationParameters; + +/** + * Translation formatting context. + */ +public interface TranslationFormatContext { + + /** + * Translation key. + * + * @return the translation key + */ + String getKey(); + + /** + * Translation language. + * + * @return the Language object + */ + Language getLanguage(); + + /** + * Translation itself. + * + * @return the Translation object + */ + Translation getTranslation(); + + /** + * Translation parameters. + * + * @return the TranslationParameters object + */ + TranslationParameters getParameters(); +} diff --git a/api/src/main/java/org/densy/polyglot/api/parameter/TrParameters.java b/api/src/main/java/org/densy/polyglot/api/parameter/TrParameters.java deleted file mode 100644 index c53786e..0000000 --- a/api/src/main/java/org/densy/polyglot/api/parameter/TrParameters.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.densy.polyglot.api.parameter; - -import org.densy.polyglot.api.parameter.formatter.TrParameterFormatter; - -/** - * Base interface for translation parameters. - */ -public interface TrParameters { - - /** - * Applies parameters to text using the specified formatter. - * - * @param text the text to format - * @param formatter the parameter formatter - * @return formatted text - */ - String applyTo(String text, TrParameterFormatter formatter); -} diff --git a/api/src/main/java/org/densy/polyglot/api/parameter/TranslationParameters.java b/api/src/main/java/org/densy/polyglot/api/parameter/TranslationParameters.java new file mode 100644 index 0000000..b2b880b --- /dev/null +++ b/api/src/main/java/org/densy/polyglot/api/parameter/TranslationParameters.java @@ -0,0 +1,8 @@ +package org.densy.polyglot.api.parameter; + +/** + * Base interface for translation parameters. + */ +public interface TranslationParameters { + +} diff --git a/api/src/main/java/org/densy/polyglot/api/parameter/formatter/TrParameterFormatter.java b/api/src/main/java/org/densy/polyglot/api/parameter/formatter/TrParameterFormatter.java deleted file mode 100644 index c23e024..0000000 --- a/api/src/main/java/org/densy/polyglot/api/parameter/formatter/TrParameterFormatter.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.densy.polyglot.api.parameter.formatter; - -import org.densy.polyglot.api.parameter.TrParameters; - -/** - * Parameter formatter interface. - */ -public interface TrParameterFormatter { - - /** - * Formats text by applying parameters. - * - * @param text the text to format - * @param parameters the parameters to apply - * @return formatted text - */ - String format(String text, TrParameters parameters); - - /** - * Gets the parameter type supported by this formatter. - * - * @return supported parameter type class - */ - Class getSupportedParameterType(); -} diff --git a/build.gradle.kts b/build.gradle.kts index 24abfb1..ae86d6c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,7 @@ java { allprojects { group = "org.densy.polyglot" - version = "1.0.5-SNAPSHOT" + version = "1.1.0-SNAPSHOT" } subprojects { @@ -26,6 +26,7 @@ subprojects { } dependencies { + compileOnlyApi("org.jetbrains:annotations:24.1.0") compileOnlyApi("org.projectlombok:lombok:1.18.38") annotationProcessor("org.projectlombok:lombok:1.18.38") } diff --git a/core/src/main/java/org/densy/polyglot/core/BaseTranslation.java b/core/src/main/java/org/densy/polyglot/core/BaseTranslation.java index 574a1f3..919aae9 100644 --- a/core/src/main/java/org/densy/polyglot/core/BaseTranslation.java +++ b/core/src/main/java/org/densy/polyglot/core/BaseTranslation.java @@ -1,22 +1,22 @@ package org.densy.polyglot.core; -import org.densy.polyglot.api.util.FallbackStrategy; import org.densy.polyglot.api.Translation; import org.densy.polyglot.api.context.TranslationContext; +import org.densy.polyglot.api.formatter.TranslationFormatter; import org.densy.polyglot.api.language.Language; -import org.densy.polyglot.api.parameter.TrParameters; -import org.densy.polyglot.api.parameter.formatter.TrParameterFormatter; +import org.densy.polyglot.api.parameter.TranslationParameters; import org.densy.polyglot.api.provider.TranslationProvider; +import org.densy.polyglot.api.util.FallbackStrategy; import org.densy.polyglot.api.util.LanguageStrategy; -import org.densy.polyglot.core.parameter.KeyedTrParameters; -import org.densy.polyglot.core.parameter.SimpleTrParameters; -import org.densy.polyglot.core.parameter.formatter.BraceKeyedParameterFormatter; -import org.densy.polyglot.core.parameter.formatter.BracketSimpleParameterFormatter; +import org.densy.polyglot.core.formatter.EscapeSequenceFormatter; +import org.densy.polyglot.core.formatter.NestedTranslationFormatter; +import org.densy.polyglot.core.formatter.PatternArrayParameterFormatter; +import org.densy.polyglot.core.formatter.PatternKeyedParameterFormatter; +import org.densy.polyglot.core.formatter.context.TranslationFormatContextImpl; +import org.densy.polyglot.core.parameter.ArrayTranslationParameters; +import org.jetbrains.annotations.UnmodifiableView; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; /** * Base implementation of the translation. @@ -25,7 +25,7 @@ public class BaseTranslation implements Translation { private final TranslationContext context; private final Map> translations; - private final Map, TrParameterFormatter> formatters; + private final List formatters; private Language defaultLanguage; private LanguageStrategy languageStrategy; @@ -34,10 +34,15 @@ public class BaseTranslation implements Translation { public BaseTranslation(TranslationContext context, TranslationProvider provider) { this.context = context; this.translations = new HashMap<>(); - this.formatters = new HashMap<>(); + this.formatters = new ArrayList<>(); - this.formatters.put(SimpleTrParameters.class, new BracketSimpleParameterFormatter()); - this.formatters.put(KeyedTrParameters.class, new BraceKeyedParameterFormatter()); + // Adding default formatters. + // Order is important: the array parameter formatter + // must be before the keyed parameter formatter. + this.addFormatter(new PatternArrayParameterFormatter()); + this.addFormatter(new PatternKeyedParameterFormatter(context)); + this.addFormatter(new NestedTranslationFormatter()); + this.addFormatter(new EscapeSequenceFormatter()); if (provider != null) { translations.putAll(provider.getTranslations()); @@ -48,11 +53,11 @@ public BaseTranslation(TranslationContext context, TranslationProvider provider) @Override public String translate(Language language, String key) { - return translate(language, key, (TrParameters) null); + return translate(language, key, (TranslationParameters) null); } @Override - public String translate(Language language, String key, TrParameters parameters) { + public String translate(Language language, String key, TranslationParameters parameters) { Language targetLanguage = resolveLanguage(language); String translation = findTranslation(targetLanguage, key); @@ -60,12 +65,12 @@ public String translate(Language language, String key, TrParameters parameters) translation = fallbackStrategy != null ? fallbackStrategy.get(key) : key; } - return applyParameters(translation, parameters); + return applyParameters(language, key, translation, parameters); } @Override public String translate(Language language, String key, Object... parameters) { - return translate(language, key, new SimpleTrParameters(parameters)); + return translate(language, key, new ArrayTranslationParameters(parameters)); } private Language resolveLanguage(Language requestedLanguage) { @@ -136,33 +141,14 @@ private String findTranslation(Language language, String key) { return null; } - protected String applyParameters(String text, TrParameters parameters) { + protected String applyParameters(Language language, String key, String text, TranslationParameters parameters) { if (text == null) return null; - String result = text; - - if (parameters instanceof SimpleTrParameters) { - TrParameterFormatter simpleFormatter = formatters.get(SimpleTrParameters.class); - if (simpleFormatter != null) { - result = simpleFormatter.format(result, parameters); - } - } - - KeyedTrParameters keyedParams; - - if (parameters instanceof KeyedTrParameters keyedTrParameters) { - KeyedTrParameters merged = new KeyedTrParameters(context.getGlobalParameters()); - merged = merged.merge(keyedTrParameters); - keyedParams = merged; - } else { - keyedParams = new KeyedTrParameters(context.getGlobalParameters()); - } + var context = new TranslationFormatContextImpl(key, language, this, parameters); - if (keyedParams != null && !keyedParams.getParameters().isEmpty()) { - TrParameterFormatter keyedFormatter = formatters.get(KeyedTrParameters.class); - if (keyedFormatter != null) { - result = keyedFormatter.format(result, keyedParams); - } + String result = text; + for (TranslationFormatter formatter : formatters) { + result = formatter.format(result, context); } return result; @@ -188,19 +174,55 @@ public void setFallbackStrategy(FallbackStrategy fallbackStrategy) { this.fallbackStrategy = fallbackStrategy; } + @Override + public @UnmodifiableView Map> getTranslations() { + return Collections.unmodifiableMap(translations); + } + @Override public void addTranslation(Language language, String key, String value) { if (language == null || key == null || value == null) { return; } - translations.computeIfAbsent(language, k -> new HashMap<>()).put(key, value); + this.translations.computeIfAbsent(language, k -> new HashMap<>()).put(key, value); + } + + @Override + public void addTranslations(Language language, Map translations) { + if (language == null || translations == null) { + return; + } + this.translations.computeIfAbsent(language, k -> new HashMap<>()).putAll(translations); } @Override - public void setParameterFormatter(Class parameterType, TrParameterFormatter formatter) { - if (parameterType != null && formatter != null) { - formatters.put(parameterType, formatter); + public void removeTranslation(Language language, String key) { + if (language == null || key == null) { + return; } + if (!translations.containsKey(language)) { + return; + } + translations.get(language).remove(key); + // remove empty language map + if (translations.get(language).isEmpty()) { + translations.remove(language); + } + } + + @Override + public @UnmodifiableView List getFormatters() { + return Collections.unmodifiableList(formatters); + } + + @Override + public void addFormatter(TranslationFormatter formatter) { + formatters.add(formatter); + } + + @Override + public void removeFormatter(TranslationFormatter formatter) { + formatters.remove(formatter); } @Override diff --git a/core/src/main/java/org/densy/polyglot/core/context/BaseTranslationContext.java b/core/src/main/java/org/densy/polyglot/core/context/BaseTranslationContext.java index 4207510..760f949 100644 --- a/core/src/main/java/org/densy/polyglot/core/context/BaseTranslationContext.java +++ b/core/src/main/java/org/densy/polyglot/core/context/BaseTranslationContext.java @@ -7,7 +7,7 @@ import org.densy.polyglot.api.provider.TranslationProvider; import org.densy.polyglot.core.BaseTranslation; import org.densy.polyglot.core.language.SimpleLanguageStandard; -import org.densy.polyglot.core.parameter.KeyedTrParameters; +import org.densy.polyglot.core.parameter.KeyedTranslationParameters; import org.densy.polyglot.core.provider.EmptyProvider; import java.util.HashMap; @@ -19,13 +19,13 @@ public class BaseTranslationContext implements TranslationContext { private final Map> globalTranslations; - private KeyedTrParameters globalParameters; + private KeyedTranslationParameters globalParameters; private Language defaultLanguage; private LanguageStandard languageStandard; public BaseTranslationContext() { this.globalTranslations = new HashMap<>(); - this.globalParameters = new KeyedTrParameters(); + this.globalParameters = new KeyedTranslationParameters(); this.languageStandard = new SimpleLanguageStandard(); } @@ -46,7 +46,7 @@ public Map getGlobalParameters() { @Override public void addGlobalParameters(Map parameters) { - this.globalParameters = this.globalParameters.merge(new KeyedTrParameters(parameters)); + this.globalParameters = this.globalParameters.merge(new KeyedTranslationParameters(parameters)); } @Override diff --git a/core/src/main/java/org/densy/polyglot/core/formatter/EscapeSequenceFormatter.java b/core/src/main/java/org/densy/polyglot/core/formatter/EscapeSequenceFormatter.java new file mode 100644 index 0000000..d182c5d --- /dev/null +++ b/core/src/main/java/org/densy/polyglot/core/formatter/EscapeSequenceFormatter.java @@ -0,0 +1,46 @@ +package org.densy.polyglot.core.formatter; + +import org.densy.polyglot.api.formatter.context.TranslationFormatContext; +import org.densy.polyglot.api.formatter.TranslationFormatter; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Post-processor formatter that removes escape sequences. + * This should be the LAST formatter in the chain. + *

+ * Converts: + * - \{param} -> {param} + * - \\{param} -> \{param} + * - \\\{param} -> \{param} + * - \\\\{param} -> \\{param} + *

+ * In general: removes one backslash from each pair/sequence of backslashes + */ +public class EscapeSequenceFormatter implements TranslationFormatter { + private static final Pattern ESCAPE_PATTERN = Pattern.compile("\\\\+"); + + @Override + public String format(String text, TranslationFormatContext context) { + Matcher matcher = ESCAPE_PATTERN.matcher(text); + StringBuilder result = new StringBuilder(); + + while (matcher.find()) { + String backslashes = matcher.group(0); + int count = backslashes.length(); + + // Remove one slash from each pair; if the number is odd, round down. + // \\ -> \ + // \\\ -> \ + // \\\\ -> \\ + int resultCount = count / 2; + String replacement = "\\".repeat(resultCount); + + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } + + matcher.appendTail(result); + return result.toString(); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/densy/polyglot/core/formatter/NestedTranslationFormatter.java b/core/src/main/java/org/densy/polyglot/core/formatter/NestedTranslationFormatter.java new file mode 100644 index 0000000..ac372f4 --- /dev/null +++ b/core/src/main/java/org/densy/polyglot/core/formatter/NestedTranslationFormatter.java @@ -0,0 +1,82 @@ +package org.densy.polyglot.core.formatter; + +import org.densy.polyglot.api.formatter.context.TranslationFormatContext; +import org.densy.polyglot.api.formatter.TranslationFormatter; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Formatter for nested translations in the format {translation-key} + * Allows embedding other translations within a translation string. + *

+ * Example: "Welcome message: {welcome.message}" will be replaced with the translation of "welcome.message" + *

+ * Supports escaping: \{key} will not be replaced + */ +public class NestedTranslationFormatter implements TranslationFormatter { + + public static final Pattern DEFAULT_PATTERN = Pattern.compile("(\\\\*)\\{([^}]+)}"); + + private final Pattern pattern; + private final int maxDepth; + + public NestedTranslationFormatter() { + this(DEFAULT_PATTERN); + } + + public NestedTranslationFormatter(Pattern pattern) { + this(pattern, 5); + } + + public NestedTranslationFormatter(Pattern pattern, int maxDepth) { + this.pattern = pattern; + this.maxDepth = maxDepth; + } + + @Override + public String format(String text, TranslationFormatContext context) { + return formatRecursive(text, context, 0); + } + + private String formatRecursive(String text, TranslationFormatContext context, int depth) { + // Protection against infinite recursion + if (depth >= maxDepth) { + return text; + } + + Matcher matcher = pattern.matcher(text); + StringBuilder result = new StringBuilder(); + + while (matcher.find()) { + String fullMatch = matcher.group(0); + String backslashes = matcher.group(1); + String key = matcher.group(2); + int backslashCount = backslashes.length(); + + boolean isEscaped = backslashCount % 2 == 1; + + String replacement; + if (isEscaped) { + // escaped, so we leave it as it is + replacement = fullMatch; + } else { + String nestedTranslation = context.getTranslation().translate(context.getLanguage(), key, context.getParameters()); + + if (nestedTranslation != null && !nestedTranslation.equals(key)) { + // recursively process nested translations + String processedNested = formatRecursive(nestedTranslation, context, depth + 1); + replacement = backslashes + processedNested; + } else { + // no translation found, so we'll leave it as is. + replacement = fullMatch; + } + } + + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } + + matcher.appendTail(result); + return result.toString(); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/densy/polyglot/core/formatter/PatternArrayParameterFormatter.java b/core/src/main/java/org/densy/polyglot/core/formatter/PatternArrayParameterFormatter.java new file mode 100644 index 0000000..8b435e0 --- /dev/null +++ b/core/src/main/java/org/densy/polyglot/core/formatter/PatternArrayParameterFormatter.java @@ -0,0 +1,60 @@ +package org.densy.polyglot.core.formatter; + +import org.densy.polyglot.api.formatter.context.TranslationFormatContext; +import org.densy.polyglot.api.formatter.TranslationFormatter; +import org.densy.polyglot.core.parameter.ArrayTranslationParameters; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Formatter for simple parameters in the format {0}, {1}, {2}... + */ +public class PatternArrayParameterFormatter implements TranslationFormatter { + public static final Pattern DEFAULT_PATTERN = Pattern.compile("(\\\\*)\\{(\\d+)}"); + + private final Pattern pattern; + + public PatternArrayParameterFormatter() { + this(DEFAULT_PATTERN); + } + + public PatternArrayParameterFormatter(Pattern pattern) { + this.pattern = pattern; + } + + @Override + public String format(String text, TranslationFormatContext context) { + if (!(context.getParameters() instanceof ArrayTranslationParameters simpleParams)) { + return text; + } + + Object[] params = simpleParams.getParameters(); + Matcher matcher = pattern.matcher(text); + + StringBuilder result = new StringBuilder(); + while (matcher.find()) { + String fullMatch = matcher.group(0); + String backslashes = matcher.group(1); + String indexStr = matcher.group(2); + int index = Integer.parseInt(indexStr); + int backslashCount = backslashes.length(); + + boolean isEscaped = backslashCount % 2 == 1; + + String replacement; + if (isEscaped || index < 0 || index >= params.length) { + // if escaped or parameter not found, so leave it as it is + replacement = fullMatch; + } else { + // replace the parameter + replacement = backslashes + params[index]; + } + + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } + matcher.appendTail(result); + + return result.toString(); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/densy/polyglot/core/formatter/PatternKeyedParameterFormatter.java b/core/src/main/java/org/densy/polyglot/core/formatter/PatternKeyedParameterFormatter.java new file mode 100644 index 0000000..077fdc2 --- /dev/null +++ b/core/src/main/java/org/densy/polyglot/core/formatter/PatternKeyedParameterFormatter.java @@ -0,0 +1,68 @@ +package org.densy.polyglot.core.formatter; + +import org.densy.polyglot.api.context.TranslationContext; +import org.densy.polyglot.api.formatter.context.TranslationFormatContext; +import org.densy.polyglot.api.formatter.TranslationFormatter; +import org.densy.polyglot.core.parameter.KeyedTranslationParameters; + +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Formatter for named parameters in the format {key} + */ +public class PatternKeyedParameterFormatter implements TranslationFormatter { + public static final Pattern DEFAULT_PATTERN = Pattern.compile("(\\\\*)\\{([^}]+)}"); + + private final TranslationContext context; + private final Pattern pattern; + + public PatternKeyedParameterFormatter(TranslationContext context) { + this(context, DEFAULT_PATTERN); + } + + public PatternKeyedParameterFormatter(TranslationContext context, Pattern pattern) { + this.context = context; + this.pattern = pattern; + } + + @Override + public String format(String text, TranslationFormatContext formatContext) { + KeyedTranslationParameters keyedParams; + if (formatContext.getParameters() instanceof KeyedTranslationParameters keyedTrParameters) { + KeyedTranslationParameters merged = new KeyedTranslationParameters(context.getGlobalParameters()); + merged = merged.merge(keyedTrParameters); + keyedParams = merged; + } else { + keyedParams = new KeyedTranslationParameters(context.getGlobalParameters()); + } + + Map params = keyedParams.getParameters(); + Matcher matcher = pattern.matcher(text); + StringBuilder result = new StringBuilder(); + + while (matcher.find()) { + String fullMatch = matcher.group(0); + String backslashes = matcher.group(1); + String key = matcher.group(2); + int backslashCount = backslashes.length(); + + boolean isEscaped = backslashCount % 2 == 1; + + String replacement; + if (isEscaped || !params.containsKey(key)) { + // if escaped or parameter not found, so leave it as it is + replacement = fullMatch; + } else { + // replace the parameter + replacement = backslashes + params.get(key); + } + + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } + + matcher.appendTail(result); + return result.toString(); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/densy/polyglot/core/formatter/context/TranslationFormatContextImpl.java b/core/src/main/java/org/densy/polyglot/core/formatter/context/TranslationFormatContextImpl.java new file mode 100644 index 0000000..10357ef --- /dev/null +++ b/core/src/main/java/org/densy/polyglot/core/formatter/context/TranslationFormatContextImpl.java @@ -0,0 +1,18 @@ +package org.densy.polyglot.core.formatter.context; + +import lombok.Data; +import org.densy.polyglot.api.Translation; +import org.densy.polyglot.api.formatter.context.TranslationFormatContext; +import org.densy.polyglot.api.language.Language; +import org.densy.polyglot.api.parameter.TranslationParameters; + +/** + * Implementation of the translation formatting context. + */ +@Data +public class TranslationFormatContextImpl implements TranslationFormatContext { + private final String key; + private final Language language; + private final Translation translation; + private final TranslationParameters parameters; +} diff --git a/core/src/main/java/org/densy/polyglot/core/parameter/ArrayTranslationParameters.java b/core/src/main/java/org/densy/polyglot/core/parameter/ArrayTranslationParameters.java new file mode 100644 index 0000000..77671db --- /dev/null +++ b/core/src/main/java/org/densy/polyglot/core/parameter/ArrayTranslationParameters.java @@ -0,0 +1,17 @@ +package org.densy.polyglot.core.parameter; + +import lombok.Getter; +import org.densy.polyglot.api.parameter.TranslationParameters; + +/** + * Simple indexed translation parameters. + */ +@Getter +public class ArrayTranslationParameters implements TranslationParameters { + + private final Object[] parameters; + + public ArrayTranslationParameters(Object... parameters) { + this.parameters = parameters; + } +} diff --git a/core/src/main/java/org/densy/polyglot/core/parameter/KeyedTrParameters.java b/core/src/main/java/org/densy/polyglot/core/parameter/KeyedTranslationParameters.java similarity index 55% rename from core/src/main/java/org/densy/polyglot/core/parameter/KeyedTrParameters.java rename to core/src/main/java/org/densy/polyglot/core/parameter/KeyedTranslationParameters.java index ce10714..cec5ffe 100644 --- a/core/src/main/java/org/densy/polyglot/core/parameter/KeyedTrParameters.java +++ b/core/src/main/java/org/densy/polyglot/core/parameter/KeyedTranslationParameters.java @@ -1,8 +1,7 @@ package org.densy.polyglot.core.parameter; -import org.densy.polyglot.api.parameter.TrParameters; -import org.densy.polyglot.api.parameter.formatter.TrParameterFormatter; import lombok.Getter; +import org.densy.polyglot.api.parameter.TranslationParameters; import java.util.HashMap; import java.util.Map; @@ -11,39 +10,41 @@ * Key-value translation parameters. */ @Getter -public class KeyedTrParameters implements TrParameters { +public class KeyedTranslationParameters implements TranslationParameters { private final Map parameters; - public KeyedTrParameters() { + public KeyedTranslationParameters() { this.parameters = new HashMap<>(); } - public KeyedTrParameters(Map parameters) { + public KeyedTranslationParameters(Map parameters) { this.parameters = new HashMap<>(parameters != null ? parameters : Map.of()); } - public KeyedTrParameters put(String key, Object value) { + /** + * Sets the value of the parameter by key. + * + * @param key parameter key + * @param value parameter value + * @return this object instance + */ + public KeyedTranslationParameters put(String key, Object value) { parameters.put(key, value); return this; } - @Override - public String applyTo(String text, TrParameterFormatter formatter) { - return formatter.format(text, this); - } - /** * Merges the current KeyedTrParameters with another KeyedTrParameters. * * @param other Another KeyedTrParameters * @return Merged KeyedTrParameters */ - public KeyedTrParameters merge(KeyedTrParameters other) { + public KeyedTranslationParameters merge(KeyedTranslationParameters other) { if (other == null) return this; Map merged = new HashMap<>(other.getParameters()); merged.putAll(this.parameters); - return new KeyedTrParameters(merged); + return new KeyedTranslationParameters(merged); } } diff --git a/core/src/main/java/org/densy/polyglot/core/parameter/SimpleTrParameters.java b/core/src/main/java/org/densy/polyglot/core/parameter/SimpleTrParameters.java deleted file mode 100644 index a2c07be..0000000 --- a/core/src/main/java/org/densy/polyglot/core/parameter/SimpleTrParameters.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.densy.polyglot.core.parameter; - -import org.densy.polyglot.api.parameter.TrParameters; -import org.densy.polyglot.api.parameter.formatter.TrParameterFormatter; -import lombok.Getter; - -/** - * Simple indexed translation parameters. - */ -@Getter -public class SimpleTrParameters implements TrParameters { - - private final Object[] parameters; - - public SimpleTrParameters(Object... parameters) { - this.parameters = parameters; - } - - @Override - public String applyTo(String text, TrParameterFormatter formatter) { - return formatter.format(text, this); - } -} diff --git a/core/src/main/java/org/densy/polyglot/core/parameter/TrParameters.java b/core/src/main/java/org/densy/polyglot/core/parameter/TrParameters.java new file mode 100644 index 0000000..a7858b9 --- /dev/null +++ b/core/src/main/java/org/densy/polyglot/core/parameter/TrParameters.java @@ -0,0 +1,33 @@ +package org.densy.polyglot.core.parameter; + +import lombok.experimental.UtilityClass; + +import java.util.Map; + +/** + * A utility class containing basic parameters for translations. + */ +@UtilityClass +public class TrParameters { + + /** + * Creates an array parameter. + */ + public ArrayTranslationParameters array(Object... parameters) { + return new ArrayTranslationParameters(parameters); + } + + /** + * Creates a keyed parameters from map. + */ + public KeyedTranslationParameters keyed(Map parameters) { + return new KeyedTranslationParameters(parameters); + } + + /** + * Creates an empty keyed parameters. + */ + public KeyedTranslationParameters keyed() { + return new KeyedTranslationParameters(); + } +} diff --git a/core/src/main/java/org/densy/polyglot/core/parameter/formatter/BraceKeyedParameterFormatter.java b/core/src/main/java/org/densy/polyglot/core/parameter/formatter/BraceKeyedParameterFormatter.java deleted file mode 100644 index d5a5469..0000000 --- a/core/src/main/java/org/densy/polyglot/core/parameter/formatter/BraceKeyedParameterFormatter.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.densy.polyglot.core.parameter.formatter; - -import org.densy.polyglot.api.parameter.TrParameters; -import org.densy.polyglot.api.parameter.formatter.TrParameterFormatter; -import org.densy.polyglot.core.parameter.KeyedTrParameters; - -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Formatter for named parameters in the format {key} - */ -public class BraceKeyedParameterFormatter implements TrParameterFormatter { - private static final Pattern PATTERN = Pattern.compile("\\{([^}]+)}"); - - @Override - public String format(String text, TrParameters parameters) { - if (!(parameters instanceof KeyedTrParameters keyedParams)) { - return text; - } - - Map params = keyedParams.getParameters(); - Matcher matcher = PATTERN.matcher(text); - - StringBuilder result = new StringBuilder(); - while (matcher.find()) { - String key = matcher.group(1); - String replacement = ""; - if (params.containsKey(key)) { - replacement = String.valueOf(params.get(key)); - } - matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); - } - matcher.appendTail(result); - - return result.toString(); - } - - @Override - public Class getSupportedParameterType() { - return KeyedTrParameters.class; - } -} - diff --git a/core/src/main/java/org/densy/polyglot/core/parameter/formatter/BracketSimpleParameterFormatter.java b/core/src/main/java/org/densy/polyglot/core/parameter/formatter/BracketSimpleParameterFormatter.java deleted file mode 100644 index be61ec3..0000000 --- a/core/src/main/java/org/densy/polyglot/core/parameter/formatter/BracketSimpleParameterFormatter.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.densy.polyglot.core.parameter.formatter; - -import org.densy.polyglot.api.parameter.TrParameters; -import org.densy.polyglot.api.parameter.formatter.TrParameterFormatter; -import org.densy.polyglot.core.parameter.SimpleTrParameters; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Formatter for simple parameters in the format [0], [1], [2]... - */ -public class BracketSimpleParameterFormatter implements TrParameterFormatter { - private static final Pattern PATTERN = Pattern.compile("\\[(\\d+)]"); - - @Override - public String format(String text, TrParameters parameters) { - if (!(parameters instanceof SimpleTrParameters simpleParams)) { - return text; - } - - Object[] params = simpleParams.getParameters(); - Matcher matcher = PATTERN.matcher(text); - - StringBuilder result = new StringBuilder(); - while (matcher.find()) { - int index = Integer.parseInt(matcher.group(1)); - String replacement = ""; - if (index >= 0 && index < params.length) { - replacement = String.valueOf(params[index]); - } - matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); - } - matcher.appendTail(result); - - return result.toString(); - } - - @Override - public Class getSupportedParameterType() { - return SimpleTrParameters.class; - } -} -