| name | fluent-impl-project-setup |
| description | Use when setting up a new Project Fluent project from scratch or adding Fluent to an existing app. Prevents misconfigured dependencies, wrong directory structures, and missing FTL validation tooling. Covers npm installation, FTL+TypeScript+React configuration, message ID conventions, and file naming. Keywords: @fluent/bundle, @fluent/react, project setup, FTL directory structure, localization scaffold, add i18n to project, install Fluent, internationalize my app, translation setup.
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires @fluent/bundle 0.18+, @fluent/react 0.15+. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
fluent-impl-project-setup
Quick Reference
Required Packages
| Package | Purpose | Version |
|---|
@fluent/bundle | Core runtime: parse FTL, format messages | 0.18+ (current: 0.19.1) |
@fluent/react | React bindings: LocalizationProvider, Localized, useLocalization | 0.15+ (current: 0.15.2) |
@fluent/langneg | Locale negotiation: match user preferences to available locales | 0.7.0 |
Optional Packages
| Package | Purpose | When to Install |
|---|
@fluent/syntax | Full AST parser/serializer for FTL validation and tooling | CI linting, editor tooling, programmatic FTL generation |
Peer Dependencies
| Package | Required By | Minimum Version |
|---|
react | @fluent/react | 16.8.0 (hooks support) |
@fluent/bundle | @fluent/react | 0.16.0 |
Critical Warnings
NEVER use @fluent/syntax for runtime message formatting -- it is a tooling library. ALWAYS use @fluent/bundle for runtime formatting.
NEVER install the legacy fluent package -- it was renamed to @fluent/bundle in v0.13.0. The old package is unmaintained.
NEVER skip installing @fluent/langneg -- without locale negotiation, your app cannot match user preferences to available translations.
ALWAYS install all three core packages together -- they form an inseparable stack for any React-based Fluent project.
ALWAYS check addResource() return value for parse errors -- silent failures cause missing translations at runtime.
Setup Workflow
Step 1: Install Dependencies
npm install @fluent/bundle @fluent/react @fluent/langneg
npm install --save-dev @fluent/syntax
Step 2: Create Directory Structure
my-app/
├── public/
│ └── locales/ # FTL files served statically
│ ├── en-US/
│ │ ├── main.ftl # Core UI messages
│ │ ├── errors.ftl # Error messages
│ │ └── settings.ftl # Settings page messages
│ ├── fr/
│ │ ├── main.ftl
│ │ ├── errors.ftl
│ │ └── settings.ftl
│ └── de/
│ ├── main.ftl
│ ├── errors.ftl
│ └── settings.ftl
├── src/
│ ├── l10n/
│ │ ├── index.ts # initLocalization(), locale constants
│ │ └── bundles.ts # Bundle generation logic
│ ├── components/
│ │ └── LocalizedApp.tsx # LocalizationProvider wrapper
│ └── App.tsx
└── package.json
ALWAYS place FTL files under public/locales/{locale}/ for static serving via fetch().
ALWAYS create a dedicated src/l10n/ directory for localization setup code.
ALWAYS organize FTL files by feature, NOT by component -- one FTL file per feature area per locale.
Step 3: Create FTL Files
# public/locales/en-US/main.ftl
app-title = My Application
nav-home = Home
nav-settings = Settings
welcome-user = Welcome, { $userName }!
items-count = { $count ->
[one] You have { $count } item.
*[other] You have { $count } items.
}
Step 4: Create Localization Module
import { FluentBundle, FluentResource } from "@fluent/bundle";
import { negotiateLanguages } from "@fluent/langneg";
import { ReactLocalization } from "@fluent/react";
export const AVAILABLE_LOCALES = ["en-US", "fr", "de"];
export const DEFAULT_LOCALE = "en-US";
async function fetchMessages(locale: string): Promise<string> {
const response = await fetch(`/locales/${locale}/main.ftl`);
return response.text();
}
function createBundle(locale: string, messages: string): FluentBundle {
const bundle = new FluentBundle(locale);
const resource = new FluentResource(messages);
errors = bundle.(resource);
(errors.) {
errors.( .(, e));
}
bundle;
}
(): <> {
negotiated = (
navigator.,
,
{ : }
);
bundles = .(
negotiated.( (locale) => {
messages = (locale);
(locale, messages);
})
);
(bundles);
}
Step 5: Wire Up React
import React, { useState, useEffect } from "react";
import { LocalizationProvider, Localized } from "@fluent/react";
import { ReactLocalization } from "@fluent/react";
import { initLocalization } from "./l10n";
function App() {
const [l10n, setL10n] = useState<ReactLocalization | null>(null);
useEffect(() => {
initLocalization().then(setL10n);
}, []);
if (!l10n) {
return <div>Loading translations...</div>;
}
return (
<LocalizationProvider l10n={l10n}>
<Localized id="app-title">
<h1>My Application</h1>
</Localized>
</LocalizationProvider>
);
}
export default ;
Decision Trees
Which Packages Do I Need?
Is this a React project?
├── YES → Install @fluent/bundle + @fluent/react + @fluent/langneg
│ Do you need FTL linting or validation in CI?
│ ├── YES → Also install @fluent/syntax (devDependency)
│ └── NO → Core three packages are sufficient
└── NO → Install @fluent/bundle + @fluent/langneg only
(React bindings are not needed for vanilla JS/Node.js)
How Should I Organize FTL Files?
How many messages does the app have?
├── < 50 messages → Single file per locale: locales/en-US.ftl
├── 50-200 messages → Feature-based: locales/en-US/{feature}.ftl
└── > 200 messages → Feature-based + code-splitting per route
FTL File Naming Convention
| File Name | Content |
|---|
main.ftl | Core UI: navigation, headers, common buttons |
errors.ftl | Error messages, validation feedback |
settings.ftl | Settings/preferences page |
auth.ftl | Login, registration, password reset |
dashboard.ftl | Dashboard-specific messages |
Semantic Message ID Conventions
ALWAYS use the component-element-action pattern for message IDs:
| Pattern | Example | When to Use |
|---|
{page}-{element} | login-title | Static text elements |
{page}-{element}-{action} | login-button-submit | Interactive elements |
{component}-{descriptor} | nav-home | Reusable components |
{feature}-{state} | upload-progress | State descriptions |
{feature}-error-{type} | auth-error-invalid-email | Error messages |
ALWAYS use kebab-case for message IDs: welcome-user, NOT welcomeUser or welcome_user.
NEVER use generic IDs like button1, text3, or message -- they are impossible to maintain.
ALWAYS change the message ID when the meaning changes -- this signals translators that re-translation is needed.
FTL Validation with @fluent/syntax
Use @fluent/syntax in CI pipelines or development scripts to validate FTL files:
import { FluentParser } from "@fluent/syntax";
import { readFileSync, readdirSync } from "fs";
import { join } from "path";
const parser = new FluentParser({ withSpans: true });
function validateFtlFile(filePath: string): boolean {
const source = readFileSync(filePath, "utf-8");
const resource = parser.parse(source);
let valid = true;
for (const entry of resource.body) {
if (entry.type === "Junk") {
console.error(`Parse error in ${filePath}:`, entry.annotations);
valid = false;
}
}
return valid;
}
NEVER import @fluent/syntax in application runtime code -- it adds unnecessary bundle weight. Use it ONLY in build scripts, CI, and development tooling.
Reference Links
Official Sources