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:
en_US/main.ftl:
close-button = Close
de_DE/main.ftl:
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
- es_MX
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
Fluent.get_preferred_locales()
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:
en_US/main.ftl:
time-elapsed = Time elapsed: { $duration }s.
and
de_DE/main.ftl:
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:
print(fl.format(
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:
fl.format(
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):
-
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.
-
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).
-
Add the
data-l10n-id
anddata-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). -
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!