From 22ffcba8e6ac9f675c3ff619b2c6aef300fb78e0 Mon Sep 17 00:00:00 2001 From: Robin Ward <robin.ward@gmail.com> Date: Wed, 1 Apr 2015 14:18:46 -0400 Subject: [PATCH] Convert `Discourse.Post` to ES6 and use Store model - Includes acceptance tests for composer (post, edit) - Supports acceptance testing of bootbox --- .../discourse/adapters/post.js.es6 | 11 + .../discourse/controllers/composer.js.es6 | 5 +- .../discourse/lib/Markdown.Editor.js | 14 +- .../discourse/models/composer.js.es6 | 190 ++++---- .../models/{_post.js => post.js.es6} | 173 +++---- app/assets/javascripts/main_include.js | 1 + app/controllers/posts_controller.rb | 6 +- .../acceptance/composer-test.js.es6 | 117 +++++ .../acceptance/header-test-staff.js.es6 | 6 +- test/javascripts/fixtures/post.js.es6 | 4 + .../fixtures/session-fixtures.js.es6 | 4 + test/javascripts/fixtures/topic.js.es6 | 2 +- .../helpers/create-pretender.js.es6 | 56 ++- test/javascripts/helpers/qunit-helpers.js.es6 | 46 +- test/javascripts/models/composer-test.js.es6 | 87 ++-- test/javascripts/test_helper.js | 6 +- vendor/assets/javascripts/bootbox.js | 8 +- vendor/assets/javascripts/bootstrap-modal.js | 438 +++++++++++------- vendor/assets/javascripts/ember-cloaking.js | 13 +- 19 files changed, 747 insertions(+), 440 deletions(-) create mode 100644 app/assets/javascripts/discourse/adapters/post.js.es6 rename app/assets/javascripts/discourse/models/{_post.js => post.js.es6} (78%) create mode 100644 test/javascripts/acceptance/composer-test.js.es6 create mode 100644 test/javascripts/fixtures/post.js.es6 create mode 100644 test/javascripts/fixtures/session-fixtures.js.es6 diff --git a/app/assets/javascripts/discourse/adapters/post.js.es6 b/app/assets/javascripts/discourse/adapters/post.js.es6 new file mode 100644 index 000000000..1705a07e6 --- /dev/null +++ b/app/assets/javascripts/discourse/adapters/post.js.es6 @@ -0,0 +1,11 @@ +import RestAdapter from 'discourse/adapters/rest'; + +export default RestAdapter.extend({ + + // GET /posts doesn't include a type + find(store, type, findArgs) { + return this._super(store, type, findArgs).then(function(result) { + return {post: result}; + }); + } +}); diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index cc61d4a3b..2a2f13cf4 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -61,7 +61,8 @@ export default DiscourseController.extend({ if (postId) { this.set('model.loading', true); const composer = this; - return Discourse.Post.load(postId).then(function(post) { + + return this.store.find('post', postId).then(function(post) { const quote = Discourse.Quote.build(post, post.get("raw")); composer.appendBlockAtCursor(quote); composer.set('model.loading', false); @@ -412,7 +413,7 @@ export default DiscourseController.extend({ composerModel.set('topic', opts.topic); } } else { - composerModel = composerModel || Discourse.Composer.create(); + composerModel = composerModel || Discourse.Composer.create({ store: this.store }); composerModel.open(opts); } diff --git a/app/assets/javascripts/discourse/lib/Markdown.Editor.js b/app/assets/javascripts/discourse/lib/Markdown.Editor.js index 7c6ef1358..568b486db 100644 --- a/app/assets/javascripts/discourse/lib/Markdown.Editor.js +++ b/app/assets/javascripts/discourse/lib/Markdown.Editor.js @@ -323,7 +323,13 @@ // Adds a listener callback to a DOM element which is fired on a specified // event. util.addEvent = function (elem, event, listener) { - elem.addEventListener(event, listener, false); + var wrapped = function() { + var wrappedArgs = Array.prototype.slice(arguments); + Ember.run(function() { + listener.call(this, wrappedArgs); + }); + }; + elem.addEventListener(event, wrapped, false); }; @@ -904,7 +910,7 @@ // TODO allow us to inject this in (its our debouncer) var debounce = function(func,wait,trickle) { var timeout = null; - return function(){ + return function() { var context = this; var args = arguments; @@ -924,8 +930,8 @@ currentWait = wait; } - if (timeout) { clearTimeout(timeout); } - timeout = setTimeout(later, currentWait); + if (timeout) { Ember.run.cancel(timeout); } + timeout = Ember.run.later(later, currentWait); } } diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index fa101a177..8171432a0 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -29,7 +29,7 @@ const CLOSED = 'closed', const Composer = Discourse.Model.extend({ archetypes: function() { - return Discourse.Site.currentProp('archetypes'); + return this.site.get('archetypes'); }.property(), creatingTopic: Em.computed.equal('action', CREATE_TOPIC), @@ -127,21 +127,16 @@ const Composer = Discourse.Model.extend({ } else { // has a category? (when needed) return this.get('canCategorize') && - !Discourse.SiteSettings.allow_uncategorized_topics && + !this.siteSettings.allow_uncategorized_topics && !this.get('categoryId') && - !Discourse.User.currentProp('staff'); + !this.user.get('staff'); } }.property('loading', 'canEditTitle', 'titleLength', 'targetUsernames', 'replyLength', 'categoryId', 'missingReplyCharacters'), - /** - Is the title's length valid? - - @property titleLengthValid - **/ titleLengthValid: function() { - if (Discourse.User.currentProp('admin') && this.get('post.static_doc') && this.get('titleLength') > 0) return true; + if (this.user.get('admin') && this.get('post.static_doc') && this.get('titleLength') > 0) return true; if (this.get('titleLength') < this.get('minimumTitleLength')) return false; - return (this.get('titleLength') <= Discourse.SiteSettings.max_topic_title_length); + return (this.get('titleLength') <= this.siteSettings.max_topic_title_length); }.property('minimumTitleLength', 'titleLength', 'post.static_doc'), // The icon for the save button @@ -194,9 +189,9 @@ const Composer = Discourse.Model.extend({ **/ minimumTitleLength: function() { if (this.get('privateMessage')) { - return Discourse.SiteSettings.min_private_message_title_length; + return this.siteSettings.min_private_message_title_length; } else { - return Discourse.SiteSettings.min_topic_title_length; + return this.siteSettings.min_topic_title_length; } }.property('privateMessage'), @@ -216,12 +211,12 @@ const Composer = Discourse.Model.extend({ **/ minimumPostLength: function() { if( this.get('privateMessage') ) { - return Discourse.SiteSettings.min_private_message_post_length; + return this.siteSettings.min_private_message_post_length; } else if (this.get('topicFirstPost')) { // first post (topic body) - return Discourse.SiteSettings.min_first_post_length; + return this.siteSettings.min_first_post_length; } else { - return Discourse.SiteSettings.min_post_length; + return this.siteSettings.min_post_length; } }.property('privateMessage', 'topicFirstPost'), @@ -249,7 +244,7 @@ const Composer = Discourse.Model.extend({ _setupComposer: function() { const val = (Discourse.Mobile.mobileView ? false : (Discourse.KeyValueStore.get('composer.showPreview') || 'true')); this.set('showPreview', val === 'true'); - this.set('archetypeId', Discourse.Site.currentProp('default_archetype')); + this.set('archetypeId', this.site.get('default_archetype')); }.on('init'), /** @@ -349,15 +344,15 @@ const Composer = Discourse.Model.extend({ this.setProperties({ categoryId: opts.categoryId || this.get('topic.category.id'), - archetypeId: opts.archetypeId || Discourse.Site.currentProp('default_archetype'), + archetypeId: opts.archetypeId || this.site.get('default_archetype'), metaData: opts.metaData ? Em.Object.create(opts.metaData) : null, reply: opts.reply || this.get("reply") || "" }); if (opts.postId) { this.set('loading', true); - Discourse.Post.load(opts.postId).then(function(result) { - composer.set('post', result); + this.store.find('post', opts.postId).then(function(post) { + composer.set('post', post); composer.set('loading', false); }); } @@ -370,10 +365,10 @@ const Composer = Discourse.Model.extend({ this.setProperties(topicProps); - Discourse.Post.load(opts.post.get('id')).then(function(result) { + this.store.find('post', opts.post.get('id')).then(function(post) { composer.setProperties({ - reply: result.get('raw'), - originalText: result.get('raw'), + reply: post.get('raw'), + originalText: post.get('raw'), loading: false }); }); @@ -467,7 +462,7 @@ const Composer = Discourse.Model.extend({ createPost(opts) { const post = this.get('post'), topic = this.get('topic'), - currentUser = Discourse.User.current(), + user = this.user, postStream = this.get('topic.postStream'); let addedToStream = false; @@ -477,17 +472,17 @@ const Composer = Discourse.Model.extend({ imageSizes: opts.imageSizes, cooked: this.getCookedHtml(), reply_count: 0, - name: currentUser.get('name'), - display_username: currentUser.get('name'), - username: currentUser.get('username'), - user_id: currentUser.get('id'), - user_title: currentUser.get('title'), - uploaded_avatar_id: currentUser.get('uploaded_avatar_id'), - user_custom_fields: currentUser.get('custom_fields'), - post_type: Discourse.Site.currentProp('post_types.regular'), + name: user.get('name'), + display_username: user.get('name'), + username: user.get('username'), + user_id: user.get('id'), + user_title: user.get('title'), + uploaded_avatar_id: user.get('uploaded_avatar_id'), + user_custom_fields: user.get('custom_fields'), + post_type: this.site.get('post_types.regular'), actions_summary: [], - moderator: currentUser.get('moderator'), - admin: currentUser.get('admin'), + moderator: user.get('moderator'), + admin: user.get('admin'), yours: true, newPost: true, read: true @@ -520,7 +515,7 @@ const Composer = Discourse.Model.extend({ // we would need to handle oneboxes and other bits that are not even in the // engine, staging will just cause a blank post to render if (!_.isEmpty(createdPost.get('cooked'))) { - state = postStream.stagePost(createdPost, currentUser); + state = postStream.stagePost(createdPost, user); if(state === "alreadyStaging"){ return; @@ -529,69 +524,64 @@ const Composer = Discourse.Model.extend({ } } - const composer = this, - promise = new Ember.RSVP.Promise(function(resolve, reject) { - composer.set('composeState', SAVING); - - createdPost.save(function(result) { - let saving = true; - - createdPost.updateFromJson(result); - - if (topic) { - // It's no longer a new post - createdPost.set('newPost', false); - topic.set('draft_sequence', result.draft_sequence); - postStream.commitPost(createdPost); - addedToStream = true; - } else { - // We created a new topic, let's show it. - composer.set('composeState', CLOSED); - saving = false; - - // Update topic_count for the category - const category = Discourse.Site.currentProp('categories').find(function(x) { return x.get('id') === (parseInt(createdPost.get('category'),10) || 1); }); - if (category) category.incrementProperty('topic_count'); - Discourse.notifyPropertyChange('globalNotice'); - } - - composer.clearState(); - composer.set('createdPost', createdPost); - - if (addedToStream) { - composer.set('composeState', CLOSED); - } else if (saving) { - composer.set('composeState', SAVING); - } - - return resolve({ post: result }); - }, function(error) { - // If an error occurs - if (postStream) { - postStream.undoPost(createdPost); - } - composer.set('composeState', OPEN); - - // TODO extract error handling code - let parsedError; - try { - const parsedJSON = $.parseJSON(error.responseText); - if (parsedJSON.errors) { - parsedError = parsedJSON.errors[0]; - } else if (parsedJSON.failed) { - parsedError = parsedJSON.message; - } - } - catch(ex) { - parsedError = "Unknown error saving post, try again. Error: " + error.status + " " + error.statusText; - } - reject(parsedError); - }); - }); - + const composer = this; + composer.set('composeState', SAVING); composer.set("stagedPost", state === "staged" && createdPost); - return promise; + return createdPost.save().then(function(result) { + let saving = true; + createdPost.updateFromJson(result); + + if (topic) { + // It's no longer a new post + createdPost.set('newPost', false); + topic.set('draft_sequence', result.draft_sequence); + postStream.commitPost(createdPost); + addedToStream = true; + } else { + // We created a new topic, let's show it. + composer.set('composeState', CLOSED); + saving = false; + + // Update topic_count for the category + const category = composer.site.get('categories').find(function(x) { return x.get('id') === (parseInt(createdPost.get('category'),10) || 1); }); + if (category) category.incrementProperty('topic_count'); + Discourse.notifyPropertyChange('globalNotice'); + } + + composer.clearState(); + composer.set('createdPost', createdPost); + + if (addedToStream) { + composer.set('composeState', CLOSED); + } else if (saving) { + composer.set('composeState', SAVING); + } + + return { post: result }; + }).catch(function(error) { + + // If an error occurs + if (postStream) { + postStream.undoPost(createdPost); + } + composer.set('composeState', OPEN); + + // TODO extract error handling code + let parsedError; + try { + const parsedJSON = $.parseJSON(error.responseText); + if (parsedJSON.errors) { + parsedError = parsedJSON.errors[0]; + } else if (parsedJSON.failed) { + parsedError = parsedJSON.message; + } + } + catch(ex) { + parsedError = "Unknown error saving post, try again. Error: " + error.status + " " + error.statusText; + } + throw parsedError; + }); }, getCookedHtml() { @@ -604,7 +594,7 @@ const Composer = Discourse.Model.extend({ // Do not save when there is no reply if (!this.get('reply')) return; // Do not save when the reply's length is too small - if (this.get('replyLength') < Discourse.SiteSettings.min_post_length) return; + if (this.get('replyLength') < this.siteSettings.min_post_length) return; const data = { reply: this.get('reply'), @@ -673,6 +663,14 @@ Composer.reopenClass({ } }, + create(args) { + args = args || {}; + args.user = args.user || Discourse.User.current(); + args.site = args.site || Discourse.Site.current(); + args.siteSettings = args.siteSettings || Discourse.SiteSettings; + return this._super(args); + }, + serializeToTopic(fieldName, property) { if (!property) { property = fieldName; } _edit_topic_serializer[fieldName] = property; diff --git a/app/assets/javascripts/discourse/models/_post.js b/app/assets/javascripts/discourse/models/post.js.es6 similarity index 78% rename from app/assets/javascripts/discourse/models/_post.js rename to app/assets/javascripts/discourse/models/post.js.es6 index 7ae7058cb..1d5474433 100644 --- a/app/assets/javascripts/discourse/models/_post.js +++ b/app/assets/javascripts/discourse/models/post.js.es6 @@ -1,20 +1,12 @@ -/** - A data model representing a post in a topic +const Post = Discourse.Model.extend({ - @class Post - @extends Discourse.Model - @namespace Discourse - @module Discourse -**/ -Discourse.Post = Discourse.Model.extend({ - - init: function() { + init() { this.set('replyHistory', []); }, shareUrl: function() { - var user = Discourse.User.current(); - var userSuffix = user ? '?u=' + user.get('username_lower') : ''; + const user = Discourse.User.current(); + const userSuffix = user ? '?u=' + user.get('username_lower') : ''; if (this.get('firstPost')) { return this.get('topic.url') + userSuffix; @@ -33,7 +25,7 @@ Discourse.Post = Discourse.Model.extend({ userDeleted: Em.computed.empty('user_id'), showName: function() { - var name = this.get('name'); + const name = this.get('name'); return name && (name !== this.get('username')) && Discourse.SiteSettings.display_name_on_posts; }.property('name', 'username'), @@ -69,17 +61,17 @@ Discourse.Post = Discourse.Model.extend({ }.property("user_id"), wikiChanged: function() { - var data = { wiki: this.get("wiki") }; + const data = { wiki: this.get("wiki") }; this._updatePost("wiki", data); }.observes('wiki'), postTypeChanged: function () { - var data = { post_type: this.get("post_type") }; + const data = { post_type: this.get("post_type") }; this._updatePost("post_type", data); }.observes("post_type"), - _updatePost: function (field, data) { - var self = this; + _updatePost(field, data) { + const self = this; Discourse.ajax("/posts/" + this.get("id") + "/" + field, { type: "PUT", data: data @@ -103,7 +95,7 @@ Discourse.Post = Discourse.Model.extend({ editCount: function() { return this.get('version') - 1; }.property('version'), flagsAvailable: function() { - var post = this; + const post = this; return Discourse.Site.currentProp('flagTypes').filter(function(item) { return post.get("actionByName." + item.get('name_key') + ".can_act"); }); @@ -119,9 +111,8 @@ Discourse.Post = Discourse.Model.extend({ }); }.property('actions_summary.@each.users', 'actions_summary.@each.count'), - // Save a post and call the callback when done. - save: function(complete, error) { - var self = this; + save() { + const self = this; if (!this.get('newPost')) { // We're updating a post return Discourse.ajax("/posts/" + (this.get('id')), { @@ -135,19 +126,17 @@ Discourse.Post = Discourse.Model.extend({ // If we received a category update, update it self.set('version', result.post.version); if (result.category) Discourse.Site.current().updateCategory(result.category); - if (complete) complete(Discourse.Post.create(result.post)); - }).catch(function(result) { - // Post failed to update - if (error) error(result); + return Discourse.Post.create(result.post); }); } else { // We're saving a post - var data = this.getProperties(Discourse.Composer.serializedFieldsForCreate()); + const data = this.getProperties(Discourse.Composer.serializedFieldsForCreate()); data.reply_to_post_number = this.get('reply_to_post_number'); data.image_sizes = this.get('imageSizes'); + data.nested_post = true; - var metaData = this.get('metaData'); + const metaData = this.get('metaData'); // Put the metaData into the request if (metaData) { data.meta_data = {}; @@ -158,34 +147,22 @@ Discourse.Post = Discourse.Model.extend({ type: 'POST', data: data }).then(function(result) { - // Post created - if (complete) complete(Discourse.Post.create(result)); - }).catch(function(result) { - // Failed to create a post - if (error) error(result); + return Discourse.Post.create(result.post); }); } }, - /** - Expands the first post's content, if embedded and shortened. - - @method expandFirstPost - **/ - expand: function() { - var self = this; + // Expands the first post's content, if embedded and shortened. + expand() { + const self = this; return Discourse.ajax("/posts/" + this.get('id') + "/expand-embed").then(function(post) { self.set('cooked', "<section class='expanded-embed'>" + post.cooked + "</section>" ); }); }, - /** - Recover a deleted post - - @method recover - **/ - recover: function() { - var post = this; + // Recover a deleted post + recover() { + const post = this; post.setProperties({ deleted_at: null, deleted_by: null, @@ -207,11 +184,8 @@ Discourse.Post = Discourse.Model.extend({ /** Changes the state of the post to be deleted. Does not call the server, that should be done elsewhere. - - @method setDeletedState - @param {Discourse.User} deletedBy The user deleting the post **/ - setDeletedState: function(deletedBy) { + setDeletedState(deletedBy) { this.set('oldCooked', this.get('cooked')); // Moderators can delete posts. Users can only trigger a deleted at message, unless delete_removed_posts_after is 0. @@ -237,10 +211,8 @@ Discourse.Post = Discourse.Model.extend({ Changes the state of the post to NOT be deleted. Does not call the server. This can only be called after setDeletedState was called, but the delete failed on the server. - - @method undoDeletedState **/ - undoDeleteState: function() { + undoDeleteState() { if (this.get('oldCooked')) { this.setProperties({ deleted_at: null, @@ -253,13 +225,7 @@ Discourse.Post = Discourse.Model.extend({ } }, - /** - Deletes a post - - @method destroy - @param {Discourse.User} deletedBy The user deleting the post - **/ - destroy: function(deletedBy) { + destroy(deletedBy) { this.setDeletedState(deletedBy); return Discourse.ajax("/posts/" + this.get('id'), { data: { context: window.location.pathname }, @@ -270,14 +236,11 @@ Discourse.Post = Discourse.Model.extend({ /** Updates a post from another's attributes. This will normally happen when a post is loading but is already found in an identity map. - - @method updateFromPost - @param {Discourse.Post} otherPost The post we're updating from **/ - updateFromPost: function(otherPost) { - var self = this; + updateFromPost(otherPost) { + const self = this; Object.keys(otherPost).forEach(function (key) { - var value = otherPost[key], + let value = otherPost[key], oldValue = self[key]; if (key === "replyHistory") { @@ -287,7 +250,7 @@ Discourse.Post = Discourse.Model.extend({ if (!value) { value = null; } if (!oldValue) { oldValue = null; } - var skip = false; + let skip = false; if (typeof value !== "function" && oldValue !== value) { // wishing for an identity map if (key === "reply_to_user" && value && oldValue) { @@ -304,17 +267,14 @@ Discourse.Post = Discourse.Model.extend({ /** Updates a post from a JSON packet. This is normally done after the post is saved to refresh any attributes. - - @method updateFromJson - @param {Object} obj The Json data to update with **/ - updateFromJson: function(obj) { + updateFromJson(obj) { if (!obj) return; - var skip, oldVal; + let skip, oldVal; // Update all the properties - var post = this; + const post = this; _.each(obj, function(val,key) { if (key !== 'actions_summary'){ oldVal = post[key]; @@ -336,12 +296,11 @@ Discourse.Post = Discourse.Model.extend({ // Rebuild actions summary this.set('actions_summary', Em.A()); if (obj.actions_summary) { - var lookup = Em.Object.create(); + const lookup = Em.Object.create(); _.each(obj.actions_summary,function(a) { - var actionSummary; a.post = post; a.actionType = Discourse.Site.current().postActionTypeById(a.id); - actionSummary = Discourse.ActionSummary.create(a); + const actionSummary = Discourse.ActionSummary.create(a); post.get('actions_summary').pushObject(actionSummary); lookup.set(a.actionType.get('name_key'), actionSummary); }); @@ -350,7 +309,7 @@ Discourse.Post = Discourse.Model.extend({ }, // Load replies to this post - loadReplies: function() { + loadReplies() { if(this.get('loadingReplies')){ return; } @@ -358,12 +317,12 @@ Discourse.Post = Discourse.Model.extend({ this.set('loadingReplies', true); this.set('replies', []); - var self = this; + const self = this; return Discourse.ajax("/posts/" + (this.get('id')) + "/replies") .then(function(loaded) { - var replies = self.get('replies'); + const replies = self.get('replies'); _.each(loaded,function(reply) { - var post = Discourse.Post.create(reply); + const post = Discourse.Post.create(reply); post.set('topic', self.get('topic')); replies.pushObject(post); }); @@ -375,7 +334,7 @@ Discourse.Post = Discourse.Model.extend({ // Whether to show replies directly below showRepliesBelow: function() { - var replyCount = this.get('reply_count'); + const replyCount = this.get('reply_count'); // We don't show replies if there aren't any if (replyCount === 0) return false; @@ -387,13 +346,13 @@ Discourse.Post = Discourse.Model.extend({ if (replyCount > 1) return true; // If we have *exactly* one reply, we have to consider if it's directly below us - var topic = this.get('topic'); + const topic = this.get('topic'); return !topic.isReplyDirectlyBelow(this); }.property('reply_count'), - expandHidden: function() { - var self = this; + expandHidden() { + const self = this; return Discourse.ajax("/posts/" + this.get('id') + "/cooked.json").then(function (result) { self.setProperties({ cooked: result.cooked, @@ -402,17 +361,17 @@ Discourse.Post = Discourse.Model.extend({ }); }, - rebake: function () { + rebake() { return Discourse.ajax("/posts/" + this.get("id") + "/rebake", { type: "PUT" }); }, - unhide: function () { + unhide() { return Discourse.ajax("/posts/" + this.get("id") + "/unhide", { type: "PUT" }); }, - toggleBookmark: function() { - var self = this, - bookmarkedTopic; + toggleBookmark() { + const self = this; + let bookmarkedTopic; this.toggleProperty("bookmarked"); @@ -435,16 +394,16 @@ Discourse.Post = Discourse.Model.extend({ } }); -Discourse.Post.reopenClass({ +Post.reopenClass({ - createActionSummary: function(result) { + createActionSummary(result) { if (result.actions_summary) { - var lookup = Em.Object.create(); + const lookup = Em.Object.create(); // this area should be optimized, it is creating way too many objects per post result.actions_summary = result.actions_summary.map(function(a) { a.post = result; a.actionType = Discourse.Site.current().postActionTypeById(a.id); - var actionSummary = Discourse.ActionSummary.create(a); + const actionSummary = Discourse.ActionSummary.create(a); lookup[a.actionType.name_key] = actionSummary; return actionSummary; }); @@ -452,8 +411,8 @@ Discourse.Post.reopenClass({ } }, - create: function(obj) { - var result = this._super.apply(this, arguments); + create(obj) { + const result = this._super.apply(this, arguments); this.createActionSummary(result); if (obj && obj.reply_to_user) { result.set('reply_to_user', Discourse.User.create(obj.reply_to_user)); @@ -461,14 +420,14 @@ Discourse.Post.reopenClass({ return result; }, - updateBookmark: function(postId, bookmarked) { + updateBookmark(postId, bookmarked) { return Discourse.ajax("/posts/" + postId + "/bookmark", { type: 'PUT', data: { bookmarked: bookmarked } }); }, - deleteMany: function(selectedPosts, selectedReplies) { + deleteMany(selectedPosts, selectedReplies) { return Discourse.ajax("/posts/destroy_many", { type: 'DELETE', data: { @@ -478,37 +437,33 @@ Discourse.Post.reopenClass({ }); }, - loadRevision: function(postId, version) { + loadRevision(postId, version) { return Discourse.ajax("/posts/" + postId + "/revisions/" + version + ".json").then(function (result) { return Ember.Object.create(result); }); }, - hideRevision: function(postId, version) { + hideRevision(postId, version) { return Discourse.ajax("/posts/" + postId + "/revisions/" + version + "/hide", { type: 'PUT' }); }, - showRevision: function(postId, version) { + showRevision(postId, version) { return Discourse.ajax("/posts/" + postId + "/revisions/" + version + "/show", { type: 'PUT' }); }, - loadQuote: function(postId) { + loadQuote(postId) { return Discourse.ajax("/posts/" + postId + ".json").then(function (result) { - var post = Discourse.Post.create(result); + const post = Discourse.Post.create(result); return Discourse.Quote.build(post, post.get('raw')); }); }, - loadRawEmail: function(postId) { + loadRawEmail(postId) { return Discourse.ajax("/posts/" + postId + "/raw-email").then(function (result) { return result.raw_email; }); - }, - - load: function(postId) { - return Discourse.ajax("/posts/" + postId + ".json").then(function (result) { - return Discourse.Post.create(result); - }); } }); + +export default Post; diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index 9816d42f5..9a6e97574 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -25,6 +25,7 @@ //= require ./discourse/lib/safari-hacks //= require_tree ./discourse/adapters //= require ./discourse/models/model +//= require ./discourse/models/post //= require ./discourse/models/user_action //= require ./discourse/models/composer //= require ./discourse/models/post-stream diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index e632b8f27..57b829e53 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -336,7 +336,11 @@ class PostsController < ApplicationController # doesn't return the post as the root JSON object, but as a nested object. # If a param is present it uses that result structure. def backwards_compatible_json(json_obj, success) - json_obj = json_obj[:post] || json_obj['post'] unless params[:nested_post] + json_obj.symbolize_keys! + if params[:nested_post].blank? && json_obj[:errors].blank? + json_obj = json_obj[:post] + end + render json: json_obj, status: (!!success) ? 200 : 422 end diff --git a/test/javascripts/acceptance/composer-test.js.es6 b/test/javascripts/acceptance/composer-test.js.es6 new file mode 100644 index 000000000..14788f02b --- /dev/null +++ b/test/javascripts/acceptance/composer-test.js.es6 @@ -0,0 +1,117 @@ +import { acceptance } from "helpers/qunit-helpers"; + +acceptance("Composer", { loggedIn: true }); + +test("Tests the Composer controls", () => { + visit("/"); + andThen(() => { + ok(exists('#create-topic'), 'the create button is visible'); + }); + + click('#create-topic'); + andThen(() => { + ok(exists('#wmd-input'), 'the composer input is visible'); + ok(exists('.title-input .popup-tip.bad.hide'), 'title errors are hidden by default'); + ok(exists('.textarea-wrapper .popup-tip.bad.hide'), 'body errors are hidden by default'); + }); + + click('a.toggle-preview'); + andThen(() => { + ok(!exists('#wmd-preview:visible'), "clicking the toggle hides the preview"); + }); + + click('a.toggle-preview'); + andThen(() => { + ok(exists('#wmd-preview:visible'), "clicking the toggle shows the preview again"); + }); + + click('#reply-control button.create'); + andThen(() => { + ok(!exists('.title-input .popup-tip.bad.hide'), 'it shows the empty title error'); + ok(!exists('.textarea-wrapper .popup-tip.bad.hide'), 'it shows the empty body error'); + }); + + fillIn('#reply-title', "this is my new topic title"); + andThen(() => { + ok(exists('.title-input .popup-tip.good'), 'the title is now good'); + }); + + fillIn('#wmd-input', "this is the *content* of a post"); + andThen(() => { + equal(find('#wmd-preview').html(), "<p>this is the <em>content</em> of a post</p>", "it previews content"); + ok(exists('.textarea-wrapper .popup-tip.good'), 'the body is now good'); + }); + + click('#reply-control a.cancel'); + andThen(() => { + ok(exists('.bootbox.modal'), 'it pops up a confirmation dialog'); + }); + + click('.modal-footer a:eq(1)'); + andThen(() => { + ok(!exists('.bootbox.modal'), 'the confirmation can be cancelled'); + }); + +}); + +test("Create a topic with server side errors", () => { + visit("/"); + click('#create-topic'); + fillIn('#reply-title', "this title triggers an error"); + fillIn('#wmd-input', "this is the *content* of a post"); + click('#reply-control button.create'); + andThen(() => { + ok(exists('.bootbox.modal'), 'it pops up an error message'); + }); + click('.bootbox.modal a.btn-primary'); + andThen(() => { + ok(!exists('.bootbox.modal'), 'it dismisses the error'); + ok(exists('#wmd-input'), 'the composer input is visible'); + }); +}); + +test("Create a Topic", () => { + visit("/"); + click('#create-topic'); + fillIn('#reply-title', "Internationalization Localization"); + fillIn('#wmd-input', "this is the *content* of a new topic post"); + click('#reply-control button.create'); + andThen(() => { + equal(currentURL(), "/t/internationalization-localization/280", "it transitions to the newly created topic URL"); + }); +}); + +test("Create a Reply", () => { + visit("/t/internationalization-localization/280"); + + click('#topic-footer-buttons .btn.create'); + andThen(() => { + ok(exists('#wmd-input'), 'the composer input is visible'); + ok(!exists('#reply-title'), 'there is no title since this is a reply'); + }); + + fillIn('#wmd-input', 'this is the content of my reply'); + click('#reply-control button.create'); + andThen(() => { + exists('#post_12345', 'it inserts the post into the document'); + }); +}); + +test("Edit the first post", () => { + visit("/t/internationalization-localization/280"); + + click('.topic-post:eq(0) button[data-action=showMoreActions]'); + click('.topic-post:eq(0) button[data-action=edit]'); + andThen(() => { + equal(find('#wmd-input').val().indexOf('Any plans to support'), 0, 'it populates the input with the post text'); + }); + + fillIn('#wmd-input', "This is the new text for the post"); + fillIn('#reply-title', "This is the new text for the title"); + click('#reply-control button.create'); + andThen(() => { + ok(!exists('#wmd-input'), 'it closes the composer'); + ok(find('#topic-title h1').text().indexOf('This is the new text for the title') !== -1, 'it shows the new title'); + ok(find('.topic-post:eq(0) .cooked').text().indexOf('This is the new text for the post') !== -1, 'it updates the post'); + }); +}); diff --git a/test/javascripts/acceptance/header-test-staff.js.es6 b/test/javascripts/acceptance/header-test-staff.js.es6 index 2f4db1aa2..fb3278ee8 100644 --- a/test/javascripts/acceptance/header-test-staff.js.es6 +++ b/test/javascripts/acceptance/header-test-staff.js.es6 @@ -1,10 +1,6 @@ import { acceptance } from "helpers/qunit-helpers"; -acceptance("Header (Staff)", { - user: { username: 'test', - staff: true, - site_flagged_posts_count: 1 } -}); +acceptance("Header (Staff)", { loggedIn: true }); test("header", () => { visit("/"); diff --git a/test/javascripts/fixtures/post.js.es6 b/test/javascripts/fixtures/post.js.es6 new file mode 100644 index 000000000..c2d22a62c --- /dev/null +++ b/test/javascripts/fixtures/post.js.es6 @@ -0,0 +1,4 @@ +export default { + "/posts/398": {"id":398,"name":"Uwe Keim","username":"uwe_keim","avatar_template":"/user_avatar/meta.discourse.org/uwe_keim/{size}/5697.png","uploaded_avatar_id":5697,"created_at":"2013-02-05T21:29:00.280Z","cooked":"<p>Any plans to support localization of UI elements, so that I (for example) could set up a completely German speaking forum?</p>","post_number":1,"post_type":1,"updated_at":"2013-02-05T21:29:00.280Z","like_count":0,"reply_count":1,"reply_to_post_number":null,"quote_count":0,"avg_time":25,"incoming_link_count":314,"reads":475,"score":1702.25,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Uwe Keim","primary_group_name":null,"version":2,"can_edit":true,"can_delete":false,"can_recover":true,"user_title":null,"raw":"Any plans to support localization of UI elements, so that I (for example) could set up a completely German speaking forum?","actions_summary":[{"id":2,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":255,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false} +}; + diff --git a/test/javascripts/fixtures/session-fixtures.js.es6 b/test/javascripts/fixtures/session-fixtures.js.es6 new file mode 100644 index 000000000..fa940ccec --- /dev/null +++ b/test/javascripts/fixtures/session-fixtures.js.es6 @@ -0,0 +1,4 @@ +export default { + "/session/current.json": {"current_user":{"id":19,"username":"eviltrout","uploaded_avatar_id":5275,"avatar_template":"/user_avatar/localhost/eviltrout/{size}/5275.png","name":"Robin Ward","total_unread_notifications":205,"unread_notifications":0,"unread_private_messages":0,"admin":true,"notification_channel_position":null,"site_flagged_posts_count":1,"moderator":true,"staff":true,"title":"co-founder","reply_count":859,"topic_count":36,"enable_quoting":true,"external_links_in_new_tab":false,"dynamic_favicon":true,"trust_level":4,"can_edit":true,"can_invite_to_forum":true,"should_be_redirected_to_top":false,"disable_jump_reply":false,"custom_fields":{},"muted_category_ids":[],"dismissed_banner_key":null,"akismet_review_count":0}} +}; + diff --git a/test/javascripts/fixtures/topic.js.es6 b/test/javascripts/fixtures/topic.js.es6 index b072266f4..7d5be12cb 100644 --- a/test/javascripts/fixtures/topic.js.es6 +++ b/test/javascripts/fixtures/topic.js.es6 @@ -1,2 +1,2 @@ /*jshint maxlen:10000000 */ -export default {"/t/280.json": {"post_stream":{"posts":[{"id":398,"name":"Uwe Keim","username":"uwe_keim","avatar_template":"//www.gravatar.com/avatar/53a82f701ae492808834e621de2586eb.png?s={size}&r=pg&d=identicon","created_at":"2013-02-05T16:29:00.000-05:00","cooked":"<p>Any plans to support localization of UI elements, so that I (for example) could set up a completely German speaking forum?</p>","post_number":1,"post_type":1,"updated_at":"2013-02-05T16:29:00.000-05:00","reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":23,"incoming_link_count":9,"reads":390,"score":158.15,"yours":false,"topic_slug":"internationalization-localization","topic_id":280,"display_username":"Uwe Keim","version":1,"can_edit":false,"can_delete":false,"can_recover":false,"link_counts":[{"url":"http://meta.discourse.org/t/comrades-lets-join-our-efforts-on-ukrainian-and-russian-translations/4403/5","internal":true,"reflection":true,"title":"Comrades let's join our efforts on ukrainian and russian translations","clicks":1},{"url":"http://meta.discourse.org/t/bookmark-last-read-sometimes-doesn-t-go-to-the-end-of-a-topic/4825/9","internal":true,"reflection":true,"title":"Bookmark/last read sometimes doesn't go to the end of a topic","clicks":0},{"url":"http://meta.discourse.org/t/suggestion-translation-on-admin-panel/6923/5","internal":true,"reflection":true,"title":"Suggestion: Translation on admin panel","clicks":0},{"url":"http://meta.discourse.org/t/translation-workflow/6102","internal":true,"reflection":true,"title":"Translation workflow","clicks":0},{"url":"http://meta.discourse.org/t/missing-user-value-in-chinese-localized-page/7406/6","internal":true,"reflection":true,"title":"[missing {{user}} value] in Chinese localized page","clicks":0},{"url":"http://meta.discourse.org/t/changing-language-phrase-does-not-affect-on-the-site/8429/3","internal":true,"reflection":true,"title":"Changing language phrase does not affect on the site?","clicks":0},{"url":"http://meta.discourse.org/t/internationalization-i18n-provided-for-discourse/2073/2","internal":true,"reflection":true,"title":"Internationalization I18n provided for discourse ?","clicks":0},{"url":"http://meta.discourse.org/t/language-mirrors/2378/2","internal":true,"reflection":true,"title":"Language mirrors","clicks":0},{"url":"http://meta.discourse.org/t/roadplan-for-discourse/2939/5","internal":true,"reflection":true,"title":"Roadplan for Discourse","clicks":0},{"url":"http://meta.discourse.org/t/solving-xda-developer-style-forums/4368/4","internal":true,"reflection":true,"title":"Solving XDA-Developer style forums","clicks":0}],"read":false,"user_title":null,"actions_summary":[{"id":2,"count":0,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":false,"staff":false,"user_id":255,"hidden":false,"hidden_reason_id":null,"trust_level":1,"deleted_at":null,"user_deleted":false,"edit_reason":null},{"id":419,"name":"Tim Stone","username":"tms","avatar_template":"//www.gravatar.com/avatar/3981cd271c302f5cba628c6b6d2b32ee.png?s={size}&r=pg&d=identicon","created_at":"2013-02-05T16:32:47.000-05:00","cooked":"<p>The application strings <a href=\"https://github.com/discourse/discourse/blob/master/config/locales/en.yml\" rel=\"nofollow\">are externalized</a>, so localization should be entirely possible with enough translation effort.</p>","post_number":2,"post_type":1,"updated_at":"2013-02-06T05:15:27.000-05:00","reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":26,"incoming_link_count":12,"reads":376,"score":256.5,"yours":false,"topic_slug":"internationalization-localization","topic_id":280,"display_username":"Tim Stone","version":2,"can_edit":false,"can_delete":false,"can_recover":false,"link_counts":[{"url":"https://github.com/discourse/discourse/blob/master/config/locales/en.yml","internal":false,"reflection":false,"clicks":91}],"read":false,"user_title":null,"actions_summary":[{"id":2,"count":3,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":false,"staff":false,"user_id":9,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null},{"id":1060,"name":"Jeff Atwood","username":"codinghorror","avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","created_at":"2013-02-05T21:26:24.000-05:00","cooked":"<p>Yep, all strings are going through a lookup table.*</p>\n\n<div class=\"onebox-result\">\n <div class=\"source\">\n <div class=\"info\">\n <a href=\"https://github.com/discourse/discourse/blob/master/config/locales\" class=\"source track-link\" target=\"_blank\">\n <img class=\"favicon\" src=\"//meta-discourse.r.worldssl.net/assets/favicons/github-4d1f6c8515ae3dadc2c12d0b128f40c1.png\"> github.com\n </a>\n </div>\n </div>\n <div class=\"onebox-result-body\"> \n <h4><a href=\"https://github.com/discourse/discourse/blob/master/config/locales\" target=\"_blank\">https://github.com/discourse/discourse/blob/master/config/locales</a></h4>\n <pre><code class=\"config/locales\">\n\n\n\n\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" id=\"vp\" content=\"initial-scale=1.0,user-scalable=no,maximum-scale=1,width=device-width\">\n <meta name=\"viewport\" id=\"vp\" content=\"initial-scale=1.0,user-scalable=no,maximum-scale=1\" media=\"(device-height: 568px)\">\n <link rel=\"apple-touch-icon-precomposed\" href=\"apple-touch-icon-precomposed.png\">\n <link rel=\"apple-touch-icon-precomposed\" sizes=\"114x114\" href=\"apple-touch-icon-114-precomposed.png\">\n <meta name=\"google-analytics\" content=\"UA-3769691-2\">\n\n <meta content=\"authenticity_token\" name=\"csrf-param\" />\n<meta content=\"nFiTSR4YODX1WTtDmWoO2Xbd9vatOibhb97Rm3nw+8I=\" name=\"csrf-token\" />\n\n <meta content=\"collector.githubapp.com\" name=\"octolytics-host\" /><meta content=\"collector-cdn.github.com\" name=\"octolytics-script-host\" /><meta content=\"github\" name=\"octolytics-app-id\" /><meta content=\"40479403:2C5C:A159F60:52CDD76D\" name=\"octolytics-dimension-request_id\" />\n <meta content=\"mobile\" name=\"octolytics-dimension-device\" />\n <meta content=\"3220138\" name=\"octolytics-dimension-user_id\" /><meta content=\"discourse\" name=\"octolytics-dimension-user_login\" /><meta content=\"7569578\" name=\"octolytics-dimension-repository_id\" /><meta content=\"discourse/discourse\" name=\"octolytics-dimension-repository_nwo\" /><meta content=\"true\" name=\"octolytics-dimension-repository_public\" /><meta content=\"false\" name=\"octolytics-dimension-repository_is_fork\" /><meta content=\"7569578\" name=\"octolytics-dimension-repository_network_root_id\" /><meta content=\"discourse/discourse\" name=\"octolytics-dimension-repository_network_root_nwo\" />\n \n\n <title>discourse/discourse · GitHub</title>\n\n <link href=\"https://github.global.ssl.fastly.net/assets/mobile-9164f493ea1c72c93c839eba1f16fb0a36e0f638.css\" media=\"all\" rel=\"stylesheet\" type=\"text/css\" />\n <script async=\"async\" defer=\"defer\" src=\"https://github.global.ssl.fastly.net/assets/mobile-756d65616fde6492018be51d0739ed8ccfcc25b6.js\" type=\"text/javascript\"></script>\n</head>\n<body class=\"\">\n <header class=\"nav-bar clearfix\">\n <div class=\"nav-bar-inner\">\n \n\n <button class=\"header-button header-nav-button touchable js-show-global-nav \">\n <span class=\"octicon octicon-three-bars\"></span>\n <span class=\"octicon octicon-primitive-dot\"></span>\n </button>\n\n <div class=\"nav-bar-title-text\">\n <span class=\"octicon octicon-repo\"></span>\n\n <strong><a href=\"/discourse\">discourse</a></strong>\n /\n <strong><a href=\"/discourse/discourse\">discourse</a></strong>\n\n </div>\n </div>\n\n <nav class=\"nav-bar-tabs repo-nav-bar-tabs \">\n <ul>\n\n\n <li>\n <a href=\"/explore\"><span class=\"octicon octicon-telescope\"></span> Explore</a>\n </li>\n\n <li>\n <a href=\"/login\"><span class=\"octicon octicon-log-in\"></span> Sign in</a>\n </li>\n\n <li class=\"section-title\">This repository</li>\n <li><a href=\"/discourse/discourse?files=1\"><span class=\"octicon octicon-code\"></span> Code</a></li>\n <li><a href=\"/discourse/discourse/issues\"><span class=\"octicon octicon-issue-opened\"></span> Issues</a></li>\n <li><a href=\"/discourse/discourse/pulls\"><span class=\"octicon octicon-git-pull-request\"></span> Pull Requests</a></li>\n <li><a href=\"/discourse/discourse/pulse\"><span class=\"octicon octicon-pulse\"></span> Pulse</a></li>\n\n </ul>\n </nav>\n </header>\n\n \n\n \n\n\n<div class=\"file-browser\">\n\n <p class=\"history-link\">\n <a href=\"/discourse/discourse/commits/master/config/locales\">\n <span class=\"octicon octicon-history\"></span>\n View 5857 commits\n </a>\n </p>\n\n <div class=\"bubble\">\n <ul class=\"bubble-list files-list\">\n <li class=\"path\">\n <span class=\"branch\">master</span>\n <span class='bold'>\n <span itemtype=\"http://data-vocabulary.org/Breadcrumb\">\n <a href=\"/discourse/discourse?files=1\" data-branch=\"master\" data-direction=\"back\" itemscope=\"url\">\n <span itemprop=\"title\">discourse</span>\n </a>\n </span>\n </span>\n <span class=\"separator\"> / </span>\n\n <span itemscope=\"\" itemtype=\"http://data-vocabulary.org/Breadcrumb\"><a href=\"/discourse/discourse/tree/master/config\" data-branch=\"master\" data-direction=\"back\" data-pjax=\"true\" itemscope=\"url\"><span itemprop=\"title\">config</span></a></span><span class=\"separator\"> / </span><strong class=\"final-path\">locales</strong>\n </li>\n\n <li>\n <a href=\"/discourse/discourse/blob/master/config/locales/client.cs.yml\" class=\"file-list-item\">\n <span class=\"octicon octicon-file-text\"></span>\n client.cs.yml\n <span class=\"timestamp\">28 days ago</span>\n </a>\n </li>\n <li>\n <a href=\"/discourse/discourse/blob/master/config/locales/client.da.yml\" class=\"file-list-item\">\n <span class=\"octicon octicon-file-text\"></span>\n client.da.yml\n </a>\n </li>\n <li></code></pre>\n\n This file has been truncated. <a href=\"https://github.com/discourse/discourse/blob/master/config/locales\" target=\"_blank\">show original</a>\n </div>\n</div>\n\n<p>So you could replace that lookup table with the \"de\" one to get German.</p>\n\n<p><sub>* we didn't get all the strings into the lookup table for launch, but the vast, vast majority of them are and the ones that are not, we will fix as we go!</sub></p>","post_number":3,"post_type":1,"updated_at":"2013-06-18T22:58:28.000-04:00","reply_count":3,"reply_to_post_number":null,"quote_count":0,"avg_time":33,"incoming_link_count":4,"reads":367,"score":155.05,"yours":false,"topic_slug":"internationalization-localization","topic_id":280,"display_username":"Jeff Atwood","version":3,"can_edit":false,"can_delete":false,"can_recover":false,"link_counts":[{"url":"https://github.com/discourse/discourse/blob/master/config/locales","internal":false,"reflection":false,"clicks":27},{"url":"http://meta.discourse.org/t/github-onebox-rendering-issue/7616","internal":true,"reflection":true,"title":"GitHub OneBox Rendering Issue","clicks":0}],"read":false,"user_title":"co-founder","actions_summary":[{"id":2,"count":3,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":true,"staff":true,"user_id":32,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null},{"id":3623,"name":"Shade","username":"shade","avatar_template":"//www.gravatar.com/avatar/02c3f1806f6962f56168c7bd9f8924b8.png?s={size}&r=pg&d=identicon","created_at":"2013-02-07T07:55:33.000-05:00","cooked":"<p>Is it a coincidence that the strings file is 1337 lines long? <img src=\"/plugins/emoji/images/smiley.png\" title=\":smiley:\" class=\"emoji\" alt=\"smiley\" width=\"64\" height=\"64\"></p>","post_number":4,"post_type":1,"updated_at":"2013-02-07T07:55:33.000-05:00","reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":21,"incoming_link_count":11,"reads":324,"score":255.85,"yours":false,"topic_slug":"internationalization-localization","topic_id":280,"display_username":"Shade","version":1,"can_edit":false,"can_delete":false,"can_recover":false,"link_counts":[{"url":"http://meta.discourse.org/t/hi-support-chinese/4393/6","internal":true,"reflection":true,"title":"Hi, support Chinese?","clicks":0}],"read":false,"user_title":null,"actions_summary":[{"id":2,"count":7,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":false,"staff":false,"user_id":1808,"hidden":false,"hidden_reason_id":null,"trust_level":1,"deleted_at":null,"user_deleted":false,"edit_reason":null},{"id":3651,"name":"Pekka Gaiser","username":"pekka","avatar_template":"//www.gravatar.com/avatar/100a6c42a31a56e882475725d65537f8.png?s={size}&r=pg&d=identicon","created_at":"2013-02-07T09:02:07.000-05:00","cooked":"<p><aside class=\"quote\" data-post=\"3\" data-topic=\"280\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img width=\"20\" height=\"20\" src=\"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s=40&r=pg&d=identicon\" class=\"avatar\">codinghorror:</div>\n<blockquote><p>So you could replace that lookup table with the \"de\" one to get German.</p></blockquote></aside></p>\n\n<p>The problem I see here is that this file is likely two grow and change massively over the next couple months, and tracking these changes in order to keep a localized file up to date is going to be a <em>bitch</em>. </p>\n\n<p>I wonder where there is a tool that can compare two yml structures and point out which nodes are missing? That would help keep track of new strings.</p>\n\n<p>Re keeping track of <em>changed</em> strings, <a class=\"mention\" href=\"/users/codinghorror\">@codinghorror</a> I found this very interesting: <a href=\"http://stackoverflow.com/questions/4232922/why-do-people-use-plain-english-as-translation-placeholders\" rel=\"nofollow\">http://stackoverflow.com/questions/4232922/why-do-people-use-plain-english-as-translation-placeholders</a> if plain English placeholders were used, any change in strings would lead to a <em>new</em> node in the yml file, making keeping the translation up to date easier. Maybe worth thinking about in the future.</p>","post_number":5,"post_type":1,"updated_at":"2013-02-07T09:05:42.000-05:00","reply_count":2,"reply_to_post_number":3,"quote_count":1,"avg_time":23,"incoming_link_count":6,"reads":317,"score":179.55,"yours":false,"topic_slug":"internationalization-localization","topic_id":280,"display_username":"Pekka Gaiser","version":1,"can_edit":false,"can_delete":false,"can_recover":false,"link_counts":[{"url":"http://stackoverflow.com/questions/4232922/why-do-people-use-plain-english-as-translation-placeholders","internal":false,"reflection":false,"clicks":56}],"read":false,"user_title":null,"actions_summary":[{"id":2,"count":2,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null},{"id":3654,"name":"Sam Saffron","username":"sam","avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","created_at":"2013-02-07T09:05:39.000-05:00","cooked":"<p>Yes, I really like the concept of fuzzy matching for localization, perhaps you can chase up <a href=\"https://github.com/SlexAxton\" rel=\"nofollow\">alex sexton</a> he was meaning to upload a localization tool for this kind of stuff. </p>\n\n<p>Also, I am a big fan of <a href=\"https://github.com/SlexAxton/messageformat.js\" rel=\"nofollow\">ICU message format</a>, but it is not the \"Rails way (tm)\". </p>","post_number":6,"post_type":1,"updated_at":"2013-02-07T09:05:39.000-05:00","reply_count":1,"reply_to_post_number":5,"quote_count":0,"avg_time":19,"incoming_link_count":2,"reads":276,"score":86.15,"yours":false,"topic_slug":"internationalization-localization","topic_id":280,"display_username":"Sam Saffron","version":1,"can_edit":false,"can_delete":false,"can_recover":false,"link_counts":[{"url":"https://github.com/SlexAxton/messageformat.js","internal":false,"reflection":false,"clicks":38},{"url":"https://github.com/SlexAxton","internal":false,"reflection":false,"clicks":9}],"read":false,"user_title":"co-founder","reply_to_user":{"username":"pekka","avatar_template":"//www.gravatar.com/avatar/100a6c42a31a56e882475725d65537f8.png?s={size}&r=pg&d=identicon"},"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null},{"id":3655,"name":"Pekka Gaiser","username":"pekka","avatar_template":"//www.gravatar.com/avatar/100a6c42a31a56e882475725d65537f8.png?s={size}&r=pg&d=identicon","created_at":"2013-02-07T09:08:17.000-05:00","cooked":"<p>Looks interesting, I'll take a peek.</p>\n\n<p>As said on dev, the best tool I can see in terms of giving translators a proper interface <em>and</em> quality control would be something like <a href=\"http://translate.wordpress.org/projects/bbpress/dev\" rel=\"nofollow\">GlotPress</a>. It's based on the PO messages format (is that somehow related to ICU?) but looks pretty great.</p>\n\n<p><aside class=\"quote\" data-post=\"6\" data-topic=\"280\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img width=\"20\" height=\"20\" src=\"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s=40&r=pg&d=identicon\" class=\"avatar\">sam:</div>\n<blockquote><p>fuzzy matching for localization</p></blockquote></aside></p>\n\n<p>I'm not familiar with the term in this context, you mean keeping the English version in the code base (instead of a generic code like <code>message_error_nametooshort</code> ?)</p>","post_number":7,"post_type":1,"updated_at":"2013-02-07T09:12:02.000-05:00","reply_count":1,"reply_to_post_number":6,"quote_count":1,"avg_time":17,"incoming_link_count":0,"reads":276,"score":76.05,"yours":false,"topic_slug":"internationalization-localization","topic_id":280,"display_username":"Pekka Gaiser","version":1,"can_edit":false,"can_delete":false,"can_recover":false,"link_counts":[{"url":"http://translate.wordpress.org/projects/bbpress/dev","internal":false,"reflection":false,"clicks":14}],"read":false,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null},{"id":3658,"name":"Sam Saffron","username":"sam","avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","created_at":"2013-02-07T09:12:22.000-05:00","cooked":"<p>ICU Message format is basically Gettext on steroids, Gettext has been around for so many years and actually works pretty well, being super prevalent in Linux. </p>\n\n<p>Trouble is you need a fuzzy matcher for translators if you are going to store stuff like <code>mf.compile( 'This is a message.' )</code> in source, one letter change and all your translators need to validate it.</p>","post_number":8,"post_type":1,"updated_at":"2013-02-07T09:12:22.000-05:00","reply_count":1,"reply_to_post_number":7,"quote_count":0,"avg_time":11,"incoming_link_count":0,"reads":251,"score":70.75,"yours":false,"topic_slug":"internationalization-localization","topic_id":280,"display_username":"Sam Saffron","version":1,"can_edit":false,"can_delete":false,"can_recover":false,"link_counts":[{"url":"http://meta.discourse.org/t/what-i-love-about-wordpress-plugins/5697","internal":true,"reflection":true,"title":"What I love about WordPress plugins","clicks":0}],"read":false,"user_title":"co-founder","reply_to_user":{"username":"pekka","avatar_template":"//www.gravatar.com/avatar/100a6c42a31a56e882475725d65537f8.png?s={size}&r=pg&d=identicon"},"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null},{"id":3660,"name":"Pekka Gaiser","username":"pekka","avatar_template":"//www.gravatar.com/avatar/100a6c42a31a56e882475725d65537f8.png?s={size}&r=pg&d=identicon","created_at":"2013-02-07T09:14:12.000-05:00","cooked":"<p><aside class=\"quote\" data-post=\"8\" data-topic=\"280\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img width=\"20\" height=\"20\" src=\"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s=40&r=pg&d=identicon\" class=\"avatar\">sam:</div>\n<blockquote><p>one letter change and all your translators need to validate it.</p></blockquote></aside></p>\n\n<p>Yeah, that's why I've always been a friend of <code>message_error_nametooshort</code> placeholders, until I asked the SO question linked above. The accepted answer makes a good argument against those placeholders: you <em>want</em> translations to break even on small changes in the English original because the translations will probably need to reflect the change, too. Maybe that's not the case right now as new stuff is being checked in pretty much every couple of hours, but in the long run, it'll be overwhelmingly true.</p>","post_number":9,"post_type":1,"updated_at":"2013-02-07T09:18:09.000-05:00","reply_count":1,"reply_to_post_number":8,"quote_count":1,"avg_time":10,"incoming_link_count":0,"reads":249,"score":70.3,"yours":false,"topic_slug":"internationalization-localization","topic_id":280,"display_username":"Pekka Gaiser","version":1,"can_edit":false,"can_delete":false,"can_recover":false,"read":false,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null},{"id":3667,"name":"Tim Stone","username":"tms","avatar_template":"//www.gravatar.com/avatar/3981cd271c302f5cba628c6b6d2b32ee.png?s={size}&r=pg&d=identicon","created_at":"2013-02-07T09:25:16.000-05:00","cooked":"<p>Hmm...You could theoretically also build something into the development process that would monitor changes to the English locale file and make a translator-friendly list of changes between versions.</p>","post_number":10,"post_type":1,"updated_at":"2013-02-07T09:25:16.000-05:00","reply_count":1,"reply_to_post_number":9,"quote_count":0,"avg_time":8,"incoming_link_count":0,"reads":236,"score":67.6,"yours":false,"topic_slug":"internationalization-localization","topic_id":280,"display_username":"Tim Stone","version":1,"can_edit":false,"can_delete":false,"can_recover":false,"read":false,"user_title":null,"reply_to_user":{"username":"pekka","avatar_template":"//www.gravatar.com/avatar/100a6c42a31a56e882475725d65537f8.png?s={size}&r=pg&d=identicon"},"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":false,"staff":false,"user_id":9,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null},{"id":3673,"name":"Sam Saffron","username":"sam","avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","created_at":"2013-02-07T09:30:21.000-05:00","cooked":"<p>Yeah, totally, also we could build tools for dev that make introducing string less annoying and make it possible to garbage collect old unused strings, I hate trudging through that file.</p>","post_number":11,"post_type":1,"updated_at":"2013-02-07T09:30:21.000-05:00","reply_count":1,"reply_to_post_number":10,"quote_count":0,"avg_time":7,"incoming_link_count":0,"reads":233,"score":66.95,"yours":false,"topic_slug":"internationalization-localization","topic_id":280,"display_username":"Sam Saffron","version":1,"can_edit":false,"can_delete":false,"can_recover":false,"read":false,"user_title":"co-founder","reply_to_user":{"username":"tms","avatar_template":"//www.gravatar.com/avatar/3981cd271c302f5cba628c6b6d2b32ee.png?s={size}&r=pg&d=identicon"},"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null},{"id":3675,"name":"Pekka Gaiser","username":"pekka","avatar_template":"//www.gravatar.com/avatar/100a6c42a31a56e882475725d65537f8.png?s={size}&r=pg&d=identicon","created_at":"2013-02-07T09:33:38.000-05:00","cooked":"<p><aside class=\"quote\" data-post=\"11\" data-topic=\"280\" data-full=\"true\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img width=\"20\" height=\"20\" src=\"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s=40&r=pg&d=identicon\" class=\"avatar\">sam:</div>\n<blockquote><p>Yeah, totally, also we could build tools for dev that make introducing string less annoying and make it possible to garbage collect old unused strings, I hate trudging through that file.</p></blockquote></aside></p>\n\n<p>As said, I'd look into whether WP's tools can't be reused for this with some tweaking. They seem to be able to scan a code base for new strings, and make them available automatically to translators.</p>\n\n<p>They're PHP based which isn't ideal, but it looks like they've done a crapload of work to take the hassle out of translations.</p>","post_number":12,"post_type":1,"updated_at":"2013-02-07T09:34:39.000-05:00","reply_count":1,"reply_to_post_number":11,"quote_count":1,"avg_time":8,"incoming_link_count":1,"reads":231,"score":71.6,"yours":false,"topic_slug":"internationalization-localization","topic_id":280,"display_username":"Pekka Gaiser","version":1,"can_edit":false,"can_delete":false,"can_recover":false,"read":false,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null},{"id":3690,"name":"Valts","username":"Vilx","avatar_template":"//www.gravatar.com/avatar/7bd2e50770e937761cfc3811a332bccc.png?s={size}&r=pg&d=identicon","created_at":"2013-02-07T10:05:35.000-05:00","cooked":"<p>This site looks so nice with all the little tweaks like \"10 minutes ago\" instead of simply time, etc - I wonder if there will also be support for proper pluralization in other languages? That's a pretty hard task though, I don't think I've ever seen a website that has done that. But it would be <em>awesome</em>.</p>","post_number":13,"post_type":1,"updated_at":"2013-02-07T10:05:35.000-05:00","reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":8,"incoming_link_count":11,"reads":246,"score":149.6,"yours":false,"topic_slug":"internationalization-localization","topic_id":280,"display_username":"Valts","version":1,"can_edit":false,"can_delete":false,"can_recover":false,"read":false,"user_title":null,"actions_summary":[{"id":2,"count":3,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":false,"staff":false,"user_id":1216,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null},{"id":3925,"name":"Eric Kidd","username":"emk","avatar_template":"//www.gravatar.com/avatar/528ca205857ff8f648359dcd3e74c84a.png?s={size}&r=pg&d=identicon","created_at":"2013-02-07T14:37:06.000-05:00","cooked":"<p><aside class=\"quote\" data-post=\"12\" data-topic=\"280\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img width=\"20\" height=\"20\" src=\"//www.gravatar.com/avatar/100a6c42a31a56e882475725d65537f8.png?s=40&r=pg&d=identicon\" class=\"avatar\">pekka:</div>\n<blockquote><p>As said, I'd look into whether WP's tools can't be reused for this with some tweaking. They seem to be able to scan a code base for new strings, and make them available automatically to translators.</p></blockquote></aside></p>\n\n<p>I've had pretty decent luck using Localeapp to localize Rails applications:</p>\n\n<p><a href=\"http://www.localeapp.com/\" class=\"onebox\" target=\"_blank\" rel=\"nofollow\">http://www.localeapp.com/</a></p>\n\n<p>The developer workflow took me about an hour to really get used to, and there were a few minor glitches. But the non-technical translators had very few problems. One limitation: It insists on rewriting all those yaml files full of strings.</p>\n\n<p>Anyway, it's worth a look, and it's free for open source, if I recall correctly. Certainly easier than doing a whole bunch of toolsmithing from scratch.</p>","post_number":14,"post_type":1,"updated_at":"2013-02-07T14:37:06.000-05:00","reply_count":1,"reply_to_post_number":12,"quote_count":1,"avg_time":9,"incoming_link_count":0,"reads":237,"score":127.85,"yours":false,"topic_slug":"internationalization-localization","topic_id":280,"display_username":"Eric Kidd","version":1,"can_edit":false,"can_delete":false,"can_recover":false,"link_counts":[{"url":"http://www.localeapp.com/","internal":false,"reflection":false,"clicks":59}],"read":false,"user_title":null,"actions_summary":[{"id":2,"count":3,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":false,"staff":false,"user_id":1860,"hidden":false,"hidden_reason_id":null,"trust_level":1,"deleted_at":null,"user_deleted":false,"edit_reason":null},{"id":3938,"name":"Pekka Gaiser","username":"pekka","avatar_template":"//www.gravatar.com/avatar/100a6c42a31a56e882475725d65537f8.png?s={size}&r=pg&d=identicon","created_at":"2013-02-07T14:52:13.000-05:00","cooked":"<p><aside class=\"quote\" data-post=\"14\" data-topic=\"280\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img width=\"20\" height=\"20\" src=\"//www.gravatar.com/avatar/528ca205857ff8f648359dcd3e74c84a.png?s=40&r=pg&d=identicon\" class=\"avatar\">emk:</div>\n<blockquote><p>I've had pretty decent luck using Localeapp to localize Rails applications</p></blockquote></aside></p>\n\n<p>Ohhh. Looking sexy. <em>droool</em></p>","post_number":15,"post_type":1,"updated_at":"2013-02-07T14:52:13.000-05:00","reply_count":1,"reply_to_post_number":14,"quote_count":1,"avg_time":8,"incoming_link_count":0,"reads":217,"score":63.8,"yours":false,"topic_slug":"internationalization-localization","topic_id":280,"display_username":"Pekka Gaiser","version":1,"can_edit":false,"can_delete":false,"can_recover":false,"read":false,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null},{"id":3982,"name":"Eric Kidd","username":"emk","avatar_template":"//www.gravatar.com/avatar/528ca205857ff8f648359dcd3e74c84a.png?s={size}&r=pg&d=identicon","created_at":"2013-02-07T15:52:22.000-05:00","cooked":"<p><aside class=\"quote\" data-post=\"15\" data-topic=\"280\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img width=\"20\" height=\"20\" src=\"//www.gravatar.com/avatar/100a6c42a31a56e882475725d65537f8.png?s=40&r=pg&d=identicon\" class=\"avatar\">pekka:</div>\n<blockquote><p>Ohhh. Looking sexy. droool</p></blockquote></aside></p>\n\n<p>Yeah, it's pretty. <img src=\"/plugins/emoji/images/smile.png\" title=\":smile:\" class=\"emoji\" alt=\"smile\" width=\"64\" height=\"64\"> But there were still some rough edges as of a few months ago.</p>\n\n<p>Whether or not those rough edges are a deal-breaker will probably depends on whether or not localization is already a source of acute pain. If you're already hurting, Localeapp is a pretty useful tool, especially when it comes to enlisting non-technical translators.</p>\n\n<p>But it does require changing how you work with text, and adding one new tool to the mix. So for projects that just don't want to know about non-English languages, it's not yet seamless the way Unicode is these days.</p>\n\n<p>(Sweet forum software, by the way. I was just testing out Egyptian hieroglyphics on the test server, because they're well off the Basic Multilingual Plane, and tend to flush Unicode bugs. Everything worked flawlessly.)</p>","post_number":16,"post_type":1,"updated_at":"2013-02-07T15:52:22.000-05:00","reply_count":1,"reply_to_post_number":15,"quote_count":1,"avg_time":8,"incoming_link_count":0,"reads":209,"score":62.2,"yours":false,"topic_slug":"internationalization-localization","topic_id":280,"display_username":"Eric Kidd","version":1,"can_edit":false,"can_delete":false,"can_recover":false,"read":false,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":false,"staff":false,"user_id":1860,"hidden":false,"hidden_reason_id":null,"trust_level":1,"deleted_at":null,"user_deleted":false,"edit_reason":null},{"id":3989,"name":"Pekka Gaiser","username":"pekka","avatar_template":"//www.gravatar.com/avatar/100a6c42a31a56e882475725d65537f8.png?s={size}&r=pg&d=identicon","created_at":"2013-02-07T16:04:15.000-05:00","cooked":"<p><aside class=\"quote\" data-post=\"16\" data-topic=\"280\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img width=\"20\" height=\"20\" src=\"//www.gravatar.com/avatar/528ca205857ff8f648359dcd3e74c84a.png?s=40&r=pg&d=identicon\" class=\"avatar\">emk:</div>\n<blockquote><p>But it does require changing how you work with text, and adding one new tool to the mix. So for projects that just don't want to know about non-English languages, it's not yet seamless the way Unicode is these days.</p></blockquote></aside></p>\n\n<p>Interesting, thanks for the insight. I don't think localization is seriously on their table right now, there's likely to be many other things on the table before it... but it will become an issue sooner or later.</p>","post_number":17,"post_type":1,"updated_at":"2013-02-07T16:04:15.000-05:00","reply_count":2,"reply_to_post_number":16,"quote_count":1,"avg_time":8,"incoming_link_count":0,"reads":212,"score":67.8,"yours":false,"topic_slug":"internationalization-localization","topic_id":280,"display_username":"Pekka Gaiser","version":1,"can_edit":false,"can_delete":false,"can_recover":false,"read":false,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null},{"id":3996,"name":"Sam Saffron","username":"sam","avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","created_at":"2013-02-07T16:12:06.000-05:00","cooked":"<p>I had an idea ... what if in dev mode, you could right-click on a page and get access to all the translations on the page, make your edits and have it refreshed live. </p>\n\n<p>I think it would be awesome, very doable technically.</p>","post_number":18,"post_type":1,"updated_at":"2013-02-07T16:12:06.000-05:00","reply_count":2,"reply_to_post_number":17,"quote_count":0,"avg_time":9,"incoming_link_count":0,"reads":220,"score":144.45,"yours":false,"topic_slug":"internationalization-localization","topic_id":280,"display_username":"Sam Saffron","version":1,"can_edit":false,"can_delete":false,"can_recover":false,"read":false,"user_title":"co-founder","reply_to_user":{"username":"pekka","avatar_template":"//www.gravatar.com/avatar/100a6c42a31a56e882475725d65537f8.png?s={size}&r=pg&d=identicon"},"actions_summary":[{"id":2,"count":6,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null},{"id":4009,"name":"Pekka Gaiser","username":"pekka","avatar_template":"//www.gravatar.com/avatar/100a6c42a31a56e882475725d65537f8.png?s={size}&r=pg&d=identicon","created_at":"2013-02-07T16:18:47.000-05:00","cooked":"<p>That would be fricking cool. There'd still be some leftovers (like error messages that normally never show up, etc.) but you could corral those up on a specific page.</p>\n\n<p>It could have a dropdown giving you all the languages that you have a .yml for in the locale directory, and write the changes into the one selected. I'm sure people would love it.</p>","post_number":19,"post_type":1,"updated_at":"2013-02-07T16:22:10.000-05:00","reply_count":0,"reply_to_post_number":18,"quote_count":0,"avg_time":9,"incoming_link_count":0,"reads":203,"score":56.05,"yours":false,"topic_slug":"internationalization-localization","topic_id":280,"display_username":"Pekka Gaiser","version":1,"can_edit":false,"can_delete":false,"can_recover":false,"read":false,"user_title":null,"reply_to_user":{"username":"sam","avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon"},"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null},{"id":4012,"name":"Marco Ceppi","username":"marcoceppi","avatar_template":"//www.gravatar.com/avatar/4ddc8924e79bcec03256821af65fca91.png?s={size}&r=pg&d=identicon","created_at":"2013-02-07T16:22:46.000-05:00","cooked":"<p>If you use gettext format you could leverage <a href=\"https://translations.launchpad.net/\" rel=\"nofollow\">Launchpad</a> translations and the community behind it.</p>","post_number":20,"post_type":1,"updated_at":"2013-02-07T16:22:46.000-05:00","reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":10,"incoming_link_count":1,"reads":204,"score":61.3,"yours":false,"topic_slug":"internationalization-localization","topic_id":280,"display_username":"Marco Ceppi","version":1,"can_edit":false,"can_delete":false,"can_recover":false,"link_counts":[{"url":"https://translations.launchpad.net/","internal":false,"reflection":false,"clicks":8}],"read":false,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":false,"staff":false,"user_id":761,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null}],"stream":[398,419,1060,3623,3651,3654,3655,3658,3660,3667,3673,3675,3690,3925,3938,3982,3989,3996,4009,4012,4025,4056,4058,4093,4129,6288,6302,6683,6687,7059,7078,7197,7445,7448,7524,7528,7784,8379,8426,8427,8569,8570,8577,8861,8992,8999,9000,9002,9015,9048,9052,9104,9408,9435,9625,9631,9655,9896,10386,10400,10547,10671,10700,10710,10714,10753,10786,10846,10893,10994,11001,11107,11221,11225,11229,11251,11660,12453,12454,12462,12624,12625,12627,12628,12629,12630,12918,13501,13507,17251,17252,17977,20706,21397,25473,30505,30512]},"draft":null,"draft_key":"topic_280","draft_sequence":null,"pinned":false,"details":{"auto_close_at":null,"created_by":{"id":255,"username":"uwe_keim","avatar_template":"//www.gravatar.com/avatar/53a82f701ae492808834e621de2586eb.png?s={size}&r=pg&d=identicon"},"last_poster":{"id":19,"username":"eviltrout","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon"},"participants":[{"id":212,"username":"alxndr","avatar_template":"//www.gravatar.com/avatar/51c9cfe3d5ebd64a79983aa3117f4aed.png?s={size}&r=pg&d=identicon","post_count":11},{"id":1,"username":"sam","avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","post_count":11},{"id":7,"username":"pekka","avatar_template":"//www.gravatar.com/avatar/100a6c42a31a56e882475725d65537f8.png?s={size}&r=pg&d=identicon","post_count":8},{"id":461,"username":"kuba","avatar_template":"//www.gravatar.com/avatar/1835cb6a5f35bd4089e416a99af90f5f.png?s={size}&r=pg&d=identicon","post_count":7},{"id":2995,"username":"tattoo","avatar_template":"//www.gravatar.com/avatar/645454e097898e3f0d9a54c699995678.png?s={size}&r=pg&d=identicon","post_count":6},{"id":2540,"username":"jgourdon","avatar_template":"//www.gravatar.com/avatar/3f0ee7e17ec820c458958ed7b0e8538b.png?s={size}&r=pg&d=identicon","post_count":5},{"id":1860,"username":"emk","avatar_template":"//www.gravatar.com/avatar/528ca205857ff8f648359dcd3e74c84a.png?s={size}&r=pg&d=identicon","post_count":4},{"id":19,"username":"eviltrout","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","post_count":4},{"id":1275,"username":"dacap","avatar_template":"//www.gravatar.com/avatar/ec0ebc7c17f649d03ee78d4eba56ef73.png?s={size}&r=pg&d=identicon","post_count":4},{"id":3190,"username":"gururea","avatar_template":"//www.gravatar.com/avatar/5ffb222c9c1bd2d99d9267c1557ca984.png?s={size}&r=pg&d=identicon","post_count":3},{"id":1895,"username":"maciek","avatar_template":"//www.gravatar.com/avatar/e3fe0c49f509994d67045602f49808ee.png?s={size}&r=pg&d=identicon","post_count":3},{"id":3704,"username":"mojzis","avatar_template":"//localhost:3000/uploads/default/avatars/2d3/5f5/e677798a1a/{size}.jpg","post_count":3},{"id":22,"username":"splattne","avatar_template":"//www.gravatar.com/avatar/7847006dbf49f1722b07c8da396f1275.png?s={size}&r=pg&d=identicon","post_count":2},{"id":32,"username":"codinghorror","avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","post_count":2},{"id":1979,"username":"Superuser","avatar_template":"//www.gravatar.com/avatar/a7f1529299c8fb9a263b8e8afcab23da.png?s={size}&r=pg&d=identicon","post_count":2},{"id":3620,"username":"potthast","avatar_template":"//www.gravatar.com/avatar/1753724263a5dee3e38790e6ac3d685c.png?s={size}&r=pg&d=identicon","post_count":2},{"id":3818,"username":"Tudor","avatar_template":"//www.gravatar.com/avatar/8f367608e1d013beed72a8941bb768ca.png?s={size}&r=pg&d=identicon","post_count":2},{"id":9,"username":"tms","avatar_template":"//www.gravatar.com/avatar/3981cd271c302f5cba628c6b6d2b32ee.png?s={size}&r=pg&d=identicon","post_count":2},{"id":255,"username":"uwe_keim","avatar_template":"//www.gravatar.com/avatar/53a82f701ae492808834e621de2586eb.png?s={size}&r=pg&d=identicon","post_count":1},{"id":2753,"username":"mikl","avatar_template":"//www.gravatar.com/avatar/2c3b9882e6898958b892a218b5493af9.png?s={size}&r=pg&d=identicon","post_count":1},{"id":5052,"username":"vulkanino","avatar_template":"//www.gravatar.com/avatar/811bf232b634245aebba5323462d885c.png?s={size}&r=pg&d=identicon","post_count":1},{"id":761,"username":"marcoceppi","avatar_template":"//www.gravatar.com/avatar/4ddc8924e79bcec03256821af65fca91.png?s={size}&r=pg&d=identicon","post_count":1},{"id":2316,"username":"pakl","avatar_template":"//www.gravatar.com/avatar/42910619ef3d550e37f7150caa0d94ff.png?s={size}&r=pg&d=identicon","post_count":1},{"id":5564,"username":"Sjors","avatar_template":"//www.gravatar.com/avatar/2fb09bd6501779802459a171d3f8fbd9.png?s={size}&r=pg&d=identicon","post_count":1}],"suggested_topics":[{"id":5894,"title":"Spam-blocking URL Blacklist","fancy_title":"Spam-blocking URL Blacklist","slug":"spam-blocking-url-blacklist","posts_count":5,"reply_count":3,"highest_post_number":5,"image_url":null,"created_at":"2013-04-15T11:09:39.000-04:00","last_posted_at":"2013-04-15T13:33:11.000-04:00","bumped":true,"bumped_at":"2013-04-15T13:33:11.000-04:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"archetype":"regular","like_count":3,"views":244,"category_id":2},{"id":7116,"title":"Custom fields and Custom post types - (ex. article, recipe)","fancy_title":"Custom fields and Custom post types - (ex. article, recipe)","slug":"custom-fields-and-custom-post-types-ex-article-recipe","posts_count":2,"reply_count":1,"highest_post_number":2,"image_url":null,"created_at":"2013-06-02T17:34:37.000-04:00","last_posted_at":"2013-06-03T01:27:34.000-04:00","bumped":true,"bumped_at":"2013-06-03T01:27:34.000-04:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"archetype":"regular","like_count":0,"views":206,"category_id":2},{"id":7111,"title":"Make the 1 level of sub-categorization freeform","fancy_title":"Make the 1 level of sub-categorization freeform","slug":"make-the-1-level-of-sub-categorization-freeform","posts_count":8,"reply_count":4,"highest_post_number":8,"image_url":"https://www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s=40&r=pg&d=identicon","created_at":"2013-06-02T14:47:06.000-04:00","last_posted_at":"2013-06-04T18:07:33.000-04:00","bumped":true,"bumped_at":"2013-06-04T18:07:33.000-04:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"archetype":"regular","like_count":0,"views":197,"category_id":2},{"id":1127,"title":"Automated Signatures?","fancy_title":"Automated Signatures?","slug":"automated-signatures","posts_count":75,"reply_count":57,"highest_post_number":76,"image_url":null,"created_at":"2013-02-06T05:40:37.000-05:00","last_posted_at":"2013-11-19T18:31:32.000-05:00","bumped":true,"bumped_at":"2013-11-19T18:31:32.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"archetype":"regular","like_count":156,"views":2371,"category_id":2},{"id":8667,"title":"Add \"Share\" modal to original thread post?","fancy_title":"Add “Share” modal to original thread post?","slug":"add-share-modal-to-original-thread-post","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":"/uploads/meta_discourse/1497/ff2adcd46c71a223.png","created_at":"2013-07-26T10:22:48.000-04:00","last_posted_at":"2013-07-26T10:22:49.000-04:00","bumped":true,"bumped_at":"2013-07-26T10:22:49.000-04:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"archetype":"regular","like_count":0,"views":122,"category_id":2},{"id":708,"title":"Special topic types","fancy_title":"Special topic types","slug":"special-topic-types","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2013-02-05T21:03:26.000-05:00","last_posted_at":"2013-02-05T21:03:26.000-05:00","bumped":false,"bumped_at":"2013-02-05T21:03:26.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"archetype":"regular","like_count":1,"views":108,"category_id":2},{"id":6569,"title":"Linebreaks require two trailing spaces in blockquotes, etc","fancy_title":"Linebreaks require two trailing spaces in blockquotes, etc","slug":"linebreaks-require-two-trailing-spaces-in-blockquotes-etc","posts_count":3,"reply_count":2,"highest_post_number":3,"image_url":null,"created_at":"2013-05-12T02:40:48.000-04:00","last_posted_at":"2013-05-15T11:01:50.000-04:00","bumped":true,"bumped_at":"2013-05-15T11:01:50.000-04:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"archetype":"regular","like_count":0,"views":168,"category_id":2}],"links":[{"url":"https://github.com/discourse/discourse/blob/master/config/locales/en.yml","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"91","user_id":9},{"url":"http://www.localeapp.com/","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"59","user_id":1860},{"url":"http://stackoverflow.com/questions/4232922/why-do-people-use-plain-english-as-translation-placeholders","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"56","user_id":7},{"url":"https://github.com/SlexAxton/messageformat.js","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"38","user_id":1},{"url":"https://github.com/discourse/discourse/blob/master/config/locales","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"27","user_id":32},{"url":"http://www.localeapp.com/projects/1537/translations?utf8=%E2%9C%93&search=source_code","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"21","user_id":1860},{"url":"https://github.com/berk/tr8n","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"20","user_id":1},{"url":"https://translations.launchpad.net/","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"18","user_id":761},{"url":"https://www.transifex.com/","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"16","user_id":1979},{"url":"http://translate.wordpress.org/projects/bbpress/dev","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"14","user_id":7},{"url":"http://weblate.org","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"13","user_id":2316},{"url":"https://github.com/discourse/discourse/pull/493","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"12","user_id":2753},{"url":"https://github.com/SlexAxton","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"9","user_id":1},{"url":"https://github.com/discourse/discourse/tree/master/config/locales","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"8","user_id":19},{"url":"https://github.com/gururea/discourse/tree/master/config/locales","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"8","user_id":3190},{"url":"https://github.com/dacap/discourse/tree/spanish","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"7","user_id":1275},{"url":"https://github.com/discourse/discourse/blob/master/config/locales/client.en.yml#L691","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"7","user_id":461},{"url":"https://github.com/discourse/discourse/blob/master/config/locales/client.nl.yml","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"5","user_id":461},{"url":"http://tr8n.github.com/","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"5","user_id":212},{"url":"https://github.com/discourse/discourse/commit/c5761eae8afe37e20cec0d0f9d14b85b6e585bda","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"5","user_id":212},{"url":"http://www.getlocalization.com/","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"5","user_id":22},{"url":"http://blog.discourse.org/2013/04/discourse-as-your-first-rails-app/","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"4","user_id":1995},{"url":"http://translate.sourceforge.net/wiki/virtaal/index","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"4","user_id":1979},{"url":"https://poeditor.com/","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"4","user_id":1979},{"url":"http://www.madanalogy.com/2012/06/rails-i18n-translations-in-yaml.html","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"3","user_id":3190},{"url":"http://meta.discourse.org/t/translation-workflow/6102/6","title":"Translation workflow","fancy_title":null,"internal":true,"reflection":false,"clicks":"3","user_id":1995},{"url":"https://github.com/alxndr/discourse/blob/i18n-chinese/config/locales/server.zh.yml","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"3","user_id":212},{"url":"http://www.slideshare.net/HeatherRivers/linguistic-potluck-crowdsourcing-localization-with-rails","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"2","user_id":212},{"url":"http://www.youtube.com/watch?v=MqqdzJ98q7s","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"2","user_id":212},{"url":"http://en.wikipedia.org/wiki/T%E2%80%93V_distinction","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"2","user_id":3620},{"url":"http://pootle.locamotion.org/","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"2","user_id":3190},{"url":"http://en.lichess.org/@/Hellball","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"2","user_id":1979},{"url":"http://sugarjs.com/dates#date_locales","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"1","user_id":461},{"url":"http://meta.discourse.org/t/comrades-lets-join-our-efforts-on-ukrainian-and-russian-translations/4403/5","title":"Comrades let's join our efforts on ukrainian and russian translations","fancy_title":null,"internal":true,"reflection":true,"clicks":"1","user_id":3417},{"url":"https://github.com/discourse/discourse/blob/master/app/assets/javascripts/locales/date_locales.js","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"1","user_id":461},{"url":"http://meta.discourse.org/t/changing-language-phrase-does-not-affect-on-the-site/8429/3","title":"Changing language phrase does not affect on the site?","fancy_title":null,"internal":true,"reflection":true,"clicks":"0","user_id":6122},{"url":"http://meta.discourse.org/t/solving-xda-developer-style-forums/4368/4","title":"Solving XDA-Developer style forums","fancy_title":null,"internal":true,"reflection":true,"clicks":"0","user_id":639},{"url":"http://meta.discourse.org/t/translation-workflow/6102","title":"Translation workflow","fancy_title":null,"internal":true,"reflection":true,"clicks":"0","user_id":4702},{"url":"http://meta.discourse.org/t/bookmark-last-read-sometimes-doesn-t-go-to-the-end-of-a-topic/4825/9","title":"Bookmark/last read sometimes doesn't go to the end of a topic","fancy_title":null,"internal":true,"reflection":true,"clicks":"0","user_id":3681},{"url":"http://meta.discourse.org/t/suggestion-translation-on-admin-panel/6923/5","title":"Suggestion: Translation on admin panel","fancy_title":null,"internal":true,"reflection":true,"clicks":"0","user_id":1},{"url":"http://meta.discourse.org/t/roadplan-for-discourse/2939/5","title":"Roadplan for Discourse","fancy_title":null,"internal":true,"reflection":true,"clicks":"0","user_id":32},{"url":"http://meta.discourse.org/t/internationalization-i18n-provided-for-discourse/2073/2","title":"Internationalization I18n provided for discourse ?","fancy_title":null,"internal":true,"reflection":true,"clicks":"0","user_id":114},{"url":"http://meta.discourse.org/t/language-mirrors/2378/2","title":"Language mirrors","fancy_title":null,"internal":true,"reflection":true,"clicks":"0","user_id":32},{"url":"http://meta.discourse.org/t/hi-support-chinese/4393/6","title":"Hi, support Chinese?","fancy_title":null,"internal":true,"reflection":true,"clicks":"0","user_id":2014},{"url":"http://meta.discourse.org/t/roadplan-for-discourse/2939/3","title":"Roadplan for Discourse","fancy_title":null,"internal":true,"reflection":true,"clicks":"0","user_id":2540},{"url":"http://meta.discourse.org/t/missing-user-value-in-chinese-localized-page/7406/6","title":"[missing {{user}} value] in Chinese localized page","fancy_title":null,"internal":true,"reflection":true,"clicks":"0","user_id":1},{"url":"http://meta.discourse.org/t/github-onebox-rendering-issue/7616","title":"GitHub OneBox Rendering Issue","fancy_title":null,"internal":true,"reflection":true,"clicks":"0","user_id":5372},{"url":"http://guides.rubyonrails.org/i18n.html#the-public-i18n-api","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":"0","user_id":1895},{"url":"http://meta.discourse.org/t/translation-tools-transifex-localeapp/7763","title":"Translation Tools: Transifex? Localeapp?","fancy_title":null,"internal":true,"reflection":true,"clicks":"0","user_id":2},{"url":"http://meta.discourse.org/t/what-i-love-about-wordpress-plugins/5697","title":"What I love about WordPress plugins","fancy_title":null,"internal":true,"reflection":true,"clicks":"0","user_id":1}]},"highest_post_number":98,"deleted_by":null,"id":280,"title":"Internationalization / localization","fancy_title":"Internationalization / localization","posts_count":97,"created_at":"2013-02-05T16:29:00.000-05:00","views":3500,"reply_count":66,"participant_count":34,"like_count":128,"last_posted_at":"2013-10-13T13:30:47.000-04:00","visible":true,"closed":false,"archived":false,"has_summary":true,"archetype":"regular","slug":"internationalization-localization","category_id":2,"word_count":5576,"deleted_at":null}}; +export default {"/t/280.json": {"post_stream":{"posts":[{"id":398,"name":"Uwe Keim","username":"uwe_keim","avatar_template":"/user_avatar/meta.discourse.org/uwe_keim/{size}/5697.png","uploaded_avatar_id":5697,"created_at":"2013-02-05T21:29:00.280Z","cooked":"<p>Any plans to support localization of UI elements, so that I (for example) could set up a completely German speaking forum?</p>","post_number":1,"post_type":1,"updated_at":"2013-02-05T21:29:00.280Z","like_count":0,"reply_count":1,"reply_to_post_number":null,"quote_count":0,"avg_time":25,"incoming_link_count":314,"reads":475,"score":1702.25,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Uwe Keim","primary_group_name":null,"version":2,"can_edit":true,"can_delete":false,"can_recover":true,"link_counts":[{"url":"https://meta.discourse.org/t/language-mirrors/2378/2","internal":true,"reflection":true,"title":"Language mirrors","clicks":3},{"url":"https://meta.discourse.org/t/translation-workflow/6102","internal":true,"reflection":true,"title":"Translation workflow","clicks":2},{"url":"https://meta.discourse.org/t/solving-xda-developer-style-forums/4368/4","internal":true,"reflection":true,"title":"Solving XDA-Developer style forums","clicks":2},{"url":"https://meta.discourse.org/t/comrades-lets-join-our-efforts-on-ukrainian-and-russian-translations/4403/5","internal":true,"reflection":true,"title":"Comrades let's join our efforts on ukrainian and russian translations","clicks":1},{"url":"https://meta.discourse.org/t/bookmark-last-read-sometimes-doesn-t-go-to-the-end-of-a-topic/4825/9","internal":true,"reflection":true,"title":"Bookmark/last read sometimes doesn't go to the end of a topic","clicks":0},{"url":"https://meta.discourse.org/t/roadplan-for-discourse/2939/5","internal":true,"reflection":true,"title":"Roadplan for Discourse 2013","clicks":0}],"read":true,"user_title":null,"actions_summary":[{"id":2,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":255,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":419,"name":"Tim Stone","username":"tms","avatar_template":"/user_avatar/meta.discourse.org/tms/{size}/40181.png","uploaded_avatar_id":40181,"created_at":"2013-02-05T21:32:47.649Z","cooked":"<p>The application strings <a href=\"https://github.com/discourse/discourse/blob/master/config/locales/en.yml\" rel=\"nofollow\">are externalized</a>, so localization should be entirely possible with enough translation effort.</p>","post_number":2,"post_type":1,"updated_at":"2013-02-06T10:15:27.965Z","like_count":4,"reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":27,"incoming_link_count":16,"reads":460,"score":308.35,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Tim Stone","primary_group_name":null,"version":2,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"https://github.com/discourse/discourse/blob/master/config/locales/en.yml","internal":false,"reflection":false,"clicks":118}],"read":true,"user_title":"Great contributor","actions_summary":[{"id":2,"count":4,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":9,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":1060,"name":"Jeff Atwood","username":"codinghorror","avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png","uploaded_avatar_id":5297,"created_at":"2013-02-06T02:26:24.922Z","cooked":"<p>Yep, all strings are going through a lookup table.*</p>\n\n<p><a href=\"https://github.com/discourse/discourse/blob/master/config/locales\">master/config/locales</a></p>\n\n<p>So you could replace that lookup table with the \"de\" one to get German.</p>\n\n<p><sub>* we didn't get all the strings into the lookup table for launch, but the vast, vast majority of them are and the ones that are not, we will fix as we go!</sub></p>","post_number":3,"post_type":1,"updated_at":"2014-02-24T05:23:39.211Z","like_count":4,"reply_count":3,"reply_to_post_number":null,"quote_count":0,"avg_time":33,"incoming_link_count":5,"reads":449,"score":191.45,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Jeff Atwood","primary_group_name":"discourse","version":4,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"https://github.com/discourse/discourse/blob/master/config/locales","internal":false,"reflection":false,"title":"discourse/config/locales at master · discourse/discourse · GitHub","clicks":62},{"url":"https://meta.discourse.org/t/github-onebox-rendering-issue/7616","internal":true,"reflection":true,"title":"GitHub OneBox Rendering Issue","clicks":0}],"read":true,"user_title":"co-founder","actions_summary":[{"id":2,"count":4,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":true,"admin":true,"staff":true,"user_id":32,"hidden":false,"hidden_reason_id":null,"trust_level":3,"deleted_at":null,"user_deleted":false,"edit_reason":"","can_view_edit_history":true,"wiki":false},{"id":3623,"name":"Shade","username":"shade","avatar_template":"/user_avatar/meta.discourse.org/shade/{size}/8306.png","uploaded_avatar_id":8306,"created_at":"2013-02-07T12:55:33.129Z","cooked":"<p>Is it a coincidence that the strings file is 1337 lines long? <img src=\"//meta-discourse.global.ssl.fastly.net/images/emoji/twitter/smiley.png\" title=\":smiley:\" class=\"emoji\" alt=\"smiley\" width=\"64\" height=\"64\"></p>","post_number":4,"post_type":1,"updated_at":"2013-02-07T12:55:33.129Z","like_count":7,"reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":20,"incoming_link_count":15,"reads":401,"score":291.2,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Shade","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"https://meta.discourse.org/t/hi-support-chinese/4393/6","internal":true,"reflection":true,"title":"Hi, support Chinese?","clicks":0}],"read":true,"user_title":null,"actions_summary":[{"id":2,"count":7,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":1808,"hidden":false,"hidden_reason_id":null,"trust_level":1,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3651,"name":"Pekka Gaiser","username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253,"created_at":"2013-02-07T14:02:07.869Z","cooked":"<p><aside class=\"quote\" data-post=\"3\" data-topic=\"280\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img width=\"20\" height=\"20\" src=\"/user_avatar/meta.discourse.org/codinghorror/40/5297.png\" class=\"avatar\">codinghorror said:</div>\n<blockquote><p>So you could replace that lookup table with the \"de\" one to get German.</p></blockquote></aside></p>\n\n<p>The problem I see here is that this file is likely two grow and change massively over the next couple months, and tracking these changes in order to keep a localized file up to date is going to be a <em>bitch</em>. </p>\n\n<p>I wonder where there is a tool that can compare two yml structures and point out which nodes are missing? That would help keep track of new strings.</p>\n\n<p>Re keeping track of <em>changed</em> strings, <a class=\"mention\" href=\"/users/codinghorror\">@codinghorror</a> I found this very interesting: <a href=\"http://stackoverflow.com/questions/4232922/why-do-people-use-plain-english-as-translation-placeholders\" rel=\"nofollow\">http://stackoverflow.com/questions/4232922/why-do-people-use-plain-english-as-translation-placeholders</a> if plain English placeholders were used, any change in strings would lead to a <em>new</em> node in the yml file, making keeping the translation up to date easier. Maybe worth thinking about in the future.</p>","post_number":5,"post_type":1,"updated_at":"2013-02-07T14:05:42.328Z","like_count":2,"reply_count":2,"reply_to_post_number":3,"quote_count":1,"avg_time":22,"incoming_link_count":10,"reads":386,"score":213.3,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Pekka Gaiser","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"http://stackoverflow.com/questions/4232922/why-do-people-use-plain-english-as-translation-placeholders","internal":false,"reflection":false,"title":"internationalization - Why do people use plain english as translation placeholders? - Stack Overflow","clicks":63}],"read":true,"user_title":null,"actions_summary":[{"id":2,"count":2,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3654,"name":"Sam Saffron","username":"sam","avatar_template":"/user_avatar/meta.discourse.org/sam/{size}/5243.png","uploaded_avatar_id":5243,"created_at":"2013-02-07T14:05:39.910Z","cooked":"<p>Yes, I really like the concept of fuzzy matching for localization, perhaps you can chase up <a href=\"https://github.com/SlexAxton\">alex sexton</a> he was meaning to upload a localization tool for this kind of stuff. </p>\n\n<p>Also, I am a big fan of <a href=\"https://github.com/SlexAxton/messageformat.js\">ICU message format</a>, but it is not the \"Rails way (tm)\". </p>","post_number":6,"post_type":1,"updated_at":"2013-02-07T14:05:39.910Z","like_count":1,"reply_count":1,"reply_to_post_number":5,"quote_count":0,"avg_time":17,"incoming_link_count":4,"reads":329,"score":106.65,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Sam Saffron","primary_group_name":"discourse","version":1,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"https://github.com/SlexAxton/messageformat.js","internal":false,"reflection":false,"title":"SlexAxton/messageformat.js · GitHub","clicks":46},{"url":"https://github.com/SlexAxton","internal":false,"reflection":false,"title":"SlexAxton (Alex Sexton) · GitHub","clicks":10}],"read":true,"user_title":"co-founder","reply_to_user":{"username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253},"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":true,"admin":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3655,"name":"Pekka Gaiser","username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253,"created_at":"2013-02-07T14:08:17.493Z","cooked":"<p>Looks interesting, I'll take a peek.</p>\n\n<p>As said on dev, the best tool I can see in terms of giving translators a proper interface <em>and</em> quality control would be something like <a href=\"http://translate.wordpress.org/projects/bbpress/dev\" rel=\"nofollow\">GlotPress</a>. It's based on the PO messages format (is that somehow related to ICU?) but looks pretty great.</p>\n\n<p><aside class=\"quote\" data-post=\"6\" data-topic=\"280\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img width=\"20\" height=\"20\" src=\"/user_avatar/meta.discourse.org/sam/40/5243.png\" class=\"avatar\">sam said:</div>\n<blockquote><p>fuzzy matching for localization</p></blockquote></aside></p>\n\n<p>I'm not familiar with the term in this context, you mean keeping the English version in the code base (instead of a generic code like <code>message_error_nametooshort</code> ?)</p>","post_number":7,"post_type":1,"updated_at":"2013-02-07T14:12:02.965Z","like_count":1,"reply_count":1,"reply_to_post_number":6,"quote_count":1,"avg_time":16,"incoming_link_count":0,"reads":326,"score":86.0,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Pekka Gaiser","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"http://translate.wordpress.org/projects/bbpress/dev","internal":false,"reflection":false,"title":"WordPress › Development < GlotPress","clicks":16}],"read":true,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3658,"name":"Sam Saffron","username":"sam","avatar_template":"/user_avatar/meta.discourse.org/sam/{size}/5243.png","uploaded_avatar_id":5243,"created_at":"2013-02-07T14:12:22.582Z","cooked":"<p>ICU Message format is basically Gettext on steroids, Gettext has been around for so many years and actually works pretty well, being super prevalent in Linux. </p>\n\n<p>Trouble is you need a fuzzy matcher for translators if you are going to store stuff like <code>mf.compile( 'This is a message.' )</code> in source, one letter change and all your translators need to validate it.</p>","post_number":8,"post_type":1,"updated_at":"2013-02-07T14:12:22.582Z","like_count":1,"reply_count":1,"reply_to_post_number":7,"quote_count":0,"avg_time":11,"incoming_link_count":2,"reads":296,"score":89.75,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Sam Saffron","primary_group_name":"discourse","version":1,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"https://meta.discourse.org/t/what-i-love-about-wordpress-plugins/5697","internal":true,"reflection":true,"title":"What I love about WordPress plugins","clicks":0}],"read":true,"user_title":"co-founder","reply_to_user":{"username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253},"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":true,"admin":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3660,"name":"Pekka Gaiser","username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253,"created_at":"2013-02-07T14:14:12.666Z","cooked":"<p><aside class=\"quote\" data-post=\"8\" data-topic=\"280\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img width=\"20\" height=\"20\" src=\"/user_avatar/meta.discourse.org/sam/40/5243.png\" class=\"avatar\">sam said:</div>\n<blockquote><p>one letter change and all your translators need to validate it.</p></blockquote></aside></p>\n\n<p>Yeah, that's why I've always been a friend of <code>message_error_nametooshort</code> placeholders, until I asked the SO question linked above. The accepted answer makes a good argument against those placeholders: you <em>want</em> translations to break even on small changes in the English original because the translations will probably need to reflect the change, too. Maybe that's not the case right now as new stuff is being checked in pretty much every couple of hours, but in the long run, it'll be overwhelmingly true.</p>","post_number":9,"post_type":1,"updated_at":"2013-02-07T14:18:09.569Z","like_count":1,"reply_count":1,"reply_to_post_number":8,"quote_count":1,"avg_time":10,"incoming_link_count":0,"reads":293,"score":79.1,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Pekka Gaiser","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3667,"name":"Tim Stone","username":"tms","avatar_template":"/user_avatar/meta.discourse.org/tms/{size}/40181.png","uploaded_avatar_id":40181,"created_at":"2013-02-07T14:25:16.859Z","cooked":"<p>Hmm...You could theoretically also build something into the development process that would monitor changes to the English locale file and make a translator-friendly list of changes between versions.</p>","post_number":10,"post_type":1,"updated_at":"2013-02-07T14:25:16.859Z","like_count":1,"reply_count":1,"reply_to_post_number":9,"quote_count":0,"avg_time":7,"incoming_link_count":0,"reads":275,"score":75.35,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Tim Stone","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":"Great contributor","reply_to_user":{"username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253},"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":9,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3673,"name":"Sam Saffron","username":"sam","avatar_template":"/user_avatar/meta.discourse.org/sam/{size}/5243.png","uploaded_avatar_id":5243,"created_at":"2013-02-07T14:30:21.401Z","cooked":"<p>Yeah, totally, also we could build tools for dev that make introducing string less annoying and make it possible to garbage collect old unused strings, I hate trudging through that file.</p>","post_number":11,"post_type":1,"updated_at":"2013-02-07T14:30:21.401Z","like_count":1,"reply_count":1,"reply_to_post_number":10,"quote_count":0,"avg_time":7,"incoming_link_count":1,"reads":273,"score":79.95,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Sam Saffron","primary_group_name":"discourse","version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":"co-founder","reply_to_user":{"username":"tms","avatar_template":"/user_avatar/meta.discourse.org/tms/{size}/40181.png","uploaded_avatar_id":40181},"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":true,"admin":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3675,"name":"Pekka Gaiser","username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253,"created_at":"2013-02-07T14:33:38.280Z","cooked":"<p><aside class=\"quote\" data-post=\"11\" data-topic=\"280\" data-full=\"true\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img width=\"20\" height=\"20\" src=\"/user_avatar/meta.discourse.org/sam/40/5243.png\" class=\"avatar\">sam said:</div>\n<blockquote><p>Yeah, totally, also we could build tools for dev that make introducing string less annoying and make it possible to garbage collect old unused strings, I hate trudging through that file.</p></blockquote></aside></p>\n\n<p>As said, I'd look into whether WP's tools can't be reused for this with some tweaking. They seem to be able to scan a code base for new strings, and make them available automatically to translators.</p>\n\n<p>They're PHP based which isn't ideal, but it looks like they've done a crapload of work to take the hassle out of translations.</p>","post_number":12,"post_type":1,"updated_at":"2013-02-07T14:34:39.910Z","like_count":1,"reply_count":1,"reply_to_post_number":11,"quote_count":1,"avg_time":7,"incoming_link_count":2,"reads":273,"score":84.95,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Pekka Gaiser","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3690,"name":"Valts","username":"Vilx","avatar_template":"/user_avatar/meta.discourse.org/vilx/{size}/7299.png","uploaded_avatar_id":7299,"created_at":"2013-02-07T15:05:35.867Z","cooked":"<p>This site looks so nice with all the little tweaks like \"10 minutes ago\" instead of simply time, etc - I wonder if there will also be support for proper pluralization in other languages? That's a pretty hard task though, I don't think I've ever seen a website that has done that. But it would be <em>awesome</em>.</p>","post_number":13,"post_type":1,"updated_at":"2013-02-07T15:05:35.867Z","like_count":3,"reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":7,"incoming_link_count":11,"reads":290,"score":158.35,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Valts","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":null,"actions_summary":[{"id":2,"count":3,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":1216,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3925,"name":"Eric Kidd","username":"emk","avatar_template":"/user_avatar/meta.discourse.org/emk/{size}/8400.png","uploaded_avatar_id":8400,"created_at":"2013-02-07T19:37:06.194Z","cooked":"<p><aside class=\"quote\" data-post=\"12\" data-topic=\"280\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img width=\"20\" height=\"20\" src=\"/user_avatar/meta.discourse.org/pekka/40/5253.png\" class=\"avatar\">pekka said:</div>\n<blockquote><p>As said, I'd look into whether WP's tools can't be reused for this with some tweaking. They seem to be able to scan a code base for new strings, and make them available automatically to translators.</p></blockquote></aside></p>\n\n<p>I've had pretty decent luck using Localeapp to localize Rails applications:</p>\n\n<p><a href=\"http://www.localeapp.com/\" class=\"onebox\" target=\"_blank\" rel=\"nofollow\">http://www.localeapp.com/</a></p>\n\n<p>The developer workflow took me about an hour to really get used to, and there were a few minor glitches. But the non-technical translators had very few problems. One limitation: It insists on rewriting all those yaml files full of strings.</p>\n\n<p>Anyway, it's worth a look, and it's free for open source, if I recall correctly. Certainly easier than doing a whole bunch of toolsmithing from scratch.</p>","post_number":14,"post_type":1,"updated_at":"2013-02-07T19:37:06.194Z","like_count":3,"reply_count":1,"reply_to_post_number":12,"quote_count":1,"avg_time":9,"incoming_link_count":0,"reads":283,"score":137.05,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Eric Kidd","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"http://www.localeapp.com/","internal":false,"reflection":false,"title":"Easy localization for Rails apps | Locale","clicks":69}],"read":true,"user_title":null,"actions_summary":[{"id":2,"count":3,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":1860,"hidden":false,"hidden_reason_id":null,"trust_level":1,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3938,"name":"Pekka Gaiser","username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253,"created_at":"2013-02-07T19:52:13.748Z","cooked":"<p><aside class=\"quote\" data-post=\"14\" data-topic=\"280\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img width=\"20\" height=\"20\" src=\"/user_avatar/meta.discourse.org/emk/40/8400.png\" class=\"avatar\">emk said:</div>\n<blockquote><p>I've had pretty decent luck using Localeapp to localize Rails applications</p></blockquote></aside></p>\n\n<p>Ohhh. Looking sexy. <em>droool</em></p>","post_number":15,"post_type":1,"updated_at":"2013-02-07T19:52:13.748Z","like_count":1,"reply_count":1,"reply_to_post_number":14,"quote_count":1,"avg_time":7,"incoming_link_count":0,"reads":260,"score":72.35,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Pekka Gaiser","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3982,"name":"Eric Kidd","username":"emk","avatar_template":"/user_avatar/meta.discourse.org/emk/{size}/8400.png","uploaded_avatar_id":8400,"created_at":"2013-02-07T20:52:22.454Z","cooked":"<p><aside class=\"quote\" data-post=\"15\" data-topic=\"280\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img width=\"20\" height=\"20\" src=\"/user_avatar/meta.discourse.org/pekka/40/5253.png\" class=\"avatar\">pekka said:</div>\n<blockquote><p>Ohhh. Looking sexy. droool</p></blockquote></aside></p>\n\n<p>Yeah, it's pretty. <img src=\"//meta-discourse.global.ssl.fastly.net/images/emoji/twitter/smile.png\" title=\":smile:\" class=\"emoji\" alt=\"smile\" width=\"64\" height=\"64\"> But there were still some rough edges as of a few months ago.</p>\n\n<p>Whether or not those rough edges are a deal-breaker will probably depends on whether or not localization is already a source of acute pain. If you're already hurting, Localeapp is a pretty useful tool, especially when it comes to enlisting non-technical translators.</p>\n\n<p>But it does require changing how you work with text, and adding one new tool to the mix. So for projects that just don't want to know about non-English languages, it's not yet seamless the way Unicode is these days.</p>\n\n<p>(Sweet forum software, by the way. I was just testing out Egyptian hieroglyphics on the test server, because they're well off the Basic Multilingual Plane, and tend to flush Unicode bugs. Everything worked flawlessly.)</p>","post_number":16,"post_type":1,"updated_at":"2013-02-07T20:52:22.454Z","like_count":1,"reply_count":1,"reply_to_post_number":15,"quote_count":1,"avg_time":7,"incoming_link_count":0,"reads":254,"score":71.15,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Eric Kidd","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":1860,"hidden":false,"hidden_reason_id":null,"trust_level":1,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3989,"name":"Pekka Gaiser","username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253,"created_at":"2013-02-07T21:04:15.405Z","cooked":"<p><aside class=\"quote\" data-post=\"16\" data-topic=\"280\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img width=\"20\" height=\"20\" src=\"/user_avatar/meta.discourse.org/emk/40/8400.png\" class=\"avatar\">emk said:</div>\n<blockquote><p>But it does require changing how you work with text, and adding one new tool to the mix. So for projects that just don't want to know about non-English languages, it's not yet seamless the way Unicode is these days.</p></blockquote></aside></p>\n\n<p>Interesting, thanks for the insight. I don't think localization is seriously on their table right now, there's likely to be many other things on the table before it... but it will become an issue sooner or later.</p>","post_number":17,"post_type":1,"updated_at":"2013-02-07T21:04:15.405Z","like_count":1,"reply_count":2,"reply_to_post_number":16,"quote_count":1,"avg_time":7,"incoming_link_count":0,"reads":255,"score":76.35,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Pekka Gaiser","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3996,"name":"Sam Saffron","username":"sam","avatar_template":"/user_avatar/meta.discourse.org/sam/{size}/5243.png","uploaded_avatar_id":5243,"created_at":"2013-02-07T21:12:06.575Z","cooked":"<p>I had an idea ... what if in dev mode, you could right-click on a page and get access to all the translations on the page, make your edits and have it refreshed live. </p>\n\n<p>I think it would be awesome, very doable technically.</p>","post_number":18,"post_type":1,"updated_at":"2013-02-07T21:12:06.575Z","like_count":7,"reply_count":2,"reply_to_post_number":17,"quote_count":0,"avg_time":8,"incoming_link_count":0,"reads":264,"score":168.2,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Sam Saffron","primary_group_name":"discourse","version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":"co-founder","reply_to_user":{"username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253},"actions_summary":[{"id":2,"count":7,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":true,"admin":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":4009,"name":"Pekka Gaiser","username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253,"created_at":"2013-02-07T21:18:47.422Z","cooked":"<p>That would be fricking cool. There'd still be some leftovers (like error messages that normally never show up, etc.) but you could corral those up on a specific page.</p>\n\n<p>It could have a dropdown giving you all the languages that you have a .yml for in the locale directory, and write the changes into the one selected. I'm sure people would love it.</p>","post_number":19,"post_type":1,"updated_at":"2013-02-07T21:22:10.692Z","like_count":1,"reply_count":0,"reply_to_post_number":18,"quote_count":0,"avg_time":8,"incoming_link_count":1,"reads":241,"score":68.6,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Pekka Gaiser","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":null,"reply_to_user":{"username":"sam","avatar_template":"/user_avatar/meta.discourse.org/sam/{size}/5243.png","uploaded_avatar_id":5243},"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":4012,"name":"Marco Ceppi","username":"marcoceppi","avatar_template":"/user_avatar/meta.discourse.org/marcoceppi/{size}/6552.png","uploaded_avatar_id":6552,"created_at":"2013-02-07T21:22:46.376Z","cooked":"<p>If you use gettext format you could leverage <a href=\"https://translations.launchpad.net/\" rel=\"nofollow\">Launchpad</a> translations and the community behind it.</p>","post_number":20,"post_type":1,"updated_at":"2013-02-07T21:22:46.376Z","like_count":1,"reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":9,"incoming_link_count":2,"reads":244,"score":74.25,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Marco Ceppi","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"https://translations.launchpad.net/","internal":false,"reflection":false,"title":"Launchpad Translations","clicks":13}],"read":true,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":761,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false}],"stream":[398,419,1060,3623,3651,3654,3655,3658,3660,3667,3673,3675,3690,3925,3938,3982,3989,3996,4009,4012],"gaps":{"before":{"20706":[20125]},"after":{}}},"id":280,"title":"Internationalization / localization","fancy_title":"Internationalization / localization","posts_count":103,"created_at":"2013-02-05T21:29:00.174Z","views":5211,"reply_count":67,"participant_count":40,"like_count":135,"last_posted_at":"2015-03-04T15:07:10.487Z","visible":true,"closed":false,"archived":false,"has_summary":true,"archetype":"regular","slug":"internationalization-localization","category_id":2,"word_count":6198,"deleted_at":null,"draft":null,"draft_key":"topic_280","draft_sequence":4,"posted":true,"unpinned":null,"pinned_globally":false,"pinned":false,"pinned_at":null,"details":{"auto_close_at":null,"auto_close_hours":null,"auto_close_based_on_last_post":false,"created_by":{"id":255,"username":"uwe_keim","uploaded_avatar_id":5697,"avatar_template":"/user_avatar/meta.discourse.org/uwe_keim/{size}/5697.png"},"last_poster":{"id":14091,"username":"Luciano_Fantuzzi","uploaded_avatar_id":39484,"avatar_template":"/user_avatar/meta.discourse.org/luciano_fantuzzi/{size}/39484.png"},"participants":[{"id":212,"username":"alxndr","uploaded_avatar_id":5619,"avatar_template":"/user_avatar/meta.discourse.org/alxndr/{size}/5619.png","post_count":11},{"id":1,"username":"sam","uploaded_avatar_id":5243,"avatar_template":"/user_avatar/meta.discourse.org/sam/{size}/5243.png","post_count":11},{"id":7,"username":"pekka","uploaded_avatar_id":5253,"avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","post_count":8},{"id":461,"username":"kuba","uploaded_avatar_id":6049,"avatar_template":"/user_avatar/meta.discourse.org/kuba/{size}/6049.png","post_count":7},{"id":2995,"username":"tattoo","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/tattoo/{size}/3.png","post_count":6},{"id":2540,"username":"jgourdon","uploaded_avatar_id":9537,"avatar_template":"/user_avatar/meta.discourse.org/jgourdon/{size}/9537.png","post_count":5},{"id":1860,"username":"emk","uploaded_avatar_id":8400,"avatar_template":"/user_avatar/meta.discourse.org/emk/{size}/8400.png","post_count":4},{"id":1275,"username":"dacap","uploaded_avatar_id":7401,"avatar_template":"/user_avatar/meta.discourse.org/dacap/{size}/7401.png","post_count":4},{"id":19,"username":"eviltrout","uploaded_avatar_id":5275,"avatar_template":"/user_avatar/meta.discourse.org/eviltrout/{size}/5275.png","post_count":4},{"id":3704,"username":"mojzis","uploaded_avatar_id":31201,"avatar_template":"/user_avatar/meta.discourse.org/mojzis/{size}/31201.png","post_count":3},{"id":3190,"username":"gururea","uploaded_avatar_id":10663,"avatar_template":"/user_avatar/meta.discourse.org/gururea/{size}/10663.png","post_count":3},{"id":1895,"username":"maciek","uploaded_avatar_id":8463,"avatar_template":"/user_avatar/meta.discourse.org/maciek/{size}/8463.png","post_count":3},{"id":22,"username":"splattne","uploaded_avatar_id":5280,"avatar_template":"/user_avatar/meta.discourse.org/splattne/{size}/5280.png","post_count":2},{"id":1979,"username":"Superuser","uploaded_avatar_id":8604,"avatar_template":"/user_avatar/meta.discourse.org/superuser/{size}/8604.png","post_count":2},{"id":3818,"username":"Tudor","uploaded_avatar_id":11675,"avatar_template":"/user_avatar/meta.discourse.org/tudor/{size}/11675.png","post_count":2},{"id":32,"username":"codinghorror","uploaded_avatar_id":5297,"avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png","post_count":2},{"id":3620,"username":"potthast","uploaded_avatar_id":11363,"avatar_template":"/user_avatar/meta.discourse.org/potthast/{size}/11363.png","post_count":2},{"id":9,"username":"tms","uploaded_avatar_id":40181,"avatar_template":"/user_avatar/meta.discourse.org/tms/{size}/40181.png","post_count":2},{"id":14091,"username":"Luciano_Fantuzzi","uploaded_avatar_id":39484,"avatar_template":"/user_avatar/meta.discourse.org/luciano_fantuzzi/{size}/39484.png","post_count":1},{"id":255,"username":"uwe_keim","uploaded_avatar_id":5697,"avatar_template":"/user_avatar/meta.discourse.org/uwe_keim/{size}/5697.png","post_count":1},{"id":9006,"username":"berk","uploaded_avatar_id":19348,"avatar_template":"/user_avatar/meta.discourse.org/berk/{size}/19348.png","post_count":1},{"id":754,"username":"danneu","uploaded_avatar_id":6540,"avatar_template":"/user_avatar/meta.discourse.org/danneu/{size}/6540.png","post_count":1},{"id":761,"username":"marcoceppi","uploaded_avatar_id":6552,"avatar_template":"/user_avatar/meta.discourse.org/marcoceppi/{size}/6552.png","post_count":1},{"id":2753,"username":"mikl","uploaded_avatar_id":9918,"avatar_template":"/user_avatar/meta.discourse.org/mikl/{size}/9918.png","post_count":1}],"suggested_topics":[{"id":27331,"title":"Polls are still very buggy","fancy_title":"Polls are still very buggy","slug":"polls-are-still-very-buggy","posts_count":4,"reply_count":1,"highest_post_number":4,"image_url":"/uploads/default/_optimized/cd1/b8c/c162528887_690x401.png","created_at":"2015-04-08T09:51:00.357Z","last_posted_at":"2015-04-08T15:59:16.258Z","bumped":true,"bumped_at":"2015-04-08T16:05:09.842Z","unseen":false,"last_read_post_number":3,"unread":0,"new_posts":1,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"notification_level":2,"bookmarked":false,"liked":false,"archetype":"regular","like_count":11,"views":55,"category_id":1},{"id":27343,"title":"Mobile theme doesn't show last activity time for topics on category page","fancy_title":"Mobile theme doesn’t show last activity time for topics on category page","slug":"mobile-theme-doesnt-show-last-activity-time-for-topics-on-category-page","posts_count":4,"reply_count":2,"highest_post_number":4,"image_url":"/uploads/default/_optimized/13e/25c/bd30b466be_281x500.png","created_at":"2015-04-08T14:20:51.177Z","last_posted_at":"2015-04-08T15:40:30.037Z","bumped":true,"bumped_at":"2015-04-08T15:40:30.037Z","unseen":false,"last_read_post_number":2,"unread":0,"new_posts":2,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"notification_level":2,"bookmarked":false,"liked":false,"archetype":"regular","like_count":3,"views":23,"category_id":9},{"id":27346,"title":"Reply+{messagekey}@... optionaly in header \"from\" in addition to \"reply-to\"","fancy_title":"Reply+{messagekey}@… optionaly in header “from” in addition to “reply-to”","slug":"reply-messagekey-optionaly-in-header-from-in-addition-to-reply-to","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2015-04-08T16:05:13.103Z","last_posted_at":"2015-04-08T16:05:13.415Z","bumped":true,"bumped_at":"2015-04-08T16:05:13.415Z","unseen":true,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"archetype":"regular","like_count":0,"views":8,"category_id":2},{"id":19670,"title":"Parsing (Oneboxing) IMDB links","fancy_title":"Parsing (Oneboxing) IMDB links","slug":"parsing-oneboxing-imdb-links","posts_count":8,"reply_count":1,"highest_post_number":8,"image_url":null,"created_at":"2014-09-05T07:19:26.161Z","last_posted_at":"2015-04-07T09:21:21.570Z","bumped":true,"bumped_at":"2015-04-07T09:21:21.570Z","unseen":false,"last_read_post_number":8,"unread":0,"new_posts":0,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"notification_level":2,"bookmarked":false,"liked":false,"archetype":"regular","like_count":4,"views":253,"category_id":2},{"id":7512,"title":"Support for Piwik Analytics as an alternative to Google Analytics","fancy_title":"Support for Piwik Analytics as an alternative to Google Analytics","slug":"support-for-piwik-analytics-as-an-alternative-to-google-analytics","posts_count":53,"reply_count":41,"highest_post_number":65,"image_url":"/plugins/emoji/images/smile.png","created_at":"2013-06-16T01:32:30.596Z","last_posted_at":"2015-02-22T13:46:26.845Z","bumped":true,"bumped_at":"2015-02-22T13:46:26.845Z","unseen":false,"last_read_post_number":65,"unread":0,"new_posts":0,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"notification_level":2,"bookmarked":false,"liked":false,"archetype":"regular","like_count":62,"views":1877,"category_id":2},{"id":25480,"title":"CSS admin-contents reloaded","fancy_title":"CSS admin-contents reloaded","slug":"css-admin-contents-reloaded","posts_count":22,"reply_count":15,"highest_post_number":22,"image_url":null,"created_at":"2015-02-21T12:15:57.707Z","last_posted_at":"2015-03-02T23:24:18.899Z","bumped":true,"bumped_at":"2015-03-02T23:24:18.899Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"archetype":"regular","like_count":21,"views":185,"category_id":2},{"id":26576,"title":"Badge timestamp should be the time the badge was granted?","fancy_title":"Badge timestamp should be the time the badge was granted?","slug":"badge-timestamp-should-be-the-time-the-badge-was-granted","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":null,"created_at":"2015-03-20T13:22:08.266Z","last_posted_at":"2015-03-21T00:33:52.243Z","bumped":true,"bumped_at":"2015-03-21T00:33:52.243Z","unseen":false,"last_read_post_number":1,"unread":0,"new_posts":0,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"notification_level":1,"bookmarked":false,"liked":false,"archetype":"regular","like_count":9,"views":87,"category_id":2}],"links":[{"url":"https://github.com/discourse/discourse/blob/master/config/locales/en.yml","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":118,"user_id":9,"domain":"github.com"},{"url":"http://www.localeapp.com/","title":"Easy localization for Rails apps | Locale","fancy_title":null,"internal":false,"reflection":false,"clicks":69,"user_id":1860,"domain":"www.localeapp.com"},{"url":"http://stackoverflow.com/questions/4232922/why-do-people-use-plain-english-as-translation-placeholders","title":"internationalization - Why do people use plain english as translation placeholders? - Stack Overflow","fancy_title":null,"internal":false,"reflection":false,"clicks":63,"user_id":7,"domain":"stackoverflow.com"},{"url":"https://github.com/discourse/discourse/blob/master/config/locales","title":"discourse/config/locales at master · discourse/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":62,"user_id":32,"domain":"github.com"},{"url":"https://github.com/SlexAxton/messageformat.js","title":"SlexAxton/messageformat.js · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":46,"user_id":1,"domain":"github.com"},{"url":"http://www.localeapp.com/projects/1537/translations?utf8=%E2%9C%93&search=source_code","title":"langforums | Locale","fancy_title":null,"internal":false,"reflection":false,"clicks":25,"user_id":1860,"domain":"www.localeapp.com"},{"url":"https://translations.launchpad.net/","title":"Launchpad Translations","fancy_title":null,"internal":false,"reflection":false,"clicks":23,"user_id":761,"domain":"translations.launchpad.net"},{"url":"https://www.transifex.com/","title":"Transifex - Continuous Localization Platform","fancy_title":null,"internal":false,"reflection":false,"clicks":22,"user_id":1979,"domain":"www.transifex.com"},{"url":"https://github.com/berk/tr8n","title":"berk/tr8n · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":22,"user_id":1,"domain":"github.com"},{"url":"http://translate.wordpress.org/projects/bbpress/dev","title":"WordPress › Development < GlotPress","fancy_title":null,"internal":false,"reflection":false,"clicks":16,"user_id":7,"domain":"translate.wordpress.org"},{"url":"http://weblate.org","title":"Weblate - web-based translation","fancy_title":null,"internal":false,"reflection":false,"clicks":15,"user_id":2316,"domain":"weblate.org"},{"url":"https://github.com/discourse/discourse/tree/master/config/locales","title":"discourse/config/locales at master · discourse/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":14,"user_id":19,"domain":"github.com"},{"url":"https://github.com/discourse/discourse/pull/493","title":"Danish translation. by mikl · Pull Request #493 · discourse/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":12,"user_id":2753,"domain":"github.com"},{"url":"https://github.com/SlexAxton","title":"SlexAxton (Alex Sexton) · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":10,"user_id":1,"domain":"github.com"},{"url":"https://github.com/gururea/discourse/tree/master/config/locales","title":"discourse/config/locales at master · gururea/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":9,"user_id":3190,"domain":"github.com"},{"url":"https://github.com/discourse/discourse/blob/master/config/locales/client.en.yml#L691","title":"discourse/config/locales/client.en.yml at master · discourse/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":7,"user_id":461,"domain":"github.com"},{"url":"https://github.com/dacap/discourse/tree/spanish","title":"dacap/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":7,"user_id":1275,"domain":"github.com"},{"url":"https://github.com/discourse/discourse/blob/master/config/locales/client.nl.yml","title":"discourse/config/locales/client.nl.yml at master · discourse/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":7,"user_id":461,"domain":"github.com"},{"url":"https://github.com/discourse/discourse/commit/c5761eae8afe37e20cec0d0f9d14b85b6e585bda","title":"Support for Simplified Chinese thanks to tangramor · c5761ea · discourse/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":6,"user_id":212,"domain":"github.com"},{"url":"http://tr8n.github.com/","title":"tr8n","fancy_title":null,"internal":false,"reflection":false,"clicks":6,"user_id":212,"domain":"tr8n.github.com"},{"url":"http://www.getlocalization.com/","title":"Crowdsourced, Social and Collaborative App & Website Translation - Get Localization","fancy_title":null,"internal":false,"reflection":false,"clicks":6,"user_id":22,"domain":"www.getlocalization.com"},{"url":"http://blog.discourse.org/2013/04/discourse-as-your-first-rails-app/","title":"Discourse as Your First Rails App","fancy_title":null,"internal":false,"reflection":false,"clicks":5,"user_id":1995,"domain":"blog.discourse.org"},{"url":"https://github.com/alxndr/discourse/blob/i18n-chinese/config/locales/server.zh.yml","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":5,"user_id":212,"domain":"github.com"},{"url":"http://translate.sourceforge.net/wiki/virtaal/index","title":"Easy-to-use and powerful offline translation tool | Virtaal","fancy_title":null,"internal":false,"reflection":false,"clicks":4,"user_id":1979,"domain":"translate.sourceforge.net"},{"url":"https://poeditor.com/","title":"POEditor - online software localization tool","fancy_title":null,"internal":false,"reflection":false,"clicks":4,"user_id":1979,"domain":"poeditor.com"},{"url":"http://en.lichess.org/@/Hellball","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":3,"user_id":1979,"domain":"en.lichess.org"},{"url":"http://en.wikipedia.org/wiki/T%E2%80%93V_distinction","title":"T–V distinction - Wikipedia, the free encyclopedia","fancy_title":null,"internal":false,"reflection":false,"clicks":3,"user_id":3620,"domain":"en.wikipedia.org"},{"url":"http://www.slideshare.net/HeatherRivers/linguistic-potluck-crowdsourcing-localization-with-rails","title":"Linguistic Potluck: Crowdsourcing localization with Rails","fancy_title":null,"internal":false,"reflection":false,"clicks":3,"user_id":212,"domain":"www.slideshare.net"},{"url":"https://meta.discourse.org/t/language-mirrors/2378/2","title":"Language mirrors","fancy_title":null,"internal":true,"reflection":true,"clicks":3,"user_id":32,"domain":"meta.discourse.org"},{"url":"http://www.madanalogy.com/2012/06/rails-i18n-translations-in-yaml.html","title":"Mad Analogy: Rails i18n translations in Yaml: translation tool support","fancy_title":null,"internal":false,"reflection":false,"clicks":3,"user_id":3190,"domain":"www.madanalogy.com"},{"url":"https://github.com/tr8n","title":"Translation Exchange · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":3,"user_id":9006,"domain":"github.com"},{"url":"http://pootle.locamotion.org/","title":"Main | Pootle Demo","fancy_title":null,"internal":false,"reflection":false,"clicks":2,"user_id":3190,"domain":"pootle.locamotion.org"},{"url":"http://www.youtube.com/watch?v=MqqdzJ98q7s","title":"GoGaRuCo 2012 - Linguistic Potluck: Crowdsourcing Localization in Rails by Heather Rivers - YouTube","fancy_title":null,"internal":false,"reflection":false,"clicks":2,"user_id":212,"domain":"www.youtube.com"},{"url":"https://meta.discourse.org/t/translation-workflow/6102","title":"Translation workflow","fancy_title":null,"internal":true,"reflection":true,"clicks":2,"user_id":4702,"domain":"meta.discourse.org"},{"url":"https://meta.discourse.org/t/solving-xda-developer-style-forums/4368/4","title":"Solving XDA-Developer style forums","fancy_title":null,"internal":true,"reflection":true,"clicks":2,"user_id":639,"domain":"meta.discourse.org"},{"url":"https://tr8nhub.com","title":"TranslationExchange","fancy_title":null,"internal":false,"reflection":false,"clicks":2,"user_id":9006,"domain":"tr8nhub.com"},{"url":"https://meta.discourse.org/t/roadplan-for-discourse/2939/3","title":"Roadplan for Discourse 2013","fancy_title":null,"internal":true,"reflection":true,"clicks":1,"user_id":2540,"domain":"meta.discourse.org"},{"url":"http://sugarjs.com/dates#date_locales","title":"Dates - Sugar","fancy_title":null,"internal":false,"reflection":false,"clicks":1,"user_id":461,"domain":"sugarjs.com"},{"url":"http://blog.discourse.org/2013/03/localizing-discourse/","title":"Localizing Discourse","fancy_title":null,"internal":false,"reflection":false,"clicks":1,"user_id":893,"domain":"blog.discourse.org"},{"url":"https://github.com/discourse/discourse/blob/master/app/assets/javascripts/locales/date_locales.js","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":1,"user_id":461,"domain":"github.com"},{"url":"http://transifex.com/projects/p/discourse-pt-br/","title":"Discourse-Translations-Project localization","fancy_title":null,"internal":false,"reflection":false,"clicks":1,"user_id":893,"domain":"transifex.com"},{"url":"https://github.com/discourse/discourse/issues/279","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":1,"user_id":893,"domain":"github.com"},{"url":"https://meta.discourse.org/t/comrades-lets-join-our-efforts-on-ukrainian-and-russian-translations/4403/5","title":"Comrades let's join our efforts on ukrainian and russian translations","fancy_title":null,"internal":true,"reflection":true,"clicks":1,"user_id":3417,"domain":"meta.discourse.org"},{"url":"https://meta.discourse.org/t/translation-workflow/6102/6","title":"Translation workflow","fancy_title":null,"internal":true,"reflection":false,"clicks":0,"user_id":1995,"domain":"meta.discourse.org"},{"url":"https://meta.discourse.org/t/bookmark-last-read-sometimes-doesn-t-go-to-the-end-of-a-topic/4825/9","title":"Bookmark/last read sometimes doesn't go to the end of a topic","fancy_title":null,"internal":true,"reflection":true,"clicks":0,"user_id":3681,"domain":"meta.discourse.org"},{"url":"https://github.com/discourse/discourse/blob/master/config/locales/client.de.yml","title":"discourse/config/locales/client.de.yml at master · discourse/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":0,"user_id":2,"domain":"github.com"},{"url":"https://meta.discourse.org/t/what-i-love-about-wordpress-plugins/5697","title":"What I love about WordPress plugins","fancy_title":null,"internal":true,"reflection":true,"clicks":0,"user_id":1,"domain":"meta.discourse.org"},{"url":"https://meta.discourse.org/t/github-onebox-rendering-issue/7616","title":"GitHub OneBox Rendering Issue","fancy_title":null,"internal":true,"reflection":true,"clicks":0,"user_id":5372,"domain":"meta.discourse.org"},{"url":"https://github.com/discourse/discourse/blob/master/config/locales/server.de.yml","title":"discourse/config/locales/server.de.yml at master · discourse/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":0,"user_id":2,"domain":"github.com"},{"url":"https://meta.discourse.org/t/roadplan-for-discourse/2939/5","title":"Roadplan for Discourse 2013","fancy_title":null,"internal":true,"reflection":true,"clicks":0,"user_id":32,"domain":"meta.discourse.org"},{"url":"https://meta.discourse.org/t/translation-tools-transifex-localeapp/7763","title":"Translation Tools: Transifex? Localeapp?","fancy_title":null,"internal":true,"reflection":true,"clicks":0,"user_id":2,"domain":"meta.discourse.org"},{"url":"http://guides.rubyonrails.org/i18n.html#the-public-i18n-api","title":"Rails Internationalization (I18n) API — Ruby on Rails Guides","fancy_title":null,"internal":false,"reflection":false,"clicks":0,"user_id":1895,"domain":"guides.rubyonrails.org"},{"url":"https://meta.discourse.org/t/hi-support-chinese/4393/6","title":"Hi, support Chinese?","fancy_title":null,"internal":true,"reflection":true,"clicks":0,"user_id":2014,"domain":"meta.discourse.org"},{"url":"https://meta.discourse.org/t/translation-tools-transifex-localeapp/7763/41","title":"Translation Tools: Transifex? Localeapp?","fancy_title":null,"internal":true,"reflection":false,"clicks":0,"user_id":6626,"domain":"meta.discourse.org"}],"notification_level":2,"notifications_reason_id":4,"can_move_posts":true,"can_edit":true,"can_delete":true,"can_recover":true,"can_remove_allowed_users":true,"can_invite_to":true,"can_create_post":true,"can_reply_as_new_topic":true,"can_flag_topic":true},"highest_post_number":104,"last_read_post_number":104,"deleted_by":null,"has_deleted":true,"actions_summary":[{"id":4,"count":0,"hidden":false,"can_act":true},{"id":7,"count":0,"hidden":false,"can_act":true},{"id":8,"count":0,"hidden":false,"can_act":true}],"chunk_size":20,"bookmarked":false,"tags":null}}; diff --git a/test/javascripts/helpers/create-pretender.js.es6 b/test/javascripts/helpers/create-pretender.js.es6 index cc51c3db0..e6184eee2 100644 --- a/test/javascripts/helpers/create-pretender.js.es6 +++ b/test/javascripts/helpers/create-pretender.js.es6 @@ -2,7 +2,7 @@ function parsePostData(query) { const result = {}; query.split("&").forEach(function(part) { const item = part.split("="); - result[item[0]] = decodeURIComponent(item[1]); + result[item[0]] = decodeURIComponent(item[1]).replace(/\+/g, ' '); }); return result; } @@ -33,9 +33,16 @@ const _moreWidgets = [ {id: 224, name: 'Good Repellant'} ]; +function loggedIn() { + return !!Discourse.User.current(); +} + export default function() { + const server = new Pretender(function() { + const fixturesByUrl = {}; + // Load any fixtures automatically const self = this; Ember.keys(require._eak_seen).forEach(function(entry) { @@ -44,6 +51,7 @@ export default function() { if (fixture && fixture.default) { const obj = fixture.default; Ember.keys(obj).forEach(function(url) { + fixturesByUrl[url] = obj[url]; self.get(url, function() { return response(obj[url]); }); @@ -52,6 +60,20 @@ export default function() { } }); + this.get('/composer-messages', () => { return response([]); }); + + this.get("/latest.json", () => { + const json = fixturesByUrl['/latest.json']; + + if (loggedIn()) { + // Stuff to let us post + json.topic_list.can_create_topic = true; + json.topic_list.draft_key = "new_topic"; + json.topic_list.draft_sequence = 1; + } + return response(json); + }); + this.get("/t/id_for/:slug", function() { return response({id: 280, slug: "internationalization-localization", url: "/t/internationalization-localization/280"}); }); @@ -99,6 +121,33 @@ export default function() { this.delete('/posts/:post_id', success); this.put('/posts/:post_id/recover', success); + this.put('/posts/:post_id', (request) => { + return response({ post: {id: request.params.post_id, version: 2 } }); + }); + + this.put('/t/:slug/:id', (request) => { + const data = parsePostData(request.requestBody); + + return response(200, { basic_topic: {id: request.params.id, + title: data.title, + fancy_title: data.title, + slug: request.params.slug } }) + }); + + this.post('/posts', function(request) { + const data = parsePostData(request.requestBody); + + if (data.title === "this title triggers an error") { + return response(422, {errors: ['That title has already been taken']}); + } else { + return response(200, { + success: true, + action: 'create_post', + post: {id: 12345, topic_id: 280, topic_slug: 'internationalization-localization'} + }); + } + }); + this.get('/widgets/:widget_id', function(request) { const w = _widgets.findBy('id', parseInt(request.params.widget_id)); if (w) { @@ -130,8 +179,11 @@ export default function() { }); this.delete('/widgets/:widget_id', success); - }); + this.post('/topics/timings', function() { + return response(200, {}); + }); + }); server.prepareBody = function(body){ if (body && typeof body === "object") { diff --git a/test/javascripts/helpers/qunit-helpers.js.es6 b/test/javascripts/helpers/qunit-helpers.js.es6 index 17b82bd17..054a4686a 100644 --- a/test/javascripts/helpers/qunit-helpers.js.es6 +++ b/test/javascripts/helpers/qunit-helpers.js.es6 @@ -1,20 +1,59 @@ /* global asyncTest */ +import sessionFixtures from 'fixtures/session-fixtures'; import siteFixtures from 'fixtures/site_fixtures'; +import HeaderView from 'discourse/views/header'; + +function currentUser() { + return Discourse.User.create(sessionFixtures['/session/current.json'].current_user); +} + +function logIn() { + Discourse.User.resetCurrent(currentUser()); +} + +const Plugin = $.fn.modal; +const Modal = Plugin.Constructor; + +function AcceptanceModal(option, _relatedTarget) { + return this.each(function () { + var $this = $(this); + var data = $this.data('bs.modal'); + var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option === 'object' && option); + + if (!data) $this.data('bs.modal', (data = new Modal(this, options))); + data.$body = $('#ember-testing'); + + if (typeof option === 'string') data[option](_relatedTarget); + else if (options.show) data.show(_relatedTarget); + }); +} + +window.bootbox.$body = $('#ember-testing'); +$.fn.modal = AcceptanceModal; + +var oldAvatar = Discourse.Utilities.avatarImg; function acceptance(name, options) { module("Acceptance: " + name, { setup: function() { Ember.run(Discourse, Discourse.advanceReadiness); + // Don't render avatars in acceptance tests, it's faster and no 404s + Discourse.Utilities.avatarImg = () => ""; + + // For now don't do scrolling stuff in Test Mode + Ember.CloakedCollectionView.scrolled = Ember.K; + HeaderView.reopen({examineDockHeader: Ember.K}); + var siteJson = siteFixtures['site.json'].site; if (options) { if (options.setup) { options.setup.call(this); } - if (options.user) { - Discourse.User.resetCurrent(Discourse.User.create(options.user)); + if (options.loggedIn) { + logIn(); } if (options.settings) { @@ -34,6 +73,7 @@ function acceptance(name, options) { options.teardown.call(this); } + Discourse.Utilities.avatarImg = oldAvatar; Discourse.reset(); } }); @@ -61,4 +101,4 @@ function fixture(selector) { return $("#qunit-fixture"); } -export { acceptance, controllerFor, asyncTestDiscourse, fixture }; +export { acceptance, controllerFor, asyncTestDiscourse, fixture, logIn, currentUser }; diff --git a/test/javascripts/models/composer-test.js.es6 b/test/javascripts/models/composer-test.js.es6 index 413156077..1727d2893 100644 --- a/test/javascripts/models/composer-test.js.es6 +++ b/test/javascripts/models/composer-test.js.es6 @@ -1,16 +1,18 @@ -module("Discourse.Composer", { - setup: function() { - sandbox.stub(Discourse.User, 'currentProp').withArgs('admin').returns(false); - }, +import { currentUser } from 'helpers/qunit-helpers'; - teardown: function() { - Discourse.User.currentProp.restore(); - } -}); +module("model:composer"); + +function createComposer(opts) { + opts = opts || {}; + opts.user = opts.user || currentUser(); + opts.site = Discourse.Site.current(); + opts.siteSettings = Discourse.SiteSettings; + return Discourse.Composer.create(opts); +} test('replyLength', function() { - var replyLength = function(val, expectedLength) { - var composer = Discourse.Composer.create({ reply: val }); + const replyLength = function(val, expectedLength) { + const composer = createComposer({ reply: val }); equal(composer.get('replyLength'), expectedLength); }; @@ -23,8 +25,8 @@ test('replyLength', function() { test('missingReplyCharacters', function() { Discourse.SiteSettings.min_first_post_length = 40; - var missingReplyCharacters = function(val, isPM, isFirstPost, expected, message) { - var composer = Discourse.Composer.create({ reply: val, creatingPrivateMessage: isPM, creatingTopic: isFirstPost }); + const missingReplyCharacters = function(val, isPM, isFirstPost, expected, message) { + const composer = createComposer({ reply: val, creatingPrivateMessage: isPM, creatingTopic: isFirstPost }); equal(composer.get('missingReplyCharacters'), expected, message); }; @@ -34,8 +36,8 @@ test('missingReplyCharacters', function() { }); test('missingTitleCharacters', function() { - var missingTitleCharacters = function(val, isPM, expected, message) { - var composer = Discourse.Composer.create({ title: val, creatingPrivateMessage: isPM }); + const missingTitleCharacters = function(val, isPM, expected, message) { + const composer = createComposer({ title: val, creatingPrivateMessage: isPM }); equal(composer.get('missingTitleCharacters'), expected, message); }; @@ -44,7 +46,7 @@ test('missingTitleCharacters', function() { }); test('replyDirty', function() { - var composer = Discourse.Composer.create(); + const composer = createComposer(); ok(!composer.get('replyDirty'), "by default it's false"); composer.setProperties({ @@ -58,7 +60,7 @@ test('replyDirty', function() { }); test("appendText", function() { - var composer = Discourse.Composer.create(); + const composer = createComposer(); blank(composer.get('reply'), "the reply is blank by default"); @@ -89,7 +91,7 @@ test("appendText", function() { test("Title length for regular topics", function() { Discourse.SiteSettings.min_topic_title_length = 5; Discourse.SiteSettings.max_topic_title_length = 10; - var composer = Discourse.Composer.create(); + const composer = createComposer(); composer.set('title', 'asdf'); ok(!composer.get('titleLengthValid'), "short titles are not valid"); @@ -104,7 +106,7 @@ test("Title length for regular topics", function() { test("Title length for private messages", function() { Discourse.SiteSettings.min_private_message_title_length = 5; Discourse.SiteSettings.max_topic_title_length = 10; - var composer = Discourse.Composer.create({action: Discourse.Composer.PRIVATE_MESSAGE}); + const composer = createComposer({action: Discourse.Composer.PRIVATE_MESSAGE}); composer.set('title', 'asdf'); ok(!composer.get('titleLengthValid'), "short titles are not valid"); @@ -119,7 +121,7 @@ test("Title length for private messages", function() { test("Title length for private messages", function() { Discourse.SiteSettings.min_private_message_title_length = 5; Discourse.SiteSettings.max_topic_title_length = 10; - var composer = Discourse.Composer.create({action: Discourse.Composer.PRIVATE_MESSAGE}); + const composer = createComposer({action: Discourse.Composer.PRIVATE_MESSAGE}); composer.set('title', 'asdf'); ok(!composer.get('titleLengthValid'), "short titles are not valid"); @@ -132,10 +134,10 @@ test("Title length for private messages", function() { }); test('editingFirstPost', function() { - var composer = Discourse.Composer.create(); + const composer = createComposer(); ok(!composer.get('editingFirstPost'), "it's false by default"); - var post = Discourse.Post.create({id: 123, post_number: 2}); + const post = Discourse.Post.create({id: 123, post_number: 2}); composer.setProperties({post: post, action: Discourse.Composer.EDIT }); ok(!composer.get('editingFirstPost'), "it's false when not editing the first post"); @@ -145,7 +147,7 @@ test('editingFirstPost', function() { }); test('clearState', function() { - var composer = Discourse.Composer.create({ + const composer = createComposer({ originalText: 'asdf', reply: 'asdf2', post: Discourse.Post.create({id: 1}), @@ -163,61 +165,48 @@ test('clearState', function() { test('initial category when uncategorized is allowed', function() { Discourse.SiteSettings.allow_uncategorized_topics = true; - var composer = Discourse.Composer.open({action: 'createTopic', draftKey: 'asfd', draftSequence: 1}); + const composer = Discourse.Composer.open({action: 'createTopic', draftKey: 'asfd', draftSequence: 1}); equal(composer.get('categoryId'),undefined,"Uncategorized by default"); }); test('initial category when uncategorized is not allowed', function() { Discourse.SiteSettings.allow_uncategorized_topics = false; - var composer = Discourse.Composer.open({action: 'createTopic', draftKey: 'asfd', draftSequence: 1}); + const composer = Discourse.Composer.open({action: 'createTopic', draftKey: 'asfd', draftSequence: 1}); ok(composer.get('categoryId') === undefined, "Uncategorized by default. Must choose a category."); }); test('showPreview', function() { - var new_composer = function() { + const newComposer = function() { return Discourse.Composer.open({action: 'createTopic', draftKey: 'asfd', draftSequence: 1}); }; Discourse.Mobile.mobileView = true; - equal(new_composer().get('showPreview'), false, "Don't show preview in mobile view"); + equal(newComposer().get('showPreview'), false, "Don't show preview in mobile view"); Discourse.KeyValueStore.set({ key: 'composer.showPreview', value: 'true' }); - equal(new_composer().get('showPreview'), false, "Don't show preview in mobile view even if KeyValueStore wants to"); + equal(newComposer().get('showPreview'), false, "Don't show preview in mobile view even if KeyValueStore wants to"); Discourse.KeyValueStore.remove('composer.showPreview'); Discourse.Mobile.mobileView = false; - equal(new_composer().get('showPreview'), true, "Show preview by default in desktop view"); + equal(newComposer().get('showPreview'), true, "Show preview by default in desktop view"); }); test('open with a quote', function() { - var quote = '[quote="neil, post:5, topic:413"]\nSimmer down you two.\n[/quote]'; - var new_composer = function() { + const quote = '[quote="neil, post:5, topic:413"]\nSimmer down you two.\n[/quote]'; + const newComposer = function() { return Discourse.Composer.open({action: Discourse.Composer.REPLY, draftKey: 'asfd', draftSequence: 1, quote: quote}); }; - equal(new_composer().get('originalText'), quote, "originalText is the quote" ); - equal(new_composer().get('replyDirty'), false, "replyDirty is initally false with a quote" ); -}); - - -module("Discourse.Composer as admin", { - setup: function() { - Discourse.SiteSettings.min_topic_title_length = 5; - Discourse.SiteSettings.max_topic_title_length = 10; - sandbox.stub(Discourse.User, 'currentProp').withArgs('admin').returns(true); - }, - - teardown: function() { - Discourse.SiteSettings.min_topic_title_length = 15; - Discourse.SiteSettings.max_topic_title_length = 255; - Discourse.User.currentProp.restore(); - } + equal(newComposer().get('originalText'), quote, "originalText is the quote" ); + equal(newComposer().get('replyDirty'), false, "replyDirty is initally false with a quote" ); }); test("Title length for static page topics as admin", function() { - var composer = Discourse.Composer.create(); + Discourse.SiteSettings.min_topic_title_length = 5; + Discourse.SiteSettings.max_topic_title_length = 10; + const composer = createComposer(); - var post = Discourse.Post.create({id: 123, post_number: 2, static_doc: true}); + const post = Discourse.Post.create({id: 123, post_number: 2, static_doc: true}); composer.setProperties({post: post, action: Discourse.Composer.EDIT }); composer.set('title', 'asdf'); diff --git a/test/javascripts/test_helper.js b/test/javascripts/test_helper.js index 87c2812f0..bc38f15d1 100644 --- a/test/javascripts/test_helper.js +++ b/test/javascripts/test_helper.js @@ -59,7 +59,11 @@ sinon.config = { useFakeServer: false }; -window.assetPath = function() { return null; }; +window.assetPath = function(url) { + if (url.indexOf('defer') === 0) { + return "/assets/" + url; + } +}; // Stop the message bus so we don't get ajax calls window.MessageBus.stop(); diff --git a/vendor/assets/javascripts/bootbox.js b/vendor/assets/javascripts/bootbox.js index d19f1557c..69ec027ef 100644 --- a/vendor/assets/javascripts/bootbox.js +++ b/vendor/assets/javascripts/bootbox.js @@ -427,16 +427,17 @@ var bootbox = window.bootbox || (function(document, $) { }); // well, *if* we have a primary - give the first dom element focus - div.on('shown', function() { + div.on('shown.bs.modal', function() { div.find("a.btn-primary:first").focus(); }); - div.on('hidden', function() { + div.on('hidden.bs.modal', function() { div.remove(); }); // wire up button handlers div.on('click', '.modal-footer a', function(e) { + Ember.run(function() { var handler = $(this).data("handler"), cb = callbacks[handler], @@ -462,10 +463,11 @@ var bootbox = window.bootbox || (function(document, $) { if (hideModal !== false) { div.modal("hide"); } + }); }); // stick the modal right at the bottom of the main body out of the way - $("body").append(div); + (that.$body || $("body")).append(div); div.modal({ // unless explicitly overridden take whatever our default backdrop value is diff --git a/vendor/assets/javascripts/bootstrap-modal.js b/vendor/assets/javascripts/bootstrap-modal.js index 0465cb525..1dd9c0df9 100644 --- a/vendor/assets/javascripts/bootstrap-modal.js +++ b/vendor/assets/javascripts/bootstrap-modal.js @@ -1,218 +1,338 @@ -/* ========================================================= - * bootstrap-modal.js v2.0.3 - * http://twitter.github.com/bootstrap/javascript.html#modals - * ========================================================= - * Copyright 2012 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================= */ +/* ======================================================================== + * Bootstrap: modal.js v3.3.4 + * http://getbootstrap.com/javascript/#modals + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ -!function ($) { ++function ($) { + 'use strict'; - "use strict"; // jshint ;_; + // MODAL CLASS DEFINITION + // ====================== + var Modal = function (element, options) { + this.options = options + this.$body = $(document.body) + this.$element = $(element) + this.$dialog = this.$element.find('.modal-dialog') + this.$backdrop = null + this.isShown = null + this.originalBodyPad = null + this.scrollbarWidth = 0 + this.ignoreBackdropClick = false - /* MODAL CLASS DEFINITION - * ====================== */ - - var Modal = function (content, options) { - this.options = options - this.$element = $(content) - .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) + if (this.options.remote) { + this.$element + .find('.modal-content') + .load(this.options.remote, $.proxy(function () { + this.$element.trigger('loaded.bs.modal') + }, this)) + } } - Modal.prototype = { + Modal.VERSION = '3.3.4' - constructor: Modal - - , toggle: function () { - return this[!this.isShown ? 'show' : 'hide']() - } - - , show: function () { - var that = this - , e = $.Event('show') - - this.$element.trigger(e) - - if (this.isShown || e.isDefaultPrevented()) return - - $('body').addClass('modal-open') - - this.isShown = true - - escape.call(this) - backdrop.call(this, function () { - var transition = $.support.transition && that.$element.hasClass('fade') - - if (!that.$element.parent().length) { - that.$element.appendTo(document.body) //don't move modals dom position - } - - that.$element - .show() - - if (transition) { - that.$element[0].offsetWidth // force reflow - } - - that.$element.addClass('in') - - transition ? - that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) : - that.$element.trigger('shown') - - }) - } - - , hide: function (e) { - e && e.preventDefault() - - var that = this - - e = $.Event('hide') - - this.$element.trigger(e) - - if (!this.isShown || e.isDefaultPrevented()) return - - this.isShown = false - - $('body').removeClass('modal-open') - - escape.call(this) - - this.$element.removeClass('in') - - $.support.transition && this.$element.hasClass('fade') ? - hideWithTransition.call(this) : - hideModal.call(this) - } + Modal.TRANSITION_DURATION = 300 + Modal.BACKDROP_TRANSITION_DURATION = 150 + Modal.DEFAULTS = { + backdrop: true, + keyboard: true, + show: true } + Modal.prototype.toggle = function (_relatedTarget) { + return this.isShown ? this.hide() : this.show(_relatedTarget) + } - /* MODAL PRIVATE METHODS - * ===================== */ - - function hideWithTransition() { + Modal.prototype.show = function (_relatedTarget) { var that = this - , timeout = setTimeout(function () { - that.$element.off($.support.transition.end) - hideModal.call(that) - }, 500) + var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget }) - this.$element.one($.support.transition.end, function () { - clearTimeout(timeout) - hideModal.call(that) + this.$element.trigger(e) + + if (this.isShown || e.isDefaultPrevented()) return + + this.isShown = true + + this.checkScrollbar() + this.setScrollbar() + this.$body.addClass('modal-open') + + this.escape() + this.resize() + + this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) + + this.$dialog.on('mousedown.dismiss.bs.modal', function () { + that.$element.one('mouseup.dismiss.bs.modal', function (e) { + if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true + }) + }) + + this.backdrop(function () { + var transition = $.support.transition && that.$element.hasClass('fade') + + if (!that.$element.parent().length) { + that.$element.appendTo(that.$body) // don't move modals dom position + } + + that.$element + .show() + .scrollTop(0) + + that.adjustDialog() + + if (transition) { + that.$element[0].offsetWidth // force reflow + } + + that.$element.addClass('in') + + that.enforceFocus() + + var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget }) + + transition ? + that.$dialog // wait for modal to slide in + .one('bsTransitionEnd', function () { + that.$element.trigger('focus').trigger(e) + }) + .emulateTransitionEnd(Modal.TRANSITION_DURATION) : + that.$element.trigger('focus').trigger(e) }) } - function hideModal(that) { - this.$element - .hide() - .trigger('hidden') + Modal.prototype.hide = function (e) { + if (e) e.preventDefault() - backdrop.call(this) + e = $.Event('hide.bs.modal') + + this.$element.trigger(e) + + if (!this.isShown || e.isDefaultPrevented()) return + + this.isShown = false + + this.escape() + this.resize() + + $(document).off('focusin.bs.modal') + + this.$element + .removeClass('in') + .off('click.dismiss.bs.modal') + .off('mouseup.dismiss.bs.modal') + + this.$dialog.off('mousedown.dismiss.bs.modal') + + $.support.transition && this.$element.hasClass('fade') ? + this.$element + .one('bsTransitionEnd', $.proxy(this.hideModal, this)) + .emulateTransitionEnd(Modal.TRANSITION_DURATION) : + this.hideModal() } - function backdrop(callback) { + Modal.prototype.enforceFocus = function () { + $(document) + .off('focusin.bs.modal') // guard against infinite focus loop + .on('focusin.bs.modal', $.proxy(function (e) { + if (this.$element[0] !== e.target && !this.$element.has(e.target).length) { + this.$element.trigger('focus') + } + }, this)) + } + + Modal.prototype.escape = function () { + if (this.isShown && this.options.keyboard) { + this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) { + e.which == 27 && this.hide() + }, this)) + } else if (!this.isShown) { + this.$element.off('keydown.dismiss.bs.modal') + } + } + + Modal.prototype.resize = function () { + if (this.isShown) { + $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this)) + } else { + $(window).off('resize.bs.modal') + } + } + + Modal.prototype.hideModal = function () { var that = this - , animate = this.$element.hasClass('fade') ? 'fade' : '' + this.$element.hide() + this.backdrop(function () { + that.$body.removeClass('modal-open') + that.resetAdjustments() + that.resetScrollbar() + that.$element.trigger('hidden.bs.modal') + }) + } + + Modal.prototype.removeBackdrop = function () { + this.$backdrop && this.$backdrop.remove() + this.$backdrop = null + } + + Modal.prototype.backdrop = function (callback) { + var that = this + var animate = this.$element.hasClass('fade') ? 'fade' : '' if (this.isShown && this.options.backdrop) { var doAnimate = $.support.transition && animate - this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />') - .appendTo(document.body) + this.$backdrop = $(document.createElement('div')) + .addClass('modal-backdrop ' + animate) + .appendTo(this.$body) - if (this.options.backdrop != 'static') { - this.$backdrop.click($.proxy(this.hide, this)) - } + this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) { + if (this.ignoreBackdropClick) { + this.ignoreBackdropClick = false + return + } + if (e.target !== e.currentTarget) return + this.options.backdrop == 'static' + ? this.$element[0].focus() + : this.hide() + }, this)) if (doAnimate) this.$backdrop[0].offsetWidth // force reflow this.$backdrop.addClass('in') + if (!callback) return + doAnimate ? - this.$backdrop.one($.support.transition.end, callback) : + this.$backdrop + .one('bsTransitionEnd', callback) + .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : callback() } else if (!this.isShown && this.$backdrop) { this.$backdrop.removeClass('in') - $.support.transition && this.$element.hasClass('fade')? - this.$backdrop.one($.support.transition.end, $.proxy(removeBackdrop, this)) : - removeBackdrop.call(this) + var callbackRemove = function () { + that.removeBackdrop() + callback && callback() + } + $.support.transition && this.$element.hasClass('fade') ? + this.$backdrop + .one('bsTransitionEnd', callbackRemove) + .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : + callbackRemove() } else if (callback) { callback() } } - function removeBackdrop() { - this.$backdrop.remove() - this.$backdrop = null + // these following methods are used to handle overflowing modals + + Modal.prototype.handleUpdate = function () { + this.adjustDialog() } - function escape() { - var that = this - if (this.isShown && this.options.keyboard) { - $(document).on('keyup.dismiss.modal', function ( e ) { - e.which == 27 && that.hide() - }) - } else if (!this.isShown) { - $(document).off('keyup.dismiss.modal') - } - } + Modal.prototype.adjustDialog = function () { + var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight - - /* MODAL PLUGIN DEFINITION - * ======================= */ - - $.fn.modal = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('modal') - , options = $.extend({}, $.fn.modal.defaults, $this.data(), typeof option == 'object' && option) - if (!data) $this.data('modal', (data = new Modal(this, options))) - if (typeof option == 'string') data[option]() - else if (options.show) data.show() + this.$element.css({ + paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '', + paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : '' }) } - $.fn.modal.defaults = { - backdrop: true - , keyboard: true - , show: true + Modal.prototype.resetAdjustments = function () { + this.$element.css({ + paddingLeft: '', + paddingRight: '' + }) } + Modal.prototype.checkScrollbar = function () { + var fullWindowWidth = window.innerWidth + if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8 + var documentElementRect = document.documentElement.getBoundingClientRect() + fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left) + } + this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth + this.scrollbarWidth = this.measureScrollbar() + } + + Modal.prototype.setScrollbar = function () { + var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10) + this.originalBodyPad = document.body.style.paddingRight || '' + if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth) + } + + Modal.prototype.resetScrollbar = function () { + this.$body.css('padding-right', this.originalBodyPad) + } + + Modal.prototype.measureScrollbar = function () { // thx walsh + var scrollDiv = document.createElement('div') + scrollDiv.className = 'modal-scrollbar-measure' + this.$body.append(scrollDiv) + var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth + this.$body[0].removeChild(scrollDiv) + return scrollbarWidth + } + + + // MODAL PLUGIN DEFINITION + // ======================= + + function Plugin(option, _relatedTarget) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.modal') + var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) + + if (!data) $this.data('bs.modal', (data = new Modal(this, options))) + + if (typeof option == 'string') data[option](_relatedTarget) + else if (options.show) data.show(_relatedTarget) + }) + } + + var old = $.fn.modal + + $.fn.modal = Plugin $.fn.modal.Constructor = Modal - /* MODAL DATA-API - * ============== */ + // MODAL NO CONFLICT + // ================= - $(function () { - $('body').on('click.modal.data-api', '[data-toggle="modal"]', function ( e ) { - var $this = $(this), href - , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 - , option = $target.data('modal') ? 'toggle' : $.extend({}, $target.data(), $this.data()) + $.fn.modal.noConflict = function () { + $.fn.modal = old + return this + } - e.preventDefault() - $target.modal(option) + + // MODAL DATA-API + // ============== + + $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { + var $this = $(this) + var href = $this.attr('href') + var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7 + var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) + + if ($this.is('a')) e.preventDefault() + + $target.one('show.bs.modal', function (showEvent) { + if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown + $target.one('hidden.bs.modal', function () { + $this.is(':visible') && $this.trigger('focus') + }) }) + Plugin.call($target, option, this) }) -}(window.jQuery); +}(jQuery); diff --git a/vendor/assets/javascripts/ember-cloaking.js b/vendor/assets/javascripts/ember-cloaking.js index cc698f39d..e47030abc 100644 --- a/vendor/assets/javascripts/ember-cloaking.js +++ b/vendor/assets/javascripts/ember-cloaking.js @@ -148,11 +148,14 @@ // Find the bottom view and what's onscreen while (bottomView < childViews.length) { var view = childViews[bottomView], - $view = view.$(), - // in case of not full-window scrolling - scrollOffset = this.get('wrapperTop') || 0, - viewTop = $view.offset().top + scrollOffset, - viewBottom = viewTop + $view.height(); + $view = view.$(); + + if (!$view) { break; } + + // in case of not full-window scrolling + var scrollOffset = this.get('wrapperTop') || 0, + viewTop = $view.offset().top + scrollOffset, + viewBottom = viewTop + $view.height(); if (viewTop > viewportBottom) { break; } toUncloak.push(view);