Skip to content

Commit

Permalink
Theme translations and blog localisation (TryGhost#8437)
Browse files Browse the repository at this point in the history
refs TryGhost#5345, refs TryGhost#3801

- Blog localisation
  - default is `en` (English)
  - you can change the language code in the admin panel, see TryGhost/Admin#703
  - blog behaviour changes depending on the language e.g. date helper format
  - theme translation get's loaded if available depending on the language setting
  - falls back to english if not available

- Theme translation
  - complete automatic translation of Ghost's frontend for site visitors (themes, etc.), to quickly deploy a site in a non-English language
  - added {{t}} and {{lang}} helper
  - no backend or admin panel translations (!)
  - easily readable translation keys - very simple translation
  - server restart required when adding new language files or changing existing files in the theme
  - no language code validation for now (will be added soon)
  - a full theme translation requires to translate Ghost core templates (e.g. subscriber form)
  - when activating a different theme, theme translations are auto re-loaded
  - when switching language of blog, theme translations are auto re-loaded

- Bump gscan to version 1.3.0 to support more known helpers

**Documentation can be found at https://themes.ghost.org/v1.20.0/docs/i18n.**
  • Loading branch information
juan-g authored and kirrg001 committed Jan 9, 2018
1 parent dcb2aa9 commit f671f9d
Show file tree
Hide file tree
Showing 16 changed files with 369 additions and 65 deletions.
15 changes: 11 additions & 4 deletions core/server/helpers/date.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@

var proxy = require('./proxy'),
moment = require('moment-timezone'),
SafeString = proxy.SafeString;
SafeString = proxy.SafeString,
i18n = proxy.i18n;

module.exports = function (date, options) {
var timezone, format, timeago, timeNow;
var timezone, format, timeago, timeNow, dateMoment;

if (!options && date.hasOwnProperty('hash')) {
options = date;
Expand All @@ -30,10 +31,16 @@ module.exports = function (date, options) {
timezone = options.data.blog.timezone;
timeNow = moment().tz(timezone);

// i18n: Making dates, including month names, translatable to any language.
// Documentation: http://momentjs.com/docs/#/i18n/
// Locales: https://github.com/moment/moment/tree/develop/locale
dateMoment = moment(date);
dateMoment.locale(i18n.locale());

if (timeago) {
date = timezone ? moment(date).tz(timezone).from(timeNow) : moment(date).fromNow();
date = timezone ? dateMoment.tz(timezone).from(timeNow) : dateMoment.fromNow();
} else {
date = timezone ? moment(date).tz(timezone).format(format) : moment(date).format(format);
date = timezone ? dateMoment.tz(timezone).format(format) : dateMoment.format(format);
}

return new SafeString(date);
Expand Down
4 changes: 4 additions & 0 deletions core/server/helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ coreHelpers.ghost_head = require('./ghost_head');
coreHelpers.img_url = require('./img_url');
coreHelpers.is = require('./is');
coreHelpers.has = require('./has');
coreHelpers.lang = require('./lang');
coreHelpers.meta_description = require('./meta_description');
coreHelpers.meta_title = require('./meta_title');
coreHelpers.navigation = require('./navigation');
Expand All @@ -29,6 +30,7 @@ coreHelpers.post_class = require('./post_class');
coreHelpers.prev_post = require('./prev_next');
coreHelpers.next_post = require('./prev_next');
coreHelpers.reading_time = require('./reading_time');
coreHelpers.t = require('./t');
coreHelpers.tags = require('./tags');
coreHelpers.title = require('./title');
coreHelpers.twitter_url = require('./twitter_url');
Expand All @@ -47,6 +49,7 @@ registerAllCoreHelpers = function registerAllCoreHelpers() {
registerThemeHelper('has', coreHelpers.has);
registerThemeHelper('is', coreHelpers.is);
registerThemeHelper('img_url', coreHelpers.img_url);
registerThemeHelper('lang', coreHelpers.lang);
registerThemeHelper('meta_description', coreHelpers.meta_description);
registerThemeHelper('meta_title', coreHelpers.meta_title);
registerThemeHelper('navigation', coreHelpers.navigation);
Expand All @@ -55,6 +58,7 @@ registerAllCoreHelpers = function registerAllCoreHelpers() {
registerThemeHelper('plural', coreHelpers.plural);
registerThemeHelper('post_class', coreHelpers.post_class);
registerThemeHelper('reading_time', coreHelpers.reading_time);
registerThemeHelper('t', coreHelpers.t);
registerThemeHelper('tags', coreHelpers.tags);
registerThemeHelper('title', coreHelpers.title);
registerThemeHelper('twitter_url', coreHelpers.twitter_url);
Expand Down
21 changes: 21 additions & 0 deletions core/server/helpers/lang.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// # lang helper
// {{lang}} gives the current language tag
// Usage example: <html lang="{{lang}}">
//
// Examples of language tags from RFC 5646:
// de (German)
// fr (French)
// ja (Japanese)
// en-US (English as used in the United States)
//
// Standard:
// Language tags in HTML and XML
// https://www.w3.org/International/articles/language-tags/

var proxy = require('./proxy'),
i18n = proxy.i18n,
SafeString = proxy.SafeString;

module.exports = function lang() {
return new SafeString(i18n.locale());
};
7 changes: 4 additions & 3 deletions core/server/helpers/plural.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// # Plural Helper
// Usage: `{{plural 0 empty='No posts' singular='% post' plural='% posts'}}`
// Usage example: `{{plural ../pagination.total empty='No posts' singular='1 post' plural='% posts'}}`
// or for translatable themes, with (t) translation helper's subexpressions:
// `{{plural ../pagination.total empty=(t "No posts") singular=(t "1 post") plural=(t "% posts")}}`
//
// pluralises strings depending on item count
// Pluralises strings depending on item count
//
// The 1st argument is the numeric variable which the helper operates on
// The 2nd argument is the string that will be output if the variable's value is 0
Expand Down Expand Up @@ -30,4 +32,3 @@ module.exports = function plural(number, options) {
return new SafeString(options.hash.plural.replace('%', number));
}
};

26 changes: 22 additions & 4 deletions core/server/helpers/reading_time.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
// # Reading Time Helper
//
// Usage: `{{reading_time}}`
// or for translatable themes, with (t) translation helper's subexpressions:
// `{{reading_time seconds=(t "< 1 min read") minute=(t "1 min read") minutes=(t "% min read")}}`
// and in the theme translation file, for example Spanish es.json:
// "< 1 min read": "< 1 min de lectura",
// "1 min read": "1 min de lectura",
// "% min read": "% min de lectura",
//
// Returns estimated reading time for post

var proxy = require('./proxy'),
_ = require('lodash'),
schema = require('../data/schema').checks,
SafeString = proxy.SafeString,
localUtils = proxy.localUtils;

module.exports = function reading_time() {// eslint-disable-line camelcase
module.exports = function reading_time(options) {// eslint-disable-line camelcase
options = options || {};
options.hash = options.hash || {};

var html,
wordsPerMinute = 275,
wordsPerSecond = wordsPerMinute / 60,
wordCount,
imageCount,
readingTimeSeconds,
readingTime;
readingTimeMinutes,
readingTime,
seconds = _.isString(options.hash.seconds) ? options.hash.seconds : '< 1 min read',
minute = _.isString(options.hash.minute) ? options.hash.minute : '1 min read',
minutes = _.isString(options.hash.minutes) ? options.hash.minutes : '% min read';

// only calculate reading time for posts
if (!schema.isPost(this)) {
Expand All @@ -31,10 +45,14 @@ module.exports = function reading_time() {// eslint-disable-line camelcase
// add 12 seconds to reading time if feature image is present
readingTimeSeconds = imageCount ? readingTimeSeconds + 12 : readingTimeSeconds;

readingTimeMinutes = Math.round(readingTimeSeconds / 60);

if (readingTimeSeconds < 60) {
readingTime = '< 1 min read';
readingTime = seconds;
} else if (readingTimeMinutes === 1) {
readingTime = minute;
} else {
readingTime = `${Math.round(readingTimeSeconds / 60)} min read`;
readingTime = minutes.replace('%', readingTimeMinutes);
}

return new SafeString(readingTime);
Expand Down
26 changes: 26 additions & 0 deletions core/server/helpers/t.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// # t helper
// i18n: Translatable handlebars expressions for templates of the front-end and themes.
// Front-end: .hbs templates in core/server, overridden by copies in themes. Themes: in content/themes.
//
// Usage examples, for example in .hbs theme templates:
// {{t "Get the latest posts delivered right to your inbox"}}
// {{{t "Proudly published with {ghostlink}" ghostlink="<a href=\"https://ghost.org\">Ghost</a>"}}}
//
// To preserve HTML, use {{{t}}}. This helper doesn't use a SafeString object which would prevent escaping,
// because often other helpers need that (t) returns a string to be able to work as subexpression; e.g.:
// {{tags prefix=(t " on ")}}

var proxy = require('./proxy'),
i18n = proxy.i18n;

module.exports = function t(text, options) {
var bindings = {},
prop;
for (prop in options.hash) {
if (options.hash.hasOwnProperty(prop)) {
bindings[prop] = options.hash[prop];
}
}
bindings.isThemeString = true;
return i18n.t(text, bindings);
};
11 changes: 9 additions & 2 deletions core/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ function init() {

var ghostServer, parentApp;

// Initialize Internationalization
// Initialize default internationalization, just for core now
// (settings for language and theme not yet available here)
common.i18n.init();
debug('I18n done');
debug('Default i18n done for core');
models.init();
debug('models done');

Expand All @@ -52,6 +53,12 @@ function init() {
return settings.init();
}).then(function () {
debug('Update settings cache done');
// Full internationalization for core could be here
// in a future version with backend translations
// (settings for language and theme available here;
// internationalization for theme is done
// shortly after, when activating the theme)
//
// Initialize the permissions actions and objects
return permissions.init();
}).then(function () {
Expand Down
Loading

0 comments on commit f671f9d

Please sign in to comment.