-
Notifications
You must be signed in to change notification settings - Fork 27
Dynamic translations
By casting values to string and comparing them to hardcoded strings, it is possible to display strings that are in the player's language.
The value Map.PRACTICE_RANGE
is one of the few that have separate translations for each language, and is therefore a good language detector. Using a dictionary, we can map the translation of practice range to our translated strings:
print({
"Practice Range": "english string",
"Trainingsbereich": "german string",
"Champ de tir": "french string",
}["{}".format(Map.PRACTICE_RANGE)])
It is important however that "{}".format(Map.PRACTICE_RANGE)
be evaluated client-side. This means it must be in the action directly (cannot be stored into a variable), and the reevaluation must include strings. Else, it will instead take the language of whoever last modified the code.
This dictionary compiles to:
["english string", "german string", "french string"][max(false, ["Practice Range", "Trainingsbereich", "Champ de tir"].index("{0}".format(Map.PRACTICE_RANGE)))]
We can save elements by caching the practice range translations. However, this only works if you have translations for each language, and all translations will have to be in the same order:
globalvar practiceRangeTranslations = ["Practice Range", "Trainingsbereich", "Champ de tir"]
print(["english string", "german string", "french string"][max(false, practiceRangeTranslations.index("{0}".format(Map.PRACTICE_RANGE)))])
And, to keep it a dictionary, we can make it a JS macro:
const availableLanguages = ["en-US", "de-DE", "fr-FR"]
//check if all languages are here
for (var lang of availableLanguages) {
if (!translations[lang]) {
throw new Error("Missing language '"+lang+"'");
}
}
//check if we put a language that isn't available
for (var key in translations) {
if (!availableLanguages.includes(key)) {
throw new Error("Language '"+key+"' is invalid");
}
}
JSON.stringify(availableLanguages.map(x => translations[x]))+'[max(false, practiceRangeTranslations.index("{0}".format(Map.PRACTICE_RANGE)))]';
#!define i18nstr(translations) __script__(i18nstr.js)
print(i18nstr({
"en-US": "English string",
"fr-FR": "French string",
"de-DE": "German string",
}))
Let's say we have a string "Wave {}".format(currentWave)
. In French it is "Vague {}"
but in Chinese it is "第{}波"
.
There are two ways of handling formatters. The first is to duplicate the code:
print(i18nstr({
"en-US": "Wave {}".format(currentWave),
"fr-FR": "Vague {}".format(currentWave),
"zh-CN": "第{}波".format(currentWave),
}))
The second is to allocate a separate string for each text to translate. Here, we must allocate 2 strings because the Chinese string has text after the formatter. In this specific case, we use a variable to store the empty string to save elements:
print("{}{}{}".format(i18nstr({
"en-US": "Wave",
"fr-FR": "Vague",
"zh-CN": "第",
}), currentWave, i18nstr({
"en-US": emptyString,
"fr-FR": emptyString,
"zh-CN": "波",
})))
Which method you use will depend on the length of the formatters. Here, the formatter content is just currentWave
, so the first method is preferrable. However, if the formatter content was a complex formula, it might be wise to do the second method in order to avoid code duplication.
For ease of use by translators, putting the strings in an enum will make the process easier and the code more readable:
enum String:
SCORE = i18nstr({
"en-US": "Score: {}".format(score),
"fr-FR": "Score: {}".format(score),
"zh-CN": "分数:{}".format(score),
}),
WAVE = i18nstr({
"en-US": "Wave {}".format(currentWave),
"fr-FR": "Vague {}".format(currentWave),
"zh-CN": "第{}波".format(currentWave),
}),
rule "Init":
print(String.SCORE)
print(String.WAVE)
It has been reported to me that the workshop's cache may interfer with the translation. The workshop may decide (despite reevaluation being enabled for strings) that the string never changes (probably because the only dependency is the practiceRangeTranslations
variable, and that variable never changes).
A solution would be to introduce a false dependency on getTotalTimeElapsed()
, by replacing max(false
by max(not getTotalTimeElapsed()
. We can assume that getTotalTimeElapsed()
is not 0 (or it is but at the very beginning of the game and it doesn't matter).
Or, replacing by max(isInSuddenDeath()
would also work, provided your gamemode is not CTF. You could also replace it by isMatchEnded()
if you assume this value to always be false while displaying your strings.
This has yet to be tested.
OverPy:
- Overview
- General Usage
- General Syntax
- Rules and Events
- Functions
- Control Flow
- Strings
- Compiler Options
- Custom game settings
- Preprocessing
- Advanced Constructs
Various stuff:
Development:
- [Coming soon]