FEATURE: Support for localized themes (#6848)

FEATURE: Support for localized themes (#6848)

  • Themes can supply translation files in a format like /locales/{locale}.yml. These files should be valid YAML, with a single top level key equal to the locale being defined. For now these can only be defined using the discourse_theme CLI, importing a .tar.gz, or from a GIT repository.

  • Fallback is handled on a global level (if the locale is not defined in the theme), as well as on individual keys (if some keys are missing from the selected interface language).

  • Administrators can override individual keys on a per-theme basis in the /admin/customize/themes user interface.

  • Theme developers should access defined translations using the new theme prefix variables: JavaScript: I18n.t(themePrefix("my_translation_key")) Handlebars: {{theme-i18n "my_translation_key"}} or {{i18n (theme-prefix "my_translation_key")}}

  • To design for backwards compatibility, theme developers can check for the presence of the themePrefix variable in JavaScript

  • As part of this, the old {{themeSetting.setting_name}} syntax is deprecated in favour of {{theme-setting "setting_name"}}

diff --git a/app/assets/javascripts/admin/components/theme-setting-editor.js.es6 b/app/assets/javascripts/admin/components/theme-setting-editor.js.es6
new file mode 100644
index 0000000..c95dd22
--- /dev/null
+++ b/app/assets/javascripts/admin/components/theme-setting-editor.js.es6
@@ -0,0 +1,12 @@
+import BufferedContent from "discourse/mixins/buffered-content";
+import SettingComponent from "admin/mixins/setting-component";
+
+export default Ember.Component.extend(BufferedContent, SettingComponent, {
+  layoutName: "admin/templates/components/site-setting",
+  _save() {
+    return this.get("model").saveSettings(
+      this.get("setting.setting"),
+      this.get("buffered.value")
+    );
+  }
+});
diff --git a/app/assets/javascripts/admin/components/theme-setting.js.es6 b/app/assets/javascripts/admin/components/theme-setting.js.es6
deleted file mode 100644
index c95dd22..0000000
--- a/app/assets/javascripts/admin/components/theme-setting.js.es6
+++ /dev/null
@@ -1,12 +0,0 @@
-import BufferedContent from "discourse/mixins/buffered-content";
-import SettingComponent from "admin/mixins/setting-component";
-
-export default Ember.Component.extend(BufferedContent, SettingComponent, {
-  layoutName: "admin/templates/components/site-setting",
-  _save() {
-    return this.get("model").saveSettings(
-      this.get("setting.setting"),
-      this.get("buffered.value")
-    );
-  }
-});
diff --git a/app/assets/javascripts/admin/components/theme-translation.js.es6 b/app/assets/javascripts/admin/components/theme-translation.js.es6
new file mode 100644
index 0000000..ad0cb8a
--- /dev/null
+++ b/app/assets/javascripts/admin/components/theme-translation.js.es6
@@ -0,0 +1,16 @@
+import BufferedContent from "discourse/mixins/buffered-content";
+import SettingComponent from "admin/mixins/setting-component";
+
+export default Ember.Component.extend(BufferedContent, SettingComponent, {
+  layoutName: "admin/templates/components/site-setting",
+  setting: Ember.computed.alias("translation"),
+  type: "string",
+  settingName: Ember.computed.alias("translation.key"),
+
+  _save() {
+    return this.get("model").saveTranslation(
+      this.get("translation.key"),
+      this.get("buffered.value")
+    );
+  }
+});
diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6
index 0b31a29..807c21e 100644
--- a/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6
+++ b/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6
@@ -16,7 +16,8 @@ export default Ember.Controller.extend({
     { id: 0, name: "common" },
     { id: 1, name: "desktop" },
     { id: 2, name: "mobile" },
-    { id: 3, name: "settings" }
+    { id: 3, name: "settings" },
+    { id: 4, name: "translations" }
   ],
 
   fieldsForTarget: function(target) {
diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6
index a5a703c..5b67d6d 100644
--- a/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6
+++ b/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6
@@ -90,11 +90,15 @@ export default Ember.Controller.extend({
     return settings.map(setting => ThemeSettings.create(setting));
   },
 
-  @computed("settings")
-  hasSettings(settings) {
-    return settings.length > 0;
+  hasSettings: Ember.computed.notEmpty("settings"),
+
+  @computed("model.translations")
+  translations(translations) {
+    return translations.map(setting => ThemeSettings.create(setting));
   },
 
+  hasTranslations: Ember.computed.notEmpty("translations"),
+
   @computed("model.remoteError", "updatingRemote")
   showRemoteError(errorMessage, updating) {
     return errorMessage && !updating;
diff --git a/app/assets/javascripts/admin/models/theme.js.es6 b/app/assets/javascripts/admin/models/theme.js.es6
index 83f42d9..a3ae230 100644
--- a/app/assets/javascripts/admin/models/theme.js.es6
+++ b/app/assets/javascripts/admin/models/theme.js.es6
@@ -188,6 +188,10 @@ const Theme = RestModel.extend({
     const settings = {};
     settings[name] = value;
     return this.save({ settings });
+  },
+
+  saveTranslation(name, value) {
+    return this.save({ translations: { [name]: value } });
   }
 });
 
diff --git a/app/assets/javascripts/admin/templates/customize-themes-show.hbs b/app/assets/javascripts/admin/templates/customize-themes-show.hbs
index de44bd6..fd47daf 100644
--- a/app/assets/javascripts/admin/templates/customize-themes-show.hbs
+++ b/app/assets/javascripts/admin/templates/customize-themes-show.hbs
@@ -138,7 +138,18 @@
       <div class="mini-title">{{i18n "admin.customize.theme.theme_settings"}}</div>
       {{#d-section class="form-horizontal theme settings"}}
         {{#each settings as |setting|}}
-          {{theme-setting setting=setting model=model class="theme-setting"}}
+          {{theme-setting-editor setting=setting model=model class="theme-setting"}}
+        {{/each}}
+      {{/d-section}}
+    </div>
+  {{/if}}
+
+  {{#if hasTranslations}}
+    <div class="control-unit">
+      <div class="mini-title">{{i18n "admin.customize.theme.theme_translations"}}</div>
+      {{#d-section class="form-horizontal theme settings translations"}}
+        {{#each translations as |translation|}}
+          {{theme-translation translation=translation model=model class="theme-translation"}}
         {{/each}}
       {{/d-section}}
     </div>
diff --git a/app/assets/javascripts/discourse-common/lib/helpers.js.es6 b/app/assets/javascripts/discourse-common/lib/helpers.js.es6
index 4e83da6..bbaaf62 100644
--- a/app/assets/javascripts/discourse-common/lib/helpers.js.es6
+++ b/app/assets/javascripts/discourse-common/lib/helpers.js.es6
@@ -46,19 +46,24 @@ function resolveParams(ctx, options) {
 }
 
 export function registerUnbound(name, fn) {
-  const func = function(property, options) {
-    if (
-      options.types &&
-      (options.types[0] === "ID" || options.types[0] === "PathExpression")
-    ) {
-      property = get(this, property, options);
+  const func = function(...args) {
+    const options = args.pop();
+    const properties = args;
+
+    for (let i = 0; i < properties.length; i++) {
+      if (
+        options.types &&
+        (options.types[i] === "ID" || options.types[i] === "PathExpression")
+      ) {
+        properties[i] = get(this, properties[i], options);
+      }
     }
 
-    return fn.call(this, property, resolveParams(this, options));
+    return fn.call(this, ...properties, resolveParams(this, options));
   };
 
   _helpers[name] = Ember.Helper.extend({
-    compute: (params, args) => fn(params[0], args)
+    compute: (params, args) => fn(...params, args)
   });
   Handlebars.registerHelper(name, func);
 }
diff --git a/app/assets/javascripts/discourse/helpers/theme-helpers.js.es6 b/app/assets/javascripts/discourse/helpers/theme-helpers.js.es6
new file mode 100644
index 0000000..eae8dfa
--- /dev/null
+++ b/app/assets/javascripts/discourse/helpers/theme-helpers.js.es6
@@ -0,0 +1,23 @@
+import { registerUnbound } from "discourse-common/lib/helpers";
+import deprecated from "discourse-common/lib/deprecated";
+
+registerUnbound("theme-i18n", (themeId, key, params) => {
+  return I18n.t(`theme_translations.${themeId}.${key}`, params);
+});
+
+registerUnbound(
+  "theme-prefix",
+  (themeId, key) => `theme_translations.${themeId}.${key}`
+);
+
+registerUnbound("theme-setting", (themeId, key, hash) => {
+  if (hash.deprecated) {
+    deprecated(
+      "The `{{themeSetting.setting_name}}` syntax is deprecated. Use `{{theme-setting 'setting_name'}}` instead",
+      { since: "v2.2.0.beta8", dropFrom: "v2.3.0" }
+    );
+  }
+  return Discourse.__container__
+    .lookup("service:theme-settings")
+    .getSetting(themeId, key);
+});

[... diff too long, it was truncated ...]

GitHub sha: 880311dd

1 Like

This commit has been mentioned on Discourse Meta. There might be relevant details there:

This commit has been mentioned on Discourse Meta. There might be relevant details there:

This commit has been mentioned on Discourse Meta. There might be relevant details there:

This commit has been mentioned on Discourse Meta. There might be relevant details there: