Allow different default locales per tenant

Note that, for everything to work consistently, we need to make sure
that the default locale is one of the available locales.

Also note that we aren't overwriting the `#save ` method set by
globalize. I didn't feel too comfortable changing a monkey-patch which
ideally shouldn't be there in the first place, I haven't found a case
where `Globalize.locale` is `nil` (since it defaults to `I18n.locale`,
which should never be `nil`), so using `I18n.default_locale` probably
doesn't affect us.
This commit is contained in:
Javi Martín
2024-03-04 20:03:25 +01:00
parent 722e50a669
commit 6de4737b70
22 changed files with 156 additions and 26 deletions

View File

@@ -56,7 +56,7 @@ class ApplicationController < ActionController::Base
elsif Setting.enabled_locales.include?(session[:locale]&.to_sym) elsif Setting.enabled_locales.include?(session[:locale]&.to_sym)
session[:locale] session[:locale]
else else
I18n.default_locale Setting.default_locale
end end
end end

View File

@@ -44,7 +44,7 @@ class Management::BaseController < ActionController::Base
session[:locale] = params[:locale].to_s session[:locale] = params[:locale].to_s
end end
session[:locale] ||= I18n.default_locale.to_s session[:locale] ||= Setting.default_locale.to_s
I18n.with_locale(session[:locale], &action) I18n.with_locale(session[:locale], &action)
end end

View File

@@ -37,7 +37,7 @@ module SearchDictionarySelector
private private
def find_from_i18n_default def find_from_i18n_default
key_to_lookup = I18n.default_locale.to_s.split("-").first.to_sym key_to_lookup = Setting.default_locale.to_s.split("-").first.to_sym
dictionary = I18N_TO_DICTIONARY[key_to_lookup] dictionary = I18N_TO_DICTIONARY[key_to_lookup]
dictionary ||= "simple" dictionary ||= "simple"

View File

@@ -76,7 +76,7 @@ class Mailer < ApplicationMailer
def user_invite(email) def user_invite(email)
@email_to = email @email_to = email
I18n.with_locale(I18n.default_locale) do I18n.with_locale(Setting.default_locale) do
mail(to: @email_to, subject: t("mailers.user_invite.subject", org_name: Setting["org_name"])) mail(to: @email_to, subject: t("mailers.user_invite.subject", org_name: Setting["org_name"]))
end end
end end

View File

@@ -82,7 +82,7 @@ module Globalizable
translation_class.instance_eval do translation_class.instance_eval do
validates method, validates method,
length: options[:length], length: options[:length],
if: lambda { |translation| translation.locale == I18n.default_locale } if: lambda { |translation| translation.locale == Setting.default_locale }
end end
if options.count > 1 if options.count > 1
translation_class.instance_eval do translation_class.instance_eval do

View File

@@ -96,6 +96,7 @@ class Setting < ApplicationRecord
# Code to be included at the top (inside <head>) of every page (useful for tracking) # Code to be included at the top (inside <head>) of every page (useful for tracking)
"html.per_page_code_head": "", "html.per_page_code_head": "",
"locales.enabled": nil, "locales.enabled": nil,
"locales.default": nil,
"map.latitude": 51.48, "map.latitude": 51.48,
"map.longitude": 0.0, "map.longitude": 0.0,
"map.zoom": 10, "map.zoom": 10,
@@ -224,7 +225,16 @@ class Setting < ApplicationRecord
def enabled_locales def enabled_locales
locales = Setting["locales.enabled"].to_s.split.map(&:to_sym) locales = Setting["locales.enabled"].to_s.split.map(&:to_sym)
(locales & I18n.available_locales).presence || I18n.available_locales [
default_locale,
*((locales & I18n.available_locales).presence || I18n.available_locales)
].uniq
end
def default_locale
locale = Setting["locales.default"].to_s.strip.to_sym
([locale] & I18n.available_locales).first || I18n.default_locale
end end
end end
end end

View File

@@ -339,7 +339,7 @@ class User < ApplicationRecord
end end
def locale def locale
self[:locale] || I18n.default_locale.to_s self[:locale] || Setting.default_locale.to_s
end end
def confirmation_required? def confirmation_required?

View File

@@ -24,7 +24,7 @@ end
def random_locales def random_locales
[ [
I18n.default_locale, Setting.default_locale,
*(Setting.enabled_locales & %i[en es]), *(Setting.enabled_locales & %i[en es]),
*Setting.enabled_locales.sample(4) *Setting.enabled_locales.sample(4)
].uniq.take(5) ].uniq.take(5)

View File

@@ -8,6 +8,7 @@ describe Admin::SiteCustomization::ContentBlocks::FormContentBlockComponent do
end end
it "only includes enabled settings" do it "only includes enabled settings" do
Setting["locales.default"] = "de"
Setting["locales.enabled"] = "de fr" Setting["locales.enabled"] = "de fr"
render_inline component render_inline component

View File

@@ -8,6 +8,7 @@ describe Admin::SiteCustomization::ContentBlocks::FormHeadingContentBlockCompone
end end
it "only includes enabled settings" do it "only includes enabled settings" do
Setting["locales.default"] = "de"
Setting["locales.enabled"] = "de fr" Setting["locales.enabled"] = "de fr"
render_inline component render_inline component

View File

@@ -101,6 +101,7 @@ describe Layout::LocaleSwitcherComponent do
context "when not all available locales are enabled" do context "when not all available locales are enabled" do
before do before do
allow(I18n).to receive(:available_locales).and_return(%i[en es fr]) allow(I18n).to receive(:available_locales).and_return(%i[en es fr])
Setting["locales.default"] = "es"
Setting["locales.enabled"] = "es fr" Setting["locales.enabled"] = "es fr"
end end

View File

@@ -23,9 +23,11 @@ describe ApplicationController do
describe "#switch_locale" do describe "#switch_locale" do
it "uses the default locale by default" do it "uses the default locale by default" do
Setting["locales.default"] = "pt-BR"
get :index get :index
expect(response.body).to eq "en" expect(response.body).to eq "pt-BR"
end end
it "uses the locale in the parameters when it's there" do it "uses the locale in the parameters when it's there" do

View File

@@ -34,9 +34,11 @@ describe Management::BaseController do
describe "#switch_locale" do describe "#switch_locale" do
it "uses the default locale by default" do it "uses the default locale by default" do
Setting["locales.default"] = "pt-BR"
get :index get :index
expect(response.body).to eq "en" expect(response.body).to eq "pt-BR"
end end
it "uses the locale in the parameters when it's there" do it "uses the locale in the parameters when it's there" do

View File

@@ -29,13 +29,14 @@ describe SubscriptionsController do
end end
it "only accepts enabled locales" do it "only accepts enabled locales" do
Setting["locales.enabled"] = "en nl" Setting["locales.default"] = "fr"
Setting["locales.enabled"] = "fr nl"
create(:user, locale: "es", subscriptions_token: "mytoken") create(:user, locale: "es", subscriptions_token: "mytoken")
get :edit, params: { token: "mytoken" } get :edit, params: { token: "mytoken" }
expect(session[:locale]).to eq "en" expect(session[:locale]).to eq "fr"
end end
end end
end end

View File

@@ -3,27 +3,23 @@ require "rails_helper"
describe SearchDictionarySelector do describe SearchDictionarySelector do
context "from I18n default locale" do context "from I18n default locale" do
before { allow(subject).to receive(:call).and_call_original } before { allow(subject).to receive(:call).and_call_original }
around do |example|
original_i18n_default = I18n.default_locale
begin
example.run
ensure
I18n.default_locale = original_i18n_default
end
end
it "returns correct dictionary for simple locale" do it "returns correct dictionary for simple locale" do
I18n.default_locale = :es Setting["locales.default"] = "es"
expect(subject.call).to eq("spanish") expect(subject.call).to eq("spanish")
end end
it "returns correct dictionary for compound locale" do it "returns correct dictionary for compound locale" do
I18n.default_locale = :"pt-BR" Setting["locales.default"] = "pt-BR"
expect(subject.call).to eq("portuguese") expect(subject.call).to eq("portuguese")
end end
it "returns simple for unsupported locale" do it "returns simple for unsupported locale" do
expect(I18n).to receive(:default_locale).and_return(:pl) # avoiding I18n::InvalidLocale allow(I18n).to receive(:available_locales).and_return(%i[en pl])
Setting["locales.default"] = "pl"
expect(subject.call).to eq("simple") expect(subject.call).to eq("simple")
end end
end end

View File

@@ -30,6 +30,16 @@ describe Mailer do
end end
end end
describe "#user_invite" do
it "uses the default locale setting" do
Setting["locales.default"] = "es"
Mailer.user_invite("invited@consul.dev").deliver_now
expect(ActionMailer::Base.deliveries.last.body.to_s).to match "<html lang=\"es\""
end
end
describe "#manage_subscriptions_token" do describe "#manage_subscriptions_token" do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:proposal) { create(:proposal, author: user) } let(:proposal) { create(:proposal, author: user) }

View File

@@ -21,6 +21,7 @@ describe Budget::ContentBlock do
end end
it "is not valid with a disabled locale" do it "is not valid with a disabled locale" do
Setting["locales.default"] = "pt-BR"
Setting["locales.enabled"] = "nl pt-BR" Setting["locales.enabled"] = "nl pt-BR"
block.locale = "en" block.locale = "en"

View File

@@ -0,0 +1,47 @@
require "rails_helper"
describe Globalizable do
before do
dummy_banner = Class.new(ApplicationRecord) do
def self.name
"DummyBanner"
end
self.table_name = "banners"
translates :title, touch: true
include Globalizable
has_many :translations, class_name: "DummyBanner::Translation", foreign_key: "banner_id"
validates_translation :title, length: { minimum: 7 }
end
stub_const("DummyBanner", dummy_banner)
end
describe ".validates_translation" do
it "validates length for the default locale" do
Setting["locales.default"] = "es"
dummy = DummyBanner.new
dummy.translations.build(locale: "es", title: "Short")
dummy.translations.build(locale: "fr", title: "Long enough")
I18n.with_locale(:fr) do
expect(dummy).not_to be_valid
end
end
it "does not validate length for other locales" do
Setting["locales.default"] = "es"
dummy = DummyBanner.new
dummy.translations.build(locale: "es", title: "Long enough")
dummy.translations.build(locale: "fr", title: "Long enough")
dummy.translations.build(locale: "en", title: "Short")
I18n.with_locale(:fr) do
expect(dummy).to be_valid
end
end
end
end

View File

@@ -160,6 +160,7 @@ RSpec.describe I18nContent do
end end
it "does not store new keys for disabled translations" do it "does not store new keys for disabled translations" do
Setting["locales.default"] = "es"
Setting["locales.enabled"] = "es" Setting["locales.enabled"] = "es"
I18nContent.update([{ id: "shared.yes", values: { "value_en" => "Oh, yeah" }}]) I18nContent.update([{ id: "shared.yes", values: { "value_en" => "Oh, yeah" }}])

View File

@@ -260,7 +260,7 @@ describe Setting do
end end
describe ".available_locales" do describe ".available_locales" do
before { allow(I18n).to receive(:available_locales).and_return(%i[de en es pt-BR]) } before { allow(I18n).to receive_messages(default_locale: :de, available_locales: %i[de en es pt-BR]) }
it "uses I18n available locales by default" do it "uses I18n available locales by default" do
Setting["locales.enabled"] = "" Setting["locales.enabled"] = ""
@@ -280,6 +280,12 @@ describe Setting do
expect(Setting.enabled_locales).to eq %i[de en pt-BR] expect(Setting.enabled_locales).to eq %i[de en pt-BR]
end end
it "adds the default locale to the list of available locales" do
Setting["locales.enabled"] = "en es"
expect(Setting.enabled_locales).to eq %i[de en es]
end
it "ignores extra whitespace between locales" do it "ignores extra whitespace between locales" do
Setting["locales.enabled"] = " de en pt-BR " Setting["locales.enabled"] = " de en pt-BR "
@@ -293,9 +299,9 @@ describe Setting do
end end
it "ignores words that don't make sense in this context" do it "ignores words that don't make sense in this context" do
Setting["locales.enabled"] = "yes es 1234 en SuperCool" Setting["locales.enabled"] = "yes de 1234 en SuperCool"
expect(Setting.enabled_locales).to eq %i[es en] expect(Setting.enabled_locales).to eq %i[de en]
end end
it "uses I18n available locales when no locale is available" do it "uses I18n available locales when no locale is available" do
@@ -304,4 +310,44 @@ describe Setting do
expect(Setting.enabled_locales).to eq %i[de en es pt-BR] expect(Setting.enabled_locales).to eq %i[de en es pt-BR]
end end
end end
describe ".default_locale" do
before { allow(I18n).to receive_messages(default_locale: :en, available_locales: %i[de en es pt-BR]) }
it "uses I18n default locale by default" do
Setting["locales.default"] = ""
expect(Setting.default_locale).to eq :en
end
it "allows defining the default locale" do
Setting["locales.default"] = "de"
expect(Setting.default_locale).to eq :de
end
it "handles locales which include a dash" do
Setting["locales.default"] = "pt-BR"
expect(Setting.default_locale).to eq :"pt-BR"
end
it "ignores extra whitespace in the locale name" do
Setting["locales.default"] = " es "
expect(Setting.default_locale).to eq :es
end
it "ignores locales which aren't available" do
Setting["locales.default"] = "fr"
expect(Setting.default_locale).to eq :en
end
it "ignores an array of several locales" do
Setting["locales.default"] = "de es"
expect(Setting.default_locale).to eq :en
end
end
end end

View File

@@ -29,6 +29,7 @@ RSpec.describe SiteCustomization::ContentBlock do
end end
it "is not valid with a disabled locale" do it "is not valid with a disabled locale" do
Setting["locales.default"] = "nl"
Setting["locales.enabled"] = "nl pt-BR" Setting["locales.enabled"] = "nl pt-BR"
block.locale = "en" block.locale = "en"

View File

@@ -81,6 +81,16 @@ describe User do
end end
end end
describe "#locale" do
it "defaults to the default locale setting" do
Setting["locales.default"] = "nl"
user = build(:user, locale: nil)
expect(user.locale).to eq "nl"
end
end
describe "preferences" do describe "preferences" do
describe "email_on_comment" do describe "email_on_comment" do
it "is false by default" do it "is false by default" do