Refactoring Ruby on Rails i18n YAML files using dictionaries

10,250

Solution 1

TLDNR; Don't hack your file format, improve the rails helpers and help to establish a standardized key structure!

TLDR;

Don't want to rain on your parade, but I have a few issues with this technique. The dilemma of where to use the dot shortcut and how the rails helpers' key structure differs can be a bit puzzling.

As I understand it, the question is basically about DRYing up your locale files and using a feature of the YAML language to achieve this.

Firstly, anchors are only really guaranteed to work for YAML so this solution can't be applied generically to I18n. This technique is probably not feasible if you use a different backend. Be it SQL, Redis or Json, I'm not aware of any of them having any kind symlinking functionality. And that's without going too much into the fact that under the hood, the translations are in fact duplicated.

The second and bigger problem that I have is about linguistics. Your example makes the case that all of these terms are exactly equal in context and in meaning. Unfortunately this is only ever the case in extremely simple examples.

Undoubtedly, as your app grows or as you add additional languages, you'll find that a Person's "name" attribute has to be distinct from say a Book's "name" attribute which in English we'll call a "title" - OK, this example is really convoluted ;) but as you mix in more and more languages this situation does occur frequently and ideally, we want a generic way of dealing with it.

I think in large part, the complexity comes from the rails helpers that have evolved with different defaults without there being a convention for key structures.

Going back to your example you mention 2 things that I think are really distinct : activerecord attribute translations which use the rails helpers and view translations which use the dot shortcut.

Let me give you an example of a workflow that is super frequent :

  1. You create a form with the User's "name" field in this situation, you want to use the generic "name" attribute translations (label_tag should use something like :'attributes.name'). This is the simplest, DRYest case to get you up and running quickly, bulk translating simple attributes.
  2. A while later you decide that User's "name" needs to be translated as "full name" for this model only so you create a new translation that has a higher priority in label_tag's lookup call (say :'activerecord.attributes.users.name'))
  3. Later still, the marketing guy has the brilliant idea of displaying this field's label as "enter your funky fresh name" on this page (and only on this page). We're not describing the name attribute anymore, we're describing a particular view of this form; this is where the dot shortcut comes in converting :'.form.name' to something like ':users.new.form.name'.

There is no way we could handle this situation with a shared "dictionary". Sure our locale file would be DRY, but our linguistic/translation concerns are vastly different from our developer concerns here (sadly).

On the plus side, we can get clearer about what kind of content we're describing and reflect that in our key structures and in our tools - that for me is the way forward! :)

Solution 2

I just release a gem called i18n-recursive-lookup that allows a definition to contain embedded references to other definitions by introducing the special embedded marker ${}

https://github.com/annkissam/i18n-recursive-lookup

Using it you could refactor your example to:

dictionary:
  email: Email
  name: Name
  password: Password
  confirmation: Confirmation

activerecord:
  attributes:
    user:
      email: ${dictionary.email}
      name: ${dictionary.name}
      password: ${dictionary.password}
      password_confirmation: ${dictionary.confirmation}
  models:
    user: User
users:
  fields:  
    email: ${dictionary.email}
    name: ${dictionary.name}
    password: ${dictionary.password}
    confirmation: ${dictionary.confirmation}
sessions:
  new:
    email: ${dictionary.email}
    password: ${dictionary.password}

The nice thing is that once compiled the translations are written back to the translation store so that all interpolation/recursive lookup happens once.

I know this might not answer the more philosophical questions about what the 'right' way to DRY up translations is, but I thought it's a better alternative to using the & label reference YML hack.

Solution 3

Improving of refactiring YAML files, especially for those who have many models:

ru:
  dictionary:
    name: &name "Имя"
    title_ru: &title_ru "Заголовок (ru)"
    title_en: &title_en "Заголовок (en)"
    content_ru: &content_ru "Содержание (ru)"
    content_en: &content_en "Содержание (en)"
    role: &role "Роль"
    created_at: &created_at "Создано в"
    updated_at: &updated_at "Обновлено в"
    published: &published "Опубликовано"

    nomination: &nomination
      name: *name
      title_ru: *title_ru
      title_en: *title_en

    post: &post
      content_ru: *content_ru
      content_en: *content_en
      published: *published

    dates: &dates
      created_at: *created_at
      updated_at: *updated_at

  activerecord:
    attributes:
      article:
        <<: *nomination
        <<: *post
        <<: *dates

      user:
        <<: *dates
        role: *role
        email: "Электропочта"

Userful link

Share:
10,250

Related videos on Youtube

Paul Fioravanti
Author by

Paul Fioravanti

Talks to computers.

Updated on September 14, 2022

Comments

  • Paul Fioravanti
    Paul Fioravanti over 1 year

    This StackOverflow question gave me food for thought on what is a good structure for Rails i18n files, so I thought I'd share another structure for refactoring Rails i18n yml files for your consideration/criticism.

    Given that I would like to

    1. keep the default app structure so I can use shorthand "lazy" lookups like t('.some_translation') in my views, as well as have an idea where translations are used in the app,
    2. avoid as much string repetition as possible, in particular with words that are not just the same, but also have identical contexts/meanings,
    3. only have to change a key once to have it reflected everywhere it's referenced,

    for a config/locales/en.yml file that looks something like this:

    activerecord:
      attributes:
        user:
          email: Email
          name: Name
          password: Password
          password_confirmation: Confirmation
      models:
        user: User
    users:
      fields:
        email: Email
        name: Name
        password: Password
        confirmation: Confirmation
    sessions:
      new:
        email: Email
        password: Password
    

    I can see that there is significant repetition, and that the context of words like "Email" and "Password" are unambiguous and have the same meaning in their respective views. It would be a bit annoying to have to go and change them all if I decide to change "Email" to "e-mail", so I'd like to refactor the strings to reference a dictionary of some sort. So, how about adding a dictionary hash to the top of the file with some & anchors like this:

    dictionary:
      email: &email Email
      name: &name Name
      password: &password Password
      confirmation: &confirmation Confirmation
    
    activerecord:
      attributes:
        user:
          email: *email
          name: *name
          password: *password
          password_confirmation: *confirmation
      models:
        user: User
    users:
      fields:  
        email: *email
        name: *name
        password: *password
        confirmation: *confirmation
    sessions:
      new:
        email: *email
        password: *password
    

    You could still continue to use static strings (eg "User" above), but whenever you get more than one instance of exactly the same word/phrase in your views, you could refactor it out to the dictionary. If the dictionary translation of a key in the base language doesn't make sense for a target language, then just change out the referenced value in the target language to a static string or add it as an extra entry to the target language's dictionary. I'm sure each language's dictionary could be refactored out into another file if they get too big and unwieldy (as long as it then gets reimported at the top of the translation file so the references work).

    This way of structuring i18n yaml files seems to work well with some local test apps I tried it on. I'm hoping the wonderful Localeapp will provide support for this kind of anchoring/referencing in the future. But anyway, all this dictionary talk can't possibly be an original idea, so are there other issues with anchor referencing in YAML, or maybe just with the whole "dictionary" concept in general? Or is it just better to just rip out the default backend entirely and replace it with Redis or something if you have needs beyond Rails default i18n conventions?

    Edit:

    I wanted to try and address tigrish's workflow example mentioned in a comment below up here, rather than as another comment below his answer. Please excuse me if I don't seem to be getting the points being made or if I'm just naive:

    Point 1: you have a general "name" attribute for ActiveRecord models, and they all just point to the generic dictionary for name:

    dictionary:
      name: &name Name
    
    activerecord:
      attributes:
        name: *name
        user:
          name: *name
        product:
          name: *name
    

    Point 2: Name for User model only needs to be changed. Other names stay the same.

    Option 1: Keep the model field names the same on the backend and just change the front end translation it points to.

    dictionary:
      name: &name Name
      full_name: &full_name Full Name
    
    activerecord:
      attributes:
        name: *name
        user:
          name: *full_name
        product:
          name: *name
    

    Option 2: Change the User model field name as well. This would require changing any references to this key in the code, and a change_table/rename_column migration.

    dictionary:
      name: &name Name
      full_name: &full_name Full Name
    
    activerecord:
      attributes:
        name: *name
        user:
          full_name: *full_name
        product:
          name: *name
    

    Option 3: If you want to be very thorough, refactor the information contained in a "name" out in to separate database/Activemodel fields, which would need new dictionary entries and a migration. You can decide on your views how you would want a "full name" to display:

    dictionary:
      name: &name Name
      name_prefix: &name_prefix Prefix
      first_name: &first_name First
      middle_name: &middle_name Middle
      last_name: &last_name Last
      name_suffix: &name_suffix Suffix
    
    activerecord:
      attributes:
        name: *name
        user:
          name_prefix: *name_prefix
          first_name: *first_name
          middle_name: *middle_name
          last_name: *last_name
          name_suffix: *name_suffix
        product:
          name: *name
    

    Point 3: Anyone for any reason needs a translation change, Marketing in this case. I'll follow on from Point 2 Option 1's example

    Option 1: Model field names same, just change the front end translation.

    dictionary:
      name: &name Name
      full_name: &full_name Full Name
      funky_name: &funky_name Ur Phunky Phresh Naym
    
    activerecord:
      attributes:
        name: *name
        user:
          name: *full_name
        product:
          name: *name
    sessions: # Sign up page keys
      new:
        name: *funky_name
    

    Option 2: "Funky name" desperately needs to be saved to the database, too, for some reason. Let's call it a username if no one objects (or funky_name if for some reason Marketing insists).

    dictionary:
      name: &name Name
      full_name: &full_name Full Name
      funky_name: &funky_name Ur Phunky Phresh Naym
    
    activerecord:
      attributes:
        name: *name
        user:
          name: *full_name
          username: *funky_name
        product:
          name: *name
    sessions: # Sign up page keys
      new:
        name: *name
        funky_name: *funky_name
    

    Right, so I admit that I have little idea what I'm doing, however, I'm willing to be shot down publicly in order to understand why this way of working with i18n in Haml is a bad idea in a Rails app. Difficult to read? Maintenance nightmare? Is it really considered 'hacking the file format' if I use (what I think is) a feature of the language?

    Thanks again to tigrish for driving me to get all this out.

  • Paul Fioravanti
    Paul Fioravanti almost 12 years
    Thank you for taking the time to write such a detailed response, especially since it was a question that you answered that prompted me to have a think about this. I thought of dictionaries because on projects I've worked on that used translation companies, there has always been "translation memories", which I assume is "reusable dictionary of some sort". I admit to having no idea whether this idea is compatible with other Rails i18n backends, but in your workflow, would you not just add dictionary entries for "full name" and "funky fresh name" and just have the appropriate keys reference it?
  • tigrish
    tigrish almost 12 years
    As far as I'm aware, the most common use of translation memory is for a translator to figure out how certain words should be translated in order to be consistent. Ex: when showing a control panel, should it be called "preferences" or "settings" or "control panel" etc... For me, this is more of a tool for translators as opposed to something that developers should be using to trim down their locale files. Again, the path to being succinct lies in developing standardized defaults for I18n.
  • tigrish
    tigrish almost 12 years
    To answer your question re: "full name", no you would hopefully not have to create new entries for them. This would mean changing your views and potentially stopping to use the built in helpers. Ideally we would only need to create translations with the correct keys for them to take precedence in the right places.
  • Paul Fioravanti
    Paul Fioravanti almost 12 years
    Thanks for your comments overall. If you feel like responding to my edit, then that would be great, otherwise, I think what you've written is definitely worth an accept. Cheers.
  • ZiggyTheHamster
    ZiggyTheHamster over 10 years
    I like this because it makes it so that when the contexts really do result in different words/phrases, you can do the right thing in the desired language. My use case would be for "gallons". I call this volume unit somewhere, but I have a few translations that say "gallons" explicitly and wanted to DRY that up.