Rails I18n: calling different logic based on locale - ruby-on-rails

I have a custom helper method which outputs the percentage saved. For example it will calculate the discount for an item and output "20 percent off".
I am localizing the site to Chinese, and in Chinese the same discount is expressed differently. "20% off" is expressed as "8 Cut" or "80% of original price". Since these two expression are quite different, I think I need to write two versions of the helper method.
Currently I wrote it like this, checking locale in the helper itself:
def percent_off(discount, locale=I18n.locale)
if not locale.to_s.include?('zh')
n = ((1 - (discount.preferential_price / discount.original_price)) * 100) .to_i
"#{n}% off"
else
# Chinese
n = ((discount.preferential_price / discount.original_price) * 10) .to_i
"#{n} cut"
end
end
Is there a better way to do this?

You might want to refactor your code so that the calculation of the number used to represent the discount is separated from the selection/creation of your localized message.
Here is an idea along those lines:
In your en.yml:
# value for "n% off"
discount_msg: "%{n}% off!"
In your zh.yml:
# value for "(100-n)% of original price"
discount_msg: "%{n}% of original price (in Chinese)"
Next refactor the percent_off helper method so that it only calculates the correct value discount value depending upon the implied locale:
def percent_off(discount)
n = ((1 - (discount.preferential_price / discount.original_price)) * 100) .to_i
if I18n.locale.to_s.include?('zh')
n = 100 - n
end
n
end
Then, you could invoke the above like this:
I18n.t('discount_msg', :n=>percent_off(discount))

The only bad thing gere in your helper I can see is that you write percent_off discount, but it's not evident what it'll return ( so I would create 2 different helper methods here.
Is I noted using locale check in views doesn't look pretty when you view blocks became completely different for different translations, so I've created an i18n helper method for that purpose:
module ApplicationHelper
def translate_to(locales, &blk)
locales = [locales.to_s] if !locales.is_a?(Array)
yield if locales.include?(I18n.locale.to_s)
end
alias :tto :translate_to
end
and in views:
<% tto :en do %>
<% # some en-translated routine %>
<% end %>
<% tto :zh do %>
<% # zh-translated routine %>
<% end %>
Not sure it's a best way to manage translated blocks but I found it useful ))

Related

Calculate the sum in an loop

I'm currently working on a rails project, I'm kinda newbie in this language.
Using Devise, I want to have the sum of the sign_in_count and display it.
My sign_in_count is an integer and have a default value at 0, incrementing every time the user sign in.
here's what I've tried so far :
count_sign_in = 0
#users.each do |user|
count_sign_in << user.sign_in_count
end
But as you can imagine, it doesn't work ...
And I want to have the sum per week and per month if that's possible.
Any help ?
Many thanks.
I'd use a single SQL query
User.sum(:sign_in_count)
correct your code:
count_sign_in = 0
#users.each do |user|
count_sign_in += user.sign_in_count # similar to count_sign_in = count_sign_in + user.sign_in_count
end
Note: count_sign_in is not an array, to push element in array we use << in ruby
As already answered above, the easiest way to do this is to use an sql aggregation.
User.sum(:sign_in_count)
You also perform a sum on a collection of object. In this case an #each iteration is not the optimale as it require an extra locale variable. The Enumerable module provides a bunch of useful methods like sum
#users.sum { |user| user.sign_in_count }
It can also be written shorten using a symbol to proc
#users.sum(&:sign_in_count)

Set unit for number_to_currency with a user setting?

I want to allow users to change the currency unit throughout their account.
The obvious way to do it is to pass the unit parameter to number_to_currency, but given number_to_currency is used hundreds of times throughout the app, it seems a little repetitive to do that.
So is there some way to change what unit is used for all instances of number_to_currency based on a setting stored in the database for each user?
Sounds to me like you need some sort of global function / variable to define the symbol
I would do it like this:
#app/helpers/application_helper.rb
def unit
User.find(current_user.id).select(:currency_type) #I don't know how your units are stored - you may need logic to return the correctly formatted unit
end
This will allow you to call: <%= number_to_currency, unit: unit %>
Overriding Helper Method
number_to_currency is literally just a helper itself, which means you can append options on the fly:
Original
# File actionpack/lib/action_view/helpers/number_helper.rb, line 106
def number_to_currency(number, options = {})
return unless number
options = escape_unsafe_delimiters_and_separators(options.symbolize_keys)
wrap_with_output_safety_handling(number, options.delete(:raise)) {
ActiveSupport::NumberHelper.number_to_currency(number, options)
}
end
Amended
#app/helpers/application_herlper.rb
def number_to_currency(number, options ={})
unit = User.find(current_user.id).select(:currency_type)
options[:unit] = unit unless options[:unit].present?
super
end
You could pass the currency as an option to the number_to_currency method as shown:
number_to_currency(1234567890.506, locale: :fr)
In that case you would need to replace :fr to whatever points to the user's setting and to create such a locale with such options:
number:
currency:
format:
unit: '€'
format: '%n %u'
separator: ","
delimiter: "."
precision: 2
significant: false
strip_insignificant_zeros: false
Or you set the unit in another way:
number_to_currency(1234567890.50, unit: "£", format: "%u %n")
=> "£ 1.234.567.890,50"
Hope this helps you.

Define and use array of text values corresponding to numeric values

I'm new to the Ruby on Rails environment, so I am stuck on what might be a simple question: I'm looking to define some text strings/labels that correspond to a numeric value. These values will be stored in the database and then used in my code instead of the numeric values.
In C, I would to something like this:
#define Accounting 0
#define Engineering 1
#define Education 2
...to be used like this:
if (field_of_study == Accounting) ...
I want to be able to do this in Rails controllers/views. I currently have to do something like this in my views to display items:
<tr>
<td><%= link_to user.name, user %></td>
<% if user.studyField == 0 %>
<td>Accounting</td>
<% elsif user.studyField == 1 %>
<td>Engineering</td>
<% elsif user.studyField == 2 %>
<td>Education</td>
<% end %>
</tr>
I would also like to use the text strings/labels in a drop-down menu in the form_for form and then save it using the numeric identifier. Do I need a before_save method to translate the two or is their an automatic way of doing this?
You might find this helpful: Ruby on Rails: Where to define global constants?.
In Rails, since all models are autoloaded by default, you might find it convenient to define your constants in the models, as follows
class User < ActiveRecord::Base
ACCOUNTING = 0
ENGINEERING = 1
EDUCATION = 2
end
or even
class User < ActiveRecord::Base
FIELDS = { accounting: 0, engineering: 1, education: 2 }
end
These can be used anywhere with User::ACCOUNTING or User::FIELDS[:accounting]. To use the second version inside a form, you can use
select('user', 'study_field', User::FIELDS)
Refer to select for more details.
There are a couple of ways to do this. You can assign the constants to integers and they should be saved to the database as integers:
# config/initializers/constants.rb
Accounting = 0
Engineering = 1
This is a bit ugly because Accounting is literally equal to zero. In Rails console:
Accounting == 0
=> true
However, this is probably the most straightforward way to meet your requirement and it looks like this is how your approached the problem with C.

How can I get will_paginate to correctly display the number of entries?

WillPaginate has a page_entries_info view helper to output text like "Displaying contracts 1 - 35 of 4825 in total".
However, I'm finding that when I try to use it like this...
= page_entries_info #contracts
It outputs...
Displaying Contract 1 - 35 of 4825 in total
(It outputs the singular name of the model, rather than pluralized, all lower case.)
Do I need to feed it some other param?
I tried page_entries_info #contracts, :model => Contract but got the same result.
I'm using version 3.0.3 -- the current version.
Incidentally, can someone point me to the API docs for WillPaginate?
will_paginate API docs: https://github.com/mislav/will_paginate/wiki/API-documentation
Short answer
You can specify the model option as a string, which will be correctly pluralized.
page_entries_info #contracts, :model => 'contract'
# Displaying contracts 1 - 35 of 4825 in total
Longer answer
The will_paginate docs suggest that you use the i18n mechanism for customizing output. This is kind of a pain since AFAICT you have to write out singular and plural form for all of your models in the config/locales/*.yml files (e.g., en.yml), and the %{foo}-style syntax doesn't seem to be ERB, but just placeholders, so you can't do things like %{foo.downcase}.
If you write your own helper, you get complete control over the output. For example:
def my_page_info(collection)
model_class = collection.first.class
if model_class.respond_to? :model_name
model = model_class.model_name.human.downcase
models = model.pluralize
if collection.total_entries == 0
"No #{models} found."
elsif collection.total_entries == 1
"Found one #{model}."
elsif collection.total_pages == 1
"Displaying all #{collection.total_entries} #{models}"
else
"Displaying #{models} #{collection.offset + 1} " +
"to #{collection.offset + collection.length} " +
"of #{number_with_delimiter collection.total_entries} in total"
end
end
end
# Displaying contracts 1 - 35 of 4,825 in total

How to refactor this Ruby (controller) code?

This is the code in my reports controller, it just looks so bad, can anyone give me some suggestions on how to tidy it up?
# app\controller\reports_controller.rb
#report_lines = []
#sum_wp, #sum_projcted_wp, #sum_il, #sum_projcted_il, #sum_li,#sum_gross_profit ,#sum_opportunities = [0,0,0,0,0,0,0]
date = #start_date
num_of_months.times do
wp,projected_wp, invoice_line,projected_il,line_item, opp = Report.data_of_invoicing_and_delivery_report(#part_or_service,date)
#sum_wp += wp
#sum_projcted_wp +=projected_wp
#sum_il=invoice_line
#sum_projcted_il +=projected_il
#sum_li += line_item
gross_profit = invoice_line - line_item
#sum_gross_profit += gross_profit
#sum_opportunities += opp
#report_lines << [date.strftime("%m/%Y"),wp,projected_wp ,invoice_line,projected_il,line_item,gross_profit,opp]
date = date.next_month
end
I'm looking to use some method like
#sum_a,#sum_b,#sum_c += [1,2,3]
My instant thought is: move the code to a model.
The objective should be "Thin Controllers", so they should not contain business logic.
Second, I like to present my report lines to my Views as OpenStruct() objects, which seems cleaner to me.
So I'd consider moving this accumulation logic into (most likely) a class method on Report and returning an array of "report line" OpenStructs and a single totals OpenStruct to pass to my View.
My controller code would become something like this:
#report_lines, #report_totals = Report.summarised_data_of_inv_and_dlvry_rpt(#part_or_service, #start_date, num_of_months)
EDIT: (A day later)
Looking at that adding accumulating-into-an-array thing, I came up with this:
require 'test/unit'
class Array
def add_corresponding(other)
each_index { |i| self[i] += other[i] }
end
end
class TestProblem < Test::Unit::TestCase
def test_add_corresponding
a = [1,2,3,4,5]
assert_equal [3,5,8,11,16], a.add_corresponding([2,3,5,7,11])
assert_equal [2,3,6,8,10], a.add_corresponding([-1,-2,-2,-3,-6])
end
end
Look: a test! It seems to work OK. There are no checks for differences in size between the two arrays, so there's lots of ways it could go wrong, but the concept seems sound enough. I'm considering trying something similar that would let me take an ActiveRecord resultset and accumulate it into an OpenStruct, which is what I tend to use in my reports...
Our new Array method might reduce the original code to something like this:
totals = [0,0,0,0,0,0,0]
date = #start_date
num_of_months.times do
wp, projected_wp, invoice_line, projected_il, line_item, opp = Report.data_of_invoicing_and_delivery_report(#part_or_service,date)
totals.add_corresponding [wp, projected_wp, invoice_line, projected_il, line_item, opp, invoice_line - line_item]
#report_lines << [date.strftime("%m/%Y"),wp,projected_wp ,invoice_line,projected_il,line_item,gross_profit,opp]
date = date.next_month
end
#sum_wp, #sum_projcted_wp, #sum_il, #sum_projcted_il, #sum_li, #sum_opportunities, #sum_gross_profit = totals
...which if Report#data_of_invoicing_and_delivery_report could also calculate gross_profit would reduce even further to:
num_of_months.times do
totals.add_corresponding(Report.data_of_invoicing_and_delivery_report(#part_or_service,date))
end
Completely un-tested, but that's a hell of a reduction for the addition of a one-line method to Array and performing a single extra subtraction in a model.
Create a summation object that contains all those fields, pass the entire array to #sum.increment_sums(Report.data_of...)

Resources