mirror of
https://github.com/discourse/discourse.git
synced 2026-09-05 04:40:41 -05:00
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:
@@ -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])
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
+231
-2
@@ -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");
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user