DEV: Catch missing translations during test runs (#26258)

This configuration makes it so that a missing translation will raise an error during test execution. Better discover there than after deploy.
This commit is contained in:
Ted Johansson 2024-05-24 22:15:53 +08:00 committed by GitHub
parent 9db83c37e4
commit 69205cb1e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 219 additions and 76 deletions

View File

@ -69,7 +69,7 @@ class UserEmail < ActiveRecord::Base
self.errors.add( self.errors.add(
:user_id, :user_id,
I18n.t( I18n.t(
"active_record.errors.model.user_email.attributes.user_id.reassigning_primary_email", "activerecord.errors.models.user_email.attributes.user_id.reassigning_primary_email",
), ),
) )
end end

View File

@ -63,6 +63,9 @@ Discourse::Application.configure do
}, },
] ]
# Catch missing translations during test runs.
config.i18n.raise_on_missing_translations = true
config.after_initialize do config.after_initialize do
ActiveRecord::LogSubscriber.backtrace_cleaner.add_silencer do |line| ActiveRecord::LogSubscriber.backtrace_cleaner.add_silencer do |line|
line =~ %r{lib/freedom_patches} line =~ %r{lib/freedom_patches}

View File

@ -556,6 +556,7 @@ en:
private_message_abbrev: "Msg" private_message_abbrev: "Msg"
rss_description: rss_description:
hot: "Hot topics"
latest: "Latest topics" latest: "Latest topics"
top: "Top topics" top: "Top topics"
top_all: "All time top topics" top_all: "All time top topics"
@ -1472,6 +1473,7 @@ en:
description: "External sources that have linked to this site the most." description: "External sources that have linked to this site the most."
top_referred_topics: top_referred_topics:
title: "Top Referred Topics" title: "Top Referred Topics"
xaxis: ""
labels: labels:
num_clicks: "Clicks" num_clicks: "Clicks"
topic: "Topic" topic: "Topic"

View File

@ -10,6 +10,7 @@ class MigrateOldModeratorPosts < ActiveRecord::Migration[4.2]
end end
def up def up
Rails.application.config.i18n.raise_on_missing_translations = false
migrate_key("closed.enabled") migrate_key("closed.enabled")
migrate_key("closed.disabled") migrate_key("closed.disabled")
migrate_key("archived.enabled") migrate_key("archived.enabled")
@ -18,5 +19,6 @@ class MigrateOldModeratorPosts < ActiveRecord::Migration[4.2]
migrate_key("pinned.disabled") migrate_key("pinned.disabled")
migrate_key("pinned_globally.enabled") migrate_key("pinned_globally.enabled")
migrate_key("pinned_globally.disabled") migrate_key("pinned_globally.disabled")
Rails.application.config.i18n.raise_on_missing_translations = true
end end
end end

View File

@ -36,6 +36,11 @@ class Archetype
@archetypes[name] = Archetype.new(name, options) @archetypes[name] = Archetype.new(name, options)
end end
def self.deregister(name)
@archetypes ||= {}
@archetypes.delete(name)
end
# default archetypes # default archetypes
register "regular" register "regular"
register "private_message" register "private_message"

View File

@ -122,7 +122,8 @@ module I18n
dup_options = nil dup_options = nil
if options if options
dup_options = options.dup dup_options = options.dup
should_raise = dup_options.delete(:raise) should_raise =
dup_options.delete(:raise) || Rails.application.config.i18n.raise_on_missing_translations
locale = dup_options.delete(:locale) locale = dup_options.delete(:locale)
end end

View File

@ -3,7 +3,26 @@
describe DiscourseAutomation::AdminAutomationsController do describe DiscourseAutomation::AdminAutomationsController do
fab!(:automation) fab!(:automation)
before { SiteSetting.discourse_automation_enabled = true } before do
SiteSetting.discourse_automation_enabled = true
I18n.backend.store_translations(
:en,
{
discourse_automation: {
scriptables: {
something_about_us: {
title: "Something about us.",
description: "We rock!",
},
},
triggerables: {
title: "Triggerables",
description: "Triggerables",
},
},
},
)
end
describe "#show" do describe "#show" do
context "when logged in as an admin" do context "when logged in as an admin" do

View File

@ -41,6 +41,19 @@ describe DiscourseAutomation::AutomationSerializer do
DiscourseAutomation::Scriptable.add("foo") do DiscourseAutomation::Scriptable.add("foo") do
field :bar, component: :text, triggerable: DiscourseAutomation::Triggers::TOPIC field :bar, component: :text, triggerable: DiscourseAutomation::Triggers::TOPIC
end end
I18n.backend.store_translations(
:en,
{
discourse_automation: {
scriptables: {
foo: {
title: "Something about us.",
description: "We rock!",
},
},
},
},
)
end end
context "when automation is not using the specific trigger" do context "when automation is not using the specific trigger" do

View File

@ -6,6 +6,34 @@ describe "DiscourseAutomation | smoke test", type: :system, js: true do
fab!(:badge) { Fabricate(:badge, name: "badge") } fab!(:badge) { Fabricate(:badge, name: "badge") }
before do before do
I18n.backend.store_translations(
:en,
{
discourse_automation: {
scriptables: {
test: {
title: "Test",
description: "Test",
},
something_about_us: {
title: "Something about us.",
description: "We rock!",
},
nothing_about_us: {
title: "Nothing about us.",
description: "We don't rock!",
},
},
triggerables: {
title: "Triggerable",
description: "Triggerable",
user_first_logged_in: {
description: "User first logged in.",
},
},
},
},
)
SiteSetting.discourse_automation_enabled = true SiteSetting.discourse_automation_enabled = true
sign_in(admin) sign_in(admin)
end end

View File

@ -23,6 +23,19 @@ describe "StalledWiki" do
before do before do
automation.upsert_field!("stalled_after", "choices", { value: "PT10H" }, target: "trigger") automation.upsert_field!("stalled_after", "choices", { value: "PT10H" }, target: "trigger")
automation.upsert_field!("retriggered_after", "choices", { value: "PT1H" }, target: "trigger") automation.upsert_field!("retriggered_after", "choices", { value: "PT1H" }, target: "trigger")
I18n.backend.store_translations(
:en,
{
discourse_automation: {
scriptables: {
something_about_us: {
title: "Something about us.",
description: "We rock!",
},
},
},
},
)
end end
it "supports manual triggering" do it "supports manual triggering" do

View File

@ -615,7 +615,7 @@ RSpec.describe DiscourseNarrativeBot::TrackSelector do
it "should not trigger the bot" do it "should not trigger the bot" do
post.update!( post.update!(
raw: raw:
"`@discobot #{I18n.t("discourse_narrative_bot.track_selector.reset_trigger")} #{I18n.t(DiscourseNarrativeBot::NewUserNarrative.reset_trigger)}`", "`@discobot #{I18n.t("discourse_narrative_bot.track_selector.reset_trigger")} #{DiscourseNarrativeBot::NewUserNarrative.reset_trigger}`",
) )
expect { described_class.new(:reply, user, post_id: post.id).select }.to_not change { expect { described_class.new(:reply, user, post_id: post.id).select }.to_not change {
@ -750,7 +750,7 @@ RSpec.describe DiscourseNarrativeBot::TrackSelector do
user: Fabricate(:user), user: Fabricate(:user),
topic: topic, topic: topic,
raw: raw:
"@discobot #{I18n.t("discourse_narrative_bot.track_selector.reset_trigger")} #{I18n.t(DiscourseNarrativeBot::NewUserNarrative.reset_trigger)}", "@discobot #{I18n.t("discourse_narrative_bot.track_selector.reset_trigger")} #{DiscourseNarrativeBot::NewUserNarrative.reset_trigger}",
) )
user user

View File

@ -2,7 +2,9 @@
RSpec.describe Jobs::CheckTranslationOverrides do RSpec.describe Jobs::CheckTranslationOverrides do
fab!(:up_to_date_translation) { Fabricate(:translation_override, translation_key: "title") } fab!(:up_to_date_translation) { Fabricate(:translation_override, translation_key: "title") }
fab!(:deprecated_translation) { Fabricate(:translation_override, translation_key: "foo.bar") } fab!(:deprecated_translation) do
allow_missing_translations { Fabricate(:translation_override, translation_key: "foo.bar") }
end
fab!(:outdated_translation) do fab!(:outdated_translation) do
Fabricate(:translation_override, translation_key: "posts", original_translation: "outdated") Fabricate(:translation_override, translation_key: "posts", original_translation: "outdated")
end end

View File

@ -34,6 +34,10 @@ RSpec.describe Jobs::BulkUserTitleUpdate do
let(:customized_badge_name) { "Merit Badge" } let(:customized_badge_name) { "Merit Badge" }
before do before do
I18n.backend.store_translations(
:en,
{ badges: { protector_of_the_realm: { name: "Protector of the Realm" } } },
)
TranslationOverride.upsert!(I18n.locale, Badge.i18n_key(badge.name), customized_badge_name) TranslationOverride.upsert!(I18n.locale, Badge.i18n_key(badge.name), customized_badge_name)
BadgeGranter.grant(badge, user) BadgeGranter.grant(badge, user)
user.update(title: customized_badge_name) user.update(title: customized_badge_name)

View File

@ -25,12 +25,13 @@ RSpec.describe Archetype do
end end
end end
describe "register an archetype" do describe "register an archetype" do
it "has one more element" do it "has one more element" do
@list = Archetype.list.dup @list = Archetype.list.dup
Archetype.register("glados") Archetype.register("glados")
expect(Archetype.list.size).to eq(@list.size + 1) expect(Archetype.list.size).to eq(@list.size + 1)
expect(Archetype.list.find { |a| a.id == "glados" }).to be_present expect(Archetype.list.find { |a| a.id == "glados" }).to be_present
Archetype.deregister("glados")
end end
end end
end end

View File

@ -23,16 +23,20 @@ RSpec.describe "translate accelerator" do
I18n::MissingTranslationData, I18n::MissingTranslationData,
) )
orig = I18n.t("i_am_an_unknown_key99") allow_missing_translations do
orig = I18n.t("i_am_an_unknown_key99")
expect(I18n.t("i_am_an_unknown_key99").object_id).to eq(orig.object_id) expect(I18n.t("i_am_an_unknown_key99").object_id).to eq(orig.object_id)
expect(I18n.t("i_am_an_unknown_key99")).to eq("Translation missing: en.i_am_an_unknown_key99") expect(I18n.t("i_am_an_unknown_key99")).to eq("Translation missing: en.i_am_an_unknown_key99")
end
end end
it "has the same 'translation missing' message as upstream" do it "has the same 'translation missing' message as upstream" do
expect(I18n.t("this_key_does_not_exist")).to eq( allow_missing_translations do
I18n.translate_no_cache("this_key_does_not_exist"), expect(I18n.t("this_key_does_not_exist")).to eq(
) I18n.translate_no_cache("this_key_does_not_exist"),
)
end
end end
it "returns the correct language" do it "returns the correct language" do

View File

@ -236,31 +236,32 @@ RSpec.describe JsLocaleHelper do
end end
it "correctly evaluates message formats in en fallback" do it "correctly evaluates message formats in en fallback" do
JsLocaleHelper.set_translations("en", "en" => { "js" => { "something_MF" => "en mf" } }) allow_missing_translations do
JsLocaleHelper.set_translations("en", "en" => { "js" => { "something_MF" => "en mf" } })
JsLocaleHelper.set_translations("de", "de" => { "js" => { "something_MF" => "de mf" } })
JsLocaleHelper.set_translations("de", "de" => { "js" => { "something_MF" => "de mf" } }) TranslationOverride.upsert!("en", "js.something_MF", <<~MF.strip)
There {
UNREAD, plural,
=0 {are no}
one {is one unread}
other {are # unread}
}
MF
TranslationOverride.upsert!("en", "js.something_MF", <<~MF.strip) v8_ctx.eval(JsLocaleHelper.output_locale("de"))
There { v8_ctx.eval(JsLocaleHelper.output_client_overrides("de"))
UNREAD, plural, v8_ctx.eval(<<~JS)
=0 {are no} for (let [key, value] of Object.entries(I18n._mfOverrides || {})) {
one {is one unread} key = key.replace(/^[a-z_]*js\./, "");
other {are # unread} I18n._compiledMFs[key] = value;
} }
MF JS
v8_ctx.eval(JsLocaleHelper.output_locale("de")) expect(v8_ctx.eval("I18n.messageFormat('something_MF', { UNREAD: 1 })")).to eq(
v8_ctx.eval(JsLocaleHelper.output_client_overrides("de")) "There is one unread",
v8_ctx.eval(<<~JS) )
for (let [key, value] of Object.entries(I18n._mfOverrides || {})) { end
key = key.replace(/^[a-z_]*js\./, "");
I18n._compiledMFs[key] = value;
}
JS
expect(v8_ctx.eval("I18n.messageFormat('something_MF', { UNREAD: 1 })")).to eq(
"There is one unread",
)
end end
LocaleSiteSetting.values.each do |locale| LocaleSiteSetting.values.each do |locale|

View File

@ -17,6 +17,8 @@ RSpec.describe Plugin::Instance do
some_ruby some_ruby
TEXT TEXT
around { |example| allow_missing_translations(&example) }
after { DiscoursePluginRegistry.reset! } after { DiscoursePluginRegistry.reset! }
# NOTE: sample_plugin_site_settings.yml is always loaded in tests in site_setting.rb # NOTE: sample_plugin_site_settings.yml is always loaded in tests in site_setting.rb

View File

@ -330,6 +330,8 @@ RSpec.describe SiteSettingExtension do
describe "string setting with regex" do describe "string setting with regex" do
it "Supports custom validation errors" do it "Supports custom validation errors" do
I18n.backend.store_translations(:en, { oops: "oops" })
settings.setting(:test_str, "bob", regex: "hi", regex_error: "oops") settings.setting(:test_str, "bob", regex: "hi", regex_error: "oops")
settings.refresh! settings.refresh!

View File

@ -282,7 +282,7 @@ RSpec.describe IncomingLinksReport do
it "returns localized titles" do it "returns localized titles" do
stub_empty_referred_topics_data stub_empty_referred_topics_data
expect(top_referred_topics[:title]).to be_present expect(top_referred_topics[:title]).to be_present
expect(top_referred_topics[:xaxis]).to be_present expect(top_referred_topics[:xaxis]).to be_blank
expect(top_referred_topics[:ytitles]).to be_present expect(top_referred_topics[:ytitles]).to be_present
expect(top_referred_topics[:ytitles][:num_clicks]).to be_present expect(top_referred_topics[:ytitles][:num_clicks]).to be_present
end end

View File

@ -6,7 +6,7 @@ RSpec.describe TranslationOverride do
before do before do
I18n.backend.store_translations( I18n.backend.store_translations(
I18n.locale, I18n.locale,
"user_notifications.user_did_something" => "%{first} %{second}", { user_notifications: { user_did_something: "%{first} %{second}" } },
) )
I18n.backend.store_translations( I18n.backend.store_translations(
@ -21,7 +21,11 @@ RSpec.describe TranslationOverride do
describe "when interpolation keys are missing" do describe "when interpolation keys are missing" do
it "should not be valid" do it "should not be valid" do
translation_override = translation_override =
TranslationOverride.upsert!(I18n.locale, "some_key", "%{key} %{omg}") TranslationOverride.upsert!(
I18n.locale,
"user_notifications.user_did_something",
"%{key} %{omg}",
)
expect(translation_override.errors.full_messages).to include( expect(translation_override.errors.full_messages).to include(
I18n.t( I18n.t(
@ -129,15 +133,17 @@ RSpec.describe TranslationOverride do
describe "invalid keys" do describe "invalid keys" do
it "does not transform 'tonz'" do it "does not transform 'tonz'" do
translation_override = allow_missing_translations do
TranslationOverride.upsert!(I18n.locale, "something.tonz", "%{key3} %{key4} hello") translation_override =
expect(translation_override.errors.full_messages).to include( TranslationOverride.upsert!(I18n.locale, "something.tonz", "%{key3} %{key4} hello")
I18n.t( expect(translation_override.errors.full_messages).to include(
"activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys", I18n.t(
keys: "key3, key4", "activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys",
count: 2, keys: "key3, key4",
), count: 2,
) ),
)
end
end end
end end
end end
@ -145,6 +151,7 @@ RSpec.describe TranslationOverride do
end end
it "upserts values" do it "upserts values" do
I18n.backend.store_translations(:en, { some: { key: "initial value" } })
TranslationOverride.upsert!("en", "some.key", "some value") TranslationOverride.upsert!("en", "some.key", "some value")
ovr = TranslationOverride.where(locale: "en", translation_key: "some.key").first ovr = TranslationOverride.where(locale: "en", translation_key: "some.key").first
@ -164,6 +171,7 @@ RSpec.describe TranslationOverride do
end end
it "stores js for a message format key" do it "stores js for a message format key" do
I18n.backend.store_translations(:en, { some: { key_MF: "initial value" } })
TranslationOverride.upsert!( TranslationOverride.upsert!(
"ru", "ru",
"some.key_MF", "some.key_MF",
@ -300,7 +308,9 @@ RSpec.describe TranslationOverride do
end end
context "when the original translation no longer exists" do context "when the original translation no longer exists" do
fab!(:translation) { Fabricate(:translation_override, translation_key: "foo.bar") } fab!(:translation) do
allow_missing_translations { Fabricate(:translation_override, translation_key: "foo.bar") }
end
it { expect(translation.original_translation_deleted?).to eq(true) } it { expect(translation.original_translation_deleted?).to eq(true) }
end end

View File

@ -2746,7 +2746,7 @@ RSpec.describe User do
end end
describe "#title=" do describe "#title=" do
fab!(:badge) { Fabricate(:badge, name: "Badge", allow_title: false) } fab!(:badge) { Badge.find_by(name: "Welcome") }
it "sets granted_title_badge_id correctly" do it "sets granted_title_badge_id correctly" do
BadgeGranter.grant(badge, user) BadgeGranter.grant(badge, user)

View File

@ -204,6 +204,7 @@ RSpec.configure do |config|
config.include FastImageHelpers config.include FastImageHelpers
config.include WithServiceHelper config.include WithServiceHelper
config.include ServiceMatchers config.include ServiceMatchers
config.include I18nHelpers
config.mock_framework = :mocha config.mock_framework = :mocha
config.order = "random" config.order = "random"

View File

@ -108,33 +108,35 @@ RSpec.describe Admin::SiteTextsController do
end end
it "does not return overrides for keys that do not exist in English" do it "does not return overrides for keys that do not exist in English" do
SiteSetting.default_locale = :ru allow_missing_translations do
TranslationOverride.create!( SiteSetting.default_locale = :ru
locale: :ru, TranslationOverride.create!(
translation_key: "missing_plural_key.one", locale: :ru,
value: "ONE", translation_key: "missing_plural_key.one",
) value: "ONE",
TranslationOverride.create!( )
locale: :ru, TranslationOverride.create!(
translation_key: "another_missing_key", locale: :ru,
value: "foo", translation_key: "another_missing_key",
) value: "foo",
)
get "/admin/customize/site_texts.json", get "/admin/customize/site_texts.json",
params: { params: {
q: "missing_plural_key", q: "missing_plural_key",
locale: default_locale, locale: default_locale,
} }
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.parsed_body["site_texts"]).to be_empty expect(response.parsed_body["site_texts"]).to be_empty
get "/admin/customize/site_texts.json", get "/admin/customize/site_texts.json",
params: { params: {
q: "another_missing_key", q: "another_missing_key",
locale: default_locale, locale: default_locale,
} }
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.parsed_body["site_texts"]).to be_empty expect(response.parsed_body["site_texts"]).to be_empty
end
end end
it "returns site text from fallback locale if current locale doesn't have a translation" do it "returns site text from fallback locale if current locale doesn't have a translation" do

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
RSpec.describe ExtraLocalesController do RSpec.describe ExtraLocalesController do
around { |example| allow_missing_translations(&example) }
describe "#show" do describe "#show" do
it "won't work with a weird parameter" do it "won't work with a weird parameter" do
get "/extra-locales/-invalid..character!!" get "/extra-locales/-invalid..character!!"

View File

@ -3172,7 +3172,10 @@ RSpec.describe UsersController do
fab!(:badge) { Fabricate(:badge, name: "Demogorgon", allow_title: true) } fab!(:badge) { Fabricate(:badge, name: "Demogorgon", allow_title: true) }
let(:user_badge) { BadgeGranter.grant(badge, user1) } let(:user_badge) { BadgeGranter.grant(badge, user1) }
before { TranslationOverride.upsert!("en", "badges.demogorgon.name", "Boss") } before do
I18n.backend.store_translations(:en, { badges: { demogorgon: { name: "D'Artagnan" } } })
TranslationOverride.upsert!("en", "badges.demogorgon.name", "Boss")
end
after { TranslationOverride.revert!("en", ["badges.demogorgon.name"]) } after { TranslationOverride.revert!("en", ["badges.demogorgon.name"]) }

View File

@ -3,7 +3,10 @@
require_relative "../../../script/import_scripts/base" require_relative "../../../script/import_scripts/base"
RSpec.describe ImportScripts::Base do RSpec.describe ImportScripts::Base do
before { STDOUT.stubs(:write) } before do
I18n.backend.store_translations(:en, { test: "Test" })
STDOUT.stubs(:write)
end
class MockSpecImporter < ImportScripts::Base class MockSpecImporter < ImportScripts::Base
def initialize(data) def initialize(data)

View File

@ -330,6 +330,10 @@ RSpec.describe BadgeGranter do
let(:customized_badge_name) { "Merit Badge" } let(:customized_badge_name) { "Merit Badge" }
before do before do
I18n.backend.store_translations(
:en,
{ badges: { Badge.i18n_name(badge.name) => { name: "Badge 0" } } },
)
TranslationOverride.upsert!(I18n.locale, Badge.i18n_key(badge.name), customized_badge_name) TranslationOverride.upsert!(I18n.locale, Badge.i18n_key(badge.name), customized_badge_name)
end end
@ -381,6 +385,10 @@ RSpec.describe BadgeGranter do
it "removes custom badge titles" do it "removes custom badge titles" do
custom_badge_title = "this is a badge title" custom_badge_title = "this is a badge title"
I18n.backend.store_translations(
:en,
{ badges: { Badge.i18n_name(badge.name) => { name: "Badge 0" } } },
)
TranslationOverride.create!( TranslationOverride.create!(
translation_key: badge.translation_key, translation_key: badge.translation_key,
value: custom_badge_title, value: custom_badge_title,

View File

@ -3,6 +3,8 @@
RSpec.describe ProblemCheck::TranslationOverrides do RSpec.describe ProblemCheck::TranslationOverrides do
subject(:check) { described_class.new } subject(:check) { described_class.new }
around { |example| allow_missing_translations(&example) }
describe ".call" do describe ".call" do
before { Fabricate(:translation_override, status: status) } before { Fabricate(:translation_override, status: status) }

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
module I18nHelpers
def allow_missing_translations
Rails.application.config.i18n.raise_on_missing_translations = false
yield
ensure
Rails.application.config.i18n.raise_on_missing_translations = true
end
end