Dead code is a compiler error
Unused page-model properties fail the build. Unused fragments can fail the build. Unused catalog keys fail when you own the bundle. A field that nothing prints is leftover surface area, not a feature.
Java · Kotlin · JDK 26
Thim is an opinionated compile-time HTML renderer. Templates, translations, and page models are compiler inputs. Missing properties, unused copy, dead fragments, CSS, and incomplete locales fail compilation. The request path is generated Java writing UTF-8.
Closed world
JTE lets you write Java in the template. JStachio compiles Mustache against a model. Thim competes with both, then refuses more. The language is small on purpose. If the compiler cannot prove a value, a message, or a URL, the application does not compile.
Unused page-model properties fail the build. Unused fragments can fail the build. Unused catalog keys fail when you own the bundle. A field that nothing prints is leftover surface area, not a feature.
YAML catalogs compile into the renderer. Every locale must carry the
same files, keys, and argument types. CLDR plural rules are embedded.
lang on <html> comes from the request
locale when you omit it.
Static HTML is stored as UTF-8 bytes. Dynamic values are encoded for their output context. There is no OGNL, no reflective dispatch, no runtime template lookup, and no leftover engine in the jar.
Familiar HTML
Name the model after the template. home.html resolves to
HomePage. Return the model from a controller. Thim generates the
renderer.
<!doctype html>
<html>
<body>
<h1 th:text="#{home.title(version=${version})}">Home</h1>
<p th:text="${greeting}">Greeting</p>
<p th:text="#{home.inbox(unreadCount=${unreadCount})}">Unread</p>
<li th:each="feature : ${features}">
<a th:href="@{/feature/{name}(name=${feature.name})}"
th:text="${feature.name}">Name</a>
</li>
</body>
</html>
data class HomePage(
val version: String,
val greeting: String,
val unreadCount: Int,
val features: List<Feature>,
)
@GetMapping("/")
fun home() = HomePage(
version = "0.10.0",
greeting = "Typed models, compiled HTML.",
unreadCount = 3,
features = listOf(
Feature("Safe", "Checked while you compile."),
Feature("Small", "Dependency-free Java runtime."),
),
)
What the compiler refuses
Thim is narrower than a general template engine. That is the product. Prepare computed display values on the page model. Do not ask the template to invent types, look up templates, or evaluate an expression language.
Any/Object, maps, lazy collections?.javascript: URLslabel / ARIA / local anchorsBatteries included
Catalogs are a failsafe YAML 1.2 subset: mappings and string scalars only.
no, true, and dates stay text. SnakeYAML Engine is
a compiler dependency. Request time never parses YAML, looks up a key, or
interprets a pattern. The generated renderer already contains the UTF-8
bytes and the locale branch.
title: Thim {version}
inbox:
_plural: unreadCount
one: One unread message
other: "{unreadCount} unread messages"
salutation:
_select: audience
MEMBER: Welcome back, {name}
other: Welcome, {name}
title: Thim {version}
inbox:
_plural: unreadCount
one: Én ulest melding
other: "{unreadCount} uleste meldinger"
salutation:
_select: audience
MEMBER: Velkommen tilbake, {name}
other: Velkommen, {name}
switch (messageLocale) {
case 1 -> switch (PluralRules
.cardinal("nb", "", unread)) {
case "one" -> output.raw(STATIC, …);
default -> {
output.text(unread);
output.raw(STATIC, …);
}
};
default -> /* en contract */
}
Integer cardinal rules from Unicode CLDR 49 are embedded for the European
languages Thim lists in the README. A _plural catalog in an
unsupported language fails compilation rather than guessing. The same catalog is
automatically available to backend code through generated, typed factories returning
CompiledMessage;
runtime string keys and a second message bundle are not needed. Argument-free
messages also provide constant *Reference values for annotation
APIs, with generated isReference and resolveReference
methods at the framework boundary.
Among compile-time engines
Choose JTE if you want Java or Kotlin in the template and an IntelliJ plugin. Choose JStachio if you want Mustache and logic-less views. Choose Thim if you want Thymeleaf-shaped HTML, compiled locale catalogs, and a compiler that treats leftover model fields and leftover copy as defects.
| Contract | Thim | JTE | JStachio |
|---|---|---|---|
| Template language | Restricted th:* HTML |
Java or Kotlin in the template | Mustache 1.3 |
| Type checking | Page model vs template | The host language | Model bindings |
| Unused model surface | Error in strict mode | Parameters are declared in the template | Unused fields are allowed |
| Compiled locale catalogs | YAML in, branches out | Build it yourself | Build it yourself |
| Locale parity | Same keys and arguments in every locale | Not a compiler concern | Not a compiler concern |
| Plurals and selects | CLDR cardinals, enum selects | Host-language if |
Lambdas or extra models |
| Expression language | None | Java / Kotlin | Mustache sections and lambdas |
| Runtime engine | None on the request path | Optional engine, precompile, hot reload | Optional JMustache fallback |
| Layouts | Fixed fragments, inlined at compile time | Content blocks and template calls | Mustache inheritance |
| Spring | MVC adapter, optional typed routes | Integrations | First-party Spring support |
| Opinion | Closed world | Flexible | Logic-less |
All three compile templates to Java and escape HTML. Thim does not claim to be the fastest of the three. It claims to reject more invalid programs before they start a JVM.
Runtime
The runtime, generated renderers, Spring adapter, and Gradle plugin are
Java. A request creates one buffered HtmlOutput, copies static
byte ranges, and encodes escaped values into that buffer. It creates no
intermediate escaped strings. Composition is erased when fragments are
inlined. Each template jar publishes its registry through
ServiceLoader.
HtmlOutput, RenderContext, TrustedUrl, SafeHtml, and CLDR plural tables.
Records, beans, and data classes keep their source-level types. Templates can only print scalars.
The MVC adapter recognizes page models and ThimResult. Non-Spring apps call TemplateSet directly.
Get started
Templates live in src/main/resources/templates. Catalogs live in
src/main/resources/i18n. The default model package is
<group>.page. Override a directory only for a migration.
Requires JDK 26 or newer.
plugins {
kotlin("jvm")
id("no.beint.thim") version "0.10.0"
}
thim {
generatedPackage.set("your.group.thim.generated")
modelPackages.set(listOf("your.group.page"))
generateRoutes.set(true)
}
plugins {
java
id("no.beint.thim") version "0.10.0"
}
thim {
generatedPackage.set("your.group.thim.generated")
modelPackages.set(listOf("your.group.page"))
}
Artifacts are on Maven Central. Add mavenCentral() to plugin
management and dependency resolution. Use
./gradlew thimCheck --continuous while you edit templates.
Full instructions live in the
README.