Character Sheet Translation

Character Sheet i18n, or internationalization, will allow you to design your character sheet in such a way that our community of translators will be able to translate your sheet into their language, making that language available to anyone on Roll20. If your sheet has been translated into another language, say French, and someone using your sheet has their account language set to French your sheet will show up with the translated texts for that user.


Designing Your Sheet

Setting up your sheet to be able to respond to the i18n service is very simple, it just can take some time, especially if you are working with a large pre-existing sheet. There are 2 steps you must take to allow your sheet to be translated. Step one, mark the elements that you want to contain the translated text. Step two, create a translations file that contains all of the strings that will be translated. This file will be fed to our translation service, Crowdin, where our volunteers can go string-by-string and translate them into another language. This will generate the same translation file in all of the other languages, which we will then use accordingly when someone has that language selected on Roll20.

Since creating one single design that can fit multiple sizes of translated text can be hard, we've added a class to the parent "charsheet" element to let you have separate css for individual languages. The class is "lang-[2 char language code]", so if the language is English "lang-en" or French "lang-fr". This will allow you, after your sheet has been translated, to change a style, specifically for one language, if the new text for that language doesn't fit your current design.


Step One, Sheet Formatting

You will want to add the "data-i18n=[unique-key]" attribute to every element that has text in it that you want translated. Keep all of the text that is already there in the element, because it can be used to generate your translation file for you when you are done (explained later). It is especially important that the text stays there while the sheet-i18n code is only on Dev, otherwise, your sheet will no longer work on the main site until sheet-i18n makes it to main. If you want further examples of how the i18n translations work, check out the 5th Edition OGL by Roll20 sheet which has already been formatted with the i18n text.


Standard Text

<div class="col">
    <div class="row">
        <span data-i18n="acrobatics-u">ACROBATICS <span>(Dex)</span></span>
        <input class="num" type="text" name="attr_npc_acrobatics" placeholder="0">
    </div>
    <div class="row">
        <span data-i18n="animal-handling-u">ANIMAL HANDLING</span>
        <input class="num" type="text" name="attr_npc_animal_handling" placeholder="0">
    </div>
    <div class="row">
        <span data-i18n="arcana-u">ARCANA</span>
        <input class="num" type="text" name="attr_npc_arcana" placeholder="0">
    </div>
    <div class="row">
        <span data-i18n="athletics-u">ATHLETICS</span>
        <input class="num" type="text" name="attr_npc_athletics" placeholder="0">
    </div>
    <div class="row">
        <span data-i18n="deception-u">DECEPTION</span>
        <input class="num" type="text" name="attr_npc_deception" placeholder="0">
    </div>
    <div class="row">
        <span data-i18n="history-u">HISTORY</span>
        <input class="num" type="text" name="attr_npc_history" placeholder="0">
    </div>
</div>

Notice how every span in this code block has the data-i18n attribute with a key that references the text contained in the span. Also notice that there is HTML within the span, this is totally fine. The HTML will be marked by Crowdin so it is easy for them to copy and paste the HTML into their translation. While HTML is okay inside of the translation, try and keep it as simple as possible because the translators are most likely not code-savvy and may have issues copying the code if it's not simple. It is very important that every unique string has a unique key; notice that since this text is in all caps I'm using a -u at the end of each key. That is because these words show up later in the sheet, but are cased normally.

There are some cases where you will want to add extra context to the key that the translator might not get from the text alone, like with abbreviations.

<div class="row">
    <span data-i18n="components:-u">COMPONENTS:</span>
    <input type="checkbox" name="attr_spellcomp_v" value="{{v=1}}" checked="checked">
    <span data-i18n="spell-component-verbal">V</span>
    <input type="checkbox" name="attr_spellcomp_s" value="{{s=1}}" checked="checked">
    <span data-i18n="spell-component-somatic">S</span>
    <input type="checkbox" name="attr_spellcomp_m" value="{{m=1}}" checked="checked">
    <span data-i18n="spell-component-material">M</span>
    <input type="text" name="attr_spellcomp_materials" accept="Material" style="margin-left: 17px; width: 215px;" placeholder="ruby dust worth 50gp" data-i18n-place="ruby-dust-place">
</div>

Notice the keys for the V, S, and M texts spells out exactly what the single character is referring to. Otherwise, the translator would have no idea what "V" means. This way the translator can change V to the first letter of whatever "Verbal" is in their language if it is different.


Element Attribute Text

Many times you will have text inside of an element attribute that needs translated, for example, placeholder and titles. To replace the text in these attributes you can use "data-i18n-[attribute]='key'" to tell the parser that the key you are supplying is to be used to replace the text within the given attribute. For example, if you were replacing the text in a placeholder attribute:

<input name="attr_weapon_name" data-i18n-placeholder="weapon-name" placeholder="Hand Axe" />

The supported attributes are: title, alt, aria-label, label, placeholder

Variable Replacement

One special case is if you have text within a string that you do not want translated you can use special variables and the "data-i18n-vars" attribute.

<span data-i18n="var-test" data-i18n-vars="will not|be translated">This text {{0}} {{1}}</span>

The text inside of the data-i18n-vars attribute will be divided by the "|" character and placed in an array. You can then reference that array using {{[index]}} to reference the specific row of the array.


Dynamic Key Replacement

There are some situations where you need to fetch a translation based off of a changing key. For example, you have a "class selection" dropdown menu where the user can select their class. The class names that show up in this list can be translated using the normal tools. But, you cannot translate the value inside of the option in the select box. (we do this so that if you change languages, the sheet data doesn't have to be translated. The translation system stays purely in the front-end and not in the data layer) Instead, where you want to display this class name, translated, you mark the span with the attribute "data-i18n-dynamic". This will tell the parser to use the value that would normally show up in the span as the key to fetch a translation.

// This part doesn't have to change
<select class="hiding" name="attr_class" style="width: 64px;">
	<option value="Barbarian" data-i18n="barbarian">Bárbaro</option>
	<option value="Bard" data-i18n="bard">Bardo</option>
	<option value="Cleric" data-i18n="cleric">Clerigo</option>
	<option value="Druid" data-i18n="druid">Druida</option>
</select>
<span data-i18n="class-options">Class Options</span> (<span name="attr_class" data-i18n-dynamic></span>)
{ "Barbarian":"Bárbaro", "Bard":"Bardo", "Cleric":"Clerigo", "Druid":"Druida" }

Now, with the above code, when you select "Bárbaro" from the select box, "Barbarian" will be saved under attr_class. But when the sheet refreshes and the span with name="attr_class" would normally be filled in with "Barbarian", instead "Barbarian" will be used as the key to reference the translation JSON and display "Bárbaro" instead.


Roll Queries

Text in roll queries cannot be translated in the character sheet HTML, however, using Sheet Worker Scripts and attributes, the text can be translated in JavaScript and substituted into the roll query.

<script type="text/worker">

on("sheet:opened", function(eventInfo){
    
    setAttrs({
        bonusmacro: getTranslationByKey("ask-bonus")
    });     
    
});

</script>
<span style="display: none" data-i18n="ask-bonus">What is your bonus?</span> <button type='roll' value='[[d20 + ?{@{bonusmacro}|0}]]' ></button>

The <span> tag will not appear, but it will cause the key "ask-bonus" to be included in the generated translation file. The script will create an attribute called "bonusmacro" with the value "What is your bonus?", and the roll button will use that value to send the command, "[[d20 + ?{What is your bonus?|0}]]". If the sheet is opened while another translation file is active, it will set the attribute to the new translated value for "bonusmacro". This technique can be used to substitute individual keys, or a single attribute can be used to store a complex query, with the Sheet Worker Script inserting translated keys into the query code as needed.

<script type="text/worker">
on("sheet:opened", function(eventInfo){
    
    setAttrs({
        rollquery: "?{" + getTranslationByKey("advantageordisadvantage") +"|" + getTranslationByKey("neither") + ",d20|" + getTranslationByKey("advantage") + ",2d20kh1[" + getTranslationByKey("withadvantage") + "]|" + getTranslationByKey("disadvantage") + ",2d20kl1[" + getTranslationByKey("withdisadvantage") + "]}",
    });
    
});

</script>
<span style="display: none" data-i18n="advantageordisadvantage">Are you rolling with Advantage or Disadvantage?</span> 
<span style="display: none" data-i18n="neither">Neither</span> 
<span style="display: none" data-i18n="advantage">Advantage</span> 
<span style="display: none" data-i18n="withadvantage">with Advantage</span> 
<span style="display: none" data-i18n="disadvantage">Disadvantage</span> 
<span style="display: none" data-i18n="withdisadvantage">with Disadvantage</span> 
<button type='roll' value='[[@{rollquery}]]' ></button>

This example will create the following roll using the default translation file

[[?{Are you rolling with Advantage or Disadvantage?|Neither,d20|Advantage,2d20kh1[with Advantage]|Disadvantage,2d20kl1[with Disadvantage]}]]

 


Roll Templates

Static HTML inside of the sheet's roll template definition can use all of the above tools, including Dynamic Keys. If it is in the sheet's rolltemplate definition, code like {{strength-key}} will give the translated value of whatever is the value of {{strength-key}}. If you would like to send dynamically translated values inside of a roll function, use the tool below.

Inside of a roll function, using a roll template, you can wrap any text in "^{ [key] }" to denote the text as a translation key. This text will be replaced with the translated version associated with that key. This step happens after character sheet values are swapped in, so "^{ @{ability_key} }" where ability_key = STRENGTH will be parsed as "^{STRENGTH}" and will display whatever translation value you have for "STRENGTH". This also works on the key, for the key/value pairs used in the allprops() function. For example: "{{^{TEXT}=@{content} }}" will have the translation value for "TEXT" as "{{key}}" inside of the allprops() function.


List Ordering

If you have a list of items in a container, that in English is alphabetically ordered, and you want to be able to change the order in other languages you can use this tool. The re-ordering will respect all current formatting in the list, so even if your list spans several sub-containers, everything should remain formatted the same way.

Identify the container. All list items must be inside of the container, or they will be ignored:

data-i18n-list="list-key"

Identify the individual items in the list. These items can be a block of HTML. When they are reordered, the whole block will be moved.

data-i18n-list-item="item-key"

(Optional) The "starting" order of the list is assumed to be read top-down in the HTML. If your order is different, you can manually number the items (starting at 1) to set the order.

data-i18n-list-item-num="1"

(Special) This is not a tag that you will "use", it is a tag that will be created if there is an error with your formatting. If there is an error with your List Order code, the list will be ignored and this tag will be created on the list container element with the error. If you are not seeing the list be reordered, like you expect, check the parent element for an error.

data-i18n-error="Error Message"

Add the key to your translation file. This will contain a comma-delimited list of all of the item keys, in the language of the original translation. Reordering these keys will reorder the items. The list key can be used for several lists, if all of the items being sorted are the "same" items. The HTML doesn't have to be the same, only the words that are being sorted. For example, a list of "Skills" in D&D may show up in several places on a sheet with totally different HTML but they can all use the same list key. I will use the example code below so generate my list key.

"skills-list":"acrobatics,arcana,sleight-of-hand,survival"

To reorder this list for the Czech language, the list key-value would look like:

 "skills-list":"acrobatics,sleight-of-hand,arcana,survival"

Example:

<div data-i18n-list="skills-list"> // Label your list container and point it to the key it will use to set the order
    <div> // notice that the items are in a sub-container, these columns will still exist after the reordering happens
        <div data-i18n-list-item="acrobatics"> // Label your list items, these are the blocks that will be reordered
            <span data-i18n="acrobatics-u">ACROBATICS</span>
            <input type="text" name="attr_npc_acrobatics" placeholder="0">
        </div>
        <div data-i18n-list-item="arcana">
            <span data-i18n="arcana-u">ARCANA</span>
            <input type="text" name="attr_npc_arcana" placeholder="0">
        </div>
    </div>
    <div>
        <div data-i18n-list-item="sleight-of-hand">
            <span data-i18n="sleight-of-hand-u">SLEIGHT OF HAND</span>
            <input type="text" name="attr_npc_sleight_of_hand" placeholder="0">
        </div>
        <div data-i18n-list-item="survival">
            <span data-i18n="survival-u">SURVIVAL</span>
            <input type="text" name="attr_npc_survival" placeholder="0">
        </div>
    </div>
</div>

The same example only, rather than having the list go down the columns then across, we're going to have the list go across the columns then down:

<div data-i18n-list="skills-list">
    <div>
        <div data-i18n-list-item="acrobatics" data-i18n-list-item-num="1">
            <span data-i18n="acrobatics-u">ACROBATICS</span>
            <input type="text" name="attr_npc_acrobatics" placeholder="0">
        </div>
        <div data-i18n-list-item="sleight-of-hand" data-i18n-list-item-num="3">
            <span data-i18n="sleight-of-hand-u">SLEIGHT OF HAND</span>
            <input type="text" name="attr_npc_sleight_of_hand" placeholder="0">
        </div>
    </div>
    <div>
        <div data-i18n-list-item="arcana" data-i18n-list-item-num="2">
            <span data-i18n="arcana-u">ARCANA</span>
            <input type="text" name="attr_npc_arcana" placeholder="0">
        </div>
        <div data-i18n-list-item="survival" data-i18n-list-item-num="4">
            <span data-i18n="survival-u">SURVIVAL</span>
            <input type="text" name="attr_npc_survival" placeholder="0">
        </div>
    </div>
</div>

Step Two, Generating the Translation File

Once you have formatted your sheet, following the instructions above, you can very easily create your translation file. After loading your sheet within the editor, go into the game, and there open the Developer Tools for your browser and go to the console. At the very bottom of the console, where you can enter code, type "i18nOutput". (it will most likely start to auto-complete once you start typing) Hit enter to retrieve this object, and it will dump a JSON string containing all of the keys you have used with the corresponding text from within the element/placeholder. You should be able to then copy this text into your favorite JSON format (optional, but recommended) and then into the "Translation" tab in the Game Settings Custom Sheet area.

If you wish to create the translation.json manually, you can use the following format:

{
    "key": "string",
    "key-2": "Another string."
}

Once you have your formatted sheet and the translation JSON saved for your custom sheet, it should look exactly like it did before you translated everything, assuming everything was done correctly.

If you see any red text like [translation-key] this is a key that you are telling the system to use, but have not entered into the translation JSON. If you use the JSON generated by the system, you should not see any of these.

Once you have a working translation file, you can upload this with the rest of your sheet code to the root of your character sheet folder as "translation.json". This will automatically be picked up by our system and uploaded to Crowdin, where it can then be translated.


Changing your Translation

If you wish to change your translation after it has already been generated, you can edit your translation.json file, and the changes will be propagated to the language files the next time translations are updated. However, certain changes will not cause translation values to be overwritten, even if the values haven't been translated yet. In other words, sometimes changes to the translation.json will not show up in your sheet.

Changes that will not trigger an update include changes where the only difference is white space or special characters and changes that only remove characters and do not change or add new characters.


Submitting your changes to Roll20

See Beginner's Guide to GitHub


See Also

 
Was this article helpful?
11 out of 24 found this helpful