Rails I18n put span around translated text if fallback was used - ruby-on-rails

I'm using Rails built in I18n (Simple backend). I've set the default locale to :en and enabled fallbacks. Let's say I have translations for a specific item in English and Spanish. Now a German visitor comes to my site and it falls back to English. How would I go about detecting that fallback and wrapping it in a span?
<span class="fallback">Hello</span> instead of just Hello
This way I could then use client side machine translations.
I'm hoping to avoid writing my own backend to replace "Simple".

Had to resort to over-riding the translate function in I18n::Backend::FallBacks
https://github.com/svenfuchs/i18n/blob/master/lib/i18n/backend/fallbacks.rb
module I18n
module Backend
module Fallbacks
def translate(locale, key, options = {})
return super if options[:fallback]
default = extract_non_symbol_default!(options) if options[:default]
options[:fallback] = true
I18n.fallbacks[locale].each do |fallback|
catch(:exception) do
result = super(fallback, key, options)
if locale != fallback
return "<span class=\"translation_fallback\">#{result}</span>".html_safe unless result.nil?
else
return result unless result.nil?
end
end
end
options.delete(:fallback)
return super(locale, nil, options.merge(:default => default)) if default
throw(:exception, I18n::MissingTranslation.new(locale, key, options))
end
end
end
end
I just put this code in an initializer.
It feels very messy to me... I would still love to mark someone else's better answer as correct.

A better solution, using the metadata module from I18n. Also logs in a private log file to help spot missing translations. You can replace the calls with Rails.logger or remove them.
I18n::Backend::Simple.include(I18n::Backend::Metadata)
# This work with <%= t %>,but not with <%= I18n.t %>
module ActionView
module Helpers
module TranslationHelper
alias_method :translate_basic, :translate
mattr_accessor :i18n_logger
def translate(key, options = {})
#i18n_logger ||= Logger.new("#{Rails.root}/log/I18n.log")
#i18n_logger.info "Translate key '#{key}' with options #{options.inspect}"
options.merge!(:rescue_format => :html) unless options.key?(:rescue_format)
options.merge!(:locale => I18n.locale) unless options.key?(:locale)
reqested_locale = options[:locale].to_sym
s = translate_basic(key, options)
if s.translation_metadata[:locale] != reqested_locale &&
options[:rescue_format] == :html && Rails.env.development?
#i18n_logger.error "* Translate missing for key '#{key}' with options #{options.inspect}"
missing_key = I18n.normalize_keys(reqested_locale, key, options[:scope])
#i18n_logger.error "* Add key #{missing_key.join(".")}\n"
%(<span class="translation_fallback" title="translation fallback #{reqested_locale}->#{s.translation_metadata[:locale]} for '#{key}'">#{s}</span>).html_safe
else
s
end
end
alias :t :translate
end
end
end
Then style with CSS
.translation_fallback {
background-color: yellow;
}
.translation_missing {
background-color: red;
}

Related

Rails 3 - default_url_options based on url being generated

In rails 3, is it possible to gain access to the controller/action of the URL being generated inside of default_url_options()? In rails 2 you were passed a Hash of the options that were about to be passed to url_for() that you could of course alter.
E.g. Rails 2 code:
==== config/routes.rb
map.foo '/foo/:shard/:id', :controller => 'foo', :action => 'show'
==== app/controllers/application.rb
def default_url_options options = {}
options = super(options)
if options[:controller] == 'some_controller' and options[:id]
options[:shard] = options[:id].to_s[0..2]
end
options
end
==== anywhere
foo_path(:id => 12345) # => /foo/12/12345
However, in rails 3, that same code fails due to the fact that default_url_options is not passed any options hash, and I have yet to find out how to test what the controller is.
FWIW, the above "sharding" is due to when you turn caching on, if you have a large number of foo rows in your DB, then you're going to hit the inode limit on unix based systems for number of files in 1 folder at some point. The correct "fix" here is to probably alter the cache settings to store the file in the sharded path rather than shard the route completely. At the time of writing the above code though, part of me felt it was nice to always have the cached file in the same structure as the route, in case you ever wanted something outside of rails to serve the cache.
But alas, I'd still be interested in a solution for the above, purely because it's eating at me that I couldn't figure it out.
Edit: Currently I have the following which I'll have to ditch, since you lose all other named_route functionality.
==== config/routes.rb
match 'foo/:shard/:id' => 'foo#show', :as => 'original_foo'
==== app/controllers/application.rb
helpers :foo_path
def foo_path *args
opts = args.first if opts.is_a?(Array)
args = opts.merge(:shard => opts[:id].to_s[0..2]) if opts.is_a?(Hash) and opts[:id]
original_foo_path(args)
end
define a helper like
# app/helpers/foo_helper.rb
module FooHelper
def link_to name, options = {}, &block
options[:shard] = options[:id].to_s[0..1] if options[:id]
super name, options, &block
end
end
and then do the following in your view, seems to work for me
<%= link_to("my shard", id: 12345) %>
edit: or customize the foo_path as
module FooHelper
def link_to name, options = {}, &block
options[:shard] = options[:id].to_s[0..1] if options[:id]
super name, options, &block
end
def foo_path options = {}
options[:shard] = options[:id].to_s[0..1] if options[:id]
super options
end
end

Papertrail and Carrierwave

I have a model that use both: Carrierwave for store photos, and PaperTrail for versioning.
I also configured Carrierwave for store diferent files when updates (That's because I want to version the photos) with config.remove_previously_stored_files_after_update = false
The problem is that PaperTrail try to store the whole Ruby Object from the photo (CarrierWave Uploader) instead of simply a string (that would be its url)
(version table, column object)
---
first_name: Foo
last_name: Bar
photo: !ruby/object:PhotoUploader
model: !ruby/object:Bla
attributes:
id: 2
first_name: Foo1
segundo_nombre: 'Bar1'
........
How can I fix this to store a simple string in the photo version?
You can override item_before_change on your versioned model so you don't call the uploader accesor directly and use write_attribute instead. Alternatively, since you might want to do that for several models, you can monkey-patch the method directly, like this:
module PaperTrail
module Model
module InstanceMethods
private
def item_before_change
previous = self.dup
# `dup` clears timestamps so we add them back.
all_timestamp_attributes.each do |column|
previous[column] = send(column) if respond_to?(column) && !send(column).nil?
end
previous.tap do |prev|
prev.id = id
changed_attributes.each do |attr, before|
if defined?(CarrierWave::Uploader::Base) && before.is_a?(CarrierWave::Uploader::Base)
prev.send(:write_attribute, attr, before.url && File.basename(before.url))
else
prev[attr] = before
end
end
end
end
end
end
end
Not sure if it's the best solution, but it seems to work.
Adding #beardedd's comment as an answer because I think this is a better way to handle the problem.
Name your database columns something like picture_filename and then in your model mount the uploader using:
class User < ActiveRecord::Base
has_paper_trail
mount_uploader :picture, PictureUploader, mount_on: :picture_filename
end
You still use the user.picture.url attribute to access your model but PaperTrail will store revisions under picture_filename.
Here is a bit updated version of monkeypatch from #rabusmar, I use it for rails 4.2.0 and paper_trail 4.0.0.beta2, in /config/initializers/paper_trail.rb.
The second method override is required if you use optional object_changes column for versions. It works in a bit strange way for carrierwave + fog if you override filename in uploader, old value will be from cloud and new one from local filename, but in my case it's ok.
Also I have not checked if it works correctly when you restore old version.
module PaperTrail
module Model
module InstanceMethods
private
# override to keep only basename for carrierwave attributes in object hash
def item_before_change
previous = self.dup
# `dup` clears timestamps so we add them back.
all_timestamp_attributes.each do |column|
if self.class.column_names.include?(column.to_s) and not send("#{column}_was").nil?
previous[column] = send("#{column}_was")
end
end
enums = previous.respond_to?(:defined_enums) ? previous.defined_enums : {}
previous.tap do |prev|
prev.id = id # `dup` clears the `id` so we add that back
changed_attributes.select { |k,v| self.class.column_names.include?(k) }.each do |attr, before|
if defined?(CarrierWave::Uploader::Base) && before.is_a?(CarrierWave::Uploader::Base)
prev.send(:write_attribute, attr, before.url && File.basename(before.url))
else
before = enums[attr][before] if enums[attr]
prev[attr] = before
end
end
end
end
# override to keep only basename for carrierwave attributes in object_changes hash
def changes_for_paper_trail
_changes = changes.delete_if { |k,v| !notably_changed.include?(k) }
if PaperTrail.serialized_attributes?
self.class.serialize_attribute_changes(_changes)
end
if defined?(CarrierWave::Uploader::Base)
Hash[
_changes.to_hash.map do |k, values|
[k, values.map { |value| value.is_a?(CarrierWave::Uploader::Base) ? value.url && File.basename(value.url) : value }]
end
]
else
_changes.to_hash
end
end
end
end
end
This is what actually functions for me, put this on config/initializers/paper_trail/.rb
module PaperTrail
module Reifier
class << self
def reify_attributes(model, version, attrs)
enums = model.class.respond_to?(:defined_enums) ? model.class.defined_enums : {}
AttributeSerializers::ObjectAttribute.new(model.class).deserialize(attrs)
attrs.each do |k, v|
is_enum_without_type_caster = ::ActiveRecord::VERSION::MAJOR < 5 && enums.key?(k)
if model.send("#{k}").is_a?(CarrierWave::Uploader::Base)
if v.present?
model.send("remote_#{k}_url=", v["#{k}"][:url])
model.send("#{k}").recreate_versions!
else
model.send("remove_#{k}!")
end
else
if model.has_attribute?(k) && !is_enum_without_type_caster
model[k.to_sym] = v
elsif model.respond_to?("#{k}=")
model.send("#{k}=", v)
elsif version.logger
version.logger.warn(
"Attribute #{k} does not exist on #{version.item_type} (Version id: #{version.id})."
)
end
end
end
end
end
end
end
This overrides the reify method to work on S3 + heroku
For uploaders to keep old files from updated or deleted records do this in the uploader
configure do |config|
config.remove_previously_stored_files_after_update = false
end
def remove!
true
end
Then make up some routine to clear old files from time to time, good luck
I want to add to the previous answers the following:
It can happen that you upload different files with the same name, and this may overwrite your previous file, so you won't be able to restore the old one.
You may use a timestamp in file names or create random and unique filenames for all versioned files.
Update
This doesn't seem to work in all edge cases for me, when assigning more than a single file to the same object within a single request request.
I'm using this right now:
def filename
[#cache_id, original_filename].join('-') if original_filename.present?
end
This seems to work, as the #cache_id is generated for each and every upload again (which isn't the case as it seems for the ideas provided in the links above).
#Sjors Provoost
We also need to override pt_recordable_object method in PaperTrail::Model::InstanceMethods module
def pt_recordable_object
attr = attributes_before_change
object_attrs = object_attrs_for_paper_trail(attr)
hash = Hash[
object_attrs.to_hash.map do |k, value|
[k, value.is_a?(CarrierWave::Uploader::Base) ? value.url && File.basename(value.url) : value ]
end
]
if self.class.paper_trail_version_class.object_col_is_json?
hash
else
PaperTrail.serializer.dump(hash)
end
end

Rails sanitize remove default allowed tags

How would I use sanitize, but tell it to disallow some enabled by default tags? The documentation states that I can put this in my application.rb
config.after_initialize do
ActionView::Base.sanitized_allowed_tags.delete 'div'
end
Can I instead pass this as an argument to sanitize?
Yes you can specify which tags and attributes to allow on a per-call basis. From the fine manual:
Custom Use (only the mentioned tags and attributes are allowed, nothing else)
<%= sanitize #article.body, :tags => %w(table tr td), :attributes => %w(id class style) %>
But the problem with that is that :tags has to include all the tags you want to allow.
The sanitize documentation says to
See ActionView::Base for full docs on the available options.
but the documentation is a lie, ActionView::Base says nothing about the available options.
So, as usual, we have to go digging through the source and hope they don't silently change the interface. Tracing through the code a bit yields this:
def tokenize(text, options)
options[:parent] = []
options[:attributes] ||= allowed_attributes
options[:tags] ||= allowed_tags
super
end
def process_node(node, result, options)
result << case node
when HTML::Tag
if node.closing == :close
options[:parent].shift
else
options[:parent].unshift node.name
end
process_attributes_for node, options
options[:tags].include?(node.name) ? node : nil
else
bad_tags.include?(options[:parent].first) ? nil : node.to_s.gsub(/</, "<")
end
end
The default value for options[:tags] in tokenize and the way options[:tags] is used in process_node are of interest and tell us that if options[:tags] has anything then it has to include the entire set of allowed tags and there aren't any other options for controlling the tag set.
Also, if we look at sanitize_helper.rb, we see that sanitized_allowed_tags is just a wrapper for the allowed_tags in the whitelist sanitizer:
def sanitized_allowed_tags
white_list_sanitizer.allowed_tags
end
You should be able to add your own helper that does something like this (untested off-the-top-of-my-head code):
def sensible_sanitize(html, options)
if options.include? :not_tags
options[:tags] = ActionView::Base.sanitized_allowed_tags - options[:not_tags]
end
sanitize html, options
end
and then you could
<%= sensible_sanitize #stuff, :not_tags => [ 'div' ] %>
to use the standard default tags except for <div>.
I know you're looking for a way to pass the tags in, and for that mu's answer looks good!
I was keen to set them up globally which was trickier than I'd hoped as ActionView::Base has overridden sanitized_allowed_tags= to append rather than replace!
I ended up with the following to my application.rb:
SANITIZE_ALLOWED_TAGS = %w{a ul ol li b i}
config.after_initialize do
ActionView::Base.sanitized_allowed_tags.clear
ActionView::Base.sanitized_allowed_tags += SANITIZE_ALLOWED_TAGS
end

mongo_mapper custom data types for localization

i have created a LocalizedString custom data type for storing / displaying translations using mongo_mapper.
This works for one field but as soon as i introduce another field they get written over each and display only one value for both fields. The to_mongo and from_mongo seem to be not workings properly. Please can any one help with this ? her is the code :
class LocalizedString
attr_accessor :translations
def self.from_mongo(value)
puts self.inspect
#translations ||= if value.is_a?(Hash)
value
elsif value.nil?
{}
else
{ I18n.locale.to_s => value }
end
#translations[I18n.locale.to_s]
end
def self.to_mongo(value)
puts self.inspect
if value.is_a?(Hash)
#translations = value
else
#translations[I18n.locale.to_s] = value
end
#translations
end
end
Thank alot
Rick
The problem is that from within your [to|from]_mongo methods, #translations refers to a class variable, not the instance variable you expect. So what's happening is that each time from_mongo is called, it overwrites the value.
A fixed version would be something like this:
class LocalizedString
attr_accessor :translations
def initialize( translations = {} )
#translations = translations
end
def self.from_mongo(value)
if value.is_a?(Hash)
LocalizedString.new(value)
elsif value.nil?
LocalizedString.new()
else
LocalizedString.new( { I18n.locale.to_s => value })
end
end
def self.to_mongo(value)
value.translations if value.present?
end
end
I found that jared's response didn't work for me -- I would get that translations was not found when using LocalizedString in an EmbeddedDocument.
I would get a similar problem on rick's solution where translations was nil when using embedded documents. To get a working solution, I took Rick's solution, changed the translation variable to be an instance variable so it wouldn't be overwritten for each new field that used LocalizedString, and then added a check to make sure translations wasn't nil (and create a new Hash if it was).
Of all the LocalizedString solutions floating around, this is the first time I've been able to get it working on EmbeddedDocuments and without the overwritting problem -- there still may be other issues! :)
class LocalizedString
attr_accessor :translations
def self.from_mongo(value)
puts self.inspect
translations ||= if value.is_a?(Hash)
value
elsif value.nil?
{}
else
{ I18n.locale.to_s => value }
end
translations[I18n.locale.to_s]
end
def self.to_mongo(value)
puts self.inspect
if value.is_a?(Hash)
translations = value
else
if translations.nil?
translations = Hash.new()
end
translations[I18n.locale.to_s] = value
end
translations
end
end
I found this post: which was very helpful. He extended HashWithIndifferentAccess to work as a LocalizedString. The only thing I didn't like about it was having to explicly specify the locale when setting it each time -- I wanted it to work more like a string. of course, you can't overload the = operator (at least I don't think you can) so I used <<, and added a to_s method that would output the string of the current locale....
class LocalizedString < HashWithIndifferentAccess
def self.from_mongo(value)
LocalizedString.new(value || {})
end
def available_locales
symbolize_keys.keys
end
def to_s
self[I18n.locale]
end
def in_current_locale=(value)
self[I18n.locale] = value
end
def << (value)
self[I18n.locale] = value
end
end
and then I have a class like:
class SimpleModel
include MongoMapper::Document
key :test, LocalizedString
end
and can do things like
I18n.locale = :en
a = SimpleModel.new
a.test << "English"
I18n.locale = :de
a.test << "German"
puts a.test # access the translation for the current locale
I18n.locale = :en
puts a.test # access the translation for the current locale
puts a.test[:de] # access a translation explicitly
puts a.test[:en]
puts a.test.inspect
and get
German
English
German
English
{"en"=>"English", "de"=>"German"}
so there we go -- this one actually seems to work for me. Comments welcome, and hope this helps someone!

find untranslated locales in rails

I'm using rails 2.3.5 with i18n. I's there a way to find all not yet translated locales in all views?
Maybe a after_filter in the application controller, but which code I can use for this job?
thanks
When using the i18n gem (which Rails does), you can specify your own exception handler. Try this code:
# A simple exception handler that behaves like the default exception handler
# but additionally logs missing translations to a given log.
#
module I18n
class << self
def missing_translations_logger
##missing_translations_logger ||= Logger.new("#{RAILS_ROOT}/log/missing_translations.log")
end
def missing_translations_log_handler(exception, locale, key, options)
if MissingTranslationData === exception # use MissingTranslation in Rails 3.x !!!
puts "logging #{exception.message}"
missing_translations_logger.warn(exception.message)
return exception.message
else
raise exception
end
end
end
end
I18n.exception_handler = :missing_translations_log_handler
(put it for example into RAILS_ROOT/config/initializers/i18n.rb)
Now, whenever you try to translate a key for which you have no translation specified, a warning gets printed into RAILS_ROOT/log/missing_translations.log.
Hope this helps!
I couldn't find a simple trick to do this, so I did this. First implement a 'before_filter' in your application_controller.rb
before_filter :set_user_language
# set the language, 'zen' is a special URL parameter that makes localizations the use the 't' method visible
def set_user_language
# turn on 'zen' to see localization by adding 'zen=true' to query string, will stay on until a query with 'zen=false'
session[:zen] = (session[:zen] || params[:zen] == "true") && params[:zen] != "false"
I18n.locale = 'en'
end
The above finds 'zen=true' and 'zen=false' in the query string. Then add this method to your application_helper.rb:
def t(*args)
result = super(*args)
result = "[#{result}]" if session[:zen] && result.is_a?(String)
result
end
With this method 'zen=true' makes the 't' method display localized strings in square brackets []. To turn it off enter a query string with 'zen=false'.

Resources