Add message format support that can be used on complex localization strings

Add message about new and unread topics at the bottom of topics
move localization helper into lib
This commit is contained in:
Sam 2013-05-30 15:53:40 +10:00
parent e93b7a3b20
commit 8874c9ea75
77 changed files with 2279 additions and 29 deletions

View File

@ -101,7 +101,7 @@
</table> </table>
</div> </div>
<br/> <br/>
<h3>{{{unbound view.browseMoreMessage}}}</h3> <h3>{{{view.browseMoreMessage}}}</h3>
</div> </div>
{{/if}} {{/if}}
{{/if}} {{/if}}

View File

@ -440,20 +440,41 @@ Discourse.TopicView = Discourse.View.extend(Discourse.Scrolling, {
} }
}, },
topicTrackingState: function(){
return Discourse.TopicTrackingState.current();
}.property(),
browseMoreMessage: (function() { browseMoreMessage: (function() {
var category, opts; var category, opts;
opts = { opts = {
latestLink: "<a href=\"/\">" + (Em.String.i18n("topic.view_latest_topics")) + "</a>" latestLink: "<a href=\"/\">" + (Em.String.i18n("topic.view_latest_topics")) + "</a>"
}; };
if (category = this.get('controller.content.category')) { if (category = this.get('controller.content.category')) {
opts.catLink = Discourse.Utilities.categoryLink(category); opts.catLink = Discourse.Utilities.categoryLink(category);
return Ember.String.i18n("topic.read_more_in_category", opts);
} else { } else {
opts.catLink = "<a href=\"" + Discourse.getURL("/categories") + "\">" + (Em.String.i18n("topic.browse_all_categories")) + "</a>"; opts.catLink = "<a href=\"" + Discourse.getURL("/categories") + "\">" + (Em.String.i18n("topic.browse_all_categories")) + "</a>";
}
var tracking = this.get('topicTrackingState');
var unreadTopics = tracking.countUnread();
var newTopics = tracking.countNew();
if (newTopics + unreadTopics > 0) {
if(category) {
return I18n.messageFormat("topic.read_more_in_category_MF", {"UNREAD": unreadTopics, "NEW": newTopics, catLink: opts.catLink})
} else {
return I18n.messageFormat("topic.read_more_MF", {"UNREAD": unreadTopics, "NEW": newTopics, latestLink: opts.latestLink})
}
}
else if (category) {
return Ember.String.i18n("topic.read_more_in_category", opts);
} else {
return Ember.String.i18n("topic.read_more", opts); return Ember.String.i18n("topic.read_more", opts);
} }
}).property() }).property('topicTrackingState.messageCount')
}); });

View File

@ -1,23 +0,0 @@
module JsLocaleHelper
def self.output_locale(locale)
locale_str = locale.to_s
translations = YAML::load(File.open("#{Rails.root}/config/locales/client.#{locale_str}.yml"))
# 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'].merge!(admin_contents) if admin_contents.present?
result = "I18n.translations = #{translations.to_json};\n"
result << "I18n.locale = '#{locale_str}'\n"
result
end
end

View File

@ -19,6 +19,7 @@ module Discourse
# -- all .rb files in that directory are automatically loaded. # -- all .rb files in that directory are automatically loaded.
require 'discourse' require 'discourse'
require 'js_locale_helper'
# mocha hates us, active_support/testing/mochaing.rb line 2 is requiring the wrong # mocha hates us, active_support/testing/mochaing.rb line 2 is requiring the wrong
# require, patched in source, on upgrade remove this # require, patched in source, on upgrade remove this

View File

@ -505,6 +505,11 @@ en:
toggle_information: "toggle topic details" toggle_information: "toggle topic details"
read_more_in_category: "Want to read more? Browse other topics in {{catLink}} or {{latestLink}}." read_more_in_category: "Want to read more? Browse other topics in {{catLink}} or {{latestLink}}."
read_more: "Want to read more? {{catLink}} or {{latestLink}}." read_more: "Want to read more? {{catLink}} or {{latestLink}}."
# keys ending with _MF use message format, see /spec/components/js_local_helper_spec.rb for samples
read_more_in_category_MF: "There {UNREAD, plural, one {is <a href='/unread'>1 unread</a>} other {are <a href='/unread'># unread</a>}} and {NEW, plural, one {<a href='/new'>1 new</a> topic} other {<a href='/new'># new</a> topics}} remaining, or browse other topics in {catLink}"
read_more_MF: "There {UNREAD, plural, one {is <a href='/unread'>1 unread</a>} other {are <a href='/unread'># unread</a>}} and {NEW, plural, one {<a href='/new'>1 new</a> topic} other {<a href='/new'># new</a> topics}} remaining, or {latestLink}"
browse_all_categories: Browse all categories browse_all_categories: Browse all categories
view_latest_topics: view latest topics view_latest_topics: view latest topics

View File

@ -0,0 +1,6 @@
MessageFormat.locale.af = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.am = function(n) {
if (n === 0 || n == 1) {
return 'one';
}
return 'other';
};

View File

@ -0,0 +1,18 @@
MessageFormat.locale.ar = function(n) {
if (n === 0) {
return 'zero';
}
if (n == 1) {
return 'one';
}
if (n == 2) {
return 'two';
}
if ((n % 100) >= 3 && (n % 100) <= 10 && n == Math.floor(n)) {
return 'few';
}
if ((n % 100) >= 11 && (n % 100) <= 99 && n == Math.floor(n)) {
return 'many';
}
return 'other';
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.bg = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.bn = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,18 @@
MessageFormat.locale.br = function (n) {
if (n === 0) {
return 'zero';
}
if (n == 1) {
return 'one';
}
if (n == 2) {
return 'two';
}
if (n == 3) {
return 'few';
}
if (n == 6) {
return 'many';
}
return 'other';
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.ca = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,9 @@
MessageFormat.locale.cs = function (n) {
if (n == 1) {
return 'one';
}
if (n == 2 || n == 3 || n == 4) {
return 'few';
}
return 'other';
};

View File

@ -0,0 +1,18 @@
MessageFormat.locale.cy = function (n) {
if (n === 0) {
return 'zero';
}
if (n == 1) {
return 'one';
}
if (n == 2) {
return 'two';
}
if (n == 3) {
return 'few';
}
if (n == 6) {
return 'many';
}
return 'other';
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.da = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.de = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.el = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.en = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.es = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.et = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.eu = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,3 @@
MessageFormat.locale.fa = function ( n ) {
return "other";
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.fi = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.fil = function(n) {
if (n === 0 || n == 1) {
return 'one';
}
return 'other';
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.fr = function (n) {
if (n >= 0 && n < 2) {
return 'one';
}
return 'other';
};

View File

@ -0,0 +1,9 @@
MessageFormat.locale.ga = function (n) {
if (n == 1) {
return 'one';
}
if (n == 2) {
return 'two';
}
return 'other';
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.gl = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.gsw = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.gu = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.he = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.hi = function(n) {
if (n === 0 || n == 1) {
return 'one';
}
return 'other';
};

View File

@ -0,0 +1,14 @@
MessageFormat.locale.hr = function (n) {
if ((n % 10) == 1 && (n % 100) != 11) {
return 'one';
}
if ((n % 10) >= 2 && (n % 10) <= 4 &&
((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) {
return 'few';
}
if ((n % 10) === 0 || ((n % 10) >= 5 && (n % 10) <= 9) ||
((n % 100) >= 11 && (n % 100) <= 14) && n == Math.floor(n)) {
return 'many';
}
return 'other';
};

View File

@ -0,0 +1,3 @@
MessageFormat.locale.hu = function(n) {
return 'other';
};

View File

@ -0,0 +1,3 @@
MessageFormat.locale.id = function(n) {
return 'other';
};

View File

@ -0,0 +1,3 @@
MessageFormat.locale["in"] = function(n) {
return 'other';
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.is = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.it = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.iw = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,3 @@
MessageFormat.locale.ja = function ( n ) {
return "other";
};

View File

@ -0,0 +1,3 @@
MessageFormat.locale.kn = function ( n ) {
return "other";
};

View File

@ -0,0 +1,3 @@
MessageFormat.locale.ko = function ( n ) {
return "other";
};

View File

@ -0,0 +1,9 @@
MessageFormat.locale.lag = function (n) {
if (n === 0) {
return 'zero';
}
if (n > 0 && n < 2) {
return 'one';
}
return 'other';
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.ln = function(n) {
if (n === 0 || n == 1) {
return 'one';
}
return 'other';
};

View File

@ -0,0 +1,10 @@
MessageFormat.locale.lt = function (n) {
if ((n % 10) == 1 && ((n % 100) < 11 || (n % 100) > 19)) {
return 'one';
}
if ((n % 10) >= 2 && (n % 10) <= 9 &&
((n % 100) < 11 || (n % 100) > 19) && n == Math.floor(n)) {
return 'few';
}
return 'other';
};

View File

@ -0,0 +1,9 @@
MessageFormat.locale.lv = function (n) {
if (n === 0) {
return 'zero';
}
if ((n % 10) == 1 && (n % 100) != 11) {
return 'one';
}
return 'other';
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.mk = function (n) {
if ((n % 10) == 1 && n != 11) {
return 'one';
}
return 'other';
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.ml = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,10 @@
MessageFormat.locale.mo = function (n) {
if (n == 1) {
return 'one';
}
if (n === 0 || n != 1 && (n % 100) >= 1 &&
(n % 100) <= 19 && n == Math.floor(n)) {
return 'few';
}
return 'other';
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.mr = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,3 @@
MessageFormat.locale.ms = function ( n ) {
return "other";
};

View File

@ -0,0 +1,12 @@
MessageFormat.locale.mt = function (n) {
if (n == 1) {
return 'one';
}
if (n === 0 || ((n % 100) >= 2 && (n % 100) <= 4 && n == Math.floor(n))) {
return 'few';
}
if ((n % 100) >= 11 && (n % 100) <= 19 && n == Math.floor(n)) {
return 'many';
}
return 'other';
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.nl = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.no = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.or = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,15 @@
MessageFormat.locale.pl = function (n) {
if (n == 1) {
return 'one';
}
if ((n % 10) >= 2 && (n % 10) <= 4 &&
((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) {
return 'few';
}
if ((n % 10) === 0 || n != 1 && (n % 10) == 1 ||
((n % 10) >= 5 && (n % 10) <= 9 || (n % 100) >= 12 && (n % 100) <= 14) &&
n == Math.floor(n)) {
return 'many';
}
return 'other';
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.pt = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,10 @@
MessageFormat.locale.ro = function (n) {
if (n == 1) {
return 'one';
}
if (n === 0 || n != 1 && (n % 100) >= 1 &&
(n % 100) <= 19 && n == Math.floor(n)) {
return 'few';
}
return 'other';
};

View File

@ -0,0 +1,14 @@
MessageFormat.locale.ru = function (n) {
if ((n % 10) == 1 && (n % 100) != 11) {
return 'one';
}
if ((n % 10) >= 2 && (n % 10) <= 4 &&
((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) {
return 'few';
}
if ((n % 10) === 0 || ((n % 10) >= 5 && (n % 10) <= 9) ||
((n % 100) >= 11 && (n % 100) <= 14) && n == Math.floor(n)) {
return 'many';
}
return 'other';
};

View File

@ -0,0 +1,9 @@
MessageFormat.locale.shi = function(n) {
if (n >= 0 && n <= 1) {
return 'one';
}
if (n >= 2 && n <= 10 && n == Math.floor(n)) {
return 'few';
}
return 'other';
};

View File

@ -0,0 +1,9 @@
MessageFormat.locale.sk = function (n) {
if (n == 1) {
return 'one';
}
if (n == 2 || n == 3 || n == 4) {
return 'few';
}
return 'other';
};

View File

@ -0,0 +1,12 @@
MessageFormat.locale.sl = function (n) {
if ((n % 100) == 1) {
return 'one';
}
if ((n % 100) == 2) {
return 'two';
}
if ((n % 100) == 3 || (n % 100) == 4) {
return 'few';
}
return 'other';
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.sq = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,14 @@
MessageFormat.locale.sr = function (n) {
if ((n % 10) == 1 && (n % 100) != 11) {
return 'one';
}
if ((n % 10) >= 2 && (n % 10) <= 4 &&
((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) {
return 'few';
}
if ((n % 10) === 0 || ((n % 10) >= 5 && (n % 10) <= 9) ||
((n % 100) >= 11 && (n % 100) <= 14) && n == Math.floor(n)) {
return 'many';
}
return 'other';
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.sv = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.sw = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.ta = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.te = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,3 @@
MessageFormat.locale.th = function ( n ) {
return "other";
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.tl = function(n) {
if (n === 0 || n == 1) {
return 'one';
}
return 'other';
};

View File

@ -0,0 +1,3 @@
MessageFormat.locale.tr = function(n) {
return 'other';
};

View File

@ -0,0 +1,14 @@
MessageFormat.locale.uk = function (n) {
if ((n % 10) == 1 && (n % 100) != 11) {
return 'one';
}
if ((n % 10) >= 2 && (n % 10) <= 4 &&
((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) {
return 'few';
}
if ((n % 10) === 0 || ((n % 10) >= 5 && (n % 10) <= 9) ||
((n % 100) >= 11 && (n % 100) <= 14) && n == Math.floor(n)) {
return 'many';
}
return 'other';
};

View File

@ -0,0 +1,6 @@
MessageFormat.locale.ur = function ( n ) {
if ( n === 1 ) {
return "one";
}
return "other";
};

View File

@ -0,0 +1,3 @@
MessageFormat.locale.vi = function ( n ) {
return "other";
};

View File

@ -0,0 +1,3 @@
MessageFormat.locale.zh = function ( n ) {
return "other";
};

File diff suppressed because it is too large Load Diff

74
lib/js_locale_helper.rb Normal file
View File

@ -0,0 +1,74 @@
module JsLocaleHelper
def self.output_locale(locale, translations = nil)
locale_str = locale.to_s
translations ||= YAML::load(File.open("#{Rails.root}/config/locales/client.#{locale_str}.yml"))
# 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'].merge!(admin_contents) if admin_contents.present?
message_formats = strip_out_message_formats!(translations[locale_str]['js'])
result = generate_message_format(message_formats, locale_str)
result << "I18n.translations = #{translations.to_json};\n"
result << "I18n.locale = '#{locale_str}'\n"
result
end
def self.generate_message_format(message_formats, locale_str)
formats = message_formats.map{|k,v| k.inspect << " : " << compile_message_format(locale_str ,v)}.join(" , ")
result = "MessageFormat = {locale: {}};\n"
result << File.read(Rails.root + "lib/javascripts/locale/#{locale_str}.js") << "\n"
result << "I18n.messageFormat = (function(formats){
var f = formats;
return function(key, options) {
var fn = f[key];
if(fn){
try {
return fn(options);
} catch(err) {
return err.message;
}
} else {
return 'Missing Key: ' + key
}
return f[key](options);
};
})({#{formats}});"
end
def self.compile_message_format(locale, format)
ctx = V8::Context.new
ctx.load(Rails.root + 'lib/javascripts/messageformat.js')
ctx.eval("mf = new MessageFormat('#{locale}');")
ctx.eval("mf.precompile(mf.parse(#{format.inspect}))")
rescue V8::Error => e
message = "Invalid Format: " << e.message
"function(){ return #{message.inspect};}"
end
def self.strip_out_message_formats!(hash, prefix = "", rval = {})
if Hash === hash
hash.each do |k,v|
if Hash === v
rval.merge!(strip_out_message_formats!(v, prefix + (prefix.length > 0 ? "." : "") << k, rval))
elsif k.end_with?("_MF")
rval[prefix + (prefix.length > 0 ? "." : "") << k] = v
hash.delete(k)
end
end
end
rval
end
end

View File

@ -0,0 +1,82 @@
require 'spec_helper'
require_dependency 'js_locale_helper'
describe JsLocaleHelper do
it 'should be able to generate translations' do
JsLocaleHelper.output_locale('en').length.should > 0
end
def setup_message_format(format)
@ctx = V8::Context.new
@ctx.eval('MessageFormat = {locale: {}};')
@ctx.load(Rails.root + 'lib/javascripts/locale/en.js')
compiled = JsLocaleHelper.compile_message_format('en', format)
@ctx.eval("var test = #{compiled}")
end
def localize(opts)
@ctx.eval("test(#{opts.to_json})")
end
it 'handles plurals' do
setup_message_format('{NUM_RESULTS, plural,
one {1 result}
other {# results}
}')
localize(NUM_RESULTS: 1).should == '1 result'
localize(NUM_RESULTS: 2).should == '2 results'
end
it 'handles double plurals' do
setup_message_format('{NUM_RESULTS, plural,
one {1 result}
other {# results}
} and {NUM_APPLES, plural,
one {1 apple}
other {# apples}
}')
localize(NUM_RESULTS: 1, NUM_APPLES: 2).should == '1 result and 2 apples'
localize(NUM_RESULTS: 2, NUM_APPLES: 1).should == '2 results and 1 apple'
end
it 'handles select' do
setup_message_format('{GENDER, select, male {He} female {She} other {They}} read a book')
localize(GENDER: 'male').should == 'He read a book'
localize(GENDER: 'female').should == 'She read a book'
localize(GENDER: 'none').should == 'They read a book'
end
it 'can strip out message formats' do
hash = {"a" => "b", "c" => { "d" => {"f_MF" => "bob"} }}
JsLocaleHelper.strip_out_message_formats!(hash).should == {"c.d.f_MF" => "bob"}
hash["c"]["d"].should == {}
end
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 {#}}"
}
}
}))
ctx.eval('I18n.translations')["en"]["js"]["hello"].should == "world"
ctx.eval('I18n.translations')["en"]["js"]["test_MF"].should be_nil
ctx.eval('I18n.messageFormat("test_MF", { HELLO: "hi", COUNT: 3 })').should == "hi 3 ducks"
ctx.eval('I18n.messageFormat("error_MF", { HELLO: "hi", COUNT: 3 })').should =~ /Invalid Format/
ctx.eval('I18n.messageFormat("missing", {})').should =~ /missing/
ctx.eval('I18n.messageFormat("simple_MF", {})').should =~ /COUNT/ # error
end
end