Fluent for Anvil - Perfect Translations for your Anvil App

Hello everyone!

I created a library called fluent_anvil that makes it easy to serve high-quality translated and localized versions of your Anvil app. You can make your App appeal to and accessible for everyone. All you need to do is add it as a third party dependency with the token UHLC7WE6TELL25TO . It is published under MIT License.

The library serves as a Python interface to Fluent. It is a localization system developed by Mozilla for natural-sounding translations. In contrast to gettext you can create more nuanced and natural sounding translations. For example, Polish has more plural forms than English or German. Therefore, selecting the right translation requires knowing how many there are of something. With localization systems like gettext, this requires adding additional logic inside your application. With fluent, this language-specific logic is encapsulated in the translation file and does not impact other translations.

Personally, I think the greatest thing about fluent apart from the translation quality is that is easier to learn and use than gettext: It uses only one simple text file format (.ftl) and does not require specialized extraction tools. Often, translations are as simple as this:

close-button = Close

close-button = Schließen

For simple translations, the syntax stays simple. If a translation happens to be more complicated for a language, you only need to add the logic in the translation file for that particular language. You can find out more at Project Fluent.

The translation happens entirely on the client side. Therefore, it works on the free plan as well since there is no need to install a special package.

Quick Guide
In Anvil’s assets section, add a directory to place your translations in, ideally you have one subfolder for each locale, e.g.

  • localization
    • es_MX
      • main.ftl
    • en_US
      • main.ftl
    • de_DE
      • main.ftl

With Fluent, you can use variables for placeholders or selecting the appropriate translation. In the following example we are going to greet the user. Therefore, we use a variable as a placeholder for the user’s name. Assume that the content of es_MX/main.ftl is:
hello = Hola { $name }.

Then, import two classes in your form (Message is optional but required for the examples):

from fluent_anvil.lib import Fluent, Message

If you want to know which locale the user prefers, just call


This will return a list of locales such as ['de-DE'] that the user prefers (this method does not use Fluent but the get-user-locale package).

Now, you can initialize Fluent using the following (we ignore the preferred locale for now):

fl = Fluent("localization/{locale}/main.ftl", "es-MX", ["en-US", "es-MX"])

This will initialize fluent with the Mexican Spanish locale. The first parameter is a template string indicating where the translation files are stored. The placeholder {locale} is replaced with the desired locale (hyphens converted to underscore, because Anvil does not allow hyphens in directory names). The second parameter is the desired locale. The last parameter is a list of fallback locales that will be iterated through if translation fails. Generally, all methods of the Python object accept locales regardless of whether you use hyphens or underscores. Note that you do not have to provide the full URL starting with ./_/theme/. It will be prepended automatically. If your translation files are stored somewhere else entirely you can explicitly set the prefix by adding it to the end of the parameter list.

Now, you can greet the user:

print(fluent.format("hello", name="John"))

Every variable you want to have access to in your .ftl files can be added as a keyword variable. Apart from facts like the user’s name this can be used to determine a natural sounding translation. These variables may include the count of something or the time of day. Depending on the type of variable, Fluent will automatically format the value according to the selected locale. For example, these messages:
time-elapsed = Time elapsed: { $duration }s.
time-elapsed = Vergangene Zeit: { $duration }s.
After calling a command like

print(fluent.format("time-elapsed", duration=12342423.234 ))

the message will show up with locale en-US as:
Time elapsed: <U+2068>12,342,423.234<U+2069>s.
While with locale “de_DE” it will show up as:
Vergangene Zeit: <U+2068>12.342.423,234<U+2069>s.
Pay attention to the use of dot and comma which is specific to the respective countries.

You can translate multiple strings at once (that’s more efficient than one by one) by wrapping them in Message objects:

    Message("hello", name="World"), 
    Message("welcome-back", name="John"),

This returns a list of all translations in the same order as the corresponding Message instances. That’s nice already. However, my favorite feature is the possibility to write directly to GUI component attributes:

    Message("hello", name="world"), 
    Message(self.label, "text", "hello", name="John"),

You just provide the component and the name of the attribute you want to write to (similar to Python’s setattr() function).

You can switch to a different locale on the fly using set_locale(). Again, the first parameter is the desired locale and the second is a list of fallback locales.

fluent.set_locale("en-US", ["en-GB", "en-AU"])

Bonus Round: Translate your HTML Templates
You can translate your static content as well. Just add the tags data-l10n-id for the message id and data-l10n-args for context variables (if needed) like this:

<h1 id='welcome' data-l10n-id='hello' data-l10n-args='{"name": "world"}'>Localize me!</h1>

If you do not initialize a Fluent instance, you will see “Localize me!”. As soon as the Fluent instance is initialized (e.g. with locale es-MX), the text changes to “Hola ⁨world⁩”. If Fluent would fail for some reason, the default text (in this case “Localize me!” would be shown.

How can the Anvil Team help?
If you like the library and want to go further into this direction, there are a few things that would make translating apps with Fluent easier (listed from most to least helpful):

  1. Make Fluent’s .ftl files editable as a text file. Right now, if you click on a ftl-file, the editor does not show anything. You have to clone the repository and make changes locally. You could also add the option “open as text” into the context menu of the assets section regardless of the file type.

  2. Add ftl to the “Add Asset” modal window. This way you can create an ftl file in the editor instead of uploading a finished or empty file (if you decide to implement the first suggestion).

  3. Add the data-l10n-id and data-l10n-args tags into to the standard / anvil_extras components and make them editable in the Anvil editor as well as by setting a property. This way users would not have to define a Message object for each component to translate. If you do that, you could also run Fluent’s DOM translation mechanism by default. This would essentially make this library obsolete (I would be fine with that).

  4. There are multiple packages available for fluent, e.g. @fluent/syntax for parsing, serializing, and working with the Fluent syntax. Maybe it is possible to implement syntax highlighting with that.

As of 2023-03-03 this is the first version of the library. I have tried everything I have just shown but if you notice any errors, feel free to tell me. If you want to contribute or just have a look at the source code you can clone the repository at github.

Enjoy! :wink:


When I try to add this as a third-party dependency, I get an error:

I think maybe Anvil Staff need to get involved to authorize third-party dependencies to work like that? Meanwhile, you could instead provide a clone link, though the github already provides one way to access the code.

1 Like

Yes, I think it might need some additional permissions from the Anvil staff. I used the App token instead of the clone link, because the clone link would show a message like "[email address] has invited you to make a copy of their app ‘fluent_anvil’ " and I did not want to practically publish my email address. I just sent a personal message to the Anvil staff and asked for help.

1 Like

This app has now been made available as a third-party dependency, so adding it to your app now works from outside @Marcus’s account! (You can try again, @hugetim )

That’s right – per the docs, you’ll want to drop a line to us if you’d like to publish an app as a public third-party dependency.


Thank you for making the App available as third-party dependency. That was quick! :+1:

1 Like

Yep, I can confirm it works now. :muscle:

1 Like

There are two new features now:

  1. You can now translate the values of dictionaries. This is handy if you want to translate the options of a dropdown. For example:
my_greeting_options= {
        0: "greeting-hello",
        1: "greeting-welcome",
        2: "greeting-welcome-back",

my_greeting_options= fluent.format(my_greeting_options)
  1. The fluent_anvil library now comes with a component called “NativeDatepicker” which uses the browser’s <input type=“date” or <input type=“datetime-local” elements. From what I have read about date pickers I assume that the browser compatibility regarding older browsers is slightly reduced compared to Anvil’s built-in date picker. However, the NativeDatepicker has two advantages: First, it uses the user’s local calendar format and month names. Second, it looks much better on mobile devices. Take a look at the pictures below. The browser is set to German in all cases. However, only the native_datepicker provides a localized date format.

Anvil’s date picker on Android using Firefox:

The new NativeDatepicker on Android using Firefox:

The new NativeDatepicker on Windows using Google Chrome:
Calender Chrome


Hello again,
I would like to announce a larger update of fluent_anvil with cool new features and a few bug fixes. Unfortunately, this is a breaking change. If you would like to continue using the old API, just clone the app and checkout the commit tagged “v0.1.0”. The changes are outlined in the section “Breaking Changes” below. I noticed that Anvil Extras allows you to simply select the desired version. However, I was not able to reproduce this with fluent_anvil. I assume that I would need a paid plan for this.
Also, I do not know whether anybody is using the library (a counter would be a nice feature :wink:) and I do not want to break your app without prior notice. So, I will publish the new version sometime between 12th and 14th of May and not immediately. However, you can use the new version already by selecting the development version in your dependency settings.

You can find a complete and updated tutorial on the Fluent-Anvil Github Page.

Feature 1: Translating Lists of Dictionaries (Anvil Extras MultiSelectDropdown)

Lists of dictionaries are commonly used to model tables: The list entries represent the table’s rows and each dictionary entry represents a named column. You can translate these using the new format_table() method. In the following example we are going to translate the names of time units. The data structure is typical to what the MultiSelectDropdown from the Anvil Extras package expects:

options = [
    {"value": "nanosecond", "key": "unit-nanosecond"},
    {"value": "microsecond", "key": "unit-microsecond"},
    {"value": "millisecond", "key": "unit-millisecond"},
    {"value": "second", "key": "unit-second"},
    {"value": "minute", "key": "unit-minute"},
    {"value": "hour", "key": "unit-hour"},
    {"value": "day", "key": "unit-day"},
    {"value": "week", "key": "unit-week"},
    {"value": "year", "key": "unit-year"}

translated_options = fluent.format_table(options, ["key"], my_contenxt_var = "my context")

The first parameter is the table and the second parameter is a list of keys to translate. The fluent_anvil library assumes that the value of every given key represents a message id. Other keys and their values will not be touched and returned as-is. Context variables can be provided as keyworded arguments or omited completely. For example, consider you selected “de_DE” as locale and had someone provide you with the corresponding .ftl file containing a German translation. The result might look like this:

translated_options = [
    {"value": "nanosecond", "key": "Nanosekunde"},
    {"value": "microsecond", "key": "Mikrosekunde"},
    {"value": "millisecond", "key": "Millisekunde"},
    {"value": "second", "key": "Sekunde"},
    {"value": "minute", "key": "Minute"},
    {"value": "hour", "key": "Stunde"},
    {"value": "day", "key": "Tag"},
    {"value": "week", "key": "Woche"},
    {"value": "year", "key": "Jahr"}

Feature 2: Validation

An essential part of a good user interface is proper input validation. This requires that you provide feedback to the user in a language the user understands. The fluent_anvil library now has you covered there as well: The validator module defines a Validator class with which you can define a translated validation procedure.

As an example, consider a datepicker called my_datepicker that allows the user to define a deadline for a task. We want to validate that the selected date is not in the past. Otherwise, a message informing the user about the invalid date shall be shown using a label component called my_label. A solution might look like this:

from fluent_anvil.lib import Validator
from datetime import datetime

deadline_validator = Validator(
    lambda value: value >= datetime.now().astimezone(),
    my_context_var = "my context"

The Validator initialization only requires two parameters:

  • A function that returns True, if the value to be validated passed the validation test. False, otherwise. Alternatively, you may also provide a Zod validator from Anvil Extras.
  • A message id that represents an explanatory message to the user if validation fails.

In the form class of your Anvil app, you can define a change event for the datepicker in which validation is performed:

from ._anvil_designer import EditDateTemplate
from fluent_anvil.lib import ValidationError

# Some other code like the definition of deadline_validator.

class EditDateForm(EditDateTemplate):

    # Some other code

    def my_datepicker_change(self, **event_args):
            self.my_label.text = ""
        except ValidationError as error:
            self.my_label.text = str(error)

The validate(value, *args, **kwargs) method calls the lambda function defined earlier. The validation function is not limited to a single parameter. You can define an arbitrary validation function signature as long as it has at least one required parameter. You then provide all required parameter values to validate(value, *args, **kwargs) which in turn passes them on to your validation function without change.

If validation succeeds, the label’s text attribute is set to an empty string (effectively hiding it). If validation fails, a ValidationError is thrown. The exception message will contain the translation of the message "deadline-in-the-past" defined earlier.

Multiple validation steps can be chained by alternately providing validation function and message id during initialization of the Validator class like this:

from fluent_anvil.lib import Validator

text_length_validator = Validator(
    lambda text: len(text) > 10,
    lambda text: len(text) < 120,
    my_context_var = "my context"

When calling validate(value, *args, **kwargs) the validation functions are called one after another. In the above example, it is first checked whether the text is long enough. After that, it is checked whether the text is short enough. As usual, optional context variables can be passed on to the Fluent translation string by providing them as keyworded arguments.

Validator objects are callable. This is useful, if you do not want to throw an exception:

from ._anvil_designer import EditDateTemplate
from fluent_anvil.lib import ValidationError

# Some other code like the definition of deadline_validator.

class EditDateForm(EditDateTemplate):

    # Some other code

    def my_datepicker_change(self, **event_args):
        self.my_label.text = deadline_validator(self.my_deadline.value, "")

If the provided value passes validation, the given default value is returned (usually an empty string, None or some other special value). Otherwise, the translated error message is returned.

So, should you use my_validator.validate(value, *args, **kwargs) or my_validator(value, default, *args, **kwargs)? This depends on what you want to do in case validation fails. If you just want to display a message, call the validator. If you want to do multiple things at once like showing the message and changing the role of a component (e.g. to highlight the text box for which validation failed), use my_validator.validate(value, *args, **kwargs) in a try…except block as shown in the first example.

Breaking Changes:

  • Instead of having to initialize the Fluent class, there is now a singleton instance called fluent. Therefore, the import mentioned in my first post becomes:
    from fluent_anvil.lib import fluent, Message

  • You can now configure the instance using:
    fluent.configure(["es-MX"], "localization/{locale}/main.ftl")
    This will tell fluent to use the Mexican Spanish locale. The first parameter is a list of desired locales. Locales are now given in the order of preference (most preferable first). This means, Fluent will always try the first locale in the list when trying to find a translation. If a translation is not available for that locale, Fluent will try the others one after another until a suitable translation has been found. The second parameter is the template string indicating where the translation files are stored.

  • The template string “localization/{locale}/main.ftl” is now the default. So, if you store your translations files in the way outlined in my first post, you can omit it.

  • Note that you do not have to provide the full URL starting with ./_/theme/ . It will be prepended automatically. If your translation files are stored somewhere else entirely you can explicitly set the prefix by adding it to the end of the parameter list of fluent.configure().

  • The set_locale() method now accepts a single list of locales, e.g. fluent.set_locale(["en-US", "en-GB", "en-AU"]). Again, the first locale is the most preferable. The others are fallback locales.

1 Like

there is another update for Fluent Anvil! It know comes with extensive data from the IETF Language Subtag Registry and the Common Locale Data Repository.

The IETF Language Subtag Registry practically defines tags for all known languages (including almost extinct ones). Fluent Anvil comes with a preprocessed copy of this registry (excluding obsolete and duplicate tags). You can easily use it to create dropdowns that allow your users to define a locale. For example, your users may upload a document and provide the locale it is written in.

You can access this registry by using one of the following functions:

from fluent_anvil.lib import fluent

# Returns a dictionary with language tags as keys and the name of the language as value.
# Example: 
# {
#   'soa': 'Thai Song', 
#   'adf': 'Dhofari Arabic', 
#   'atv': 'Northern Altai', 
#   'aqm': 'Atohwaim', 
#   ...
# }

# Returns a dictionary with region tags as keys and the name of the region as value.
# Example: 
# {
#   '419': 'Latin America', 
#   'TL': 'Timor-Leste', 
#   'GD': 'Grenada', 
#   'SA': 'Saudi Arabia', 
#   'LU': 'Luxembourg', 
#   'ID': 'Indonesia', 
#   'PF': 'French Polynesia', 
#   ...
# }

# Returns a dictionary with script tags as keys and the name of the script as value.
# Example: 
# {
#   'Glag': 'Glagolitic', 
#   'Loma': 'Loma', 
#   'Batk': 'Batak', 
#   'Avst': 'Avestan', 
#   'Khmr': 'Khmer',
#   ...
# }

While IETF language tags are used to compose an arbitrary locale code (even nonsensical ones like “de-SA” for German as spoken in Saudi Arabia), the Common Locale Data Repository (CLDR) provides locale information like country names, month names, currency names, currency formatting, etc. for real locale codes such as “yue-Hant” for Traditional Cantonese. You can obtain all available locale options by using:

from fluent_anvil.lib import fluent

# Returns a dictionary with locale codes as keys and the name of the locale as value.
# Example: 
# {
#   'en-MS': 'English (Montserrat)', 
#   'ksh': 'Colognian', 
#   'fr-CF': 'French (Central African Republic)', 
#   'wae': 'Walser', 
#   'pt-LU': 'Portuguese (Luxembourg)', 
#   'fr': 'French', 
#   'en-JE': 'English (Jersey)', 
#   ...
# }

You can also obtain possible currency options like this:

from fluent_anvil.lib import fluent
# Returns a dictionary with currency tags as keys and the name of the currency as value.
# Example: 
# {
#   'IQD': 'Iraqi Dinar', 
#   'SDP': 'Sudanese Pound (1957–1998)', 
#   'ARL': 'Argentine Peso Ley (1970–1983)', 
#   'ESA': 'Spanish Peseta (A account)', 
#   'MAD': 'Moroccan Dirham', 
#   'AWG': 'Aruban Florin', 
#   'CHF': 'Swiss Franc', 
#   'GNF': 'Guinean Franc',
#   ...
# }

All names returned by the get_[something]_options() (i.e. the dictionary values) are given in the selected locale or one of the selected fallback locales (as set by fluent.set_locale()) if the user’s browser can translate them. If not, the names are returned in English. If you would rather omit the name than bother your users with an English translation, you can do so by setting the translatable_only parameter of these functions to True.

All these functions also have a style parameter that can be set to one of the following:

  • fluent.STYLE_DIALECT_NARROW ; e.g. “Traditional Chinese (Hong Kong)”
  • fluent.STYLE_STANDARD_NARROW ; e.g. “Chinese (Traditional, Hong Kong)”

Note that not all styles lead to a different result for all subtags or locale codes. In fact, the results are often the same for multiple styles. This is not a bug, because how the styles are interpreted depends on the selected output locale (as set by fluent.set_locale()) as well as the locale code or subtag that shall be translated.

If you already have a subtag, locale code, or currency code, you can translate it into the locale selected by fluent.set_locale() like this:

from fluent_anvil.lib import fluent

# Returns "American English"

# Returns "Germany"

# Returns "Latin"

# Returns "Icelandic Króna"

The style parameter explained above is also available for all get_[something]_name() functions.
More information is available on Fluent Anvil’s Github Repository.

Everything described above is available starting now!
Enjoy! :wink:

1 Like

Hi again,
I just published a new update that I should have thought of a lot sooner :man_facepalming: :

It is now possible to structure your translations into multiple files (e.g. you may have a separate file for each form). You can do this by simply providing the path templates to all .ftl files as a list:

files = [
fluent.configure(["es-MX"], files)
1 Like

Hi Marcus,
that sounds great. I have understood how to translate for example a label.
How does it work with the data from tables let’s say for a repeating panel.
How will that be translated?
Cheers Aaron

Hello Aaron,
if you have predefined translations stored in a .ftl file (see introductory post), then you can use fluent.format_table(...) (see example from May 2).

However, I think you are intending to translate user generated content that is stored in a database table and not in a file somewhere as part of the application’s assets. For this, you have to wait for the next release. I am currently working on that. :wink:

Best regards,

Hello again,
good news! There is now a larger update available on the development branch that I will publish in about two weeks, so you have time to adapt, if necessary. In particular, the fluent.configure() method changed slightly. Here is a quick overview:

Feature 1: FluentAnvil now automatically selects the locale your users prefer most.
An important change comes from the new requirement to place an index.lst file in your localization directory. It is just a list of all locales you have translation files for (one per line) and that you want to serve your users (omit locales you want to hide for whatever reason) like this:


In turn, FluentAnvil will automatically determine which locales your users prefer and select the best matching, available locale automatically. It tries to achieve the minimum language distance (as proposed by the Unicode Consortium) between the locales the user prefers and the ones available. You can still force a specific locale, though.

Feature 2: You can now use your validators on server-side as well.
When receiving data from the client, validation should also be performed on the server. This is because the client can alter the code running on the client’s browser but not the code running on the server. Now, you do not have to write your validators again and can instead just reuse the ones you have already written for the client! It works like this:


# Import your text_length_validator from the above example here.

def save_text(text):
    text_length_validator.validate(text, True)
    # Do the actual saving here 


    anvil.server.call("save_text", "My nice text.")
except ValidationError as e:

If validation fails, FluentAnvil will send the corresponding message id to the client. Just use the translate() method on the exception, convert it to a string and display it somewhere as shown in the example above.

Feature 3: FluentAnvil can now directly format dates and numbers.
You can now use FluentAnvil to directly format numbers and dates without defining a message id like this:

import datetime

# Print the number 32000 the way it is written in the USA.
fluent.locale = ["en-US"]
print(fluent.format(320000)) # Displayed as: 320,000

# Print the number 32000 the way it is written in Germany.
fluent.locale = ["de_DE"]
print(fluent.format(320000)) # Displayed as: 320.000

mydate = datetime.datetime.fromisoformat("2011-11-04T03:05:23")

# Print the date 2011-11-04T03:05:23 the way it is written in the USA.
fluent.locale = ["en_US"]
print(fluent.format(mydate)) # Displayed as: Nov 4, 2011, 3:05:23 AM

# Print the date 2011-11-04T03:05:23 the way it is written in Germany.
fluent.locale = ["de_DE"]
print(fluent.format(mydate)) # Displayed as: 04.11.2011, 03:05:23

mydate = datetime.time.fromisoformat("04:23:01")

# Print the time 04:23:01 the way it is written in the USA.
fluent.locale = ["en_US"]
print(fluent.format(mydate)) # Displayed as: 4:23 AM

# Print the time 04:23:01 the way it is written in Germany.
fluent.locale = ["de_DE"]
print(fluent.format(mydate)) # Displayed as: 04:23

If you have special requirements regarding the way dates and numbers shall be formatted, you have various options for customization at your disposal. You can provide these using fluent.configure(). For example:

    # Make dates really long and verbose.
    datetime_options = {
        "dateStyle": "full", 
        "timeStyle": "full",
    # Use scientific notation and always display the sign.
    number_options = {
        "notation": "scientific",
        "signDisplay": "always"

With that, the previous example becomes:

import datetime

# Print the number 32000 the way it is written in the USA.
fluent.locale = ["en-US"]
print(fluent.format(320000)) # Displayed as: +3.2E5

# Print the number 32000 the way it is written in Germany.
fluent.locale = ["de_DE"]
print(fluent.format(320000)) # Displayed as: +3,2E5

mydate = datetime.datetime.fromisoformat("2011-11-04T03:05:23")

# Print the date 2011-11-04T03:05:23 the way it is written in the USA.
fluent.locale = ["en_US"]
print(fluent.format(mydate)) # Displayed as: Friday, November 4, 2011 at 3:05:23 AM Central European Standard Time

# Print the date 2011-11-04T03:05:23 the way it is written in Germany.
fluent.locale = ["de_DE"]
print(fluent.format(mydate)) # Displayed as: Freitag, 4. November 2011 um 03:05:23 Mitteleuropäische Normalzeit

Feature 4: With the new Text class you can manage multiple translations of static text.
The primary use case of this class is best explained using an example: Imagine you are a German speaker living in Germany. Your first language is German and most of your colleagues at work speak German but a few of your colleagues only speak English. You want to write some technical text. You can explain it best in German, but you also want your English-speaking colleagues to understand it. Fortunately, the system you write your text in allows you to enter your text in multiple languages. This is where this class comes into play: It manages multiple translations of the same static text.

If you apply str(my_text_object) on your text, it will also automatically select the language the reader prefers. You can also transmit the Text object between client and server because it is a portable class. You can save some bandwidth by using the slice() method before transmission so that only one or a small number of translations is transmitted.

Feature 5: Documentation
There is now a complete API documentation available here.

Functions like fluent.get_language_options(), fluent.get_regions_options(), etc. are now much faster and do not require a server function call anymore.

Enjoy! :sunglasses:

1 Like

I just published the new version I introduced in my last post. Sorry for the delay.

1 Like