UX: Text customization for different languages. (#11729)

Admins can now edit translations in different languages without having to change their locale. We display a warning when there's a fallback language set.
This commit is contained in:
Roman Rizzi 2021-01-18 14:53:45 -03:00 committed by GitHub
parent 7ac9a4d2ec
commit ea8b5c18db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 255 additions and 96 deletions

View File

@ -7,6 +7,7 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
export default Controller.extend(bufferedProperty("siteText"), { export default Controller.extend(bufferedProperty("siteText"), {
saved: false, saved: false,
queryParams: ["locale"],
@discourseComputed("buffered.value") @discourseComputed("buffered.value")
saveDisabled(value) { saveDisabled(value) {
@ -15,9 +16,11 @@ export default Controller.extend(bufferedProperty("siteText"), {
actions: { actions: {
saveChanges() { saveChanges() {
const buffered = this.buffered; const attrs = this.buffered.getProperties("value");
attrs.locale = this.locale;
this.siteText this.siteText
.save(buffered.getProperties("value")) .save(attrs)
.then(() => { .then(() => {
this.commitBuffer(); this.commitBuffer();
this.set("saved", true); this.set("saved", true);
@ -27,10 +30,11 @@ export default Controller.extend(bufferedProperty("siteText"), {
revertChanges() { revertChanges() {
this.set("saved", false); this.set("saved", false);
bootbox.confirm(I18n.t("admin.site_text.revert_confirm"), (result) => { bootbox.confirm(I18n.t("admin.site_text.revert_confirm"), (result) => {
if (result) { if (result) {
this.siteText this.siteText
.revert() .revert(this.locale)
.then((props) => { .then((props) => {
const buffered = this.buffered; const buffered = this.buffered;
buffered.setProperties(props); buffered.setProperties(props);

View File

@ -1,4 +1,5 @@
import Controller from "@ember/controller"; import Controller from "@ember/controller";
import discourseComputed from "discourse-common/utils/decorators";
import discourseDebounce from "discourse-common/lib/debounce"; import discourseDebounce from "discourse-common/lib/debounce";
let lastSearch; let lastSearch;
@ -6,23 +7,49 @@ export default Controller.extend({
searching: false, searching: false,
siteTexts: null, siteTexts: null,
preferred: false, preferred: false,
queryParams: ["q", "overridden"], queryParams: ["q", "overridden", "locale"],
locale: null,
q: null, q: null,
overridden: false, overridden: false,
init() {
this._super(...arguments);
this.set("locale", this.siteSettings.default_locale);
},
_performSearch() { _performSearch() {
this.store this.store
.find("site-text", this.getProperties("q", "overridden")) .find("site-text", this.getProperties("q", "overridden", "locale"))
.then((results) => { .then((results) => {
this.set("siteTexts", results); this.set("siteTexts", results);
}) })
.finally(() => this.set("searching", false)); .finally(() => this.set("searching", false));
}, },
@discourseComputed()
availableLocales() {
return JSON.parse(this.siteSettings.available_locales);
},
@discourseComputed("locale")
fallbackLocaleFullName() {
if (this.siteTexts.extras.fallback_locale) {
return this.availableLocales.find((l) => {
return l.value === this.siteTexts.extras.fallback_locale;
}).name;
}
},
actions: { actions: {
edit(siteText) { edit(siteText) {
this.transitionToRoute("adminSiteText.edit", siteText.get("id")); this.transitionToRoute("adminSiteText.edit", siteText.get("id"), {
queryParams: {
locale: this.locale,
localeFullName: this.availableLocales[this.locale],
},
});
}, },
toggleOverridden() { toggleOverridden() {
@ -39,5 +66,14 @@ export default Controller.extend({
lastSearch = q; lastSearch = q;
} }
}, },
updateLocale(value) {
this.setProperties({
searching: true,
locale: value,
});
discourseDebounce(this, this._performSearch, 400);
},
}, },
}); });

View File

@ -1,10 +1,10 @@
import RestModel from "discourse/models/rest"; import RestModel from "discourse/models/rest";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
const { getProperties } = Ember; import { getProperties } from "@ember/object";
export default RestModel.extend({ export default RestModel.extend({
revert() { revert(locale) {
return ajax(`/admin/customize/site_texts/${this.id}`, { return ajax(`/admin/customize/site_texts/${this.id}?locale=${locale}`, {
type: "DELETE", type: "DELETE",
}).then((result) => getProperties(result.site_text, "value", "can_revert")); }).then((result) => getProperties(result.site_text, "value", "can_revert"));
}, },

View File

@ -1,10 +1,30 @@
import Route from "@ember/routing/route"; import Route from "@ember/routing/route";
import { ajax } from "discourse/lib/ajax";
export default Route.extend({ export default Route.extend({
queryParams: {
locale: { replace: true },
},
model(params) { model(params) {
return this.store.find("site-text", params.id); return ajax(
`/admin/customize/site_texts/${params.id}?locale=${params.locale}`
).then((result) => {
return this.store.createRecord("site-text", result.site_text);
});
}, },
setupController(controller, siteText) { setupController(controller, siteText) {
controller.setProperties({ siteText, saved: false }); const locales = JSON.parse(this.siteSettings.available_locales);
const localeFullName = locales.find((locale) => {
return locale.value === controller.locale;
}).name;
controller.setProperties({
siteText,
saved: false,
localeFullName: localeFullName,
});
}, },
}); });

View File

@ -6,12 +6,13 @@ export default Route.extend({
queryParams: { queryParams: {
q: { replace: true }, q: { replace: true },
overridden: { replace: true }, overridden: { replace: true },
locale: { replace: true },
}, },
model(params) { model(params) {
return this.store.find( return this.store.find(
"site-text", "site-text",
getProperties(params, "q", "overridden") getProperties(params, "q", "overridden", "locale")
); );
}, },

View File

@ -1,9 +1,12 @@
<div class="edit-site-text"> <div class="edit-site-text">
<div class="title"> <div class="title">
<h3>{{siteText.id}}</h3> <h3>{{siteText.id}}</h3>
</div> </div>
<div class="title">
<h4>{{i18n "admin.site_text.locale"}} {{localeFullName}}</h4>
</div>
{{expanding-text-area value=buffered.value rows="1" class="site-text-value"}} {{expanding-text-area value=buffered.value rows="1" class="site-text-value"}}
{{#save-controls model=siteText action=(action "saveChanges") saved=saved saveDisabled=saveDisabled}} {{#save-controls model=siteText action=(action "saveChanges") saved=saved saveDisabled=saveDisabled}}
@ -12,7 +15,7 @@
{{/if}} {{/if}}
{{/save-controls}} {{/save-controls}}
{{#link-to "adminSiteText.index" class="go-back"}} {{#link-to "adminSiteText.index" (query-params locale=locale) class="go-back"}}
{{d-icon "arrow-left"}} {{d-icon "arrow-left"}}
{{i18n "admin.site_text.go_back"}} {{i18n "admin.site_text.go_back"}}
{{/link-to}} {{/link-to}}

View File

@ -16,6 +16,21 @@
</div> </div>
<p class="filter-options"> <p class="filter-options">
<div class="locale">
<label>{{i18n "admin.site_text.locale"}}</label>
{{combo-box
valueProperty="value"
content=availableLocales
value=locale
onChange=(action "updateLocale")
class="locale-search"
options=(hash
filterable=true
none="user.locale.default"
)
}}
</div>
<label> <label>
{{input type="checkbox" checked=overridden click=(action "toggleOverridden")}} {{input type="checkbox" checked=overridden click=(action "toggleOverridden")}}
{{i18n "admin.site_text.show_overriden"}} {{i18n "admin.site_text.show_overriden"}}
@ -24,6 +39,13 @@
</div> </div>
{{#conditional-loading-spinner condition=searching}} {{#conditional-loading-spinner condition=searching}}
{{#if fallbackLocaleFullName}}
<div class="alert alert-info">
{{d-icon "exclamation-circle"}}
{{i18n "admin.site_text.fallback_locale_warning" fallback=fallbackLocaleFullName}}
</div>
{{/if}}
{{#if siteTexts.extras.recommended}} {{#if siteTexts.extras.recommended}}
<p><b>{{i18n "admin.site_text.recommended"}}</b></p> <p><b>{{i18n "admin.site_text.recommended"}}</b></p>
{{/if}} {{/if}}

View File

@ -8,6 +8,10 @@ import { test } from "qunit";
acceptance("Admin - Site Texts", function (needs) { acceptance("Admin - Site Texts", function (needs) {
needs.user(); needs.user();
needs.settings({
available_locales: JSON.stringify([{ name: "English", value: "en" }]),
default_locale: "en",
});
test("search for a key", async function (assert) { test("search for a key", async function (assert) {
await visit("/admin/customize/site_texts"); await visit("/admin/customize/site_texts");
@ -31,7 +35,7 @@ acceptance("Admin - Site Texts", function (needs) {
}); });
test("edit and revert a site text by key", async function (assert) { test("edit and revert a site text by key", async function (assert) {
await visit("/admin/customize/site_texts/site.test"); await visit("/admin/customize/site_texts/site.test?locale=en");
assert.equal(queryAll(".title h3").text(), "site.test"); assert.equal(queryAll(".title h3").text(), "site.test");
assert.ok(!exists(".saved")); assert.ok(!exists(".saved"));

View File

@ -644,9 +644,15 @@ export function applyDefaultHandlers(pretender) {
pretender.get("/admin/customize/site_texts", (request) => { pretender.get("/admin/customize/site_texts", (request) => {
if (request.queryParams.overridden) { if (request.queryParams.overridden) {
return response(200, { site_texts: [overridden] }); return response(200, {
site_texts: [overridden],
extras: { locale: "en" },
});
} else { } else {
return response(200, { site_texts: [siteText, overridden] }); return response(200, {
site_texts: [siteText, overridden],
extras: { locale: "en" },
});
} }
}); });

View File

@ -204,6 +204,12 @@ $mobile-breakpoint: 700px;
.reseed { .reseed {
float: right; float: right;
} }
.locale {
margin-bottom: 0.5em;
}
.locale-search {
width: 50%;
}
} }
.text-highlight { .text-highlight {
font-weight: bold; font-weight: bold;

View File

@ -21,14 +21,13 @@ class Admin::SiteTextsController < Admin::AdminController
query = params[:q] || "" query = params[:q] || ""
locale = params[:locale] || I18n.locale locale = fetch_locale(params[:locale])
raise Discourse::InvalidParameters.new(:locale) if !I18n.locale_available?(locale)
if query.blank? && !overridden if query.blank? && !overridden
extras[:recommended] = true extras[:recommended] = true
results = I18n.with_locale(locale) { self.class.preferred_keys.map { |k| record_for(k) } } results = self.class.preferred_keys.map { |k| record_for(key: k, locale: locale) }
else else
results = I18n.with_locale(locale) { find_translations(query, overridden) } results = find_translations(query, overridden, locale)
if results.any? if results.any?
extras[:regex] = I18n::Backend::DiscourseI18n.create_search_regexp(query, as_string: true) extras[:regex] = I18n::Backend::DiscourseI18n.create_search_regexp(query, as_string: true)
@ -53,20 +52,37 @@ class Admin::SiteTextsController < Admin::AdminController
last = first + per_page last = first + per_page
extras[:has_more] = true if results.size > last extras[:has_more] = true if results.size > last
render_serialized(results[first..last - 1], SiteTextSerializer, root: 'site_texts', rest_serializer: true, extras: extras, overridden_keys: overridden_keys)
if LocaleSiteSetting.fallback_locale(locale).present?
extras[:fallback_locale] = LocaleSiteSetting.fallback_locale(locale)
end
overridden = overridden_keys(locale)
render_serialized(
results[first..last - 1],
SiteTextSerializer,
root: 'site_texts',
rest_serializer: true,
extras: extras,
overridden_keys: overridden,
)
end end
def show def show
site_text = find_site_text locale = fetch_locale(params[:locale])
site_text = find_site_text(locale)
render_serialized(site_text, SiteTextSerializer, root: 'site_text', rest_serializer: true) render_serialized(site_text, SiteTextSerializer, root: 'site_text', rest_serializer: true)
end end
def update def update
site_text = find_site_text locale = fetch_locale(params.dig(:site_text, :locale))
value = site_text[:value] = params[:site_text][:value]
site_text = find_site_text(locale)
value = site_text[:value] = params.dig(:site_text, :value)
id = site_text[:id] id = site_text[:id]
old_value = I18n.t(id) old_value = I18n.t(id, locale: locale)
translation_override = TranslationOverride.upsert!(I18n.locale, id, value)
translation_override = TranslationOverride.upsert!(locale, id, value)
if translation_override.errors.empty? if translation_override.errors.empty?
StaffActionLogger.new(current_user).log_site_text_change(id, value, old_value) StaffActionLogger.new(current_user).log_site_text_change(id, value, old_value)
@ -88,11 +104,14 @@ class Admin::SiteTextsController < Admin::AdminController
end end
def revert def revert
site_text = find_site_text locale = fetch_locale(params[:locale])
site_text = find_site_text(locale)
id = site_text[:id] id = site_text[:id]
old_text = I18n.t(id) old_text = I18n.t(id, locale: locale)
TranslationOverride.revert!(I18n.locale, id) TranslationOverride.revert!(locale, id)
site_text = find_site_text
site_text = find_site_text(locale)
StaffActionLogger.new(current_user).log_site_text_change(id, site_text[:value], old_text) StaffActionLogger.new(current_user).log_site_text_change(id, site_text[:value], old_text)
system_badge_id = Badge.find_system_badge_id_from_translation_key(id) system_badge_id = Badge.find_system_badge_id_from_translation_key(id)
if system_badge_id.present? if system_badge_id.present?
@ -137,39 +156,39 @@ class Admin::SiteTextsController < Admin::AdminController
badge_parts[0] == 'badges' && badge_parts[2] == 'name' badge_parts[0] == 'badges' && badge_parts[2] == 'name'
end end
def record_for(key, value = nil) def record_for(key:, value: nil, locale:)
if key.ends_with?("_MF") if key.ends_with?("_MF")
override = TranslationOverride.where(translation_key: key, locale: I18n.locale).pluck(:value) override = TranslationOverride.where(translation_key: key, locale: locale).pluck(:value)
value = override&.first value = override&.first
end end
value ||= I18n.t(key) value ||= I18n.t(key, locale: locale)
{ id: key, value: value } { id: key, value: value }
end end
PLURALIZED_REGEX = /(.*)\.(zero|one|two|few|many|other)$/ PLURALIZED_REGEX = /(.*)\.(zero|one|two|few|many|other)$/
def find_site_text def find_site_text(locale)
if self.class.restricted_keys.include?(params[:id]) if self.class.restricted_keys.include?(params[:id])
raise Discourse::InvalidAccess.new(nil, nil, custom_message: 'email_template_cant_be_modified') raise Discourse::InvalidAccess.new(nil, nil, custom_message: 'email_template_cant_be_modified')
end end
if I18n.exists?(params[:id]) || TranslationOverride.exists?(locale: I18n.locale, translation_key: params[:id]) if I18n.exists?(params[:id], locale) || TranslationOverride.exists?(locale: locale, translation_key: params[:id])
return record_for(params[:id]) return record_for(key: params[:id], locale: locale)
end end
if PLURALIZED_REGEX.match(params[:id]) if PLURALIZED_REGEX.match(params[:id])
value = fix_plural_keys($1, {}).fetch($2.to_sym) value = fix_plural_keys($1, {}, locale).detect { |plural| plural[0] == $2.to_sym }
return record_for(params[:id], value) if value return record_for(key: params[:id], value: value[1], locale: value[2]) if value
end end
raise Discourse::NotFound raise Discourse::NotFound
end end
def find_translations(query, overridden) def find_translations(query, overridden, locale)
translations = Hash.new { |hash, key| hash[key] = {} } translations = Hash.new { |hash, key| hash[key] = {} }
I18n.search(query, overridden: overridden).each do |key, value| I18n.search(query, overridden: overridden, locale: locale).each do |key, value|
if PLURALIZED_REGEX.match(key) if PLURALIZED_REGEX.match(key)
translations[$1][$2] = value translations[$1][$2] = value
else else
@ -183,30 +202,46 @@ class Admin::SiteTextsController < Admin::AdminController
next unless I18n.exists?(key, :en) next unless I18n.exists?(key, :en)
if value&.is_a?(Hash) if value&.is_a?(Hash)
value = fix_plural_keys(key, value) fix_plural_keys(key, value, locale).each do |plural|
value.each do |plural_key, plural_value| plural_key = plural[0]
results << record_for("#{key}.#{plural_key}", plural_value) plural_value = plural[1]
results << record_for(
key: "#{key}.#{plural_key}", value: plural_value, locale: plural.last
)
end end
else else
results << record_for(key, value) results << record_for(key: key, value: value, locale: locale)
end end
end end
results results
end end
def fix_plural_keys(key, value) def fix_plural_keys(key, value, locale)
value = value.with_indifferent_access value = value.with_indifferent_access
plural_keys = I18n.t('i18n.plural.keys') plural_keys = I18n.t('i18n.plural.keys', locale: locale)
return value if value.keys.size == plural_keys.size && plural_keys.all? { |k| value.key?(k) } return value if value.keys.size == plural_keys.size && plural_keys.all? { |k| value.key?(k) }
fallback_value = I18n.t(key, locale: :en, default: {}) fallback_value = I18n.t(key, locale: :en, default: {})
plural_keys.map do |k| plural_keys.map do |k|
[k, value[k] || fallback_value[k] || fallback_value[:other]] if value[k]
end.to_h [k, value[k], locale]
else
[k, fallback_value[k] || fallback_value[:other], :en]
end
end
end end
def overridden_keys def overridden_keys(locale)
TranslationOverride.where(locale: I18n.locale).pluck(:translation_key) TranslationOverride.where(locale: locale).pluck(:translation_key)
end
def fetch_locale(locale_from_params)
locale_from_params.tap do |locale|
if locale.blank? || !I18n.locale_available?(locale)
raise Discourse::InvalidParameters.new(:locale)
end
end
end end
end end

View File

@ -4826,6 +4826,8 @@ en:
go_back: "Back to Search" go_back: "Back to Search"
recommended: "We recommend customizing the following text to suit your needs:" recommended: "We recommend customizing the following text to suit your needs:"
show_overriden: "Only show overridden" show_overriden: "Only show overridden"
locale: "Language:"
fallback_locale_warning: "You are editing a variety of %{fallback}. Users who choose %{fallback} as their interface language won't see your changes."
more_than_50_results: "There are more than 50 results. Please refine your search." more_than_50_results: "There are more than 50 results. Please refine your search."
settings: # used by theme and site settings settings: # used by theme and site settings

View File

@ -71,7 +71,7 @@ module I18n
opts ||= {} opts ||= {}
target = opts[:backend] || backend target = opts[:backend] || backend
results = opts[:overridden] ? {} : target.search(config.locale, query) results = opts[:overridden] ? {} : target.search(locale, query)
regexp = I18n::Backend::DiscourseI18n.create_search_regexp(query) regexp = I18n::Backend::DiscourseI18n.create_search_regexp(query)
(overrides_by_locale(locale) || {}).each do |k, v| (overrides_by_locale(locale) || {}).each do |k, v|

View File

@ -5,6 +5,7 @@ require 'rails_helper'
RSpec.describe Admin::SiteTextsController do RSpec.describe Admin::SiteTextsController do
fab!(:admin) { Fabricate(:admin) } fab!(:admin) { Fabricate(:admin) }
fab!(:user) { Fabricate(:user) } fab!(:user) { Fabricate(:user) }
let(:default_locale) { I18n.locale }
after do after do
TranslationOverride.delete_all TranslationOverride.delete_all
@ -18,7 +19,7 @@ RSpec.describe Admin::SiteTextsController do
context "when not logged in as an admin" do context "when not logged in as an admin" do
it "raises an error if you aren't logged in" do it "raises an error if you aren't logged in" do
put '/admin/customize/site_texts/some_key.json', params: { put '/admin/customize/site_texts/some_key.json', params: {
site_text: { value: 'foo' } site_text: { value: 'foo' }, locale: default_locale
} }
expect(response.status).to eq(404) expect(response.status).to eq(404)
@ -28,7 +29,7 @@ RSpec.describe Admin::SiteTextsController do
sign_in(user) sign_in(user)
put "/admin/customize/site_texts/some_key.json", params: { put "/admin/customize/site_texts/some_key.json", params: {
site_text: { value: 'foo' } site_text: { value: 'foo' }, locale: default_locale
} }
expect(response.status).to eq(404) expect(response.status).to eq(404)
@ -46,13 +47,13 @@ RSpec.describe Admin::SiteTextsController do
describe '#index' do describe '#index' do
it 'returns json' do it 'returns json' do
get "/admin/customize/site_texts.json", params: { q: 'title' } get "/admin/customize/site_texts.json", params: { q: 'title', locale: default_locale }
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.parsed_body['site_texts']).to include(include("id" => "title")) expect(response.parsed_body['site_texts']).to include(include("id" => "title"))
end end
it 'sets has_more to true if more than 50 results were found' do it 'sets has_more to true if more than 50 results were found' do
get "/admin/customize/site_texts.json", params: { q: 'e' } get "/admin/customize/site_texts.json", params: { q: 'e', locale: default_locale }
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.parsed_body['site_texts'].size).to eq(50) expect(response.parsed_body['site_texts'].size).to eq(50)
expect(response.parsed_body['extras']['has_more']).to be_truthy expect(response.parsed_body['extras']['has_more']).to be_truthy
@ -61,23 +62,23 @@ RSpec.describe Admin::SiteTextsController do
it 'works with pages' do it 'works with pages' do
texts = Set.new texts = Set.new
get "/admin/customize/site_texts.json", params: { q: 'e' } get "/admin/customize/site_texts.json", params: { q: 'e', locale: default_locale }
response.parsed_body['site_texts'].each { |text| texts << text['id'] } response.parsed_body['site_texts'].each { |text| texts << text['id'] }
expect(texts.size).to eq(50) expect(texts.size).to eq(50)
get "/admin/customize/site_texts.json", params: { q: 'e', page: 1 } get "/admin/customize/site_texts.json", params: { q: 'e', page: 1, locale: default_locale }
response.parsed_body['site_texts'].each { |text| texts << text['id'] } response.parsed_body['site_texts'].each { |text| texts << text['id'] }
expect(texts.size).to eq(100) expect(texts.size).to eq(100)
end end
it 'works with locales' do it 'works with locales' do
get "/admin/customize/site_texts.json", params: { q: 'yes_value', locale: 'en' } get "/admin/customize/site_texts.json", params: { q: 'yes_value', locale: default_locale }
value = response.parsed_body['site_texts'].find { |text| text['id'] == 'js.yes_value' }['value'] value = response.parsed_body['site_texts'].find { |text| text['id'] == 'js.yes_value' }['value']
expect(value).to eq(I18n.with_locale(:en) { I18n.t('js.yes_value') }) expect(value).to eq(I18n.t('js.yes_value', locale: default_locale))
get "/admin/customize/site_texts.json", params: { q: 'yes_value', locale: 'de' } get "/admin/customize/site_texts.json", params: { q: 'yes_value', locale: 'de' }
value = response.parsed_body['site_texts'].find { |text| text['id'] == 'js.yes_value' }['value'] value = response.parsed_body['site_texts'].find { |text| text['id'] == 'js.yes_value' }['value']
expect(value).to eq(I18n.with_locale(:de) { I18n.t('js.yes_value') }) expect(value).to eq(I18n.t('js.yes_value', locale: :de))
end end
it 'returns an error on invalid locale' do it 'returns an error on invalid locale' do
@ -85,9 +86,14 @@ RSpec.describe Admin::SiteTextsController do
expect(response.status).to eq(400) expect(response.status).to eq(400)
end end
it 'returns an error on empty locale' do
get "/admin/customize/site_texts.json"
expect(response.status).to eq(400)
end
it 'normalizes quotes during search' do it 'normalizes quotes during search' do
value = %q|“Thats a magic sock.”| value = %q|“Thats a magic sock.”|
put "/admin/customize/site_texts/title.json", params: { site_text: { value: value } } put "/admin/customize/site_texts/title.json", params: { site_text: { value: value, locale: default_locale } }
[ [
%q|That's a 'magic' sock.|, %q|That's a 'magic' sock.|,
@ -97,7 +103,7 @@ RSpec.describe Admin::SiteTextsController do
%q|«That's a 'magic' sock.»|, %q|«That's a 'magic' sock.»|,
%q|„Thats a magic sock.“| %q|„Thats a magic sock.“|
].each do |search_term| ].each do |search_term|
get "/admin/customize/site_texts.json", params: { q: search_term } get "/admin/customize/site_texts.json", params: { q: search_term, locale: default_locale }
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.parsed_body['site_texts']).to include(include("id" => "title", "value" => value)) expect(response.parsed_body['site_texts']).to include(include("id" => "title", "value" => value))
end end
@ -105,14 +111,14 @@ RSpec.describe Admin::SiteTextsController do
it 'normalizes ellipsis' do it 'normalizes ellipsis' do
value = "Loading Discussion…" value = "Loading Discussion…"
put "/admin/customize/site_texts/embed.loading.json", params: { site_text: { value: value } } put "/admin/customize/site_texts/embed.loading.json", params: { site_text: { value: value, locale: default_locale } }
[ [
"Loading Discussion", "Loading Discussion",
"Loading Discussion...", "Loading Discussion...",
"Loading Discussion…" "Loading Discussion…"
].each do |search_term| ].each do |search_term|
get "/admin/customize/site_texts.json", params: { q: search_term } get "/admin/customize/site_texts.json", params: { q: search_term, locale: default_locale }
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.parsed_body['site_texts']).to include(include("id" => "embed.loading", "value" => value)) expect(response.parsed_body['site_texts']).to include(include("id" => "embed.loading", "value" => value))
end end
@ -123,11 +129,11 @@ RSpec.describe Admin::SiteTextsController do
TranslationOverride.create!(locale: :ru, translation_key: 'missing_plural_key.one', value: 'ONE') TranslationOverride.create!(locale: :ru, translation_key: 'missing_plural_key.one', value: 'ONE')
TranslationOverride.create!(locale: :ru, translation_key: 'another_missing_key', value: 'foo') TranslationOverride.create!(locale: :ru, translation_key: 'another_missing_key', value: 'foo')
get "/admin/customize/site_texts.json", params: { q: 'missing_plural_key' } get "/admin/customize/site_texts.json", params: { q: 'missing_plural_key', 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", params: { q: 'another_missing_key' } get "/admin/customize/site_texts.json", params: { q: 'another_missing_key', 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
@ -141,7 +147,7 @@ RSpec.describe Admin::SiteTextsController do
it 'finds the correct plural keys for the locale' do it 'finds the correct plural keys for the locale' do
SiteSetting.default_locale = locale SiteSetting.default_locale = locale
get '/admin/customize/site_texts.json', params: { q: 'colour' } get '/admin/customize/site_texts.json', params: { q: 'colour', locale: locale }
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = ::JSON.parse(response.body, symbolize_names: true) json = ::JSON.parse(response.body, symbolize_names: true)
@ -202,7 +208,7 @@ RSpec.describe Admin::SiteTextsController do
describe '#show' do describe '#show' do
it 'returns a site text for a key that exists' do it 'returns a site text for a key that exists' do
get "/admin/customize/site_texts/js.topic.list.json" get "/admin/customize/site_texts/js.topic.list.json", params: { locale: default_locale }
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
@ -214,7 +220,7 @@ RSpec.describe Admin::SiteTextsController do
end end
it 'returns a site text for a key with ampersand' do it 'returns a site text for a key with ampersand' do
get "/admin/customize/site_texts/js.emoji_picker.food_&_drink.json" get "/admin/customize/site_texts/js.emoji_picker.food_&_drink.json", params: { locale: default_locale }
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
@ -226,30 +232,46 @@ RSpec.describe Admin::SiteTextsController do
end end
it 'returns not found for missing keys' do it 'returns not found for missing keys' do
get "/admin/customize/site_texts/made_up_no_key_exists.json" get "/admin/customize/site_texts/made_up_no_key_exists.json", params: { locale: default_locale }
expect(response.status).to eq(404) expect(response.status).to eq(404)
end end
it 'returns overridden = true if there is a translation_overrides record for the key' do it 'returns overridden = true if there is a translation_overrides record for the key' do
key = 'js.topic.list' key = 'js.topic.list'
put "/admin/customize/site_texts/#{key}.json", params: { put "/admin/customize/site_texts/#{key}.json", params: {
site_text: { value: I18n.t(key) } site_text: { value: I18n.t(key, locale: default_locale), locale: default_locale }
} }
expect(response.status).to eq(200) expect(response.status).to eq(200)
get "/admin/customize/site_texts/#{key}.json" get "/admin/customize/site_texts/#{key}.json", params: { locale: default_locale }
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
expect(json['site_text']['overridden']).to eq(true) expect(json['site_text']['overridden']).to eq(true)
TranslationOverride.destroy_all TranslationOverride.destroy_all
get "/admin/customize/site_texts/#{key}.json" get "/admin/customize/site_texts/#{key}.json", params: { locale: default_locale }
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
expect(json['site_text']['overridden']).to eq(false) expect(json['site_text']['overridden']).to eq(false)
end end
it 'returns a site text in the given locale' do
get "/admin/customize/site_texts/js.topic.list.json", params: { locale: 'es' }
expect(response.status).to eq(200)
json = response.parsed_body
site_text = json['site_text']
expect(site_text['id']).to eq('js.topic.list')
expect(site_text['value']).to eq(I18n.t("js.topic.list", locale: :es))
end
it 'fails if locale is not given' do
get "/admin/customize/site_texts/js.topic.list.json"
expect(response.status).to eq(400)
end
context 'plural keys' do context 'plural keys' do
before do before do
I18n.backend.store_translations(:en, colour: { one: '%{count} colour', other: '%{count} colours' }) I18n.backend.store_translations(:en, colour: { one: '%{count} colour', other: '%{count} colours' })
@ -257,12 +279,10 @@ RSpec.describe Admin::SiteTextsController do
shared_examples 'has correct plural keys' do shared_examples 'has correct plural keys' do
it 'returns the correct plural keys for the locale' do it 'returns the correct plural keys for the locale' do
SiteSetting.default_locale = locale
expected_translations.each do |key, value| expected_translations.each do |key, value|
id = "colour.#{key}" id = "colour.#{key}"
get "/admin/customize/site_texts/#{id}.json" get "/admin/customize/site_texts/#{id}.json", params: { locale: locale }
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
@ -307,7 +327,7 @@ RSpec.describe Admin::SiteTextsController do
describe '#update & #revert' do describe '#update & #revert' do
it "returns 'not found' when an unknown key is used" do it "returns 'not found' when an unknown key is used" do
put '/admin/customize/site_texts/some_key.json', params: { put '/admin/customize/site_texts/some_key.json', params: {
site_text: { value: 'foo' } site_text: { value: 'foo', locale: default_locale }
} }
expect(response.status).to eq(404) expect(response.status).to eq(404)
@ -318,7 +338,7 @@ RSpec.describe Admin::SiteTextsController do
it "works as expected with correct keys" do it "works as expected with correct keys" do
put '/admin/customize/site_texts/js.emoji_picker.animals_%26_nature.json', params: { put '/admin/customize/site_texts/js.emoji_picker.animals_%26_nature.json', params: {
site_text: { value: 'foo' } site_text: { value: 'foo', locale: default_locale }
} }
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -332,7 +352,7 @@ RSpec.describe Admin::SiteTextsController do
it "does not update restricted keys" do it "does not update restricted keys" do
put '/admin/customize/site_texts/user_notifications.confirm_old_email.title.json', params: { put '/admin/customize/site_texts/user_notifications.confirm_old_email.title.json', params: {
site_text: { value: 'foo' } site_text: { value: 'foo', locale: default_locale }
} }
expect(response.status).to eq(403) expect(response.status).to eq(403)
@ -347,7 +367,7 @@ RSpec.describe Admin::SiteTextsController do
I18n.backend.store_translations(SiteSetting.default_locale, some_key: '%{first} %{second}') I18n.backend.store_translations(SiteSetting.default_locale, some_key: '%{first} %{second}')
put "/admin/customize/site_texts/some_key.json", params: { put "/admin/customize/site_texts/some_key.json", params: {
site_text: { value: 'hello %{key} %{omg}' } site_text: { value: 'hello %{key} %{omg}', locale: default_locale }
} }
expect(response.status).to eq(422) expect(response.status).to eq(422)
@ -364,7 +384,7 @@ RSpec.describe Admin::SiteTextsController do
original_title = I18n.t(:title) original_title = I18n.t(:title)
put "/admin/customize/site_texts/title.json", params: { put "/admin/customize/site_texts/title.json", params: {
site_text: { value: 'yay' } site_text: { value: 'yay', locale: default_locale }
} }
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -374,7 +394,7 @@ RSpec.describe Admin::SiteTextsController do
expect(log.new_value).to eq('yay') expect(log.new_value).to eq('yay')
expect(log.action).to eq(UserHistory.actions[:change_site_text]) expect(log.action).to eq(UserHistory.actions[:change_site_text])
delete "/admin/customize/site_texts/title.json" delete "/admin/customize/site_texts/title.json", params: { locale: default_locale }
expect(response.status).to eq(200) expect(response.status).to eq(200)
log = UserHistory.last log = UserHistory.last
@ -387,7 +407,7 @@ RSpec.describe Admin::SiteTextsController do
it 'updates and reverts the key' do it 'updates and reverts the key' do
orig_title = I18n.t(:title) orig_title = I18n.t(:title)
put "/admin/customize/site_texts/title.json", params: { site_text: { value: 'hello' } } put "/admin/customize/site_texts/title.json", params: { site_text: { value: 'hello', locale: default_locale } }
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
@ -398,7 +418,7 @@ RSpec.describe Admin::SiteTextsController do
expect(site_text['value']).to eq('hello') expect(site_text['value']).to eq('hello')
# Revert # Revert
delete "/admin/customize/site_texts/title.json" delete "/admin/customize/site_texts/title.json", params: { locale: default_locale }
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
@ -410,34 +430,34 @@ RSpec.describe Admin::SiteTextsController do
end end
it 'returns site texts for the correct locale' do it 'returns site texts for the correct locale' do
SiteSetting.default_locale = :ru locale = :ru
ru_title = 'title ru' ru_title = 'title ru'
ru_mf_text = 'ru {NUM_RESULTS, plural, one {1 result} other {many} }' ru_mf_text = 'ru {NUM_RESULTS, plural, one {1 result} other {many} }'
put "/admin/customize/site_texts/title.json", params: { site_text: { value: ru_title } } put "/admin/customize/site_texts/title.json", params: { site_text: { value: ru_title, locale: locale } }
expect(response.status).to eq(200) expect(response.status).to eq(200)
put "/admin/customize/site_texts/js.topic.read_more_MF.json", params: { site_text: { value: ru_mf_text } } put "/admin/customize/site_texts/js.topic.read_more_MF.json", params: { site_text: { value: ru_mf_text, locale: locale } }
expect(response.status).to eq(200) expect(response.status).to eq(200)
get "/admin/customize/site_texts/title.json" get "/admin/customize/site_texts/title.json", params: { locale: locale }
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
expect(json['site_text']['value']).to eq(ru_title) expect(json['site_text']['value']).to eq(ru_title)
get "/admin/customize/site_texts/js.topic.read_more_MF.json" get "/admin/customize/site_texts/js.topic.read_more_MF.json", params: { locale: locale }
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
expect(json['site_text']['value']).to eq(ru_mf_text) expect(json['site_text']['value']).to eq(ru_mf_text)
SiteSetting.default_locale = :en en_locale = :en
get "/admin/customize/site_texts/title.json" get "/admin/customize/site_texts/title.json", params: { locale: en_locale }
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
expect(json['site_text']['value']).to_not eq(ru_title) expect(json['site_text']['value']).to_not eq(ru_title)
get "/admin/customize/site_texts/js.topic.read_more_MF.json" get "/admin/customize/site_texts/js.topic.read_more_MF.json", params: { locale: en_locale }
expect(response.status).to eq(200) expect(response.status).to eq(200)
json = response.parsed_body json = response.parsed_body
expect(json['site_text']['value']).to_not eq(ru_mf_text) expect(json['site_text']['value']).to_not eq(ru_mf_text)
@ -459,7 +479,7 @@ RSpec.describe Admin::SiteTextsController do
action: Jobs::BulkUserTitleUpdate::UPDATE_ACTION action: Jobs::BulkUserTitleUpdate::UPDATE_ACTION
}) do }) do
put '/admin/customize/site_texts/badges.regular.name.json', params: { put '/admin/customize/site_texts/badges.regular.name.json', params: {
site_text: { value: 'Terminator' } site_text: { value: 'Terminator', locale: default_locale }
} }
end end
@ -468,14 +488,14 @@ RSpec.describe Admin::SiteTextsController do
granted_badge_id: badge.id, granted_badge_id: badge.id,
action: Jobs::BulkUserTitleUpdate::RESET_ACTION action: Jobs::BulkUserTitleUpdate::RESET_ACTION
}) do }) do
delete "/admin/customize/site_texts/badges.regular.name.json" delete "/admin/customize/site_texts/badges.regular.name.json", params: { locale: default_locale }
end end
end end
it 'does not update matching user titles when overriding non-title badge text' do it 'does not update matching user titles when overriding non-title badge text' do
expect_not_enqueued_with(job: :bulk_user_title_update) do expect_not_enqueued_with(job: :bulk_user_title_update) do
put '/admin/customize/site_texts/badges.regular.long_description.json', params: { put '/admin/customize/site_texts/badges.regular.long_description.json', params: {
site_text: { value: 'Terminator' } site_text: { value: 'Terminator', locale: default_locale }
} }
end end
end end