Skip to content

Translations

Two distinct mechanisms are involved in i18n (internationalisation):

  • translation of the values when depending on the language used for storing the Model;

  • translation of the fields labels when objects are rendered within a View;

As naming convention, language identification uses the ISO codes for language and country, separated by a dash. The country code being optional. Each code is two letters long, lower case for language and uppercase for country.

Syntax: {ISO639-1 lang}[-{ISO-3166-1 country}]

Examples :

  • fr: standard french
  • fr-CA: french Canadian
  • zh-CN : simplified chinese

Model - Translation of fields values

Any non-relational field can be translated (see Fields types). The ORM uses a dedicated Model core\Translation to store the terms translations. When a field is marked as multilang, the values of its translations can be retrieved with a SQL query.

'Translation' object fields:

<?php
/*[...]*/

class Translation extends Model {

  public static function getColumns() {
    return [
        'language' => [
          'type'              => 'string',
          'usage'             => 'language/iso-639:2'
        ],
        'object_class' => [
          'type'              => 'string'
        ],
        'object_field' => [
          'type'              => 'string'
        ],
        'object_id' => [
          'type'              => 'integer'
        ],
        'value' => [
         'type'               => 'binary'
        ]
    ];
  }
}

For the value field, the SQL MEDIUMBLOB type is used (overhead of 3 bytes, max size of 16,7 Mo).

Indeed, the size of the 'value' column may vary greatly from one type to another (the binary type may represent a document, a picture, a video, …). And, most of the time, fields that must be translated are texts (string, short_text or text). In any case, a 3 characters overhead is acceptable (and set a 16 Mo limit should not be a problem).

Note : as the only condition for the SQL type is to be compatible with the associated eQual type, in order for a binary field to be translated (for instance a PDF doc available in different languages), the size of the PDF should remain smaller than the SQL MEDIUMBLOB type (16 MB with MySQL).

Using multilang in CRUD

i18n is applied on a field basis.

To mark a field for i18n support and allow to store several translations for it, the multilang property must be set to true in the related class definition file.

Example :

<?php
'field' => [
    'type' => 'string', 
    'multilang' => true
]

To understand how it works, let's compare with a normal http UPDATE controller (which is also valid for CREATE) :

<?php
// [...]
MyClass::ids($id)
        ->update($fields)
        ->read(['field1', 'field2'])
        ->first();

Now let's use the multilang field by adding a parameter in our request :

<?php
// [...]
$lang = 'fr';
MyClass::ids($id)
        ->update($fields, $lang)
        ->read(['field1', 'field2'], $lang)
        ->first();

Using the $lang parameter (DEFAULT_LANG by default) indicates the ORM which value to return for fields set as multilang.

Note: you don't need to specify language for a DELETE request since the whole object gets deleted

And it's the same process for a GET request, for instance :

<?php
// [...]
$lang = 'en';
MyClass::search()
        ->read(['field1', 'field2'], $lang)
        ->get(true);

Now there is a special case you're very likely to encounter : CREATE an object with multiple translations at once.

Here is how you should proceed :

<?php
// [...]
$lang1 = 'fr';
$lang2 = 'en';

MyClass::create($fields, $lang1)
        ->read(['field1', 'field2'])
        ->first();

MyClass::ids($id)->update($fields, $lang2);

What it does, is updating your initial object with the secondary language

If you want to add more than 2 languages, you can repeat that last line for as many languages needed.

Like so :

<?php
// [...]
MyClass::ids($id)->update($fields, $lang2); // $lang2 = 'en'
MyClass::ids($id)->update($fields, $lang3); // $lang3 = 'es'

Each package can have an optional folder named i18n.

If such folder exists, it must contain a subfolder for each language used by the backend (according to naming convention and as defined in the configuration).

Inside those folders, translations are stored in .json files which prefixes are identical to the class they refer to (ex. : User.json).

Those translation files use UTF-8 encoding and JSON format, and contain the translation of all terms that might be translated (attributes label, description , selection, and help).

The translation file has the name and the plural of the name of the class, as well as a description for it.

The model section which consist of a map of all the field names that are present in the class, to which is associated a list of attributes with their related translations.

Possible attributes are :

  • label attribute is the name of the field that we want the user to see and it must start with a capital letter.

  • description attribute is considered an additional information or a small brief for the field, to assist users in understanding the use of it. This attribute should start with a capital letter and end with either ".", "?" or "!".

  • selection attribute is usually added for the "type" field of the class, which allows the translation of the available options of the field.

  • help attribute is going to be used as a more detailed explanation about the view's fields, in case the user didn't properly understand the description. For the moment, this attribute is going to be an empty string, but should be added to all the fields.

The view section that translates all the section names of the tabs or the groups, if present in the form view. Since some classes may have multiple types of views, like "Partner.form.default.json" and "Partner.form.payer.json", the view section will contain the name of the type (in this case "form.default" and "form.payer"). Inside each object a name, description and layout containing the "id" of the view's sections and assigning a label to it, will be displayed as shown below.

"view": {
  "form.default": {
    "name": "Partenaires",
    "description": "Formulaire de base pour afficher les partenaires",
    "layout": {}
  },
  "list.default": {
    "name": "Partneraires",
    "description": "Liste de base des partenaires pré-existants",
    "layout": {}
  },
  "form.payer": {
    "name": "Client",
    "description": "Formulaire de base pour les clients pré-existants",
    "layout": {}
  },
  "list.customer": {
    "name": "Clients",
    "description": "Liste de base des clients pré-existants",
    "layout": {}
  },
  "list.contact": {
    "name": "Contacts",
    "description": "List des contacts pré-existants",
    "layout": {}
  }
}

In this example, multiple view type exists but all the layouts are empty because none of them were divided into sections. A none empty layout, which means that the view is actually divided into sections or tabs will look like the following example, where "section.identity_info", "section.identity_main_address", "section.identity_addresses", and "section.identity_description" are the sections ids and the labels are the name we want to be shown on the user side. Usually "list.default" type have an empty layout because it is a table, so it will not be divided into sections.

"view": {
  "form.default": {
    "name": "Identité",
    "description": "Formulaire de base pour afficher des groupes",
    "layout": {
      "section.identity_info": {
        "label": "Informations générales"
      },
      "section.identity_main_address": {
        "label": "Adresse principale"
      },
      "section.identity_addresses": {
        "label": "Autres adresses"
      },
      "section.identity_description": {
        "label": "Description"
      }
    }
  },
  "list.default": {
    "name": "Liste d'identités",
    "description": "Liste de base d'identités pré-existantes",
    "layout": {}
  }
}

The error section is used for translating the form validation (Usually added for the Login/Sign Up form).

In every i18n (translation) folder in each package, the user has the ability to create a Markdown (.md) file to better explain what the view does and how it works. For example, to explain more about Booking list view, we can create a "Booking.list.default.md" file inside the i18n folder where this view is present and write down more explanation about it. We can write that this view represents all the booking history related to a center of the logged user, and that they should be displayed in descending order which means that the most recent posts are the ones shown first.

Example of french (fr) translation using

Example of french (fr) translation using i18n

{
  "name": "Règle de Tva",
  "plural": "Règles de Tva",
  "description": "Les règles comptables permettent de préciser sur quel compte une opération doit être imputée.",
  "model": {
    "name":{
      "label":"Nom", 
      "description": "Nom de la règle comptable.", 
      "help": ""},
    "description":{
        "label":"Description", 
        "description": "Brève description de la règle pour servir de mémo.", 
        "help": ""},
    "type":{
        "label":"Type", 
        "description": "Type d'opération auquel cette règle se rapporte.", 
        "selection": {
            "purchase": "achat", 
            "sale": "vente"}, 
        "help": ""},
        "vat_rule_id":{
            "label":"Règle de TVA", 
            "description": "Règle de TVA à laquelle cette ligne est liée.", 
            "help": ""
        },
        "accounting_rule_line_ids":{
            "label":"Lignes liées à cette règle", 
            "description": "Lignes liées à cette règle.", 
            "help": ""}
  },
  "view": {
    "form.default": {
      "name": "Règles de Tva",
      "description": "Vue par défaut des règles comptables avec tous les champs",
      "layout": {
        "section.accounting_rules_section": {
          "label": "Détails"
        },
        "section.accounting_rulelines": {
          "label": "Ligne de règle comptable"
        }
      }
    },
    "list.default": {
      "name": "Liste des Règles de Tva",
      "description": "Cette vue est destinée à afficher les règles comptables.",
      "layout": {}
    }
  },
  "error": {

  }
}

Multiple translation files

Classes may be used in different packages (extending parent classes with the possibility of adding new fields) and therefore, they also may have different translation files.

Here is an example of an extended class.

<?php
namespace lodging\sale\booking;

class Contact extends \sale\booking\Contact {

    public static function getName() {
        return "Contact";
    }

    public static function getDescription() {
        return "Booking contacts are persons involved in the organisation of 
        a booking.";
    }

    public static function getColumns() {

        return [
            'owner_identity_id' => [
                'type'              => 'many2one',
                'foreign_object'    => 'lodging\identity\Identity',
                'description'       => 'The organisation which the targeted 
                identity is a partner of.',
                'default'           => 1
            ],

            'partner_identity_id' => [
                'type'              => 'many2one',
                'foreign_object'    => 'lodging\identity\Identity',
                'description'       => 'The targeted identity (the partner).',
                'onupdate'          => 'identity\Partner::onupdatePartnerIdentityId',
                'required'          => true
            ],

            'booking_id' => [
                'type'              => 'many2one',
                'foreign_object'    => 'lodging\sale\booking\Booking',
                'description'       => 'Booking the contact relates to.',
                'required'          => true
            ],

            'owner_identity_id' => [
                'type'              => 'many2one',
                'foreign_object'    => 'lodging\identity\Identity',
                'description'       => 'The organisation which the targeted 
                identity is a partner of.',
                'default'           => 1
            ]

        ];
    }
}

If multiple translation files exist, the child class (extending the parent) gets overloaded with the parent (being extended) translation files. Also, if the same fields are translated, the translation used is the lowest/furthest one in the translation hierarchy (the translation file of the child class).

For example, if the class Contact inside the package sale and the one inside the package lodging both have inside the i18n folder a file named Contact.json in which the field booking_id is translated, the translation inside the package lodging (extending the class inside the package sale) will be the one used.

On the other hand, if different fields are translated, the child class (inside the package lodging in the above example) will inherit the translations from the parent classes.

This scenario goes on until we reach the root-parent class "equal\orm\Model".

Translation of Menus

Even though Menu views are different than Form Views, their translation is the same. However, menu's don't contain anything in there model section since it has no fields, which is why it won't be displayed in the translation of it. The view section is similar to the ones shown above, containing the "id" of the section, its "label" and an empty "layout" object because all items in the menu are considered as a new view type even if it's a children item in the view. Below is the structured menu translation:

{
  "description": "",
  "view": {
    "item.bookings": {
      "label": "Réservations",
      "description": "",
      "layout": {}
    },
    "item.customers": {
      "label": "Clients",
      "description": "",
      "layout": {}
    },
    "item.planning": {
      "label": "Calendrier",
      "description": "",
      "layout": {}
    }
  }
}

Translation files are stored in a specific i18n folders, in the packages directory which entities belong to. Generic filename format is : packages/{package_name}/i18n/{lang}/{class_name}.json

Translation files are requested using the config_i18n controller:

Example : ```bash $ ./equal.run --get=config_i18n --entity=core\User --lang=fr