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
Related
No helper clause for Api.Router.Helpers.v1_user_organization_path
defined for action :show with arity 3. Please check that the function,
arity and action are correct. The following v1_user_organization_path
actions are defined under your router:
* :create
* :index
* :show
* :update
router.ex
defmodule Api.Router do
use Api.Web, :router
pipeline :api do
plug :accepts, ["json"]
end
scope "/", Api do
pipe_through :api
end
scope "/v1", Api.V1, as: :v1 do
pipe_through :api
resources "/users", UserController, only: [:create, :show, :update] do
resources "/organizations", OrganizationController, only: [:create, :update, :index, :show]
end
end
end
and when I do mix phoenix.routes I see the following v1_user_organization_path getting generated. The problem is I don't know how to use it and I don't know what I should pass into it. Is there a way I can check what this generated method accepts?
The error I get is occuring here
organization_controller.ex
def create(conn, %{"user_id" => user_id, "organization" => organization_params}) do
changeset = Organization.changeset(%Organization{}, organization_params)
case Repo.insert(changeset) do
{:ok, organization} ->
conn
|> put_status(:created)
|> put_resp_header("location", v1_user_organization_path(conn, :show, organization))
|> render("show.json", organization: organization)
{:error, changeset} ->
conn
|> put_status(:unprocessable_entity)
|> render(Api.ChangesetView, "error.json", changeset: changeset)
end
end
At put_resp_header("location", v1_user_organization_path(conn, :show, organization))
You can look at the auto generated documentation for the router helper functions using h in iex -S mix and reading the output of mix phoenix.routes for some help. For example, for the following routes:
resources "/posts", PostController do
resources "/comments", CommentController
end
I get:
iex(1)> h MyApp.Router.Helpers.post_comment_path
def post_comment_path(conn_or_endpoint, action, post_id)
def post_comment_path(conn_or_endpoint, action, post_id, params)
def post_comment_path(conn_or_endpoint, action, post_id, id, params)
$ mix phoenix.routes
post_comment_path GET /posts/:post_id/comments MyApp.CommentController :index
post_comment_path GET /posts/:post_id/comments/:id/edit MyApp.CommentController :edit
post_comment_path GET /posts/:post_id/comments/new MyApp.CommentController :new
post_comment_path GET /posts/:post_id/comments/:id MyApp.CommentController :show
post_comment_path POST /posts/:post_id/comments MyApp.CommentController :create
post_comment_path PATCH /posts/:post_id/comments/:id MyApp.CommentController :update
PUT /posts/:post_id/comments/:id MyApp.CommentController :update
post_comment_path DELETE /posts/:post_id/comments/:id MyApp.CommentController :delete
It's not clear from just the function signature which action accepts how many arguments, but if you read the output of mix phoenix.routes, you can see that :show (last column) requires a post_id and an id.
The output of h is also not completely accurate because it doesn't tell you that the arity 4 version also accepts (conn_or_endpoint, action, post_id, id) and not just (conn_or_endpoint, action, post_id, params).
I don't think there's any better auto generated documentation for the generated route functions right now in Phoenix. I usually just look at the output of mix phoenix.routes and pass in conn_or_endpoint followed by the action followed by every :var in the route, optionally followed by a params map.
My application works and api json requests and for regular html. My router.ex
defmodule MyApp.Router do
use MyApp.Web, :router
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 "/api", MyApp do
pipe_through :api # Use the default browser stack
scope "/v1", V1, as: :v1 do
resources "/users", UserController, except: [:new, :edit, :index]
end
end
scope "/", MyApp do
pipe_through :browser # Use the default browser stack
get "/confirm/:token", UserController, :confirm, as: :user_confirm
end
end
my web/controllers/v1/user_controller.ex
defmodule MyApp.V1.UserController do
use MyApp.Web, :controller
def create(conn, %{"user" => user_params}) do
...
conn
|> put_status(:created)
|> put_resp_header("location", v1_user_path(conn, :show, user))
|> render("sign_up.json", user: Map.put(user, :session, result[:session]))
...
end
and my web/controllers/user_controller.rb
defmodule MyApp.UserController do
use MyApp.Web, :controller
alias MyApp.User
def confirm(conn, %{"token" => token}) do
...
render(conn, "confirmed.html")
...
end
end
my web/views/v1/user_view.ex
defmodule MyApp.V1.UserView do
use MyApp.Web, :view
...
end
and my web/views/user_view.ex
defmodule MyApp.UserView do
use MyApp.Web, :view
end
Everything works fine until I added a route and a controller for html.
Now, when I make a request for api json, I get an error
Request: POST /api/v1/users
** (exit) an exception was raised:
** (UndefinedFunctionError) function MyApp.V1.UserView.render/2 is undefined (module MyApp.V1.UserView is not available)
But if I delete web/vews/user_view.ex, then this query works without errors.
How can you correct this error?
These types of errors can usually be resolved by running mix clean. You may also see this type of error during Live code reload in dev. It case, try restarting the Phoenix.Server, and if that does not help, run mix clean
I have this routing
scope "/api", MosaicApi do
pipe_through :api
# resources "/cards", CardController, except: [:new, :edit]
resources "/estimates/:product", EstimateController, except: [:new, :edit]
Initially I used the generated CardController and things worked (at least POST/create did), but now I want to generalise as Card is a product type and I have a variety of other products that need to expose the exact same CRUD operations. So I am trying to morph Card* to Estimate*
In EstimateController I now have this
defmodule Api.CardController do
use Api.Web, :controller
alias Api.Card
def create(conn, %{"product" => product}) do
conn
|> render("result.json", product: product)
end
...
What I want to do is pattern match on product to bring into scope the relevant Struct (Card, ...), but I've got stuck as the code above yields this error
undefined function Api.EstimateController.init/1 (module Api.EstimateController is not available)
Api.EstimateController.init(:create)
I'm confused as init is not mentioned in http://www.phoenixframework.org/docs/controllers at all
Other indications that things are mostly good
mix phoenix.routes
page_path GET / Api.PageController :index
estimate_path GET /api/estimates/:product Api.EstimateController :index
estimate_path GET /api/estimates/:product/:id Api.EstimateController :show
estimate_path POST /api/estimates/:product Api.EstimateController :create
The function init/1 is defined in https://github.com/phoenixframework/phoenix/blob/v1.1.4/lib/phoenix/controller/pipeline.ex#L98
def init(action) when is_atom(action) do
action
end
It looks like you are not calling use Phoenix.Controller in your EstimatesController.
Usually this is done with:
use Api.Web, :controller
I'm using Sinatra with namespace.
When I tried to use condition, I met a problem.
Here's the snippet of code
class MainApp < Sinatra::Base
register Sinatra::Namespace
set(:role) do |role|
condition{
### DETECT WHERE THIS IS CALLED
p role
true
}
end
namespace '/api', :role => :admin do
before do
p "before"
end
get '/hoo' do
p "hoo"
end
end
namespace '/api' do
get '/bar' do
p "bar"
end
end
end
The above code outputs following message to console when accessing /api/hoo
:admin
:admin
"before"
:admin
"hoo"
I could not understand why :admin is displayed three times. However, maybe one is from namespace, and other twos are from before and get '/hoo'.
On the other hand, accessing /api/bar shows :admin two times.
I just want to do the filtering only before get '/hoo'. Is there any idea?
NOTE: I don't wan't to change URL from /api/hoo to something like /api/baz/hoo
You can debug the steps using the caller:
http://ruby-doc.org/core-2.0/Kernel.html#method-i-caller
(Note: I wouldn't recommend to leave caller in production code unless you absolutely need it for introspection, because it's quite slow.)
Re the Sinatra filters in particular, note that you can at the very least qualify the route and conditions they apply to:
http://www.sinatrarb.com/intro#Filters
before '/protected/*' do
authenticate!
end
before :agent => /Songbird/ do
# ...
end
I can't recollect how to get the http method, but if you look at the sinatra source code you'll likely find it -- last I looked, I recollect each of get, post, etc. to forward their call to the same function, with a method parameter.
I'm working on a Rails 3.2 application with the following routing conditions:
scope "(:locale)", locale: /de|en/ do
resources :categories, only: [:index, :show]
get "newest/index", as: :newest
end
I've a controller with the following:
class LocaleController < ApplicationController
def set
session[:locale_override] = params[:locale]
redirect_to params[:return_to]
end
end
I'm using this with something like this in the templates:
= link_to set_locale_path(locale: :de, return_to: current_path(locale: :de)) do
= image_tag 'de.png', style: 'vertical-align: middle'
= t('.languages.german')
I'm wondering why there doesn't exist a helper in Rails such as current_path, something which is able to infer what route we are currently using, and re-route to it include new options.
The problem I have is using something like redirect_to :back, one pushes the user back to /en/........ (or /de/...) which makes for a crappy experience.
Until now I was storing the locale in the session, but this won't work for Google, and other indexing services.
I'm sure if I invested enough time I could some up with something that was smart enough to detect which route matched, and swap out the locale part, but I feel like this would be a hack.
I'm open to all thoughts, but this SO question suggests just using sub(); unfortunately with such short and frequently occurring strings as locale short codes, probably isn't too wise.
If you are using the :locale scope, you can use url_for as current_path:
# Given your page is /en/category/newest with :locale set to 'en' by scope
url_for(:locale => :en) # => /en/category/newest
url_for(:locale => :de) # => /de/kategorie/neueste
In case somebody looks here, you can use request.fullpath which should give you all after domain name and therefore, will include locale.