Fluent Syntax Guide

Fluent is a localization paradigm designed to unleash the expressive power of the natural language. The format used to describe translation resources used by Fluent is called FTL.

FTL is designed to be simple to read, but at the same time allows to represent complex concepts from natural languages like gender, plurals, conjugations, and others.

The following chapters will demonstrate how to use FTL to solve localization challenges. Each chapter contains a hands-on example of simple FTL concepts.

Hello, world!

In Fluent, the basic unit of translation is called a message. Messages are containers for information. You use messages to identify, store, and recall translation information to be used in the product. The simplest example of a message looks like this:

hello = Hello, world!

Each message has an identifier that allows the developer to bind it to the place in the software where it will be used. The above message is called hello.

In its simplest form, a message has just a single text value. In the example above the value is Hello, world!. The value begins at the first non-blank character after the = sign, but there are rare exceptions related to multiline text values. The next chapter (Writing Text) has all the details.

The majority of messages in Fluent will look similar to the one above. Fluent has been designed to keep these simple translations simple. Sometimes, however, messages need more complexity. Throughout this guide, you'll learn how to adjust messages to the grammar of your language and the requirements of the localized product.

Read on to learn how to read and write Fluent!

Writing Text

Fluent is a file format designed for storing translations. In consequence, text is the most important part of any Fluent file. Messages, terms, variants and attributes all store their values as text. In Fluent, text values are referred to as patterns.

Placeables

In Fluent, text can interpolate values of other messages, as well as external data passed into the translation from the app. Use the curly braces to start and end interpolating another expression inside of a pattern:

# $title (String) - The title of the bookmark to remove.
remove-bookmark = Are you sure you want to remove { $title }?

Refer to the chapters about placeables for more information.

Multiline Text

Text can span multiple lines. In such cases, Fluent will calculate the common indent of all indented lines and remove it from the final text value.

multi = Text can also span multiple lines as long as
    each new line is indented by at least one space.
    Because all lines in this message are indented
    by the same amount, all indentation will be
    removed from the final value.

indents =
    Indentation common to all indented lines is removed
    from the final text value.
      This line has 2 spaces in front of it.

Refer to the chapter about multiline text for more information.

Placeables

Text in Fluent may use special syntax to incorporate small pieces of programmable interface. Those pieces are denoted with curly braces { and } and are called placeables.

It's common to use placeables to interpolate external variables into the translation. Variable values are provided by the developer and they will be set on runtime. They may also dynamically change as the user uses the localized product.

# $title (String) - The title of the bookmark to remove.
remove-bookmark = Really remove { $title }?

It's also possible to interpolate other messages and terms inside of text values.

-brand-name = Firefox
installing = Installing { -brand-name }.

Lastly, placeables can be used to insert special characters into text values. For instance, due to placeables using { and } as delimiters, inserting a literal curly brace into the translation requires special care. Quoted text can be effectively used for the purpose:

opening-brace = This message features an opening curly brace: {"{"}.
closing-brace = This message features a closing curly brace: {"}"}.

Special Characters

In Fluent, text is the most important part of the file. As such, it doesn't have any special syntax: you can just write it without any delimiters (e.g. quotes) and you can use characters from all Unicode planes. Regular text should be enough to store the vast majority of translations. In rare cases when it's not, another type of text can be used: quoted text.

Quoted Text

Quoted text uses double quotes as delimiters and cannot contain line breaks. Like other types of expressions, it can be used inside of placeables (e.g. {"abc"}). It's rarely needed but can be used to insert characters which are otherwise considered special by Fluent in the given context. For instance, due to placeables using { and } as delimiters, inserting a literal curly brace into the translation requires special care. Quoted text can be effectively used for the purpose:

opening-brace = This message features an opening curly brace: {"{"}.
closing-brace = This message features a closing curly brace: {"}"}.

In the example above the {"{"} syntax can be read as a piece of quoted text "{" being interpolated into regular text via the placeable syntax: {…}. As can be seen from this example, curly braces carry no special meaning in quoted text. As a result, quoted text cannot feature any interpolations.

The same strategy as above can be used to ensure blank space is preserved in the translations:

blank-is-removed =     This message starts with no blanks.
blank-is-preserved = {"    "}This message starts with 4 spaces.

In very rare cases, you may need to resort to quoted text to use a literal dot (.), star (*) or bracket ([) when they are used as the first character on a new line. Otherwise, they would start a new attribute or a new variant.

leading-bracket =
    This message has an opening square bracket
    at the beginning of the third line:
    {"["}.
attribute-how-to =
    To add an attribute to this messages, write
    {".attr = Value"} on a new line.
    .attr = An actual attribute (not part of the text value above)

Escape Sequences

Quoted text supports a small number of escape sequences. The backslash character (\) starts an escape sequence in quoted text. In regular text, the backslash is just the literal character with no special meaning.

Escape sequenceMeaning
\"The literal double quote.
\uHHHHA Unicode character in the U+0000 to U+FFFF range.
\UHHHHHHAny Unicode character.
\\The literal backslash.

Escape sequences are rarely needed, but Fluent supports them for the sake of edge cases. In real life application using the actual character in the regular text should be preferred.

# This is OK, but cryptic and hard to read and edit.
literal-quote1 = Text in {"\""}double quotes{"\""}.

# This is preferred. Just use the actual double quote character.
literal-quote2 = Text in "double quotes".

Quoted text should be used sparingly, most often in scenarios which call for a special character, or when enclosing characters in {" and "} makes them easier to spot. For instance, the non-breaking space character looks like a regular space in most text editors and it's easy to miss in a translation. A Unicode escape sequence inside of a quoted text may be used to make it stand out:

privacy-label = Privacy{"\u00A0"}Policy

A similar approach will make it clear which dash character is used in the following example:

# The dash character is an EM DASH but depending on the font face,
# it might look like an EN DASH.
which-dash1 = It's a dash—or is it?

# Using a Unicode escape sequence makes the intent clear.
which-dash2 = It's a dash{"\u2014"}or is it?

Any Unicode character can be used in regular text values and in quoted text. Unless readability calls for using the escape sequence, always prefer the actual Unicode character.

# This will work fine, but the codepoint can be considered
# cryptic by other translators.
tears-of-joy1 = {"\U01F602"}

# This is preferred. You can instantly see what the Unicode
# character used here is.
tears-of-joy2 = 😂

Note for Developers

If you're writing Fluent inside another programming language that uses backslash for escaping, you'll need to use two backslashes to start an escape sequence in Fluent's quoted text. The first backslash is parsed by the host programming language and makes the second backslash a normal character in that language. The second backslash can then be correctly parsed by Fluent.

In JavaScript, for instance, the privacy-label message from one of the previous examples could be added programmatically to a bundle by using two backslashes in the source code:

let bundle = new FluentBundle("en");
bundle.addMessages(`
privacy-label = Privacy{"\\u00A0"}Policy
`);

Multiline Text

Text can span multiple lines as long as it is indented by at least one space. Only the space character (U+0020) can be used for indentation. Fluent treats tab characters as regular text.

single = Text can be written in a single line.

multi = Text can also span multiple lines
    as long as each new line is indented
    by at least one space.

block =
    Sometimes it's more readable to format
    multiline text as a "block", which means
    starting it on a new line. All lines must
    be indented by at least one space.

In almost all cases, patterns start at the first non-blank character and end at the last non-blank character. In other words, the leading and trailing blanks are ignored. There's one exception to this rule, due to another rule which we'll cover below (in the multiline2 example).

leading-spaces =     This message's value starts with the word "This".
leading-lines =


    This message's value starts with the word "This".
    The blank lines under the identifier are ignored.

Line breaks and blank lines are preserved as long as they are positioned inside of multiline text, i.e. there's text before and after them.

blank-lines =

    The blank line above this line is ignored.
    This is a second line of the value.

    The blank line above this line is preserved.

In multiline patterns, all common indent is removed when the text value is spread across multiple indented lines.

multiline1 =
    This message has 4 spaces of indent
        on the second line of its value.

We can visualize this behavior in the following manner and we'll use this convention in the rest of this chapter:

# █ denotes the indent common to all lines (removed from the value).
# · denotes the indent preserved in the final value.
multiline1 =
████This message has 4 spaces of indent
████····on the second line of its value.

This behavior also applies when the first line of a text block is indented relative to the following lines. This is the only case where a sequence of leading blank characters might be preserved as part of the text value, even if they're technically in the leading position.

multiline2 =
████··This message starts with 2 spaces on the first
████first line of its value. The first 4 spaces of indent
████are removed from all lines.

Only the indented lines comprising the multiline pattern participate in this behavior. Specifically, if the text starts on the same line as the message identifier, then this first line is not considered as indented, and is excluded from the dedentation behavior. In such cases, the first line (the unindented one) still has its leading blanks ignored—because patterns start on the first non-blank character.

multiline3 = This message has 4 spaces of indent
████····on the second line of its value. The first
████line is not considered indented at all.

# Same value as multiline3 above.
multiline4 =     This message has 4 spaces of indent
████····on the second line of its value. The first
████line is not considered indented at all.

Note that if a multiline pattern starts on the same line as the identifier and it only consists of one more line of text below it, then the indent common to all indented lines is equal to the indent of the second line, i.e. the only indented line. All indent will be removed in this case.

multiline5 = This message ends up having no indent
████████on the second line of its value.

Variables

Variables are pieces of data received from the app. They are provided by the developer of the app and may be interpolated into the translation with placeables. Variables can dynamically change as the user is using the localized product.

Variables are referred to via the $variable-name syntax:

welcome = Welcome, { $user }!
unread-emails = { $user } has { $email-count } unread emails.

For instance, if the current user's name is Jane and she has 5 unread emails, the above translations will be displayed as:

Welcome, Jane!
Jane has 5 unread emails.

There are all kinds of external data that might be useful in providing a good localization: user names, number of unread messages, battery level, current time, time left before an alarm goes off, etc. Fluent offers a number of features designed to make working with variables convenient.

Variants and Selectors

In some languages, using a number in the middle of a translated sentence will require proper plural forms of words associated with the number. In Fluent, you can define multiple variants of the translation, each for a different plural category.

Implicit Formatting

Numbers and dates are automatically formatted according to your language's formatting rules. Consider the following translation:

# $duration (Number) - The duration in seconds.
time-elapsed = Time elapsed: { $duration }s.

For the $duration variable value of 12345.678, the following message will be displayed in the product, assuming the language is set to British or American English. Note the use of comma as the thousands separator.

Time elapsed: 12,345.678s.

In South African English, however, the result would be:

Time elapsed: 12 345,678s.

Explicit Formatting

In some cases the localizer might want to have a greater control over how a number or a date is formatted in text. Fluent provides built-in functions for this purpose.

# $duration (Number) - The duration in seconds.
time-elapsed = Time elapsed: { NUMBER($duration, maximumFractionDigits: 0) }s.

For the same value of $duration as above, the result will look like the following for American and British English:

Time elapsed: 12,345s.

Message References

Another use-case for placeables is referencing one message in another one.

menu-save = Save
help-menu-save = Click { menu-save } to save the file.

Referencing other messages generally helps to keep certain translations consistent across the interface and makes maintenance easier.

It is also particularly handy for keeping branding separated from the rest of the translations, so that it can be changed easily when needed, e.g. during the build process of the application. This use-case is best served by defining a term with a leading dash -, like -brand-name in the example below.

-brand-name = Firefox
installing = Installing { -brand-name }.

Using a term here indicates to tools and to the localization runtime that -brand-name is not supposed to be used directly in the product but rather should be referenced in other messages.

Selectors

emails =
    { $unreadEmails ->
        [one] You have one unread email.
       *[other] You have { $unreadEmails } unread emails.
    }

One of the most common cases when a localizer needs to use a placeable is when there are multiple variants of the string that depend on some external variable. In the example above, the emails message depends on the value of the $unreadEmails variable.

FTL has the select expression syntax which allows to define multiple variants of the translation and choose between them based on the value of the selector. The * indicator identifies the default variant. A default variant is required.

The selector may be a string, in which case it will be compared directly to the keys of variants defined in the select expression. For selectors which are numbers, the variant keys either match the number exactly or they match the CLDR plural category for the number. The possible categories are: zero, one, two, few, many, and other. For instance, English has two plural categories: one and other.

If the translation requires a number to be formatted in a particular non-default manner, the selector should use the same formatting options. The formatted number will then be used to choose the correct CLDR plural category which, for some languages, might be different than the category of the unformatted number:

your-score =
    { NUMBER($score, minimumFractionDigits: 1) ->
        [0.0]   You scored zero points. What happened?
       *[other] You scored { NUMBER($score, minimumFractionDigits: 1) } points.
    }

Using formatting options also allows for selectors using ordinal rather than cardinal plurals:

your-rank = { NUMBER($pos, type: "ordinal") ->
   [1] You finished first!
   [one] You finished {$pos}st
   [two] You finished {$pos}nd
   [few] You finished {$pos}rd
  *[other] You finished {$pos}th
}

Attributes

login-input = Predefined value
    .placeholder = email@example.com
    .aria-label = Login input value
    .title = Type your login email

UI elements often contain multiple translatable messages per one widget. For example, an HTML form input may have a value, but also a placeholder attribute, aria-label attribute, and maybe a title attribute.

Another example would be a Web Component confirm window with an OK button, Cancel button, and a message.

In order to prevent having to define multiple separate messages for representing different strings within a single element, FTL allows you to add attributes to messages.

This feature is particularly useful in translating more complex widgets since, thanks to all attributes being stored on a single unit, it's easier for editors, comments, and tools to identify and work with the given message.

Attributes may also be used to define grammatical properties of terms. Attributes of terms are private and cannot be retrieved by the localization runtime. They can only be used as selectors.

Terms

Terms are similar to regular messages but they can only be used as references in other messages. Their identifiers start with a single dash - like in the example above: -brand-name. The runtime cannot retrieve terms directly. They are best used to define vocabulary and glossary items which can be used consistently across the localization of the entire product.

-brand-name = Firefox

about = About { -brand-name }.
update-successful = { -brand-name } has been updated.

Parameterized Terms

Term values follow the same rules as message values. They can be simple text, or they can interpolate other expressions, including variables. However, while messages receive data for variables directly from the app, terms receive such data from messages in which they are used. Such references take the form of -term(…) where the variables available inside of the term are defined between the parentheses, e.g. -term(param: "value").

# A contrived example to demonstrate how variables
# can be passed to terms.
-https = https://{ $host }
visit = Visit { -https(host: "example.com") } for more information.

The above example isn't very useful and a much better approach in this particular case would be to use the full address directly in the visit message. There is, however, another use-case which takes full advantage of this feature. By passing variables into the term, you can define select expressions with multiple variants of the same term value.

-brand-name =
    { $case ->
       *[nominative] Firefox
        [locative] Firefoksie
    }

# "About Firefox."
about = Informacje o { -brand-name(case: "locative") }.

This pattern can be very useful for defining multiple facets of the term, which can correspond to grammatical cases or other grammatical or stylistic properties of the language. In many inflected languages (e.g. German, Finnish, Hungarian, all Slavic languages), the about preposition governs the grammatical case of the complement. It might be accusative (German), ablative (Latin), or locative (Slavic languages). The grammatical cases can be defined as variants of the same term and referred to via parameterization from other messages. This is what happens in the about message above.

If no parameters are passed into the term, or if the term is referenced without any parentheses, the default variant will be used.

-brand-name =
    { $case ->
       *[nominative] Firefox
        [locative] Firefoksie
    }

# "Firefox has been successfully updated."
update-successful = { -brand-name } został pomyślnie zaktualizowany.

Terms and Attributes

Sometimes translations might vary depending on some grammatical trait of a term references in them. Terms can store this grammatical information about themselves in attributes. In the example below the form of the past tense of has been updated depends on the grammatical gender of -brand-name.

-brand-name = Aurora
    .gender = feminine

update-successful =
    { -brand-name.gender ->
        [masculine] { -brand-name } został zaktualizowany.
        [feminine] { -brand-name } została zaktualizowana.
       *[other] Program { -brand-name } został zaktualizowany.
    }

Use attributes to describe grammatical traits and properties. Genders, animacy, whether the term message starts with a vowel or not etc. Attributes of terms are private and cannot be retrieved by the localization runtime. They can only be used as selectors. If needed, they can also be parameterized using the -term.attr(param: "value") syntax.

Comments

Comments in Fluent start with #, ##, or ###, and can be used to document messages and to define the outline of the file.

Single-hash comments (#) can be standalone or can be bound to messages. If a comment is located right above a message it is considered part of the message and localization tools will present the message and the comment together. Otherwise the comment is standalone (which is useful for commenting parts of the file out).

Double-hash comments (##) are always standalone. They can be used to divide files into smaller groups of messages related to each other; they are group-level comments. Think of them as of headers with a description. Group-level comments are intended as a hint for localizers and tools about the layout of the localization resource. The grouping ends with the next group comment or at the end of the file.

Triple-hash comments (###) are also always standalone and apply to the entire file; they are file-level comments. They can be used to provide information about the purpose or the context of the entire file.

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

### Localization for Server-side strings of Firefox Screenshots

## Global phrases shared across pages

my-shots = My Shots
home-link = Home
screenshots-description =
    Screenshots made simple. Take, save, and
    share screenshots without leaving Firefox.

## Creating page

# Note: { $title } is a placeholder for the title of the web page
# captured in the screenshot. The default, for pages without titles, is
# creating-page-title-default.
creating-page-title = Creating { $title }
creating-page-title-default = page
creating-page-wait-message = Saving your shot…

Builtins

emails = You have { $unreadEmails } unread emails.
emails2 = You have { NUMBER($unreadEmails) } unread emails.

last-notice =
    Last checked: { DATETIME($lastChecked, day: "numeric", month: "long") }.

In most cases, Fluent will automatically select the right formatter for the argument and format it into a given locale.

In other cases, the developer will have to annotate the argument with additional information on how to format it (see Partial Arguments)

But in rare cases there may be a value for a localizer to select some formatting options that are specific to the given locale.

Examples include: defining month as short or long in the DATE formatter (using arguments defined in Intl.DateTimeFormat) or whether to use grouping separator when displaying a large number.

Functions in FTL

Functions provide additional functionality available to the localizers. They can be either used to format data according to the current language's rules or can provide additional data that the localizer may use (like, the platform, or time of the day) to fine tune the translation.

FTL implementations should ship with a number of built-in functions that can be used from within localization messages.

The list of available functions is extensible and environments may want to introduce additional functions, designed to aid localizers writing translations targeted for such environments.

Using Functions

FTL Functions can only be called inside of placeables. Use them to return a value to be interpolated in the message or as selectors in select expressions.

Example:

today-is = Today is { DATETIME($date) }

Function parameters

Functions may accept positional and named arguments. Some named arguments are only available to developers when they pre-format variables passed as arguments to translations (see Partially-formatted variables below).

Built-in Functions

Built-in functions are very generic and should be applicable to any translation environment.

NUMBER

Formats a number to a string in a given locale.

Example:

dpi-ratio = Your DPI ratio is { NUMBER($ratio, minimumFractionDigits: 2) }

Parameters:

currencyDisplay
useGrouping
minimumIntegerDigits
minimumFractionDigits
maximumFractionDigits
minimumSignificantDigits
maximumSignificantDigits

Developer parameters:

style
currency

See the Intl.NumberFormat for the description of the parameters.

DATETIME

Formats a date to a string in a given locale.

Example:

today-is = Today is { DATETIME($date, month: "long", year: "numeric", day: "numeric") }

Parameters:

hour12
weekday
era
year
month
day
hour
minute
second
timeZoneName

Developer parameters:

timeZone

See the Intl.DateTimeFormat for the description of the parameters.

Implicit use

In order to simplify most common scenarios, FTL will run some default functions while resolving placeables.

NUMBER

If the variable passed from the developer is a number and is used in a placeable, FTL will implicitly call a NUMBER function on it.

Example:

emails = Number of unread emails { $unreadEmails }

emails2 = Number of unread emails { NUMBER($unreadEmails) }

Numbers used as selectors in select expressions will match the number exactly or they will match the current language's CLDR plural category for the number.

The following examples are equivalent and will both work. The second example may be used to pass additional formatting options to the NUMBER formatter for the purpose of choosing the correct plural category:

liked-count = { $num ->
        [0]     No likes yet.
        [one]   One person liked your message
       *[other] { $num } people liked your message
    }

liked-count2 = { NUMBER($num) ->
        [0]     No likes yet.
        [one]   One person liked your message
       *[other] { $num } people liked your message
    }

DATETIME

If the variable passed from the developer is a date and is used in a placeable, FTL will implicitly call a DATETIME function on it.

Example:

log-time = Entry time: { $date }

log-time2 = Entry time: { DATETIME($date) }

Partially-formatted variables

In most cases localizers don't need to call Functions explicitly, thanks to the implicit formatting. If the implicit formatting is not sufficient, the Function can be called explicitly with additional parameters. To ease the burden this might have on localizers, Fluent implementations may allow developers to set the default formatting parameters for the variables they pass.

In other words, developers can provide variables which are already wrapped in a partial Function call.

today = Today is { $day }
ctx.format('today', {
  day: new FluentDateTime(new Date(), {
    weekday: 'long'
  })
})

If the localizer wishes to modify the parameters, for example because the string doesn't fit in the UI, they can pass the variable to the same Function and overload the parameters set by the developer.

today = Today is { DATETIME($day, weekday: "short") }

Dive deeper

You can experiment with the syntax using Fluent Playground, an interactive editor available in the web browser.

If you are a tool author, you may be interested in the formal description of the syntax available in the projectfluent/fluent repository.