Getting Started
If you'd like to help translate Anki into your native language, your help would be most appreciated!
Translations can be done through our online translation interface. To make changes, you will need an account. Please post a private message on the support site with the following information, and we can get an account created for you:
- The language or languages you'd like to translate.
- Your email address. Please note that the email address you provide will be visible to people browsing GitHub.
- The username you'd like to use (for example, 'bob5')
- Please include the following text in your message: "I license any translations I contribute under the 3-clause BSD license."
After you've contributed some translations, if you'd like to be added to Anki's About screen, please send a message and we'll gladly add you (or you can submit a PR if you know how).
Translating Anki's User Interface
Anki's translations are divided up into two parts:
- The core module contains text usable on all Anki platforms.
- The desktop module contains text only used in the computer version of Anki.
Core Interface
The core
module in the translation site
uses a translations system called Fluent. It is
a recently-developed system that makes it easier for translators to deal with
plurals, and makes it easier for developers to provide useful comments to help
with translation.
Translations in the core
module are divided up into multiple files, with each file
focusing on a particular topic, such as scheduling, deck options, and so on, as
can be seen here.
Simple Replacements
When the text is a simple string, all you need to do is write the text in your native language and click Save (or press the Enter key).
Under the English text, many strings will contain a comment to help you understand where the string is being used, or to give an example of how it appears.
"Context" is the short name for this string, and may sometimes give you a hint as to where it is used or what it is trying to represent.
Variables
When you see {$something}
in the English text, it means that that text will
be replaced with something else. You can change the position of {$something}
in your translation, but please don't change the text inside - you should not
attempt to translate the text inside the {...}
part. You can click on the
{$something}
part of the English text to automatically copy it into your
translation.
Plurals
Many languages change words depending on number. For example, English uses "1 cat", but "3 cats".
Some of the strings to translate will look like this:
In the top part, you can see there are two forms for English - the one
case (second), and the other
case (seconds).
The bottom section shows the translation in Polish, which has four different
plural forms. In this case, the few
case and many
case have been translated
with the same translation.
The translation website should automatically show the appropriate number of boxes for your language. Languages like Japanese that do not have separate plural forms will only need to enter in a translation once.
Advanced Plurals
The translation website supports an advanced mode for when you need more flexibility. You can access the advanced mode by clicking on the FTL icon on the bottom left. The previous example then looks like this:
The first line is the unique identifier for this message - it can also be seen in the "CONTEXT" label near the comments.
The rest of the text means the following:
- Check the value of amount
- If the amount is
one
, show{$amount} sekunda
- If the amount is
few
, show{$amount} sekundy
- If the amount is
many
, show{$amount} sekund
- If the amount is
other
, show{$amount} sekundy
The definitions of few, many and other depend on your language.
The *
character before other means that any unknown value should
match other
. Since in this example few
is the same as other
, the
text could thus be shortened to:
time-span-seconds = { $amount ->
[one] { $amount } sekunda
[many] { $amount } sekund
*[other] { $amount } sekundy
}
As few
is not listed here, it will be covered in the other
case.
It's generally best to list out all cases however, as otherwise the
translation website will shown a warning that you may have forgotten
to translate a case.
When writing these rules manually, please make sure to add at least one space to the start of each line except the first.
While there was little benefit from rewriting the text in this case, this flexibility becomes useful when your language doesn't fit well with the structure of the original English string.
For example, when viewing cards in the Browse screen in Anki, Anki shows new cards with a number, such as "New #1", or "New #532". The English doesn't use plurals in this case, but other languages may need to.
If you were translating this into Spanish, you'd see the following screen:
To add a plural form, click the FTL button, and then change the text to read:
due-for-new-card = { $number ->
[one] Nueva #{ $number }
*[other] Nuevas #{ $number }
}
Here's another example. In this case the English text is using plurals as well, but only a small part needs to change. When translating to Polish, the "used by" portion needs to change as well. So the default editing screen is not sufficient:
To fix this, we click on the FTL button to get a blank template:
And then modify the template as required:
Other Languages
If you're not sure how something should be translated, you can click on the Locales tab on the right. You can see a summary of the translation on that screen, or click on a particular language to see how it has translated all its plural forms.
Special Characters
When a translated string is more than one line long, please keep an eye
out for lines that start with .
or [
. Strings can start with these characters
on the first line, but if a translation stretches over multiple lines, these
characters will cause problems.
For example, the following is fine:
some-translation =
This is a translation that occurs
over ... two
or three lines.
But the following will cause problems, because the second line starts with a [
,
and the third starts with a .
:
some-translation =
[This is a translation that occurs over
...two
or three lines.]
When you need to use a [
or .
character at the start of a a line
in a multi-line string, please escape it by wrapping it in {"
and
"}
- for example:
some-translation =
{"["}This is a translation that occurs over
{"."}.. two
or three lines.]
Testing
Each time a new Anki beta is released, it will include any translation changes that were made since the last time. Please give the betas a try, so you can see how your translations appear in the app. There's a beta testing section on the support site that you can follow for beta updates.
When translating, please try to keep the translations about the same length as the original English text. If it is not possible, and you find that text is not appearing properly in a beta (such as two labels overlapping), please report the issue on the support site.
When testing the betas, if you notice that some text is not translatable, please report this on the support site so it can be fixed.
Desktop Interface
The majority of Anki's interface is now included in the core
module. desktop
contains some strings specific to the computer version, such add-ons. The strings
were mostly automatically migrated from Anki's old translation system, so there
may be some strings in core that only apply to the computer version, or vice versa.
The Manual
Translating the manual is trickier than translating Anki's interface.
If you're a fairly technical user, you can fork the manual's repo, and translate one or more of the manual's text files.
After translating at least one file, please get in touch.
For Developers
When adding user-visible strings to Anki's codebase, extra work is required to make the strings translatable.
Anki's codebase has recently migrated away from the old gettext system,
and now only uses Fluent's col.tr
and aqt.utils.tr
.
Please start by taking a look at the core documentation to see how strings are presented to translators. Note how translators can not see the areas of the code where a string is used, so comments are often required to help the translators understand what they are translating.
Adding New Strings
As an example, imagine we want to add the string "You have x add-ons".
To add a new translatable string to the codebase, we first need to identify
whether it belongs in the core
module (text likely to be used by all Anki clients),
or whether it is specific to the computer version (the desktop
module).
Add-ons are only supported by the computer version, so we'll want to use
the desktop
module in this example.
- The English
core
files are stored inftl/core
- The English
desktop
files are stored inftl/qt
We'll look for a file like ftl/qt/addons.ftl
, and add one if no appropriate
one exists. Then we need to add the string to the file.
Each string needs a key that uniquely identifies it. It should start with the same text as the filename, and then contain at least a few words separated by hyphens. For example, we might add the following to the file:
addons-you-have-count = You have { $count } add-ons.
If count
is 5, then the string will be "You have 5 add-ons.". This means
it will look strange if count
is 1 - we'd see "You have 1 add-ons." instead
of "You have 1 add-on.". We'll need to add another string to cover the singular
case:
addons-you-have-count = You have { $count ->
[one] 1 add-on
*[other] {$count} add-ons
}.
The text above tells Fluent to use the first string in the singular case, and use the second string in all other cases.
While an improvement, this can make it a bit hard for translators when the "you have" part needs to change depending on the number. So while more verbose, it is better to list out the alternatives in full where possible:
addons-you-have-count = { $count ->
[one] You have { $count } add-on.
*[other] You have { $count } add-ons.
}
This leaves the most control in the hands of the translators to be able to translate the sentence with a natural structure.
Note how the variable was used in the singular case as well. While in English
$count will only be 1 in the singular case, in other languages, the
singular case can be used for other numbers, such as 21. While translators
could replace the 1
with { $count }
in their translation, they sometimes
do not realise they need to, so using the variable makes mistakes less likely.
Please note that the bulk of the strings in the .ftl files were imported from the older gettext system, so many of them may not demonstrate best practice. The older system also encouraged constructs like:
msg = "%s %d %s" % (_("Studied"), card_count, _("today"))
Please avoid constructing strings in Python like this, as it makes it hard for translators to translate, and not all languages use English spaces. The above example would be better made into a single translatable string that takes a count argument.
Finally, we should also add one or more comments for translators:
### Lines starting with three hashes are shown in the translation system
### for every string in the file.
### You can use it for a general summary of what a file is dealing with. Not
### all files will need this.
## Lines starting with two hashes are shown for every following string, until
## another set of two hashes is encountered, or the file ends.
# This is a comment that will only apply to the following string. Eg:
# Shown in the add-on management screen (Tools>Add-ons), in the title bar.
addons-you-have-count = { $count ->
[one] You have { $count } add-on.
*[other] You have { $count } add-ons.
}
Accessing the New String
Once you've added one or more strings to the .ftl files, run Anki in the source tree as usual, which will compile the new strings, and make them accessible in Python/Typescript/Rust.
Python
To resolve a string, you can use the following code:
from aqt.utils import tr
msg = tr.addons_you_have_count(count=3)
- Note how a snake_case() function has been automatically defined as part of the build process, with the correct arguments. These will be checked by the tests, to ensure you don't accidentally use a missing string, or omit an argument/pass the wrong one.
- The generated function has a docstring based on the original text in the ftl file, so if you're using an editor like PyCharm, you can hover the mouse over a function to see the text it will resolve to.
- Code in qt/ can use the
aqt.utils.tr()
function, but code in pylib should usecol.tr()
instead.
If you'd like to test out the strings in the Python repl, make sure to call set_lang() first.
>>> from anki.lang import set_lang
>>> from aqt.utils import tr
>>> set_lang("en", "")
>>> tr.addons_you_have_count(count=3)
'You have \u20683\u2069 add-ons.'
Note the unicode characters that were inserted. These ensure that when left-to-right and right-to-left languages are mixed, the text flows correctly. For debugging, you can strip the characters off:
>>> from anki.lang import without_unicode_isolation as nosep
>>> nosep(addons_you_have_count(count=3))
'You have 3 add-ons.'
>>> nosep(addons_you_have_count(count=1))
'You have 1 add-on.'
TypeScript
A global is available with automatically generated functions in camelCaps. Eg:
import * as tr from "../lib/ftl";
console.log(tr.statisticsCardsPerDay({ count: 5 }));
The functions have docstrings that have the original text, so you can hover over them to see what the text says.
To make use of the translations, they first need to be fetched from the backend. You'll need to specify the modules you want included, eg:
import { setupI18n, ModuleName } from "../lib/i18n";
await setupI18n({ modules: [ModuleName.STATISTICS] });
Rust
The backend and collection structures have an I18n object in .tr, which can be called to get a translation, eg, in a Collection method:
#![allow(unused)] fn main() { println!("{}", self.tr.database_check_missing_templates(5)) }
The docstring contains the original translation, and your editor can show you the names of each argument.
The return value is a Cow - if you need a String, you can call .to_string()
or
.into()
on it.
Legacy style
Anki versions before 2.1.44 used a single function and a separate constant instead. Eg, instead of
from aqt.utils import tr
msg = tr.addons_you_have_count(count=3)
they instead used:
from aqt.utils import tr, TR
msg = tr(TR.ADDONS_YOU_HAVE_COUNT, count=3)
This old style will still work for now, but will be removed in the future.
Repeated Content
Sometimes you will need multiple translations that have some text shared between them. For example, the search code has a list of separate error message, such as:
search-invalid-added = Invalid search: 'added:' must be followed by a positive number of days.
search-invalid-edited = Invalid search: 'edited:' must be followed by a positive number of days.
Since there are a lot of separate messages with the same prefix, we can split it off into a separate string:
search-invalid-search = Invalid search: { $reason }
search-invalid-added = 'added:' must be followed by a positive number reason.
search-invalid-edited = 'edited:' must be followed by a positive number of days.
The code can then compose the message from two separate entries. Note that we're using a Fluent variable in the outer string, as some languages may want to place the reason first, not include a space after the colon, and so on.
Avoid HTML where possible
Translators may not have any development experience, and HTML can be difficult to read and translate correctly. Prefer plain text where possible, and when HTML is required or would make things much clearer, consider using markdown instead (see the search translations for an example of how this is done).
Avoid Strings That Will Change
Avoid doing things like listing out a series of options in a string, if there's any chance that list will change in the future. When the string later gets updated (and assuming people don't forget to update it), it will need to be given a new ID so that translators become aware of it, and doing so will mean all the existing translations get invalidated until a translator has a chance to update them, which may take months and sometimes even years.
Avoid Excess Strings
Please try to be conservative with the number of new strings you add, as translator time is precious, and the more strings included in Anki, the more overwhelming it can be for translators. If you need to display an error message for some obscure error that most people will never hit, it probably doesn't need a translation.
Add-ons
Add-ons can make use of existing strings in Anki, but if you wish to add new strings, you'll need to add them to your add-on rather than Anki. How you approach translations in your add-on(s) is up to you:
- If you don't care about translations, the simplest solution is to use plain strings.
- The next-easiest option is to place all strings in a dict or python module, and accept pull requests from users that add a new language. You can then select the appropriate dict by looking at what anki.lang.currentLang is set to.
- If you want proper plural support, you'll want to consider using gettext, though it is considerably more involved than the above solution. It requires separate build steps to extract strings from your source and build compiled versions of the strings. You can have translators email you a completed .po file, or send you a PR. There are online translation services like Launchpad and Crowdin that support gettext, but you would need to either manually upload and download files, or spend some time setting up scripts to do so.
Fluent is not currently recommended for add-ons. There is limited tooling support for it, and you would need to bundle the Fluent Python libraries with your add-on.
AnkiMobile
AnkiMobile's translations are divided into two parts:
- The
core
module contains text common to both the computer version and AnkiMobile. - The
mobile
module contains text only used in AnkiMobile.
If you have not signed up to the translation site yet, please see Getting Started.
Please start with the core
module, which is documented here.
Once the core
module is mostly translated in your language, you can then
move on to the mobile
module.
When translations are nearing completion, please reach out on the support site, as new languages need to be manually added to the build process.
Once a new language has been added, your translations will be visible in subsequent beta builds and app store releases. Please give the betas a try, so you can see how your translations appear in the app. If you're not already a beta tester, please request an invite on the support site.
When translating, please try to keep the translations about the same length as the original English text. If it is not possible, and you find that text is not appearing properly in a beta (such as two labels overlapping), please report the issue on the support site.
Thank you again for your help!
Translating AnkiDroid
AnkiDroid has its own translation site:
https://github.com/ankidroid/Anki-Android/wiki/Translating-AnkiDroid
Work is currently underway to integrate Anki's core
module and its translations
into AnkiDroid, but for now, AnkiDroid only uses its own translations.