mirror of
https://github.com/discourse/discourse.git
synced 2026-08-03 09:53:24 -05:00
UX: additional AI translation chart adjustments (#35454)
Follow-up to be8b2b3, which wasn't quite enough adjustment to get the
desired outcome. At the moment it's correctly showing how many posts
need to be translated (1203), but it's still showing the total of posts
in the default locale (190284), rather than distinguishing the total
number of posts that need translating in that language.
<img width="2222" height="482" alt="image"
src="https://github.com/user-attachments/assets/0e4e1bee-0033-4b98-8f0c-b90d95845203"
/>
This PR further adjusts the chart to show "remaining work" for each
locale, rather than showing the total number of posts. So the outcome
should be more like
<img width="2210" height="638" alt="image"
src="https://github.com/user-attachments/assets/baa099d4-20f0-40bc-8fbb-3d2d5308e97a"
/>
I've also updated the tooltip on the chart to clarify that this is the
number of posts not in the language that need to be translated into it
This commit is contained in:
+3
-10
@@ -23,17 +23,10 @@ module DiscourseAi
|
||||
)
|
||||
end
|
||||
|
||||
candidates = DiscourseAi::Translation::PostCandidates
|
||||
candidates.get_total_and_with_locale_count in { total:, posts_with_detected_locale: }
|
||||
totals = DiscourseAi::Translation::PostCandidates.get_total_and_with_locale_count
|
||||
progress = DiscourseAi::Translation::PostCandidates.get_completion_all_locales
|
||||
|
||||
render json:
|
||||
base_result.merge(
|
||||
{
|
||||
translation_progress: candidates.get_completion_all_locales,
|
||||
total:,
|
||||
posts_with_detected_locale:,
|
||||
},
|
||||
)
|
||||
render json: base_result.merge(totals.merge(translation_progress: progress))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -86,11 +86,6 @@ export default class AiTranslations extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
get showDefaultLocaleNote() {
|
||||
const defaultLocale = this.siteSettings.default_locale;
|
||||
return this.selectedLocales.includes(defaultLocale);
|
||||
}
|
||||
|
||||
@action
|
||||
navigateToLocalizationSettings() {
|
||||
this.router.transitionTo("adminConfig.localization.settings", {
|
||||
@@ -197,7 +192,7 @@ export default class AiTranslations extends Component {
|
||||
}
|
||||
|
||||
get chartRightPadding() {
|
||||
const max = Math.max(...this.data.map(({ done }) => done));
|
||||
const max = Math.max(...this.data.map(({ total }) => total));
|
||||
switch (true) {
|
||||
case max >= 100000:
|
||||
return 90;
|
||||
@@ -232,21 +227,21 @@ export default class AiTranslations extends Component {
|
||||
}
|
||||
|
||||
const colors = this.chartColors;
|
||||
const defaultLocale = this.siteSettings.default_locale;
|
||||
|
||||
const processedData = this.data.map(({ locale, total, done }) => {
|
||||
const donePercentage = (total > 0 ? (done / total) * 100 : 0).toFixed(0);
|
||||
const isDefault = locale === defaultLocale;
|
||||
const localeName = this.languageNameLookup.getLanguageName(locale);
|
||||
const languageNameForTooltip = localeName.split(" (")[0];
|
||||
|
||||
return {
|
||||
locale: isDefault ? `${localeName}*` : localeName,
|
||||
locale: localeName,
|
||||
done,
|
||||
total,
|
||||
donePercentage,
|
||||
tooltip: [
|
||||
i18n("discourse_ai.translations.progress_chart.tooltip_translated", {
|
||||
done,
|
||||
total,
|
||||
language: languageNameForTooltip,
|
||||
}),
|
||||
],
|
||||
};
|
||||
@@ -257,7 +252,7 @@ export default class AiTranslations extends Component {
|
||||
{
|
||||
tooltip: processedData.map(({ tooltip }) => tooltip),
|
||||
data: processedData.map(({ donePercentage }) => donePercentage),
|
||||
totalItems: processedData.map(({ done }) => done),
|
||||
totalItems: processedData.map(({ total }) => total),
|
||||
backgroundColor: colors.progress,
|
||||
barThickness: 30,
|
||||
borderRadius: 4,
|
||||
@@ -454,13 +449,6 @@ export default class AiTranslations extends Component {
|
||||
class="ai-translations__chart"
|
||||
/>
|
||||
</div>
|
||||
{{#if this.showDefaultLocaleNote}}
|
||||
<div class="ai-translations__default-locale-note">
|
||||
{{i18n
|
||||
"discourse_ai.translations.progress_chart.default_locale_note"
|
||||
}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</:content>
|
||||
</AdminConfigAreaCard>
|
||||
|
||||
|
||||
-8
@@ -8,14 +8,6 @@
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
&__default-locale-note {
|
||||
margin-top: var(--space-4);
|
||||
padding: var(--space-2);
|
||||
font-size: var(--font-down-1);
|
||||
color: var(--primary-medium);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&__locale-input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -342,13 +342,12 @@ en:
|
||||
incomplete_language_detection_description: "%{done} of %{total} eligible posts have their language detected."
|
||||
backfill_disabled: "Backfilling is disabled, only new posts will be translated."
|
||||
description_tooltip:
|
||||
one: 'There is %{count} post eligible for translation.'
|
||||
other: 'There are %{count} posts eligible for translation.'
|
||||
one: 'There is %{count} post ready to translate.'
|
||||
other: 'There are %{count} posts ready to translate.'
|
||||
progress_chart:
|
||||
title: "Translation Progress"
|
||||
data_label: "%{percentage}%"
|
||||
tooltip_translated: "%{done} out of %{total} posts translated"
|
||||
default_locale_note: "*This is the site's default locale. Only posts needing translation into it are shown."
|
||||
tooltip_translated: "%{done} out of %{total} non-%{language} posts translated"
|
||||
bar_done:
|
||||
one: "%{count} post"
|
||||
other: "%{count} posts"
|
||||
|
||||
@@ -85,14 +85,12 @@ module DiscourseAi
|
||||
|
||||
def self.completion_all_locales
|
||||
supported = SiteSetting.content_localization_supported_locales.split("|")
|
||||
default_locale = SiteSetting.default_locale
|
||||
values_rows = supported.map { |loc| "('#{loc}')" }.join(", ")
|
||||
|
||||
sql = <<~SQL
|
||||
WITH supported AS (
|
||||
SELECT localestr,
|
||||
split_part(localestr, '_', 1) AS base,
|
||||
(localestr = '#{default_locale}') AS is_default
|
||||
split_part(localestr, '_', 1) AS base
|
||||
FROM (VALUES #{values_rows}) AS t(localestr)
|
||||
),
|
||||
eligible_posts AS (
|
||||
@@ -101,12 +99,12 @@ module DiscourseAi
|
||||
all_posts_count AS (
|
||||
SELECT COUNT(*)::bigint AS count FROM eligible_posts
|
||||
),
|
||||
non_default_locale_counts AS (
|
||||
non_target_locale_counts AS (
|
||||
SELECT s.base,
|
||||
COUNT(*)::bigint AS count
|
||||
FROM eligible_posts p
|
||||
CROSS JOIN supported s
|
||||
WHERE s.is_default AND split_part(p.locale, '_', 1) != s.base
|
||||
WHERE split_part(p.locale, '_', 1) != s.base
|
||||
GROUP BY s.base
|
||||
),
|
||||
done_per_base AS (
|
||||
@@ -114,25 +112,20 @@ module DiscourseAi
|
||||
COUNT(*)::bigint AS done
|
||||
FROM eligible_posts p
|
||||
JOIN supported s ON TRUE
|
||||
WHERE split_part(p.locale, '_', 1) = s.base
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM post_localizations pl
|
||||
WHERE pl.post_id = p.id
|
||||
AND split_part(pl.locale, '_', 1) = s.base
|
||||
)
|
||||
WHERE split_part(p.locale, '_', 1) != s.base AND EXISTS (
|
||||
SELECT 1
|
||||
FROM post_localizations pl
|
||||
WHERE pl.post_id = p.id
|
||||
AND split_part(pl.locale, '_', 1) = s.base
|
||||
)
|
||||
GROUP BY s.base
|
||||
)
|
||||
SELECT s.localestr AS locale,
|
||||
COALESCE(d.done, 0) AS done,
|
||||
CASE
|
||||
WHEN s.is_default THEN COALESCE(ndl.count, 0)
|
||||
ELSE a.count
|
||||
END AS total
|
||||
COALESCE(ntl.count, 0) AS total
|
||||
FROM supported s
|
||||
LEFT JOIN done_per_base d ON d.base = s.base
|
||||
LEFT JOIN non_default_locale_counts ndl ON ndl.base = s.base
|
||||
CROSS JOIN all_posts_count a
|
||||
LEFT JOIN non_target_locale_counts ntl ON ntl.base = s.base
|
||||
SQL
|
||||
|
||||
DB.query(sql).map { |r| { locale: r.locale, done: r.done, total: r.total } }
|
||||
|
||||
@@ -60,7 +60,11 @@ describe DiscourseAi::Translation::PostCandidates do
|
||||
end
|
||||
|
||||
describe ".get_completion_all_locales" do
|
||||
before { SiteSetting.content_localization_supported_locales = "en_GB|pt|es" }
|
||||
before do
|
||||
SiteSetting.content_localization_supported_locales = "en_GB|pt|es"
|
||||
SiteSetting.ai_translation_backfill_max_age_days = 30
|
||||
SiteSetting.ai_translation_backfill_limit_to_public_content = false
|
||||
end
|
||||
|
||||
it "returns empty state when no posts exist" do
|
||||
Post.delete_all
|
||||
@@ -78,11 +82,18 @@ describe DiscourseAi::Translation::PostCandidates do
|
||||
it "returns progress grouped by base locale (of en_GB) and correct totals" do
|
||||
post1 = Fabricate(:post, locale: "en_GB")
|
||||
post2 = Fabricate(:post, locale: "fr")
|
||||
Fabricate(:post, locale: "es")
|
||||
post3 = Fabricate(:post, locale: "es")
|
||||
Fabricate(:post, locale: nil) # not eligible
|
||||
|
||||
# add an en_GB localization to a non-en base post
|
||||
Fabricate(:post_localization, post: post2, locale: "en")
|
||||
PostLocalization.create!(
|
||||
post: post2,
|
||||
locale: "en",
|
||||
raw: "Translated to English",
|
||||
cooked: "<p>Translated to English</p>",
|
||||
post_version: post2.version,
|
||||
localizer_user_id: Discourse.system_user.id,
|
||||
)
|
||||
|
||||
result = DiscourseAi::Translation::PostCandidates.completion_all_locales
|
||||
expect(result.length).to eq(3)
|
||||
@@ -91,9 +102,9 @@ describe DiscourseAi::Translation::PostCandidates do
|
||||
|
||||
en_entry = result.find { |r| r[:locale] == "en_GB" }
|
||||
expect(en_entry).to be_present
|
||||
# post1 (en_GB base=en) + post2 (localization en_GB base=en)
|
||||
expect(en_entry[:done]).to eq(2)
|
||||
expect(en_entry[:total]).to eq(3)
|
||||
# total is non-English posts (post2 + post3)
|
||||
expect(en_entry[:done]).to eq(1)
|
||||
expect(en_entry[:total]).to eq(2)
|
||||
|
||||
pt_entry = result.find { |r| r[:locale] == "pt" }
|
||||
expect(pt_entry).to be_present
|
||||
@@ -101,8 +112,8 @@ describe DiscourseAi::Translation::PostCandidates do
|
||||
expect(pt_entry[:total]).to eq(3)
|
||||
es_entry = result.find { |r| r[:locale] == "es" }
|
||||
expect(es_entry).to be_present
|
||||
expect(es_entry[:done]).to eq(1)
|
||||
expect(es_entry[:total]).to eq(3)
|
||||
expect(es_entry[:done]).to eq(0)
|
||||
expect(es_entry[:total]).to eq(2)
|
||||
fr_entry = result.find { |r| r[:locale] == "fr" }
|
||||
expect(fr_entry).to be_nil
|
||||
end
|
||||
|
||||
@@ -22,10 +22,19 @@ describe DiscourseAi::Admin::AiTranslationsController do
|
||||
SiteSetting.ai_translation_backfill_max_age_days = 30
|
||||
SiteSetting.ai_translation_backfill_limit_to_public_content = false
|
||||
|
||||
Fabricate.times(14, :post, locale: "en")
|
||||
Fabricate.times(1, :post, locale: "fr")
|
||||
english_posts = Fabricate.times(14, :post, locale: "en")
|
||||
french_post = Fabricate(:post, locale: "fr")
|
||||
Fabricate.times(4, :post)
|
||||
|
||||
PostLocalization.create!(
|
||||
post: french_post,
|
||||
locale: "en",
|
||||
raw: "Translated to English",
|
||||
cooked: "<p>Translated to English</p>",
|
||||
post_version: french_post.version,
|
||||
localizer_user_id: admin.id,
|
||||
)
|
||||
|
||||
get "/admin/plugins/discourse-ai/ai-translations.json"
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
@@ -43,17 +52,70 @@ describe DiscourseAi::Admin::AiTranslationsController do
|
||||
expect(locale_data["locale"]).to eq("en")
|
||||
# en is the default locale, so total should only be posts requiring translation (1 French post)
|
||||
expect(locale_data["total"]).to eq(1)
|
||||
expect(locale_data["done"]).to eq(14)
|
||||
# done should be 1 because we translated the French post to English
|
||||
expect(locale_data["done"]).to eq(1)
|
||||
end
|
||||
|
||||
it "shows all posts for non-default locales but only posts requiring translation for the default locale" do
|
||||
it "shows only posts requiring translation for all locales (consistent behavior)" do
|
||||
SiteSetting.ai_translation_backfill_max_age_days = 30
|
||||
SiteSetting.ai_translation_backfill_limit_to_public_content = false
|
||||
SiteSetting.default_locale = "en"
|
||||
|
||||
Fabricate.times(100, :post, locale: "en")
|
||||
Fabricate.times(10, :post, locale: "fr")
|
||||
Fabricate.times(5, :post, locale: "es")
|
||||
english_posts = Fabricate.times(100, :post, locale: "en")
|
||||
french_posts = Fabricate.times(10, :post, locale: "fr")
|
||||
spanish_posts = Fabricate.times(5, :post, locale: "es")
|
||||
|
||||
french_posts
|
||||
.take(8)
|
||||
.each do |post|
|
||||
PostLocalization.create!(
|
||||
post: post,
|
||||
locale: "en",
|
||||
raw: "Translated to English",
|
||||
cooked: "<p>Translated to English</p>",
|
||||
post_version: post.version,
|
||||
localizer_user_id: admin.id,
|
||||
)
|
||||
end
|
||||
spanish_posts
|
||||
.take(3)
|
||||
.each do |post|
|
||||
PostLocalization.create!(
|
||||
post: post,
|
||||
locale: "en",
|
||||
raw: "Translated to English",
|
||||
cooked: "<p>Translated to English</p>",
|
||||
post_version: post.version,
|
||||
localizer_user_id: admin.id,
|
||||
)
|
||||
end
|
||||
|
||||
english_posts
|
||||
.take(50)
|
||||
.each do |post|
|
||||
PostLocalization.create!(
|
||||
post: post,
|
||||
locale: "fr",
|
||||
raw: "Translated to French",
|
||||
cooked: "<p>Translated to French</p>",
|
||||
post_version: post.version,
|
||||
localizer_user_id: admin.id,
|
||||
)
|
||||
end
|
||||
|
||||
english_posts
|
||||
.drop(50)
|
||||
.take(30)
|
||||
.each do |post|
|
||||
PostLocalization.create!(
|
||||
post: post,
|
||||
locale: "es",
|
||||
raw: "Translated to Spanish",
|
||||
cooked: "<p>Translated to Spanish</p>",
|
||||
post_version: post.version,
|
||||
localizer_user_id: admin.id,
|
||||
)
|
||||
end
|
||||
|
||||
get "/admin/plugins/discourse-ai/ai-translations.json"
|
||||
|
||||
@@ -65,14 +127,20 @@ describe DiscourseAi::Admin::AiTranslationsController do
|
||||
fr_data = progress.find { |p| p["locale"] == "fr" }
|
||||
es_data = progress.find { |p| p["locale"] == "es" }
|
||||
|
||||
# 15 non-English posts (10 fr + 5 es)
|
||||
expect(en_data["total"]).to eq(15)
|
||||
expect(en_data["done"]).to eq(100)
|
||||
# 11 translated to English (8 fr + 3 es)
|
||||
expect(en_data["done"]).to eq(11)
|
||||
|
||||
expect(fr_data["total"]).to eq(115)
|
||||
expect(fr_data["done"]).to eq(10)
|
||||
# 105 non-French posts (100 en + 5 es)
|
||||
expect(fr_data["total"]).to eq(105)
|
||||
# 50 translated to French
|
||||
expect(fr_data["done"]).to eq(50)
|
||||
|
||||
expect(es_data["total"]).to eq(115)
|
||||
expect(es_data["done"]).to eq(5)
|
||||
# 110 non-Spanish posts (100 en + 10 fr)
|
||||
expect(es_data["total"]).to eq(110)
|
||||
# 30 translated to Spanish
|
||||
expect(es_data["done"]).to eq(30)
|
||||
end
|
||||
|
||||
it "returns empty when no locales are supported" do
|
||||
|
||||
Reference in New Issue
Block a user