2025-05-29 17:28:06 +08:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
describe Jobs::LocalizeCategories do
|
|
|
|
subject(:job) { described_class.new }
|
|
|
|
|
|
|
|
def localize_all_categories(*locales)
|
|
|
|
Category.all.each do |category|
|
|
|
|
locales.each { |locale| Fabricate(:category_localization, category:, locale:, name: "x") }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
before do
|
2025-07-16 10:56:18 -07:00
|
|
|
assign_fake_provider_to(:ai_default_llm_model)
|
2025-05-29 17:28:06 +08:00
|
|
|
SiteSetting.discourse_ai_enabled = true
|
|
|
|
SiteSetting.ai_translation_enabled = true
|
2025-07-09 22:21:51 +08:00
|
|
|
SiteSetting.content_localization_supported_locales = "pt_BR|zh_CN"
|
2025-05-29 17:28:06 +08:00
|
|
|
|
|
|
|
Jobs.run_immediately!
|
|
|
|
end
|
|
|
|
|
|
|
|
it "does nothing when DiscourseAi::Translation::CategoryLocalizer is disabled" do
|
|
|
|
SiteSetting.discourse_ai_enabled = false
|
|
|
|
|
|
|
|
DiscourseAi::Translation::CategoryLocalizer.expects(:localize).never
|
|
|
|
|
2025-06-21 15:45:09 +08:00
|
|
|
job.execute({ limit: 10 })
|
2025-05-29 17:28:06 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
it "does nothing when ai_translation_enabled is disabled" do
|
|
|
|
SiteSetting.ai_translation_enabled = false
|
|
|
|
|
|
|
|
DiscourseAi::Translation::CategoryLocalizer.expects(:localize).never
|
|
|
|
|
2025-06-21 15:45:09 +08:00
|
|
|
job.execute({ limit: 10 })
|
2025-05-29 17:28:06 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
it "does nothing when no target languages are configured" do
|
2025-06-19 12:23:56 +08:00
|
|
|
SiteSetting.content_localization_supported_locales = ""
|
2025-05-29 17:28:06 +08:00
|
|
|
|
|
|
|
DiscourseAi::Translation::CategoryLocalizer.expects(:localize).never
|
|
|
|
|
2025-06-21 15:45:09 +08:00
|
|
|
job.execute({ limit: 10 })
|
2025-05-29 17:28:06 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
it "does nothing when no categories exist" do
|
|
|
|
Category.destroy_all
|
|
|
|
|
|
|
|
DiscourseAi::Translation::CategoryLocalizer.expects(:localize).never
|
|
|
|
|
2025-06-21 15:45:09 +08:00
|
|
|
job.execute({ limit: 10 })
|
|
|
|
end
|
|
|
|
|
|
|
|
it "does nothing when the limit is zero" do
|
|
|
|
DiscourseAi::Translation::CategoryLocalizer.expects(:localize).never
|
|
|
|
|
|
|
|
expect { job.execute({}) }.to raise_error(Discourse::InvalidParameters, /limit/)
|
|
|
|
job.execute({ limit: 0 })
|
2025-05-29 17:28:06 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
it "translates categories to the configured locales" do
|
2025-06-06 22:41:48 +08:00
|
|
|
Category.update_all(locale: "en")
|
2025-05-29 17:28:06 +08:00
|
|
|
number_of_categories = Category.count
|
2025-06-06 22:41:48 +08:00
|
|
|
|
2025-05-29 17:28:06 +08:00
|
|
|
DiscourseAi::Translation::CategoryLocalizer
|
|
|
|
.expects(:localize)
|
2025-07-09 22:21:51 +08:00
|
|
|
.with(is_a(Category), "pt_BR")
|
2025-05-29 17:28:06 +08:00
|
|
|
.times(number_of_categories)
|
|
|
|
DiscourseAi::Translation::CategoryLocalizer
|
|
|
|
.expects(:localize)
|
|
|
|
.with(is_a(Category), "zh_CN")
|
|
|
|
.times(number_of_categories)
|
|
|
|
|
2025-06-21 15:45:09 +08:00
|
|
|
job.execute({ limit: 10 })
|
2025-05-29 17:28:06 +08:00
|
|
|
end
|
|
|
|
|
2025-06-21 15:45:09 +08:00
|
|
|
it "limits the number of localizations" do
|
|
|
|
SiteSetting.content_localization_supported_locales = "pt"
|
|
|
|
|
|
|
|
6.times { Fabricate(:category) }
|
|
|
|
Category.update_all(locale: "en")
|
2025-05-29 17:28:06 +08:00
|
|
|
|
2025-06-06 22:41:48 +08:00
|
|
|
DiscourseAi::Translation::CategoryLocalizer
|
|
|
|
.expects(:localize)
|
2025-06-21 15:45:09 +08:00
|
|
|
.with(is_a(Category), "pt")
|
|
|
|
.times(5)
|
2025-05-29 17:28:06 +08:00
|
|
|
|
2025-06-21 15:45:09 +08:00
|
|
|
job.execute({ limit: 5 })
|
2025-05-29 17:28:06 +08:00
|
|
|
end
|
|
|
|
|
2025-06-21 15:45:09 +08:00
|
|
|
it "skips categories that already have localizations" do
|
|
|
|
localize_all_categories("pt", "zh_CN")
|
2025-05-29 17:28:06 +08:00
|
|
|
|
2025-07-09 22:21:51 +08:00
|
|
|
DiscourseAi::Translation::CategoryLocalizer
|
|
|
|
.expects(:localize)
|
|
|
|
.with(is_a(Category), "pt_BR")
|
|
|
|
.never
|
2025-05-29 17:28:06 +08:00
|
|
|
DiscourseAi::Translation::CategoryLocalizer
|
|
|
|
.expects(:localize)
|
2025-06-21 15:45:09 +08:00
|
|
|
.with(is_a(Category), "zh_CN")
|
2025-05-29 17:28:06 +08:00
|
|
|
.never
|
|
|
|
|
2025-06-21 15:45:09 +08:00
|
|
|
job.execute({ limit: 10 })
|
2025-05-29 17:28:06 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
it "handles translation errors gracefully" do
|
|
|
|
localize_all_categories("pt", "zh_CN")
|
|
|
|
|
2025-06-06 22:41:48 +08:00
|
|
|
category1 = Fabricate(:category, name: "First", description: "First description", locale: "en")
|
2025-05-29 17:28:06 +08:00
|
|
|
DiscourseAi::Translation::CategoryLocalizer
|
|
|
|
.expects(:localize)
|
2025-07-09 22:21:51 +08:00
|
|
|
.with(category1, "pt_BR")
|
|
|
|
.once
|
2025-05-29 17:28:06 +08:00
|
|
|
.raises(StandardError.new("API error"))
|
|
|
|
DiscourseAi::Translation::CategoryLocalizer.expects(:localize).with(category1, "zh_CN").once
|
|
|
|
|
2025-06-21 15:45:09 +08:00
|
|
|
expect { job.execute({ limit: 10 }) }.not_to raise_error
|
2025-05-29 17:28:06 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
it "skips read-restricted categories when configured" do
|
|
|
|
SiteSetting.ai_translation_backfill_limit_to_public_content = true
|
|
|
|
|
2025-06-06 22:41:48 +08:00
|
|
|
category1 = Fabricate(:category, name: "Public Category", read_restricted: false, locale: "en")
|
|
|
|
category2 = Fabricate(:category, name: "Private Category", read_restricted: true, locale: "en")
|
2025-05-29 17:28:06 +08:00
|
|
|
|
|
|
|
DiscourseAi::Translation::CategoryLocalizer
|
|
|
|
.expects(:localize)
|
|
|
|
.with(category1, any_parameters)
|
|
|
|
.twice
|
|
|
|
DiscourseAi::Translation::CategoryLocalizer
|
|
|
|
.expects(:localize)
|
|
|
|
.with(category2, any_parameters)
|
|
|
|
.never
|
|
|
|
|
2025-06-21 15:45:09 +08:00
|
|
|
job.execute({ limit: 10 })
|
2025-05-29 17:28:06 +08:00
|
|
|
end
|
2025-06-06 22:41:48 +08:00
|
|
|
|
|
|
|
it "skips creating localizations in the same language as the category's locale" do
|
|
|
|
Category.update_all(locale: "pt")
|
|
|
|
|
|
|
|
DiscourseAi::Translation::CategoryLocalizer.expects(:localize).with(is_a(Category), "pt").never
|
|
|
|
DiscourseAi::Translation::CategoryLocalizer
|
|
|
|
.expects(:localize)
|
|
|
|
.with(is_a(Category), "zh_CN")
|
|
|
|
.times(Category.count)
|
|
|
|
|
2025-06-21 15:45:09 +08:00
|
|
|
job.execute({ limit: 10 })
|
2025-06-06 22:41:48 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
it "deletes existing localizations that match the category's locale" do
|
|
|
|
# update all categories to portuguese
|
|
|
|
Category.update_all(locale: "pt")
|
|
|
|
|
|
|
|
localize_all_categories("pt", "zh_CN")
|
|
|
|
|
2025-06-21 15:45:09 +08:00
|
|
|
expect { job.execute({ limit: 10 }) }.to change {
|
|
|
|
CategoryLocalization.exists?(locale: "pt")
|
|
|
|
}.from(true).to(false)
|
2025-06-06 22:41:48 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
it "doesn't process categories with nil locale" do
|
|
|
|
# Add a category with nil locale
|
|
|
|
nil_locale_category = Fabricate(:category, name: "No Locale", locale: nil)
|
|
|
|
|
|
|
|
# Make sure our query for categories with non-null locales excludes it
|
|
|
|
DiscourseAi::Translation::CategoryLocalizer
|
|
|
|
.expects(:localize)
|
|
|
|
.with(nil_locale_category, any_parameters)
|
|
|
|
.never
|
|
|
|
|
2025-06-21 15:45:09 +08:00
|
|
|
job.execute({ limit: 10 })
|
2025-06-06 22:41:48 +08:00
|
|
|
end
|
2025-05-29 17:28:06 +08:00
|
|
|
end
|