UX: Allow user to set post and topic title language when manually creating post translations in modal (#41734)

Related:
https://meta.discourse.org/t/manual-localization-doesnt-seem-to-be-working/407319

Users have gotten tripped over and over again when manually inserting
translations for posts, not knowing they need to set the post language
first before the translation is shown (explained
[here](https://meta.discourse.org/t/manual-localization-doesnt-seem-to-be-working/407319/7?u=nat)).

This PR adds the ability to allow the user to set the post language in
the modal when they are working with translations, highlighting the fact
that they need to set the post language before translations can be shown
This commit is contained in:
Natalie Tay
2026-07-16 10:43:39 +08:00
committed by GitHub
parent 3af83540b0
commit cb739f7f2d
19 changed files with 770 additions and 24 deletions
@@ -1,7 +1,44 @@
@use "lib/viewport";
.post-translations-modal {
table {
width: 100%;
}
&__language-settings {
display: grid;
gap: var(--space-3);
margin-block-end: var(--space-4);
}
&__language-control {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: var(--space-2);
align-items: end;
@include viewport.until(sm) {
display: contents;
}
}
&__language-actions {
display: flex;
gap: var(--space-1);
@include viewport.until(sm) {
order: 1;
}
&.is-hidden {
visibility: hidden;
pointer-events: none;
@include viewport.until(sm) {
display: none;
}
}
}
}
.fk-d-menu.post-language-selector-content {
@@ -54,6 +54,15 @@ class PostLocalizationsController < ApplicationController
end
end
def update_locale
post = Post.find_by(id: params[:post_id])
raise Discourse::NotFound unless post
updated_post =
PostLocaleUpdater.update(post:, locale: params.fetch(:locale).presence, user: current_user)
render json: { locale: updated_post.locale }, status: :ok
end
def destroy
post_id, locale = params.require(%i[post_id locale])
@@ -38,6 +38,15 @@ class TopicLocalizationsController < ApplicationController
end
end
def update_locale
topic = Topic.find_by(id: params[:topic_id])
raise Discourse::NotFound unless topic
updated_topic =
TopicLocaleUpdater.update(topic:, locale: params.fetch(:locale).presence, user: current_user)
render json: { locale: updated_topic.locale }, status: :ok
end
def destroy
topic_id, locale = params.require(%i[topic_id locale])
+9 -1
View File
@@ -4881,8 +4881,16 @@ en:
one: "View %{count} translated locale"
other: "View %{count} translated locales"
modal:
title: "Translations for post"
title: "Translations"
confirm_delete: "Are you sure you want to delete translations for '%{languageCode}'?"
language_notice: "Set the original post language so translations can be shown to readers."
post_language: "Post language"
topic_language: "Topic title language"
save_post_language: "Save post language"
save_topic_language: "Save topic title language"
discard_language_change: "Discard change"
post_language_updated: "Post language updated"
topic_language_updated: "Topic title language updated"
success: "Translations have been updated successfully"
post_language_selector:
title: "Post language"
+2
View File
@@ -1297,10 +1297,12 @@ Discourse::Application.routes.draw do
end
get "/post_localizations/:id" => "post_localizations#show"
put "/post_localizations/:post_id/locale" => "post_localizations#update_locale"
post "/post_localizations/create_or_update", to: "post_localizations#create_or_update"
delete "/post_localizations/destroy", to: "post_localizations#destroy"
get "topic_localizations/:topic_id/:locale" => "topic_localizations#show"
put "topic_localizations/:topic_id/locale" => "topic_localizations#update_locale"
post "topic_localizations/create_or_update", to: "topic_localizations#create_or_update"
delete "topic_localizations/destroy", to: "topic_localizations#destroy"
@@ -4,28 +4,70 @@ import { fn } from "@ember/helper";
import { action } from "@ember/object";
import { service } from "@ember/service";
import DEditorOriginalTranslationPreview from "discourse/components/d-editor-original-translation-preview";
import Form from "discourse/components/form";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import Composer from "discourse/models/composer";
import PostLocalization from "discourse/models/post-localization";
import TopicLocalization from "discourse/models/topic-localization";
import { eq } from "discourse/truth-helpers";
import DButton from "discourse/ui-kit/d-button";
import DConditionalLoadingSpinner from "discourse/ui-kit/d-conditional-loading-spinner";
import DModal from "discourse/ui-kit/d-modal";
import dConcatClass from "discourse/ui-kit/helpers/d-concat-class";
import { i18n } from "discourse-i18n";
export default class PostTranslationsModal extends Component {
@service composer;
@service dialog;
@service languageNameLookup;
@service siteSettings;
@service toasts;
@tracked postLocalizations = null;
@tracked loading = false;
@tracked savingPostLocale = false;
@tracked savingTopicLocale = false;
@tracked savedPostLocale;
@tracked savedTopicLocale;
constructor() {
super(...arguments);
this.savedPostLocale = this.post.locale ?? null;
this.savedTopicLocale = this.topic?.locale ?? null;
this.postLocaleFormData = { locale: this.savedPostLocale };
this.topicLocaleFormData = { locale: this.savedTopicLocale };
this.loadPostLocalizations();
}
get post() {
return this.args.model.post;
}
get topic() {
return this.post.topic;
}
get localeOptions() {
const locales = [...this.siteSettings.available_locales];
const availableValues = new Set(locales.map(({ value }) => value));
[this.post.locale, this.topic?.locale].forEach((locale) => {
if (locale && !availableValues.has(locale)) {
locales.push({ value: locale });
}
});
return locales.map(({ value }) => ({
value,
label: this.localeLabel(value),
}));
}
localeLabel(locale) {
return `${this.languageNameLookup.getLanguageName(locale)} (${locale})`;
}
async loadPostLocalizations() {
this.loading = true;
@@ -34,15 +76,73 @@ export default class PostTranslationsModal extends Component {
this.args.model.post.id
);
this.postLocalizations = post_localizations;
this.loading = false;
this.postLocalizations = post_localizations.map((localization) => ({
...localization,
languageName: this.localeLabel(localization.locale),
}));
} catch (error) {
popupAjaxError(error);
} finally {
this.loading = false;
}
}
get canLocalizePost() {
return this.args.model.post.can_localize_post;
return this.post.can_localize_post;
}
@action
async savePostLocale(selectedLocale, commitField) {
this.savingPostLocale = true;
try {
const { locale } = await PostLocalization.updateLocale(
this.post.id,
selectedLocale
);
this.post.set("locale", locale);
this.savedPostLocale = locale;
commitField("locale");
this.toasts.success({
data: {
message: i18n("post.localizations.modal.post_language_updated"),
},
});
} catch (error) {
popupAjaxError(error);
} finally {
this.savingPostLocale = false;
}
}
@action
async saveTopicLocale(selectedLocale, commitField) {
this.savingTopicLocale = true;
try {
const { locale } = await TopicLocalization.updateLocale(
this.topic.id,
selectedLocale
);
this.topic.set("locale", locale);
this.savedTopicLocale = locale;
commitField("locale");
this.toasts.success({
data: {
message: i18n("post.localizations.modal.topic_language_updated"),
},
});
} catch (error) {
popupAjaxError(error);
} finally {
this.savingTopicLocale = false;
}
}
@action
async discardLocale(set, commitField, savedLocale) {
await set("locale", savedLocale);
commitField("locale");
}
@action
@@ -53,9 +153,9 @@ export default class PostTranslationsModal extends Component {
this.args.closeModal();
const originalLocale = this.args.model.post?.locale;
const originalLocale = this.post.locale;
const { raw } = await ajax(`/posts/${this.args.model.post.id}.json`);
const { raw } = await ajax(`/posts/${this.post.id}.json`);
const composerOpts = {
action: Composer.ADD_TRANSLATION,
@@ -69,7 +169,7 @@ export default class PostTranslationsModal extends Component {
translationText: () => this.composer.model?.reply,
},
},
post: this.args.model.post,
post: this.post,
selectedTranslationLocale: locale.locale,
};
@@ -83,10 +183,10 @@ export default class PostTranslationsModal extends Component {
}
try {
await PostLocalization.destroy(this.args.model.post.id, locale);
await PostLocalization.destroy(this.post.id, locale);
if (this.args.model.post.firstPost) {
await TopicLocalization.destroy(this.args.model.post.topic_id, locale);
if (this.post.firstPost) {
await TopicLocalization.destroy(this.post.topic_id, locale);
}
} catch (error) {
popupAjaxError(error);
@@ -111,17 +211,155 @@ export default class PostTranslationsModal extends Component {
<DModal
@title={{i18n "post.localizations.modal.title"}}
@closeModal={{@closeModal}}
@inline={{@inline}}
class="post-translations-modal"
>
<:body>
<DConditionalLoadingSpinner @size="large" @condition={{this.loading}} />
<div class="post-translations-modal__language-settings">
{{#if this.post.firstPost}}
<Form
@data={{this.topicLocaleFormData}}
class="post-translations-modal__language-form post-translations-modal__topic-language"
as |form topicData|
>
<form.Field
@name="locale"
@title={{i18n "post.localizations.modal.topic_language"}}
@type="select"
@format="full"
@showOptional={{false}}
@disabled={{this.savingTopicLocale}}
as |field|
>
<div class="post-translations-modal__language-control">
<field.Control
@includeNone={{true}}
@nonePlaceholder={{i18n
"post.localizations.post_language_selector.none"
}}
as |select|
>
{{#each this.localeOptions as |locale|}}
<select.Option @value={{locale.value}}>
{{locale.label}}
</select.Option>
{{/each}}
</field.Control>
<div
class={{dConcatClass
"post-translations-modal__language-actions"
(if
(eq topicData.locale this.savedTopicLocale) "is-hidden"
)
}}
>
<form.Button
@action={{fn
this.saveTopicLocale
topicData.locale
form.commitField
}}
@icon="check"
@isLoading={{this.savingTopicLocale}}
@disabled={{this.savingTopicLocale}}
@title="post.localizations.modal.save_topic_language"
@ariaLabel="post.localizations.modal.save_topic_language"
class="btn-primary --save"
/>
<form.Button
@action={{fn
this.discardLocale
form.set
form.commitField
this.savedTopicLocale
}}
@icon="xmark"
@disabled={{this.savingTopicLocale}}
@title="post.localizations.modal.discard_language_change"
@ariaLabel="post.localizations.modal.discard_language_change"
class="btn-default --discard"
/>
</div>
</div>
</form.Field>
</Form>
{{/if}}
<Form
@data={{this.postLocaleFormData}}
class="post-translations-modal__language-form post-translations-modal__post-language"
as |form postData|
>
<form.Field
@name="locale"
@title={{i18n "post.localizations.modal.post_language"}}
@type="select"
@format="full"
@helpText={{i18n "post.localizations.modal.language_notice"}}
@showOptional={{false}}
@disabled={{this.savingPostLocale}}
as |field|
>
<div class="post-translations-modal__language-control">
<field.Control
@includeNone={{true}}
@nonePlaceholder={{i18n
"post.localizations.post_language_selector.none"
}}
as |select|
>
{{#each this.localeOptions as |locale|}}
<select.Option @value={{locale.value}}>
{{locale.label}}
</select.Option>
{{/each}}
</field.Control>
<div
class={{dConcatClass
"post-translations-modal__language-actions"
(if (eq postData.locale this.savedPostLocale) "is-hidden")
}}
>
<form.Button
@action={{fn
this.savePostLocale
postData.locale
form.commitField
}}
@icon="check"
@isLoading={{this.savingPostLocale}}
@disabled={{this.savingPostLocale}}
@title="post.localizations.modal.save_post_language"
@ariaLabel="post.localizations.modal.save_post_language"
class="btn-primary --save"
/>
<form.Button
@action={{fn
this.discardLocale
form.set
form.commitField
this.savedPostLocale
}}
@icon="xmark"
@disabled={{this.savingPostLocale}}
@title="post.localizations.modal.discard_language_change"
@ariaLabel="post.localizations.modal.discard_language_change"
class="btn-default --discard"
/>
</div>
</div>
</form.Field>
</Form>
</div>
{{#if this.postLocalizations}}
<table>
<thead>
<tr>
<th>{{i18n "post.localizations.table.locale"}}</th>
<th>{{i18n "post.localizations.table.actions"}}</th>
<th colspan="2">{{i18n "post.localizations.table.actions"}}</th>
</tr>
</thead>
<tbody>
@@ -129,7 +367,7 @@ export default class PostTranslationsModal extends Component {
<tr>
<td
class="post-translations-modal__locale"
>{{localization.locale}}</td>
>{{localization.languageName}}</td>
<td class="post-translations-modal__edit-action">
{{#if this.canLocalizePost}}
<DButton
@@ -37,7 +37,8 @@ export default class PostMenuAddTranslationButton extends Component {
}
@action
viewTranslations() {
async viewTranslations() {
await this.dMenu.close();
this.modal.show(PostTranslationsModal, { model: { post: this.args.post } });
}
@@ -22,6 +22,13 @@ export default class PostLocalization extends RestModel {
});
}
static updateLocale(postId, locale) {
return ajax(`/post_localizations/${postId}/locale`, {
type: "PUT",
data: { locale: locale ?? "" },
});
}
static destroy(postId, locale) {
return ajax("/post_localizations/destroy", {
type: "DELETE",
@@ -17,6 +17,13 @@ export default class TopicLocalization extends RestModel {
});
}
static updateLocale(topicId, locale) {
return ajax(`/topic_localizations/${topicId}/locale`, {
type: "PUT",
data: { locale: locale ?? "" },
});
}
static destroy(topicId, locale) {
return ajax("/topic_localizations/destroy", {
type: "DELETE",
@@ -1,9 +1,12 @@
import { getOwner } from "@ember/owner";
import { click, render } from "@ember/test-helpers";
import { click, findAll, render } from "@ember/test-helpers";
import { module, test } from "qunit";
import PostTranslationsModal from "discourse/components/modal/post-translations";
import AddTranslation from "discourse/components/post/menu/buttons/add-translation";
import noop from "discourse/helpers/noop";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import pretender from "discourse/tests/helpers/create-pretender";
import pretender, { response } from "discourse/tests/helpers/create-pretender";
import formKit from "discourse/tests/helpers/form-kit-helper";
import { i18n } from "discourse-i18n";
module(
@@ -13,11 +16,18 @@ module(
hooks.beforeEach(function () {
const store = getOwner(this).lookup("service:store");
const topic = store.createRecord("topic", {
id: 1,
locale: null,
});
const post = store.createRecord("post", {
id: 1,
topic_id: 1,
post_number: 1,
locale: null,
post_localizations_count: 0,
can_localize_post: true,
topic,
});
this.post = post;
@@ -25,6 +35,15 @@ module(
// positive case for menu to always show
this.siteSettings.content_localization_enabled = true;
this.siteSettings.available_content_localization_locales = [
{ name: "English", value: "en" },
{ name: "French (Français)", value: "fr" },
];
this.siteSettings.available_locales = [
{ name: "English", value: "en" },
{ name: "French (Français)", value: "fr" },
{ name: "German (Deutsch)", value: "de" },
];
this.currentUser.admin = true;
pretender.get("/posts/1.json", () => {
@@ -33,6 +52,23 @@ module(
pretender.get("/t/1.json", () => {
return [200, {}, { raw: "Test post content" }];
});
pretender.get("/post_localizations/1", () => {
return [
200,
{},
{
post_localizations: [{ id: 1, locale: "fr", raw: "Bonjour" }],
},
];
});
this.postLocaleUpdates = 0;
pretender.put("/post_localizations/1/locale", () => {
this.postLocaleUpdates += 1;
return response({ locale: "de" });
});
pretender.put("/topic_localizations/1/locale", () => {
return response({ locale: "de" });
});
});
test("renders menu button when user can localize", async function (assert) {
@@ -92,5 +128,198 @@ module(
.dom(".post-action-menu__view-translation")
.hasText(i18n("post.localizations.view", { count: 5 }));
});
test("manages original languages in the translations modal", async function (assert) {
this.post.locale = "en";
this.post.topic.locale = "fr";
this.model = { post: this.post };
await render(
<template>
<PostTranslationsModal
@model={{this.model}}
@closeModal={{noop}}
@inline={{true}}
/>
</template>
);
assert
.dom(".post-translations-modal .d-modal__title-text")
.hasText(
i18n("post.localizations.modal.title"),
"the modal has a general title"
);
assert
.dom(
".post-translations-modal__post-language .form-kit__container-help-text"
)
.hasText(
i18n("post.localizations.modal.language_notice"),
"the source language requirement is explained below its selector"
);
assert
.dom(".post-translations-modal__section-title")
.doesNotExist("the language fields have no redundant section heading");
assert
.dom(".post-translations-modal__topic-language")
.includesText(
i18n("post.localizations.modal.topic_language"),
"first posts include a separate topic title language"
);
assert.true(
findAll(
".post-translations-modal__language-form"
)[0].classList.contains("post-translations-modal__topic-language"),
"the topic title language is shown before the post language"
);
assert
.dom(".post-translations-modal__locale")
.hasText(
"French (Français) (fr)",
"translation locales use readable names"
);
assert.deepEqual(
formKit(".post-translations-modal__post-language")
.field("locale")
.options(),
["__NONE__", "en", "fr", "de"],
"the selector includes locales outside the content localization list"
);
assert
.dom(
".post-translations-modal__post-language .post-translations-modal__language-actions"
)
.hasClass(
"is-hidden",
"save controls are hidden before the value changes"
);
await formKit(".post-translations-modal__post-language")
.field("locale")
.select("de");
assert
.dom(
".post-translations-modal__post-language .post-translations-modal__language-actions"
)
.doesNotHaveClass(
"is-hidden",
"save controls appear after the value changes"
);
assert
.dom(".post-translations-modal__post-language .--save")
.hasAttribute(
"aria-label",
i18n("post.localizations.modal.save_post_language"),
"the save control has an accessible name"
);
assert
.dom(".post-translations-modal__post-language .--discard")
.hasAttribute(
"aria-label",
i18n("post.localizations.modal.discard_language_change"),
"the discard control has an accessible name"
);
await click(".post-translations-modal__post-language .--discard");
assert
.dom(
".post-translations-modal__post-language .post-translations-modal__language-actions"
)
.hasClass("is-hidden", "discarding restores the unchanged state");
});
test("closes the translations menu when opening the modal", async function (assert) {
this.post.post_localizations_count = 1;
await render(<template><AddTranslation @post={{this.post}} /></template>);
await click(".update-translations-menu");
await click(".post-action-menu__view-translation");
assert
.dom(
"[data-content][data-identifier='post-action-menu-edit-translations']"
)
.doesNotExist("the translations menu closes behind the modal");
});
test("hides language actions after saving", async function (assert) {
this.post.locale = "en";
this.model = { post: this.post };
await render(
<template>
<PostTranslationsModal
@model={{this.model}}
@closeModal={{noop}}
@inline={{true}}
/>
</template>
);
await formKit(".post-translations-modal__post-language")
.field("locale")
.select("de");
await click(".post-translations-modal__post-language .--save");
assert.strictEqual(this.postLocaleUpdates, 1, "the post locale is saved");
assert.strictEqual(
this.post.locale,
"de",
"the saved locale updates the post"
);
assert
.dom(
".post-translations-modal__post-language .post-translations-modal__language-actions"
)
.hasClass("is-hidden", "saving clears the changed state");
await formKit(".post-translations-modal__post-language")
.field("locale")
.select("en");
await click(".post-translations-modal__post-language .--discard");
assert.strictEqual(
formKit(".post-translations-modal__post-language")
.field("locale")
.value(),
"de",
"discarding after a save restores the latest saved value"
);
await formKit(".post-translations-modal__topic-language")
.field("locale")
.select("de");
await click(".post-translations-modal__topic-language .--save");
assert
.dom(
".post-translations-modal__topic-language .post-translations-modal__language-actions"
)
.hasClass("is-hidden", "saving the topic clears its changed state");
});
test("does not show topic title language for replies", async function (assert) {
this.post.post_number = 2;
this.model = { post: this.post };
await render(
<template>
<PostTranslationsModal
@model={{this.model}}
@closeModal={{noop}}
@inline={{true}}
/>
</template>
);
assert
.dom(".post-translations-modal__post-language")
.exists("the reply language can be changed");
assert
.dom(".post-translations-modal__topic-language")
.doesNotExist("topic title language is limited to the first post");
});
}
);
+18
View File
@@ -0,0 +1,18 @@
# frozen_string_literal: true
class PostLocaleUpdater
def self.update(post:, locale:, user:)
Guardian.new(user).ensure_can_localize_post!(post)
validate_locale!(locale)
post.update!(locale:)
post
end
def self.validate_locale!(locale)
return if locale.nil? || LocaleSiteSetting.supported_locales.include?(locale)
raise Discourse::InvalidParameters.new(:locale)
end
private_class_method :validate_locale!
end
-5
View File
@@ -828,7 +828,6 @@ class PostRevisor
update_topic_excerpt
update_category_description
update_topic_locale
end
def update_topic_excerpt
@@ -845,10 +844,6 @@ class PostRevisor
end
end
def update_topic_locale
@topic.update(locale: @fields[:locale]) if @fields.has_key?(:locale)
end
def advance_draft_sequence
@post.advance_draft_sequence
end
+18
View File
@@ -0,0 +1,18 @@
# frozen_string_literal: true
class TopicLocaleUpdater
def self.update(topic:, locale:, user:)
Guardian.new(user).ensure_can_localize_topic!(topic)
validate_locale!(locale)
topic.update!(locale:)
topic
end
def self.validate_locale!(locale)
return if locale.nil? || LocaleSiteSetting.supported_locales.include?(locale)
raise Discourse::InvalidParameters.new(:locale)
end
private_class_method :validate_locale!
end
+4 -4
View File
@@ -414,13 +414,13 @@ describe PostRevisor do
expect(post.locale).to eq("ja")
end
it "also updates the topic's locale if first post" do
post = Fabricate(:post)
it "keeps the topic locale unchanged when editing the first post locale" do
post = Fabricate(:post, locale: "en")
post.topic.update!(locale: "fr")
PostRevisor.new(post).revise!(post.user, locale: "ja")
post.reload
expect(post.topic.locale).to eq("ja")
expect(post.topic.reload.locale).to eq("fr")
end
end
@@ -10,6 +10,7 @@ describe PostLocalizationsController do
before do
SiteSetting.content_localization_enabled = true
SiteSetting.content_localization_supported_locales = "ja"
SiteSetting.content_localization_allowed_groups = group.id.to_s
group.add(user)
sign_in(user)
@@ -131,6 +132,56 @@ describe PostLocalizationsController do
end
end
describe "#update_locale" do
it "updates the original post locale without changing the topic locale" do
post_record.topic.update!(locale: "fr")
original_version = post_record.version
put "/post_localizations/#{post_record.id}/locale.json", params: { locale: "de" }
expect(response.status).to eq(200)
expect(response.parsed_body["locale"]).to eq("de")
expect(post_record.reload.locale).to eq("de")
expect(post_record.version).to eq(original_version)
expect(post_record.topic.reload.locale).to eq("fr")
end
it "returns forbidden when the user cannot localize the post" do
group.remove(user)
expect {
put "/post_localizations/#{post_record.id}/locale.json", params: { locale: "ja" }
}.not_to change { post_record.reload.locale }
expect(response.status).to eq(403)
end
it "rejects values that are not a single known locale" do
post_record.update!(locale: "en")
%w[not_a_locale en|ja].each do |invalid_locale|
expect {
put "/post_localizations/#{post_record.id}/locale.json",
params: {
locale: invalid_locale,
}
}.not_to change { post_record.reload.locale }
expect(response.status).to eq(400)
end
end
it "allows the original post locale to be cleared" do
post_record.update!(locale: "en")
put "/post_localizations/#{post_record.id}/locale.json", params: { locale: "" }
expect(response.status).to eq(200)
expect(response.parsed_body["locale"]).to be_nil
expect(post_record.reload.locale).to be_nil
end
end
describe "#destroy" do
it "destroys the localization" do
Fabricate(:post_localization, post: post_record, locale:)
@@ -10,6 +10,7 @@ describe TopicLocalizationsController do
before do
SiteSetting.content_localization_enabled = true
SiteSetting.content_localization_supported_locales = "ja"
SiteSetting.content_localization_allowed_groups = group.id.to_s
group.add(user)
sign_in(user)
@@ -97,6 +98,41 @@ describe TopicLocalizationsController do
end
end
describe "#update_locale" do
it "updates the original topic title locale without changing the first post locale" do
first_post = Fabricate(:post, topic:, locale: "fr")
put "/topic_localizations/#{topic.id}/locale.json", params: { locale: "de" }
expect(response.status).to eq(200)
expect(response.parsed_body["locale"]).to eq("de")
expect(topic.reload.locale).to eq("de")
expect(first_post.reload.locale).to eq("fr")
end
it "returns forbidden when the user cannot localize the topic" do
group.remove(user)
expect {
put "/topic_localizations/#{topic.id}/locale.json", params: { locale: "ja" }
}.not_to change { topic.reload.locale }
expect(response.status).to eq(403)
end
it "rejects values that are not a single known locale" do
topic.update!(locale: "en")
%w[not_a_locale en|ja].each do |invalid_locale|
expect {
put "/topic_localizations/#{topic.id}/locale.json", params: { locale: invalid_locale }
}.not_to change { topic.reload.locale }
expect(response.status).to eq(400)
end
end
end
describe "#destroy" do
fab!(:topic_localization) { Fabricate(:topic_localization, topic:, locale: "ja") }
@@ -4,6 +4,49 @@ module PageObjects
module Modals
class ViewTranslationsModal < PageObjects::Modals::Base
MODAL_SELECTOR = ".post-translations-modal"
def select_post_language(language)
within(full_modal_selector) do
select(language, from: I18n.t("js.post.localizations.modal.post_language"))
end
self
end
def select_topic_language(language)
within(full_modal_selector) do
select(language, from: I18n.t("js.post.localizations.modal.topic_language"))
end
self
end
def save_post_language
find("#{full_modal_selector} .post-translations-modal__post-language .--save").click
self
end
def save_topic_language
find("#{full_modal_selector} .post-translations-modal__topic-language .--save").click
self
end
def has_post_language?(language)
has_select?(I18n.t("js.post.localizations.modal.post_language"), selected: language)
end
def has_topic_language?(language)
has_select?(I18n.t("js.post.localizations.modal.topic_language"), selected: language)
end
def has_language_notice?
has_css?(
"#{full_modal_selector} .post-translations-modal__post-language .form-kit__container-help-text",
text: I18n.t("js.post.localizations.modal.language_notice"),
)
end
def has_translation_language?(language)
has_css?("#{full_modal_selector} .post-translations-modal__locale", text: language)
end
end
end
end
+7
View File
@@ -116,6 +116,13 @@ module PageObjects
find_post_action_button(post, button).click
end
def open_post_translations(post)
click_post_action_button(post, :show_more)
click_post_action_button(post, :add_translation)
find(".post-action-menu__view-translation").click
self
end
def find_post_action_buttons(post)
within_post(post) { find(".post-controls .actions") }
end
+31
View File
@@ -108,6 +108,37 @@ describe "Post translations" do
expect(find(".post-translations-modal__locale")).to have_text("fr")
end
it "lets a user set independent post and topic title languages without closing the modal" do
post.update!(locale: nil)
topic.update!(locale: nil)
toasts = PageObjects::Components::Toasts.new
topic_page.visit_topic(topic)
topic_page.open_post_translations(post)
expect(view_translations_modal).to be_open
expect(view_translations_modal).to have_language_notice
expect(view_translations_modal).to have_translation_language("French (Français) (fr)")
view_translations_modal.select_post_language("English (en)").save_post_language
expect(toasts).to have_success(I18n.t("js.post.localizations.modal.post_language_updated"))
expect(view_translations_modal).to be_open
expect(view_translations_modal).to have_language_notice
view_translations_modal.select_topic_language("Spanish (Español) (es)").save_topic_language
expect(toasts).to have_success(I18n.t("js.post.localizations.modal.topic_language_updated"))
expect(view_translations_modal).to be_open
view_translations_modal.close
page.refresh
topic_page.open_post_translations(post)
expect(view_translations_modal).to have_post_language("English (en)")
expect(view_translations_modal).to have_topic_language("Spanish (Español) (es)")
end
it "allows a user to edit a translation" do
topic_page.visit_topic(topic)
topic_page.click_post_action_button(post, :show_more)