Simple blazing-fast placeholders library with yet unlimited capabilities. Part of the okaeri-platform.
Add repository to the repositories section:
<repository>
<id>okaeri-releases</id>
<url>https://repo.okaeri.cloud/releases</url>
</repository>Add dependency to the dependencies section:
<dependency>
<groupId>eu.okaeri</groupId>
<artifactId>okaeri-placeholders-[platform]</artifactId>
<version>6.0.0-beta.2</version>
</dependency>Add repository to the repositories section:
maven { url "https://repo.okaeri.cloud/releases" }Add dependency to the maven section:
implementation 'eu.okaeri:okaeri-placeholders-[platform]:6.0.0-beta.2'# basic message
Hello World!
# with simple placeholder
Hello {who}!
# with simple placeholder and fallback value (shorthand for .or("unknown"))
Hello {who|unknown}!
# with subfields
Hello {who.name}!
# with subfields and fallback value
Hello {who.name|unknown}!
# pluralization for 143 locales (okaeri-pluralize)
I would like {amount} {apple,apples#amount}.
I would like {amount} {amount.plural("apple","apples")}.
# boolean translation
Active: {yes,no#status}
Active: {status.bool("yes","no")}
# number formatting
Value: {%.2f#value}
Value: {value.format("%.2f")}
# duration (e.g. "1d12h", supports precision, see: (h))
Remaining time: {duration(h)}
# instant/datetime formatting (localized, optional timezone)
Time: {ldt,medium,Europe/Paris#time}
Time: {time.datetime("medium","Europe/Paris")}
Date: {time.date("medium","UTC")}
Time only: {time.time("short","UTC")}
Custom pattern: {time.format("yyyy-MM-dd HH:mm","UTC")}
Relative (Duration): {expiry.relative(now()).format("[d]d [h]h")}
# default values with .or() - supports field references and chaining
Hello {name.or("Anonymous")}!
Display: {nickname.or(name)}
Fallback chain: {a.or(b).or(c).or("default")}
# global functions
Environment: {env(HOME)}
Current time: {now()}
Formatted: {now().format("yyyy-MM-dd","UTC")}
First non-null: {or(nickname,name,"Anonymous")}
Conditional: {if(active,"online","offline")}
Random number: {random(1,100)}
Concatenation: {concat("Hello ",name,"!")}
Join non-empty: {join(", ",first,middle,last)}
Numeric min/max: {min(a,b,c)}, {max(a,b,c)}
Sum/avg: {sum(a,b,c)}, {avg(a,b,c)}
Median/percentile: {median(a,b,c,d,e)}, {percentile(95,a,b,c,d,e)}
Clamp to range: {clamp(health,0,100)}
Length/size: {len(items)}, {len(name)}
Default value: {default(name,"Anonymous")}
Chained conditions: {cond(isAdmin,"Admin",isMod,"Mod","User")}
Switch/case: {switch(status,"online","Online","away","Away","Offline")}
# number methods
Arithmetic: {n.plus(5)}, {n.minus(3)}, {n.multiply(2)}, {n.divide(4)}
Math: {n.abs()}, {n.round()}, {n.floor()}, {n.ceil()}
Clamp/mod: {health.clamp(0,100)}, {index.mod(2)}
Comparisons: {n.gt(5)}, {n.gte(5)}, {n.lt(10)}, {n.lte(10)}, {n.between(1,10)}
Signed: {delta.signed}
# string methods
Case: {name.toLowerCase()}, {name.toUpperCase()}, {name.capitalize()}
Utility: {name.trim()}, {name.length()}, {name.isEmpty()}, {name.isBlank()}
Modify: {name.replace("a","b")}, {name.prepend("Hi ")}, {name.append("!")}
Extract: {name.substring(0,5)}, {name.repeat(3)}
Pad: {score.padStart(6,"0")}, {label.padEnd(20)}
Truncate: {description.truncate(40)}, {title.truncate(16,"…")}
Reverse: {name.reverse}
Strip: {name.removePrefix("Mr. ")}, {file.removeSuffix(".txt")}
Count: {text.count(",")}
Position: {path.indexOf("/")}, {path.lastIndexOf(".")}
Check: {name.contains("test")}, {name.startsWith("Mr")}, {name.endsWith("Jr")}
# collection methods
Basics: {items.size}, {items.isEmpty}, {items.first}, {items.last}, {items.get(0)}, {items.contains("foo")}
Ordering: {items.reverse}, {items.sort}, {items.sortDesc}, {items.distinct}
Slicing: {items.take(5)}, {items.drop(2)}
Joining: {items.join(", ")}
Aggregations: {scores.sum}, {scores.avg}, {scores.min}, {scores.max}
# map methods
Basics: {m.size}, {m.isEmpty}, {m.keys}, {m.values}, {m.get("key")}
Localized lookup (Map<Locale, ?>): {kit.display.localized}
# practical example: 3-level health bar with colors
# health=75 → green (>66), health=50 → yellow (>33), health=20 → red
Health: {cond(health.gt(66),"&a",health.gt(33),"&e","&c")}{health}% &7HP
# → "Health: &a75% &7HP" (green for high health)
# note: explicit $.func() and .func() also work: {$.now()}, {.now()}Basic example representing standard usage for simple placeholders. For more examples and advanced usage cases see tests.
// this is intended to be loaded from the configuration on the startup/cached and stored compiled
CompiledMessage message = CompiledMessage.of("Hola {who}! ¿Cómo estás {when}? Estoy {how}.");
// context can be reused (use #create() and #apply(message))
// or created on demand (use #of(message) and #apply())
// second version can't be reused but is faster in on-demand scenario
// especially when resulting message is using only part of the placeholders
PlaceholderContext context = PlaceholderContext.of(this.message)
.with("who", "Mundo") // in real life scenario these would be your variables
.with("when", "hoy")
.with("how", "bien");
// process message and get output: Hola Mundo! ¿Cómo estás hoy? Estoy bien.
String test = this.context.apply();For automatic subfield resolution on your classes, use the @Placeholder annotation. This enables {player.name} syntax
without manual resolver registration.
import eu.okaeri.placeholders.resolver.annotation.Placeholder;
@Placeholder
public class Player {
private String name;
private int health;
public String getName() { return this.name; }
public int getHealth() { return this.health; }
public boolean isAlive() { return this.health > 0; }
}
// usage - getters are auto-discovered
CompiledMessage message = CompiledMessage.of("{player.name} has {player.health} HP (alive: {player.alive})");
String result = PlaceholderContext.of(message)
.with("player", new Player("John", 100))
.apply();
// → "John has 100 HP (alive: true)"The annotation supports:
- Getter auto-discovery:
getName()→{obj.name},isActive()→{obj.active} - Custom names:
@Placeholder(name = "hp") int getHealth()→{obj.hp} - Selective scanning:
@Placeholder(scan = false)on class to only expose explicitly annotated methods - Method chaining:
{player.name.upper()}combines annotation discovery with registered resolvers
Note: Since JDK8 times, the built-in String#replace(CharSequence, CharSequence) has become faster or comparable to the okaeri-placeholders in some benchmarks,
but even ignoring lacking features, it is and will be always unsafe to use for placeholders due to the sole nature of chaining.
Charts are representing operations per second for each of the implementations. For the current source code of test see benchmark directory.
- Okaeri Placeholders benchmark is based on cached CompiledMessage as this is intended use of the library. PlaceholderContext is created every iteration.
- JDK8 String#replace benchmarks are chaining replace calls together, as one may do while implementing this type of system with no additional code.
- CommonsLang3 StringUtils#replaceEach benchmarks are using standard single call with no additional code.
- Spec: (AdoptOpenJDK)(build 1.8.0_282-b08), OS: Ubuntu 20.04.2 LTS x86_64, CPU: AMD Ryzen 5 3600 (12) @ 3.600GHz, okaeri-placeholders: 1.0.0
This group represents average messages with fields.
This group uses multiline string built by repeating string from the simple group to check for repeated fields/stress performance.
This group represents the replacement of a few evenly distributed fields inside a long string, e.g. html e-mail template. The long tricky represents long string with one field in the last 20% of the string.
This group shows differences in the performance for static strings that do not require any processing, but nor String#replace chain neither StringUtils#replaceEach can know that, causing
performance loss when the character count is going up.



