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