I am currently working on a project to internationalize one of our ruby-on-rails web applications so that it can be used in other countries (France will be the first one in this case).
A particular issue I haven't worked out yet is with the displaying of numeric fields. When displaying numbers for display purposes only, I do the following:
<%= number_to_percentage(tax.rate, :precision => 2)%>
In English, this shows 17.50, but in French it shows 17,50 (with a comma in place of the decimal point) which is as expected. The problem comes in the Edit form, when I show a text field
<%= f.text_field :rate, :size => 15 %>
When this renders a text box on the screen, the text box always shows 17.50 with a full stop rather than a comma for French. I am not sure that is correct.
When I tried doing the following:
<%= f.text_field :rate, :size => 15, :value => number_with_precision(f.object.rate, :precision => 2) %>
This did indeed show 17,50 in the text box for French, but when I click on the Update button to save the form, the Ruby validation kicks in and tells me that 17,50 is not a number (or rather it says "n'est pas un nombre"). I have to enter 17.50 to get it to save.
To be honest, I am not entirely sure on the correct thing to do here. Should all countries enter numbers with full stops in text boxes, or is there a way to get Ruby-on-Rails to display commas, and validate them appropriately?
TL;DR
This is the kind of things I hate to do over and over again (I'm serving french users, they're easily confused with dots as the decimal separator).
I exclusively use the delocalize gem now, which does the format translation automatically for you. You just have to install the gem and leave your forms as-is, everything should be taken care of for you.
I like to read, give me the long explanation
The basic conversion is quite simple, you have to convert back and forth between the following formats:
The backend one, which is usually English, used by your persistent storage (SQL database, NoSQL store, YAML, flat text file, whatever struck your fancy, ...).
The frontend one, which is whatever format your user prefers. I'm going to use French here to stick to the question*.
* also because I'm quite partial towards it ;-)
This means that you have two points where you need to do a conversion:
Outbound: when outputting your HTML, you will need to convert from English to French.
Inbound: When processing the result of the form POST, you will need to convert back from French to English.
The manual way
Let's say I have the following model, with the rate field as a decimal number with a precision of 2 (eg. 19.60):
class Tax < ActiveRecord::Base
# the attr_accessor isn't really necessary here, I just want to show that there's a database field
attr_accessor :rate
end
The outbound conversion step (English => French) can be done by overriding text_field_tag:
ActionView::Helpers::FormTagHelper.class_eval do
include ActionView::Helpers::NumberHelper
alias original_text_field_tag text_field_tag
def text_field_tag(name, value = nil, options = {})
value = options.delete(:value) if options.key?(:value)
if value.is_a?(Numeric)
value = number_with_delimiter(value) # this method uses the current locale to format our value
end
original_text_field_tag(name, value, options)
end
end
The inbound conversion step (French => English) will be handled in the model. We will override the rate attribute writer to replace every French separator with the English one:
class Tax < ActiveRecord::Base
def rate=(rate)
write_attribute(:rate, rate.gsub(I18n.t('number.format.separator'), '.')
end
end
This look nice because there's only one attribute in the example and one type of data to parse, but imagine having to do this for every number, date or time field in your model. That's not my idea of fun.
This also is a naïve* way of doing the conversions, it does not handle:
Dates
Times
Delimiters (eg. 100,000.84)
* did I already tell you I like French?
Enter delocalize
Delocalize is working on the same principle I outlined above, but does the job much more comprehensively:
It handles Date, Time, DateTime and numbers.
You don't have to do anything, just install the gem. It checks your ActiveRecord columns to determine if it's a type that needs conversion and does it automatically.
The number conversions are pretty straightforward, but the date ones are really interesting. When parsing the result of a form, it will try the date formats defined in your locale file in descending order and should be able to understand a date formatted like this: 15 janvier 2012.
Every outbound conversion will be done automatically.
It's tested.
It's active.
One caveat though: it doesn't handle client-side validations. If you're using them, you will have to figure out how to use i18n in your favourite JavaScript framework.
This is the gsub technique :
In your model :
before_validation :prepare_number
def prepare_number
self.rate.gsub(/,/, '.') if self.rate.match /,\S/
end
Related
My question is closely related to this one Rails friendly id with non-Latin characters. Following the suggested answer there, I implemented a little bit different solution (I know, it's primitive, but I just want to make sure it works before adding complex behavior).
In my user model I have:
extend FriendlyId
friendly_id :slug_candidates, :use => [:slugged]
def slug_candidates
[
[:first_name, :last_name],
[:first_name, :last_name, :uid]
]
end
def should_generate_new_friendly_id?
first_name_changed? || last_name_changed? || uid_changed? || super
end
def normalize_friendly_id(value)
ERB::Util.url_encode(value.to_s.gsub("\s","-"))
end
now when I submit "مرحبا" as :first_name through the browser, slug value is set to "%D9%85%D8%B1%D8%AD%D8%A8%D8%A7-" in the database, which is what I expect (apart from the trailing "-").
However the url shown in the browser looks like this: http://localhost:3000/en/users/%25D9%2585%25D8%25B1%25D8%25AD%25D8%25A8%25D8%25A7- , which is not what I want. Does anyone know where these extra %25s are coming from and why?
Meanwhile, I came a bit further, so I put my solution here maybe it could be helpful for someone else.
The 25s in the url seem to be the result of url_encoding the '%' in my slug. I don't know where this happens, but I modified my normalize_friendly_id function, so that it doesn't affect me anymore. Here it is:
def normalize_friendly_id(value)
sep = '-'
#strip out tashkeel etc...
parameterized_string = value.to_s.gsub(/[\u0610-\u061A\u064B-\u065F\u06D6-\u06DC\u06DF-\u06E8\u06EA-\u06ED]/,''.freeze)
# Turn unwanted chars into the separator
parameterized_string.gsub!(/[^0-9A-Za-zÀ-ÖØ-öø-ÿ\u0620-\u064A\u0660-\u0669\u0671-\u06D3\u06F0-\u06F9\u0751-\u077F]+/,sep)
unless sep.nil? || sep.empty?
re_sep = Regexp.escape(sep)
# No more than one of the separator in a row.
parameterized_string.gsub!(/#{re_sep}{2,}/, sep)
# Remove leading/trailing separator.
parameterized_string.gsub!(/^#{re_sep}|#{re_sep}$/, ''.freeze)
end
parameterized_string.downcase
end
Some comments on that:
I took only Latin and Arabic alphabets into account
I decided that if I allowed arabic characters in the url, then there is no sense to keep the friendly_id behavior of converting e.g. "ü" to "ue", "ö" to "oe", etc. So I leave such characters in the url.
I tried also to keep characters which might not be used in Arabic, but in other languages which use the Arabic alphabet such as Farsi or Urdu. I speak Arabic only, so I did a guess of which characters might be regarded as regular in other languages. For example is "ڿ" a regular character in any language? I have no idea, but I guess it could well be.
again, since I speak arabic, I stripped the "Tashkil" out of the text. I would say, that texts without tashkil are in general easier to read than the ones with. However, I don't know if I should take care of some similar stuff in other languages. Any hints are much appreciated.
Last: adding another alphabet would be as easy as adding the appropriate sequences to the regex. One only needs to know which characters should be white-listed.
I appreciate any comments or improvement suggestions.
Given the phrase:
"You've successfully donated €10 to Doctors of the World"
I wanted to dynamically generate this text for different amounts and charity names, which i did using
_("You've donated %{amount} to %{charity_name}")
Where charity_name comes from a list of possible charities and each charity is a hash with data about the charity.
I'm not a french speaker and only learnt basic french in school but the problem with doing this (which is probably immediately obvious to any speaker of a language with gendered articles) is that the "to" part of the translation can take various forms a la, au, a'l or aux depending on the noun.
eg "Vous souhaitez donner 10€ aux Médecins du Monde"
What is the best way to handle this using gettext, given that this will need to be scaled to other languages? There are only a few cases where this will need to be done because most cases of dynamic text (99%+ can be handled fine with parameters.
I've thought of 3 ways to do this:
1) Have highly dynamical text such as this generated from a function, one per message per language as necessary. The function accepts an amount and charity name as a parameter and returns the translated text.
2) Manually add a translation for "to " for each charity and use that in place of %{charity_name} and then get the translation from the po file.
3) Add an entry in each charity hash specifying the form of the "to " eg the hash for les Médecins du Monde would also store aux Médecins du Monde.
Are any of these methods viable or is there a better alternative I'm not thinking off?
May be not the best approach but I've used this for several times.
Consider a table with below fields:
id, name_en, name_ru, created_at, updated_at
I assume that you use I18n to get language parameter.
controller
def index
#lang = params[:locale] || I18n.locale
#examples = Example.all
end
view
<% #examples.each do |ex| %>
<li>
<%= ex.send("name_#{#lang}") %>
</li>
<% end %>
Above code will present name_ru (name in russian) or name_en (in english) based of I18n.locale
So I am building an on-line shop and I want two language options, English and Spanish.
I am using I18n as you would normally do for all my static text and headings ect.
But, I have a products Model that can have new Products created for listing on the site. This has fields like :name_en and :name_es, :description_en and :description_es ect.
When the admin uploads a new product they obviously need to add the English and the Spanish text.
Because I have only 2 locales what I would like to do i think is call something like
<%= Product.name_"#{I18n.locale.downcase}" %>
But obviously this does not work. How can i, or just can I, interpolate a method or Attribute?
Have I missed something obvious here and just going about it the wrong way or is there a way to do this along the lines of my thinking?
Any Help massively appreciated.
Thanks
You can use send method. Try something like:
<%= Product.send("name_#{I18n.locale.downcase}") %>
Just a word of explanation, the following are equal:
string = "Hello"
string.upcase
# => "HELLO"
string.send("upcase")
# => "HELLO"
Hope that puts you in proper direction!
Good luck!
I've spent most of the day trying to localize dates in my application.
Reading this http://guides.rubyonrails.org/i18n.html it seems that I18n.l(date) is the way to do it.
This is all very well if all I want to do is render date objects directly in my view. However a lot of the time I want to render a date in a form field:
= form_for #object do |f|
.field
= f.label :date
= f.text_field :date
This seems to call to_s on the date object and use that, with no localization.
The first workaround I tried was to monkey patch the date class to use I18n.l:
class Date
def to_s
I18n.l(self)
end
end
This seemed nice as I wouldn't have to remember to call I18n.l each time I render a date. However doing it this way breaks all my database queries as locale specific formats do not make sense in a query string!
To fix this I added extra logic to the patch:
class Date
def to_s(type = nil)
if(type == :db)
self.strftime("%Y-%m-%d")
else
I18n.l(self)
end
end
end
However this STILL does not fit - because when using dates for validation errors active record seems to use the string value sent to the db.
Can anyone share with me how you would localize date formats consistently across your views, form builders and active record error messages?
Thanks for any help
Overloading Date's to_s method is a big no-no for me.
I can't think of a magical solution that formats dates differently depending on the context and manages to do that reliably ;)
I'd suggest continuing to specify the date formats only where you need them and possibly overwriting the date.formats.default translation in your locale file to make the format more palatable to your users (this should also fix the formatting in your text_field).
I'm working with an app for a concert tour website, where all times (announcement times, on-sale start times, and event start times) are local to each particular venue's time zone. I take the user entered date/time where applicable and run a before_filter to set the appropriate time zone so that everything is stored in the database in UTC. For the 'new' form and for displaying the times in index and show actions, no problem at all. When the data is brought back out of the database and into a view, I use in_time_zone to adjust per the particular venue.
The only issue is on the edit form. The date/time select is showing the data in UTC. When I'm working on the site, I mentally adjust, but for others it's confusing. I'd like to do something along the lines of:
<%= f.datetime_select :start_datetime.in_time_zone(#event.time_zone) %>
Or, in the controller:
def edit
#performance = Performance.find(params[:id])
#event = #performance.event
#performance.start_datetime = #performance.start_datetime.in_time_zone(#event.time_zone)
end
Then simply, <%= f.datetime_select :start_datetime %>.
Unfortunately, I haven't found the right way to do this. Do you have any ideas worth giving a shot?
Thank you very much.
You may use default method of datetime_select, as following:
%br
= f.label :req_sess_start, "Session starts at"
= f.datetime_select(:req_sess_start, :start_year => 2010, :ampm => true, :default => 0.days.from_now.in_time_zone(#timezone))
Because of the default value shown, client will assume that times has to be entered in his/her local timezone, but...
this value will be actually in the timezone default to your application (as provided in application.rb, and the default is UTC).
So, you would require some server side coding to convert it to correct value.
I'm not sure if I understand what you want to do, but since you're storing the #event.time_zone, you could add
:default => start_time.in_time_zone(#event.time_zone)
to your datetime_select form field.
How about something like this:
# change PST to correct zone abbrev.
new_zone = ActiveSupport::TimeZone.new("PST")
#performance.start_datetime = #performance.start_datetime.in_time_zone(new_zone)
I just noticed this is an old post but:
If I were you, I would use a virtual attribute to represent the date time of the venue. For instance, you could add an attribute to performance called adjusted_start_time.