Add routes for i18n (#10193)

* Add routes

* Update all_routes spec

* Add i18n route specs

* Add docs

* Docs cleanup

* Add note about fragment caching
This commit is contained in:
Alex 2020-09-03 17:11:07 -04:00 committed by GitHub
parent 8099a91611
commit 2a5a933ed1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 655 additions and 462 deletions

View file

@ -5,6 +5,8 @@ Rails.application.routes.draw do
controllers tokens: "oauth/tokens"
end
# Devise does not support scoping omniauth callbacks under a dynamic segment
# so this lives outside our i18n scope.
devise_for :users, controllers: {
omniauth_callbacks: "omniauth_callbacks",
registrations: "registrations",
@ -18,497 +20,502 @@ Rails.application.routes.draw do
delete "/sign_out", to: "devise/sessions#destroy"
end
require "sidekiq/web"
require "sidekiq_unique_jobs/web"
require "sidekiq/cron/web"
# [@forem/delightful] - all routes are nested under this optional scope to
# begin supporting i18n.
scope "(/locale/:locale)", defaults: { locale: nil } do
get "/locale/:locale" => "stories#index"
require "sidekiq/web"
require "sidekiq_unique_jobs/web"
require "sidekiq/cron/web"
authenticated :user, ->(user) { user.tech_admin? } do
Sidekiq::Web.set :session_secret, Rails.application.secrets[:secret_key_base]
Sidekiq::Web.set :sessions, Rails.application.config.session_options
Sidekiq::Web.class_eval do
use Rack::Protection, origin_whitelist: [URL.url] # resolve Rack Protection HttpOrigin
end
mount Sidekiq::Web => "/sidekiq"
mount FieldTest::Engine, at: "abtests"
end
namespace :resource_admin do
# Check administrate gem docs
DashboardManifest::DASHBOARDS.each do |dashboard_resource|
resources dashboard_resource
authenticated :user, ->(user) { user.tech_admin? } do
Sidekiq::Web.set :session_secret, Rails.application.secrets[:secret_key_base]
Sidekiq::Web.set :sessions, Rails.application.config.session_options
Sidekiq::Web.class_eval do
use Rack::Protection, origin_whitelist: [URL.url] # resolve Rack Protection HttpOrigin
end
mount Sidekiq::Web => "/sidekiq"
mount FieldTest::Engine, at: "abtests"
end
root controller: DashboardManifest::ROOT_DASHBOARD, action: :index
end
namespace :resource_admin do
# Check administrate gem docs
DashboardManifest::DASHBOARDS.each do |dashboard_resource|
resources dashboard_resource
end
namespace :admin do
get "/", to: redirect("/admin/articles")
authenticate :user, ->(user) { user.has_role?(:tech_admin) } do
mount Blazer::Engine, at: "blazer"
flipper_ui = Flipper::UI.app(Flipper,
{ rack_protection: { except: %i[authenticity_token form_token json_csrf
remote_token http_origin session_hijacking] } })
mount flipper_ui, at: "feature_flags"
root controller: DashboardManifest::ROOT_DASHBOARD, action: :index
end
resources :articles, only: %i[index show update]
resources :broadcasts
resources :buffer_updates, only: %i[create update]
resources :listings, only: %i[index edit update destroy]
resources :comments, only: [:index]
resources :events, only: %i[index create update]
resources :feedback_messages, only: %i[index show]
resources :invitations, only: %i[index new create]
resources :pages, only: %i[index new create edit update destroy]
resources :mods, only: %i[index update]
resources :moderator_actions, only: %i[index]
resources :privileged_reactions, only: %i[index]
resources :permissions, only: %i[index]
resources :podcasts, only: %i[index edit update destroy] do
member do
post :fetch
post :add_admin
delete :remove_admin
namespace :admin do
get "/", to: redirect("/admin/articles")
authenticate :user, ->(user) { user.has_role?(:tech_admin) } do
mount Blazer::Engine, at: "blazer"
flipper_ui = Flipper::UI.app(Flipper,
{ rack_protection: { except: %i[authenticity_token form_token json_csrf
remote_token http_origin session_hijacking] } })
mount flipper_ui, at: "feature_flags"
end
resources :articles, only: %i[index show update]
resources :broadcasts
resources :buffer_updates, only: %i[create update]
resources :listings, only: %i[index edit update destroy]
resources :comments, only: [:index]
resources :events, only: %i[index create update]
resources :feedback_messages, only: %i[index show]
resources :invitations, only: %i[index new create]
resources :pages, only: %i[index new create edit update destroy]
resources :mods, only: %i[index update]
resources :moderator_actions, only: %i[index]
resources :privileged_reactions, only: %i[index]
resources :permissions, only: %i[index]
resources :podcasts, only: %i[index edit update destroy] do
member do
post :fetch
post :add_admin
delete :remove_admin
end
end
resources :profile_field_groups, only: %i[update create destroy]
resources :profile_fields, only: %i[index update create destroy]
resources :reactions, only: [:update]
resources :response_templates, only: %i[index new edit create update destroy]
resources :chat_channels, only: %i[index create update destroy] do
member do
delete :remove_user
end
end
resources :reports, only: %i[index show], controller: "feedback_messages" do
collection do
post "send_email"
post "create_note"
post "save_status"
end
end
resources :tags, only: %i[index update show]
resources :users, only: %i[index show edit update] do
member do
post "banish"
post "full_delete"
patch "user_status"
post "merge"
delete "remove_identity"
post "recover_identity"
post "send_email"
post "verify_email_ownership"
patch "unlock_access"
end
end
resources :organization_memberships, only: %i[update destroy create]
resources :organizations, only: %i[index show] do
member do
patch "update_org_credits"
end
end
resources :sponsorships, only: %i[index edit update new create destroy]
resources :welcome, only: %i[index create]
resources :growth, only: %i[index]
resources :tools, only: %i[index create] do
collection do
post "bust_cache"
end
end
resources :webhook_endpoints, only: :index
resource :config
resources :badges, only: %i[index edit update new create]
resources :display_ads, only: %i[index edit update new create destroy]
# These redirects serve as a safegaurd to prevent 404s for any Admins
# who have the old badge_achievement URLs bookmarked.
get "/badges/badge_achievements", to: redirect("/admin/badge_achievements")
get "/badges/badge_achievements/award_badges", to: redirect("/admin/badge_achievements/award_badges")
resources :badge_achievements, only: %i[index destroy]
get "/badge_achievements/award_badges", to: "badge_achievements#award"
post "/badge_achievements/award_badges", to: "badge_achievements#award_badges"
resources :secrets, only: %i[index]
put "secrets", to: "secrets#update"
end
namespace :stories, defaults: { format: "json" } do
resource :feed, only: [:show] do
get ":timeframe" => "feeds#show"
end
end
resources :profile_field_groups, only: %i[update create destroy]
resources :profile_fields, only: %i[index update create destroy]
resources :reactions, only: [:update]
resources :response_templates, only: %i[index new edit create update destroy]
resources :chat_channels, only: %i[index create update destroy] do
member do
delete :remove_user
namespace :api, defaults: { format: "json" } do
scope module: :v0,
constraints: ApiConstraints.new(version: 0, default: true) do
resources :articles, only: %i[index show create update] do
collection do
get "me(/:status)", to: "articles#me", as: :me, constraints: { status: /published|unpublished|all/ }
get "/:username/:slug", to: "articles#show_by_slug", as: :slug
end
end
resources :comments, only: %i[index show]
resources :videos, only: [:index]
resources :podcast_episodes, only: [:index]
resources :users, only: %i[show] do
collection do
get :me
end
end
resources :tags, only: [:index]
resources :follows, only: [:create]
namespace :followers do
get :users
get :organizations
end
resources :webhooks, only: %i[index create show destroy]
resources :listings, only: %i[index show create update]
get "/listings/category/:category", to: "listings#index", as: :listings_category
get "/analytics/totals", to: "analytics#totals"
get "/analytics/historical", to: "analytics#historical"
get "/analytics/past_day", to: "analytics#past_day"
get "/analytics/referrers", to: "analytics#referrers"
resources :health_checks, only: [] do
collection do
get :app
get :search
get :database
get :cache
end
end
end
end
resources :reports, only: %i[index show], controller: "feedback_messages" do
namespace :notifications do
resources :counts, only: [:index]
resources :reads, only: [:create]
end
namespace :incoming_webhooks do
get "/mailchimp/:secret/unsubscribe", to: "mailchimp_unsubscribes#index", as: :mailchimp_unsubscribe_check
post "/mailchimp/:secret/unsubscribe", to: "mailchimp_unsubscribes#create", as: :mailchimp_unsubscribe
end
resources :messages, only: [:create]
resources :chat_channels, only: %i[index show create update]
resources :chat_channel_memberships, only: %i[index create edit update destroy]
resources :articles, only: %i[update create destroy] do
patch "/admin_unpublish", to: "articles#admin_unpublish"
end
resources :article_mutes, only: %i[update]
resources :comments, only: %i[create update destroy] do
patch "/hide", to: "comments#hide"
patch "/unhide", to: "comments#unhide"
patch "/admin_delete", to: "comments#admin_delete"
collection do
post "send_email"
post "create_note"
post "save_status"
post "/moderator_create", to: "comments#moderator_create"
end
end
resources :tags, only: %i[index update show]
resources :users, only: %i[index show edit update] do
member do
post "banish"
post "full_delete"
patch "user_status"
post "merge"
delete "remove_identity"
post "recover_identity"
post "send_email"
post "verify_email_ownership"
patch "unlock_access"
end
resources :comment_mutes, only: %i[update]
resources :users, only: %i[index], defaults: { format: :json } # internal API
resources :users, only: %i[update] do
resource :twitch_stream_updates, only: %i[show create]
end
resources :organization_memberships, only: %i[update destroy create]
resources :organizations, only: %i[index show] do
member do
patch "update_org_credits"
end
end
resources :sponsorships, only: %i[index edit update new create destroy]
resources :welcome, only: %i[index create]
resources :growth, only: %i[index]
resources :tools, only: %i[index create] do
resources :twitch_live_streams, only: :show, param: :username
resources :reactions, only: %i[index create]
resources :response_templates, only: %i[index create edit update destroy]
resources :feedback_messages, only: %i[index create]
resources :organizations, only: %i[update create destroy]
resources :followed_articles, only: [:index]
resources :follows, only: %i[show create update] do
collection do
post "bust_cache"
get "/bulk_show", to: "follows#bulk_show"
end
end
resources :webhook_endpoints, only: :index
resource :config
resources :badges, only: %i[index edit update new create]
resources :display_ads, only: %i[index edit update new create destroy]
# These redirects serve as a safegaurd to prevent 404s for any Admins
# who have the old badge_achievement URLs bookmarked.
get "/badges/badge_achievements", to: redirect("/admin/badge_achievements")
get "/badges/badge_achievements/award_badges", to: redirect("/admin/badge_achievements/award_badges")
resources :badge_achievements, only: %i[index destroy]
get "/badge_achievements/award_badges", to: "badge_achievements#award"
post "/badge_achievements/award_badges", to: "badge_achievements#award_badges"
resources :secrets, only: %i[index]
put "secrets", to: "secrets#update"
end
namespace :stories, defaults: { format: "json" } do
resource :feed, only: [:show] do
get ":timeframe" => "feeds#show"
end
end
namespace :api, defaults: { format: "json" } do
scope module: :v0,
constraints: ApiConstraints.new(version: 0, default: true) do
resources :articles, only: %i[index show create update] do
collection do
get "me(/:status)", to: "articles#me", as: :me, constraints: { status: /published|unpublished|all/ }
get "/:username/:slug", to: "articles#show_by_slug", as: :slug
end
end
resources :comments, only: %i[index show]
resources :videos, only: [:index]
resources :podcast_episodes, only: [:index]
resources :users, only: %i[show] do
collection do
get :me
end
end
resources :tags, only: [:index]
resources :follows, only: [:create]
namespace :followers do
get :users
get :organizations
end
resources :webhooks, only: %i[index create show destroy]
resources :listings, only: %i[index show create update]
get "/listings/category/:category", to: "listings#index", as: :listings_category
get "/analytics/totals", to: "analytics#totals"
get "/analytics/historical", to: "analytics#historical"
get "/analytics/past_day", to: "analytics#past_day"
get "/analytics/referrers", to: "analytics#referrers"
resources :health_checks, only: [] do
collection do
get :app
get :search
get :database
get :cache
end
resources :image_uploads, only: [:create]
resources :notifications, only: [:index]
resources :tags, only: [:index] do
collection do
get "/onboarding", to: "tags#onboarding"
end
end
end
namespace :notifications do
resources :counts, only: [:index]
resources :reads, only: [:create]
end
namespace :incoming_webhooks do
get "/mailchimp/:secret/unsubscribe", to: "mailchimp_unsubscribes#index", as: :mailchimp_unsubscribe_check
post "/mailchimp/:secret/unsubscribe", to: "mailchimp_unsubscribes#create", as: :mailchimp_unsubscribe
end
resources :messages, only: [:create]
resources :chat_channels, only: %i[index show create update]
resources :chat_channel_memberships, only: %i[index create edit update destroy]
resources :articles, only: %i[update create destroy] do
patch "/admin_unpublish", to: "articles#admin_unpublish"
end
resources :article_mutes, only: %i[update]
resources :comments, only: %i[create update destroy] do
patch "/hide", to: "comments#hide"
patch "/unhide", to: "comments#unhide"
patch "/admin_delete", to: "comments#admin_delete"
collection do
post "/moderator_create", to: "comments#moderator_create"
resources :stripe_active_cards, only: %i[create update destroy]
resources :github_repos, only: %i[index] do
collection do
post "/update_or_create", to: "github_repos#update_or_create"
end
end
end
resources :comment_mutes, only: %i[update]
resources :users, only: %i[index], defaults: { format: :json } # internal API
resources :users, only: %i[update] do
resource :twitch_stream_updates, only: %i[show create]
end
resources :twitch_live_streams, only: :show, param: :username
resources :reactions, only: %i[index create]
resources :response_templates, only: %i[index create edit update destroy]
resources :feedback_messages, only: %i[index create]
resources :organizations, only: %i[update create destroy]
resources :followed_articles, only: [:index]
resources :follows, only: %i[show create update] do
collection do
get "/bulk_show", to: "follows#bulk_show"
resources :buffered_articles, only: [:index]
resources :events, only: %i[index show]
resources :videos, only: %i[index create new]
resources :video_states, only: [:create]
resources :twilio_tokens, only: [:show]
resources :html_variants, only: %i[index new create show edit update]
resources :html_variant_trials, only: [:create]
resources :html_variant_successes, only: [:create]
resources :tag_adjustments, only: %i[create destroy]
resources :rating_votes, only: [:create]
resources :page_views, only: %i[create update]
resources :listings, only: %i[index new create edit update destroy dashboard]
resources :credits, only: %i[index new create] do
get "purchase", on: :collection, to: "credits#new"
end
end
resources :image_uploads, only: [:create]
resources :notifications, only: [:index]
resources :tags, only: [:index] do
collection do
get "/onboarding", to: "tags#onboarding"
resources :buffer_updates, only: [:create]
resources :reading_list_items, only: [:update]
resources :poll_votes, only: %i[show create]
resources :poll_skips, only: [:create]
resources :profile_pins, only: %i[create update]
resources :partnerships, only: %i[index create show], param: :option
resources :display_ad_events, only: [:create]
resources :badges, only: [:index]
resources :user_blocks, param: :blocked_id, only: %i[show create destroy]
resources :podcasts, only: %i[new create]
resources :article_approvals, only: %i[create]
resources :video_chats, only: %i[show]
resources :user_subscriptions, only: %i[create] do
collection do
get "/subscribed", action: "subscribed"
end
end
end
resources :stripe_active_cards, only: %i[create update destroy]
resources :github_repos, only: %i[index] do
collection do
post "/update_or_create", to: "github_repos#update_or_create"
namespace :followings, defaults: { format: :json } do
get :users
get :tags
get :organizations
get :podcasts
end
end
resources :buffered_articles, only: [:index]
resources :events, only: %i[index show]
resources :videos, only: %i[index create new]
resources :video_states, only: [:create]
resources :twilio_tokens, only: [:show]
resources :html_variants, only: %i[index new create show edit update]
resources :html_variant_trials, only: [:create]
resources :html_variant_successes, only: [:create]
resources :tag_adjustments, only: %i[create destroy]
resources :rating_votes, only: [:create]
resources :page_views, only: %i[create update]
resources :listings, only: %i[index new create edit update destroy dashboard]
resources :credits, only: %i[index new create] do
get "purchase", on: :collection, to: "credits#new"
end
resources :buffer_updates, only: [:create]
resources :reading_list_items, only: [:update]
resources :poll_votes, only: %i[show create]
resources :poll_skips, only: [:create]
resources :profile_pins, only: %i[create update]
resources :partnerships, only: %i[index create show], param: :option
resources :display_ad_events, only: [:create]
resources :badges, only: [:index]
resources :user_blocks, param: :blocked_id, only: %i[show create destroy]
resources :podcasts, only: %i[new create]
resources :article_approvals, only: %i[create]
resources :video_chats, only: %i[show]
resources :user_subscriptions, only: %i[create] do
collection do
get "/subscribed", action: "subscribed"
resource :onboarding, only: :show
resources :profile_field_groups, only: %i[index], defaults: { format: :json }
get "/verify_email_ownership", to: "email_authorizations#verify", as: :verify_email_authorizations
get "/search/tags" => "search#tags"
get "/search/chat_channels" => "search#chat_channels"
get "/search/listings" => "search#listings"
get "/search/users" => "search#users"
get "/search/feed_content" => "search#feed_content"
get "/search/reactions" => "search#reactions"
get "/chat_channel_memberships/find_by_chat_channel_id" => "chat_channel_memberships#find_by_chat_channel_id"
get "/listings/dashboard" => "listings#dashboard"
get "/listings/:category" => "listings#index", :as => :listing_category
get "/listings/:category/:slug" => "listings#index", :as => :listing_slug
get "/listings/:category/:slug/:view" => "listings#index",
:constraints => { view: /moderate/ }
get "/listings/:category/:slug/delete_confirm" => "listings#delete_confirm"
delete "/listings/:category/:slug" => "listings#destroy"
get "/notifications/:filter" => "notifications#index"
get "/notifications/:filter/:org_id" => "notifications#index"
get "/notification_subscriptions/:notifiable_type/:notifiable_id" => "notification_subscriptions#show"
post "/notification_subscriptions/:notifiable_type/:notifiable_id" => "notification_subscriptions#upsert"
patch "/onboarding_update" => "users#onboarding_update"
patch "/onboarding_checkbox_update" => "users#onboarding_checkbox_update"
get "email_subscriptions/unsubscribe"
post "/chat_channels/:id/moderate" => "chat_channels#moderate"
post "/chat_channels/:id/open" => "chat_channels#open"
get "/connect" => "chat_channels#index"
get "/connect/:slug" => "chat_channels#index"
get "/chat_channels/:id/channel_info", to: "chat_channels#channel_info", as: :chat_channel_info
post "/chat_channels/create_chat" => "chat_channels#create_chat"
post "/chat_channels/block_chat" => "chat_channels#block_chat"
post "/chat_channel_memberships/remove_membership" => "chat_channel_memberships#remove_membership"
post "/chat_channel_memberships/add_membership" => "chat_channel_memberships#add_membership"
post "/join_chat_channel" => "chat_channel_memberships#join_channel"
delete "/messages/:id" => "messages#destroy"
patch "/messages/:id" => "messages#update"
get "/live/:username" => "twitch_live_streams#show"
get "/internal", to: redirect("/admin")
get "/internal/:path", to: redirect("/admin/%{path}")
post "/pusher/auth" => "pusher#auth"
# Chat channel
patch "/chat_channels/update_channel/:id" => "chat_channels#update_channel"
# Chat Channel Membership json response
get "/chat_channel_memberships/chat_channel_info/:id" => "chat_channel_memberships#chat_channel_info"
post "/chat_channel_memberships/create_membership_request" => "chat_channel_memberships#create_membership_request"
patch "/chat_channel_memberships/leave_membership/:id" => "chat_channel_memberships#leave_membership"
patch "/chat_channel_memberships/update_membership/:id" => "chat_channel_memberships#update_membership"
get "/channel_request_info/" => "chat_channel_memberships#request_details"
patch "/chat_channel_memberships/update_membership_role/:id" => "chat_channel_memberships#update_membership_role"
get "/join_channel_invitation/:channel_slug" => "chat_channel_memberships#join_channel_invitation"
post "/joining_invitation_response" => "chat_channel_memberships#joining_invitation_response"
get "/social_previews/article/:id" => "social_previews#article", :as => :article_social_preview
get "/social_previews/user/:id" => "social_previews#user", :as => :user_social_preview
get "/social_previews/organization/:id" => "social_previews#organization", :as => :organization_social_preview
get "/social_previews/tag/:id" => "social_previews#tag", :as => :tag_social_preview
get "/social_previews/listing/:id" => "social_previews#listing", :as => :listing_social_preview
get "/social_previews/comment/:id" => "social_previews#comment", :as => :comment_social_preview
get "/async_info/base_data", controller: "async_info#base_data", defaults: { format: :json }
get "/async_info/shell_version", controller: "async_info#shell_version", defaults: { format: :json }
get "/future", to: redirect("devteam/the-future-of-dev-160n")
get "/forem", to: redirect("devteam/for-empowering-community-2k6h")
# Settings
post "users/update_language_settings" => "users#update_language_settings"
post "users/update_twitch_username" => "users#update_twitch_username"
post "users/join_org" => "users#join_org"
post "users/leave_org/:organization_id" => "users#leave_org", :as => :users_leave_org
post "users/add_org_admin" => "users#add_org_admin"
post "users/remove_org_admin" => "users#remove_org_admin"
post "users/remove_from_org" => "users#remove_from_org"
delete "users/remove_identity", to: "users#remove_identity"
post "users/request_destroy", to: "users#request_destroy", as: :user_request_destroy
get "users/confirm_destroy/:token", to: "users#confirm_destroy", as: :user_confirm_destroy
delete "users/full_delete", to: "users#full_delete", as: :user_full_delete
post "organizations/generate_new_secret" => "organizations#generate_new_secret"
post "users/api_secrets" => "api_secrets#create", :as => :users_api_secrets
delete "users/api_secrets/:id" => "api_secrets#destroy", :as => :users_api_secret
# The priority is based upon order of creation: first created -> highest priority.
# See how all your routes lay out with "rake routes".
# You can have the root of your site routed with "root
get "/robots.:format" => "pages#robots"
get "/api", to: redirect("https://docs.forem.com/api")
get "/privacy" => "pages#privacy"
get "/terms" => "pages#terms"
get "/contact" => "pages#contact"
get "/code-of-conduct" => "pages#code_of_conduct"
get "/report-abuse" => "pages#report_abuse"
get "/welcome" => "pages#welcome"
get "/challenge" => "pages#challenge"
get "/checkin" => "pages#checkin"
get "/badge" => "pages#badge"
get "/💸", to: redirect("t/hiring")
get "/survey", to: redirect("https://dev.to/ben/final-thoughts-on-the-state-of-the-web-survey-44nn")
get "/events" => "events#index"
get "/workshops", to: redirect("events")
get "/sponsors" => "pages#sponsors"
get "/search" => "stories#search"
post "articles/preview" => "articles#preview"
post "comments/preview" => "comments#preview"
get "/stories/warm_comments/:username/:slug" => "stories#warm_comments"
# These routes are required by links in the sites and will most likely to be replaced by a db page
get "/about" => "pages#about"
get "/about-listings" => "pages#about_listings"
get "/security", to: "pages#bounty"
get "/community-moderation" => "pages#community_moderation"
get "/faq" => "pages#faq"
get "/page/post-a-job" => "pages#post_a_job"
get "/tag-moderation" => "pages#tag_moderation"
# NOTE: can't remove the hardcoded URL here as SiteConfig is not available here, we should eventually
# setup dynamic redirects, see <https://github.com/thepracticaldev/dev.to/issues/7267>
get "/shop", to: redirect("https://shop.dev.to")
get "/mod" => "moderations#index", :as => :mod
get "/mod/:tag" => "moderations#index"
get "/page/crayons" => "pages#crayons"
post "/fallback_activity_recorder" => "ga_events#create"
get "/page/:slug" => "pages#show"
scope "p" do
pages_actions = %w[welcome editor_guide publishing_from_rss_guide information markdown_basics badges].freeze
pages_actions.each do |action|
get action, action: action, controller: "pages"
end
end
end
namespace :followings, defaults: { format: :json } do
get :users
get :tags
get :organizations
get :podcasts
end
resource :onboarding, only: :show
resources :profile_field_groups, only: %i[index], defaults: { format: :json }
get "/settings/(:tab)" => "users#edit", :as => :user_settings
get "/settings/:tab/:org_id" => "users#edit", :constraints => { tab: /organization/ }
get "/settings/:tab/:id" => "users#edit", :constraints => { tab: /response-templates/ }
get "/signout_confirm" => "users#signout_confirm"
get "/dashboard" => "dashboards#show"
get "/dashboard/pro", to: "dashboards#pro"
get "dashboard/pro/org/:org_id", to: "dashboards#pro", as: :dashboard_pro_org
get "dashboard/following" => "dashboards#following_tags"
get "dashboard/following_tags" => "dashboards#following_tags"
get "dashboard/following_users" => "dashboards#following_users"
get "dashboard/following_organizations" => "dashboards#following_organizations"
get "dashboard/following_podcasts" => "dashboards#following_podcasts"
get "/dashboard/subscriptions" => "dashboards#subscriptions"
get "/dashboard/:which" => "dashboards#followers", :constraints => { which: /user_followers/ }
get "/dashboard/:which/:org_id" => "dashboards#show",
:constraints => {
which: /organization/
}
get "/dashboard/:username" => "dashboards#show"
get "/verify_email_ownership", to: "email_authorizations#verify", as: :verify_email_authorizations
get "/search/tags" => "search#tags"
get "/search/chat_channels" => "search#chat_channels"
get "/search/listings" => "search#listings"
get "/search/users" => "search#users"
get "/search/feed_content" => "search#feed_content"
get "/search/reactions" => "search#reactions"
get "/chat_channel_memberships/find_by_chat_channel_id" => "chat_channel_memberships#find_by_chat_channel_id"
get "/listings/dashboard" => "listings#dashboard"
get "/listings/:category" => "listings#index", :as => :listing_category
get "/listings/:category/:slug" => "listings#index", :as => :listing_slug
get "/listings/:category/:slug/:view" => "listings#index",
:constraints => { view: /moderate/ }
get "/listings/:category/:slug/delete_confirm" => "listings#delete_confirm"
delete "/listings/:category/:slug" => "listings#destroy"
get "/notifications/:filter" => "notifications#index"
get "/notifications/:filter/:org_id" => "notifications#index"
get "/notification_subscriptions/:notifiable_type/:notifiable_id" => "notification_subscriptions#show"
post "/notification_subscriptions/:notifiable_type/:notifiable_id" => "notification_subscriptions#upsert"
patch "/onboarding_update" => "users#onboarding_update"
patch "/onboarding_checkbox_update" => "users#onboarding_checkbox_update"
get "email_subscriptions/unsubscribe"
post "/chat_channels/:id/moderate" => "chat_channels#moderate"
post "/chat_channels/:id/open" => "chat_channels#open"
get "/connect" => "chat_channels#index"
get "/connect/:slug" => "chat_channels#index"
get "/chat_channels/:id/channel_info", to: "chat_channels#channel_info", as: :chat_channel_info
post "/chat_channels/create_chat" => "chat_channels#create_chat"
post "/chat_channels/block_chat" => "chat_channels#block_chat"
post "/chat_channel_memberships/remove_membership" => "chat_channel_memberships#remove_membership"
post "/chat_channel_memberships/add_membership" => "chat_channel_memberships#add_membership"
post "/join_chat_channel" => "chat_channel_memberships#join_channel"
delete "/messages/:id" => "messages#destroy"
patch "/messages/:id" => "messages#update"
get "/live/:username" => "twitch_live_streams#show"
get "/internal", to: redirect("/admin")
get "/internal/:path", to: redirect("/admin/%{path}")
post "/pusher/auth" => "pusher#auth"
# Chat channel
patch "/chat_channels/update_channel/:id" => "chat_channels#update_channel"
# Chat Channel Membership json response
get "/chat_channel_memberships/chat_channel_info/:id" => "chat_channel_memberships#chat_channel_info"
post "/chat_channel_memberships/create_membership_request" => "chat_channel_memberships#create_membership_request"
patch "/chat_channel_memberships/leave_membership/:id" => "chat_channel_memberships#leave_membership"
patch "/chat_channel_memberships/update_membership/:id" => "chat_channel_memberships#update_membership"
get "/channel_request_info/" => "chat_channel_memberships#request_details"
patch "/chat_channel_memberships/update_membership_role/:id" => "chat_channel_memberships#update_membership_role"
get "/join_channel_invitation/:channel_slug" => "chat_channel_memberships#join_channel_invitation"
post "/joining_invitation_response" => "chat_channel_memberships#joining_invitation_response"
get "/social_previews/article/:id" => "social_previews#article", :as => :article_social_preview
get "/social_previews/user/:id" => "social_previews#user", :as => :user_social_preview
get "/social_previews/organization/:id" => "social_previews#organization", :as => :organization_social_preview
get "/social_previews/tag/:id" => "social_previews#tag", :as => :tag_social_preview
get "/social_previews/listing/:id" => "social_previews#listing", :as => :listing_social_preview
get "/social_previews/comment/:id" => "social_previews#comment", :as => :comment_social_preview
get "/async_info/base_data", controller: "async_info#base_data", defaults: { format: :json }
get "/async_info/shell_version", controller: "async_info#shell_version", defaults: { format: :json }
get "/future", to: redirect("devteam/the-future-of-dev-160n")
get "/forem", to: redirect("devteam/for-empowering-community-2k6h")
# Settings
post "users/update_language_settings" => "users#update_language_settings"
post "users/update_twitch_username" => "users#update_twitch_username"
post "users/join_org" => "users#join_org"
post "users/leave_org/:organization_id" => "users#leave_org", :as => :users_leave_org
post "users/add_org_admin" => "users#add_org_admin"
post "users/remove_org_admin" => "users#remove_org_admin"
post "users/remove_from_org" => "users#remove_from_org"
delete "users/remove_identity", to: "users#remove_identity"
post "users/request_destroy", to: "users#request_destroy", as: :user_request_destroy
get "users/confirm_destroy/:token", to: "users#confirm_destroy", as: :user_confirm_destroy
delete "users/full_delete", to: "users#full_delete", as: :user_full_delete
post "organizations/generate_new_secret" => "organizations#generate_new_secret"
post "users/api_secrets" => "api_secrets#create", :as => :users_api_secrets
delete "users/api_secrets/:id" => "api_secrets#destroy", :as => :users_api_secret
# The priority is based upon order of creation: first created -> highest priority.
# See how all your routes lay out with "rake routes".
# You can have the root of your site routed with "root
get "/robots.:format" => "pages#robots"
get "/api", to: redirect("https://docs.forem.com/api")
get "/privacy" => "pages#privacy"
get "/terms" => "pages#terms"
get "/contact" => "pages#contact"
get "/code-of-conduct" => "pages#code_of_conduct"
get "/report-abuse" => "pages#report_abuse"
get "/welcome" => "pages#welcome"
get "/challenge" => "pages#challenge"
get "/checkin" => "pages#checkin"
get "/badge" => "pages#badge"
get "/💸", to: redirect("t/hiring")
get "/survey", to: redirect("https://dev.to/ben/final-thoughts-on-the-state-of-the-web-survey-44nn")
get "/events" => "events#index"
get "/workshops", to: redirect("events")
get "/sponsors" => "pages#sponsors"
get "/search" => "stories#search"
post "articles/preview" => "articles#preview"
post "comments/preview" => "comments#preview"
get "/stories/warm_comments/:username/:slug" => "stories#warm_comments"
# These routes are required by links in the sites and will most likely to be replaced by a db page
get "/about" => "pages#about"
get "/about-listings" => "pages#about_listings"
get "/security", to: "pages#bounty"
get "/community-moderation" => "pages#community_moderation"
get "/faq" => "pages#faq"
get "/page/post-a-job" => "pages#post_a_job"
get "/tag-moderation" => "pages#tag_moderation"
# NOTE: can't remove the hardcoded URL here as SiteConfig is not available here, we should eventually
# setup dynamic redirects, see <https://github.com/thepracticaldev/dev.to/issues/7267>
get "/shop", to: redirect("https://shop.dev.to")
get "/mod" => "moderations#index", :as => :mod
get "/mod/:tag" => "moderations#index"
get "/page/crayons" => "pages#crayons"
post "/fallback_activity_recorder" => "ga_events#create"
get "/page/:slug" => "pages#show"
scope "p" do
pages_actions = %w[welcome editor_guide publishing_from_rss_guide information markdown_basics badges].freeze
pages_actions.each do |action|
get action, action: action, controller: "pages"
# for testing rails mailers
unless Rails.env.production?
get "/rails/mailers" => "rails/mailers#index"
get "/rails/mailers/*path" => "rails/mailers#preview"
end
get "/embed/:embeddable", to: "liquid_embeds#show", as: "liquid_embed"
# serviceworkers
get "/serviceworker" => "service_worker#index"
get "/manifest" => "service_worker#manifest"
get "/shell_top" => "shell#top"
get "/shell_bottom" => "shell#bottom"
get "/new" => "articles#new"
get "/new/:template" => "articles#new"
get "/pod", to: "podcast_episodes#index"
get "/podcasts", to: redirect("pod")
get "/readinglist" => "reading_list_items#index"
get "/readinglist/:view" => "reading_list_items#index", :constraints => { view: /archive/ }
get "/feed" => "articles#feed", :as => "feed", :defaults => { format: "rss" }
get "/feed/tag/:tag" => "articles#feed", :as => "tag_feed", :defaults => { format: "rss" }
get "/feed/:username" => "articles#feed", :as => "user_feed", :defaults => { format: "rss" }
get "/rss" => "articles#feed", :defaults => { format: "rss" }
get "/tag/:tag" => "stories#index"
get "/t/:tag", to: "stories#index", as: :tag
get "/t/:tag/edit", to: "tags#edit"
get "/t/:tag/admin", to: "tags#admin"
patch "/tag/:id", to: "tags#update"
get "/t/:tag/top/:timeframe" => "stories#index"
get "/t/:tag/page/:page" => "stories#index"
get "/t/:tag/:timeframe" => "stories#index",
:constraints => { timeframe: /latest/ }
get "/badge/:slug" => "badges#show"
get "/top/:timeframe" => "stories#index"
get "/:timeframe" => "stories#index", :constraints => { timeframe: /latest/ }
get "/:username/series" => "collections#index", :as => "user_series"
get "/:username/series/:id" => "collections#show"
# Legacy comment format (might still be floating around app, and external links)
get "/:username/:slug/comments" => "comments#index"
get "/:username/:slug/comments/:id_code" => "comments#index"
get "/:username/:slug/comments/:id_code/edit" => "comments#edit"
get "/:username/:slug/comments/:id_code/delete_confirm" => "comments#delete_confirm"
# Proper link format
get "/:username/comment/:id_code" => "comments#index"
get "/:username/comment/:id_code/edit" => "comments#edit"
get "/:username/comment/:id_code/delete_confirm" => "comments#delete_confirm"
get "/:username/comment/:id_code/mod" => "moderations#comment"
get "/:username/comment/:id_code/settings", to: "comments#settings"
get "/:username/:slug/:view" => "stories#show",
:constraints => { view: /moderate/ }
get "/:username/:slug/mod" => "moderations#article"
get "/:username/:slug/actions_panel" => "moderations#actions_panel"
get "/:username/:slug/manage" => "articles#manage"
get "/:username/:slug/edit" => "articles#edit"
get "/:username/:slug/delete_confirm" => "articles#delete_confirm"
get "/:username/:slug/stats" => "articles#stats"
get "/:username/:view" => "stories#index",
:constraints => { view: /comments|moderate|admin/ }
get "/:username/:slug" => "stories#show"
get "/:sitemap" => "sitemaps#show",
:constraints => { format: /xml/, sitemap: /sitemap-.+/ }
get "/:username" => "stories#index", :as => "user_profile"
root "stories#index"
end
get "/settings/(:tab)" => "users#edit", :as => :user_settings
get "/settings/:tab/:org_id" => "users#edit", :constraints => { tab: /organization/ }
get "/settings/:tab/:id" => "users#edit", :constraints => { tab: /response-templates/ }
get "/signout_confirm" => "users#signout_confirm"
get "/dashboard" => "dashboards#show"
get "/dashboard/pro", to: "dashboards#pro"
get "dashboard/pro/org/:org_id", to: "dashboards#pro", as: :dashboard_pro_org
get "dashboard/following" => "dashboards#following_tags"
get "dashboard/following_tags" => "dashboards#following_tags"
get "dashboard/following_users" => "dashboards#following_users"
get "dashboard/following_organizations" => "dashboards#following_organizations"
get "dashboard/following_podcasts" => "dashboards#following_podcasts"
get "/dashboard/subscriptions" => "dashboards#subscriptions"
get "/dashboard/:which" => "dashboards#followers", :constraints => { which: /user_followers/ }
get "/dashboard/:which/:org_id" => "dashboards#show",
:constraints => {
which: /organization/
}
get "/dashboard/:username" => "dashboards#show"
# for testing rails mailers
unless Rails.env.production?
get "/rails/mailers" => "rails/mailers#index"
get "/rails/mailers/*path" => "rails/mailers#preview"
end
get "/embed/:embeddable", to: "liquid_embeds#show", as: "liquid_embed"
# serviceworkers
get "/serviceworker" => "service_worker#index"
get "/manifest" => "service_worker#manifest"
get "/shell_top" => "shell#top"
get "/shell_bottom" => "shell#bottom"
get "/new" => "articles#new"
get "/new/:template" => "articles#new"
get "/pod", to: "podcast_episodes#index"
get "/podcasts", to: redirect("pod")
get "/readinglist" => "reading_list_items#index"
get "/readinglist/:view" => "reading_list_items#index", :constraints => { view: /archive/ }
get "/feed" => "articles#feed", :as => "feed", :defaults => { format: "rss" }
get "/feed/tag/:tag" => "articles#feed", :as => "tag_feed", :defaults => { format: "rss" }
get "/feed/:username" => "articles#feed", :as => "user_feed", :defaults => { format: "rss" }
get "/rss" => "articles#feed", :defaults => { format: "rss" }
get "/tag/:tag" => "stories#index"
get "/t/:tag", to: "stories#index", as: :tag
get "/t/:tag/edit", to: "tags#edit"
get "/t/:tag/admin", to: "tags#admin"
patch "/tag/:id", to: "tags#update"
get "/t/:tag/top/:timeframe" => "stories#index"
get "/t/:tag/page/:page" => "stories#index"
get "/t/:tag/:timeframe" => "stories#index",
:constraints => { timeframe: /latest/ }
get "/badge/:slug" => "badges#show"
get "/top/:timeframe" => "stories#index"
get "/:timeframe" => "stories#index", :constraints => { timeframe: /latest/ }
get "/:username/series" => "collections#index", :as => "user_series"
get "/:username/series/:id" => "collections#show"
# Legacy comment format (might still be floating around app, and external links)
get "/:username/:slug/comments" => "comments#index"
get "/:username/:slug/comments/:id_code" => "comments#index"
get "/:username/:slug/comments/:id_code/edit" => "comments#edit"
get "/:username/:slug/comments/:id_code/delete_confirm" => "comments#delete_confirm"
# Proper link format
get "/:username/comment/:id_code" => "comments#index"
get "/:username/comment/:id_code/edit" => "comments#edit"
get "/:username/comment/:id_code/delete_confirm" => "comments#delete_confirm"
get "/:username/comment/:id_code/mod" => "moderations#comment"
get "/:username/comment/:id_code/settings", to: "comments#settings"
get "/:username/:slug/:view" => "stories#show",
:constraints => { view: /moderate/ }
get "/:username/:slug/mod" => "moderations#article"
get "/:username/:slug/actions_panel" => "moderations#actions_panel"
get "/:username/:slug/manage" => "articles#manage"
get "/:username/:slug/edit" => "articles#edit"
get "/:username/:slug/delete_confirm" => "articles#delete_confirm"
get "/:username/:slug/stats" => "articles#stats"
get "/:username/:view" => "stories#index",
:constraints => { view: /comments|moderate|admin/ }
get "/:username/:slug" => "stories#show"
get "/:sitemap" => "sitemaps#show",
:constraints => { format: /xml/, sitemap: /sitemap-.+/ }
get "/:username" => "stories#index", :as => "user_profile"
root "stories#index"
end
# rubocop:enable Metrics/BlockLength

View file

@ -0,0 +1,148 @@
---
title: Internationalization (i18n)
---
# Internationalization (i18n)
## What is internationlization (i18n)?
To over simplify the concept a bit, internalization (i18n for short) is the
process of making the platform more user-friendly in various languages for
people around the globe. This includes, but is certainly not limited to, things
like making the site available in different languages, changing currency values
to match your region, changing date formats, etc.
## What do we currently support?
Currently, we allow users to set some language settings (beta) under
`/settings/misc`. Based on your selection, the feed algorithm will prioritize
content written in your preferred languages.
We also introduced some routing to lay the groundwork for a more comprehensive
i18n implementation.
## What is the goal?
We want everyone to feel included, regardless of where they're located or what
language(s) they speak. The goal is to make the platform available in various
languages.
## How do you get involved?
The following is a high level outline of an approach to internationalization.
This is by no means set in stone.
We encourage you to open a pull request (PR) to this documentation or to
contribute to internationalization with your ideas - we're
[open-source](https://github.com/forem/forem/pulls)!
## Routing
We have logic for routes setup. You can visit a page and add `/locale/:locale`
to the beginning of the path. For example, if you visit the homepage, you can
add `/locale/fr-ca` for French, Canadian where `fr` is the language code and
`ca` is the region code.
Setting up languages under this "sub-folder" approach helps with Search Engine
Optimization (SEO), routing, and more.
_Currently, the various language routes will not do anything - it will stil show
the site in English (US)._
Once i18n is up and running, users will be able to select their preferred
language to view the platform in. These routes will be the location of various
languages.
## Translating content
There are many ways to translate static content on the platform. To start, we
can explore tools like [i18n-tasks](https://glebm.github.io/i18n-tasks/) which
also has an option to leverage Google Translate programmatically. We'll need to
create locale files (likely `.yml`) to house the translations.
## Search Engine Optimization (SEO)
It seems search engines, especially Google, don't particularly like content on a
page to be in multiple languages. To account for this on pages like articles, we
can try an approach using the canonical URL for the language the article was
written in.
For example, if we detect an article is written in Spanish, we can set the
canonical URL for that article to be `/locale/es/username/article-slug`. We can
then hide comments that are not in the same language as the article/rest of the
page (Spanish in this example) only for the views the crawlers would see. That
way, when the search engine crawler hits an article written in Spanish, the
crawler will see the entire page in Spanish. We will not hide comments for the
views that real users see.
## Caching and service workers
The platform relies on edge caching, especially with regards to articles. To
account for this, we'll need to add logic at the edge that understands what
languages the platform currently supports and where to look up the language
variant in the cache.
If the edge doesn't pick up on a user selected preference (possibly sent as an
additional header or cookie), the edge will look at the `Accept-Language` header
and normalize it. The header can include more specific preferences and look
something like: `Accept-Language: fr-ca, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5`.
There are 2 things going on here. 1) A user can specify country/region variants
for a language - `fr-ca` (French - Canada) and `fr-fr` (French - France). For
simplicity's sake, we want to normalize that sort of preference to `fr` to
start. 2) A user can specify priority using the q argument. We'll want to
interpret the user's priority preferences to match their highest priority
language with one we currently support.
Once the edge is aware of what language it should be looking for, it will set
the cache key accordingly.
We also make use of fragment caching in several places. We need to update the
keys for those caches to account for `locale` so we're not mistakenly serving a
cached fragment in a different language than intended.
Service workers load some parts of the UI up front, like the shell. Therefore,
the service worker will need to be aware of language preferences so that it
loads the shell in the correct language. We also need to make sure the service
worker/cache is aware of a change in the user's language preference.
## Additional considerations
- _Translating URLs_. For the best SEO result, we should also translate URLs
themselves into various languages. Something like the `/about` page could be
translated, for example. For now, we aren't going to account for this.
- _Translating dynamic/user generated content_. For now, we plan to _not_
automatically translate any dynamic/user generated content (articles,
comments, listings, etc.). In the future we could explore what that looks
like, how a user can opt-in/out of that, etc.
## Styles, design, and UI
We'll want to expand some design aspects to support other languages that may be
right-to-left, have different spacings, have special characters, etc.
## Next steps
A few next steps we can take on the road to internationalization.
- Update our logic to allow special characters/encodings in URLs. Currently, we
generate slugs on dynamic content like articles and tags that may include
characters that make the URL
invalid.[Here](https://github.com/forem/forem/issues/10116) is a good example.
We want to update this logic so these characters work in URLs as expected.
- Allow Forem Admins to set a "default language". Currently, if a user doesn't
select a language preference, it defaults to English ("en").
- Update service workers and caching to interpret language preferences.
- Clean up some code. There are some places we're hard-coding strings on the
frontend. We'll want to explore moving that sort data to the backend to unify
where and how we're translating.
- Translate areas of the site into English (US) first to ensure things are still
working. In other words, have the platform adhere to the default locale
instead of hard-coded strings.
- Start translating!
## Resources
- [Rails Guides - Rails Internationalization (I18n)](https://guides.rubyonrails.org/i18n.html)
- [i18n-tasks](https://glebm.github.io/i18n-tasks/)
- [Google: Managing multi-regional and multilingual sites](https://support.google.com/webmasters/answer/182192)
- [forem.dev post: What internationalization features should we support?](https://forem.dev/vaidehijoshi/what-internationalization-features-should-we-support-4kl)

View file

@ -11,6 +11,7 @@ items:
- data-update-scripts.md
- elasticsearch.md
- fastly.md
- internationalization.md
- roles.md
- pusher.md
- resource-admin.md

View file

@ -9,6 +9,7 @@ RSpec.describe "all routes", type: :routing do
controller: "stories",
action: "index",
username: podcast.slug,
locale: nil, # default locale
)
end
@ -17,6 +18,7 @@ RSpec.describe "all routes", type: :routing do
controller: "stories",
action: "index",
username: user.username,
locale: nil, # default locale
)
end
@ -26,6 +28,7 @@ RSpec.describe "all routes", type: :routing do
action: "show",
slug: "this-is-a-slug",
username: "ben",
locale: nil, # default locale
)
end

View file

@ -0,0 +1,34 @@
require "rails_helper"
RSpec.describe "i8n routes", type: :routing do
let(:locale) { "fr-ca" }
let(:i18n_route) { "/locale/#{locale}" }
let(:user) { create(:user) }
it "renders a user index if there is a user with the username successfully" do
expect(get: "#{i18n_route}/#{user.username}").to route_to(
controller: "stories",
action: "index",
username: user.username,
locale: "fr-ca",
)
end
it "renders a user's story successfully" do
expect(get: "#{i18n_route}/ben/this-is-a-slug").to route_to(
controller: "stories",
action: "show",
slug: "this-is-a-slug",
username: "ben",
locale: "fr-ca",
)
end
it "renders homepage successfully" do
expect(get: i18n_route).to route_to(
controller: "stories",
action: "index",
locale: "fr-ca",
)
end
end