How to organize your translations in Symfony

Share Button

Lire la version française

We are working at the moment on a big internationalized project, and we have consequently many translations in our application. These translations are dispatched in many bundles (7 for now) and are used throughout the application. At the begining of the development, we hadn’t any specific organisation for the translations and we put the translation in a random bundle (the one on which we were working, most of the time).

But, one beautiful and sunny day (so it was probably not in May), I opened a translation file and I realized how awful it was.
So, we started to think about a good and simple way to keep these translation files well organized. Oh, we used yaml, so if you use xliff or whatever else, maybe this post won’t help you. But maybe it will.

Organize your translations

What we wanted :

  • - know exactly where to put a new translation key
  • - know which file (and which bundle) to look to retrieve a given translation key (from a template)
  • - keep it simple to maintain
  • - have an idea of the final message just looking at the key
  • - separate the logic into the different bundles
  • - find something we will be able to use in our other projects

The files we use

For the moment, we only use 4 files (for each bundle), but it’s possible we will need to create new ones in the future. In each filename, XX represents the locale (en, fr, es, de, …) :

  • admin.XX.yml : We have a custom administration part (not public), each message from this part should live in this file.
  • flashes.XX.yml : We have lot of flash messages, so we decided to put them in a separate file. If you only have few flash messages, maybe you could put them under a flash key in your messages.XX.yml file (see the next section about keys).
  • messages.XX.yml : This is the default translator file for common messages.
  • validators.XX.yml : This is the default translator file for validation messages.

Explanations about keys

After a lot of coffees (grounded by hand), we have identified some “static” keys which will be used. In other words, each translation key must be under one (or more) of these “static” key. You will better understand later with a concrete example but here are the keys we use :

In the messages.XX.yml and admin.XX.yml files

  • action : Each label which describe an action (edit, delete, see, add an item, go to homepage, …
  • description : Each meta-description (meta tag) text (this key should be under the meta one)
  • form : Everything that is relative to form. This key will contain at least the label, legend and placeholder keys.
  • label : Each field type label (this key should be under the form one).
  • legend : Each fieldset legend (this key should be under the form one).
  • message : It’s the default key, it contains text that cannot be placed under another key.
  • meta : This key should typically contain only the keys title and/or description
  • placeholder : The placeholders for the different input/textarea fields.
  • title : This key can contain either title texts (you would put in h1, h2, … tags) or page titles (title tag).
  • warn : Each exception message (“this page does not exist”, …) or confirmation (“Are you sure?”, …) messages.
  • word : This key contains all individual words (and, or, day, …)

In the flashes.XX.yml file

  • flash : Every key should be placed under this key.
  • error : Each error flash message.
  • notice : Each notice flash message.
  • success : Each success flash message.

In the validators.XX.yml file

The “statics” keys are the constraints name. See the next section to see a concrete example in action.

Concrete example

Imagine an Forum application. Here are some of the possible objects : Category, Question, Answer, User and Comment.

Example of what the messages.en.yml could look like

# Obtao\ForumBundle\Resources\translations\messages.en.yml
forum: #the first key is always the bundle name
  action: #usually links or buttons in your application
    lastPosts: See the last posted answers
    newAnswer: Add a new answer
    newQuestion: Post a new question
  category: # an object can (and should) also be a key
    title: # The titles relative to the 'category' object
      new: Create a new category
      show: Posts of the "%name%" category
    warn: # warn messages relative to the 'category' object
      forbidden: You cannot access to this category
      notFound: The category does not exist
      add: Add a new comment
      comment: comment|comments
    description: # messages to be displayed in meta description tags
      main: This forum talks about everything, feel free to create
      members: Our forum community is rich and heterogeneous. Create and account and join us.
    title: # the messages to be displayed in the title tag
      main: Welcome to our forum
      members: The forum community
    form: # here are the messages relative to a form 'question' (probably QuestionType class)
      help: Choose the category which best describes your question
        category: Category # the label for the 'category' property of the 'question' object
        createdAt: Created at # for the 'created_at' property
        question: Question # ...
        user: Added by
        question: Type your question here
      notFound: The question does not exist
  title: # titles (usually displayed in h1, h2, ... tags, or in a menu)
    newQuestion: Post a new question
    signin: Create your account
    welcome: Welcome to our forum
    and: and
    by: by
    the: the

As you see, in addition to the “static” keys, we use the object names to keep the things organized. If you follow these conventions, you will retrieve easily a key from a template, and vice versa. For example, if you find the key ‘forum.answer.action.edit’ in a template, you can easily understand that the final message would be something like “Edit this answer” or “Edit my answer”, and you will find it into the file Obtao/ForumBundle/Resources/translations/messages.XX.yml.
You can note two things in the code example above :
- even if the translations are the same, we created two keys : forum.action.newQuestion and forum.title.newQuestion. Maybe you have already understood the difference : the first one would probably look like a link, for example :

<a href="{{ path('forum_question_new')}}">{{ 'forum.action.newQuestion'|trans }}</a>

whereas the second one would be the title of the page to which the previous link led you, for example :
<h1>{{ 'forum.action.newQuestion'|trans }}</h1>

And it’s a better idea if you decide to change one of the translations in the future. For example “Add a new post” in the link and “Create a new post” in the title.

The second thing, a bit less important, is that I have written the translations under the word key in lower case. As they are words, they can be displayed at the beginning or in the middle of a sentence. See the two examples below to understand:

{# in a Twig template #}

{# first possibility #}
{{ 'forum.word.the'|trans|capitalize }} {{ }}, {{ ''|trans }} {{ }}
{# will display "The 05/31/2013 by Gregquat" #}

{# second possibility #}
{{ ''|capitalize|trans }} {{ }}, {{ 'forum.word.the'|trans }} {{ }}
{# will display "By Gregquat, the 05/31/2013" #}

We kept the flexibility we wanted to have.

Example of what the flashes.en.yml could look like

Here is the example for the flash messages.

# Obtao\ForumBundle\Resources\translations\flashes.en.yml
forum: # again, the first key is the bundle name
  flash: # the second key should always be "flash", and only this one
      answer: # The error flash messages related to the answer message
        alreadyExist: You cannot add an answer since you already have posted one today
        cannotDelete: You cannot delete this answer
      # I always have a general default message
      general: Sorry, an error has occurred
     haveAnswers: Someone has answer to your question %name%
     profileNotComplete: You profile is not complete, you should fill all the fields
       created: Your account has been created successfully
       complete: Your profile is complete, good job!
       created: Your answer has been created
       edited: Your changes have been saved
       deleted: Your answer has been deleted
     # again a default message
     general: The action has been done successfully
       posted: Your question has been posted successfully
       removed: Your question has been removed successfully

Again, if you see the message “forum.flash.success.answer.created” in a Controller (since it’s a flash message, you would use it in a Controller), you immediately understand the translation would be something like “Your answer has been created successfully”, and that it’s relative to the ForumBundle.

Example of what the validators.en.yml could look like

Here is the example for the form validator messages.

# Obtao\ForumBundle\Resources\translations\validators.en.yml
forum: # again, the first key is the bundle name
  notBlank: This field should not be blank # a default message for the "notBlank" constraint
  emailInvalid: This is not a valid email address # another default message
  question: # an object name
    notBlank: The question must be filled
    TooShort: The question must contain at least 20 characters

As you see in the example above, I usually define default messages (I would put them in a CoreBundle, or somewhere most “common”, but I put them in the forum/validators file for the example). You can also see that I use constraint names as key, and sometime I use the object name to define specific message.


This post must be understood as a proposal on how to organize your translation messages. Obviously, you are not forced to use this organization, but I use it in my projects and I think it has proven.
So my only advice is to try it and make your own opinion. Maybe you’ll like it.
And if you are already using your own organisation system, I’m interested in knowing which one.

Share Button

5 thoughts on “How to organize your translations in Symfony

  1. Hi,

    In my case, I use separated files for forms (label, help, etc), menus (top, bottom, etc), seo (title, description, facebook, etc), routes (JMSI18nRoutingBundle), routesParameters (always for i18n) and natives validators and messages.
    Maybe I could code something to merge into a single file routes, routesParameters and seo.
    I don’t have flash file because I don’t use a lot but it’s a good idea.
    I have a backend and frontend key for each of this files, only if necessary.

    • Hello,

      That’s a pretty good idea to create separate files for forms and seo translations because these are big parts.
      But I do not want to create to many files so I won’t create a file for each part.
      But I’ll definitely do for forms and seo. Thanks

  2. Great post.
    This is exactly idea that I’m looking for. May you post an example of admin.XX.yml file?
    Do you use sonata admin as backend?

    Thanks in advance.

    • Hi,

      We do not use the sonata admin bundle. Our admin.xx.yml translations files look like the messages.xx.yml ones as they are “classic” messages files, but applied to a backend.
      For example :

          login: Log in
          logout: Log out
          save: Save
            edit: Edit the user
            delete: Delete this user
            new: Add a new user
            save: Save the user
              email: Email
              firstname: Firstname
              lastname: Lastname
              email: Enter a valid email
            notAllowed: You cannot access to users administration
          next: Next
          previous: Previous
  3. Hello! If anyone needs to translate their apps into multiple languages, it might help using a collaborative translation management platform like which can make things easier for everyone.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Protected by WP Anti Spam