Character Vault
Any Concept / Any System
Your System Come To Life
Roll20 for Android
Streamlined for your Tablet
Roll20 for iPad
Streamlined for your Tablet

Personal tools

Character Sheet Translation

From Roll20 Wiki

(Redirected from I18n)
Jump to: navigation, search

Character Sheet Translation (previously known as Character Sheet i18n), 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, French for example, and someone using your sheet has their account language set to French, your sheet will show up as the French translation of your sheet for that user.

Andreas J. has created a tool to partially automate the insertion of sheet translations code into an existing sheet. See #ACSI for more details.



See Character Sheet Development/Bugs & Quirks for known issues related to translations.

Designing Your Sheet

Setting up your sheet to be able to respond to the i18n service is fairly simple for most parts, 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.

See Changing Language-Specific Looks for examples.


List of the data-i18n global attributes available:

Attribute Use Example
data-i18n Replace the text inside an element <span data-i18n="strength">Strength</span>
data-i18n-title Replace an element's title-attribute <span data-i18n="constitution" title="Con determine how much health you have." data-i18n-title="constitution-title">Constitution</span>
data-i18n-placeholder placeholder-attribute <textarea name="attr_inventory" placeholder="backpack, 2 torches, dagger ..." data-i18n-placeholder="inventory-placeholder"></textarea>
data-i18n-alt Image description text
data-i18n-vars #Variable Replacement
data-i18n-dynamic #Dynamic Key Replacement
data-i18n-list #List Ordering


More details: Sheet.json#translation

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). If you want further examples of how the i18n translations work, check out the D&D 5E by Roll20's sheet.json sheet which has already been fitted with the i18n translations keys.

Some of the advanced translation-methods uses sheetworkers.

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 class="row">
        <span data-i18n="animal-handling-u">ANIMAL HANDLING</span>
        <input class="num" type="text" name="attr_npc_animal_handling" placeholder="0">
    <div class="row">
        <span data-i18n="arcana-u">ARCANA</span>
        <input class="num" type="text" name="attr_npc_arcana" placeholder="0">
    <div class="row">
        <span data-i18n="athletics-u">ATHLETICS</span>
        <input class="num" type="text" name="attr_npc_athletics" placeholder="0">
    <div class="row">
        <span data-i18n="deception-u">DECEPTION</span>
        <input class="num" type="text" name="attr_npc_deception" placeholder="0">
    <div class="row">
        <span data-i18n="history-u">HISTORY</span>
        <input class="num" type="text" name="attr_npc_history" placeholder="0">

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 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">

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.


If your sheet is adapted to use CSE, you can have translations for your datalist options.

<input type="text" list="lister" name="attr_list_get" value="">
<datalist id="lister">
    <option data-i18n="test">Test</option>
    <option data-i18n="test2">Test2</option>
    "test": "tester",
    "test2": "test2ada"

There seems to be an issue with translation of datalists as the value of the option is entered as the value of the input which links to the list. The ARC sheet has a workaround with a sheet worker that replaces the value with the translated text.

  on("change:repeating_inventory:name", (eventInfo) => {
    const id = eventInfo.sourceAttribute.split("_")[2];
    const updateAttrs = {};
    const name = eventInfo.newValue.trim();
    let translation = getTranslationByKey(name)
    if (translation) {
      updateAttrs[`repeating_inventory_${id}_name`] = translation;
      const searchInputs = _.difference(GLOBAL__INVENTORY_INPUTS, ["name"]);
      _.each(searchInputs, (key) => {
        const attr = `repeating_inventory_${id}_${key}`;
        const i18n = `${name}_${key}`;
        // check if property exists in global
        if (GLOBAL__INVENTORY[name][key]) {
          updateAttrs[attr] = GLOBAL__INVENTORY[name][key];
        // check if translation exists
        else if (getTranslationByKey(i18n)) {
          updateAttrs[attr] = getTranslationByKey(i18n);
      setAttrs(updateAttrs, { silent: true });

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

Tricky scenarios

It can become tricky in some scenarios, with elements breaking if you are using the wrong i18n attribute. For instance a select element could look like this (extract from the Fading Suns v2.75 sheet):

    <select class="fs-table-cell" name="attr_power_characteristic">
        <option value="" selected></option>
        <optgroup label="Body" data-i18n-label="body">
            <option value="strength" data-i18n="strength">Strength</option>
            <option value="dexterity" data-i18n="dexterity">Dexterity</option>
            <option value="endurance" data-i18n="endurance">Endurance</option>
        <optgroup label="Mind" data-i18n-label="mind">
            <option value="wits" data-i18n="wits">Wits</option>
            <option value="perception" data-i18n="perception">Perception</option>
            <option value="tech" data-i18n="tech">Tech</option>
        <optgroup label="Spirit" data-i18n-label="spirit">
            <option value="presence" data-i18n="presence">Presence</option>
            <option value="will" data-i18n="will">Will</option>
            <option value="faith" data-i18n="faith">Faith</option>

Notice how optgroup is using data-i18n-label. If you were to use a data-i18n like you are for option, then the select element would not display properly (it would show the optgroup's but not the option's).

Variable Replacement

If you need to have some text translated and other pieces of the text not translated, you can use the data-i18n-vars attribute to define text that should not be translated. Separate each bit of text that should not be translated with a vertical bar (i.e. |). This makes an array of text to be inserted untranslated. You reference this text in the translation.json using {{[index]}} to reference the specific row of the array. Remember that arrays are indexed from 0. This looks like:


<span data-i18n="translated text" data-i18n-vars=", but this isn't|neither is this">This is translated{{0}},and{{1}}</span>


	"translated text":"This is translated{{0}},and{{1}}"

All pieces of text that are not going to be translated for a given element are put in the data-i18n-vars attribute, regardless of which data-i18n property they are actually going to be used in. For instance the below code will create an action button with a title property that has the ability reference call (not translated), and help text for interacting with the action button(translated):


<button type='action' name='act_title_demo' data-i18n-title="title demo" data-i18n-vars="%{title_demo} ⦀ " title='{{0}}alt-click to sell dice'>Click Me!</button>


	"title demo":"{{0}}alt-click to sell dice"

Note that if you have translation text that you use for several elements that uses this variable functionality, all the elements must have a data-i18n-vars property. This allows a single translation key to be used across several use cases. For instance, the below code creates two buttons, each with a title referencing the ability call required to call that button, as well as help text about how to sell dice. All with a single translation key.


<button type='action' name='act_purchase_d8' data-i18n-title="title demo" data-i18n-vars="%{purchase_d8} ⦀ " title='{{0}}alt-click to sell dice'>d8</button>
<button type='action' name='act_purchase_d10' data-i18n-title="title demo" data-i18n-vars="%{purchase_d10} ⦀ " title='{{0}}alt-click to sell dice'>d10</button>


	"title demo":"{{0}}alt-click to sell dice"

However, an important limitation of this to keep in mind is that if you forget the data-i18n-vars property on an element, it's translated text will not recognize the variable call correctly, and will simply put the call in the text. As an example, if our sheet had a third button in addition to the two in the code above:


<button type='action' name='act_purchase_d12' data-i18n-title="title demo" title='{{0}}alt-click to sell dice'>d8</button>

This would simply have a title that would show the array reference instead of omitting it.

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>
<span data-i18n="class-options">Class Options</span> (<span name="attr_class" data-i18n-dynamic></span>)

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){
        bonusmacro: getTranslationByKey("ask-bonus")

<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){
        rollquery: "?{" + getTranslationByKey("advantageordisadvantage") +"|" + getTranslationByKey("neither") + ",d20|" + getTranslationByKey("advantage") + ",2d20kh1[" + getTranslationByKey("withadvantage") + "]|" + getTranslationByKey("disadvantage") + ",2d20kl1[" + getTranslationByKey("withdisadvantage") + "]}",

<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]}]]


A synchronous function which immediately returns the translation string related to the given key. If no key exists, false will be returned and a message in the console will be thrown which list the specific key that was not found in the translation JSON.

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.

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:


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.


(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.


(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.


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



<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 data-i18n-list-item="arcana">
            <span data-i18n="arcana-u">ARCANA</span>
            <input type="text" name="attr_npc_arcana" placeholder="0">
        <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 data-i18n-list-item="survival">
            <span data-i18n="survival-u">SURVIVAL</span>
            <input type="text" name="attr_npc_survival" placeholder="0">

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 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 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 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 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">

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 console.log(i18nOutput) (or just 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 formatter (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
, it means a key you have defined in the HTML, 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.

Changing Language-Specific Looks

Sometimes, sections of a sheet is coded in a way that makes sense for the original language, but may cause troubles in other languages if the translated text for the section is considerably longer or shorter than the default. In those cases, you can create CSS rules to make some parts of the sheet look/behave differently when a specific language is used with the sheet.

A class to the parent .charsheet-element to let you have separate css for individual languages, in the form of lang-[2 char language code], so if the language is English, you use .lang-en; with French, use .lang-fr, with Swedish, use .lang-sv. 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.

Example: Change width of section

A radio "button"(the area you click on) and the "button"'s width is designed to fit the word "Core", but doesn't work well if the word is longer, in which case the two need to be made wider and adjusted to the width of the word for the given language. D&D 5E by Roll20 Legacy code

<div class="container pc">
    '''<input class="tab-button core" name="attr_tab" type="radio" value="core" checked="checked"/><span data-i18n="core-u">CORE</span>'''
    <input class="tab-button bio" name="attr_tab" type="radio" value="bio"/><span data-i18n="bio-u">BIO</span>
    <input class="tab-button spells" name="attr_tab" type="radio" value="spells"/><span data-i18n="spells-u">SPELLS</span>
    <input class="tab-button options" name="attr_tab" type="radio" value="options"/><span style="font-family: pictos">y</span>
    <div class="page core">
        <div class="header">
            /* stuff in header section */
        /* stuff on page core */
    /* rest of the Player character sheet */
/* The original width of the "button" displaying the word "Core" */
input[type=radio].sheet-tab-button.sheet-core + span {
  right: 116px;
  width: 45px; }

/* The shifted position and increased width for the same "button", making the longer   */
.lang-es input[type=radio].sheet-tab-button.sheet-core,
.lang-es input[type=radio].sheet-tab-button.sheet-core + span {
  right: 137px;
  width: 76px; }

(The "bio" and "spells" buttons ahve similar adjustments)


This can even be used to change what images are show on a sheet. One example could be to replace the default game's logo with an image that shows the translated version of the logo, if such exists. The Forbidden Lands sheet by Vince is a sheet that have made it so that the sheet will show the Swedish logo/name (Svärdet's Sång) if you have your account set to Swedish.

Forbidden Lands Example

The Logo at the top of the sheet will display the image spelling "Forbidden Lands" by default, and if an account is set to Swedish, it will display the image spelling "Svärdets Sång"(the Swedish name of the game).

<div class="logo" title="FORBIDDEN LANDS" data-i18n-title="logo-forbidden-lands">
  <span class="sheet-logo-image lang-sv"></span>
/* defines the default behaviour */
span.sheet-logo-image {
  background-image: url(;
  background-size: contain;
  background-repeat: no-repeat;
  width: 220px;
  height: 70px;  
  margin: -15px 0 0 0;
  display: inline-block;
/* defines the changes to the section if the user account is set to Swedish */
.charsheet span.logo-image.lang-sv {
  background-image: url(;


ACSI - Automated Character Sheet Internationalizer A Linux command line tool that can save a bunch of time by automatically adding the sections to the code and creating the translation.json-file. Created by Andreas J.

If a HTML-file contains no translation tags, is well formatted, and doesn't have Roll Templates or Sheetworkers(script will make nonsensical changes to these sections if not removed), ACSI should be able to do a good job of creating a new version of the file that have the i18n-keys added. It's not perfect, so it will usually give a result that need to be cleaned up a bit, and on some sheets might not give a useful result at all.

It's suggested you save a version of the sheet that have the roll templates & sheetworkers removed and use that file with ACSI, and then add back them to the file afterwards to be able to inspect the result. ACSI can also generate the translation.json-file, which is a bit easier than having to create/copy it from developer web tools as suggested in the previous sections.

If you don't have the option to run the script yourself, you can contact Andreas J. and he can test how well the script does with your sheet and give you the result.

Submitting your changes to Roll20

Main Page: Beginner's Guide to GitHub

Roll20 prefers updates to sheets that already have translations enabled should go through CrowdIn, but if the i18n keys aren't in the sheet code, you need to submit your updated version to GitHub.

Alternative Guide: Short Git Guide

See Also