Rails scope routing allows nil as a path segment? - internationalization

I want to make my Rails3.2 app i18n according to the following site and to include a locale parameter in the URL path.
(http://www.wordchuck.com/en/website/blogs/5)
I implemented as
scope ':locale' do
resources :nodes
end
and other methods
def set_locale
I18n.locale = params[:locale] if params.include?('locale')
end
def default_url_options(options = {})
options.merge!({ :locale => I18n.locale })
end
in my application_controller.rb.
And now I can confirm that
http://myhost/ja/nodes/explore or http://myhost/en/nodes/explore
pass, but
http://myhost/nodes/explore
got "No route matches [GET] "/nodes/explore"" error.
I wonder that could be :locale is nil.
To make nil :locale enable and defaults to "en" when :locale is nil, what should I do?

With the way you're routes are set up, the nodes resources require that there be a locale present.
To get around this, you can simply create more routes outside of the :locale scope :
scope ':locale' do
resource :nodes
end
resource :nodes
In principle, the default locale will be used when the current locale isn't explicitly set. If you haven't changed anything in your application, this will be :en by default.
Otherwise, you can set this explicitly in your set_locale method :
def set_locale
I18n.locale = params.include?('locale') ? params[:locale] : :en
end

Related

I18n segments router Phoenix

I have an Elixir / Phoenix app which reacts differently depending on the domain (aka tenant).
A tenant has a specific locale such as "fr_FR", "en_US" and so on.
I want to translate the URIs of the router depending the current locale:
# EN
get "/classifieds/new", ClassifiedController, :new
# FR
get "/annonces/ajout", ClassifiedController, :new
So far I thought it would be possible to do something like that (pseudo code):
if locale() == :fr do
scope "/", Awesome.App, as: :app do
pipe_through :browser # Use the default browser stack
get "/annonces/ajout", ClassifiedController, :new
end
else
scope "/", Awesome.App, as: :app do
pipe_through :browser # Use the default browser stack
get "/classifieds/new", ClassifiedController, :new
end
end
It doesn't work since the router is compiled during the boot of the server, so you have no context of the current connexion (locale, domain, host and so on).
So far my solution (which works) was to create two scopes with two aliases:
scope "/", Awesome.App, as: :fr_app do
pipe_through :browser # Use the default browser stack
get "/annonces/ajout", ClassifiedController, :new
end
scope "/", Awesome.App, as: :app do
pipe_through :browser # Use the default browser stack
get "/classifieds/new", ClassifiedController, :new
end
and a helper associated:
localized_path(conn, path, action)
which takes a path (:app_classified_new_path) and prefix with fr (:fr_app_classified_new_path) if the current locale is "fr" (for example). If the path doesn't exist for the current locale, I fallback to the default locale "en").
It's working great but I see some pain points:
Every use of "something_foo_path()" helpers must be replaced by this new "localized_path()"
The app will accept each localized segments (fr, en, it, whatever) and it's not really what I want (I want to have only the fr segments to work on the tenant "fr"
I should be able to get the current locale/conn info directly in the router (feature/bugfix?) and avoid a hack like that.
Take a look at this article Practical i18n with Phoenix and Elixir. I believe it should give you exactly what you need.
Alternatively, you could play around with some macros to create blocks of translated routes. The code below does not handle all your requirements since you still have to translate the paths internally. However, it may give you some ideas.
defmodule LocalRoutes.Web.Router do
use LocalRoutes.Web, :router
#locales ~w(en fr)
import LocalRoutes.Web.Gettext
use LocalRoutes.LocalizedRouter
def locale(conn, locale) do
Plug.Conn.assign conn, :locale, locale
end
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", LocalRoutes.Web do
pipe_through :browser # Use the default browser stack
get "/", PageController, :index
end
for locale <- #locales do
Gettext.put_locale(LocalRoutes.Web.Gettext, locale)
pipeline String.to_atom(locale) do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :locale, locale
end
scope "/", LocalRoutes.Web do
pipe_through String.to_atom(locale)
get "/#{~g"classifieds"}", ClassifiedController, :index
get "/#{~g"classifieds"}/#{~g"new"}", ClassifiedController, :new
get "/#{~g"classifieds"}/:id", ClassifiedController, :show
get "/#{~g"classifieds"}/:id/#{~g"edit"}", ClassifiedController, :edit
post "/#{~g"classifieds"}", ClassifiedController, :create
patch "/#{~g"classifieds"}/:id", ClassifiedController, :update
put "/#{~g"classifieds"}/:id", ClassifiedController, :update
delete "/#{~g"classifieds"}/:id", ClassifiedController, :delete
end
end
end
defmodule LocalRoutes.LocalizedRouter do
defmacro __using__(opts) do
quote do
import unquote(__MODULE__)
end
end
defmacro sigil_g(string, _) do
quote do
dgettext "routes", unquote(string)
end
end
end

How to request separate folder view path based on controller name in Sinatra?

Here's the contents of my app/controllers/application_controller.rb:
require 'sinatra/base'
require 'slim'
require 'colorize'
class ApplicationController < Sinatra::Base
# Global helpers
helpers ApplicationHelper
# Set folders for template to
set :root, File.expand_path(File.join(File.dirname(__FILE__), '../'))
puts root.green
set :sessions,
:httponly => true,
:secure => production?,
:expire_after => 31557600, # 1 year
:secret => ENV['SESSION_SECRET'] || 'keyboardcat',
:views => File.expand_path(File.expand_path('../../views/', __FILE__)),
:layout_engine => :slim
enable :method_override
# No logging in testing
configure :production, :development do
enable :logging
end
# Global not found??
not_found do
title 'Not Found!'
slim :not_found
end
end
As you can see I'm setting the views directory as:
File.expand_path(File.expand_path('../../views/', __FILE__))
which works out to be /Users/vladdy/Desktop/sinatra/app/views
In configure.ru, I then map('/') { RootController }, and in said controller I render a view with slim :whatever
Problem is, all the views from all the controllers are all in the same spot! How do I add a folder structure to Sinatra views?
If I understand your question correctly, you want to override #find_template.
I stick this function in a helper called view_directory_helper.rb.
helpers do
def find_template(views, name, engine, &block)
views.each { |v| super(v, name, engine, &block) }
end
end
and when setting your view directory, pass in an array instead, like so:
set :views, ['views/layouts', 'views/pages', 'views/partials']
Which would let you have a folder structure like
app
-views
-layouts
-pages
-partials
-controllers
I was faced with same task. I have little experience of programming in Ruby, but for a long time been working with PHP. I think it would be easier to do on it, where you can easily get the child from the parent class. There are some difficulties. As I understand, the language provides callback functions like self.innereted for solving of this problem. But it did not help, because I was not able to determine the particular router in a given time. Maybe the environment variables can help with this. But I was able to find a workaround way to solve this problem, by parsing call stack for geting caller class and wrapping output function. I do not think this is the most elegant way to solve the problem. But I was able to realize it.
class Base < Sinatra::Application
configure do
set :views, 'app/views/'
set :root, File.expand_path('../../../', __FILE__)
end
def display(template, *args)
erb File.join(current_dir, template.to_s).to_sym, *args
end
def current_dir
caller_class.downcase!.split('::').last
end
private
def caller_class(depth = 1)
/<class:([\w]*)>/.match(parse_caller(caller(depth + 1)[1]))[1]
end
def parse_caller(at)
Regexp.last_match[3] if /^(.+?):(\d+)(?::in `(.*)')?/ =~ at
end
end
The last function is taken from here. It can be used as well as default erb function:
class Posts < Base
get '/posts' do
display :index , locals: { variables: {} }
end
end
I hope it will be useful to someone.

Rails routes with optional scope ":locale"

I'm working on a Rails 3.1 app and I'd like to set specific routes for the different languages the app is going to support.
/es/countries
/de/countries
…
For the default language ('en'), I don't want the locale to be displayed in the url.
/countries
Here is the route definition I've set.
scope "(:locale)", :locale => /es|de/ do
resources :countries
end
It works great, until I try to use a path helper with 'en' as the locale.
In the console :
app.countries_path(:locale => 'fr')
=> "/fr/countries"
app.countries_path(:locale => 'en')
=> "/countries?locale=en"
I don't want the "?locale=en".
Is there a way to tell rails that with an 'en' locale, the locale param should not be added to the url?
Thanks
This SHOULD be a better solution:
In your routes.rb,
scope "(:locale)", locale: /#{I18n.available_locales.join("|")}/, defaults: {locale: "en"} do
As MegaTux said, set defaults: {locale: "en"} in the scope.
The advantage:
The jlfenaux solution works in most contexts, but not all. In certain contexts (like basically anything outside of your main controllers and views), the path helpers will get confused and put the object or object.id in the locale parameter, which will cause errors. You'll find yourself putting locale: nil in lots of path helpers to avoid those errors.
The possible problem:
It seems that defaults: {locale: "en"} always overrides any other value you pass in for locale. The option is named default, so I'd expect it to assign locale to 'en' only when there's no value already, but that's not what happens. Anyone else experiencing this?
I finally figured out how to do it easily. You just have to set the default_url_options in the app controller as below.
def default_url_options(options={})
{ :locale => I18n.locale == I18n.default_locale ? nil : I18n.locale }
end
This way, you are sure the locale isn't sent to the path helpers.
If you don't want the query string you don't have to pass it to the helper:
1.9.2 (main):0 > app.countries_path(:locale=>:de)
=> "/de/countries"
1.9.2 (main):0 > app.countries_path
=> "/countries"
1.9.2 (main):0 > app.countries_path(:locale=>:en)
=> "/countries?locale=en"
1.9.2 (main):0 > app.countries_path
=> "/countries"
1.9.2 (main):0 > app.countries_path(:locale=>nil)
=> "/countries"
I'm doing a combination of what #Arcolye and #jifenaux are doing, plus something extra to keep the code as DRY as possible. It might not be suitable for everybody, but in my case, whenever I want to support a new locale I also have to create a new .yml file in config/locales/ anyways, so this is how it works best for me.
config/application.rb:
locale_files = Dir["#{Rails.root}/config/locales/??.yml"]
config.i18n.available_locales = locale_files.map do |d|
d.split('/').last.split('.').first
end
config.i18n.default_locale = :en
config/routes.rb
root_path = 'pages#welcome'
scope '(:locale)', locale: /#{I18n.available_locales.join('|')}/ do
# ...
end
root to: root_path
get '/:locale', to: root_path
app/controllers/application_controller.rb:
private
def default_url_options(options = {})
if I18n.default_locale != I18n.locale
{locale: I18n.locale}.merge options
else
{locale: nil}.merge options
end
end
If you decide to put default_url_options in the application_controller to fix your path helpers, keep in mind you might want to put it in your admin's application_contoller as well
In my case I solved this problem using this technique:
class ApplicationController < ActionController::Base
layout -> {
if devise_controller?
'devise'
end
}
before_action :set_locale
def set_locale
I18n.locale = params[:locale] || I18n.default_locale
end
def url_options
{ :locale => I18n.locale }.merge(super)
end
end

How can I send emails in Rails 3 using the recipient's locale?

How can I send mails in a mailer using the recipient's locale. I have the preferred locale for each user in the database. Notice this is different from the current locale (I18n.locale), as long as the current user doesn't have to be the recipient. So the difficult thing is to use the mailer in a different locale without changing I18n.locale:
def new_follower(user, follower)
#follower = follower
#user = user
mail :to=>#user.email
end
Using I18n.locale = #user.profile.locale before mail :to=>... would solve the mailer issue, but would change the behaviour in the rest of the thread.
I believe the best way to do this is with the great method I18n.with_locale, it allows you to temporarily change the I18n.locale inside a block, you can use it like this:
def new_follower(user, follower)
#follower = follower
#user = user
I18n.with_locale(#user.profile.locale) do
mail to: #user.email
end
end
And it'll change the locale just to send the email, immediately changing back after the block ends.
Source: http://www.rubydoc.info/docs/rails/2.3.8/I18n.with_locale
This answer was a dirty hack that ignored I18n's with_locale method, which is in another answer. The original answer (which works but you shouldn't use it) is below.
Quick and dirty:
class SystemMailer < ActionMailer::Base
def new_follower(user, follower)
#follower = follower
#user = user
using_locale(#user.profile.locale){mail(:to=>#user.email)}
end
protected
def using_locale(locale, &block)
original_locale = I18n.locale
I18n.locale = locale
return_value = yield
I18n.locale = original_locale
return_value
end
end
in the most resent version of rails at this time it's sufficient to use
"I18n.locale = account.locale"
in the controller and make multiple views with the following naming strategy
welcome.html.erb,
welcome.it.html.erb and e.g.
welcome.fr.html.erb
None of the above is really working since the version 3 to translate both subject and content and be sure that the locale is reseted back to the original one... so I did the following (all mailer inherit from that class:
class ResourceMailer < ActionMailer::Base
def mail(headers={}, &block)
I18n.locale = mail_locale
super
ensure
reset_locale
end
def i18n_subject(options = {})
I18n.locale = mail_locale
mailer_scope = self.class.mailer_name.gsub('/', '.')
I18n.t(:subject, options.merge(:scope => [mailer_scope, action_name], :default => action_name.humanize))
ensure
reset_locale
end
def set_locale(locale)
#mail_locale = locale
end
protected
def mail_locale
#mail_locale || I18n.locale
end
def reset_locale
I18n.locale = I18n.default_locale
end
end
You just need to set the locale before you call the mail() method:
set_locale #user.locale
You can use the i18n_subject method which scope the current path so everything is structured:
mail(:subject => i18n_subject(:name => #user.name)
This simple plugin was developed for rails 2 but seems to work in rails 3 too.
http://github.com/Bertg/i18n_action_mailer
With it you can do the following:
def new_follower(user, follower)
#follower = follower
#user = user
set_locale user.locale
mail :to => #user.email, :subject => t(:new_follower_subject)
end
The subject and mail templates are then translated using the user's locale.
Here's an updated version that also supports the '.key' short-hand notation, so you don't have to spell out each key in its entirety.
http://github.com/larspind/i18n_action_mailer
The problem with the mentioned plugins are that they don't work in all situations, for example doing User.human_name or User.human_attribute_name(...) will not translate correctly. The following is the easiest and guaranteed method to work:
stick this somewhere (in initializers or a plugin):
module I18nActionMailer
def self.included(base)
base.class_eval do
include InstanceMethods
alias_method_chain :create!, :locale
end
end
module InstanceMethods
def create_with_locale!(method_name, *parameters)
original_locale = I18n.locale
begin
create_without_locale!(method_name, *parameters)
ensure
I18n.locale = original_locale
end
end
end
end
ActionMailer::Base.send(:include, I18nActionMailer)
and then in your mailer class start your method by setting the desired locale, for example:
def welcome(user)
I18n.locale = user.locale
# etc.
end

Sinatra Variable Scope

Take the following code:
### Dependencies
require 'rubygems'
require 'sinatra'
require 'datamapper'
### Configuration
config = YAML::load(File.read('config.yml'))
name = config['config']['name']
description = config['config']['description']
username = config['config']['username']
password = config['config']['password']
theme = config['config']['theme']
set :public, 'views/themes/#{theme}/static'
### Models
DataMapper.setup(:default, "sqlite3://#{Dir.pwd}/marvin.db")
class Post
include DataMapper::Resource
property :id, Serial
property :name, String
property :body, Text
property :created_at, DateTime
property :slug, String
end
class Page
include DataMapper::Resource
property :id, Serial
property :name, String
property :body, Text
property :slug, String
end
DataMapper.auto_migrate!
### Controllers
get '/' do
#posts = Post.get(:order => [ :id_desc ])
haml :"themes/#{theme}/index"
end
get '/:year/:month/:day/:slug' do
year = params[:year]
month = params[:month]
day = params[:day]
slug = params[:slug]
haml :"themes/#{theme}/post.haml"
end
get '/:slug' do
haml :"themes/#{theme}/page.haml"
end
get '/admin' do
haml :"admin/index.haml"
end
I want to make name, and all those variables available to the entire script, as well as the views. I tried making them global variables, but no dice.
Might not be the "cleanest" way to do it, but setting them as options should work:
--> http://www.sinatrarb.com/configuration.html :)
setting:
set :foo, 'bar'
getting:
"foo is set to " + settings.foo
Make them constants. They should be anyway shouldn't they? They're not going to change.
Make a constant by writing it in all caps.
Read this article on Ruby Variable Scopes if you have any more issues.
http://www.techotopia.com/index.php/Ruby_Variable_Scope
Another clean option may be a config class, where the init method loads the YAML and then sets up the variables.
Have fun. #reply me when you've finished your new blog (I'm guessing this is what this is for).
From the Sinatra README:
Accessing Variables in Templates
Templates are evaluated within the same context as route handlers. Instance variables set in route handlers are direcly accessible by templates:
get '/:id' do
#foo = Foo.find(params[:id])
haml '%h1= #foo.name'
end
Or, specify an explicit Hash of local variables:
get '/:id' do
foo = Foo.find(params[:id])
haml '%h1= foo.name', :locals => { :foo => foo }
end
This is typically used when rendering templates as partials from within other templates.
A third option would be to set up accessors for them as helper methods. (Which are also available throughout the application and views.)
what also works:
##foo = "bar"
But don't forget to restart the server after this change

Resources