Tml

Translation Markup Language

Translation Markup Language (TML) is used to identify the non-translatable or dynamic data within the labels. It provides a way to mark data and decoration tokens within the strings that need to be translated. There are different types of applications that can use TML - web, mobile and desktop. Some use HTML, others use Wiki-Like syntax for decorating the labels. TML aims at abstracting out the decoration mechanisms of the string used by the applications and instead provides its own simple, but powerful syntax. This allows for translation sharing across multiple applications.

Basics

Tml Client SDK extends Controller and View helpers by providing a translation function, called "tr". The function can be called using any of the following ways:

tr(label, description, tokens = {}, options = {})  

You can skip description:

tr(label, tokens = {}, options = {})  

You can also pass parameters as a hash:

tr(params = {:label => LABEL, :tokens => TOKENS, :options => OPTIONS})  

Alternatively, you can use string extensions:

"some text".translate(tokens = {}, options = {}, language = Tml.session.current_language)
"some text".translate(description, tokens = {}, options = {}, language = Tml.session.current_language)

Tml provides a number of helper methods that are available in views and controllers:

tml_current_language.translate(label, description, tokens = {}, options = {})  
tml_application.language('ru').translate(label, description, tokens = {}, options = {})  

  • label is the only required parameter.

  • description is an optional parameter, but should always be used if the label by itself is not sufficient enough to provide the meaning of the phrase.

  • tokens is an optional parameter that contains a hash (or a dictionary) of token values to be substituted in the label.

  • options provides a mechanism for passing additional directives to the translation engine.

  • Let's start with a sample phrase:

    tr("Hello World")  
    

    The description of a phrase is not mandatory, but it should be used in cases when the label alone is not sufficient enough to determine the meaning of the sentence being translated. As a general rule, you should always provide description to words, phrases and sentences that are only meaningful within a specific context. Tml uses label and description together to create a unique key for each phrase. The description serves two purposes: it creates a unique key for each label and it also gives a hint to the translators for the context in which the label is used.

    For example, the following two phrases will be registered as two independent entries in a database even though the have the same label, but a different description. The user will have to translate each one of them separately as they will have different translated labels in other languages.

    tr("Invite", "Link to invite your friends to join the site")  
    tr("Invite", "An invitation you received from your friend")  
    

    It is important to provide the best possible description for each phrase from the start. Keep in mind that changing a description in the future, after it has already been translated, will register a new phrase in the database and invalidate all of its translations. On the other hand, labels that are complete sentences may not need a description as they are fully self-contained.

    Data Tokens

    In many cases your tokens would be string objects that get substituted directly into the translated sentence.

    tr("Hello {user}", :user => "Michael")  
    

    Translations can be nested.

    tr("Welcome to {city}", :city => tr("Los Angeles"))  
    

    But we need to make sure not to take translations out of context.

    tr("Please visit our {registration} to join our site.", :registration => link_to(tr("registration page"), ""))  
    Please visit our [registration page](http://) to join our site  
    

    The problem with the above example, is that the "registration page" link text would be translated differently based on the context where it appears. You must keep the two parts together to make sure the translations are accurate. You will later see how you can use decoration tokens to fix the above problem.

    If your translation key needs to use context rules, you can pass the object and the substitution value as an array.

    tr("Dear {user}", :user => [@michael, "Michael B."])  
    Dear Michael B.  
    

    You can also get the substitution value by invoking a method of the object by using a symbol in the second parameter.

    tr("Dear {user}", :user => [@michael, :name])  
    Dear Michael  
    

    You can use hashes for the token values as well.

    tr("Hello {user}", :user => {:object => @michael, :attribute => :name})  
    Hello Michael  
    

    The object itself may be a hash too. But since hashes don't provide a mechanism to extract a meaningful string value, make sure that you indicate what the substitution value should be. You can pass it as a value attribute.

    tr("Hello {user}", :user => {:object => {:gender => :@michael}, :value => "Alex"})  
    Hello Alex  
    

    Or you can pass an attribute name of an attribute in the hash.

    tr("Hello {user}", :user => {:object => {:name => "Alex"}, :attribute => :name})  
    Hello Alex  
    

    Methon Tokens

    Method tokens allow you to invoke methods on the object you are passing to get the substitution value.

    tr("Hello {user.name}, you are a {user.gender}", :user => @michael)  
    Hello Michael, you are a male  
    

    Piped Tokens

    Piped tokens work in conjunction with context rules and allow you to provide substitution values based on the object values.

    tr("You have {count || one: message, other: messages}", :count => 1)  
    You have 1 message  
    You have 2 messages  
    You have 10 messages  
    

    Double pipe "||" means that the value would be displayed, followed by the word that depends on the value. In this case, if the count value meets the criteria for the rule "one", then it will display the word set to the rule. For all other cases it would display the "other" value.

    Since the sequence of parameters is mapped to the sequence of rules, you can omit naming the parameters.

    tr("You have {count || message, messages}", :count => 2)  
    You have 1 message  
    You have 2 messages  
    You have 10 messages  
    

    Since the library comes with default pluralizers, you don't even need to provide the plural form. It will be automagically generated for you.

    tr("You have {count || message}", :count => 2)  
    You have 1 message  
    You have 2 messages  
    You have 10 messages  
    

    The same exact concept applies to other token types and context rules.

    tr("{user} updated {user | his, her} profile.", :user => @michael)  
    Michael updated his profile  
    

    Single pipe "|" means to not display the actual token value, but display the value that follows based on the context rules.

    Implied Tokens

    Implied tokens use a single pipe since the value does not need to be displayed.

    tr("{user| male: He, female: She} likes this movie.", :user => @michael)  
    He likes this movie.  
    

    Similar to the previous examples, you don't have to provide the named parameter values.

    tr("{user| He, She} likes this movie.", :user => @michael)  
    He likes this movie.  
    

    Even though the base language does not have a gender specific dependency in some cases, it is always good to wrap it with an implied token.

    tr("{user| Born on}: ", :user => @michael)  
    Born on:  
    

    As a general rule, if any of the words of your translation keys depend on a user, use implied tokens. It won't affect default translations, yet it would give translators an option make the translation accurate.

    Decoration Tokens

    Decoration tokens are used to inject HTML styling into translations. In other libraries, like in iOS or Android, the tokens can be substituted with a native decoration framework.

    The token values can be passed as lambdas.

    tr("Hello [link: World]", :bold => lambda{|value| link_to(value, "")})  
    

    Or they can be defined as strings, where {$0} indicates the translated value being processed in.

    tr("Hello [bold: World]", :bold => "<strong>{$0}</strong>")  
    

    You can also pre-define all your tokens in the configuration. Then you don't need to pass them at all. In the following example, bold is already pre-defined in the framework.

    Tml.configure do |config|  
      config.set_default_token(:custom, "<span style='font-size:larger'>{$0}</span>", :decoration)
    end  
    
    tr("Hello [custom: World]")  
    

    The default tokens can also use named parameters:

    Tml.configure do |config|  
      config.set_default_token(:link, "<a href='{$href}'>{$0}</a>", :decoration)
      config.set_default_token(:font, "<span style='font-size:{$size};font-family:{$family};color:{$color}'>{$0}</span>", :decoration)
    end  
    
    tr("[link: Click here] to [font: create a new account].", {  
        :link => {:href => "/"},
        :font => {:family => "Arial", :size => 16, :color => "green"}
    })
    

    You can also use the long notation of the decoration token.

    tr("[link] Click here [/link] to view this section of the document", :link => {:href => "/docs"})  
    

    Decoration tokens can be nested.

    tr("[link] [bold: Click here] to view [underline: this section] of the document [/link]", {  
      :link => {:href => "/docs"},
      :underline => lambda{|value| "<span style='text-decoration: underline'>#{value}</span>"}
    })
    

    Nested Tokens

    Decoration tokens can be nested and they man contain data tokens as well.

    tr("You have [link: {count||message}]", {  
      :count => 10,
      :link => lambda{|value| link_to(value, "")}
    })
    You have 10 messages  
    

    Notice that all of the nested tokens are still translated in-context and allow for very accurate translations.

    tr("You have [link: [indent: {count}] {count|message}]", {  
      :count => 10,
      :indent => "<span style='font-size:larger'>{$0}</span>",
      :link => lambda{|value| link_to(value.html_safe, "")}
    })
    

    Array Tokens

    Default Option:

    tr("{users} joined the site", {:users => [[user1, user2, user3], :name, {  
      :limit => 4,
      :separator => ', ',
      :joiner => 'and',
      :remainder => lambda{|elements| tr("#{count||other}", :count => elements.size)},
      :expandable => true,
      :collapsable => true
    })
    

    Example

    tr("{users} joined the site.", :users => [[@michael, @anna]])  
    Michael and Anna joined the site  
    
    tr("{users} joined the site.", :users => [[@michael, @anna], :name, {:joiner => "or"}])  
    Michael or Anna joined the site.  
    
    tr("{users} joined the site.", {  
      :users => [[@michael, @anna, @jenny, @alex, @peter, @thomas, @karen], :name]
    })
    Michael, Anna, Jenny, Alex and 3 others joined the site.  
    
    tr("{users} joined the site.", {  
      :users => [[@michael, @anna, @jenny, @alex, @peter, @thomas, @karen], :name, {:limit => 4, :expandable => false}]
    })
    Michael, Anna, Jenny, Alex and 3 others joined the site.  
    
    tr("{users} joined the site.", {  
      :users => [
        [@anna, @jenny, @alex, @peter, @thomas, @karen],
        :name,
        {:limit=>3, :expandable => true, :collapsable => false}
      ]
    })
    Anna, Jenny, Alex and 3 others joined the site.  
    

    Context Rules

    Context rules are used to provide translations based on a specific value of one or more tokens in a sentence.

    Number

    Languages may have simple or complex numeric rules. For example, in English, there are only two rules for "one" and "other". Slovak languages, like Russian, have 3 rules. Translator can provide a translation for each rule or rule combination based on the token values.

    1.upto(3) do |i|  
      tr("You have {count || message}", :count => i)
    end  
    You have 1 message  
    You have 2 messages  
    You have 3 messages  
    
    1.upto(5) do |i|  
      tr("Displaying [bold: {start_num}]-[bold: {end_num}] of [bold: {count}] people",
              :start_num => i,
              :end_num => i+25,
              :count => i*100 + i)
    end  
    Displaying 1-26 of 101 people  
    Displaying 2-27 of 202 people  
    Displaying 3-28 of 303 people  
    Displaying 4-29 of 404 people  
    Displaying 5-30 of 505 people  
    

    Gender

    Similarly to the numeric rules, some language have dependencies on the gender.

    [@michael, @anna].each do |user|
      tr("{user} uploaded {user | his, her} photo", :user => user)
    end  
    Michael uploaded his photo  
    Anna uploaded her photo  
    

    Sometimes tokens need to be implied:

    [@michael, @anna].each do |user|
      tr("{user | Registered} on:", :user => user)
    end  
    Registered on:  
    Registered on:  
    

    Dates

    Dates can also be used for contextual evaluation. Consider the following example:

    [Date.today, Date.today - 1.day, Date.today+1.day].each do |date|
      if date == Date.today
        tr("{user} is celebrating {user| his, her} birthday today", {:user => @michael})
      elsif date < Date.today
        tr("{user} celebrated {user| his, her} birthday on {date}", {:user => @michael})
      else
        tr("{user} will celebrate {user| his, her} birthday on {date}", {:user => @michael})
      end
    end
    
    Michael is celebrating his birthday today  
    Michael celebrated his birthday on 2/7/2016  
    Michael will celebrate his birthday on 2/9/2016  
    

    You can encode the entire structure in one sentence:

    [Date.today, Date.today - 1.day, Date.today+1.day].each do |date|
      tr("{user} {date| past: celebrated, present: celebrates, future: will celebrate} {user| his, her} birthday {date | on #date#, today, on #date#}", {
        :user => @michael, :date => date
      })
    end
    
    Michael is celebrating his birthday today  
    Michael celebrated his birthday on 2/7/2016  
    Michael will celebrate his birthday on 2/9/2016  
    

    Lists

    In languages like Hebrew, the list rules may include cases such as when all members of the list are male, female or have mixed genders. In Russian, the list rules may include cases for single member male, female, unknown or multiple members.

    [[@michael], [@michael, @anna]].each do |users|
      tr("{users || likes, like} this post", :users => users)
    end
    
    Michael likes this post  
    Anna likes this post  
    Karen and Jenny like this post  
    Thomas, Michael and Peter like this post  
    
    [[@michael], [@anna], [@karen, @jenny], [@thomas, @michael, @peter]].each do |users|
      tr("{users||has, have} arrived at {users|his, her, their} destination.",
              :users => [users, lambda{|user| tr(user.name)}]
      )
    end
    
    Michael has arrived at his destination.  
    Anna has arrived at her destination.  
    Karen and Jenny have arrived at their destination.  
    Thomas, Michael and Peter have arrived at their destination.  
    

    Language Cases

    Language cases allow you to manipulate the values of the data passed through tokens.

    English Examples

    Posessive

    tr("This is {user::pos} post", :user => @michael)
    
    This is Michael's post  
    
    [[@anna], [@karen, @jenny], [@thomas, @jenny, @peter]].each do |users|
      tr("{actor} updated {users::pos} profile", :actor => @michael, :users => [users, lambda{|user| tr(user.name)}])
    end
    
    Michael updated Anna's profile  
    Michael updated Karen's and Jenny's profile  
    Michael updated Thomas', Jenny's and Peter's profile  
    

    Counters

    1.upto(3) do |index|  
      tr("You have already sent this message {count::times}", :count => index)
    end
    
    You have already sent this message 1 times  
    You have already sent this message 2 times  
    You have already sent this message 3 times  
    

    Ordinals

    1.upto(4) do |index|  
      tr("This is your {count::ord} warning", :count => index)
    end
    
    This is your 1st warning  
    This is your 2nd warning  
    This is your 3rd warning  
    This is your 4th warning  
    

    Extensions

    Blocks

    Blocks allow you to apply a set of options to all translation keys within the block. One of the main uses cases for the blocks are grouping keys into sources. Sources are used for caching and management of translation keys.

    <%= tml_with_options_tag(:source => :sample_source) do %>  
      <%= tr("Hello World") %>
    <% end %>  
    

    You can use blocks to indicate the source language of all of the items inside.

    <%= tml_with_options_tag(:locale => 'ru') do %>  
      <%= tr("Привет Мир") %>
    <% end %>  
    

    Code Blocks

    Code Blocks will extract strings within the code block

    <%= trh() do>  
    <p>  
      <strong>Decoration Tockens</strong>
      <p>
      "Hello World"
      </p>
      <br>
      Hello World
      <p>
        <strong>Language Cases</strong>
      </p>
    <p>  
    <% end %>  
    

    Text Fields & Buttons

    Buttons or fields default text should be translatable and for that we will use function TRL

    <%= text_field_tag 'query', trl('Enter your search query here') %>  
    <%= button_tag trl("Click here") %>