
Localization runtime parsing .ftl files, resolving references and plurals, rendering locale-aware numbers/dates/lists; immutable bundles, pseudolocalization, caching formatters, codegen and Compose helpers.
A Kotlin Multiplatform port of Mozilla's Fluent,
the localization system that powers Firefox, Thunderbird, and Mozilla
VPN. fluent-kt parses .ftl files, resolves message references and
plural/ordinal variants, and renders messages with locale-aware number,
date, and list formatting.
Docs: API reference (Dokka)
| Target | Supported |
|---|---|
| JVM | ✅ |
| Linux x64 (Kotlin/Native) | ✅ |
| macOS, iOS, JS, wasm | ❌ — not yet (see todo/04) |
Generated KDoc is published on GitHub Pages:
→ https://ggallovalle.github.io/fluent-kt/
Build locally with ./gradlew dokkaGenerate (output under build/dokka/html).
import dev.kbroom.fluent.bundle.fluentBundle
import dev.kbroom.fluent.bundle.fluentArgsOf
import dev.kbroom.fluent.intl.LanguageIdentifier
// Construct a bundle from inline FTL via the DSL.
val bundle = fluentBundle(
locales = listOf(LanguageIdentifier.parse("en-US")),
) {
resource("""
greet = Hello, { $name }!
unread-emails = { $count ->
[one] You have one unread email.
*[other] You have { $count } unread emails.
}
""".trimIndent())
builtins() // NUMBER, PLURAL, DATETIME, DATE, TIME, LIST, ...
}
bundle.format("greet", fluentArgsOf("name" to "World"))
// → "Hello, World!"
bundle.format("unread-emails", fluentArgsOf("count" to 5))
// → "You have 5 unread emails."
bundle.format("unread-emails", fluentArgsOf("count" to 1))
// → "You have one unread email."A FluentBundle is built once via the DSL or FluentBundleBuilder and
never mutated afterwards. Concurrent reads from multiple threads are
safe by construction — no locks, no seal(), no per-call setup. See
the FluentBundle
class for details.
| Module | Purpose |
|---|---|
fluent-syntax |
Parser and serializer for FTL → AST. Independent of bundle/runtime. |
intl-memoizer |
Locale-aware formatters (numbers, dates, lists) with thread-safe cache. |
fluent-bundle |
FluentBundle runtime: resolver, message/term lookup, custom functions, builder DSL. The main API surface. |
fluent-pseudo |
Pseudolocalization (Accented, Bidi, Long, Widened, Hidden modes) for catching hard-coded strings. |
fluent-fallback |
Localization facade: locale chain with per-locale fallback (e.g. en-US → en → default). |
fluent-resmgr |
File-system resource loading for bundles, with locale-tag → language → base path resolution. |
fluent-testing |
Test helpers: loader for upstream fluent-rs fixtures. |
fluent |
Umbrella module re-exporting the common entry points. |
fluent-codegen |
AST walk + Kotlin emitter + locale scaffold (used by the Gradle plugin). |
fluent-gradle-plugin |
Gradle plugin io.github.ggallovalle.fluent — validate / generate / scaffold. |
fluent-compose |
Android Jetpack Compose: assets provider, multi-bundle registry, codegen remember*Messages(). |
benchmarks |
JMH microbenchmarks via kotlinx-benchmark (not published). |
Typed accessors from a multi-bundle FTL tree
(resources/i18n/{locale}/{bundle}/**/*.ftl):
plugins {
id("io.github.ggallovalle.fluent") version "0.2.2"
}
fluent {
sourceDirs.from("src/main/resources/i18n")
defaultLocale.set("en-US")
packageName.set("com.example.i18n")
}./gradlew fluentValidate
./gradlew fluentGenerate
./gradlew fluentScaffoldLocale --locale=es-MXGenerated types (from the default locale):
AppMessages(bundle) / AppL10n(localization) — typed methods + KDoc from FTL docsFtlIds.App.GREETING — message id constantsAppResources.Messages — ResourceId constants for fluent-fallback
With Compose:
fluent {
// …
generateComposeAccessors.set(true)
}
dependencies {
implementation("io.github.ggallovalle:fluent-compose:0.2.2")
}emits rememberAppMessages() / rememberErrorsMessages() that read
LocalFluentBundles — not generated *Text composables. Your UI owns
Text / Button / semantics.
setContent {
ProvideFluentFromAssets(
resourceIdsByBundle = mapOf(
"app" to AppResources.All,
"errors" to ErrorsResources.All,
),
fallbackLocale = LanguageIdentifier.parse("en-US"),
basePath = "i18n",
) {
HomeScreen()
}
}
@Composable
fun HomeScreen() {
val app = rememberAppMessages()
Text(app.greeting(name = "Ada"))
}Assets layout: src/main/assets/i18n/{locale}/{bundle}/**/*.ftl.
See examples/android-compose and
examples/README.md. Docs:
https://ggallovalle.github.io/fluent-kt/
See todo/09-ecosystem.md for the full design.
./gradlew jvmTest # JVM unit tests
./gradlew linuxX64Test # Native tests
./gradlew detektAll # Style + lint
./gradlew build # All artifacts
./gradlew :fluent-compose:testDebugUnitTest # Android Compose library
./gradlew :examples:android-compose:assembleDebug # sample app
./gradlew :benchmarks:jvmSmokeBenchmark # quick JMH smoke
./gradlew :benchmarks:jvmBenchmark # full JVM benchmarksRequires JDK 21 (Gradle target version).
Agent / contributor conventions live in AGENTS.md.
Tests use testBalloon — not JUnit
@Test classes. Pattern:
import de.infix.testBalloon.framework.core.testSuite
import kotlin.test.assertEquals
val FluentThingTest by testSuite {
test("does the thing") {
assertEquals(expected, actual)
}
}Apply id("de.infix.testBalloon") and depend on
de.infix.testBalloon:testBalloon-framework-core in the module’s test
source set (including JVM-only modules such as fluent-codegen).
MIT — same as fluent-rs's MIT option.
A Kotlin Multiplatform port of Mozilla's Fluent,
the localization system that powers Firefox, Thunderbird, and Mozilla
VPN. fluent-kt parses .ftl files, resolves message references and
plural/ordinal variants, and renders messages with locale-aware number,
date, and list formatting.
Docs: API reference (Dokka)
| Target | Supported |
|---|---|
| JVM | ✅ |
| Linux x64 (Kotlin/Native) | ✅ |
| macOS, iOS, JS, wasm | ❌ — not yet (see todo/04) |
Generated KDoc is published on GitHub Pages:
→ https://ggallovalle.github.io/fluent-kt/
Build locally with ./gradlew dokkaGenerate (output under build/dokka/html).
import dev.kbroom.fluent.bundle.fluentBundle
import dev.kbroom.fluent.bundle.fluentArgsOf
import dev.kbroom.fluent.intl.LanguageIdentifier
// Construct a bundle from inline FTL via the DSL.
val bundle = fluentBundle(
locales = listOf(LanguageIdentifier.parse("en-US")),
) {
resource("""
greet = Hello, { $name }!
unread-emails = { $count ->
[one] You have one unread email.
*[other] You have { $count } unread emails.
}
""".trimIndent())
builtins() // NUMBER, PLURAL, DATETIME, DATE, TIME, LIST, ...
}
bundle.format("greet", fluentArgsOf("name" to "World"))
// → "Hello, World!"
bundle.format("unread-emails", fluentArgsOf("count" to 5))
// → "You have 5 unread emails."
bundle.format("unread-emails", fluentArgsOf("count" to 1))
// → "You have one unread email."A FluentBundle is built once via the DSL or FluentBundleBuilder and
never mutated afterwards. Concurrent reads from multiple threads are
safe by construction — no locks, no seal(), no per-call setup. See
the FluentBundle
class for details.
| Module | Purpose |
|---|---|
fluent-syntax |
Parser and serializer for FTL → AST. Independent of bundle/runtime. |
intl-memoizer |
Locale-aware formatters (numbers, dates, lists) with thread-safe cache. |
fluent-bundle |
FluentBundle runtime: resolver, message/term lookup, custom functions, builder DSL. The main API surface. |
fluent-pseudo |
Pseudolocalization (Accented, Bidi, Long, Widened, Hidden modes) for catching hard-coded strings. |
fluent-fallback |
Localization facade: locale chain with per-locale fallback (e.g. en-US → en → default). |
fluent-resmgr |
File-system resource loading for bundles, with locale-tag → language → base path resolution. |
fluent-testing |
Test helpers: loader for upstream fluent-rs fixtures. |
fluent |
Umbrella module re-exporting the common entry points. |
fluent-codegen |
AST walk + Kotlin emitter + locale scaffold (used by the Gradle plugin). |
fluent-gradle-plugin |
Gradle plugin io.github.ggallovalle.fluent — validate / generate / scaffold. |
fluent-compose |
Android Jetpack Compose: assets provider, multi-bundle registry, codegen remember*Messages(). |
benchmarks |
JMH microbenchmarks via kotlinx-benchmark (not published). |
Typed accessors from a multi-bundle FTL tree
(resources/i18n/{locale}/{bundle}/**/*.ftl):
plugins {
id("io.github.ggallovalle.fluent") version "0.2.2"
}
fluent {
sourceDirs.from("src/main/resources/i18n")
defaultLocale.set("en-US")
packageName.set("com.example.i18n")
}./gradlew fluentValidate
./gradlew fluentGenerate
./gradlew fluentScaffoldLocale --locale=es-MXGenerated types (from the default locale):
AppMessages(bundle) / AppL10n(localization) — typed methods + KDoc from FTL docsFtlIds.App.GREETING — message id constantsAppResources.Messages — ResourceId constants for fluent-fallback
With Compose:
fluent {
// …
generateComposeAccessors.set(true)
}
dependencies {
implementation("io.github.ggallovalle:fluent-compose:0.2.2")
}emits rememberAppMessages() / rememberErrorsMessages() that read
LocalFluentBundles — not generated *Text composables. Your UI owns
Text / Button / semantics.
setContent {
ProvideFluentFromAssets(
resourceIdsByBundle = mapOf(
"app" to AppResources.All,
"errors" to ErrorsResources.All,
),
fallbackLocale = LanguageIdentifier.parse("en-US"),
basePath = "i18n",
) {
HomeScreen()
}
}
@Composable
fun HomeScreen() {
val app = rememberAppMessages()
Text(app.greeting(name = "Ada"))
}Assets layout: src/main/assets/i18n/{locale}/{bundle}/**/*.ftl.
See examples/android-compose and
examples/README.md. Docs:
https://ggallovalle.github.io/fluent-kt/
See todo/09-ecosystem.md for the full design.
./gradlew jvmTest # JVM unit tests
./gradlew linuxX64Test # Native tests
./gradlew detektAll # Style + lint
./gradlew build # All artifacts
./gradlew :fluent-compose:testDebugUnitTest # Android Compose library
./gradlew :examples:android-compose:assembleDebug # sample app
./gradlew :benchmarks:jvmSmokeBenchmark # quick JMH smoke
./gradlew :benchmarks:jvmBenchmark # full JVM benchmarksRequires JDK 21 (Gradle target version).
Agent / contributor conventions live in AGENTS.md.
Tests use testBalloon — not JUnit
@Test classes. Pattern:
import de.infix.testBalloon.framework.core.testSuite
import kotlin.test.assertEquals
val FluentThingTest by testSuite {
test("does the thing") {
assertEquals(expected, actual)
}
}Apply id("de.infix.testBalloon") and depend on
de.infix.testBalloon:testBalloon-framework-core in the module’s test
source set (including JVM-only modules such as fluent-codegen).
MIT — same as fluent-rs's MIT option.