From acc05dd3a5f3b2520b4a67da281be17bca72310f Mon Sep 17 00:00:00 2001 From: Luke GB Date: Sun, 12 Jul 2015 14:14:12 +0100 Subject: [PATCH 001/135] Fix LocalStore.remove_file to not raise if source doesn't exist FileUtils.move actually ends up raising an "unknown file type" error if the file doesn't exist instead of Errno::ENOENT. It's possible to rescue this, but in the end it's easier to just ask move not to throw an error, since we're going to throw it away anyway. --- lib/file_store/local_store.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/file_store/local_store.rb b/lib/file_store/local_store.rb index b5c172eeeb..53a20df389 100644 --- a/lib/file_store/local_store.rb +++ b/lib/file_store/local_store.rb @@ -14,7 +14,7 @@ module FileStore path = public_dir + url tombstone = public_dir + url.sub("/uploads/", "/tombstone/") FileUtils.mkdir_p(Pathname.new(tombstone).dirname) - FileUtils.move(path, tombstone) + FileUtils.move(path, tombstone, :force => true) rescue Errno::ENOENT # don't care if the file isn't there end From 728845d008f06828630a632e5c7d67e11bf15deb Mon Sep 17 00:00:00 2001 From: Kane York Date: Wed, 15 Jul 2015 08:56:46 -0700 Subject: [PATCH 002/135] FEATURE: Localization fallbacks (client) This patch sets I18n.defaultLocale in the Discourse.start() script block (it was formerly always 'en') to SiteSetting.default_locale, and patches translate() to perform fallback to defaultLocale followed by english. Additionally, when enable_verbose_localization() is called, no fallbacks will be performed. It also memoizes the file loading operations in JsLocaleHelper and strips out translations from the fallbacks that are also present in a prefered language, to minimize file size. --- app/assets/javascripts/locales/i18n.js | 12 +++ .../common/_discourse_javascript.html.erb | 1 + lib/js_locale_helper.rb | 95 +++++++++++++++---- 3 files changed, 90 insertions(+), 18 deletions(-) diff --git a/app/assets/javascripts/locales/i18n.js b/app/assets/javascripts/locales/i18n.js index 61ebac3941..c88b055160 100644 --- a/app/assets/javascripts/locales/i18n.js +++ b/app/assets/javascripts/locales/i18n.js @@ -52,6 +52,8 @@ I18n.PLACEHOLDER = /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm; I18n.fallbackRules = {}; +I18n.noFallbacks = false; + I18n.pluralizationRules = { en: function(n) { return n === 0 ? ["zero", "none", "other"] : n === 1 ? "one" : "other"; @@ -192,6 +194,15 @@ I18n.interpolate = function(message, options) { I18n.translate = function(scope, options) { options = this.prepareOptions(options); var translation = this.lookup(scope, options); + // Fallback to the default locale + if (!translation && this.currentLocale() !== this.defaultLocale && !this.noFallbacks) { + options.locale = this.defaultLocale; + translation = this.lookup(scope, options); + } + if (!translation && this.currentLocale() !== 'en' && !this.noFallbacks) { + options.locale = 'en'; + translation = this.lookup(scope, options); + } try { if (typeof translation === "object") { @@ -513,6 +524,7 @@ I18n.enable_verbose_localization = function(){ var keys = {}; var t = I18n.t; + I18n.noFallbacks = true; I18n.t = I18n.translate = function(scope, value){ var current = keys[scope]; diff --git a/app/views/common/_discourse_javascript.html.erb b/app/views/common/_discourse_javascript.html.erb index 98332fec1f..012592f0df 100644 --- a/app/views/common/_discourse_javascript.html.erb +++ b/app/views/common/_discourse_javascript.html.erb @@ -33,6 +33,7 @@ Discourse.Environment = '<%= Rails.env %>'; Discourse.SiteSettings = PreloadStore.get('siteSettings'); Discourse.LetterAvatarVersion = '<%= LetterAvatar.version %>'; + I18n.defaultLocale = '<%= SiteSetting.default_locale %>'; PreloadStore.get("customEmoji").forEach(function(emoji) { Discourse.Dialect.registerEmoji(emoji.name, emoji.url); }); diff --git a/lib/js_locale_helper.rb b/lib/js_locale_helper.rb index 2211015300..3ca8254354 100644 --- a/lib/js_locale_helper.rb +++ b/lib/js_locale_helper.rb @@ -1,30 +1,89 @@ module JsLocaleHelper - def self.output_locale(locale, translations = nil) + def self.load_translations(locale) + @loaded_translations ||= {} + @loaded_translations[locale] ||= begin + locale_str = locale.to_s + + # load default translations + translations = YAML::load(File.open("#{Rails.root}/config/locales/client.#{locale_str}.yml")) + # load plugins translations + plugin_translations = {} + Dir["#{Rails.root}/plugins/*/config/locales/client.#{locale_str}.yml"].each do |file| + plugin_translations.deep_merge! YAML::load(File.open(file)) + end + + # merge translations (plugin translations overwrite default translations) + translations[locale_str]['js'].deep_merge!(plugin_translations[locale_str]['js']) if translations[locale_str] && plugin_translations[locale_str] && plugin_translations[locale_str]['js'] + + # We used to split the admin versus the client side, but it's much simpler to just + # include both for now due to the small size of the admin section. + # + # For now, let's leave it split out in the translation file in case we want to split + # it again later, so we'll merge the JSON ourselves. + admin_contents = translations[locale_str].delete('admin_js') + translations[locale_str]['js'].deep_merge!(admin_contents) if admin_contents.present? + translations[locale_str]['js'].deep_merge!(plugin_translations[locale_str]['admin_js']) if translations[locale_str] && plugin_translations[locale_str] && plugin_translations[locale_str]['admin_js'] + + translations + end + end + + # purpose-built recursive algorithm ahoy! + def self.deep_delete_matches(deleting_from, *checking_hashes) + checking_hashes.compact! + + new_hash = deleting_from.dup + deleting_from.each do |key, value| + if value.is_a? Hash + # Recurse + new_at_key = deep_delete_matches(deleting_from[key], *(checking_hashes.map {|h| h[key]})) + if new_at_key.empty? + new_hash.delete key + else + new_hash[key] = new_at_key + end + else + if checking_hashes.any? {|h| h.include? key} + new_hash.delete key + end + end + end + new_hash + end + + def self.load_translations_merged(*locales) + @loaded_merges ||= {} + @loaded_merges[locales.join('-')] ||= begin + # TODO - this will need to be reworked to support N fallbacks in the future + all_translations = locales.map { |l| JsLocaleHelper.load_translations l } + merged_translations = {} + merged_translations[locales[0].to_s] = all_translations[0][locales[0].to_s] + if locales[1] + merged_translations[locales[1].to_s] = deep_delete_matches(all_translations[1][locales[1].to_s].dup, merged_translations[locales[0].to_s]) + end + if locales[2] + merged_translations[locales[2].to_s] = deep_delete_matches(all_translations[2][locales[2].to_s].dup, merged_translations[locales[0].to_s], merged_translations[locales[1].to_s]) + end + merged_translations + end + end + + def self.output_locale(locale, request=nil) current_locale = I18n.locale I18n.locale = locale.to_sym locale_str = locale.to_s + site_locale = SiteSetting.default_locale.to_sym - # load default translations - translations ||= YAML::load(File.open("#{Rails.root}/config/locales/client.#{locale_str}.yml")) - # load plugins translations - plugin_translations = {} - Dir["#{Rails.root}/plugins/*/config/locales/client.#{locale_str}.yml"].each do |file| - plugin_translations.deep_merge! YAML::load(File.open(file)) + if locale == :en + translations = load_translations(locale) + elsif locale == site_locale || site_locale == :en + translations = load_translations_merged(locale, :en) + else + translations = load_translations_merged(locale, site_locale, :en) end - # merge translations (plugin translations overwrite default translations) - translations[locale_str]['js'].deep_merge!(plugin_translations[locale_str]['js']) if translations[locale_str] && plugin_translations[locale_str] && plugin_translations[locale_str]['js'] - - # We used to split the admin versus the client side, but it's much simpler to just - # include both for now due to the small size of the admin section. - # - # For now, let's leave it split out in the translation file in case we want to split - # it again later, so we'll merge the JSON ourselves. - admin_contents = translations[locale_str].delete('admin_js') - translations[locale_str]['js'].deep_merge!(admin_contents) if admin_contents.present? - translations[locale_str]['js'].deep_merge!(plugin_translations[locale_str]['admin_js']) if translations[locale_str] && plugin_translations[locale_str] && plugin_translations[locale_str]['admin_js'] message_formats = strip_out_message_formats!(translations[locale_str]['js']) result = generate_message_format(message_formats, locale_str) From ecfa17b5a79dfdc91e7a4d50b42ae78a35d0a293 Mon Sep 17 00:00:00 2001 From: Kane York Date: Wed, 15 Jul 2015 09:04:45 -0700 Subject: [PATCH 003/135] FEATURE: Localization fallbacks (server-side) The FallbackLocaleList object tells I18n::Backend::Fallbacks what order the languages should be attempted in. Because of the translate_accelerator patch, the SiteSetting.default_locale is *not* guaranteed to be fully loaded after the server starts, so a call to ensure_loaded! is added after the locale is set for the current user. The declarations of config.i18n.fallbacks = true in the environment files were actually garbage, because the I18n.default_locale was SiteSetting.default_locale, so there was nothing to fall back to. *derp* --- app/controllers/application_controller.rb | 2 ++ config/cloud/cloud66/files/production.rb | 5 ---- config/environments/production.rb | 4 ---- config/environments/profile.rb | 4 ---- config/initializers/i18n.rb | 24 ++++++++++++++++++++ config/initializers/pluralization.rb | 2 -- lib/freedom_patches/translate_accelerator.rb | 5 ++++ 7 files changed, 31 insertions(+), 15 deletions(-) create mode 100644 config/initializers/i18n.rb delete mode 100644 config/initializers/pluralization.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index aa29468fb2..a643bfb515 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -155,6 +155,8 @@ class ApplicationController < ActionController::Base else SiteSetting.default_locale end + + I18n.fallbacks.ensure_loaded! end def store_preloaded(key, json) diff --git a/config/cloud/cloud66/files/production.rb b/config/cloud/cloud66/files/production.rb index b7f03a0977..db662574be 100644 --- a/config/cloud/cloud66/files/production.rb +++ b/config/cloud/cloud66/files/production.rb @@ -23,11 +23,6 @@ Discourse::Application.configure do # Specifies the header that your server uses for sending files config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx - # Enable locale fallbacks for I18n (makes lookups for any locale fall back to - # the I18n.default_locale when a translation can not be found) - config.i18n.fallbacks = true - - # you may use other configuration here for mail eg: sendgrid config.action_mailer.delivery_method = :smtp diff --git a/config/environments/production.rb b/config/environments/production.rb index 30263174ad..013e37f597 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -24,10 +24,6 @@ Discourse::Application.configure do config.log_level = :info - # Enable locale fallbacks for I18n (makes lookups for any locale fall back to - # the I18n.default_locale when a translation can not be found) - config.i18n.fallbacks = true - if GlobalSetting.smtp_address settings = { address: GlobalSetting.smtp_address, diff --git a/config/environments/profile.rb b/config/environments/profile.rb index 43531b0d5c..1784a23528 100644 --- a/config/environments/profile.rb +++ b/config/environments/profile.rb @@ -27,10 +27,6 @@ Discourse::Application.configure do # Specifies the header that your server uses for sending files config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx - # Enable locale fallbacks for I18n (makes lookups for any locale fall back to - # the I18n.default_locale when a translation can not be found) - config.i18n.fallbacks = true - # we recommend you use mailcatcher https://github.com/sj26/mailcatcher config.action_mailer.smtp_settings = { address: "localhost", port: 1025 } diff --git a/config/initializers/i18n.rb b/config/initializers/i18n.rb new file mode 100644 index 0000000000..685e6e29c4 --- /dev/null +++ b/config/initializers/i18n.rb @@ -0,0 +1,24 @@ +# order: after 02-freedom_patches.rb + +# Include pluralization module +require 'i18n/backend/pluralization' +I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization) + +# Include fallbacks module +require 'i18n/backend/fallbacks' +I18n.backend.class.send(:include, I18n::Backend::Fallbacks) + +# Configure custom fallback order +class FallbackLocaleList < Hash + def [](locale) + # user locale, site locale, english + # TODO - this can be extended to be per-language for a better user experience + # (e.g. fallback zh_TW to zh_CN / vice versa) + [locale, SiteSetting.default_locale.to_sym, :en].uniq.compact + end + + def ensure_loaded! + self[I18n.locale].each { |l| I18n.ensure_loaded! l } + end +end +I18n.fallbacks = FallbackLocaleList.new diff --git a/config/initializers/pluralization.rb b/config/initializers/pluralization.rb deleted file mode 100644 index 0a567d4451..0000000000 --- a/config/initializers/pluralization.rb +++ /dev/null @@ -1,2 +0,0 @@ -require "i18n/backend/pluralization" -I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization) diff --git a/lib/freedom_patches/translate_accelerator.rb b/lib/freedom_patches/translate_accelerator.rb index 95f9f9d0b1..7b3fc986a6 100644 --- a/lib/freedom_patches/translate_accelerator.rb +++ b/lib/freedom_patches/translate_accelerator.rb @@ -59,6 +59,11 @@ module I18n end end + def ensure_loaded!(locale) + @loaded_locales ||= [] + load_locale locale unless @loaded_locales.include?(locale) + end + def translate(key, *args) load_locale(config.locale) unless @loaded_locales.include?(config.locale) return translate_no_cache(key, *args) if args.length > 0 From 650eb86a74e2f409e3bb5f570d6f1baa0656422e Mon Sep 17 00:00:00 2001 From: Kane York Date: Wed, 15 Jul 2015 09:49:22 -0700 Subject: [PATCH 004/135] Disable in development (server) --- config/initializers/i18n.rb | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/config/initializers/i18n.rb b/config/initializers/i18n.rb index 685e6e29c4..00dab6775c 100644 --- a/config/initializers/i18n.rb +++ b/config/initializers/i18n.rb @@ -21,4 +21,15 @@ class FallbackLocaleList < Hash self[I18n.locale].each { |l| I18n.ensure_loaded! l } end end -I18n.fallbacks = FallbackLocaleList.new + +class NoFallbackLocaleList < FallbackLocaleList + def [](locale) + [locale] + end +end + +if Rails.env.production? + I18n.fallbacks = FallbackLocaleList.new +else + I18n.fallbacks = NoFallbackLocaleList.new +end From ff219bc65c657d959b23af34db5d3b5528281d88 Mon Sep 17 00:00:00 2001 From: Kane York Date: Wed, 15 Jul 2015 10:21:06 -0700 Subject: [PATCH 005/135] todo: write test --- spec/components/js_locale_helper_spec.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/components/js_locale_helper_spec.rb b/spec/components/js_locale_helper_spec.rb index a6da4a3a63..adc57e4456 100644 --- a/spec/components/js_locale_helper_spec.rb +++ b/spec/components/js_locale_helper_spec.rb @@ -84,6 +84,10 @@ describe JsLocaleHelper do expect(message).not_to match 'Plural Function not found' end + it 'performs fallbacks to english if a translation is not available' do + skip('todo: write test') + end + LocaleSiteSetting.values.each do |locale| it "generates valid date helpers for #{locale[:value]} locale" do js = JsLocaleHelper.output_locale(locale[:value]) From cb395662d0333463c7eb493c33f9adb316de3c30 Mon Sep 17 00:00:00 2001 From: Kane York Date: Wed, 15 Jul 2015 14:23:41 -0700 Subject: [PATCH 006/135] Add JsLocaleHelper tests for i18n fallback --- lib/js_locale_helper.rb | 22 ++--- spec/components/js_locale_helper_spec.rb | 107 +++++++++++++++++++---- 2 files changed, 104 insertions(+), 25 deletions(-) diff --git a/lib/js_locale_helper.rb b/lib/js_locale_helper.rb index 3ca8254354..5fe25e978b 100644 --- a/lib/js_locale_helper.rb +++ b/lib/js_locale_helper.rb @@ -1,7 +1,7 @@ module JsLocaleHelper def self.load_translations(locale) - @loaded_translations ||= {} + @loaded_translations ||= HashWithIndifferentAccess.new @loaded_translations[locale] ||= begin locale_str = locale.to_s @@ -69,19 +69,21 @@ module JsLocaleHelper end end - def self.output_locale(locale, request=nil) - current_locale = I18n.locale - I18n.locale = locale.to_sym - + def self.output_locale(locale) + locale_sym = locale.to_sym locale_str = locale.to_s + + current_locale = I18n.locale + I18n.locale = locale_sym + site_locale = SiteSetting.default_locale.to_sym - if locale == :en - translations = load_translations(locale) - elsif locale == site_locale || site_locale == :en - translations = load_translations_merged(locale, :en) + if locale_sym == :en + translations = load_translations(locale_sym) + elsif locale_sym == site_locale || site_locale == :en + translations = load_translations_merged(locale_sym, :en) else - translations = load_translations_merged(locale, site_locale, :en) + translations = load_translations_merged(locale_sym, site_locale, :en) end message_formats = strip_out_message_formats!(translations[locale_str]['js']) diff --git a/spec/components/js_locale_helper_spec.rb b/spec/components/js_locale_helper_spec.rb index adc57e4456..18d73d2df2 100644 --- a/spec/components/js_locale_helper_spec.rb +++ b/spec/components/js_locale_helper_spec.rb @@ -2,6 +2,24 @@ require 'spec_helper' require_dependency 'js_locale_helper' describe JsLocaleHelper do + + module StubLoadTranslations + def set_translations(locale, translations) + @loaded_translations ||= HashWithIndifferentAccess.new + @loaded_translations[locale] = translations + end + + def clear_cache! + @loaded_translations = nil + @loaded_merges = nil + end + end + JsLocaleHelper.extend StubLoadTranslations + + after do + JsLocaleHelper.clear_cache! + end + it 'should be able to generate translations' do expect(JsLocaleHelper.output_locale('en').length).to be > 0 end @@ -57,21 +75,23 @@ describe JsLocaleHelper do it 'handles message format special keys' do ctx = V8::Context.new ctx.eval("I18n = {};") - ctx.eval(JsLocaleHelper.output_locale('en', - { - "en" => - { - "js" => { - "hello" => "world", - "test_MF" => "{HELLO} {COUNT, plural, one {1 duck} other {# ducks}}", - "error_MF" => "{{BLA}", - "simple_MF" => "{COUNT, plural, one {1} other {#}}" - } - } - })) - expect(ctx.eval('I18n.translations')["en"]["js"]["hello"]).to eq("world") - expect(ctx.eval('I18n.translations')["en"]["js"]["test_MF"]).to eq(nil) + JsLocaleHelper.set_translations 'en', { + "en" => + { + "js" => { + "hello" => "world", + "test_MF" => "{HELLO} {COUNT, plural, one {1 duck} other {# ducks}}", + "error_MF" => "{{BLA}", + "simple_MF" => "{COUNT, plural, one {1} other {#}}" + } + } + } + + ctx.eval(JsLocaleHelper.output_locale('en')) + + expect(ctx.eval('I18n.translations["en"]["js"]["hello"]')).to eq("world") + expect(ctx.eval('I18n.translations["en"]["js"]["test_MF"]')).to eq(nil) expect(ctx.eval('I18n.messageFormat("test_MF", { HELLO: "hi", COUNT: 3 })')).to eq("hi 3 ducks") expect(ctx.eval('I18n.messageFormat("error_MF", { HELLO: "hi", COUNT: 3 })')).to match(/Invalid Format/) @@ -85,7 +105,64 @@ describe JsLocaleHelper do end it 'performs fallbacks to english if a translation is not available' do - skip('todo: write test') + JsLocaleHelper.set_translations 'en', { + "en" => { + "js" => { + "only_english" => "1-en", + "english_and_site" => "3-en", + "english_and_user" => "5-en", + "all_three" => "7-en", + } + } + } + JsLocaleHelper.set_translations 'ru', { + "ru" => { + "js" => { + "only_site" => "2-ru", + "english_and_site" => "3-ru", + "site_and_user" => "6-ru", + "all_three" => "7-ru", + } + } + } + JsLocaleHelper.set_translations 'uk', { + "uk" => { + "js" => { + "only_user" => "4-uk", + "english_and_user" => "5-uk", + "site_and_user" => "6-uk", + "all_three" => "7-uk", + } + } + } + + expected = { + "none" => "[uk.js.none]", + "only_english" => "1-en", + "only_site" => "2-ru", + "english_and_site" => "3-ru", + "only_user" => "4-uk", + "english_and_user" => "5-uk", + "site_and_user" => "6-uk", + "all_three" => "7-uk", + } + + SiteSetting.default_locale = 'ru' + I18n.locale = :uk + + ctx = V8::Context.new + ctx.eval('var window = this;') + ctx.load(Rails.root + 'app/assets/javascripts/locales/i18n.js') + ctx.eval(JsLocaleHelper.output_locale(I18n.locale)) + ctx.eval('I18n.defaultLocale = "ru";') + + # Test - unneeded translations are not emitted + expect(ctx.eval('I18n.translations.en.js').keys).to eq(["only_english"]) + expect(ctx.eval('I18n.translations.ru.js').keys).to eq(["only_site", "english_and_site"]) + + expected.each do |key, expect| + expect(ctx.eval("I18n.t(#{"js.#{key}".inspect})")).to eq(expect) + end end LocaleSiteSetting.values.each do |locale| From 15905be788fee0cc72e6f021ad89c1526f40c9e3 Mon Sep 17 00:00:00 2001 From: Kane York Date: Wed, 15 Jul 2015 14:25:24 -0700 Subject: [PATCH 007/135] Disable in development (js) --- lib/js_locale_helper.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/js_locale_helper.rb b/lib/js_locale_helper.rb index 5fe25e978b..85221ec804 100644 --- a/lib/js_locale_helper.rb +++ b/lib/js_locale_helper.rb @@ -78,12 +78,16 @@ module JsLocaleHelper site_locale = SiteSetting.default_locale.to_sym - if locale_sym == :en + if Rails.env.development? translations = load_translations(locale_sym) - elsif locale_sym == site_locale || site_locale == :en - translations = load_translations_merged(locale_sym, :en) else - translations = load_translations_merged(locale_sym, site_locale, :en) + if locale_sym == :en + translations = load_translations(locale_sym) + elsif locale_sym == site_locale || site_locale == :en + translations = load_translations_merged(locale_sym, :en) + else + translations = load_translations_merged(locale_sym, site_locale, :en) + end end message_formats = strip_out_message_formats!(translations[locale_str]['js']) From e3b778a23ac13bbadc2a1a583268c78ea561ae30 Mon Sep 17 00:00:00 2001 From: Kane York Date: Wed, 15 Jul 2015 14:30:16 -0700 Subject: [PATCH 008/135] fixup: support merging N languages (js) --- lib/js_locale_helper.rb | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/js_locale_helper.rb b/lib/js_locale_helper.rb index 85221ec804..22f7ebb835 100644 --- a/lib/js_locale_helper.rb +++ b/lib/js_locale_helper.rb @@ -55,15 +55,14 @@ module JsLocaleHelper def self.load_translations_merged(*locales) @loaded_merges ||= {} @loaded_merges[locales.join('-')] ||= begin - # TODO - this will need to be reworked to support N fallbacks in the future - all_translations = locales.map { |l| JsLocaleHelper.load_translations l } + all_translations = {} merged_translations = {} - merged_translations[locales[0].to_s] = all_translations[0][locales[0].to_s] - if locales[1] - merged_translations[locales[1].to_s] = deep_delete_matches(all_translations[1][locales[1].to_s].dup, merged_translations[locales[0].to_s]) - end - if locales[2] - merged_translations[locales[2].to_s] = deep_delete_matches(all_translations[2][locales[2].to_s].dup, merged_translations[locales[0].to_s], merged_translations[locales[1].to_s]) + loaded_locales = [] + + locales.map(&:to_s).each do |locale| + all_translations[locale] = JsLocaleHelper.load_translations locale + merged_translations[locale] = deep_delete_matches(all_translations[locale][locale], *loaded_locales.map { |l| merged_translations[l] }) + loaded_locales << locale end merged_translations end From 56276138b525ebf3ae6d3579d576b2aab7b6b03d Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 17 Jul 2015 16:23:43 +0800 Subject: [PATCH 009/135] Run main tests on Travis first. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9c2e282d89..352361f0f7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,9 +5,9 @@ env: - DISCOURSE_HOSTNAME=www.example.com - RUBY_GC_MALLOC_LIMIT=50000000 matrix: + - "RAILS_MASTER=0" - "RAILS42=1" - "RAILS_MASTER=1" - - "RAILS_MASTER=0" addons: postgresql: 9.3 From 5fc7545c0189ac81d14b976c562cb18070e92c73 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Fri, 17 Jul 2015 15:53:48 +0530 Subject: [PATCH 010/135] UX: include more details on Permalinks page --- .../admin/templates/permalinks_list_item.hbs | 26 ++++++++++++++++--- .../stylesheets/common/admin/admin_base.scss | 18 ++++++++++--- .../admin/permalinks_controller.rb | 8 ++---- app/models/permalink.rb | 10 +++++++ app/serializers/permalink_serializer.rb | 22 +++++++++++++++- 5 files changed, 69 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/admin/templates/permalinks_list_item.hbs b/app/assets/javascripts/admin/templates/permalinks_list_item.hbs index 4fbf2db55e..63d7ed839f 100644 --- a/app/assets/javascripts/admin/templates/permalinks_list_item.hbs +++ b/app/assets/javascripts/admin/templates/permalinks_list_item.hbs @@ -1,7 +1,25 @@
{{url}}
-
{{topic_id}}
-
{{post_id}}
-
{{category_id}}
-
{{external_url}}
+
+ {{#if topic_id}} + {{topic_id}} + ({{topic_title}}) + {{/if}} +
+
+ {{#if post_id}} + {{post_id}} + {{/if}} +
+
+ {{#if category_id}} + {{category_id}} + ({{category_name}}) + {{/if}} +
+
+ {{#if external_url}} + {{external_url}} + {{/if}} +
diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 05891ad344..fa68954053 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -1473,12 +1473,22 @@ table#user-badges { // Permalinks .permalinks { - .url, .external_url { - width: 300px; + .url, .topic_id, .category_id, .external_url { + text-overflow: ellipsis; + white-space: nowrap; } - .action, .topic_id, .post_id, .category_id { + .url { + width: 200px; + } + .topic_id, .external_url { + width: 220px; + } + .category_id { + width: 160px; + } + .action, .post_id { text-align: center; - width: 9.9099%; + width: 8%; } } diff --git a/app/controllers/admin/permalinks_controller.rb b/app/controllers/admin/permalinks_controller.rb index 9c2eb47fb2..875110e87a 100644 --- a/app/controllers/admin/permalinks_controller.rb +++ b/app/controllers/admin/permalinks_controller.rb @@ -3,12 +3,8 @@ class Admin::PermalinksController < Admin::AdminController before_filter :fetch_permalink, only: [:destroy] def index - filter = params[:filter] - - permalinks = Permalink - permalinks = permalinks.where('url ILIKE :filter OR external_url ILIKE :filter', filter: "%#{params[:filter]}%") if filter.present? - permalinks = permalinks.limit(100).order('created_at desc').to_a - + url = params[:filter] + permalinks = Permalink.filter_by(url) render_serialized(permalinks, PermalinkSerializer) end diff --git a/app/models/permalink.rb b/app/models/permalink.rb index c7c7d567e3..086df29440 100644 --- a/app/models/permalink.rb +++ b/app/models/permalink.rb @@ -80,6 +80,16 @@ class Permalink < ActiveRecord::Base return category.url if category nil end + + def self.filter_by(url=nil) + permalinks = Permalink + .includes(:topic, :post, :category) + .order('permalinks.created_at desc') + + permalinks.where!('url ILIKE :url OR external_url ILIKE :url', url: "%#{url}%") if url.present? + permalinks.limit!(100) + permalinks.to_a + end end # == Schema Information diff --git a/app/serializers/permalink_serializer.rb b/app/serializers/permalink_serializer.rb index 2d621ed72d..3a7440888e 100644 --- a/app/serializers/permalink_serializer.rb +++ b/app/serializers/permalink_serializer.rb @@ -1,3 +1,23 @@ class PermalinkSerializer < ApplicationSerializer - attributes :id, :url, :topic_id, :post_id, :category_id, :external_url + attributes :id, :url, :topic_id, :topic_title, :topic_url, :post_id, :post_url, :category_id, :category_name, :category_url, :external_url + + def topic_title + object.try(:topic).try(:title) + end + + def topic_url + object.try(:topic).try(:url) + end + + def post_url + object.try(:post).try(:url) + end + + def category_name + object.try(:category).try(:name) + end + + def category_url + object.try(:category).try(:url) + end end From 9f31c3d235ca8251cd6864d59642dc9430b79b4e Mon Sep 17 00:00:00 2001 From: Luke GB Date: Sun, 19 Jul 2015 21:41:14 +0100 Subject: [PATCH 011/135] Expose poll_maximum_options to client At present this doesn't actually seem to be exposed to the client. I discovered this by accident whilst trying to write my own plugin: https://github.com/discourse/discourse/blob/7b6d6b76eb3b7dbf1982c382a1f0e981e7ff8eee/app/models/site_setting.rb#L17-L18 --- plugins/poll/config/settings.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/poll/config/settings.yml b/plugins/poll/config/settings.yml index bdeb59954c..f75945a5d6 100644 --- a/plugins/poll/config/settings.yml +++ b/plugins/poll/config/settings.yml @@ -3,3 +3,4 @@ plugins: default: true poll_maximum_options: default: 20 + client: true From c552f846ae7bbc8496fd86370516dbdc2074bd3d Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 20 Jul 2015 09:45:11 +1000 Subject: [PATCH 012/135] add index --- script/import_scripts/lithium.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script/import_scripts/lithium.rb b/script/import_scripts/lithium.rb index bbc396f09c..2d21924d7b 100644 --- a/script/import_scripts/lithium.rb +++ b/script/import_scripts/lithium.rb @@ -431,7 +431,7 @@ class ImportScripts::Lithium < ImportScripts::Base puts "loading data into temp table" - PostAction.exec_sql("create temp table accepted_data(post_id int)") + PostAction.exec_sql("create temp table accepted_data(post_id int primary key)") PostAction.transaction do results.each do |result| @@ -446,6 +446,7 @@ class ImportScripts::Lithium < ImportScripts::Base end end + puts "deleting dupe answers" PostAction.exec_sql <<-SQL DELETE FROM accepted_data WHERE post_id NOT IN ( From e93665b9f750fee17683ea2657d39a508a2277b7 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 20 Jul 2015 16:56:32 +1000 Subject: [PATCH 013/135] FEATURE: site setting to allow html tables (which may come from imports) (allow_html_tables , disabled by default) --- .../javascripts/discourse/dialects/dialect.js | 10 +++- .../discourse/dialects/table_dialect.js | 52 +++++++++++++++++++ .../stylesheets/common/base/topic-post.scss | 16 ++++++ config/locales/server.en.yml | 1 + config/site_settings.yml | 3 ++ spec/components/pretty_text_spec.rb | 26 ++++++++++ vendor/assets/javascripts/better_markdown.js | 19 +++++-- 7 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 app/assets/javascripts/discourse/dialects/table_dialect.js diff --git a/app/assets/javascripts/discourse/dialects/dialect.js b/app/assets/javascripts/discourse/dialects/dialect.js index e5b9048401..62febca1a7 100644 --- a/app/assets/javascripts/discourse/dialects/dialect.js +++ b/app/assets/javascripts/discourse/dialects/dialect.js @@ -475,7 +475,7 @@ Discourse.Dialect = { **/ replaceBlock: function(args) { - this.registerBlock(args.start.toString(), function(block, next) { + var fn = function(block, next) { var linebreaks = dialect.options.traditional_markdown_linebreaks || Discourse.SiteSettings.traditional_markdown_linebreaks; @@ -565,7 +565,13 @@ Discourse.Dialect = { var emitterResult = args.emitter.call(this, contentBlocks, match, dialect.options); if (emitterResult) { result.push(emitterResult); } return result; - }); + }; + + if (args.priority) { + fn.priority = args.priority; + } + + this.registerBlock(args.start.toString(), fn); }, /** diff --git a/app/assets/javascripts/discourse/dialects/table_dialect.js b/app/assets/javascripts/discourse/dialects/table_dialect.js new file mode 100644 index 0000000000..d73393023e --- /dev/null +++ b/app/assets/javascripts/discourse/dialects/table_dialect.js @@ -0,0 +1,52 @@ +var flattenBlocks = function(blocks) { + var result = ""; + blocks.forEach(function(b) { + result += b; + if (b.trailing) { result += b.trailing; } + }); + + // bypass newline insertion + return result.replace(/[\n\r]/g, " "); +}; + +var emitter = function(contents) { + // TODO event should be fired when sanitizer loads + if (window.html4 && window.html4.ELEMENTS.td !== 1) { + window.html4.ELEMENTS.table = 0; + window.html4.ELEMENTS.tbody = 1; + window.html4.ELEMENTS.td = 1; + window.html4.ELEMENTS.thead = 1; + window.html4.ELEMENTS.th = 1; + window.html4.ELEMENTS.tr = 1; + } + return ['table', {"class": "md-table"}, flattenBlocks.apply(this, [contents])]; +}; + +var tableBlock = { + start: /()([\S\s]*)/igm, + stop: /<\/table>/igm, + rawContents: true, + emitter: emitter, + priority: 1 +}; + +var init = function(){ + if (Discourse.SiteSettings.allow_html_tables) { + Discourse.Markdown.whiteListTag("table"); + Discourse.Markdown.whiteListTag("table", "class", "md-table"); + Discourse.Markdown.whiteListTag("tbody"); + Discourse.Markdown.whiteListTag("thead"); + Discourse.Markdown.whiteListTag("tr"); + Discourse.Markdown.whiteListTag("th"); + Discourse.Markdown.whiteListTag("td"); + Discourse.Dialect.replaceBlock(tableBlock); + + } +}; + +if (Discourse.SiteSettings) { + init(); +} else { + Discourse.initializer({initialize: init, name: 'enable-html-tables'}); +} + diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 7e2444b2cc..ff80230d2d 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -243,4 +243,20 @@ blockquote > *:last-child { } +table.md-table { + thead { + border-bottom: 2px solid lighten($primary, 80%); + th { + text-align: left; + padding-bottom: 2px; + } + } + + td,th { + padding: 3px 3px 3px 10px; + } + tr { + border-bottom: 1px solid lighten($primary, 80%); + } +} diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 4b01b8b3e8..f4d2c718ca 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -842,6 +842,7 @@ en: flag_sockpuppets: "If a new user replies to a topic from the same IP address as the new user who started the topic, flag both of their posts as potential spam." traditional_markdown_linebreaks: "Use traditional linebreaks in Markdown, which require two trailing spaces for a linebreak." + allow_html_tables: "Allow tables to be entered in Markdown using HTML tags, TABLE, THEAD, TD, TR, TH are whiteliseted (requires full rebake on all old posts containing tables)" post_undo_action_window_mins: "Number of minutes users are allowed to undo recent actions on a post (like, flag, etc)." must_approve_users: "Staff must approve all new user accounts before they are allowed to access the site. WARNING: enabling this for a live site will revoke access for existing non-staff users!" ga_tracking_code: "Google analytics (ga.js) tracking code code, eg: UA-12345678-9; see http://google.com/analytics" diff --git a/config/site_settings.yml b/config/site_settings.yml index eb5755b1d9..716a89332a 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -386,6 +386,9 @@ posting: traditional_markdown_linebreaks: client: true default: false + allow_html_tables: + client: true + default: false suppress_reply_directly_below: client: true default: true diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb index 9416d0a681..de5e3fbb9c 100644 --- a/spec/components/pretty_text_spec.rb +++ b/spec/components/pretty_text_spec.rb @@ -331,4 +331,30 @@ describe PrettyText do expect(PrettyText.cook(raw)).to match_html(cooked) end + describe 'tables' do + before do + PrettyText.reset_context + end + + after do + PrettyText.reset_context + end + + it 'allows table html' do + SiteSetting.allow_html_tables = true + PrettyText.reset_context + table = "
\n
test
a
" + match = "
test
a
" + expect(PrettyText.cook(table)).to match_html(match) + + end + + it 'allows no tables when not enabled' do + SiteSetting.allow_html_tables = false + table = "
test
a
" + expect(PrettyText.cook(table)).to match_html("") + end + + end + end diff --git a/vendor/assets/javascripts/better_markdown.js b/vendor/assets/javascripts/better_markdown.js index 7a9e74b8c2..a445aacec5 100644 --- a/vendor/assets/javascripts/better_markdown.js +++ b/vendor/assets/javascripts/better_markdown.js @@ -318,13 +318,26 @@ // Build default order from insertion order. Markdown.buildBlockOrder = function(d) { - var ord = []; + var ord = [[]]; for ( var i in d ) { if ( i === "__order__" || i === "__call__" ) continue; - ord.push( i ); + + var priority = d[i].priority || 0; + ord[priority] = ord[priority] || []; + ord[priority].push( i ); } - d.__order__ = ord; + + var flattend = []; + for (i=ord.length-1; i>=0; i--){ + if (ord[i]) { + for (var j=0; j Date: Mon, 20 Jul 2015 17:05:24 +1000 Subject: [PATCH 014/135] strip classes from table instead of ignoring table --- app/assets/javascripts/discourse/dialects/table_dialect.js | 2 +- spec/components/pretty_text_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/dialects/table_dialect.js b/app/assets/javascripts/discourse/dialects/table_dialect.js index d73393023e..ad1bb375df 100644 --- a/app/assets/javascripts/discourse/dialects/table_dialect.js +++ b/app/assets/javascripts/discourse/dialects/table_dialect.js @@ -23,7 +23,7 @@ var emitter = function(contents) { }; var tableBlock = { - start: /()([\S\s]*)/igm, + start: /(]*>)([\S\s]*)/igm, stop: /<\/table>/igm, rawContents: true, emitter: emitter, diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb index de5e3fbb9c..bbbcb5d699 100644 --- a/spec/components/pretty_text_spec.rb +++ b/spec/components/pretty_text_spec.rb @@ -343,7 +343,7 @@ describe PrettyText do it 'allows table html' do SiteSetting.allow_html_tables = true PrettyText.reset_context - table = "
\n
test
a
" + table = "\n
test
a
" match = "
test
a
" expect(PrettyText.cook(table)).to match_html(match) From 4c92f05d8d53fddbc151e6a03870295aab87a8d5 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 20 Jul 2015 17:06:00 +1000 Subject: [PATCH 015/135] more tests --- spec/components/pretty_text_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb index bbbcb5d699..772b3417fd 100644 --- a/spec/components/pretty_text_spec.rb +++ b/spec/components/pretty_text_spec.rb @@ -343,7 +343,7 @@ describe PrettyText do it 'allows table html' do SiteSetting.allow_html_tables = true PrettyText.reset_context - table = "\n
test
a
" + table = "\n
test
a
" match = "
test
a
" expect(PrettyText.cook(table)).to match_html(match) From 35e6eb5cacf4137ee64e010f39d3ef36763c34cb Mon Sep 17 00:00:00 2001 From: Simon Cossar Date: Mon, 20 Jul 2015 00:42:52 -0700 Subject: [PATCH 016/135] Add wrap class --- app/assets/javascripts/discourse/templates/application.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/templates/application.hbs b/app/assets/javascripts/discourse/templates/application.hbs index 6b9b133219..23822e0f40 100644 --- a/app/assets/javascripts/discourse/templates/application.hbs +++ b/app/assets/javascripts/discourse/templates/application.hbs @@ -1,6 +1,6 @@ {{render "header"}} -
+
{{outlet}} {{render "user-card"}}
From 8840a611abd9c06723786125de680a864f7a838f Mon Sep 17 00:00:00 2001 From: Simon Cossar Date: Mon, 20 Jul 2015 00:47:53 -0700 Subject: [PATCH 017/135] Substitute wrap for container --- app/assets/stylesheets/common/base/discourse.scss | 5 +++++ app/assets/stylesheets/desktop/discourse.scss | 4 ++-- app/assets/stylesheets/desktop/topic-list.scss | 3 --- app/assets/stylesheets/desktop/topic-post.scss | 5 +---- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index d9bf627978..c98c4e891c 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -4,8 +4,13 @@ img.avatar { .container { @extend .clearfix; +} + +.wrap { + @extend .clearfix; margin-right: auto; margin-left: auto; + padding: 8px; .contents { position: relative; } diff --git a/app/assets/stylesheets/desktop/discourse.scss b/app/assets/stylesheets/desktop/discourse.scss index 52997e7b1f..0d117b4ae2 100644 --- a/app/assets/stylesheets/desktop/discourse.scss +++ b/app/assets/stylesheets/desktop/discourse.scss @@ -6,7 +6,7 @@ body { min-width: $medium-width; } - .container, + .wrap, .full-width { width: $medium-width; } @@ -17,7 +17,7 @@ and (max-width : 570px) { body { min-width: 0; } - .container, + .wrap, .full-width { min-width: 0; } diff --git a/app/assets/stylesheets/desktop/topic-list.scss b/app/assets/stylesheets/desktop/topic-list.scss index 45da7a1477..25f3a04cb4 100644 --- a/app/assets/stylesheets/desktop/topic-list.scss +++ b/app/assets/stylesheets/desktop/topic-list.scss @@ -303,9 +303,6 @@ and (max-width : 850px) { #topic-title { padding-left: 10px; } - .container.posts { - padding-left: 10px; - } .nav-pills { > li > a { diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 8f051d1e92..99794d40b3 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -1,8 +1,5 @@ -.container { - @extend .clearfix; +.wrap { max-width: $large-width; - margin-right: auto; - margin-left: auto; } .full-width { From c8085cf576b28369d2a887780e5ffd99c58a1d07 Mon Sep 17 00:00:00 2001 From: Simon Cossar Date: Mon, 20 Jul 2015 01:01:55 -0700 Subject: [PATCH 018/135] remove top and bottom padding --- app/assets/stylesheets/common/base/discourse.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index c98c4e891c..43da67c1e5 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -10,7 +10,7 @@ img.avatar { @extend .clearfix; margin-right: auto; margin-left: auto; - padding: 8px; + padding: 0 8px; .contents { position: relative; } From c47c5dd0c608f366188347a6329a036ae8c32fd3 Mon Sep 17 00:00:00 2001 From: Simon Cossar Date: Mon, 20 Jul 2015 01:02:51 -0700 Subject: [PATCH 019/135] Substitute wrap for container --- app/assets/javascripts/discourse/templates/header.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/templates/header.hbs b/app/assets/javascripts/discourse/templates/header.hbs index 4d71d1eb22..3a8bd239ed 100644 --- a/app/assets/javascripts/discourse/templates/header.hbs +++ b/app/assets/javascripts/discourse/templates/header.hbs @@ -1,4 +1,4 @@ -
+
{{home-logo minimized=showExtraInfo}} From 6ca8f14db430acdaf4f4d84b719893a21ec27e88 Mon Sep 17 00:00:00 2001 From: Simon Cossar Date: Mon, 20 Jul 2015 01:03:26 -0700 Subject: [PATCH 020/135] Remove padding from media query --- app/assets/stylesheets/desktop/topic-list.scss | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/assets/stylesheets/desktop/topic-list.scss b/app/assets/stylesheets/desktop/topic-list.scss index 25f3a04cb4..2e90524da0 100644 --- a/app/assets/stylesheets/desktop/topic-list.scss +++ b/app/assets/stylesheets/desktop/topic-list.scss @@ -298,12 +298,6 @@ button.dismiss-read { @media all and (max-width : 850px) { - // add some left padding to topics otherwise everything is 100% flush - // with left edge in portrait tablet, which looks awful - #topic-title { - padding-left: 10px; - } - .nav-pills { > li > a { font-size: 1em; @@ -312,7 +306,6 @@ and (max-width : 850px) { } .list-controls { - padding: 0 5px; .btn { font-size: 1em From e5b7cf48c030e3a06e00f12181cff84fd4165703 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Mon, 20 Jul 2015 15:37:49 +0530 Subject: [PATCH 021/135] FIX: set max value for tl3_promotion_min_duration setting --- config/site_settings.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/site_settings.yml b/config/site_settings.yml index 716a89332a..f66bfc6a39 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -628,6 +628,7 @@ trust: tl3_promotion_min_duration: default: 14 min: 0 + max: 20000 tl3_requires_likes_given: default: 30 min: 0 From c54de7159c266657cad115aaa79061e0ede2c146 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Mon, 20 Jul 2015 17:40:22 +0530 Subject: [PATCH 022/135] UX: more permalinks page improvements --- .../javascripts/admin/templates/permalinks.hbs | 6 +++--- .../admin/templates/permalinks_list_item.hbs | 14 ++++++-------- .../stylesheets/common/admin/admin_base.scss | 12 ++++++------ app/serializers/permalink_serializer.rb | 10 +++++++++- config/locales/client.en.yml | 3 +++ 5 files changed, 27 insertions(+), 18 deletions(-) diff --git a/app/assets/javascripts/admin/templates/permalinks.hbs b/app/assets/javascripts/admin/templates/permalinks.hbs index 47717627b6..7d6e1ff8fc 100644 --- a/app/assets/javascripts/admin/templates/permalinks.hbs +++ b/app/assets/javascripts/admin/templates/permalinks.hbs @@ -10,9 +10,9 @@