This is a documentation for Board Game Arena: play board games online !

Translations: Difference between revisions

From Board Game Arena
Jump to navigation Jump to search
 
(69 intermediate revisions by 17 users not shown)
Line 1: Line 1:
{{Studio_Framework_Navigation}}


Using BGA Studio, the game you create is ready to be translated to each language by the BGA community. To make this possible, you only need to specify which string must be translated and how to combine them.
Using BGA Studio, the game you create is ready to be translated to each language by the BGA community. To make this possible, you only need to specify which string must be translated and how to combine them.


== How translation works? ==
 
__TOC__
 
== Localization Overview ==
 
Localization for BGA games happens largely on the CLIENT. Games must be developed in English and English strings are sent to the client.
 
It's only at the client that your English strings will be displayed with the translation for the user's language, when such a translation exists.
 
For anyone who has worked on a system where translation happens at the server and localized strings are sent to the client: you must unlearn what you have learned. Just stick with English and send the untranslated English strings. The magic happens at the client.
 
 
== How will my strings be translated? ==


When developing your game, all strings must be in English. Strings must be coherent with the English version of the game.
When developing your game, all strings must be in English. Strings must be coherent with the English version of the game.


Before the release of the game, BGA team will do the French translation of the game.
Once your game enters Beta, the BGA player community will descend upon your game like a plague of locusts and translate it into dozens of languages before you can say "Je ne parle pas anglais".
 
 
== What do I have to do from a programming standpoint? ==
 
Not as much as you think. Read on for full details, but the golden rules are:
 
 
 
# Make sure that any string on the server side that needs to be translated on the client side is wrapped in a clienttranslate() function when defined. For example: $my_response = clienttranslate('Hey translators, please translate this string'). clienttranslate doesn't actually do anything at all (it just passes the text through) but the translation engine searches your code for strings wrapped in that function and uses them to build the list of strings that need to be translated.
# On the client side, just display your text, but wrap it in _().
## If you pass a string constant to _() - such as _('This is my string') then the parser will automatically detect and add your string to the list of strings that need to be translated, AND it will display the local translation for it when displaying.
## If you pass a variable to _() - such as _(args.my_message) then MAKE SURE that the variable is set to an English string that is either defined somewhere in your PHP code in a clienttranslate() function or somewhere else in your javascript code in a _() function. As long as it is, the translators will provide translations for it, and the _() function will display the local translation of your English string.
# A string used as the message for notifyAllPlayers or notifyPlayer will automatically have its translation displayed on the client - you don't need to do anything EXCEPT make sure that it is wrapped in a clienttranslate() on the server, so that it gets translated.
# A string used as the "description" or "descriptionmyturn" for a state in your state machine will also be automatically translated on the client - you don't need to do anything EXCEPT make sure that it is wrapped in a clienttranslate() on the server, so that it gets translated.
# A string used in the pre-games option screen will also automatically have any translation displayed at the client - you don't need to do anything EXCEPT make sure that it is wrapped in a totranslate() on the server (nota bene: you should not use clienttranslate() here as this file is processed specifically and those strings will not be included in the game translation file but in the main site translation file - with a prefix to keep translations isolated by module - as they have to be translated on main site pages. If you need exactly the same string in your game for a client side translation, it should be declared somewhere else in your code wrapped inside a clienttranslate() to be included in the game translation file.
# A string used in the statistics screen will operate like a string in the pre-game options screen. It will automatically have any translation displayed at the client as long as you make sure that it is wrapped in a totranslate() on the server.
# If you are using parameters for a call to notifyAllPlayers or notifyPlayers, or for a state's "description" or "descriptionmyturn" and those parameters contain text that needs to be translated, you will need to tell the system to display the translated versions of them by using the 'i18n' argument. See below for full details.
 
== What strings should be marked for translation ==


After the release of the game, the BGA players community will translate the game in every language.
YES:
* Every text that can be visible by the player when the game is running normally. This includes tooltips, texts on cards, error messages.


== What should be translated? ==
NO:
* Error messages that are not supposed to happen (unexpected errors).
* Player names (i.e. do not put player_name or any php key send via notification which is essentially a player name, such as 'other_player' in 'i18n' array)
* Proper names of characters or places in some cases (consult the publisher)
* Numbers (i.e  _("42"))
* Keywords used internally


Every text that can be visible by the player when the game is running normally. This includes tooltips, texts on cards, error messages, ...
== What rules should I follow for the original English strings? ==
 
For a coherent and homogeneous interface, here are some rules about ending a sentence with a final period '.'
 
* As a general rule:
** If a sentence is displayed isolated in the interface => no final period
** If a sentence is followed or could be followed by another sentence in the same interface space => final period.
 
* In detail:
** No final period:
*** button labels
*** section titles
*** menu elements
*** links triggering an isolated action
*** anything that is not a full sentence
*** current actions in the status bar
** Final period:
*** complete sentences (e.g., explanations and descriptions) that can be chained with other sentences
** Either a period or no period is acceptable (but this should be consistent throughout the game) for:
*** isolated tooltips / small sentences
*** game logs (no period is usually preferable)
*** error messages (unless there is more than one sentence in the error message; final period is mandatory in this case)
 
Otherwise, you should try to follow as closely as possible the general style and format (including capitalization) used in the published English rulebook and game materials.


This does NOT include error messages that are not supposed to happened (unexpected errors).


== Focus on translating notifications ==
== Focus on translating notifications ==
Line 22: Line 83:
However, there is one difference on BGA: notifications. The server is sending notifications to players, and most of the time the notifications are the same for every players, no matter what language each player is using. This is why notifications are translated on client side in the proper language, even if the strings are defined on server side.
However, there is one difference on BGA: notifications. The server is sending notifications to players, and most of the time the notifications are the same for every players, no matter what language each player is using. This is why notifications are translated on client side in the proper language, even if the strings are defined on server side.


== WARNING: how to make sure your strings will be translated ==
== How to not make translators crazy ;) ==


For each game, our translation tool is doing a full scan of the code, looking for translator markers like "_()" or "clientranslate()"... (see below the list of translation markers).
* Try to reuse the exact same strings (with the same case, etc.) to minimize the number of strings to translate.
** '''Example''': Consider replacing <pre>self::_("Winner")</pre> and <pre>self::_("Winners")</pre> (two strings to translate) with <pre>self::_("Winner(s)")</pre>
** '''Example 2''': <pre>clienttranslate("play a card")</pre> and <pre>clienttranslate("Play a card")</pre> means there will be two strings to translate.
* Do not mark as translatable a game element that does not have to be translated (ex: if the name of a monster on a card is "Zzzzz", maybe there's no need to translate it).
* Words does not come in the same order in each language. Thus, when you have to translate a string with an argument, do not write something like:
<pre>self::_("First part of the string, ").$argument.' '.self::_("second part of the string")</pre>
Write instead:
<pre>sprintf( self::_("First part of the string, %s second part of the string"), $argument )</pre>
(or the equivalent "dojo.string.substitute" in Javascript)
* This also applies to punctuation marks. I.e. this is not good: _("Pick")+":"
* When translators are going to translate your game, the most difficult task for them is to get the context of the string to be translated. The shorter the string is, the more difficult the task is for them. As a rule of thumb, try to avoid short, insignificant strings that require knowledge of their surrounding context. You can also leave a comment on the context of the string in the translation program (English to English) if you are the developer of the game.
* Use same sentence for plural vs singular. We prefer to write "player gets 1 coin(s)" (or "coin/s") rather than write two versions of the same string for plural and singular - it reduces the number of strings to translate. Also using the icon to represent coin in logs may solve this issue.
* Instead of writing elaborate strings like "With the effect of ZZZ, player XXX gains a new YYY", which is very difficult to translate, write strings like "ZZZ: XXX gets YYY".
* Use present tense instead of past. E.g., "player gets wood" instead of "player got wood".
* Avoid using gender-specific wording as much as possible.
* Also avoid "their". There is [[https://forum.boardgamearena.com/viewtopic.php?f=11&t=19432&start=10#p88592 a pronoun replacement system]] is in place, and cannot work if your use "their" as a singular genderless pronoun, as it removes the information that it's singular. E.g., you should write "playerX returns card to *his* hand" or "playerX returns card to *his/her* hand" and not "playerX returns card to *their* hand" as in the 2 first case the system will substitute the proper pronoun his/her/their depending on playerX declared/undeclared gender, but in the second case it will always stay "their" whatever the player preference setting. In some case its actually needed, for example if you say "King sends his troops" in the log and King is not a player name, but character in the game - it will be changed to "her" if a player is female, which is not correct, in this case their would be appropriate (or just "the troops")
* Where two strings are identical apart from (say) a number, use the same string with a ${parameter}, and call format_string_recursive (on the client) or use args (for notifications and state descriptions) to provide the details. But *never* do that for composing multiple sentences with words: other languages have different grammar and it would most likely create untranslatable strings.


If your original string is not "physically" inside one of this marker, it won't be translated.
== WARNING: Make sure your strings will be translated! ==
 
For each game, our translation tool does a full scan of the code, looking for translation markers like "_()" or "clienttranslate()". (See below for the full list of translation markers.)
 
If your original string is not completely contained inside one of these markers, it won't be translated.


<pre>
<pre>
Line 32: Line 113:
     var mystring_translated = _("my string");      // JS
     var mystring_translated = _("my string");      // JS
     $mystring_translated = self::_("my string");    // PHP
     $mystring_translated = self::_("my string");    // PHP
     $mystring_translated = sprintf( _("my string with an %s argument"), $argument );  // PHP
     $mystring_translated = sprintf( self::_("my string with an %s argument"), $argument );  // PHP


     // Examples: the following strings WILL NOT be translated:
     // Examples: the following strings WILL NOT be translated:
     $my_string = "my string";
     $my_string = "my string";
     $not_translated = self::_( $my_string );  // The original string is not bordered by a translator marker => no translation
     $not_translated = self::_( $my_string );  // The original string is not contained within a translator marker => no translation
     $not_translated = self::_( sprintf( "my string with a %s argument", $argument ) ); // Same thing
     $not_translated = self::_( sprintf( "my string with a %s argument", $argument ) ); // Ditto
</pre>
</pre>
== How to not make translators crazy ;) ==
* When you need the same string twice, try to reuse exactly the same string (with the same case) to minimize the number of strings.
* Do not mark as translatable a game element that does not have to be translated (ex: if the name of a monster on a card is "Zzzzz", maybe there's no need to translate it).
* Words does not come in the same order in each language. Thus, when you have to translate a string with an argument, do not write something like:
<pre>_("First part of the string, ").$argument.' '._("second part of the string")</pre>
Write instead:
<pre>sprintf( _("First part of the string, %s second part of the string"), $argument )</pre>
(or the equivalent "dojo.string.substitute" in Javascript)
* When translators are going to translate your game, the most difficult task for them is to get the context of the string to be translated. The more the string is a short insignificant string, the more difficult is the task for them. As a rule of thumb, try to avoid insignificant short strings.
* The BGA translation policy is to be flexible on grammar... We prefer to write "player gets 1 coin(s)" than write two versions of the same string for plural and singular - it reduces the number of strings to translate.


== On client side (Javascript) ==
== On client side (Javascript) ==
Line 62: Line 131:


// Get a string in player's language with parameter:
// Get a string in player's language with parameter:
var translated = dojo.string.substitute( "You can pick ${p} cards and discard ${d}", {
var translated = dojo.string.substitute( _("You can pick ${p} cards and discard ${d}"), {
     p: 2,
     p: 2,
     d: 4
     d: 4
Line 68: Line 137:
</pre>
</pre>


Note: what is also possible to do is  
'''WARNING:''' in Javascript strings to translate, you should never use '\n', '\t' or such, as it will break the translation bundle and result in all the Javascript translation to fail. In any case, the strings will result in HTML code, and such character codes won't have any impact on the HTML rendering. You should use HTML markup instead.
 
'''ANOTHER WARNING:''' you cannot use this function _() in the javascript object constructor, but you can achieve the same if you use it the setup method. If you do that you will see this error in browser debug console:
  Try to use a translated string in JS object declaration : impossible => string is NOT translated
 
=== Formatting on client side ===
 
Sometimes you will want to apply some formatting on the client side, but don't want to put HTML tags within the source string (which may be confusing for translators).
 
This can be achieved with the <code>bga_format</code> function and Markdown-style syntax.
 
<pre>
/**
* @function bga_format
* Replaces delimiters with HTML tags in translated text
* @param {string} translated string
* @param {Object} map of replacements to make, should be of the form
*    {
*        '*': (t) => t,
*        '_': 'classname',
*    }
*    keys are delimiters within the translated string
*    values are either strings (classname of surrounding span) or function (flexible replacements)
* @returns {string} Returns the original string, with replacements made.
*/
</pre>
 
Example:
<pre>
// Get a string in player's language:
bga_format(_('You receive a *special* _tile_'), {
    '*': (t) => '<b>' + t + '</b>',
    '_': 'tile-name'
});
</pre>
 
The above will return something along the lines of:
<pre>
You receive a <b>special</b> <span class="tile-name">tile</span>
</pre>
 
Or, if the user is playing in Spanish, for example:
<pre>
Recibes una <span class="tile-name">loseta</span> <b>especial</b>
</pre>
 
Notes:
* The text passed to <code>bga_format</code> must still be translated using <code>_()</code>, don't forget it!
* Good characters to demarcate formatting are: <code>['*', '_']</code> (other single characters will work, just make sure it will be clear to a translator)
* If the replacements object is a string, the inner text will be placed within a <code>&lt;span&gt;</code> tag with the string as a class
* If the replacements object is a function, the inner text will be passed as an argument to the function, and the return value will replace the text and the delimiters


== On server side (PHP) ==
== On server side (PHP) ==
Line 74: Line 193:
On PHP side, you can use 3 different functions to specify that a string must be translated.
On PHP side, you can use 3 different functions to specify that a string must be translated.


'''clienttranslate( "my string to translate" ):'''
=== Function clienttranslate ===
 
'''clienttranslate( 'string to translate' ):'''


This function is '''transparent''': it will return the original English string without any change. It's only purpose is to mark this string as "must be translated", and to make sure the translated version of the string will be available on client side.
This function is ''transparent'': it will return the original English string without any change. Its only purpose is to mark this string as "must be translated", and to make sure the translated version of the string will be available on client side.


In general, you use clienttranslate:
In general, you use clienttranslate:
* On your states.inc.php, for field "description" and "descriptionmyturn".
* In your '''states.inc.php''', for the fields "description" and "descriptionmyturn".


<pre>
<pre>
Line 85: Line 206:
</pre>
</pre>


* On "material.inc.php", when defining texts for game material that must be displayed on client side.
* In '''material.inc.php''', when defining text for game materials that must be displayed on the client side.
 
<pre>
<pre>
$this->card_types = array(
$this->card_types = array(


     1 => array(
     1 => array(
         'name' => clienttranslate("Amulet of Air"), // Thus, we can use "_( card_name )" on Javascript side.
         'name' => clienttranslate('Amulet of Air'), // Thus, we can use "_( card_name )" on Javascript side.
</pre>
</pre>


* When sending a notification with "notifyAllPlayers" or "notifyPlayer", for the game log string and all game log arguments that need a translation.
* When sending a notification with ''notifyAllPlayers'' or ''notifyPlayer'', in the game log string and all game log arguments that need a translation.


<pre>
<pre>
     // A game log string with no argument:
     // A game log string with no argument:
     self::notifyAllPlayers( 'pickLibraryCards', clienttranslate("Everyone draw cards from his library"), array() );
     self::notifyAllPlayers( 'pickLibraryCards', clienttranslate('Everyone draw cards from their library'), [] );
</pre>
</pre>


Translating arguments is a little bit more complex. It is using the "i18n" special argument as below:
As a consequence there is no point passing variables to this function. E.g.:
 
<pre>
    notif="foo";
    self::notifyAllPlayers( 'log', clienttranslate(notif),[]); // BAD
   
    notif=clienttranslate("foo");
    self::notifyAllPlayers( 'log', notif,[]); // GOOD
 
    self::notifyAllPlayers( 'log', clienttranslate("player gains ${num} gems"),[num=>2]); // BAD it is dynamic string enterpreted by php
    self::notifyAllPlayers( 'log', clienttranslate('player gains ${num} gems'),[num=>2]); // GOOD, difference is single quotes
 
    self::notifyAllPlayers( 'log', clienttranslate("player gains ".${num}." gems"),[num=>2]); // BAD concatenation
 
</pre>
 
Translating arguments is a little bit more complex. This uses the '''i18n''' special argument as below:


<pre>
<pre>
Line 106: Line 244:


  self::notifyAllPlayers( 'winPoints', clienttranslate('${card_name}: ${player_name} gains ${points} point(s)'), array(
  self::notifyAllPlayers( 'winPoints', clienttranslate('${card_name}: ${player_name} gains ${points} point(s)'), array(
                 'i18n' => array( 'card_name' ),    // <===== We specify here that "card_name" argument must be transate
                 'i18n' => array( 'card_name' ),    // <===== We specify here that "card_name" argument must be translated
                 'player_id' => $player_id,
                 'player_id' => $player_id,
                 'player_name' => self::getActivePlayerName(),
                 'player_name' => self::getActivePlayerName(),
Line 113: Line 251:
             ) );  
             ) );  
</pre>
</pre>
To ensure the translation of the i18n argument will be made, clienttranslate must have been used somewhere, for instance:
<pre>
$this->card_types = array(
...
    8 => array(
        'name' => clienttranslate("Amulet of Fire"),
...
</pre>
Pay attention when using the '''i18n''' argument when translating arguments for the client: do NOT use same argument for both translations AND key codes for client-side actions (like using 'card_name' to move it on the player board as described in the example). It's pretty obvious in the example, but it can be very tricky when translation is made at the end of the development (which is often the case). Use explicit argument names like 'card_name_translated' by example.
'''WARNING:''' you should NEVER use concatenation with ''clienttranslate'', as it would result in a different string to translate at runtime than the one retrieved statically in the translation system, and so translation would not be applied. If you need to compose a string, use substitution and the i18n parameter (but you also have to pay attention not to compose sentences in a way that is dependent upon the English language specific syntax, or it may be impossible to translate correctly in another language: sometimes, you need multiple full sentences rather than relying on substitution).
=== Method _ (underscore) ===


'''self::_( "my string to translate" ):'''
'''self::_( "my string to translate" ):'''


This function returns a string translated in the language of CURRENT user (ie: player who send the request to the server) (be careful, this is NOT the active player).
This function returns a string translated in the language of CURRENT user (i.e. player who send the request to the server) (be careful, this is NOT the active player).
 
This can be used like self::_('bla') or this->('bla'). You cannot use static underscore function _('bla') in php, it is not the same, it will not translate strings from your game.
 
The returned string will be surrounded by HTML tags, so you cannot use it in places where pure text is required or your string already has another html markup.
 
Most of the time, you don't need to translate strings on server side, except on the following situations:
* Throwing user exceptions
* Creating the labels for the game interface used in your template
* Rarely, in your material.inc.php, if for example you need to use some string elements in your exceptions
* Rarely, in your "getAllDatas" PHP method, as the data return by this method is used only by current user
 
Imporant! This function does two things:
* Mark string for translation
* Translates in runtime
As a consequence you absolutely cannot have $var in there! Or string concatenations.
I.e.
  $food = self::_('Banana'); // good
  $msg = self::_("User gives $food to a cat"); // bad
For the translator we will have two strings "Banana" and "User gives $food to a cat", but in runtime we will be
"Banana" and "User gives Banana to a cat" and second string would not match anything. And this is as bad
  $msg = self::_('User gives ' . $food . ' to a cat');// bad
To solve this, use sprintf() when server-side translations require variables:
  $food = self::_('Banana'); // good
  $msg = sprintf(self::_('User gives %s to a cat'), $food); // good


Most of the time, you don't need to translate strings on server side, except on the following 3 situations:
==== Throwing an exception because the player did a forbidden move ====
* When throwing an exception because the player did a forbidden move.


<pre>
<pre>
// This will display a translatable red message to the player that just do some wrong action:
// This will display a translatable red message to the player that just did some wrong action:
throw new feException( self::_('You must choose 3 cards'), true);
throw new BgaUserException( self::_('You must choose 3 cards') );
</pre>
 
Note that the use of BgaUserException signals that this exception is "expected". In theory, all exception that are expected should be translated.


// ... notice the use of "true" parameter that signal that this exception is "expected". In theory, all exception that are excepted should be translated.
<pre>
// this is the same but with variable
throw new BgaUserException(sprintf(self::_('You must choose %d cards'), $card_num));
</pre>
</pre>


* In "yourgame.view.php", when creating the labels for the game interface used in your template (.tpl) file.
==== Creating labels ====


Simple label in tpl file (i.e. label which has same value independent of anything):
in .tpl:
<pre>
  <div>{CARDS_FOR_YEAR_2}</div>
</pre>
in .view.php:
<pre>
<pre>
$this->tpl['CARDS_FOR_YEAR_2'] = self::_("Your cards for year II");
$this->tpl['CARDS_FOR_YEAR_2'] = self::_("Your cards for year II");
</pre>
</pre>


* Eventually, in your material.inc.php, if for example you need to use some string elements in your exceptions.
'''Nota bene:''' it is recommended to set translated text in your interface client side rather than use template variables and server translation, so that translation works natively in replays (that don't have server side translations). For simple strings, the framework will handle moving the translation client side, but for more complex strings it may not work. In particular, you should not use server side translation for templates with substitution variables (or use the specific function "gameview_str_replace( $search, $replace, $string )" if not possible otherwise or for old code compatibility). Also, you should never use a "to_translate" class in your .tpl as it is used internally by the framework.
 


Label which is generated based of player's name, using server side template:
in .tpl:
<pre>
  <div id="empire_title_{COLOR}" class="side_title color_{COLOR}">{EMPIRE_LABEL}</div>
</pre>
in .view.php:
<pre>
<pre>
// In material.inc.php, $this->energies[n]['nametr'] has been created with the self::_() method. This we can do this:
        $this->page->insert_block("player_board", array ("COLOR" => $color,"PLAYER_NAME" => $name,"PLAYER_NO" => $no,
throw new feException( self::_("To execute this action you need more: ").' '.$this->energies[$resource_id]['nametr'], true );
                "PLAYER_ID" => $player_id, "CLASSES" => $own?"own":"",
                "EMPIRE_LABEL" => self::raw(gameview_str_replace('${player_name}', $name, self::_('${player_name}\'s EMPIRE')))
              ));
</pre>
</pre>


* Eventually, in your "getAllDatas" PHP method, as the data return by this method is used only by current user.
Label which is generated based of player's name, using client side (which is better):
in .tpl:
<pre>
  <div id="empire_title_{COLOR}" class="side_title color_{COLOR}">Stub</div>
</pre>
in .js:
<pre>
  for (var player_id in this.gamedatas.players) {
      var player_color = this.gamedatas.players[player_id].color;
      var player_name = this.gamedatas.players[player_id].name;
      $('empire_title_'+player_color).innerHTML=this.format_string_recursive(_("${player_name}'s EMPIRE"), {player_name: player_name});
  }
</pre>
 
==== Translating strings in material file ====
 
Rarely, in your material.inc.php, if for example you need to use some string elements in your exceptions.
 
<pre>
// In material.inc.php, $this->energies[n]['nametr'] has been created with the self::_() method. Now we can do this:
throw new BgaUserException( self::_("To execute this action you need more: ").' '.$this->energies[$resource_id]['nametr'] );
</pre>
 
Note: you could also do this, so you don't need to pollute your material file
  throw new BgaUserException( self::_("To execute this action you need more: ").' '.self::_($this->energies[$resource_id]['name']) );
 
where $this->energies[$resource_id]['name'] were marked in material file using regular clienttranslate function
<pre>
  $this->energies = [
      22 => [
          'name' => clienttranslate('Sun')
          ...
      ]
  ];
</pre>
 
==== Strings in getAllDatas ====
 
Rarely, in your "getAllDatas" PHP method, as the data return by this method is used only by current user.
 
Note: not sure why would you send strings from the server in this case, as same can be achieved on the client usually.
 
=== Function totranslate ===


'''totranslate( "my string to translate" ):'''
'''totranslate( "my string to translate" ):'''
Line 150: Line 392:
* Statistics name in stats.inc.php
* Statistics name in stats.inc.php
* Option names and option values name in gameoptions.inc.php
* Option names and option values name in gameoptions.inc.php
== Top Secret Undocumented Features ==
If your string contains the clause '$${value}' (such as 'You gain $${value}.') then the translation system seems to move the position of the $ to a localized location (eg $5 in English but 5$ in French). This only seems to occur when using the argument 'value'.
In JavaScript, you can get the current user's language by retrieving the translation for the special string "$locale":
    var lang = _('$locale'); // en, fr, etc.
== On server side - advanced (PHP) ==
If you want to use translation system in your custom static modules, you will first need to expose the _() function :
<pre>
class mygame extends Table {
  // Exposing protected method translation
  public static function totranslate($text) {
    return self::_($text);
  }
}
</pre>
Then you'll be able to use this directly in other modules by doing
<pre>
throw new BgaUserException(mygame::totranslate("Translated error from my awesome module file"));
</pre>
Notice how the "totranslate" is also used by the static analysis to detect your string. So the following will not work :
<pre>
$msg = "Translated error from my awesome module file";
throw new BgaUserException(mygame::totranslate($msg));
</pre>
== Preview another language ==
Once a game is released, you can view a table as a specific language by appending a language code to the URL, such as <pre>&lang=es</pre>
== Analyze your project ==
It looks super complex but there is help!
On studio control panel you can find a button "Check project" - it runs bunch of static analysis on your project including scanning for bad translations and missing translations.
It also will extact all you translation keys as translator will see them so you can double check - and run spell checker on them.
== Other Useful Links ==
* BLOG post about translations https://bga-devs.github.io/blog/posts/translations-summary/
[[Category:Studio]]

Latest revision as of 13:53, 19 October 2024


Game File Reference



Useful Components

Official

  • Deck: a PHP component to manage cards (deck, hands, picking cards, moving cards, shuffle deck, ...).
  • Draggable: a JS component to manage drag'n'drop actions.
  • Counter: a JS component to manage a counter that can increase/decrease (ex: player's score).
  • ExpandableSection: a JS component to manage a rectangular block of HTML than can be displayed/hidden.
  • Scrollmap: a JS component to manage a scrollable game area (useful when the game area can be infinite. Examples: Saboteur or Takenoko games).
  • Stock: a JS component to manage and display a set of game elements displayed at a position.
  • Zone: a JS component to manage a zone of the board where several game elements can come and leave, but should be well displayed together (See for example: token's places at Can't Stop).

Undocumented component (if somebody knows please help with docs)

  • Wrapper: a JS component to wrap a <div> element around its child, even if these elements are absolute positioned.

Unofficial



Game Development Process



Guides for Common Topics



Miscellaneous Resources


Using BGA Studio, the game you create is ready to be translated to each language by the BGA community. To make this possible, you only need to specify which string must be translated and how to combine them.


Localization Overview

Localization for BGA games happens largely on the CLIENT. Games must be developed in English and English strings are sent to the client.

It's only at the client that your English strings will be displayed with the translation for the user's language, when such a translation exists.

For anyone who has worked on a system where translation happens at the server and localized strings are sent to the client: you must unlearn what you have learned. Just stick with English and send the untranslated English strings. The magic happens at the client.


How will my strings be translated?

When developing your game, all strings must be in English. Strings must be coherent with the English version of the game.

Once your game enters Beta, the BGA player community will descend upon your game like a plague of locusts and translate it into dozens of languages before you can say "Je ne parle pas anglais".


What do I have to do from a programming standpoint?

Not as much as you think. Read on for full details, but the golden rules are:


  1. Make sure that any string on the server side that needs to be translated on the client side is wrapped in a clienttranslate() function when defined. For example: $my_response = clienttranslate('Hey translators, please translate this string'). clienttranslate doesn't actually do anything at all (it just passes the text through) but the translation engine searches your code for strings wrapped in that function and uses them to build the list of strings that need to be translated.
  2. On the client side, just display your text, but wrap it in _().
    1. If you pass a string constant to _() - such as _('This is my string') then the parser will automatically detect and add your string to the list of strings that need to be translated, AND it will display the local translation for it when displaying.
    2. If you pass a variable to _() - such as _(args.my_message) then MAKE SURE that the variable is set to an English string that is either defined somewhere in your PHP code in a clienttranslate() function or somewhere else in your javascript code in a _() function. As long as it is, the translators will provide translations for it, and the _() function will display the local translation of your English string.
  3. A string used as the message for notifyAllPlayers or notifyPlayer will automatically have its translation displayed on the client - you don't need to do anything EXCEPT make sure that it is wrapped in a clienttranslate() on the server, so that it gets translated.
  4. A string used as the "description" or "descriptionmyturn" for a state in your state machine will also be automatically translated on the client - you don't need to do anything EXCEPT make sure that it is wrapped in a clienttranslate() on the server, so that it gets translated.
  5. A string used in the pre-games option screen will also automatically have any translation displayed at the client - you don't need to do anything EXCEPT make sure that it is wrapped in a totranslate() on the server (nota bene: you should not use clienttranslate() here as this file is processed specifically and those strings will not be included in the game translation file but in the main site translation file - with a prefix to keep translations isolated by module - as they have to be translated on main site pages. If you need exactly the same string in your game for a client side translation, it should be declared somewhere else in your code wrapped inside a clienttranslate() to be included in the game translation file.
  6. A string used in the statistics screen will operate like a string in the pre-game options screen. It will automatically have any translation displayed at the client as long as you make sure that it is wrapped in a totranslate() on the server.
  7. If you are using parameters for a call to notifyAllPlayers or notifyPlayers, or for a state's "description" or "descriptionmyturn" and those parameters contain text that needs to be translated, you will need to tell the system to display the translated versions of them by using the 'i18n' argument. See below for full details.

What strings should be marked for translation

YES:

  • Every text that can be visible by the player when the game is running normally. This includes tooltips, texts on cards, error messages.

NO:

  • Error messages that are not supposed to happen (unexpected errors).
  • Player names (i.e. do not put player_name or any php key send via notification which is essentially a player name, such as 'other_player' in 'i18n' array)
  • Proper names of characters or places in some cases (consult the publisher)
  • Numbers (i.e _("42"))
  • Keywords used internally

What rules should I follow for the original English strings?

For a coherent and homogeneous interface, here are some rules about ending a sentence with a final period '.'

  • As a general rule:
    • If a sentence is displayed isolated in the interface => no final period
    • If a sentence is followed or could be followed by another sentence in the same interface space => final period.
  • In detail:
    • No final period:
      • button labels
      • section titles
      • menu elements
      • links triggering an isolated action
      • anything that is not a full sentence
      • current actions in the status bar
    • Final period:
      • complete sentences (e.g., explanations and descriptions) that can be chained with other sentences
    • Either a period or no period is acceptable (but this should be consistent throughout the game) for:
      • isolated tooltips / small sentences
      • game logs (no period is usually preferable)
      • error messages (unless there is more than one sentence in the error message; final period is mandatory in this case)

Otherwise, you should try to follow as closely as possible the general style and format (including capitalization) used in the published English rulebook and game materials.


Focus on translating notifications

Usually, translating a website is simple: you just call a function on every string you have to translate, and the string is translated in the player's language. On Board Game Arena, this is exactly the same with the "_( string )" function.

However, there is one difference on BGA: notifications. The server is sending notifications to players, and most of the time the notifications are the same for every players, no matter what language each player is using. This is why notifications are translated on client side in the proper language, even if the strings are defined on server side.

How to not make translators crazy ;)

  • Try to reuse the exact same strings (with the same case, etc.) to minimize the number of strings to translate.
    • Example: Consider replacing
      self::_("Winner")
      and
      self::_("Winners")
      (two strings to translate) with
      self::_("Winner(s)")
    • Example 2:
      clienttranslate("play a card")
      and
      clienttranslate("Play a card")
      means there will be two strings to translate.
  • Do not mark as translatable a game element that does not have to be translated (ex: if the name of a monster on a card is "Zzzzz", maybe there's no need to translate it).
  • Words does not come in the same order in each language. Thus, when you have to translate a string with an argument, do not write something like:
self::_("First part of the string, ").$argument.' '.self::_("second part of the string")

Write instead:

sprintf( self::_("First part of the string, %s second part of the string"), $argument )

(or the equivalent "dojo.string.substitute" in Javascript)

  • This also applies to punctuation marks. I.e. this is not good: _("Pick")+":"
  • When translators are going to translate your game, the most difficult task for them is to get the context of the string to be translated. The shorter the string is, the more difficult the task is for them. As a rule of thumb, try to avoid short, insignificant strings that require knowledge of their surrounding context. You can also leave a comment on the context of the string in the translation program (English to English) if you are the developer of the game.
  • Use same sentence for plural vs singular. We prefer to write "player gets 1 coin(s)" (or "coin/s") rather than write two versions of the same string for plural and singular - it reduces the number of strings to translate. Also using the icon to represent coin in logs may solve this issue.
  • Instead of writing elaborate strings like "With the effect of ZZZ, player XXX gains a new YYY", which is very difficult to translate, write strings like "ZZZ: XXX gets YYY".
  • Use present tense instead of past. E.g., "player gets wood" instead of "player got wood".
  • Avoid using gender-specific wording as much as possible.
  • Also avoid "their". There is [a pronoun replacement system] is in place, and cannot work if your use "their" as a singular genderless pronoun, as it removes the information that it's singular. E.g., you should write "playerX returns card to *his* hand" or "playerX returns card to *his/her* hand" and not "playerX returns card to *their* hand" as in the 2 first case the system will substitute the proper pronoun his/her/their depending on playerX declared/undeclared gender, but in the second case it will always stay "their" whatever the player preference setting. In some case its actually needed, for example if you say "King sends his troops" in the log and King is not a player name, but character in the game - it will be changed to "her" if a player is female, which is not correct, in this case their would be appropriate (or just "the troops")
  • Where two strings are identical apart from (say) a number, use the same string with a ${parameter}, and call format_string_recursive (on the client) or use args (for notifications and state descriptions) to provide the details. But *never* do that for composing multiple sentences with words: other languages have different grammar and it would most likely create untranslatable strings.

WARNING: Make sure your strings will be translated!

For each game, our translation tool does a full scan of the code, looking for translation markers like "_()" or "clienttranslate()". (See below for the full list of translation markers.)

If your original string is not completely contained inside one of these markers, it won't be translated.

    // Examples: the following strings will be translated:
    var mystring_translated = _("my string");       // JS
    $mystring_translated = self::_("my string");    // PHP
    $mystring_translated = sprintf( self::_("my string with an %s argument"), $argument );   // PHP

    // Examples: the following strings WILL NOT be translated:
    $my_string = "my string";
    $not_translated = self::_( $my_string );   // The original string is not contained within a translator marker => no translation
    $not_translated = self::_( sprintf( "my string with a %s argument", $argument ) ); // Ditto

On client side (Javascript)

On client side, things are quite simple: you just have to use the "_()" function for all strings you want to translate.

Examples:

// Get a string in player's language:
var translated = _("original english string");

// Get a string in player's language with parameter:
var translated = dojo.string.substitute( _("You can pick ${p} cards and discard ${d}"), {
    p: 2,
    d: 4
} );

WARNING: in Javascript strings to translate, you should never use '\n', '\t' or such, as it will break the translation bundle and result in all the Javascript translation to fail. In any case, the strings will result in HTML code, and such character codes won't have any impact on the HTML rendering. You should use HTML markup instead.

ANOTHER WARNING: you cannot use this function _() in the javascript object constructor, but you can achieve the same if you use it the setup method. If you do that you will see this error in browser debug console:

 Try to use a translated string in JS object declaration : impossible => string is NOT translated

Formatting on client side

Sometimes you will want to apply some formatting on the client side, but don't want to put HTML tags within the source string (which may be confusing for translators).

This can be achieved with the bga_format function and Markdown-style syntax.

/**
 * @function bga_format
 * Replaces delimiters with HTML tags in translated text
 * @param {string} translated string
 * @param {Object} map of replacements to make, should be of the form
 *     {
 *         '*': (t) => t,
 *         '_': 'classname',
 *     }
 *     keys are delimiters within the translated string
 *     values are either strings (classname of surrounding span) or function (flexible replacements)
 * @returns {string} Returns the original string, with replacements made.
 */

Example:

// Get a string in player's language:
bga_format(_('You receive a *special* _tile_'), {
    '*': (t) => '<b>' + t + '</b>',
    '_': 'tile-name'
});

The above will return something along the lines of:

You receive a <b>special</b> <span class="tile-name">tile</span>

Or, if the user is playing in Spanish, for example:

Recibes una <span class="tile-name">loseta</span> <b>especial</b>

Notes:

  • The text passed to bga_format must still be translated using _(), don't forget it!
  • Good characters to demarcate formatting are: ['*', '_'] (other single characters will work, just make sure it will be clear to a translator)
  • If the replacements object is a string, the inner text will be placed within a <span> tag with the string as a class
  • If the replacements object is a function, the inner text will be passed as an argument to the function, and the return value will replace the text and the delimiters

On server side (PHP)

On PHP side, you can use 3 different functions to specify that a string must be translated.

Function clienttranslate

clienttranslate( 'string to translate' ):

This function is transparent: it will return the original English string without any change. Its only purpose is to mark this string as "must be translated", and to make sure the translated version of the string will be available on client side.

In general, you use clienttranslate:

  • In your states.inc.php, for the fields "description" and "descriptionmyturn".
      "description" => clienttranslate('${card_name}: ${actplayer} must discard 4 identical energies'),
  • In material.inc.php, when defining text for game materials that must be displayed on the client side.
$this->card_types = array(

     1 => array(
        'name' => clienttranslate('Amulet of Air'), // Thus, we can use "_( card_name )" on Javascript side.
  • When sending a notification with notifyAllPlayers or notifyPlayer, in the game log string and all game log arguments that need a translation.
     // A game log string with no argument:
     self::notifyAllPlayers( 'pickLibraryCards', clienttranslate('Everyone draw cards from their library'), [] );

As a consequence there is no point passing variables to this function. E.g.:

    notif="foo";
    self::notifyAllPlayers( 'log', clienttranslate(notif),[]); // BAD
    
    notif=clienttranslate("foo");
    self::notifyAllPlayers( 'log', notif,[]); // GOOD

    self::notifyAllPlayers( 'log', clienttranslate("player gains ${num} gems"),[num=>2]); // BAD it is dynamic string enterpreted by php 
    self::notifyAllPlayers( 'log', clienttranslate('player gains ${num} gems'),[num=>2]); // GOOD, difference is single quotes

    self::notifyAllPlayers( 'log', clienttranslate("player gains ".${num}." gems"),[num=>2]); // BAD concatenation 

Translating arguments is a little bit more complex. This uses the i18n special argument as below:

 // In the following example, we translate the game log itself, but also the "card_name" argument:

 self::notifyAllPlayers( 'winPoints', clienttranslate('${card_name}: ${player_name} gains ${points} point(s)'), array(
                'i18n' => array( 'card_name' ),     // <===== We specify here that "card_name" argument must be translated
                'player_id' => $player_id,
                'player_name' => self::getActivePlayerName(),
                'points' => $points,
                'card_name' => $this->card_types[8]['name'] // <==== Here, we provide original English string.
            ) ); 

To ensure the translation of the i18n argument will be made, clienttranslate must have been used somewhere, for instance:

$this->card_types = array(
...
     8 => array(
        'name' => clienttranslate("Amulet of Fire"),
...

Pay attention when using the i18n argument when translating arguments for the client: do NOT use same argument for both translations AND key codes for client-side actions (like using 'card_name' to move it on the player board as described in the example). It's pretty obvious in the example, but it can be very tricky when translation is made at the end of the development (which is often the case). Use explicit argument names like 'card_name_translated' by example.

WARNING: you should NEVER use concatenation with clienttranslate, as it would result in a different string to translate at runtime than the one retrieved statically in the translation system, and so translation would not be applied. If you need to compose a string, use substitution and the i18n parameter (but you also have to pay attention not to compose sentences in a way that is dependent upon the English language specific syntax, or it may be impossible to translate correctly in another language: sometimes, you need multiple full sentences rather than relying on substitution).


Method _ (underscore)

self::_( "my string to translate" ):

This function returns a string translated in the language of CURRENT user (i.e. player who send the request to the server) (be careful, this is NOT the active player).

This can be used like self::_('bla') or this->('bla'). You cannot use static underscore function _('bla') in php, it is not the same, it will not translate strings from your game.

The returned string will be surrounded by HTML tags, so you cannot use it in places where pure text is required or your string already has another html markup.

Most of the time, you don't need to translate strings on server side, except on the following situations:

  • Throwing user exceptions
  • Creating the labels for the game interface used in your template
  • Rarely, in your material.inc.php, if for example you need to use some string elements in your exceptions
  • Rarely, in your "getAllDatas" PHP method, as the data return by this method is used only by current user

Imporant! This function does two things:

  • Mark string for translation
  • Translates in runtime

As a consequence you absolutely cannot have $var in there! Or string concatenations. I.e.

 $food = self::_('Banana'); // good
 $msg = self::_("User gives $food to a cat"); // bad

For the translator we will have two strings "Banana" and "User gives $food to a cat", but in runtime we will be "Banana" and "User gives Banana to a cat" and second string would not match anything. And this is as bad

 $msg = self::_('User gives ' . $food . ' to a cat');// bad

To solve this, use sprintf() when server-side translations require variables:

 $food = self::_('Banana'); // good
 $msg = sprintf(self::_('User gives %s to a cat'), $food); // good

Throwing an exception because the player did a forbidden move

// This will display a translatable red message to the player that just did some wrong action:
throw new BgaUserException( self::_('You must choose 3 cards') );

Note that the use of BgaUserException signals that this exception is "expected". In theory, all exception that are expected should be translated.

// this is the same but with variable
throw new BgaUserException(sprintf(self::_('You must choose %d cards'), $card_num));

Creating labels

Simple label in tpl file (i.e. label which has same value independent of anything):

in .tpl:

  <div>{CARDS_FOR_YEAR_2}</div>

in .view.php:

$this->tpl['CARDS_FOR_YEAR_2'] = self::_("Your cards for year II");

Nota bene: it is recommended to set translated text in your interface client side rather than use template variables and server translation, so that translation works natively in replays (that don't have server side translations). For simple strings, the framework will handle moving the translation client side, but for more complex strings it may not work. In particular, you should not use server side translation for templates with substitution variables (or use the specific function "gameview_str_replace( $search, $replace, $string )" if not possible otherwise or for old code compatibility). Also, you should never use a "to_translate" class in your .tpl as it is used internally by the framework.


Label which is generated based of player's name, using server side template: in .tpl:

  <div id="empire_title_{COLOR}" class="side_title color_{COLOR}">{EMPIRE_LABEL}</div>

in .view.php:

        $this->page->insert_block("player_board", array ("COLOR" => $color,"PLAYER_NAME" => $name,"PLAYER_NO" => $no,
                "PLAYER_ID" => $player_id, "CLASSES" => $own?"own":"", 
                "EMPIRE_LABEL" => self::raw(gameview_str_replace('${player_name}', $name, self::_('${player_name}\'s EMPIRE')))
              ));

Label which is generated based of player's name, using client side (which is better): in .tpl:

  <div id="empire_title_{COLOR}" class="side_title color_{COLOR}">Stub</div>

in .js:

  for (var player_id in this.gamedatas.players) {
      var player_color = this.gamedatas.players[player_id].color;
      var player_name = this.gamedatas.players[player_id].name;
      $('empire_title_'+player_color).innerHTML=this.format_string_recursive(_("${player_name}'s EMPIRE"), {player_name: player_name});
  }

Translating strings in material file

Rarely, in your material.inc.php, if for example you need to use some string elements in your exceptions.

// In material.inc.php, $this->energies[n]['nametr'] has been created with the self::_() method. Now we can do this:
throw new BgaUserException( self::_("To execute this action you need more: ").' '.$this->energies[$resource_id]['nametr'] );

Note: you could also do this, so you don't need to pollute your material file

  throw new BgaUserException( self::_("To execute this action you need more: ").' '.self::_($this->energies[$resource_id]['name']) );

where $this->energies[$resource_id]['name'] were marked in material file using regular clienttranslate function

  $this->energies = [
      22 => [
          'name' => clienttranslate('Sun')
          ...
      ]
  ];

Strings in getAllDatas

Rarely, in your "getAllDatas" PHP method, as the data return by this method is used only by current user.

Note: not sure why would you send strings from the server in this case, as same can be achieved on the client usually.

Function totranslate

totranslate( "my string to translate" ):

This function works exactly like 'clienttranslate', except it tells BGA that the string is not needed on client side.

You should not use this function, except on the following cases:

  • Statistics name in stats.inc.php
  • Option names and option values name in gameoptions.inc.php

Top Secret Undocumented Features

If your string contains the clause '$${value}' (such as 'You gain $${value}.') then the translation system seems to move the position of the $ to a localized location (eg $5 in English but 5$ in French). This only seems to occur when using the argument 'value'.

In JavaScript, you can get the current user's language by retrieving the translation for the special string "$locale":

   var lang = _('$locale'); // en, fr, etc.

On server side - advanced (PHP)

If you want to use translation system in your custom static modules, you will first need to expose the _() function :

class mygame extends Table {
  // Exposing protected method translation
  public static function totranslate($text) {
    return self::_($text);
  }
}

Then you'll be able to use this directly in other modules by doing

throw new BgaUserException(mygame::totranslate("Translated error from my awesome module file"));

Notice how the "totranslate" is also used by the static analysis to detect your string. So the following will not work :

$msg = "Translated error from my awesome module file";
throw new BgaUserException(mygame::totranslate($msg));


Preview another language

Once a game is released, you can view a table as a specific language by appending a language code to the URL, such as

&lang=es

Analyze your project

It looks super complex but there is help! On studio control panel you can find a button "Check project" - it runs bunch of static analysis on your project including scanning for bad translations and missing translations. It also will extact all you translation keys as translator will see them so you can double check - and run spell checker on them.

Other Useful Links