Skip to content

ggallovalle/fluent-kt

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

113 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fluent-kt

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)

Status

Target Supported
JVM
Linux x64 (Kotlin/Native)
macOS, iOS, JS, wasm ❌ — not yet (see todo/04)

API reference

Generated KDoc is published on GitHub Pages:

https://ggallovalle.github.io/fluent-kt/

Build locally with ./gradlew dokkaGenerate (output under build/dokka/html).

Quick start

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."

Bundles are immutable

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 overview

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-USen → 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).

Gradle plugin (experimental)

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-MX

Generated types (from the default locale):

  • AppMessages(bundle) / AppL10n(localization) — typed methods + KDoc from FTL docs
  • FtlIds.App.GREETING — message id constants
  • AppResources.MessagesResourceId 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 LocalFluentBundlesnot generated *Text composables. Your UI owns Text / Button / semantics.

Android Compose (experimental)

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.

Build

./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 benchmarks

Requires JDK 21 (Gradle target version).

Developing

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).

License

MIT — same as fluent-rs's MIT option.

Acknowledgements

  • Mozilla Fluent — the original Rust implementation this project is modeled after.
  • fluent-rs — reference for test fixtures and parser semantics.

About

Project fluent kotlin implementation

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages