From 49d59089e4769742e40fc15e5f9e067947fb9771 Mon Sep 17 00:00:00 2001
From: dcsjapan <dcsjapan@users.noreply.github.com>
Date: Tue, 20 Oct 2015 13:04:43 +0900
Subject: [PATCH 1/2] Add third tier to key namespacing

- Changes all `app.trans` calls to `app.translator.trans` calls.
- Changes existing keys to [three-tier namespace structure](https://github.com/flarum/english/pull/12).
- Extracts additional strings for `lib:` namespace.
- Extracts two previously missed strings for EditGroupModal.js.
---
 js/admin/src/components/AdminNav.js           | 20 +++----
 js/admin/src/components/AppearancePage.js     | 18 +++---
 js/admin/src/components/BasicsPage.js         | 22 ++++----
 js/admin/src/components/DashboardPage.js      | 14 ++---
 js/admin/src/components/EditGroupModal.js     | 20 +++----
 js/admin/src/components/ExtensionsPage.js     |  6 +-
 js/admin/src/components/LoadingModal.js       |  2 +-
 js/admin/src/components/PermissionDropdown.js |  8 +--
 js/admin/src/components/PermissionGrid.js     | 56 +++++++++----------
 js/admin/src/components/PermissionsPage.js    |  2 +-
 js/admin/src/components/SessionDropdown.js    |  2 +-
 js/forum/src/components/AvatarEditor.js       |  4 +-
 js/forum/src/components/ChangeEmailModal.js   |  8 +--
 .../src/components/ChangePasswordModal.js     |  6 +-
 js/forum/src/components/Composer.js           |  8 +--
 js/forum/src/components/DiscussionComposer.js |  8 +--
 js/forum/src/components/DiscussionList.js     |  4 +-
 js/forum/src/components/DiscussionListItem.js |  4 +-
 .../DiscussionRenamedNotification.js          |  2 +-
 .../src/components/DiscussionRenamedPost.js   |  2 +-
 .../src/components/DiscussionsSearchSource.js |  4 +-
 js/forum/src/components/EditPostComposer.js   |  6 +-
 js/forum/src/components/EditUserModal.js      |  8 +--
 .../src/components/ForgotPasswordModal.js     | 12 ++--
 js/forum/src/components/HeaderSecondary.js    |  4 +-
 js/forum/src/components/IndexPage.js          | 10 ++--
 js/forum/src/components/LogInModal.js         | 16 +++---
 js/forum/src/components/NotificationGrid.js   |  6 +-
 js/forum/src/components/NotificationList.js   |  6 +-
 .../src/components/NotificationsDropdown.js   |  2 +-
 js/forum/src/components/PostEdited.js         |  2 +-
 js/forum/src/components/PostMeta.js           |  2 +-
 js/forum/src/components/PostStream.js         |  2 +-
 js/forum/src/components/PostStreamScrubber.js |  8 +--
 js/forum/src/components/PostsUserPage.js      |  2 +-
 js/forum/src/components/ReplyComposer.js      | 10 ++--
 js/forum/src/components/ReplyPlaceholder.js   |  2 +-
 js/forum/src/components/Search.js             |  2 +-
 js/forum/src/components/SessionDropdown.js    |  8 +--
 js/forum/src/components/SettingsPage.js       | 14 ++---
 js/forum/src/components/SignUpModal.js        | 18 +++---
 js/forum/src/components/TerminalPost.js       |  2 +-
 js/forum/src/components/UserBio.js            |  4 +-
 js/forum/src/components/UserCard.js           |  6 +-
 js/forum/src/components/UserPage.js           |  6 +-
 js/forum/src/components/UsersSearchSource.js  |  2 +-
 js/forum/src/utils/DiscussionControls.js      | 18 +++---
 js/forum/src/utils/PostControls.js            |  8 +--
 js/forum/src/utils/UserControls.js            |  6 +-
 js/lib/App.js                                 |  6 +-
 js/lib/helpers/punctuateSeries.js             |  6 +-
 js/lib/helpers/username.js                    |  2 +-
 js/lib/models/Discussion.js                   |  2 +-
 js/lib/utils/abbreviateNumber.js              |  4 +-
 54 files changed, 216 insertions(+), 216 deletions(-)

diff --git a/js/admin/src/components/AdminNav.js b/js/admin/src/components/AdminNav.js
index aa035977c..240872467 100644
--- a/js/admin/src/components/AdminNav.js
+++ b/js/admin/src/components/AdminNav.js
@@ -35,36 +35,36 @@ export default class AdminNav extends Component {
     items.add('dashboard', AdminLinkButton.component({
       href: app.route('dashboard'),
       icon: 'bar-chart',
-      children: app.trans('core.admin.nav_dashboard_button'),
-      description: app.trans('core.admin.nav_dashboard_text')
+      children: app.translator.trans('core.admin.nav.dashboard_button'),
+      description: app.translator.trans('core.admin.nav.dashboard_text')
     }));
 
     items.add('basics', AdminLinkButton.component({
       href: app.route('basics'),
       icon: 'pencil',
-      children: app.trans('core.admin.nav_basics_button'),
-      description: app.trans('core.admin.nav_basics_text')
+      children: app.translator.trans('core.admin.nav.basics_button'),
+      description: app.translator.trans('core.admin.nav.basics_text')
     }));
 
     items.add('permissions', AdminLinkButton.component({
       href: app.route('permissions'),
       icon: 'key',
-      children: app.trans('core.admin.nav_permissions_button'),
-      description: app.trans('core.admin.nav_permissions_text')
+      children: app.translator.trans('core.admin.nav.permissions_button'),
+      description: app.translator.trans('core.admin.nav.permissions_text')
     }));
 
     items.add('appearance', AdminLinkButton.component({
       href: app.route('appearance'),
       icon: 'paint-brush',
-      children: app.trans('core.admin.nav_appearance_button'),
-      description: app.trans('core.admin.nav_appearance_text')
+      children: app.translator.trans('core.admin.nav.appearance_button'),
+      description: app.translator.trans('core.admin.nav.appearance_text')
     }));
 
     items.add('extensions', AdminLinkButton.component({
       href: app.route('extensions'),
       icon: 'puzzle-piece',
-      children: app.trans('core.admin.nav_extensions_button'),
-      description: app.trans('core.admin.nav_extensions_text')
+      children: app.translator.trans('core.admin.nav.extensions_button'),
+      description: app.translator.trans('core.admin.nav.extensions_text')
     }));
 
     return items;
diff --git a/js/admin/src/components/AppearancePage.js b/js/admin/src/components/AppearancePage.js
index 319b38902..8485ee44d 100644
--- a/js/admin/src/components/AppearancePage.js
+++ b/js/admin/src/components/AppearancePage.js
@@ -18,9 +18,9 @@ export default class AppearancePage extends Component {
         <div className="container">
           <form onsubmit={this.onsubmit.bind(this)}>
             <fieldset className="AppearancePage-colors">
-              <legend>{app.trans('core.admin.appearance_colors_heading')}</legend>
+              <legend>{app.translator.trans('core.admin.appearance.colors_heading')}</legend>
               <div className="helpText">
-                {app.trans('core.admin.appearance_colors_text')}
+                {app.translator.trans('core.admin.appearance.colors_text')}
               </div>
 
               <div className="AppearancePage-colors-input">
@@ -30,33 +30,33 @@ export default class AppearancePage extends Component {
 
               {Switch.component({
                 state: this.darkMode(),
-                children: app.trans('core.admin.appearance_dark_mode_label'),
+                children: app.translator.trans('core.admin.appearance.dark_mode_label'),
                 onchange: this.darkMode
               })}
 
               {Switch.component({
                 state: this.coloredHeader(),
-                children: app.trans('core.admin.appearance_colored_header_label'),
+                children: app.translator.trans('core.admin.appearance.colored_header_label'),
                 onchange: this.coloredHeader
               })}
 
               {Button.component({
                 className: 'Button Button--primary',
                 type: 'submit',
-                children: app.trans('core.admin.appearance_submit_button'),
+                children: app.translator.trans('core.admin.appearance.submit_button'),
                 loading: this.loading
               })}
             </fieldset>
           </form>
 
           <fieldset>
-            <legend>{app.trans('core.admin.appearance_custom_styles_heading')}</legend>
+            <legend>{app.translator.trans('core.admin.appearance.custom_styles_heading')}</legend>
             <div className="helpText">
-              {app.trans('core.admin.appearance_custom_styles_text')}
+              {app.translator.trans('core.admin.appearance.custom_styles_text')}
             </div>
             {Button.component({
               className: 'Button',
-              children: app.trans('core.admin.appearance_edit_css_button'),
+              children: app.translator.trans('core.admin.appearance.edit_css_button'),
               onclick: () => app.modal.show(new EditCustomCssModal())
             })}
           </fieldset>
@@ -71,7 +71,7 @@ export default class AppearancePage extends Component {
     const hex = /^#[0-9a-f]{3}([0-9a-f]{3})?$/i;
 
     if (!hex.test(this.primaryColor()) || !hex.test(this.secondaryColor())) {
-      alert(app.trans('core.admin.appearance_enter_hex_message'));
+      alert(app.translator.trans('core.admin.appearance.enter_hex_message'));
       return;
     }
 
diff --git a/js/admin/src/components/BasicsPage.js b/js/admin/src/components/BasicsPage.js
index 1d5f5f901..def963cc9 100644
--- a/js/admin/src/components/BasicsPage.js
+++ b/js/admin/src/components/BasicsPage.js
@@ -36,17 +36,17 @@ export default class BasicsPage extends Component {
         <div className="container">
           <form onsubmit={this.onsubmit.bind(this)}>
             {FieldSet.component({
-              label: app.trans('core.admin.basics_forum_title_heading'),
+              label: app.translator.trans('core.admin.basics.forum_title_heading'),
               children: [
                 <input className="FormControl" value={this.values.forum_title()} oninput={m.withAttr('value', this.values.forum_title)}/>
               ]
             })}
 
             {FieldSet.component({
-              label: app.trans('core.admin.basics_forum_description_heading'),
+              label: app.translator.trans('core.admin.basics.forum_description_heading'),
               children: [
                 <div className="helpText">
-                  {app.trans('core.admin.basics_forum_description_text')}
+                  {app.translator.trans('core.admin.basics.forum_description_text')}
                 </div>,
                 <textarea className="FormControl" value={this.values.forum_description()} oninput={m.withAttr('value', this.values.forum_description)}/>
               ]
@@ -54,7 +54,7 @@ export default class BasicsPage extends Component {
 
             {Object.keys(this.localeOptions).length > 1
               ? FieldSet.component({
-                label: app.trans('core.admin.basics_default_language_heading'),
+                label: app.translator.trans('core.admin.basics.default_language_heading'),
                 children: [
                   Select.component({
                     options: this.localeOptions,
@@ -65,11 +65,11 @@ export default class BasicsPage extends Component {
               : ''}
 
             {FieldSet.component({
-              label: app.trans('core.admin.basics_home_page_heading'),
+              label: app.translator.trans('core.admin.basics.home_page_heading'),
               className: 'BasicsPage-homePage',
               children: [
                 <div className="helpText">
-                  {app.trans('core.admin.basics_home_page_text')}
+                  {app.translator.trans('core.admin.basics.home_page_text')}
                 </div>,
                 this.homePageItems().toArray().map(({path, label}) =>
                   <label className="checkbox">
@@ -81,11 +81,11 @@ export default class BasicsPage extends Component {
             })}
 
             {FieldSet.component({
-              label: app.trans('core.admin.basics_welcome_banner_heading'),
+              label: app.translator.trans('core.admin.basics.welcome_banner_heading'),
               className: 'BasicsPage-welcomeBanner',
               children: [
                 <div className="helpText">
-                  {app.trans('core.admin.basics_welcome_banner_text')}
+                  {app.translator.trans('core.admin.basics.welcome_banner_text')}
                 </div>,
                 <div className="BasicsPage-welcomeBanner-input">
                   <input className="FormControl" value={this.values.welcome_title()} oninput={m.withAttr('value', this.values.welcome_title)}/>
@@ -97,7 +97,7 @@ export default class BasicsPage extends Component {
             {Button.component({
               type: 'submit',
               className: 'Button Button--primary',
-              children: app.trans('core.admin.basics_submit_button'),
+              children: app.translator.trans('core.admin.basics.submit_button'),
               loading: this.loading,
               disabled: !this.changed()
             })}
@@ -123,7 +123,7 @@ export default class BasicsPage extends Component {
 
     items.add('allDiscussions', {
       path: '/all',
-      label: app.trans('core.admin.basics_all_discussions_label')
+      label: app.translator.trans('core.admin.basics.all_discussions_label')
     });
 
     return items;
@@ -143,7 +143,7 @@ export default class BasicsPage extends Component {
 
     saveSettings(settings)
       .then(() => {
-        app.alerts.show(this.successAlert = new Alert({type: 'success', children: app.trans('core.admin.basics_saved_message')}));
+        app.alerts.show(this.successAlert = new Alert({type: 'success', children: app.translator.trans('core.admin.basics.saved_message')}));
       })
       .finally(() => {
         this.loading = false;
diff --git a/js/admin/src/components/DashboardPage.js b/js/admin/src/components/DashboardPage.js
index 8d4341499..c5dd9dde4 100644
--- a/js/admin/src/components/DashboardPage.js
+++ b/js/admin/src/components/DashboardPage.js
@@ -6,14 +6,14 @@ export default class DashboardPage extends Component {
       <div className="DashboardPage">
         <div className="container">
           <h2>Welcome to Flarum Beta</h2>
-          <p>{app.trans('core.admin.dashboard_version_text', {version: <strong>{app.forum.attribute('version')}</strong>})}</p>
-          <p>{app.trans('core.admin.dashboard_beta_warning_text', {strong: <strong/>})}</p>
+          <p>{app.translator.trans('core.admin.dashboard.version_text', {version: <strong>{app.forum.attribute('version')}</strong>})}</p>
+          <p>{app.translator.trans('core.admin.dashboard.beta_warning_text', {strong: <strong/>})}</p>
           <ul>
-            <li>{app.trans('core.admin.dashboard_contributing_text', {a: <a href="http://flarum.org/docs/contributing" target="_blank"/>})}</li>
-            <li>{app.trans('core.admin.dashboard_troubleshooting_text', {a: <a href="http://flarum.org/docs/troubleshooting" target="_blank"/>})}</li>
-            <li>{app.trans('core.admin.dashboard_support_text', {a: <a href="http://discuss.flarum.org/t/support" target="_blank"/>})}</li>
-            <li>{app.trans('core.admin.dashboard_features_text', {a: <a href="http://discuss.flarum.org/t/features" target="_blank"/>})}</li>
-            <li>{app.trans('core.admin.dashboard_extension_text', {a: <a href="http://flarum.org/docs/extend" target="_blank"/>})}</li>
+            <li>{app.translator.trans('core.admin.dashboard.contributing_text', {a: <a href="http://flarum.org/docs/contributing" target="_blank"/>})}</li>
+            <li>{app.translator.trans('core.admin.dashboard.troubleshooting_text', {a: <a href="http://flarum.org/docs/troubleshooting" target="_blank"/>})}</li>
+            <li>{app.translator.trans('core.admin.dashboard.support_text', {a: <a href="http://discuss.flarum.org/t/support" target="_blank"/>})}</li>
+            <li>{app.translator.trans('core.admin.dashboard.features_text', {a: <a href="http://discuss.flarum.org/t/features" target="_blank"/>})}</li>
+            <li>{app.translator.trans('core.admin.dashboard.extension_text', {a: <a href="http://flarum.org/docs/extend" target="_blank"/>})}</li>
           </ul>
         </div>
       </div>
diff --git a/js/admin/src/components/EditGroupModal.js b/js/admin/src/components/EditGroupModal.js
index 056cde4ab..cea3bfd92 100644
--- a/js/admin/src/components/EditGroupModal.js
+++ b/js/admin/src/components/EditGroupModal.js
@@ -28,7 +28,7 @@ export default class EditGroupModal extends Modal {
         style: {backgroundColor: this.color()}
       }) : '',
       ' ',
-      this.namePlural() || app.trans('core.admin.edit_group_title')
+      this.namePlural() || app.translator.trans('core.admin.edit_group.title')
     ];
   }
 
@@ -37,22 +37,22 @@ export default class EditGroupModal extends Modal {
       <div className="Modal-body">
         <div className="Form">
           <div className="Form-group">
-            <label>{app.trans('core.admin.edit_group_name_label')}</label>
+            <label>{app.translator.trans('core.admin.edit_group.name_label')}</label>
             <div className="EditGroupModal-name-input">
-              <input className="FormControl" placeholder="Singular (e.g. Mod)" value={this.nameSingular()} oninput={m.withAttr('value', this.nameSingular)}/>
-              <input className="FormControl" placeholder="Plural (e.g. Mods)" value={this.namePlural()} oninput={m.withAttr('value', this.namePlural)}/>
+              <input className="FormControl" placeholder={app.translator.trans('core.admin.edit_group.singular_placeholder')} value={this.nameSingular()} oninput={m.withAttr('value', this.nameSingular)}/>
+              <input className="FormControl" placeholder={app.translator.trans('core.admin.edit_group.plural_placeholder')} value={this.namePlural()} oninput={m.withAttr('value', this.namePlural)}/>
             </div>
           </div>
 
           <div className="Form-group">
-            <label>{app.trans('core.admin.edit_group_color_label')}</label>
+            <label>{app.translator.trans('core.admin.edit_group.color_label')}</label>
             <input className="FormControl" placeholder="#aaaaaa" value={this.color()} oninput={m.withAttr('value', this.color)}/>
           </div>
 
           <div className="Form-group">
-            <label>{app.trans('core.admin.edit_group_icon_label')}</label>
+            <label>{app.translator.trans('core.admin.edit_group.icon_label')}</label>
             <div className="helpText">
-              {app.trans('core.admin.edit_group_icon_text', {a: <a href="http://fortawesome.github.io/Font-Awesome/icons/" tabindex="-1"/>}, {em: <em/>}, {code: <code/>})}
+              {app.translator.trans('core.admin.edit_group.icon_text', {a: <a href="http://fortawesome.github.io/Font-Awesome/icons/" tabindex="-1"/>}, {em: <em/>}, {code: <code/>})}
             </div>
             <input className="FormControl" placeholder="bolt" value={this.icon()} oninput={m.withAttr('value', this.icon)}/>
           </div>
@@ -62,11 +62,11 @@ export default class EditGroupModal extends Modal {
               type: 'submit',
               className: 'Button Button--primary EditGroupModal-save',
               loading: this.loading,
-              children: app.trans('core.admin.edit_group_submit_button')
+              children: app.translator.trans('core.admin.edit_group.submit_button')
             })}
             {this.group.exists && this.group.id() !== Group.ADMINISTRATOR_ID ? (
               <button type="button" className="Button EditGroupModal-delete" onclick={this.deleteGroup.bind(this)}>
-                {app.trans('core.admin.edit_group_delete_button')}
+                {app.translator.trans('core.admin.edit_group.delete_button')}
               </button>
             ) : ''}
           </div>
@@ -94,7 +94,7 @@ export default class EditGroupModal extends Modal {
   }
 
   deleteGroup() {
-    if (confirm(app.trans('core.admin.edit_group_delete_confirmation'))) {
+    if (confirm(app.translator.trans('core.admin.edit_group.delete_confirmation'))) {
       this.group.delete().then(() => m.redraw());
       this.hide();
     }
diff --git a/js/admin/src/components/ExtensionsPage.js b/js/admin/src/components/ExtensionsPage.js
index c22220d3d..58b1f9832 100644
--- a/js/admin/src/components/ExtensionsPage.js
+++ b/js/admin/src/components/ExtensionsPage.js
@@ -18,7 +18,7 @@ export default class ExtensionsPage extends Component {
         <div className="ExtensionsPage-header">
           <div className="container">
             {Button.component({
-              children: app.trans('core.admin.extensions_add_button'),
+              children: app.translator.trans('core.admin.extensions.add_button'),
               icon: 'plus',
               className: 'Button Button--primary',
               onclick: () => app.modal.show(new AddExtensionModal())
@@ -70,7 +70,7 @@ export default class ExtensionsPage extends Component {
     if (app.extensionSettings[name]) {
       items.add('settings', Button.component({
         icon: 'cog',
-        children: app.trans('core.admin.extensions_settings_button'),
+        children: app.translator.trans('core.admin.extensions.settings_button'),
         onclick: app.extensionSettings[name]
       }));
     }
@@ -78,7 +78,7 @@ export default class ExtensionsPage extends Component {
     if (!enabled) {
       items.add('uninstall', Button.component({
         icon: 'trash-o',
-        children: app.trans('core.admin.extensions_uninstall_button'),
+        children: app.translator.trans('core.admin.extensions.uninstall_button'),
         onclick: () => {
           app.request({
             url: app.forum.attribute('apiUrl') + '/extensions/' + name,
diff --git a/js/admin/src/components/LoadingModal.js b/js/admin/src/components/LoadingModal.js
index 1c9dd2867..e991f371a 100644
--- a/js/admin/src/components/LoadingModal.js
+++ b/js/admin/src/components/LoadingModal.js
@@ -10,7 +10,7 @@ export default class LoadingModal extends Modal {
   }
 
   title() {
-    return app.trans('core.admin.extensions_loading_title');
+    return app.translator.trans('core.admin.loading.title');
   }
 
   content() {
diff --git a/js/admin/src/components/PermissionDropdown.js b/js/admin/src/components/PermissionDropdown.js
index ea6175efb..ea19a1ae3 100644
--- a/js/admin/src/components/PermissionDropdown.js
+++ b/js/admin/src/components/PermissionDropdown.js
@@ -27,9 +27,9 @@ export default class PermissionDropdown extends Dropdown {
     const adminGroup = app.store.getById('groups', Group.ADMINISTRATOR_ID);
 
     if (everyone) {
-      this.props.label = app.trans('core.admin.permissions_dropdown_everyone_button');
+      this.props.label = app.translator.trans('core.admin.permissions_controls.everyone_button');
     } else if (members) {
-      this.props.label = app.trans('core.admin.permissions_dropdown_members_button');
+      this.props.label = app.translator.trans('core.admin.permissions_controls.members_button');
     } else {
       this.props.label = [
         badgeForId(Group.ADMINISTRATOR_ID),
@@ -40,7 +40,7 @@ export default class PermissionDropdown extends Dropdown {
     if (this.props.allowGuest) {
       this.props.children.push(
         Button.component({
-          children: app.trans('core.admin.permissions_dropdown_everyone_button'),
+          children: app.translator.trans('core.admin.permissions_controls.everyone_button'),
           icon: everyone ? 'check' : true,
           onclick: () => this.save([Group.GUEST_ID])
         })
@@ -49,7 +49,7 @@ export default class PermissionDropdown extends Dropdown {
 
     this.props.children.push(
       Button.component({
-        children: app.trans('core.admin.permissions_dropdown_members_button'),
+        children: app.translator.trans('core.admin.permissions_controls.members_button'),
         icon: members ? 'check' : true,
         onclick: () => this.save([Group.MEMBER_ID])
       }),
diff --git a/js/admin/src/components/PermissionGrid.js b/js/admin/src/components/PermissionGrid.js
index 25dd7fcc8..bbacaebe5 100644
--- a/js/admin/src/components/PermissionGrid.js
+++ b/js/admin/src/components/PermissionGrid.js
@@ -59,22 +59,22 @@ export default class PermissionGrid extends Component {
     const items = new ItemList();
 
     items.add('view', {
-      label: app.trans('core.admin.permissions_read_heading'),
+      label: app.translator.trans('core.admin.permissions.read_heading'),
       children: this.viewItems().toArray()
     }, 100);
 
     items.add('start', {
-      label: app.trans('core.admin.permissions_create_heading'),
+      label: app.translator.trans('core.admin.permissions.create_heading'),
       children: this.startItems().toArray()
     }, 90);
 
     items.add('reply', {
-      label: app.trans('core.admin.permissions_participate_heading'),
+      label: app.translator.trans('core.admin.permissions.participate_heading'),
       children: this.replyItems().toArray()
     }, 80);
 
     items.add('moderate', {
-      label: app.trans('core.admin.permissions_moderate_heading'),
+      label: app.translator.trans('core.admin.permissions.moderate_heading'),
       children: this.moderateItems().toArray()
     }, 70);
 
@@ -86,19 +86,19 @@ export default class PermissionGrid extends Component {
 
     items.add('viewDiscussions', {
       icon: 'eye',
-      label: app.trans('core.admin.permissions_view_discussions_label'),
+      label: app.translator.trans('core.admin.permissions.view_discussions_label'),
       permission: 'viewDiscussions',
       allowGuest: true
     }, 100);
 
     items.add('signUp', {
       icon: 'user-plus',
-      label: app.trans('core.admin.permissions_sign_up_label'),
+      label: app.translator.trans('core.admin.permissions.sign_up_label'),
       setting: () => SettingDropdown.component({
         key: 'allow_sign_up',
         options: [
-          {value: '1', label: app.trans('core.admin.permissions_signup_open_button')},
-          {value: '0', label: app.trans('core.admin.permissions_signup_closed_button')}
+          {value: '1', label: app.translator.trans('core.admin.permissions_controls.signup_open_button')},
+          {value: '0', label: app.translator.trans('core.admin.permissions_controls.signup_closed_button')}
         ]
       })
     }, 90);
@@ -111,25 +111,25 @@ export default class PermissionGrid extends Component {
 
     items.add('start', {
       icon: 'edit',
-      label: app.trans('core.admin.permissions_start_discussions_label'),
+      label: app.translator.trans('core.admin.permissions.start_discussions_label'),
       permission: 'startDiscussion'
     }, 100);
 
     items.add('allowRenaming', {
       icon: 'i-cursor',
-      label: app.trans('core.admin.permissions_allow_renaming_label'),
+      label: app.translator.trans('core.admin.permissions.allow_renaming_label'),
       setting: () => {
         const minutes = parseInt(app.settings.allow_renaming, 10);
 
         return SettingDropdown.component({
           defaultLabel: minutes
-            ? app.translator.transChoice('core.admin.permissions_allow_some_minutes_button', minutes, {count: minutes})
-            : app.trans('core.admin.permissions_allow_indefinitely_button'),
+            ? app.translator.transChoice('core.admin.permissions_controls.allow_some_minutes_button', minutes, {count: minutes})
+            : app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'),
           key: 'allow_renaming',
           options: [
-            {value: '-1', label: app.trans('core.admin.permissions_allow_indefinitely_button')},
-            {value: '10', label: app.trans('core.admin.permissions_allow_ten_minutes_button')},
-            {value: 'reply', label: app.trans('core.admin.permissions_allow_until_reply_button')}
+            {value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button')},
+            {value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button')},
+            {value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button')}
           ]
         });
       }
@@ -143,25 +143,25 @@ export default class PermissionGrid extends Component {
 
     items.add('reply', {
       icon: 'reply',
-      label: app.trans('core.admin.permissions_reply_to_discussions_label'),
+      label: app.translator.trans('core.admin.permissions.reply_to_discussions_label'),
       permission: 'discussion.reply'
     }, 100);
 
     items.add('allowPostEditing', {
       icon: 'pencil',
-      label: app.trans('core.admin.permissions_allow_post_editing_label'),
+      label: app.translator.trans('core.admin.permissions.allow_post_editing_label'),
       setting: () => {
         const minutes = parseInt(app.settings.allow_post_editing, 10);
 
         return SettingDropdown.component({
           defaultLabel: minutes
-            ? app.translator.transChoice('core.admin.permissions_allow_some_minutes_button', minutes, {count: minutes})
-            : app.trans('core.admin.permissions_allow_indefinitely_button'),
+            ? app.translator.transChoice('core.admin.permissions_controls.allow_some_minutes_button', minutes, {count: minutes})
+            : app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button'),
           key: 'allow_post_editing',
           options: [
-            {value: '-1', label: app.trans('core.admin.permissions_allow_indefinitely_button')},
-            {value: '10', label: app.trans('core.admin.permissions_allow_ten_minutes_button')},
-            {value: 'reply', label: app.trans('core.admin.permissions_allow_until_reply_button')}
+            {value: '-1', label: app.translator.trans('core.admin.permissions_controls.allow_indefinitely_button')},
+            {value: '10', label: app.translator.trans('core.admin.permissions_controls.allow_ten_minutes_button')},
+            {value: 'reply', label: app.translator.trans('core.admin.permissions_controls.allow_until_reply_button')}
           ]
         });
       }
@@ -175,31 +175,31 @@ export default class PermissionGrid extends Component {
 
     items.add('renameDiscussions', {
       icon: 'i-cursor',
-      label: app.trans('core.admin.permissions_rename_discussions_label'),
+      label: app.translator.trans('core.admin.permissions.rename_discussions_label'),
       permission: 'discussion.rename'
     }, 100);
 
     items.add('hideDiscussions', {
       icon: 'trash-o',
-      label: app.trans('core.admin.permissions_delete_discussions_label'),
+      label: app.translator.trans('core.admin.permissions.delete_discussions_label'),
       permission: 'discussion.hide'
     }, 90);
 
     items.add('deleteDiscussions', {
       icon: 'times',
-      label: app.trans('core.admin.permissions_delete_discussions_forever_label'),
+      label: app.translator.trans('core.admin.permissions.delete_discussions_forever_label'),
       permission: 'discussion.delete'
     }, 80);
 
     items.add('editPosts', {
       icon: 'pencil',
-      label: app.trans('core.admin.permissions_edit_and_delete_posts_label'),
+      label: app.translator.trans('core.admin.permissions.edit_and_delete_posts_label'),
       permission: 'discussion.editPosts'
     }, 70);
 
     items.add('deletePosts', {
       icon: 'times',
-      label: app.trans('core.admin.permissions_delete_posts_forever_label'),
+      label: app.translator.trans('core.admin.permissions.delete_posts_forever_label'),
       permission: 'discussion.deletePosts'
     }, 60);
 
@@ -210,7 +210,7 @@ export default class PermissionGrid extends Component {
     const items = new ItemList();
 
     items.add('global', {
-      label: app.trans('core.admin.permissions_global_heading'),
+      label: app.translator.trans('core.admin.permissions.global_heading'),
       render: item => {
         if (item.setting) {
           return item.setting();
diff --git a/js/admin/src/components/PermissionsPage.js b/js/admin/src/components/PermissionsPage.js
index 10a784342..dc228d619 100644
--- a/js/admin/src/components/PermissionsPage.js
+++ b/js/admin/src/components/PermissionsPage.js
@@ -25,7 +25,7 @@ export default class PermissionsPage extends Component {
               ))}
             <button className="Button Group Group--add" onclick={() => app.modal.show(new EditGroupModal())}>
               {icon('plus', {className: 'Group-icon'})}
-              <span className="Group-name">{app.trans('core.admin.permissions_new_group_button')}</span>
+              <span className="Group-name">{app.translator.trans('core.admin.permissions.new_group_button')}</span>
             </button>
           </div>
         </div>
diff --git a/js/admin/src/components/SessionDropdown.js b/js/admin/src/components/SessionDropdown.js
index be5edfc7a..d6ce0e56a 100644
--- a/js/admin/src/components/SessionDropdown.js
+++ b/js/admin/src/components/SessionDropdown.js
@@ -43,7 +43,7 @@ export default class SessionDropdown extends Dropdown {
     items.add('logOut',
       Button.component({
         icon: 'sign-out',
-        children: app.trans('core.admin.header_log_out_button'),
+        children: app.translator.trans('core.admin.header.log_out_button'),
         onclick: app.session.logout.bind(app.session)
       }),
       -100
diff --git a/js/forum/src/components/AvatarEditor.js b/js/forum/src/components/AvatarEditor.js
index 35acab59c..ac525d64d 100644
--- a/js/forum/src/components/AvatarEditor.js
+++ b/js/forum/src/components/AvatarEditor.js
@@ -60,7 +60,7 @@ export default class AvatarEditor extends Component {
     items.add('upload',
       Button.component({
         icon: 'upload',
-        children: app.trans('core.forum.user_avatar_upload_button'),
+        children: app.translator.trans('core.forum.user.avatar_upload_button'),
         onclick: this.upload.bind(this)
       })
     );
@@ -68,7 +68,7 @@ export default class AvatarEditor extends Component {
     items.add('remove',
       Button.component({
         icon: 'times',
-        children: app.trans('core.forum.user_avatar_remove_button'),
+        children: app.translator.trans('core.forum.user.avatar_remove_button'),
         onclick: this.remove.bind(this)
       })
     );
diff --git a/js/forum/src/components/ChangeEmailModal.js b/js/forum/src/components/ChangeEmailModal.js
index 320d9313b..35b430889 100644
--- a/js/forum/src/components/ChangeEmailModal.js
+++ b/js/forum/src/components/ChangeEmailModal.js
@@ -29,7 +29,7 @@ export default class ChangeEmailModal extends Modal {
   }
 
   title() {
-    return app.trans('core.forum.change_email_title');
+    return app.translator.trans('core.forum.change_email.title');
   }
 
   content() {
@@ -37,10 +37,10 @@ export default class ChangeEmailModal extends Modal {
       return (
         <div className="Modal-body">
           <div className="Form Form--centered">
-            <p className="helpText">{app.trans('core.forum.change_email_confirmation_message', {email: <strong>{this.email()}</strong>})}</p>
+            <p className="helpText">{app.translator.trans('core.forum.change_email.confirmation_message', {email: <strong>{this.email()}</strong>})}</p>
             <div className="Form-group">
               <Button className="Button Button--primary Button--block" onclick={this.hide.bind(this)}>
-                {app.trans('core.forum.change_email_dismiss_button')}
+                {app.translator.trans('core.forum.change_email.dismiss_button')}
               </Button>
             </div>
           </div>
@@ -63,7 +63,7 @@ export default class ChangeEmailModal extends Modal {
               className: 'Button Button--primary Button--block',
               type: 'submit',
               loading: this.loading,
-              children: app.trans('core.forum.change_email_submit_button')
+              children: app.translator.trans('core.forum.change_email.submit_button')
             })}
           </div>
         </div>
diff --git a/js/forum/src/components/ChangePasswordModal.js b/js/forum/src/components/ChangePasswordModal.js
index 82ebc4a0d..5e4e5b22a 100644
--- a/js/forum/src/components/ChangePasswordModal.js
+++ b/js/forum/src/components/ChangePasswordModal.js
@@ -11,20 +11,20 @@ export default class ChangePasswordModal extends Modal {
   }
 
   title() {
-    return app.trans('core.forum.change_password_title');
+    return app.translator.trans('core.forum.change_password.title');
   }
 
   content() {
     return (
       <div className="Modal-body">
         <div className="Form Form--centered">
-          <p className="helpText">{app.trans('core.forum.change_password_text')}</p>
+          <p className="helpText">{app.translator.trans('core.forum.change_password.text')}</p>
           <div className="Form-group">
             {Button.component({
               className: 'Button Button--primary Button--block',
               type: 'submit',
               loading: this.loading,
-              children: app.trans('core.forum.change_password_send_button')
+              children: app.translator.trans('core.forum.change_password.send_button')
             })}
           </div>
         </div>
diff --git a/js/forum/src/components/Composer.js b/js/forum/src/components/Composer.js
index b35fbe187..861367c9c 100644
--- a/js/forum/src/components/Composer.js
+++ b/js/forum/src/components/Composer.js
@@ -464,28 +464,28 @@ class Composer extends Component {
     if (this.position === Composer.PositionEnum.FULLSCREEN) {
       items.add('exitFullScreen', ComposerButton.component({
         icon: 'compress',
-        title: app.trans('core.forum.composer_exit_full_screen_tooltip'),
+        title: app.translator.trans('core.forum.composer.exit_full_screen_tooltip'),
         onclick: this.exitFullScreen.bind(this)
       }));
     } else {
       if (this.position !== Composer.PositionEnum.MINIMIZED) {
         items.add('minimize', ComposerButton.component({
           icon: 'minus minimize',
-          title: app.trans('core.forum.composer_minimize_tooltip'),
+          title: app.translator.trans('core.forum.composer.minimize_tooltip'),
           onclick: this.minimize.bind(this),
           itemClassName: 'App-backControl'
         }));
 
         items.add('fullScreen', ComposerButton.component({
           icon: 'expand',
-          title: app.trans('core.forum.composer_full_screen_tooltip'),
+          title: app.translator.trans('core.forum.composer.full_screen_tooltip'),
           onclick: this.fullScreen.bind(this)
         }));
       }
 
       items.add('close', ComposerButton.component({
         icon: 'times',
-        title: app.trans('core.forum.composer_close_tooltip'),
+        title: app.translator.trans('core.forum.composer.close_tooltip'),
         onclick: this.close.bind(this)
       }));
     }
diff --git a/js/forum/src/components/DiscussionComposer.js b/js/forum/src/components/DiscussionComposer.js
index 2b0743a44..2b10a7a72 100644
--- a/js/forum/src/components/DiscussionComposer.js
+++ b/js/forum/src/components/DiscussionComposer.js
@@ -27,10 +27,10 @@ export default class DiscussionComposer extends ComposerBody {
   static initProps(props) {
     super.initProps(props);
 
-    props.placeholder = props.placeholder || extractText(app.trans('core.forum.composer_discussion_body_placeholder'));
-    props.submitLabel = props.submitLabel || app.trans('core.forum.composer_discussion_submit_button');
-    props.confirmExit = props.confirmExit || extractText(app.trans('core.forum.composer_discussion_discard_confirmation'));
-    props.titlePlaceholder = props.titlePlaceholder || extractText(app.trans('core.forum.composer_discussion_title_placeholder'));
+    props.placeholder = props.placeholder || extractText(app.translator.trans('core.forum.composer_discussion.body_placeholder'));
+    props.submitLabel = props.submitLabel || app.translator.trans('core.forum.composer_discussion.submit_button');
+    props.confirmExit = props.confirmExit || extractText(app.translator.trans('core.forum.composer_discussion.discard_confirmation'));
+    props.titlePlaceholder = props.titlePlaceholder || extractText(app.translator.trans('core.forum.composer_discussion.title_placeholder'));
   }
 
   headerItems() {
diff --git a/js/forum/src/components/DiscussionList.js b/js/forum/src/components/DiscussionList.js
index 3a4366962..7557faec3 100644
--- a/js/forum/src/components/DiscussionList.js
+++ b/js/forum/src/components/DiscussionList.js
@@ -46,14 +46,14 @@ export default class DiscussionList extends Component {
       loading = LoadingIndicator.component();
     } else if (this.moreResults) {
       loading = Button.component({
-        children: app.trans('core.forum.discussion_list_load_more_button'),
+        children: app.translator.trans('core.forum.discussion_list.load_more_button'),
         className: 'Button',
         onclick: this.loadMore.bind(this)
       });
     }
 
     if (this.discussions.length === 0 && !this.loading) {
-      const text = app.trans('core.forum.discussion_list_empty_text');
+      const text = app.translator.trans('core.forum.discussion_list.empty_text');
       return (
         <div className="DiscussionList">
           {Placeholder.component({text})}
diff --git a/js/forum/src/components/DiscussionListItem.js b/js/forum/src/components/DiscussionListItem.js
index a7b9bde56..935aa0af7 100644
--- a/js/forum/src/components/DiscussionListItem.js
+++ b/js/forum/src/components/DiscussionListItem.js
@@ -85,7 +85,7 @@ export default class DiscussionListItem extends Component {
         <div className={'DiscussionListItem-content Slidable-content' + (isUnread ? ' unread' : '') + (isRead ? ' read' : '')}>
           <a href={startUser ? app.route.user(startUser) : '#'}
             className="DiscussionListItem-author"
-            title={extractText(app.trans('core.forum.discussion_list_started_text', {user: startUser, ago: humanTime(discussion.startTime())}))}
+            title={extractText(app.translator.trans('core.forum.discussion_list.started_text', {user: startUser, ago: humanTime(discussion.startTime())}))}
             config={function(element) {
               $(element).tooltip({placement: 'right'});
               m.route.apply(this, arguments);
@@ -106,7 +106,7 @@ export default class DiscussionListItem extends Component {
 
           <span className="DiscussionListItem-count"
             onclick={this.markAsRead.bind(this)}
-            title={showUnread ? app.trans('core.forum.discussion_list_mark_as_read_tooltip') : ''}>
+            title={showUnread ? app.translator.trans('core.forum.discussion_list.mark_as_read_tooltip') : ''}>
             {abbreviateNumber(discussion[showUnread ? 'unreadCount' : 'repliesCount']())}
           </span>
 
diff --git a/js/forum/src/components/DiscussionRenamedNotification.js b/js/forum/src/components/DiscussionRenamedNotification.js
index 549e18817..a2850cf10 100644
--- a/js/forum/src/components/DiscussionRenamedNotification.js
+++ b/js/forum/src/components/DiscussionRenamedNotification.js
@@ -20,6 +20,6 @@ export default class DiscussionRenamedNotification extends Notification {
   }
 
   content() {
-    return app.trans('core.forum.notifications_discussion_renamed_text', {user: this.props.notification.sender()});
+    return app.translator.trans('core.forum.notifications.discussion_renamed_text', {user: this.props.notification.sender()});
   }
 }
diff --git a/js/forum/src/components/DiscussionRenamedPost.js b/js/forum/src/components/DiscussionRenamedPost.js
index c5edf8159..ed7524bfa 100644
--- a/js/forum/src/components/DiscussionRenamedPost.js
+++ b/js/forum/src/components/DiscussionRenamedPost.js
@@ -14,7 +14,7 @@ export default class DiscussionRenamedPost extends EventPost {
   }
 
   descriptionKey() {
-    return 'core.forum.post_stream_discussion_renamed_text';
+    return 'core.forum.post_stream.discussion_renamed_text';
   }
 
   descriptionData() {
diff --git a/js/forum/src/components/DiscussionsSearchSource.js b/js/forum/src/components/DiscussionsSearchSource.js
index 06720fe9a..6ab1b3d99 100644
--- a/js/forum/src/components/DiscussionsSearchSource.js
+++ b/js/forum/src/components/DiscussionsSearchSource.js
@@ -28,11 +28,11 @@ export default class DiscussionsSearchSource {
     const results = this.results[query] || [];
 
     return [
-      <li className="Dropdown-header">{app.trans('core.forum.search_discussions_heading')}</li>,
+      <li className="Dropdown-header">{app.translator.trans('core.forum.search.discussions_heading')}</li>,
       <li>
         {LinkButton.component({
           icon: 'search',
-          children: app.trans('core.forum.search_all_discussions_button', {query}),
+          children: app.translator.trans('core.forum.search.all_discussions_button', {query}),
           href: app.route('index', {q: query})
         })}
       </li>,
diff --git a/js/forum/src/components/EditPostComposer.js b/js/forum/src/components/EditPostComposer.js
index 7341bd26b..7f7c06778 100644
--- a/js/forum/src/components/EditPostComposer.js
+++ b/js/forum/src/components/EditPostComposer.js
@@ -23,8 +23,8 @@ export default class EditPostComposer extends ComposerBody {
   static initProps(props) {
     super.initProps(props);
 
-    props.submitLabel = props.submitLabel || app.trans('core.forum.composer_edit_submit_button');
-    props.confirmExit = props.confirmExit || app.trans('core.forum.composer_edit_discard_confirmation');
+    props.submitLabel = props.submitLabel || app.translator.trans('core.forum.composer_edit.submit_button');
+    props.confirmExit = props.confirmExit || app.translator.trans('core.forum.composer_edit.discard_confirmation');
     props.originalContent = props.originalContent || props.post.content();
     props.user = props.user || props.post.user();
 
@@ -39,7 +39,7 @@ export default class EditPostComposer extends ComposerBody {
       <h3>
         {icon('pencil')} {' '}
         <a href={app.route.discussion(post.discussion(), post.number())} config={m.route}>
-          {app.trans('core.forum.composer_edit_post_link', {number: post.number(), discussion: post.discussion().title()})}
+          {app.translator.trans('core.forum.composer_edit.post_link', {number: post.number(), discussion: post.discussion().title()})}
         </a>
       </h3>
     ));
diff --git a/js/forum/src/components/EditUserModal.js b/js/forum/src/components/EditUserModal.js
index e409ea68b..317c1a985 100644
--- a/js/forum/src/components/EditUserModal.js
+++ b/js/forum/src/components/EditUserModal.js
@@ -38,7 +38,7 @@ export default class EditUserModal extends Modal {
         <div className="Form">
           <div className="Form-group">
             <label>Username</label>
-            <input className="FormControl" placeholder={extractText(app.trans('core.forum.edit_user_username_label'))}
+            <input className="FormControl" placeholder={extractText(app.translator.trans('core.forum.edit_user.username_label'))}
               value={this.username()}
               onchange={m.withAttr('value', this.username)} />
           </div>
@@ -46,7 +46,7 @@ export default class EditUserModal extends Modal {
           <div className="Form-group">
             <label>Email</label>
             <div>
-              <input className="FormControl" placeholder={extractText(app.trans('core.forum.edit_user_email_label'))}
+              <input className="FormControl" placeholder={extractText(app.translator.trans('core.forum.edit_user.email_label'))}
                 value={this.email()}
                 onchange={m.withAttr('value', this.email)} />
             </div>
@@ -65,7 +65,7 @@ export default class EditUserModal extends Modal {
                 Set new password
               </label>
               {this.setPassword() ? (
-                <input className="FormControl" type="password" name="password" placeholder={extractText(app.trans('core.forum.edit_user_password_label'))}
+                <input className="FormControl" type="password" name="password" placeholder={extractText(app.translator.trans('core.forum.edit_user.password_label'))}
                   value={this.password()}
                   onchange={m.withAttr('value', this.password)} />
               ) : ''}
@@ -94,7 +94,7 @@ export default class EditUserModal extends Modal {
               className: 'Button Button--primary',
               type: 'submit',
               loading: this.loading,
-              children: app.trans('core.forum.edit_user_submit_button')
+              children: app.translator.trans('core.forum.edit_user.submit_button')
             })}
           </div>
         </div>
diff --git a/js/forum/src/components/ForgotPasswordModal.js b/js/forum/src/components/ForgotPasswordModal.js
index 456186277..a610116ec 100644
--- a/js/forum/src/components/ForgotPasswordModal.js
+++ b/js/forum/src/components/ForgotPasswordModal.js
@@ -35,7 +35,7 @@ export default class ForgotPasswordModal extends Modal {
   }
 
   title() {
-    return app.trans('core.forum.forgot_password_title');
+    return app.translator.trans('core.forum.forgot_password.title');
   }
 
   content() {
@@ -43,10 +43,10 @@ export default class ForgotPasswordModal extends Modal {
       return (
         <div className="Modal-body">
           <div className="Form Form--centered">
-            <p className="helpText">{app.trans('core.forum.forgot_password_email_sent_message')}</p>
+            <p className="helpText">{app.translator.trans('core.forum.forgot_password.email_sent_message')}</p>
             <div className="Form-group">
               <Button className="Button Button--primary Button--block" onclick={this.hide.bind(this)}>
-                {app.trans('core.forum.forgot_password_dismiss_button')}
+                {app.translator.trans('core.forum.forgot_password.dismiss_button')}
               </Button>
             </div>
           </div>
@@ -57,9 +57,9 @@ export default class ForgotPasswordModal extends Modal {
     return (
       <div className="Modal-body">
         <div className="Form Form--centered">
-          <p className="helpText">{app.trans('core.forum.forgot_password_text')}</p>
+          <p className="helpText">{app.translator.trans('core.forum.forgot_password.text')}</p>
           <div className="Form-group">
-            <input className="FormControl" name="email" type="email" placeholder={extractText(app.trans('core.forum.forgot_password_email_placeholder'))}
+            <input className="FormControl" name="email" type="email" placeholder={extractText(app.translator.trans('core.forum.forgot_password.email_placeholder'))}
               value={this.email()}
               onchange={m.withAttr('value', this.email)}
               disabled={this.loading} />
@@ -69,7 +69,7 @@ export default class ForgotPasswordModal extends Modal {
               className: 'Button Button--primary Button--block',
               type: 'submit',
               loading: this.loading,
-              children: app.trans('core.forum.forgot_password_submit_button')
+              children: app.translator.trans('core.forum.forgot_password.submit_button')
             })}
           </div>
         </div>
diff --git a/js/forum/src/components/HeaderSecondary.js b/js/forum/src/components/HeaderSecondary.js
index 2561f4a48..2cdfa38ec 100644
--- a/js/forum/src/components/HeaderSecondary.js
+++ b/js/forum/src/components/HeaderSecondary.js
@@ -64,7 +64,7 @@ export default class HeaderSecondary extends Component {
       if (app.forum.attribute('allowSignUp')) {
         items.add('signUp',
           Button.component({
-            children: app.trans('core.forum.header_sign_up_link'),
+            children: app.translator.trans('core.forum.header.sign_up_link'),
             className: 'Button Button--link',
             onclick: () => app.modal.show(new SignUpModal())
           }), 10
@@ -73,7 +73,7 @@ export default class HeaderSecondary extends Component {
 
       items.add('logIn',
         Button.component({
-          children: app.trans('core.forum.header_log_in_link'),
+          children: app.translator.trans('core.forum.header.log_in_link'),
           className: 'Button Button--link',
           onclick: () => app.modal.show(new LogInModal())
         }), 0
diff --git a/js/forum/src/components/IndexPage.js b/js/forum/src/components/IndexPage.js
index 8fedbfbb0..ddbe6260a 100644
--- a/js/forum/src/components/IndexPage.js
+++ b/js/forum/src/components/IndexPage.js
@@ -147,7 +147,7 @@ export default class IndexPage extends Page {
 
     items.add('newDiscussion',
       Button.component({
-        children: app.trans(canStartDiscussion ? 'core.forum.index_start_discussion_button' : 'core.forum.index_cannot_start_discussion_button'),
+        children: app.translator.trans(canStartDiscussion ? 'core.forum.index.start_discussion_button' : 'core.forum.index.cannot_start_discussion_button'),
         icon: 'edit',
         className: 'Button Button--primary IndexPage-newDiscussion',
         itemClassName: 'App-primaryControl',
@@ -180,7 +180,7 @@ export default class IndexPage extends Page {
     items.add('allDiscussions',
       LinkButton.component({
         href: app.route('index', params),
-        children: app.trans('core.forum.index_all_discussions_link'),
+        children: app.translator.trans('core.forum.index.all_discussions_link'),
         icon: 'comments-o'
       }),
       100
@@ -201,7 +201,7 @@ export default class IndexPage extends Page {
 
     const sortOptions = {};
     for (const i in app.cache.discussionList.sortMap()) {
-      sortOptions[i] = app.trans('core.forum.index_sort_' + i + '_button');
+      sortOptions[i] = app.translator.trans('core.forum.index.sort_' + i + '_button');
     }
 
     items.add('sort',
@@ -226,7 +226,7 @@ export default class IndexPage extends Page {
 
     items.add('refresh',
       Button.component({
-        title: app.trans('core.forum.index_refresh_tooltip'),
+        title: app.translator.trans('core.forum.index.refresh_tooltip'),
         icon: 'refresh',
         className: 'Button Button--icon',
         onclick: () => app.cache.discussionList.refresh()
@@ -236,7 +236,7 @@ export default class IndexPage extends Page {
     if (app.session.user) {
       items.add('markAllAsRead',
         Button.component({
-          title: app.trans('core.forum.index_mark_all_as_read_tooltip'),
+          title: app.translator.trans('core.forum.index.mark_all_as_read_tooltip'),
           icon: 'check',
           className: 'Button Button--icon',
           onclick: this.markAllAsRead.bind(this)
diff --git a/js/forum/src/components/LogInModal.js b/js/forum/src/components/LogInModal.js
index c4acab352..50ffe318a 100644
--- a/js/forum/src/components/LogInModal.js
+++ b/js/forum/src/components/LogInModal.js
@@ -38,7 +38,7 @@ export default class LogInModal extends Modal {
   }
 
   title() {
-    return app.trans('core.forum.log_in_title');
+    return app.translator.trans('core.forum.log_in.title');
   }
 
   content() {
@@ -48,14 +48,14 @@ export default class LogInModal extends Modal {
 
         <div className="Form Form--centered">
           <div className="Form-group">
-            <input className="FormControl" name="email" placeholder={extractText(app.trans('core.forum.log_in_username_or_email_placeholder'))}
+            <input className="FormControl" name="email" placeholder={extractText(app.translator.trans('core.forum.log_in.username_or_email_placeholder'))}
               value={this.email()}
               onchange={m.withAttr('value', this.email)}
               disabled={this.loading} />
           </div>
 
           <div className="Form-group">
-            <input className="FormControl" name="password" type="password" placeholder={extractText(app.trans('core.forum.log_in_password_placeholder'))}
+            <input className="FormControl" name="password" type="password" placeholder={extractText(app.translator.trans('core.forum.log_in.password_placeholder'))}
               value={this.password()}
               onchange={m.withAttr('value', this.password)}
               disabled={this.loading} />
@@ -66,19 +66,19 @@ export default class LogInModal extends Modal {
               className: 'Button Button--primary Button--block',
               type: 'submit',
               loading: this.loading,
-              children: app.trans('core.forum.log_in_submit_button')
+              children: app.translator.trans('core.forum.log_in.submit_button')
             })}
           </div>
         </div>
       </div>,
       <div className="Modal-footer">
         <p className="LogInModal-forgotPassword">
-          <a onclick={this.forgotPassword.bind(this)}>{app.trans('core.forum.log_in_forgot_password_link')}</a>
+          <a onclick={this.forgotPassword.bind(this)}>{app.translator.trans('core.forum.log_in.forgot_password_link')}</a>
         </p>
 
         {app.forum.attribute('allowSignUp') ? (
           <p className="LogInModal-signUp">
-            {app.trans('core.forum.log_in_sign_up_text', {a: <a onclick={this.signUp.bind(this)}/>})}
+            {app.translator.trans('core.forum.log_in.sign_up_text', {a: <a onclick={this.signUp.bind(this)}/>})}
           </p>
         ) : ''}
       </div>
@@ -131,12 +131,12 @@ export default class LogInModal extends Modal {
   onerror(error) {
     switch (error.status) {
       case 401:
-        error.alert.props.children = app.trans('core.forum.log_in_confirmation_required_message', {email: error.response.emailConfirmationRequired});
+        error.alert.props.children = app.translator.trans('core.forum.log_in.confirmation_required_message', {email: error.response.emailConfirmationRequired});
         delete error.alert.props.type;
         break;
 
       case 404:
-        error.alert.props.children = app.trans('core.forum.log_in_invalid_login_message');
+        error.alert.props.children = app.translator.trans('core.forum.log_in.invalid_login_message');
         break;
 
       default:
diff --git a/js/forum/src/components/NotificationGrid.js b/js/forum/src/components/NotificationGrid.js
index 2ad9422b3..f33d05405 100644
--- a/js/forum/src/components/NotificationGrid.js
+++ b/js/forum/src/components/NotificationGrid.js
@@ -19,8 +19,8 @@ export default class NotificationGrid extends Component {
      * @type {Array}
      */
     this.methods = [
-      {name: 'alert', icon: 'bell', label: app.trans('core.forum.settings_notify_by_web_heading')},
-      {name: 'email', icon: 'envelope-o', label: app.trans('core.forum.settings_notify_by_email_heading')}
+      {name: 'alert', icon: 'bell', label: app.translator.trans('core.forum.settings.notify_by_web_heading')},
+      {name: 'email', icon: 'envelope-o', label: app.translator.trans('core.forum.settings.notify_by_email_heading')}
     ];
 
     /**
@@ -180,7 +180,7 @@ export default class NotificationGrid extends Component {
     items.add('discussionRenamed', {
       name: 'discussionRenamed',
       icon: 'pencil',
-      label: app.trans('core.forum.settings_notify_discussion_renamed_label')
+      label: app.translator.trans('core.forum.settings.notify_discussion_renamed_label')
     });
 
     return items;
diff --git a/js/forum/src/components/NotificationList.js b/js/forum/src/components/NotificationList.js
index 53e97b1e6..0ffe6385c 100644
--- a/js/forum/src/components/NotificationList.js
+++ b/js/forum/src/components/NotificationList.js
@@ -57,12 +57,12 @@ export default class NotificationList extends Component {
             {Button.component({
               className: 'Button Button--icon Button--link',
               icon: 'check',
-              title: app.trans('core.forum.notifications_mark_all_as_read_tooltip'),
+              title: app.translator.trans('core.forum.notifications.mark_all_as_read_tooltip'),
               onclick: this.markAllAsRead.bind(this)
             })}
           </div>
 
-          <h4 className="App-titleControl App-titleControl--text">{app.trans('core.forum.notifications_title')}</h4>
+          <h4 className="App-titleControl App-titleControl--text">{app.translator.trans('core.forum.notifications.title')}</h4>
         </div>
 
         <div className="NotificationList-content">
@@ -96,7 +96,7 @@ export default class NotificationList extends Component {
               );
             })
             : !this.loading
-              ? <div className="NotificationList-empty">{app.trans('core.forum.notifications_empty_text')}</div>
+              ? <div className="NotificationList-empty">{app.translator.trans('core.forum.notifications.empty_text')}</div>
               : LoadingIndicator.component({className: 'LoadingIndicator--block'})}
         </div>
       </div>
diff --git a/js/forum/src/components/NotificationsDropdown.js b/js/forum/src/components/NotificationsDropdown.js
index e2fe48ce6..86fc577b2 100644
--- a/js/forum/src/components/NotificationsDropdown.js
+++ b/js/forum/src/components/NotificationsDropdown.js
@@ -7,7 +7,7 @@ export default class NotificationsDropdown extends Dropdown {
     props.className = props.className || 'NotificationsDropdown';
     props.buttonClassName = props.buttonClassName || 'Button Button--flat';
     props.menuClassName = props.menuClassName || 'Dropdown-menu--right';
-    props.label = props.label || app.trans('core.forum.notifications_tooltip');
+    props.label = props.label || app.translator.trans('core.forum.notifications.tooltip');
     props.icon = props.icon || 'bell';
 
     super.initProps(props);
diff --git a/js/forum/src/components/PostEdited.js b/js/forum/src/components/PostEdited.js
index 740c7ceca..b252a5eae 100644
--- a/js/forum/src/components/PostEdited.js
+++ b/js/forum/src/components/PostEdited.js
@@ -15,7 +15,7 @@ export default class PostEdited extends Component {
   view() {
     const post = this.props.post;
     const editUser = post.editUser();
-    const title = extractText(app.trans('core.forum.post_edited_tooltip', {user: editUser, ago: humanTime(post.editTime())}));
+    const title = extractText(app.translator.trans('core.forum.post.edited_tooltip', {user: editUser, ago: humanTime(post.editTime())}));
 
     return (
       <span className="PostEdited" title={title}>{icon('pencil')}</span>
diff --git a/js/forum/src/components/PostMeta.js b/js/forum/src/components/PostMeta.js
index de84eddcb..3f9906182 100644
--- a/js/forum/src/components/PostMeta.js
+++ b/js/forum/src/components/PostMeta.js
@@ -33,7 +33,7 @@ export default class PostMeta extends Component {
         </a>
 
         <div className="Dropdown-menu dropdown-menu">
-          <span className="PostMeta-number">{app.trans('core.forum.post_number_tooltip', {number: post.number()})}</span>{' '}
+          <span className="PostMeta-number">{app.translator.trans('core.forum.post.number_tooltip', {number: post.number()})}</span>{' '}
           {fullTime(time)}
           {touch
             ? <a className="Button PostMeta-permalink" href={permalink}>{permalink}</a>
diff --git a/js/forum/src/components/PostStream.js b/js/forum/src/components/PostStream.js
index 41f1c8c64..7a8715176 100644
--- a/js/forum/src/components/PostStream.js
+++ b/js/forum/src/components/PostStream.js
@@ -221,7 +221,7 @@ class PostStream extends Component {
             if (dt > 1000 * 60 * 60 * 24 * 4) {
               content = [
                 <div className="PostStream-timeGap">
-                  <span>{app.trans('core.forum.post_stream_time_lapsed_text', {period: moment.duration(dt).humanize()})}</span>
+                  <span>{app.translator.trans('core.forum.post_stream.time_lapsed_text', {period: moment.duration(dt).humanize()})}</span>
                 </div>,
                 content
               ];
diff --git a/js/forum/src/components/PostStreamScrubber.js b/js/forum/src/components/PostStreamScrubber.js
index c5816e475..61a25f1f0 100644
--- a/js/forum/src/components/PostStreamScrubber.js
+++ b/js/forum/src/components/PostStreamScrubber.js
@@ -70,7 +70,7 @@ export default class PostStreamScrubber extends Component {
     const unreadCount = this.props.stream.discussion.unreadCount();
     const unreadPercent = Math.min(count - this.index, unreadCount) / count;
 
-    const viewing = app.translator.transChoice('core.forum.post_scrubber_viewing_text', count, {
+    const viewing = app.translator.transChoice('core.forum.post_scrubber.viewing_text', count, {
       index: <span className="Scrubber-index">{retain || formatNumber(this.visibleIndex())}</span>,
       count: <span className="Scrubber-count">{formatNumber(count)}</span>
     });
@@ -100,7 +100,7 @@ export default class PostStreamScrubber extends Component {
         <div className="Dropdown-menu dropdown-menu">
           <div className="Scrubber">
             <a className="Scrubber-first" onclick={this.goToFirst.bind(this)}>
-              {icon('angle-double-up')} {app.trans('core.forum.post_scrubber_original_post_link')}
+              {icon('angle-double-up')} {app.translator.trans('core.forum.post_scrubber.original_post_link')}
             </a>
 
             <div className="Scrubber-scrollbar">
@@ -115,12 +115,12 @@ export default class PostStreamScrubber extends Component {
               <div className="Scrubber-after"/>
 
               <div className="Scrubber-unread" config={styleUnread}>
-                {app.trans('core.forum.post_scrubber_unread_text', {count: unreadCount})}
+                {app.translator.trans('core.forum.post_scrubber.unread_text', {count: unreadCount})}
               </div>
             </div>
 
             <a className="Scrubber-last" onclick={this.goToLast.bind(this)}>
-              {icon('angle-double-down')} {app.trans('core.forum.post_scrubber_now_link')}
+              {icon('angle-double-down')} {app.translator.trans('core.forum.post_scrubber.now_link')}
             </a>
           </div>
         </div>
diff --git a/js/forum/src/components/PostsUserPage.js b/js/forum/src/components/PostsUserPage.js
index 856e67106..7553b8554 100644
--- a/js/forum/src/components/PostsUserPage.js
+++ b/js/forum/src/components/PostsUserPage.js
@@ -51,7 +51,7 @@ export default class PostsUserPage extends UserPage {
       footer = (
         <div className="PostsUserPage-loadMore">
           {Button.component({
-            children: app.trans('core.forum.user_posts_load_more_button'),
+            children: app.translator.trans('core.forum.user.posts_load_more_button'),
             className: 'Button',
             onclick: this.loadMore.bind(this)
           })}
diff --git a/js/forum/src/components/ReplyComposer.js b/js/forum/src/components/ReplyComposer.js
index 5682a993e..fe6c3c8fe 100644
--- a/js/forum/src/components/ReplyComposer.js
+++ b/js/forum/src/components/ReplyComposer.js
@@ -25,9 +25,9 @@ export default class ReplyComposer extends ComposerBody {
   static initProps(props) {
     super.initProps(props);
 
-    props.placeholder = props.placeholder || extractText(app.trans('core.forum.composer_reply_body_placeholder'));
-    props.submitLabel = props.submitLabel || app.trans('core.forum.composer_reply_submit_button');
-    props.confirmExit = props.confirmExit || extractText(app.trans('core.forum.composer_reply_discard_confirmation'));
+    props.placeholder = props.placeholder || extractText(app.translator.trans('core.forum.composer_reply.body_placeholder'));
+    props.submitLabel = props.submitLabel || app.translator.trans('core.forum.composer_reply.submit_button');
+    props.confirmExit = props.confirmExit || extractText(app.translator.trans('core.forum.composer_reply.discard_confirmation'));
   }
 
   headerItems() {
@@ -77,7 +77,7 @@ export default class ReplyComposer extends ComposerBody {
           let alert;
           const viewButton = Button.component({
             className: 'Button Button--link',
-            children: app.trans('core.forum.composer_reply_view_button'),
+            children: app.translator.trans('core.forum.composer_reply.view_button'),
             onclick: () => {
               m.route(app.route.post(post));
               app.alerts.dismiss(alert);
@@ -86,7 +86,7 @@ export default class ReplyComposer extends ComposerBody {
           app.alerts.show(
             alert = new Alert({
               type: 'success',
-              message: app.trans('core.forum.composer_reply_posted_message'),
+              message: app.translator.trans('core.forum.composer_reply.posted_message'),
               controls: [viewButton]
             })
           );
diff --git a/js/forum/src/components/ReplyPlaceholder.js b/js/forum/src/components/ReplyPlaceholder.js
index 09ae106a3..5eeabd4e2 100644
--- a/js/forum/src/components/ReplyPlaceholder.js
+++ b/js/forum/src/components/ReplyPlaceholder.js
@@ -44,7 +44,7 @@ export default class ReplyPlaceholder extends Component {
       <article className="Post ReplyPlaceholder" onclick={reply} onmousedown={triggerClick}>
         <header className="Post-header">
           {avatar(app.session.user, {className: 'PostUser-avatar'})}{' '}
-          {app.trans('core.forum.post_stream_reply_placeholder')}
+          {app.translator.trans('core.forum.post_stream.reply_placeholder')}
         </header>
       </article>
     );
diff --git a/js/forum/src/components/Search.js b/js/forum/src/components/Search.js
index 7ce91f99a..f4402a3a0 100644
--- a/js/forum/src/components/Search.js
+++ b/js/forum/src/components/Search.js
@@ -82,7 +82,7 @@ export default class Search extends Component {
       })}>
         <div className="Search-input">
           <input className="FormControl"
-            placeholder={extractText(app.trans('core.forum.header_search_placeholder'))}
+            placeholder={extractText(app.translator.trans('core.forum.header.search_placeholder'))}
             value={this.value()}
             oninput={m.withAttr('value', this.value)}
             onfocus={() => this.hasFocus = true}
diff --git a/js/forum/src/components/SessionDropdown.js b/js/forum/src/components/SessionDropdown.js
index e4f2ac7d6..5d7d6d9c1 100644
--- a/js/forum/src/components/SessionDropdown.js
+++ b/js/forum/src/components/SessionDropdown.js
@@ -47,7 +47,7 @@ export default class SessionDropdown extends Dropdown {
     items.add('profile',
       LinkButton.component({
         icon: 'user',
-        children: app.trans('core.forum.header_profile_button'),
+        children: app.translator.trans('core.forum.header.profile_button'),
         href: app.route.user(user)
       }),
       100
@@ -56,7 +56,7 @@ export default class SessionDropdown extends Dropdown {
     items.add('settings',
       LinkButton.component({
         icon: 'cog',
-        children: app.trans('core.forum.header_settings_button'),
+        children: app.translator.trans('core.forum.header.settings_button'),
         href: app.route('settings')
       }),
       50
@@ -66,7 +66,7 @@ export default class SessionDropdown extends Dropdown {
       items.add('administration',
         LinkButton.component({
           icon: 'wrench',
-          children: app.trans('core.forum.header_admin_button'),
+          children: app.translator.trans('core.forum.header.admin_button'),
           href: app.forum.attribute('baseUrl') + '/admin',
           target: '_blank',
           config: () => {}
@@ -80,7 +80,7 @@ export default class SessionDropdown extends Dropdown {
     items.add('logOut',
       Button.component({
         icon: 'sign-out',
-        children: app.trans('core.forum.header_log_out_button'),
+        children: app.translator.trans('core.forum.header.log_out_button'),
         onclick: app.session.logout.bind(app.session)
       }),
       -100
diff --git a/js/forum/src/components/SettingsPage.js b/js/forum/src/components/SettingsPage.js
index 4cc39ac0b..497724864 100644
--- a/js/forum/src/components/SettingsPage.js
+++ b/js/forum/src/components/SettingsPage.js
@@ -17,7 +17,7 @@ export default class SettingsPage extends UserPage {
     super.init();
 
     this.show(app.session.user);
-    app.setTitle(app.trans('core.forum.settings_title'));
+    app.setTitle(app.translator.trans('core.forum.settings.title'));
   }
 
   content() {
@@ -38,7 +38,7 @@ export default class SettingsPage extends UserPage {
 
     items.add('account',
       FieldSet.component({
-        label: app.trans('core.forum.settings_account_heading'),
+        label: app.translator.trans('core.forum.settings.account_heading'),
         className: 'Settings-account',
         children: this.accountItems().toArray()
       })
@@ -46,7 +46,7 @@ export default class SettingsPage extends UserPage {
 
     items.add('notifications',
       FieldSet.component({
-        label: app.trans('core.forum.settings_notifications_heading'),
+        label: app.translator.trans('core.forum.settings.notifications_heading'),
         className: 'Settings-notifications',
         children: [NotificationGrid.component({user: this.user})]
       })
@@ -54,7 +54,7 @@ export default class SettingsPage extends UserPage {
 
     items.add('privacy',
       FieldSet.component({
-        label: app.trans('core.forum.settings_privacy_heading'),
+        label: app.translator.trans('core.forum.settings.privacy_heading'),
         className: 'Settings-privacy',
         children: this.privacyItems().toArray()
       })
@@ -73,7 +73,7 @@ export default class SettingsPage extends UserPage {
 
     items.add('changePassword',
       Button.component({
-        children: app.trans('core.forum.settings_change_password_button'),
+        children: app.translator.trans('core.forum.settings.change_password_button'),
         className: 'Button',
         onclick: () => app.modal.show(new ChangePasswordModal())
       })
@@ -81,7 +81,7 @@ export default class SettingsPage extends UserPage {
 
     items.add('changeEmail',
       Button.component({
-        children: app.trans('core.forum.settings_change_email_button'),
+        children: app.translator.trans('core.forum.settings.change_email_button'),
         className: 'Button',
         onclick: () => app.modal.show(new ChangeEmailModal())
       })
@@ -118,7 +118,7 @@ export default class SettingsPage extends UserPage {
 
     items.add('discloseOnline',
       Switch.component({
-        children: app.trans('core.forum.settings_privacy_disclose_online_label'),
+        children: app.translator.trans('core.forum.settings.privacy_disclose_online_label'),
         state: this.user.preferences().discloseOnline,
         onchange: (value, component) => {
           this.user.pushAttributes({lastSeenTime: null});
diff --git a/js/forum/src/components/SignUpModal.js b/js/forum/src/components/SignUpModal.js
index f30946140..f1bc716be 100644
--- a/js/forum/src/components/SignUpModal.js
+++ b/js/forum/src/components/SignUpModal.js
@@ -53,7 +53,7 @@ export default class SignUpModal extends Modal {
   }
 
   title() {
-    return app.trans('core.forum.sign_up_title');
+    return app.translator.trans('core.forum.sign_up.title');
   }
 
   content() {
@@ -73,14 +73,14 @@ export default class SignUpModal extends Modal {
 
       <div className="Form Form--centered">
         <div className="Form-group">
-          <input className="FormControl" name="username" placeholder={extractText(app.trans('core.forum.sign_up_username_placeholder'))}
+          <input className="FormControl" name="username" placeholder={extractText(app.translator.trans('core.forum.sign_up.username_placeholder'))}
             value={this.username()}
             onchange={m.withAttr('value', this.username)}
             disabled={this.loading} />
         </div>
 
         <div className="Form-group">
-          <input className="FormControl" name="email" type="email" placeholder={extractText(app.trans('core.forum.sign_up_email_placeholder'))}
+          <input className="FormControl" name="email" type="email" placeholder={extractText(app.translator.trans('core.forum.sign_up.email_placeholder'))}
             value={this.email()}
             onchange={m.withAttr('value', this.email)}
             disabled={this.loading || (this.props.token && this.props.email)} />
@@ -88,7 +88,7 @@ export default class SignUpModal extends Modal {
 
         {this.props.token ? '' : (
           <div className="Form-group">
-            <input className="FormControl" name="password" type="password" placeholder={extractText(app.trans('core.forum.sign_up_password_placeholder'))}
+            <input className="FormControl" name="password" type="password" placeholder={extractText(app.translator.trans('core.forum.sign_up.password_placeholder'))}
               value={this.password()}
               onchange={m.withAttr('value', this.password)}
               disabled={this.loading} />
@@ -100,7 +100,7 @@ export default class SignUpModal extends Modal {
             className="Button Button--primary Button--block"
             type="submit"
             loading={this.loading}>
-            {app.trans('core.forum.sign_up_submit_button')}
+            {app.translator.trans('core.forum.sign_up.submit_button')}
           </Button>
         </div>
       </div>
@@ -119,13 +119,13 @@ export default class SignUpModal extends Modal {
           <div className="darkenBackground">
             <div className="container">
               {avatar(user)}
-              <h3>{app.trans('core.forum.sign_up_welcome_text', {user})}</h3>
+              <h3>{app.translator.trans('core.forum.sign_up.welcome_text', {user})}</h3>
 
-              <p>{app.trans('core.forum.sign_up_confirmation_message', {email: <strong>{user.email()}</strong>})}</p>
+              <p>{app.translator.trans('core.forum.sign_up.confirmation_message', {email: <strong>{user.email()}</strong>})}</p>
 
               <p>
                 <Button className="Button Button--primary" onclick={this.hide.bind(this)}>
-                  {app.trans('core.forum.sign_up_dismiss_button')}
+                  {app.translator.trans('core.forum.sign_up.dismiss_button')}
                 </Button>
               </p>
             </div>
@@ -140,7 +140,7 @@ export default class SignUpModal extends Modal {
   footer() {
     return [
       <p className="SignUpModal-logIn">
-        {app.trans('core.forum.sign_up_log_in_text', {a: <a onclick={this.logIn.bind(this)}/>})}
+        {app.translator.trans('core.forum.sign_up.log_in_text', {a: <a onclick={this.logIn.bind(this)}/>})}
       </p>
     ];
   }
diff --git a/js/forum/src/components/TerminalPost.js b/js/forum/src/components/TerminalPost.js
index 6dff174c1..36267238c 100644
--- a/js/forum/src/components/TerminalPost.js
+++ b/js/forum/src/components/TerminalPost.js
@@ -21,7 +21,7 @@ export default class TerminalPost extends Component {
     return (
       <span>
         {lastPost ? icon('reply') : ''}{' '}
-        {app.trans('core.forum.discussion_list_' + (lastPost ? 'replied' : 'started') + '_text', {
+        {app.translator.trans('core.forum.discussion_list.' + (lastPost ? 'replied' : 'started') + '_text', {
           user,
           ago: humanTime(time)
         })}
diff --git a/js/forum/src/components/UserBio.js b/js/forum/src/components/UserBio.js
index 29fe670cc..e14c6600e 100644
--- a/js/forum/src/components/UserBio.js
+++ b/js/forum/src/components/UserBio.js
@@ -29,7 +29,7 @@ export default class UserBio extends Component {
     let content;
 
     if (this.editing) {
-      content = <textarea className="FormControl" placeholder={extractText(app.trans('core.forum.user_bio_placeholder'))} rows="3" value={user.bio()}/>;
+      content = <textarea className="FormControl" placeholder={extractText(app.translator.trans('core.forum.user.bio_placeholder'))} rows="3" value={user.bio()}/>;
     } else {
       let subContent;
 
@@ -41,7 +41,7 @@ export default class UserBio extends Component {
         if (bioHtml) {
           subContent = m.trust(bioHtml);
         } else if (this.props.editable) {
-          subContent = <p className="UserBio-placeholder">{app.trans('core.forum.user_bio_placeholder')}</p>;
+          subContent = <p className="UserBio-placeholder">{app.translator.trans('core.forum.user.bio_placeholder')}</p>;
         }
       }
 
diff --git a/js/forum/src/components/UserCard.js b/js/forum/src/components/UserCard.js
index 4523179a9..6b9fec29c 100644
--- a/js/forum/src/components/UserCard.js
+++ b/js/forum/src/components/UserCard.js
@@ -40,7 +40,7 @@ export default class UserCard extends Component {
               className: 'UserCard-controls App-primaryControl',
               menuClassName: 'Dropdown-menu--right',
               buttonClassName: this.props.controlsButtonClassName,
-              label: app.trans('core.forum.user_controls_button'),
+              label: app.translator.trans('core.forum.user_controls.button'),
               icon: 'ellipsis-v'
             }) : ''}
 
@@ -95,13 +95,13 @@ export default class UserCard extends Component {
       items.add('lastSeen', (
         <span className={'UserCard-lastSeen' + (online ? ' online' : '')}>
           {online
-            ? [icon('circle'), ' ', app.trans('core.forum.user_online_text')]
+            ? [icon('circle'), ' ', app.translator.trans('core.forum.user.online_text')]
             : [icon('clock-o'), ' ', humanTime(lastSeenTime)]}
         </span>
       ));
     }
 
-    items.add('joined', app.trans('core.forum.user_joined_date_text', {ago: humanTime(user.joinTime())}));
+    items.add('joined', app.translator.trans('core.forum.user.joined_date_text', {ago: humanTime(user.joinTime())}));
 
     return items;
   }
diff --git a/js/forum/src/components/UserPage.js b/js/forum/src/components/UserPage.js
index 028fce827..330c47791 100644
--- a/js/forum/src/components/UserPage.js
+++ b/js/forum/src/components/UserPage.js
@@ -131,7 +131,7 @@ export default class UserPage extends Page {
     items.add('posts',
       LinkButton.component({
         href: app.route('user.posts', {username: user.username()}),
-        children: [app.trans('core.forum.user_posts_link'), <span className="Button-badge">{user.commentsCount()}</span>],
+        children: [app.translator.trans('core.forum.user.posts_link'), <span className="Button-badge">{user.commentsCount()}</span>],
         icon: 'comment-o'
       })
     );
@@ -139,7 +139,7 @@ export default class UserPage extends Page {
     items.add('discussions',
       LinkButton.component({
         href: app.route('user.discussions', {username: user.username()}),
-        children: [app.trans('core.forum.user_discussions_link'), <span className="Button-badge">{user.discussionsCount()}</span>],
+        children: [app.translator.trans('core.forum.user.discussions_link'), <span className="Button-badge">{user.discussionsCount()}</span>],
         icon: 'reorder'
       })
     );
@@ -149,7 +149,7 @@ export default class UserPage extends Page {
       items.add('settings',
         LinkButton.component({
           href: app.route('settings'),
-          children: app.trans('core.forum.user_settings_link'),
+          children: app.translator.trans('core.forum.user.settings_link'),
           icon: 'cog'
         })
       );
diff --git a/js/forum/src/components/UsersSearchSource.js b/js/forum/src/components/UsersSearchSource.js
index 78c910923..f1b2d5222 100644
--- a/js/forum/src/components/UsersSearchSource.js
+++ b/js/forum/src/components/UsersSearchSource.js
@@ -22,7 +22,7 @@ export default class UsersSearchResults {
     if (!results.length) return '';
 
     return [
-      <li className="Dropdown-header">{app.trans('core.forum.search_users_heading')}</li>,
+      <li className="Dropdown-header">{app.translator.trans('core.forum.search.users_heading')}</li>,
       results.map(user => (
         <li className="UserSearchResult" data-index={'users' + user.id()}>
           <a href={app.route.user(user)} config={m.route}>
diff --git a/js/forum/src/utils/DiscussionControls.js b/js/forum/src/utils/DiscussionControls.js
index 9c54495ba..414a7184f 100644
--- a/js/forum/src/utils/DiscussionControls.js
+++ b/js/forum/src/utils/DiscussionControls.js
@@ -55,14 +55,14 @@ export default {
         !app.session.user || discussion.canReply()
           ? Button.component({
             icon: 'reply',
-            children: app.trans(app.session.user ? 'core.forum.discussion_controls_reply_button' : 'core.forum.discussion_controls_log_in_to_reply_button'),
+            children: app.translator.translator.trans(app.session.user ? 'core.forum.discussion_controls.reply_button' : 'core.forum.discussion_controls.log_in_to_reply_button'),
             onclick: this.replyAction.bind(discussion, true, false)
           })
           : Button.component({
             icon: 'reply',
-            children: app.trans('core.forum.discussion_controls_cannot_reply_button'),
+            children: app.translator.trans('core.forum.discussion_controls.cannot_reply_button'),
             className: 'disabled',
-            title: app.trans('core.forum.discussion_controls_cannot_reply_text')
+            title: app.translator.trans('core.forum.discussion_controls.cannot_reply_text')
           })
       );
     }
@@ -85,7 +85,7 @@ export default {
     if (discussion.canRename()) {
       items.add('rename', Button.component({
         icon: 'pencil',
-        children: app.trans('core.forum.discussion_controls_rename_button'),
+        children: app.translator.trans('core.forum.discussion_controls.rename_button'),
         onclick: this.renameAction.bind(discussion)
       }));
     }
@@ -109,21 +109,21 @@ export default {
       if (discussion.canHide()) {
         items.add('hide', Button.component({
           icon: 'trash-o',
-          children: app.trans('core.forum.discussion_controls_delete_button'),
+          children: app.translator.trans('core.forum.discussion_controls.delete_button'),
           onclick: this.hideAction.bind(discussion)
         }));
       }
     } else if (discussion.canDelete()) {
       items.add('restore', Button.component({
         icon: 'reply',
-        children: app.trans('core.forum.discussion_controls_restore_button'),
+        children: app.translator.trans('core.forum.discussion_controls.restore_button'),
         onclick: this.restoreAction.bind(discussion),
         disabled: discussion.commentsCount() === 0
       }));
 
       items.add('delete', Button.component({
         icon: 'times',
-        children: app.trans('core.forum.discussion_controls_delete_forever_button'),
+        children: app.translator.trans('core.forum.discussion_controls.delete_forever_button'),
         onclick: this.deleteAction.bind(discussion)
       }));
     }
@@ -216,7 +216,7 @@ export default {
    * @return {Promise}
    */
   deleteAction() {
-    if (confirm(extractText(app.trans('core.forum.discussion_controls_delete_confirmation')))) {
+    if (confirm(extractText(app.translator.trans('core.forum.discussion_controls.delete_confirmation')))) {
       // If there is a discussion list in the cache, remove this discussion.
       if (app.cache.discussionList) {
         app.cache.discussionList.removeDiscussion(this);
@@ -239,7 +239,7 @@ export default {
    */
   renameAction() {
     const currentTitle = this.title();
-    const title = prompt(extractText(app.trans('core.forum.discussion_controls_rename_text')), currentTitle);
+    const title = prompt(extractText(app.translator.trans('core.forum.discussion_controls.rename_text')), currentTitle);
 
     // If the title is different to what it was before, then save it. After the
     // save has completed, update the post stream as there will be a new post
diff --git a/js/forum/src/utils/PostControls.js b/js/forum/src/utils/PostControls.js
index 5ba6a7634..afe6a1ab3 100644
--- a/js/forum/src/utils/PostControls.js
+++ b/js/forum/src/utils/PostControls.js
@@ -60,7 +60,7 @@ export default {
       if (!post.isHidden()) {
         items.add('edit', Button.component({
           icon: 'pencil',
-          children: app.trans('core.forum.post_controls_edit_button'),
+          children: app.translator.trans('core.forum.post_controls.edit_button'),
           onclick: this.editAction.bind(post)
         }));
       }
@@ -85,7 +85,7 @@ export default {
       if (post.canEdit()) {
         items.add('hide', Button.component({
           icon: 'trash-o',
-          children: app.trans('core.forum.post_controls_delete_button'),
+          children: app.translator.trans('core.forum.post_controls.delete_button'),
           onclick: this.hideAction.bind(post)
         }));
       }
@@ -93,14 +93,14 @@ export default {
       if (post.contentType() === 'comment' && post.canEdit()) {
         items.add('restore', Button.component({
           icon: 'reply',
-          children: app.trans('core.forum.post_controls_restore_button'),
+          children: app.translator.trans('core.forum.post_controls.restore_button'),
           onclick: this.restoreAction.bind(post)
         }));
       }
       if (post.canDelete() && post.number() !== 1) {
         items.add('delete', Button.component({
           icon: 'times',
-          children: app.trans('core.forum.post_controls_delete_forever_button'),
+          children: app.translator.trans('core.forum.post_controls.delete_forever_button'),
           onclick: this.deleteAction.bind(post)
         }));
       }
diff --git a/js/forum/src/utils/UserControls.js b/js/forum/src/utils/UserControls.js
index a420eaa9a..a84207e24 100644
--- a/js/forum/src/utils/UserControls.js
+++ b/js/forum/src/utils/UserControls.js
@@ -60,7 +60,7 @@ export default {
     if (user.canEdit()) {
       items.add('edit', Button.component({
         icon: 'pencil',
-        children: app.trans('core.forum.user_controls_edit_button'),
+        children: app.translator.trans('core.forum.user_controls.edit_button'),
         onclick: this.editAction.bind(user)
       }));
     }
@@ -83,7 +83,7 @@ export default {
     if (user.id() !== '1' && user.canDelete()) {
       items.add('delete', Button.component({
         icon: 'times',
-        children: app.trans('core.forum.user_controls_delete_button'),
+        children: app.translator.trans('core.forum.user_controls.delete_button'),
         onclick: this.deleteAction.bind(user)
       }));
     }
@@ -95,7 +95,7 @@ export default {
    * Delete the user.
    */
   deleteAction() {
-    if (confirm(app.trans('core.forum.user_controls_delete_confirmation'))) {
+    if (confirm(app.translator.trans('core.forum.user_controls.delete_confirmation'))) {
       this.delete().then(() => {
         if (app.current instanceof UserPage && app.current.user === this) {
           app.history.back();
diff --git a/js/lib/App.js b/js/lib/App.js
index e136e257e..cd36c40f9 100644
--- a/js/lib/App.js
+++ b/js/lib/App.js
@@ -255,16 +255,16 @@ export default class App {
 
         case 401:
         case 403:
-          children = 'You do not have permission to do that.';
+          children = app.translator.trans('core.lib.error.permission_denied_message');
           break;
 
         case 404:
         case 410:
-          children = 'The requested resource was not found.';
+          children = app.translator.trans('core.lib.error.not_found_message');
           break;
 
         default:
-          children = 'Oops! Something went wrong. Please reload the page and try again.';
+          children = app.translator.trans('core.lib.error.generic_message');
       }
 
       error.alert = new Alert({
diff --git a/js/lib/helpers/punctuateSeries.js b/js/lib/helpers/punctuateSeries.js
index 219e311fa..8a53c1b24 100644
--- a/js/lib/helpers/punctuateSeries.js
+++ b/js/lib/helpers/punctuateSeries.js
@@ -11,7 +11,7 @@
  */
 export default function punctuateSeries(items) {
   if (items.length === 2) {
-    return app.trans('core.lib.series_two_text', {
+    return app.translator.trans('core.lib.series.two_text', {
       first: items[0],
       second: items[1]
     });
@@ -21,10 +21,10 @@ export default function punctuateSeries(items) {
     // into the translator along with the first and last item.
     const second = items
       .slice(1, items.length - 1)
-      .reduce((list, item) => list.concat([item, app.trans('core.lib.series_glue_text')]), [])
+      .reduce((list, item) => list.concat([item, app.translator.trans('core.lib.series.glue_text')]), [])
       .slice(0, -1);
 
-    return app.trans('core.lib.series_three_text', {
+    return app.translator.trans('core.lib.series.three_text', {
       first: items[0],
       second,
       third: items[items.length - 1]
diff --git a/js/lib/helpers/username.js b/js/lib/helpers/username.js
index 8134d558d..1396edf1e 100644
--- a/js/lib/helpers/username.js
+++ b/js/lib/helpers/username.js
@@ -6,7 +6,7 @@
  * @return {Object}
  */
 export default function username(user) {
-  const name = (user && user.username()) || app.trans('core.forum.user_deleted_text');
+  const name = (user && user.username()) || app.translator.trans('core.lib.deleted_user_text');
 
   return <span className="username">{name}</span>;
 }
diff --git a/js/lib/models/Discussion.js b/js/lib/models/Discussion.js
index 210286075..e3135a37f 100644
--- a/js/lib/models/Discussion.js
+++ b/js/lib/models/Discussion.js
@@ -86,7 +86,7 @@ Object.assign(Discussion.prototype, {
     const items = new ItemList();
 
     if (this.isHidden()) {
-      items.add('hidden', <Badge type="hidden" icon="trash" label="Hidden"/>);
+      items.add('hidden', <Badge type="hidden" icon="trash" label={app.translator.trans('core.lib.hidden_discussion_tooltip')}/>);
     }
 
     return items;
diff --git a/js/lib/utils/abbreviateNumber.js b/js/lib/utils/abbreviateNumber.js
index a86d7de0b..360c6b82d 100644
--- a/js/lib/utils/abbreviateNumber.js
+++ b/js/lib/utils/abbreviateNumber.js
@@ -11,9 +11,9 @@
 export default function abbreviateNumber(number) {
   // TODO: translation
   if (number >= 1000000) {
-    return Math.floor(number / 1000000) + 'M';
+    return Math.floor(number / 1000000) + app.translator.trans('core.lib.number_suffix.mega_text');
   } else if (number >= 1000) {
-    return Math.floor(number / 1000) + 'K';
+    return Math.floor(number / 1000) + app.translator.trans('core.lib.number_suffix.kilo_text');
   } else {
     return number.toString();
   }

From 659cfb72addcc464f430bdae1399422d6e2e049b Mon Sep 17 00:00:00 2001
From: dcsjapan <dcsjapan@users.noreply.github.com>
Date: Tue, 20 Oct 2015 15:44:58 +0900
Subject: [PATCH 2/2] Fix the double correction

- Fixes one mangled app.translator call.
---
 js/forum/src/utils/DiscussionControls.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/js/forum/src/utils/DiscussionControls.js b/js/forum/src/utils/DiscussionControls.js
index 414a7184f..d62c3e4e5 100644
--- a/js/forum/src/utils/DiscussionControls.js
+++ b/js/forum/src/utils/DiscussionControls.js
@@ -55,7 +55,7 @@ export default {
         !app.session.user || discussion.canReply()
           ? Button.component({
             icon: 'reply',
-            children: app.translator.translator.trans(app.session.user ? 'core.forum.discussion_controls.reply_button' : 'core.forum.discussion_controls.log_in_to_reply_button'),
+            children: app.translator.trans(app.session.user ? 'core.forum.discussion_controls.reply_button' : 'core.forum.discussion_controls.log_in_to_reply_button'),
             onclick: this.replyAction.bind(discussion, true, false)
           })
           : Button.component({