Skip to content

Commit

Permalink
I18n arguments in template with nested variables.
Browse files Browse the repository at this point in the history
  • Loading branch information
brett-smith committed Sep 17, 2024
1 parent 42793c7 commit 10a4a27
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 10 deletions.
69 changes: 68 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,73 @@ If *parameter* evaluates to false as either a *variable* or *condition*, the exp
<button type="button" ${clipboard-empty:=disabled} id="paste">Paste</button>
```

### Internationalisation

#### In A Template

A special form of Variable Expansion is used for internationalisation in a template. This supports arguments, as well as nested variables as arguments.

You must still set a `ResourceBundle` on the model for I18n keys in a template to work.

```java
var model = TemplateModel.ofContent(
"""
<p>${%someKey}</p>
""").
bundle(MyClass.class);
```

And `MyClass.properties` ..

```
someKey=Some internationalised text
```

##### Basic

The simplest syntax is `${%someKey}`, which will replace `someKey` with whatever the value is in the supplied `RessourceBundle`.

##### With Arguments

To supply arguments, a comma separated list is used. A comma is used by default, but any separator may be configured, and the separator may be escaped using a backslash `\`.

For example `${%someKey arg0,arg 1 with space,arg 2 \, with comma}`

##### With Nested Variables

An argument can also be a nested variable that is available in the same scope as the replacement.

Fixed text arguments and variables can be mixed in an I18N expression. However, you *cannot* currently mix text and variables in the same argument, for example `${%someKey prefix${var}suffix}` will *not* work.

For example `${%someKey arg0,${var1},${var2}`

#### In Code

An alternative way to do parameterised i18n messages is in the model. This is particular useful when the calculation of the message is complex or inconvenient to express as either simple strings in the template, or as argument variables.

In this case, you use the variable pattern `${someName}` instead of the I18n syntext with the `%` prefix.

```java

var model = TemplateModel.ofContent(
"""
<p>${someI18NVariable}</p>
<small>${someOtherI18NVariable}</small>
""").
i18n("someI18NVariable", "keyInBundle").
i18n("someOtherI18NVariable", "keyInBundleWithArgs", "arg0", "arg1").
bundle(MyClass.class);
```

And the bundle ..

```
keyInBundle=Some internationalised text
keyInBundleWithARgs=Some internationalised text {0} {1}
```

As with most `TemplateModel` attributes, you can defer calculation of the template text by supplying the appropriate `Supplier<..>` instead of a direct object reference.

### Tags

TinyTemplates primary use is with HTML and fragments of HTML. Tags by default use an *XML* syntax so as to
Expand Down Expand Up @@ -343,7 +410,7 @@ var model = TemplateModel.ofContent(html).

```

Lists make some default variables to each row.
Lists make some default variables available to each row.

* `_size`, the size of the list.
* `_index`, the zero-based index of the current row.
Expand Down
75 changes: 68 additions & 7 deletions src/main/java/com/sshtools/tinytemplate/Templates.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@

public class Templates {

private final static List<String> EMPTY_ARGS = Collections.emptyList();

@FunctionalInterface
public interface VariableStore extends Function<String, Object> {
default boolean contains(String key) {
Expand All @@ -69,6 +71,12 @@ public final static class Builder {
private Function<String, Boolean> conditionEvaluator;
private Optional<Logger> logger = Optional.of(defaultStdOutLogger());
private Function<String, ?> variableSupplier;
private char argumentSeparator = ',';

public Builder withArgumentSeparator(char argumentSeparator) {
this.argumentSeparator = argumentSeparator;
return this;
}

public Builder withNullsAsNull() {
return withNullsAreEmpty(false);
Expand Down Expand Up @@ -161,12 +169,14 @@ private boolean eval(Map<String, ? extends Object> map, String k) {
private final Function<String, Boolean> conditionEvaluator;
private final Pattern varPattern;
private final Pattern ternPattern;
private final char argumentSeparator;

private VariableExpander(Builder bldr) {
exprPattern = Pattern.compile(REGEXP);
varPattern = Pattern.compile(VAR_REGEXP);
ternPattern = Pattern.compile(TERN_REGEXP);

this.argumentSeparator = bldr.argumentSeparator;
this.bundles = Collections.unmodifiableSet(new LinkedHashSet<>(bldr.bundles));
this.logger = bldr.logger;
this.conditionEvaluator = bldr.conditionEvaluator;
Expand Down Expand Up @@ -200,9 +210,25 @@ public String expand(String input) {
if (intro.equals("%")) {
/* i18n */
var word = mtchr.group(2);
var idx = input.indexOf(' ');
var args = idx == -1 ? new String[0] : splitArguments(input.substring(idx + 1));
if(args.length > 0) {
for(int i = 0 ; i < args.length ; i++) {
try {
args[i] = expand(args[i]);
}
catch(IllegalArgumentException iae) {
args[i] = args[i];
}
}
}
for (var bundle : bundles) {
try {
return bundle.get().getString(word);
if(args.length == 0)
return bundle.get().getString(word);
else {
return MessageFormat.format(bundle.get().getString(word), (Object[])args);
}
} catch (MissingResourceException mre) {
}
}
Expand Down Expand Up @@ -284,6 +310,29 @@ public String expand(String input) {
return input;
}

private String[] splitArguments(String input) {
var esc = false;
var l = new ArrayList<String>();
var b = new StringBuilder();
for(var ch : input.toCharArray()) {
if(ch == '\\' && !esc) {
esc = true;
}
else if(ch == argumentSeparator && !esc) {
l.add(b.toString());
b.setLength(0);
}
else {
b.append(ch);
esc = false;
}
}
if(b.length() > 0) {
l.add(b.toString());
}
return l.toArray(new String[0]);
}

private Object supplyVal(String param) {
var val = variableSupplier.apply(param);
if (missingThrowsException && val == null)
Expand Down Expand Up @@ -779,6 +828,7 @@ private final static class Block {
boolean capture = false;
int nestDepth = 0;
String var;
int braceDepth = 0;

Block(Block parent, TemplateModel model, VariableExpander expander, Reader reader/* , String scope */) {
this(parent, model, expander, reader, null, true);
Expand Down Expand Up @@ -892,19 +942,28 @@ private void read(Block block) {
case VAR_START:
if (process && ch == '{') {
block.state = State.VAR_BRACE;
block.braceDepth++;
buf.append(ch);
} else {
flushBuf(ch, buf, block);
}
break;
case VAR_BRACE:
// TODO nested variables
if (!esc && ch == '}') {
buf.append(ch);
expandToBuffer(block, buf);
buf.setLength(0);
block.state = State.START;
if(block.braceDepth == 1) {
buf.append(ch);
expandToBuffer(block, buf);
buf.setLength(0);
block.state = State.START;
}
else {
buf.append(ch);
}
block.braceDepth--;
} else {
if(!esc && ch == '{') {
block.braceDepth++;
}
buf.append(ch);
}
break;
Expand Down Expand Up @@ -1089,9 +1148,11 @@ else if (process) {
private void expandToBuffer(Block block, StringBuilder buf) {
IllegalArgumentException exception = null;
StringBuilder oblock = block.out;
var varStr = buf.toString();
varStr = varStr.substring(2, varStr.length() - 1);
while(block != null) {
try {
oblock.append(block.expander.process(buf.toString()));
oblock.append(block.expander.expand(varStr));
return;
}
catch(IllegalArgumentException iae) {
Expand Down
68 changes: 67 additions & 1 deletion src/test/java/com/sshtools/tinytemplate/TemplatesTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,13 @@ public void testExpandMissingThrows() {
public void testExpandI18N() {
var exp = new VariableExpander.Builder().
withBundles(ResourceBundle.getBundle(TemplatesTest.class.getName())).
fromSimpleMap(createVars()).
build();

assertEquals("Some localised key", exp.expand("%someKey"));
assertEquals("%someMissingKey", exp.expand("%someMissingKey"));
assertEquals("Some key with args arg0 arg1 arg2", exp.expand("%someKeyWithArgs arg0,arg1,arg2"));
assertEquals("Some key with args 27 arg1 Some name", exp.expand("%someKeyWithArgs ${AGE},arg1,${NAME}"));
}

@Test
Expand Down Expand Up @@ -1353,7 +1356,70 @@ public void testTemplateObjectInCondition() {
}

@Test
public void testTemplateFlaseConditionInObjectInTrueCondition() {
public void testTemplateI18N() {
Assertions.assertEquals("""
<p>Some text</p>
<p>Some localised key</p>
<p>Some other text</p>
""",
createParser().process(TemplateModel.ofContent("""
<p>Some text</p>
<p>${%someKey}</p>
<p>Some other text</p>
""").bundle(TemplatesTest.class)
));

}

@Test
public void testTemplateI18NArgs() {
Assertions.assertEquals("""
<p>Some text</p>
<p>Some key with args arg0 arg1 arg2</p>
<p>Some other text</p>
""",
createParser().process(TemplateModel.ofContent("""
<p>Some text</p>
<p>${%someKeyWithArgs arg0,arg1,arg2}</p>
<p>Some other text</p>
""").bundle(TemplatesTest.class)
));
}

@Test
public void testTemplateI18NArgsAndEscapes() {
Assertions.assertEquals("""
<p>Some text</p>
<p>Some key with args arg0,\\ arg1 arg2</p>
<p>Some other text</p>
""",
createParser().process(TemplateModel.ofContent("""
<p>Some text</p>
<p>${%someKeyWithArgs arg0\\\\,\\\\\\\\,arg1,arg2}</p>
<p>Some other text</p>
""").bundle(TemplatesTest.class)
));
}

@Test
public void testTemplateI18NVars() {
Assertions.assertEquals("""
<p>Some text</p>
<p>Some key with args arg0 VAL1 VAL2</p>
<p>Some other text</p>
""",
createParser().process(TemplateModel.ofContent("""
<p>Some text</p>
<p>${%someKeyWithArgs arg0,${VAR1},${VAR2}}</p>
<p>Some other text</p>
""").bundle(TemplatesTest.class).
variable("VAR1", "VAL1").
variable("VAR2", "VAL2")
));
}

@Test
public void testTemplateFalseConditionInObjectInTrueCondition() {
Assertions.assertEquals("""
<p>Some text</p>
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
someKey=Some localised key
someKey=Some localised key
someKeyWithArgs=Some key with args {0} {1} {2}

0 comments on commit 10a4a27

Please sign in to comment.