114

I'd like to i18n a text that looks like this:

Already signed up? Log in!

Note that there is a link on the text. On this example it points to google - in reality it will point to my app's log_in_path.

I've found two ways of doing this, but none of them looks "right".

The first way I know involves having this my en.yml:

log_in_message: "Already signed up? <a href='{{url}}'>Log in!</a>"

And in my view:

<p> <%= t('log_in_message', :url => login_path) %> </p>

This works, but having the <a href=...</a> part on the en.yml doesn't look very clean to me.

The other option I know is using localized views - login.en.html.erb, and login.es.html.erb.

This also doesn't feel right since the only different line would be the aforementioned one; the rest of the view (~30 lines) would be repeated for all views. It would not be very DRY.

I guess I could use "localized partials" but that seems too cumberstone; I think I prefer the first option to having so many tiny view files.

So my question is: is there a "proper" way to implement this?

idmean
  • 14,540
  • 9
  • 54
  • 83
kikito
  • 51,734
  • 32
  • 149
  • 189
  • What about this? http://stackoverflow.com/questions/12334183/rails-i18n-better-way-of-interpolating-links – Mark Boulder Sep 19 '12 at 14:45
  • @Wuggy Foofie You should have not duplicated the question. And Simone's answer is better than the ones you got. – kikito Sep 19 '12 at 15:03

10 Answers10

198

en.yml

log_in_message_html: "This is a text, with a %{href} inside."
log_in_href: "link"

login.html.erb

<p> <%= t("log_in_message_html", href: link_to(t("log_in_href"), login_path)) %> </p>
MSC
  • 3,286
  • 5
  • 29
  • 47
Simone Carletti
  • 173,507
  • 49
  • 363
  • 364
  • 70
    In Rails 3 the syntax for this has changed to `%{href}` in the YAML translation string. Also, because output is automatically escaped, you need to either specify `raw` or `.html_safe` explicitly, or suffix your translation key with `_html`, as in `login_message_html` and escaping will be skipped automatically. – coreyward Jun 09 '11 at 00:16
  • 19
    just in case if it isn't obvious (and for those too lazy to check the edit log).. the [answer](http://stackoverflow.com/a/2544107/766570) above was already edited to include @coreyward's comment. – abbood Dec 09 '13 at 06:35
  • 4
    If you have anything more than a single word in the link text splitting translations like this will yield weird translations. For example "We have an amazing offering of assorted things that you can buy. You send the chopped up thing to a translator and you are likely to get two phrased that read like "We have an amazing whole bunch of items that you can buy" in other languages. Best to find a solution that doesn't split them apart. – trcarden Dec 10 '14 at 16:48
  • Note that your `t("what.ever")` reference must use double quotes. `t('what.ever')` won't work. – Archonic Jun 08 '15 at 22:13
  • 3
    @Archonic That's not true. `t('string')` is identical to `t("string")`. They're the same thing. – user229044 Jun 24 '15 at 18:29
  • @meagar I should have elaborated, the example I gave wasn't complete. Double quotes are required in ruby for string interpolation. If you're making use of `"something like #{this}"` in a translation, I found that the translation key also requires double quotes. – Archonic Jun 26 '15 at 18:16
  • 1
    @Archonic That's unrelated. We're talking about `%{}`, not `#{}`. Single or double quotes don't impact Rails' ability to interpolate variables into your locale files via `%{}`. – user229044 Jun 26 '15 at 18:25
  • 3
    I'm not sure why this has so many votes. The whole point of the question (or at least, questions like this) is to keep the single English sentence or expression as a unit in the i18n data so that translation can be done on it as a single entity. @trcarden's comment alludes to this issue. – Pistos Mar 24 '16 at 15:41
  • 4
    Got to love rails complicating the f out of links. should look like this `t('some.key', link: link_to()).html_safe` – Eddie Jul 24 '17 at 19:36
  • Seconding @Eddie above, the .html_safe piece seems necessary. – Ray Bradley Dec 17 '17 at 20:17
  • The naming of `log_in_href` is confusing. Is it a text or URL? – Weihang Jian Jun 19 '23 at 09:41
11

Separating text and link in locale.yml file works for a while but with longer text those are hard to translate and maintain as the link is in separate translation-item (as in Simones answer). If you start having many strings/translations with links you can dry it a bit more.

I made one helper in my application_helper.rb:

# Converts
# "string with __link__ in the middle." to
# "string with #{link_to('link', link_url, link_options)} in the middle."
def string_with_link(str, link_url, link_options = {})
  match = str.match(/__([^_]{2,30})__/)
  if !match.blank?
    raw($` + link_to($1, link_url, link_options) + $')
  else
    raise "string_with_link: No place for __link__ given in #{str}" if Rails.env.test?
    nil
  end
end

In my en.yml:

log_in_message: "Already signed up? __Log in!__"

And in my views:

<p><%= string_with_link(t('.log_in_message'), login_path) %></p>

This way it's easier to translate messages as also the link text is clearly defined in the locale.yml-files.

holli
  • 1,546
  • 16
  • 17
  • 6
    Great solution. I put this into a Gem, which allows you to define things link `This is a %{link:link to Google}`. It allows you to have multiple links in a single string, takes care of XSS and allowes nested translations. Have a look at https://github.com/iGEL/i18n_link/ – iGEL Jul 16 '11 at 07:55
  • i did it with "str = t str" so i just give the translate key in the function. more comfortable! – Tim Kretschmer Jul 17 '12 at 21:45
  • 1
    I would upvote @iGEL more if I could. The project has moved https://github.com/iGEL/it and if you want to use it in a controller for a `flash` message in Rails 3+ do it like this `view_context.it(key, ...)` – Chris Beck Feb 08 '14 at 18:23
  • Here is a better example for using it in a controller -- https://github.com/iGEL/it/issues/10 – Chris Beck Feb 08 '14 at 18:39
9

I took hollis solution and made a gem called it out of it. Let's look at an example:

log_in_message: "Already signed up? %{login:Log in!}"

And then

<p><%=t_link "log_in_message", :login => login_path %></p>

For more details, see https://github.com/iGEL/it.

amoebe
  • 4,857
  • 5
  • 37
  • 42
iGEL
  • 16,540
  • 11
  • 60
  • 74
5

In en.yml

registration:
    terms:
      text: "I do agree with the terms and conditions: %{gtc} / %{stc}"
      gtc: "GTC"
      stc: "STC"

In de.yml

registration:
    terms:
      text: "Ich stimme den Geschäfts- und Nutzungsbedingungen zu: %{gtc} / %{stc}"
      gtc: "AGB"
      stc: "ANB"

in new.html.erb [assumed]

<%= t(
   'registration.terms.text',
    gtc:  link_to(t('registration.terms.gtc'),  terms_and_conditions_home_index_url + "?tab=gtc"),
    stc: link_to(t('registration.terms.stc'), terms_and_conditions_home_index_url + "?tab=stc")
 ).html_safe %>
Emu
  • 5,763
  • 3
  • 31
  • 51
3

Thank you very much, holli, for sharing this approach. It works like a charm for me. Would vote you up if I could, but this is my first post so I'm lacking the proper reputation ... As an additional piece to the puzzle: The problem I realized with your approach is that it still won't work from inside the controller. I did some research and combined your approach with the one from Glenn on rubypond.

Here is what I came up with:

View helper, e.g. application_helper.rb

  def render_flash_messages
    messages = flash.collect do |key, value|
      content_tag(:div, flash_message_with_link(key, value), :class => "flash #{key}") unless key.to_s =~ /_link$/i
    end
    messages.join.html_safe
  end

  def flash_message_with_link(key, value)
    link = flash["#{key}_link".to_sym]
    link.nil? ? value : string_with_link(value, link).html_safe
  end

  # Converts
  # "string with __link__ in the middle." to
  # "string with #{link_to('link', link_url, link_options)} in the middle."
  # --> see http://stackoverflow.com/questions/2543936/rails-i18n-translating-text-with-links-inside (holli)
  def string_with_link(str, link_url, link_options = {})
    match = str.match(/__([^_]{2,30})__/)
    if !match.blank?
      $` + link_to($1, link_url, link_options) + $'
    else
      raise "string_with_link: No place for __link__ given in #{str}" if Rails.env.test?
      nil
    end
  end

In the controller:

flash.now[:alert] = t("path.to.translation")
flash.now[:alert_link] = here_comes_the_link_path # or _url

In the locale.yml:

path:
  to:
    translation: "string with __link__ in the middle"

In the view:

<%= render_flash_messages %>

Hope this post earns me the reputation to vote you up, holli :) Any feedback is welcome.

emrass
  • 6,253
  • 3
  • 35
  • 57
2

We had the following:

module I18nHelpers
  def translate key, options={}, &block
    s = super key, options  # Default translation
    if block_given?
      String.new(ERB::Util.html_escape(s)).gsub(/%\|([^\|]*)\|/){
        capture($1, &block)  # Pass in what's between the markers
      }.html_safe
    else
      s
    end
  end
  alias :t :translate
end

or more explicitly:

module I18nHelpers

  # Allows an I18n to include the special %|something| marker.
  # "something" will then be passed in to the given block, which
  # can generate whatever HTML is needed.
  #
  # Normal and _html keys are supported.
  #
  # Multiples are ok
  #
  #     mykey:  "Click %|here| and %|there|"
  #
  # Nesting should work too.
  #
  def translate key, options={}, &block

    s = super key, options  # Default translation

    if block_given?

      # Escape if not already raw HTML (html_escape won't escape if already html_safe)
      s = ERB::Util.html_escape(s)

      # ActiveSupport::SafeBuffer#gsub broken, so convert to String.
      # See https://github.com/rails/rails/issues/1555
      s = String.new(s)

      # Find the %|| pattern to substitute, then replace it with the block capture
      s = s.gsub /%\|([^\|]*)\|/ do
        capture($1, &block)  # Pass in what's between the markers
      end

      # Mark as html_safe going out
      s = s.html_safe
    end

    s
  end
  alias :t :translate


end

then in ApplicationController.rb just

class ApplicationController < ActionController::Base
  helper I18nHelpers

Given a key in the en.yml file like

mykey: "Click %|here|!"

can be used in ERB as

<%= t '.mykey' do |text| %>
  <%= link_to text, 'http://foo.com' %>
<% end %>

should generate

Click <a href="http://foo.com">here</a>!
Jaime Cham
  • 1,494
  • 1
  • 15
  • 16
1

I wanted a bit more flexibility than just adding links to flash messages from YAML files (for example the logged in username etc.) so instead I wanted to use ERB notation in the string.

As I am using bootstrap_flash so I modified the helper code as follows to decode the ERB strings before displaying:

require 'erb'

module BootstrapFlashHelper
  ALERT_TYPES = [:error, :info, :success, :warning] unless const_defined?(:ALERT_TYPES)

  def bootstrap_flash
    flash_messages = []
    flash.each do |type, message|
      # Skip empty messages, e.g. for devise messages set to nothing in a locale file.
      next if message.blank?

      type = type.to_sym
      type = :success if type == :notice
      type = :error   if type == :alert
      next unless ALERT_TYPES.include?(type)

      Array(message).each do |msg|
        begin
          msg = ERB.new(msg).result(binding) if msg
        rescue Exception=>e
          puts e.message
          puts e.backtrace
        end
        text = content_tag(:div,
                           content_tag(:button, raw("&times;"), :class => "close", "data-dismiss" => "alert") +
                           msg.html_safe, :class => "alert fade in alert-#{type}")
        flash_messages << text if msg
      end
    end
    flash_messages.join("\n").html_safe
  end
end

It is then possible to use strings like the following (using devise):

signed_in: "Welcome back <%= current_user.first_name %>! <%= link_to \"Click here\", account_path %> for your account."

This may not work for all situations and there may be an argument that code and string definitions should not be mixed (especially from a DRY perspective), but this seems to work well for me. The code should be adaptable for many other situations, the important bits being the following:

require 'erb'

....

        begin
          msg = ERB.new(msg).result(binding) if msg
        rescue Exception=>e
          puts e.message
          puts e.backtrace
        end
zelanix
  • 3,326
  • 1
  • 25
  • 35
-1

If your link is constant, you can use something like this:

description: "Text <a href=https://example.com target=_blank >Link description</a>.

and then use html_safe in your views:

t(".description").html_safe
alextriam
  • 1
  • 2
-2

I think a simple way to do this is by simply doing :

<%= link_to some_path do %>
<%= t '.some_locale_key' %>
<% end %>
Sankalp Singha
  • 4,461
  • 5
  • 39
  • 58
-4

Why not use the first way, but splitting it up like

log_in_message: Already signed up?
log_in_link_text: Log in!

And then

<p> <%= t('log_in_message') %> <%= link_to t('log_in_link_text'), login_path %> </p>
alex.zherdev
  • 23,914
  • 8
  • 62
  • 56
  • Sorry, this solution will not work. Keep in mind that I wanted to translate the text to other languages. This means that in some occassions the "link" could be at the beginning, or in the middle of the text. Your solution forces the link to be at the end (doesn't translate well). – kikito Mar 30 '10 at 09:44