From aaa09a1c6578bdf5349e6ef5f53d1854826589d4 Mon Sep 17 00:00:00 2001 From: MEFRREEX Date: Mon, 26 Jan 2026 01:27:47 +0400 Subject: [PATCH 1/7] feat: fully rework parameter formatters --- .../org/densy/polyglot/api/Translation.java | 12 +- .../densy/polyglot/api/TranslationsAware.java | 18 +++ .../api/formatter/TranslationFormatter.java | 20 ++++ .../formatter/TranslationFormatterAware.java | 15 +++ .../polyglot/api/parameter/TrParameters.java | 18 --- .../api/parameter/TranslationParameters.java | 8 ++ .../formatter/TrParameterFormatter.java | 25 ----- build.gradle.kts | 1 + .../densy/polyglot/core/BaseTranslation.java | 106 ++++++++++-------- .../PatternArrayParameterFormatter.java | 65 +++++++++++ .../PatternKeyedParameterFormatter.java | 74 ++++++++++++ .../core/parameter/ArrayTrParameters.java | 17 +++ .../core/parameter/KeyedTrParameters.java | 10 +- .../core/parameter/SimpleTrParameters.java | 23 ---- .../BraceKeyedParameterFormatter.java | 45 -------- .../BracketSimpleParameterFormatter.java | 44 -------- 16 files changed, 284 insertions(+), 217 deletions(-) create mode 100644 api/src/main/java/org/densy/polyglot/api/TranslationsAware.java create mode 100644 api/src/main/java/org/densy/polyglot/api/formatter/TranslationFormatter.java create mode 100644 api/src/main/java/org/densy/polyglot/api/formatter/TranslationFormatterAware.java delete mode 100644 api/src/main/java/org/densy/polyglot/api/parameter/TrParameters.java create mode 100644 api/src/main/java/org/densy/polyglot/api/parameter/TranslationParameters.java delete mode 100644 api/src/main/java/org/densy/polyglot/api/parameter/formatter/TrParameterFormatter.java create mode 100644 core/src/main/java/org/densy/polyglot/core/formatter/PatternArrayParameterFormatter.java create mode 100644 core/src/main/java/org/densy/polyglot/core/formatter/PatternKeyedParameterFormatter.java create mode 100644 core/src/main/java/org/densy/polyglot/core/parameter/ArrayTrParameters.java delete mode 100644 core/src/main/java/org/densy/polyglot/core/parameter/SimpleTrParameters.java delete mode 100644 core/src/main/java/org/densy/polyglot/core/parameter/formatter/BraceKeyedParameterFormatter.java delete mode 100644 core/src/main/java/org/densy/polyglot/core/parameter/formatter/BracketSimpleParameterFormatter.java 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..1901792 100644 --- a/api/src/main/java/org/densy/polyglot/api/Translation.java +++ b/api/src/main/java/org/densy/polyglot/api/Translation.java @@ -1,18 +1,18 @@ 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 { +public interface Translation extends TranslationFormatterAware, TranslationsAware { String translate(Language language, String key); - String translate(Language language, String key, TrParameters parameters); + String translate(Language language, String key, TranslationParameters parameters); String translate(Language language, String key, Object... parameters); @@ -24,9 +24,5 @@ public interface Translation { void setFallbackStrategy(FallbackStrategy fallbackStrategy); - void addTranslation(Language language, String key, String value); - - void setParameterFormatter(Class parameterType, TrParameterFormatter formatter); - Set getAvailableLanguages(); } 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..46434d9 --- /dev/null +++ b/api/src/main/java/org/densy/polyglot/api/TranslationsAware.java @@ -0,0 +1,18 @@ +package org.densy.polyglot.api; + +import org.densy.polyglot.api.language.Language; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.Map; + +public interface TranslationsAware { + + @UnmodifiableView + Map> getTranslations(); + + void addTranslation(Language language, String key, String value); + + void addTranslations(Language language, Map translations); + + void removeTranslation(Language language, String key); +} 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..d81bbf2 --- /dev/null +++ b/api/src/main/java/org/densy/polyglot/api/formatter/TranslationFormatter.java @@ -0,0 +1,20 @@ +package org.densy.polyglot.api.formatter; + +import org.densy.polyglot.api.Translation; +import org.densy.polyglot.api.parameter.TranslationParameters; + +/** + * Parameter formatter interface. + */ +public interface TranslationFormatter { + + /** + * Formats text by applying parameters. + * + * @param text the text to format + * @param translation the translation object + * @param parameters the parameters to apply + * @return formatted text + */ + String format(String text, Translation translation, TranslationParameters parameters); +} 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..7d857d1 --- /dev/null +++ b/api/src/main/java/org/densy/polyglot/api/formatter/TranslationFormatterAware.java @@ -0,0 +1,15 @@ +package org.densy.polyglot.api.formatter; + +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.List; + +public interface TranslationFormatterAware { + + @UnmodifiableView + List getFormatters(); + + void addFormatter(TranslationFormatter formatter); + + void removeFormatter(TranslationFormatter formatter); +} 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..2d696cb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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..fe0f1e2 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,19 @@ 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.PatternArrayParameterFormatter; +import org.densy.polyglot.core.formatter.PatternKeyedParameterFormatter; +import org.densy.polyglot.core.parameter.ArrayTrParameters; +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 +22,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 +31,12 @@ 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()); + // Order is important: the array parameter formatter + // must be before the keyed parameter formatter. + this.addFormatter(new PatternArrayParameterFormatter()); + this.addFormatter(new PatternKeyedParameterFormatter(context)); if (provider != null) { translations.putAll(provider.getTranslations()); @@ -48,11 +47,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); @@ -65,7 +64,7 @@ public String translate(Language language, String key, TrParameters parameters) @Override public String translate(Language language, String key, Object... parameters) { - return translate(language, key, new SimpleTrParameters(parameters)); + return translate(language, key, new ArrayTrParameters(parameters)); } private Language resolveLanguage(Language requestedLanguage) { @@ -136,33 +135,12 @@ private String findTranslation(Language language, String key) { return null; } - protected String applyParameters(String text, TrParameters parameters) { + protected String applyParameters(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()); - } - - if (keyedParams != null && !keyedParams.getParameters().isEmpty()) { - TrParameterFormatter keyedFormatter = formatters.get(KeyedTrParameters.class); - if (keyedFormatter != null) { - result = keyedFormatter.format(result, keyedParams); - } + for (TranslationFormatter formatter : formatters) { + result = formatter.format(result, this, parameters); } return result; @@ -188,19 +166,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 setParameterFormatter(Class parameterType, TrParameterFormatter formatter) { - if (parameterType != null && formatter != null) { - formatters.put(parameterType, formatter); + 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 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/formatter/PatternArrayParameterFormatter.java b/core/src/main/java/org/densy/polyglot/core/formatter/PatternArrayParameterFormatter.java new file mode 100644 index 0000000..1b243c9 --- /dev/null +++ b/core/src/main/java/org/densy/polyglot/core/formatter/PatternArrayParameterFormatter.java @@ -0,0 +1,65 @@ +package org.densy.polyglot.core.formatter; + +import org.densy.polyglot.api.Translation; +import org.densy.polyglot.api.parameter.TranslationParameters; +import org.densy.polyglot.api.formatter.TranslationFormatter; +import org.densy.polyglot.core.parameter.ArrayTrParameters; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Formatter for simple parameters in the format {0}, {1}, {2}... + * Supports escaping: \{0} - not replaced, \\{0} - backslash before replacement + */ +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, Translation translation, TranslationParameters parameters) { + if (!(parameters instanceof ArrayTrParameters simpleParams)) { + return text; + } + + Object[] params = simpleParams.getParameters(); + Matcher matcher = pattern.matcher(text); + + StringBuilder result = new StringBuilder(); + while (matcher.find()) { + String fullMatch = matcher.group(0); // all text found + String backslashes = matcher.group(1); + int index = Integer.parseInt(matcher.group(2)); + int backslashCount = backslashes.length(); + + int pairs = backslashCount / 2; + boolean isEscaped = backslashCount % 2 == 1; + + String replacement; + if (isEscaped) { + // escape, remove one slash from the beginning + replacement = "\\".repeat(pairs) + fullMatch.substring(backslashCount); + } else if (index < 0 || index >= params.length) { + // index is out of range, so leave it as it is + replacement = fullMatch; + } else { + // replace the parameter + replacement = "\\".repeat(pairs) + 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..8a1840a --- /dev/null +++ b/core/src/main/java/org/densy/polyglot/core/formatter/PatternKeyedParameterFormatter.java @@ -0,0 +1,74 @@ +package org.densy.polyglot.core.formatter; + +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.parameter.TranslationParameters; +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} + * Supports escaping: \{key} - not replaced, \\{key} - backslash before replacement + */ +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, Translation translation, TranslationParameters 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()); + } + + Map params = keyedParams.getParameters(); + Matcher matcher = pattern.matcher(text); + StringBuilder result = new StringBuilder(); + + while (matcher.find()) { + String fullMatch = matcher.group(0); // all text found + String backslashes = matcher.group(1); + String key = matcher.group(2); + int backslashCount = backslashes.length(); + + int pairs = backslashCount / 2; + boolean isEscaped = backslashCount % 2 == 1; + + String replacement; + if (isEscaped) { + // escape, remove one slash from the beginning + replacement = "\\".repeat(pairs) + fullMatch.substring(backslashCount); + } else if (!params.containsKey(key)) { + // parameter not found, so leave it as it is + replacement = fullMatch; + } else { + // replace the parameter + replacement = "\\".repeat(pairs) + 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/parameter/ArrayTrParameters.java b/core/src/main/java/org/densy/polyglot/core/parameter/ArrayTrParameters.java new file mode 100644 index 0000000..2bc1bea --- /dev/null +++ b/core/src/main/java/org/densy/polyglot/core/parameter/ArrayTrParameters.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 ArrayTrParameters implements TranslationParameters { + + private final Object[] parameters; + + public ArrayTrParameters(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/KeyedTrParameters.java index ce10714..fff1ef8 100644 --- a/core/src/main/java/org/densy/polyglot/core/parameter/KeyedTrParameters.java +++ b/core/src/main/java/org/densy/polyglot/core/parameter/KeyedTrParameters.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,7 +10,7 @@ * Key-value translation parameters. */ @Getter -public class KeyedTrParameters implements TrParameters { +public class KeyedTrParameters implements TranslationParameters { private final Map parameters; @@ -28,11 +27,6 @@ public KeyedTrParameters put(String key, Object value) { return this; } - @Override - public String applyTo(String text, TrParameterFormatter formatter) { - return formatter.format(text, this); - } - /** * Merges the current KeyedTrParameters with another KeyedTrParameters. * 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/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; - } -} - From eab2ffa1d91de4ec9ae76de1aa81c9391c30877c Mon Sep 17 00:00:00 2001 From: MEFRREEX Date: Mon, 26 Jan 2026 01:29:52 +0400 Subject: [PATCH 2/7] chore: update javadoc --- .../densy/polyglot/api/formatter/TranslationFormatter.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index d81bbf2..3395047 100644 --- a/api/src/main/java/org/densy/polyglot/api/formatter/TranslationFormatter.java +++ b/api/src/main/java/org/densy/polyglot/api/formatter/TranslationFormatter.java @@ -4,16 +4,16 @@ import org.densy.polyglot.api.parameter.TranslationParameters; /** - * Parameter formatter interface. + * Translation post formatter interface. */ public interface TranslationFormatter { /** - * Formats text by applying parameters. + * Formats translation text and by applies parameters if it needs. * * @param text the text to format * @param translation the translation object - * @param parameters the parameters to apply + * @param parameters the translation parameters * @return formatted text */ String format(String text, Translation translation, TranslationParameters parameters); From e27658402d08e56d3030d9874ccd786dc0832c11 Mon Sep 17 00:00:00 2001 From: MEFRREEX Date: Mon, 26 Jan 2026 01:53:58 +0400 Subject: [PATCH 3/7] feat: added escape sequence post-formatter + utility class TrParameters --- README.md | 7 +-- .../densy/polyglot/core/BaseTranslation.java | 6 ++- .../core/context/BaseTranslationContext.java | 8 ++-- .../formatter/EscapeSequenceFormatter.java | 47 +++++++++++++++++++ .../PatternArrayParameterFormatter.java | 20 ++++---- .../PatternKeyedParameterFormatter.java | 23 ++++----- ...s.java => ArrayTranslationParameters.java} | 4 +- ...s.java => KeyedTranslationParameters.java} | 19 +++++--- .../polyglot/core/parameter/TrParameters.java | 33 +++++++++++++ 9 files changed, 124 insertions(+), 43 deletions(-) create mode 100644 core/src/main/java/org/densy/polyglot/core/formatter/EscapeSequenceFormatter.java rename core/src/main/java/org/densy/polyglot/core/parameter/{ArrayTrParameters.java => ArrayTranslationParameters.java} (66%) rename core/src/main/java/org/densy/polyglot/core/parameter/{KeyedTrParameters.java => KeyedTranslationParameters.java} (59%) create mode 100644 core/src/main/java/org/densy/polyglot/core/parameter/TrParameters.java 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/core/src/main/java/org/densy/polyglot/core/BaseTranslation.java b/core/src/main/java/org/densy/polyglot/core/BaseTranslation.java index fe0f1e2..fed1cc1 100644 --- a/core/src/main/java/org/densy/polyglot/core/BaseTranslation.java +++ b/core/src/main/java/org/densy/polyglot/core/BaseTranslation.java @@ -8,9 +8,10 @@ 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.formatter.EscapeSequenceFormatter; import org.densy.polyglot.core.formatter.PatternArrayParameterFormatter; import org.densy.polyglot.core.formatter.PatternKeyedParameterFormatter; -import org.densy.polyglot.core.parameter.ArrayTrParameters; +import org.densy.polyglot.core.parameter.ArrayTranslationParameters; import org.jetbrains.annotations.UnmodifiableView; import java.util.*; @@ -37,6 +38,7 @@ public BaseTranslation(TranslationContext context, TranslationProvider provider) // must be before the keyed parameter formatter. this.addFormatter(new PatternArrayParameterFormatter()); this.addFormatter(new PatternKeyedParameterFormatter(context)); + this.addFormatter(new EscapeSequenceFormatter()); if (provider != null) { translations.putAll(provider.getTranslations()); @@ -64,7 +66,7 @@ public String translate(Language language, String key, TranslationParameters par @Override public String translate(Language language, String key, Object... parameters) { - return translate(language, key, new ArrayTrParameters(parameters)); + return translate(language, key, new ArrayTranslationParameters(parameters)); } private Language resolveLanguage(Language requestedLanguage) { 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..ea8e57a --- /dev/null +++ b/core/src/main/java/org/densy/polyglot/core/formatter/EscapeSequenceFormatter.java @@ -0,0 +1,47 @@ +package org.densy.polyglot.core.formatter; + +import org.densy.polyglot.api.Translation; +import org.densy.polyglot.api.formatter.TranslationFormatter; +import org.densy.polyglot.api.parameter.TranslationParameters; + +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, Translation translation, TranslationParameters parameters) { + 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/PatternArrayParameterFormatter.java b/core/src/main/java/org/densy/polyglot/core/formatter/PatternArrayParameterFormatter.java index 1b243c9..3167110 100644 --- a/core/src/main/java/org/densy/polyglot/core/formatter/PatternArrayParameterFormatter.java +++ b/core/src/main/java/org/densy/polyglot/core/formatter/PatternArrayParameterFormatter.java @@ -3,14 +3,13 @@ import org.densy.polyglot.api.Translation; import org.densy.polyglot.api.parameter.TranslationParameters; import org.densy.polyglot.api.formatter.TranslationFormatter; -import org.densy.polyglot.core.parameter.ArrayTrParameters; +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}... - * Supports escaping: \{0} - not replaced, \\{0} - backslash before replacement */ public class PatternArrayParameterFormatter implements TranslationFormatter { public static final Pattern DEFAULT_PATTERN = Pattern.compile("(\\\\*)\\{(\\d+)}"); @@ -27,7 +26,7 @@ public PatternArrayParameterFormatter(Pattern pattern) { @Override public String format(String text, Translation translation, TranslationParameters parameters) { - if (!(parameters instanceof ArrayTrParameters simpleParams)) { + if (!(parameters instanceof ArrayTranslationParameters simpleParams)) { return text; } @@ -36,24 +35,21 @@ public String format(String text, Translation translation, TranslationParameters StringBuilder result = new StringBuilder(); while (matcher.find()) { - String fullMatch = matcher.group(0); // all text found + String fullMatch = matcher.group(0); String backslashes = matcher.group(1); - int index = Integer.parseInt(matcher.group(2)); + String indexStr = matcher.group(2); + int index = Integer.parseInt(indexStr); int backslashCount = backslashes.length(); - int pairs = backslashCount / 2; boolean isEscaped = backslashCount % 2 == 1; String replacement; - if (isEscaped) { - // escape, remove one slash from the beginning - replacement = "\\".repeat(pairs) + fullMatch.substring(backslashCount); - } else if (index < 0 || index >= params.length) { - // index is out of range, so leave it as it is + 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 = "\\".repeat(pairs) + params[index]; + replacement = backslashes + params[index]; } matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); 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 index 8a1840a..966a850 100644 --- a/core/src/main/java/org/densy/polyglot/core/formatter/PatternKeyedParameterFormatter.java +++ b/core/src/main/java/org/densy/polyglot/core/formatter/PatternKeyedParameterFormatter.java @@ -4,7 +4,7 @@ import org.densy.polyglot.api.context.TranslationContext; import org.densy.polyglot.api.formatter.TranslationFormatter; import org.densy.polyglot.api.parameter.TranslationParameters; -import org.densy.polyglot.core.parameter.KeyedTrParameters; +import org.densy.polyglot.core.parameter.KeyedTranslationParameters; import java.util.Map; import java.util.regex.Matcher; @@ -12,7 +12,6 @@ /** * Formatter for named parameters in the format {key} - * Supports escaping: \{key} - not replaced, \\{key} - backslash before replacement */ public class PatternKeyedParameterFormatter implements TranslationFormatter { public static final Pattern DEFAULT_PATTERN = Pattern.compile("(\\\\*)\\{([^}]+)}"); @@ -31,13 +30,13 @@ public PatternKeyedParameterFormatter(TranslationContext context, Pattern patter @Override public String format(String text, Translation translation, TranslationParameters parameters) { - KeyedTrParameters keyedParams; - if (parameters instanceof KeyedTrParameters keyedTrParameters) { - KeyedTrParameters merged = new KeyedTrParameters(context.getGlobalParameters()); + KeyedTranslationParameters keyedParams; + if (parameters instanceof KeyedTranslationParameters keyedTrParameters) { + KeyedTranslationParameters merged = new KeyedTranslationParameters(context.getGlobalParameters()); merged = merged.merge(keyedTrParameters); keyedParams = merged; } else { - keyedParams = new KeyedTrParameters(context.getGlobalParameters()); + keyedParams = new KeyedTranslationParameters(context.getGlobalParameters()); } Map params = keyedParams.getParameters(); @@ -45,24 +44,20 @@ public String format(String text, Translation translation, TranslationParameters StringBuilder result = new StringBuilder(); while (matcher.find()) { - String fullMatch = matcher.group(0); // all text found + String fullMatch = matcher.group(0); String backslashes = matcher.group(1); String key = matcher.group(2); int backslashCount = backslashes.length(); - int pairs = backslashCount / 2; boolean isEscaped = backslashCount % 2 == 1; String replacement; - if (isEscaped) { - // escape, remove one slash from the beginning - replacement = "\\".repeat(pairs) + fullMatch.substring(backslashCount); - } else if (!params.containsKey(key)) { - // parameter not found, so leave it as it is + if (isEscaped || !params.containsKey(key)) { + // if escaped or parameter not found, so leave it as it is replacement = fullMatch; } else { // replace the parameter - replacement = "\\".repeat(pairs) + params.get(key); + replacement = backslashes + params.get(key); } matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); diff --git a/core/src/main/java/org/densy/polyglot/core/parameter/ArrayTrParameters.java b/core/src/main/java/org/densy/polyglot/core/parameter/ArrayTranslationParameters.java similarity index 66% rename from core/src/main/java/org/densy/polyglot/core/parameter/ArrayTrParameters.java rename to core/src/main/java/org/densy/polyglot/core/parameter/ArrayTranslationParameters.java index 2bc1bea..77671db 100644 --- a/core/src/main/java/org/densy/polyglot/core/parameter/ArrayTrParameters.java +++ b/core/src/main/java/org/densy/polyglot/core/parameter/ArrayTranslationParameters.java @@ -7,11 +7,11 @@ * Simple indexed translation parameters. */ @Getter -public class ArrayTrParameters implements TranslationParameters { +public class ArrayTranslationParameters implements TranslationParameters { private final Object[] parameters; - public ArrayTrParameters(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 59% 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 fff1ef8..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 @@ -10,19 +10,26 @@ * Key-value translation parameters. */ @Getter -public class KeyedTrParameters implements TranslationParameters { +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; } @@ -33,11 +40,11 @@ public KeyedTrParameters put(String key, Object value) { * @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/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(); + } +} From 95e942a3705d57ed0b369bdccdcd6f8bc4f925cd Mon Sep 17 00:00:00 2001 From: MEFRREEX Date: Mon, 26 Jan 2026 02:34:14 +0400 Subject: [PATCH 4/7] feat: formatting context + NestedTranslationFormatter --- .../org/densy/polyglot/core/BaseTranslation.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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 fed1cc1..919aae9 100644 --- a/core/src/main/java/org/densy/polyglot/core/BaseTranslation.java +++ b/core/src/main/java/org/densy/polyglot/core/BaseTranslation.java @@ -9,8 +9,10 @@ import org.densy.polyglot.api.util.FallbackStrategy; import org.densy.polyglot.api.util.LanguageStrategy; 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; @@ -34,10 +36,12 @@ public BaseTranslation(TranslationContext context, TranslationProvider provider) this.translations = new HashMap<>(); this.formatters = new ArrayList<>(); + // 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) { @@ -61,7 +65,7 @@ public String translate(Language language, String key, TranslationParameters par translation = fallbackStrategy != null ? fallbackStrategy.get(key) : key; } - return applyParameters(translation, parameters); + return applyParameters(language, key, translation, parameters); } @Override @@ -137,12 +141,14 @@ private String findTranslation(Language language, String key) { return null; } - protected String applyParameters(String text, TranslationParameters parameters) { + protected String applyParameters(Language language, String key, String text, TranslationParameters parameters) { if (text == null) return null; + var context = new TranslationFormatContextImpl(key, language, this, parameters); + String result = text; for (TranslationFormatter formatter : formatters) { - result = formatter.format(result, this, parameters); + result = formatter.format(result, context); } return result; From bbd7ae5bcc14c1814bb6132515f5d450c2fb1ad5 Mon Sep 17 00:00:00 2001 From: MEFRREEX Date: Mon, 26 Jan 2026 02:34:14 +0400 Subject: [PATCH 5/7] feat: formatting context + NestedTranslationFormatter --- .../api/formatter/TranslationFormatter.java | 10 +-- .../context/TranslationFormatContext.java | 39 +++++++++ .../densy/polyglot/core/BaseTranslation.java | 12 ++- .../formatter/EscapeSequenceFormatter.java | 5 +- .../formatter/NestedTranslationFormatter.java | 82 +++++++++++++++++++ .../PatternArrayParameterFormatter.java | 7 +- .../PatternKeyedParameterFormatter.java | 7 +- .../context/TranslationFormatContextImpl.java | 18 ++++ 8 files changed, 160 insertions(+), 20 deletions(-) create mode 100644 api/src/main/java/org/densy/polyglot/api/formatter/context/TranslationFormatContext.java create mode 100644 core/src/main/java/org/densy/polyglot/core/formatter/NestedTranslationFormatter.java create mode 100644 core/src/main/java/org/densy/polyglot/core/formatter/context/TranslationFormatContextImpl.java 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 index 3395047..3133f40 100644 --- a/api/src/main/java/org/densy/polyglot/api/formatter/TranslationFormatter.java +++ b/api/src/main/java/org/densy/polyglot/api/formatter/TranslationFormatter.java @@ -1,7 +1,6 @@ package org.densy.polyglot.api.formatter; -import org.densy.polyglot.api.Translation; -import org.densy.polyglot.api.parameter.TranslationParameters; +import org.densy.polyglot.api.formatter.context.TranslationFormatContext; /** * Translation post formatter interface. @@ -11,10 +10,9 @@ public interface TranslationFormatter { /** * Formats translation text and by applies parameters if it needs. * - * @param text the text to format - * @param translation the translation object - * @param parameters the translation parameters + * @param text the text to format + * @param context the translation format context * @return formatted text */ - String format(String text, Translation translation, TranslationParameters parameters); + String format(String text, TranslationFormatContext context); } 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/core/src/main/java/org/densy/polyglot/core/BaseTranslation.java b/core/src/main/java/org/densy/polyglot/core/BaseTranslation.java index fed1cc1..919aae9 100644 --- a/core/src/main/java/org/densy/polyglot/core/BaseTranslation.java +++ b/core/src/main/java/org/densy/polyglot/core/BaseTranslation.java @@ -9,8 +9,10 @@ import org.densy.polyglot.api.util.FallbackStrategy; import org.densy.polyglot.api.util.LanguageStrategy; 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; @@ -34,10 +36,12 @@ public BaseTranslation(TranslationContext context, TranslationProvider provider) this.translations = new HashMap<>(); this.formatters = new ArrayList<>(); + // 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) { @@ -61,7 +65,7 @@ public String translate(Language language, String key, TranslationParameters par translation = fallbackStrategy != null ? fallbackStrategy.get(key) : key; } - return applyParameters(translation, parameters); + return applyParameters(language, key, translation, parameters); } @Override @@ -137,12 +141,14 @@ private String findTranslation(Language language, String key) { return null; } - protected String applyParameters(String text, TranslationParameters parameters) { + protected String applyParameters(Language language, String key, String text, TranslationParameters parameters) { if (text == null) return null; + var context = new TranslationFormatContextImpl(key, language, this, parameters); + String result = text; for (TranslationFormatter formatter : formatters) { - result = formatter.format(result, this, parameters); + result = formatter.format(result, context); } return result; 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 index ea8e57a..d182c5d 100644 --- a/core/src/main/java/org/densy/polyglot/core/formatter/EscapeSequenceFormatter.java +++ b/core/src/main/java/org/densy/polyglot/core/formatter/EscapeSequenceFormatter.java @@ -1,8 +1,7 @@ package org.densy.polyglot.core.formatter; -import org.densy.polyglot.api.Translation; +import org.densy.polyglot.api.formatter.context.TranslationFormatContext; import org.densy.polyglot.api.formatter.TranslationFormatter; -import org.densy.polyglot.api.parameter.TranslationParameters; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -23,7 +22,7 @@ public class EscapeSequenceFormatter implements TranslationFormatter { private static final Pattern ESCAPE_PATTERN = Pattern.compile("\\\\+"); @Override - public String format(String text, Translation translation, TranslationParameters parameters) { + public String format(String text, TranslationFormatContext context) { Matcher matcher = ESCAPE_PATTERN.matcher(text); StringBuilder result = new StringBuilder(); 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 index 3167110..8b435e0 100644 --- a/core/src/main/java/org/densy/polyglot/core/formatter/PatternArrayParameterFormatter.java +++ b/core/src/main/java/org/densy/polyglot/core/formatter/PatternArrayParameterFormatter.java @@ -1,7 +1,6 @@ package org.densy.polyglot.core.formatter; -import org.densy.polyglot.api.Translation; -import org.densy.polyglot.api.parameter.TranslationParameters; +import org.densy.polyglot.api.formatter.context.TranslationFormatContext; import org.densy.polyglot.api.formatter.TranslationFormatter; import org.densy.polyglot.core.parameter.ArrayTranslationParameters; @@ -25,8 +24,8 @@ public PatternArrayParameterFormatter(Pattern pattern) { } @Override - public String format(String text, Translation translation, TranslationParameters parameters) { - if (!(parameters instanceof ArrayTranslationParameters simpleParams)) { + public String format(String text, TranslationFormatContext context) { + if (!(context.getParameters() instanceof ArrayTranslationParameters simpleParams)) { return text; } 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 index 966a850..077fdc2 100644 --- a/core/src/main/java/org/densy/polyglot/core/formatter/PatternKeyedParameterFormatter.java +++ b/core/src/main/java/org/densy/polyglot/core/formatter/PatternKeyedParameterFormatter.java @@ -1,9 +1,8 @@ package org.densy.polyglot.core.formatter; -import org.densy.polyglot.api.Translation; 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.api.parameter.TranslationParameters; import org.densy.polyglot.core.parameter.KeyedTranslationParameters; import java.util.Map; @@ -29,9 +28,9 @@ public PatternKeyedParameterFormatter(TranslationContext context, Pattern patter } @Override - public String format(String text, Translation translation, TranslationParameters parameters) { + public String format(String text, TranslationFormatContext formatContext) { KeyedTranslationParameters keyedParams; - if (parameters instanceof KeyedTranslationParameters keyedTrParameters) { + if (formatContext.getParameters() instanceof KeyedTranslationParameters keyedTrParameters) { KeyedTranslationParameters merged = new KeyedTranslationParameters(context.getGlobalParameters()); merged = merged.merge(keyedTrParameters); keyedParams = merged; 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; +} From 5b282a7341f2b936a590b9017d7d5cbc9ba572c5 Mon Sep 17 00:00:00 2001 From: MEFRREEX Date: Mon, 26 Jan 2026 02:35:48 +0400 Subject: [PATCH 6/7] chore: bump version --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 2d696cb..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 { From 27251a21ae3e2d6054bfd131ec7a324b6816719b Mon Sep 17 00:00:00 2001 From: MEFRREEX Date: Mon, 26 Jan 2026 02:40:39 +0400 Subject: [PATCH 7/7] docs: javadoc for Translation, TranslationsAware, TranslationFormatterAware --- .../org/densy/polyglot/api/Translation.java | 57 ++++++++++++++++++- .../densy/polyglot/api/TranslationsAware.java | 29 +++++++++- .../api/context/TranslationContext.java | 8 +-- .../formatter/TranslationFormatterAware.java | 21 ++++++- 4 files changed, 108 insertions(+), 7 deletions(-) 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 1901792..033713a 100644 --- a/api/src/main/java/org/densy/polyglot/api/Translation.java +++ b/api/src/main/java/org/densy/polyglot/api/Translation.java @@ -8,21 +8,76 @@ import java.util.Set; +/** + * 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); + /** + * 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); + /** + * 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 index 46434d9..fede89e 100644 --- a/api/src/main/java/org/densy/polyglot/api/TranslationsAware.java +++ b/api/src/main/java/org/densy/polyglot/api/TranslationsAware.java @@ -5,14 +5,41 @@ 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/TranslationFormatterAware.java b/api/src/main/java/org/densy/polyglot/api/formatter/TranslationFormatterAware.java index 7d857d1..b991f0b 100644 --- a/api/src/main/java/org/densy/polyglot/api/formatter/TranslationFormatterAware.java +++ b/api/src/main/java/org/densy/polyglot/api/formatter/TranslationFormatterAware.java @@ -4,12 +4,31 @@ 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