mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-15 03:01:37 -05:00
Theme translations and blog localisation (#8437)
refs #5345, refs #3801 - Blog localisation - default is `en` (English) - you can change the language code in the admin panel, see https://github.com/TryGhost/Ghost-Admin/pull/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.**
This commit is contained in:
parent
dcb2aa9ad4
commit
f671f9d2c9
16 changed files with 369 additions and 65 deletions
|
@ -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;
|
||||
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
|
@ -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');
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
|
21
core/server/helpers/lang.js
Normal file
21
core/server/helpers/lang.js
Normal file
|
@ -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());
|
||||
};
|
|
@ -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
|
||||
|
@ -30,4 +32,3 @@ module.exports = function plural(number, options) {
|
|||
return new SafeString(options.hash.plural.replace('%', number));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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)) {
|
||||
|
@ -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);
|
||||
|
|
26
core/server/helpers/t.js
Normal file
26
core/server/helpers/t.js
Normal file
|
@ -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);
|
||||
};
|
|
@ -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');
|
||||
|
||||
|
@ -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 () {
|
||||
|
|
|
@ -1,19 +1,48 @@
|
|||
/* global Intl */
|
||||
|
||||
var supportedLocales = ['en'],
|
||||
_ = require('lodash'),
|
||||
fs = require('fs-extra'),
|
||||
path = require('path'),
|
||||
chalk = require('chalk'),
|
||||
fs = require('fs-extra'),
|
||||
MessageFormat = require('intl-messageformat'),
|
||||
logging = require('./logging'),
|
||||
jp = require('jsonpath'),
|
||||
_ = require('lodash'),
|
||||
path = require('path'),
|
||||
config = require('../../config'),
|
||||
errors = require('./errors'),
|
||||
events = require('./events'),
|
||||
logging = require('./logging'),
|
||||
settingsCache = require('../../services/settings/cache'),
|
||||
_private = {},
|
||||
|
||||
// TODO: fetch this dynamically based on overall blog settings (`key = "default_locale"`) in the `settings` table
|
||||
currentLocale = 'en',
|
||||
blos,
|
||||
// currentLocale, dynamically based on overall settings (key = "default_locale") in the settings db table
|
||||
// (during Ghost's initialization, settings available inside i18n functions below; see core/server/index.js)
|
||||
//
|
||||
// E.g.: en = English (default), es = Spanish, en-US = American English, etc.
|
||||
// Standard:
|
||||
// Language tags in HTML and XML
|
||||
// https://www.w3.org/International/articles/language-tags/
|
||||
//
|
||||
// The corresponding translation files should be at content/themes/mytheme/locales/es.json, etc.
|
||||
currentLocale,
|
||||
activeTheme,
|
||||
coreStrings,
|
||||
themeStrings,
|
||||
I18n;
|
||||
|
||||
/**
|
||||
* When active theme changes, we reload theme translations
|
||||
*/
|
||||
events.on('settings.active_theme.edited', function () {
|
||||
I18n.loadThemeTranslations();
|
||||
});
|
||||
|
||||
/**
|
||||
* When locale changes, we reload theme translations
|
||||
*/
|
||||
events.on('settings.default_locale.edited', function () {
|
||||
I18n.loadThemeTranslations();
|
||||
});
|
||||
|
||||
I18n = {
|
||||
|
||||
/**
|
||||
|
@ -24,8 +53,14 @@ I18n = {
|
|||
* @returns {string}
|
||||
*/
|
||||
t: function t(path, bindings) {
|
||||
var string = I18n.findString(path),
|
||||
msg;
|
||||
var string, isTheme, msg;
|
||||
|
||||
currentLocale = I18n.locale();
|
||||
if (bindings !== undefined) {
|
||||
isTheme = bindings.isThemeString;
|
||||
delete bindings.isThemeString;
|
||||
}
|
||||
string = I18n.findString(path, {isThemeString: isTheme});
|
||||
|
||||
// If the path returns an array (as in the case with anything that has multiple paragraphs such as emails), then
|
||||
// loop through them and return an array of translated/formatted strings. Otherwise, just return the normal
|
||||
|
@ -41,7 +76,7 @@ I18n = {
|
|||
logging.error(err.message);
|
||||
|
||||
// fallback
|
||||
m = new MessageFormat(blos.errors.errors.anErrorOccurred, currentLocale);
|
||||
m = new MessageFormat(coreStrings.errors.errors.anErrorOccurred, currentLocale);
|
||||
m = msg.format();
|
||||
}
|
||||
|
||||
|
@ -56,7 +91,7 @@ I18n = {
|
|||
logging.error(err.message);
|
||||
|
||||
// fallback
|
||||
msg = new MessageFormat(blos.errors.errors.anErrorOccurred, currentLocale);
|
||||
msg = new MessageFormat(coreStrings.errors.errors.anErrorOccurred, currentLocale);
|
||||
msg = msg.format();
|
||||
}
|
||||
}
|
||||
|
@ -72,25 +107,43 @@ I18n = {
|
|||
*/
|
||||
findString: function findString(msgPath, opts) {
|
||||
var options = _.merge({log: true}, opts || {}),
|
||||
matchingString, path;
|
||||
candidateString, matchingString, path;
|
||||
|
||||
// no path? no string
|
||||
if (_.isEmpty(msgPath) || !_.isString(msgPath)) {
|
||||
chalk.yellow('i18n:t() - received an empty path.');
|
||||
chalk.yellow('i18n.t() - received an empty path.');
|
||||
return '';
|
||||
}
|
||||
|
||||
if (blos === undefined) {
|
||||
// If not in memory, load translations for core
|
||||
if (coreStrings === undefined) {
|
||||
I18n.init();
|
||||
}
|
||||
|
||||
matchingString = blos;
|
||||
if (options.isThemeString) {
|
||||
// If not in memory, load translations for theme
|
||||
if (themeStrings === undefined) {
|
||||
I18n.loadThemeTranslations();
|
||||
}
|
||||
// Both jsonpath's dot-notation and bracket-notation start with '$'
|
||||
// E.g.: $.store.book.title or $['store']['book']['title']
|
||||
// The {{t}} translation helper passes the default English text
|
||||
// The full Unicode jsonpath with '$' is built here
|
||||
// jp.stringify and jp.value are jsonpath methods
|
||||
// Info: https://www.npmjs.com/package/jsonpath
|
||||
path = jp.stringify(['$', msgPath]);
|
||||
candidateString = jp.value(themeStrings, path) || msgPath;
|
||||
} else {
|
||||
// Backend messages use dot-notation, and the '$.' prefix is added here
|
||||
// While bracket-notation allows any Unicode characters in keys for themes,
|
||||
// dot-notation allows only word characters in keys for backend messages
|
||||
// (that is \w or [A-Za-z0-9_] in RegExp)
|
||||
path = '$.' + msgPath;
|
||||
candidateString = jp.value(coreStrings, path);
|
||||
}
|
||||
|
||||
matchingString = candidateString || {};
|
||||
|
||||
path = msgPath.split('.');
|
||||
path.forEach(function (key) {
|
||||
// reassign matching object, or set to an empty string if there is no match
|
||||
matchingString = matchingString[key] || {};
|
||||
});
|
||||
if (_.isObject(matchingString) || _.isEqual(matchingString, {})) {
|
||||
if (options.log) {
|
||||
logging.error(new errors.IncorrectUsageError({
|
||||
|
@ -98,7 +151,7 @@ I18n = {
|
|||
}));
|
||||
}
|
||||
|
||||
matchingString = blos.errors.errors.anErrorOccurred;
|
||||
matchingString = coreStrings.errors.errors.anErrorOccurred;
|
||||
}
|
||||
|
||||
return matchingString;
|
||||
|
@ -106,47 +159,120 @@ I18n = {
|
|||
|
||||
doesTranslationKeyExist: function doesTranslationKeyExist(msgPath) {
|
||||
var translation = I18n.findString(msgPath, {log: false});
|
||||
return translation !== blos.errors.errors.anErrorOccurred;
|
||||
return translation !== coreStrings.errors.errors.anErrorOccurred;
|
||||
},
|
||||
|
||||
/**
|
||||
* Setup i18n support:
|
||||
* - Load proper language file in to memory
|
||||
* - Polyfill node.js if it does not have Intl support or support for a particular locale
|
||||
* - Load proper language file into memory
|
||||
*/
|
||||
init: function init() {
|
||||
// read file for current locale and keep its content in memory
|
||||
blos = fs.readFileSync(path.join(__dirname, '..', '..', 'translations', currentLocale + '.json'));
|
||||
// This function is called during Ghost's initialization.
|
||||
// Reading translation file for messages from core .js files and keeping its content in memory
|
||||
// The English file is always loaded, until back-end translations are enabled in future versions.
|
||||
// Before that, see previous tasks on issue #6526 (error codes or identifiers, error message
|
||||
// translation at the point of display...)
|
||||
coreStrings = fs.readFileSync(path.join(__dirname, '..', '..', 'translations', 'en.json'));
|
||||
|
||||
// if translation file is not valid, you will see an error
|
||||
try {
|
||||
blos = JSON.parse(blos);
|
||||
coreStrings = JSON.parse(coreStrings);
|
||||
} catch (err) {
|
||||
blos = undefined;
|
||||
coreStrings = undefined;
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (global.Intl) {
|
||||
// Determine if the built-in `Intl` has the locale data we need.
|
||||
var hasBuiltInLocaleData,
|
||||
IntlPolyfill;
|
||||
_private.initializeIntl();
|
||||
},
|
||||
|
||||
hasBuiltInLocaleData = supportedLocales.every(function (locale) {
|
||||
return Intl.NumberFormat.supportedLocalesOf(locale)[0] === locale &&
|
||||
Intl.DateTimeFormat.supportedLocalesOf(locale)[0] === locale;
|
||||
});
|
||||
/**
|
||||
* Setup i18n support for themes:
|
||||
* - Load proper language file into memory
|
||||
*/
|
||||
loadThemeTranslations: function loadThemeTranslations() {
|
||||
// This function is called during theme initialization, and when switching language or theme.
|
||||
currentLocale = I18n.locale();
|
||||
activeTheme = settingsCache.get('active_theme');
|
||||
|
||||
if (!hasBuiltInLocaleData) {
|
||||
// `Intl` exists, but it doesn't have the data we need, so load the
|
||||
// polyfill and replace the constructors with need with the polyfill's.
|
||||
IntlPolyfill = require('intl');
|
||||
Intl.NumberFormat = IntlPolyfill.NumberFormat;
|
||||
Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat;
|
||||
// Reading file for current locale and active theme and keeping its content in memory
|
||||
if (activeTheme) {
|
||||
// Reading translation file for theme .hbs templates.
|
||||
// Compatibility with both old themes and i18n-capable themes.
|
||||
// Preventing missing files.
|
||||
try {
|
||||
themeStrings = fs.readFileSync(path.join(config.getContentPath('themes'), activeTheme, 'locales', currentLocale + '.json'));
|
||||
} catch (err) {
|
||||
themeStrings = undefined;
|
||||
if (err.code === 'ENOENT') {
|
||||
logging.warn('Theme\'s file locales/' + currentLocale + '.json not found.');
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
if (themeStrings === undefined && currentLocale !== 'en') {
|
||||
logging.warn('Falling back to locales/en.json.');
|
||||
try {
|
||||
themeStrings = fs.readFileSync(path.join(config.getContentPath('themes'), activeTheme, 'locales', 'en.json'));
|
||||
} catch (err) {
|
||||
themeStrings = undefined;
|
||||
if (err.code === 'ENOENT') {
|
||||
logging.warn('Theme\'s file locales/en.json not found.');
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (themeStrings !== undefined) {
|
||||
// if translation file is not valid, you will see an error
|
||||
try {
|
||||
themeStrings = JSON.parse(themeStrings);
|
||||
} catch (err) {
|
||||
themeStrings = undefined;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No `Intl`, so use and load the polyfill.
|
||||
global.Intl = require('intl');
|
||||
}
|
||||
|
||||
if (themeStrings === undefined) {
|
||||
// even if empty, themeStrings must be an object for jp.value
|
||||
themeStrings = {};
|
||||
}
|
||||
|
||||
_private.initializeIntl();
|
||||
},
|
||||
|
||||
/**
|
||||
* Exporting the current locale (e.g. "en") to make it available for other files as well,
|
||||
* such as core/server/helpers/date.js and core/server/helpers/lang.js
|
||||
*/
|
||||
locale: function locale() {
|
||||
return settingsCache.get('default_locale');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Setup i18n support:
|
||||
* - Polyfill node.js if it does not have Intl support or support for a particular locale
|
||||
*/
|
||||
_private.initializeIntl = function initializeIntl() {
|
||||
var hasBuiltInLocaleData, IntlPolyfill;
|
||||
|
||||
if (global.Intl) {
|
||||
// Determine if the built-in `Intl` has the locale data we need.
|
||||
hasBuiltInLocaleData = supportedLocales.every(function (locale) {
|
||||
return Intl.NumberFormat.supportedLocalesOf(locale)[0] === locale &&
|
||||
Intl.DateTimeFormat.supportedLocalesOf(locale)[0] === locale;
|
||||
});
|
||||
if (!hasBuiltInLocaleData) {
|
||||
// `Intl` exists, but it doesn't have the data we need, so load the
|
||||
// polyfill and replace the constructors with need with the polyfill's.
|
||||
IntlPolyfill = require('intl');
|
||||
Intl.NumberFormat = IntlPolyfill.NumberFormat;
|
||||
Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat;
|
||||
}
|
||||
} else {
|
||||
// No `Intl`, so use and load the polyfill.
|
||||
global.Intl = require('intl');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -10,8 +10,8 @@ describe('Helpers', function () {
|
|||
var hbsHelpers = ['each', 'if', 'unless', 'with', 'helperMissing', 'blockHelperMissing', 'log', 'lookup'],
|
||||
ghostHelpers = [
|
||||
'asset', 'author', 'body_class', 'content', 'date', 'encode', 'excerpt', 'facebook_url', 'foreach', 'get',
|
||||
'ghost_foot', 'ghost_head', 'has', 'img_url', 'is', 'meta_description', 'meta_title', 'navigation',
|
||||
'next_post', 'page_url', 'pagination', 'plural', 'post_class', 'prev_post', 'reading_time', 'tags', 'title', 'twitter_url',
|
||||
'ghost_foot', 'ghost_head', 'has', 'img_url', 'is', 'lang', 'meta_description', 'meta_title', 'navigation',
|
||||
'next_post', 'page_url', 'pagination', 'plural', 'post_class', 'prev_post', 'reading_time', 't', 'tags', 'title', 'twitter_url',
|
||||
'url'
|
||||
],
|
||||
expectedHelpers = _.concat(hbsHelpers, ghostHelpers);
|
||||
|
|
24
core/test/unit/helpers/lang_spec.js
Normal file
24
core/test/unit/helpers/lang_spec.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
'use strict';
|
||||
|
||||
const should = require('should'),
|
||||
settingsCache = require('../../../server/services/settings/cache'),
|
||||
helpers = require('../../../server/helpers'),
|
||||
proxy = require('../../../server/helpers/proxy');
|
||||
|
||||
describe('{{lang}} helper', function () {
|
||||
beforeEach(function () {
|
||||
settingsCache.set('default_locale', {value: 'en'});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
settingsCache.shutdown();
|
||||
});
|
||||
|
||||
it('returns correct language tag', function () {
|
||||
let expected = proxy.i18n.locale(),
|
||||
rendered = helpers.lang.call();
|
||||
|
||||
should.exist(rendered);
|
||||
rendered.string.should.equal(expected);
|
||||
});
|
||||
});
|
64
core/test/unit/helpers/t_spec.js
Normal file
64
core/test/unit/helpers/t_spec.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
'use strict';
|
||||
|
||||
const should = require('should'),
|
||||
path = require('path'),
|
||||
settingsCache = require('../../../server/services/settings/cache'),
|
||||
helpers = require('../../../server/helpers'),
|
||||
common = require('../../../server/lib/common'),
|
||||
configUtils = require('../../utils/configUtils');
|
||||
|
||||
describe('{{t}} helper', function () {
|
||||
beforeEach(function () {
|
||||
settingsCache.set('active_theme', {value: 'casper'});
|
||||
configUtils.set('paths:contentPath', path.join(__dirname, '../../utils/fixtures/'));
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
configUtils.restore();
|
||||
settingsCache.shutdown();
|
||||
});
|
||||
|
||||
it('theme translation is DE', function () {
|
||||
settingsCache.set('default_locale', {value: 'de'});
|
||||
|
||||
let rendered = helpers.t.call({}, 'Top left Button', {
|
||||
hash: {}
|
||||
});
|
||||
|
||||
rendered.should.eql('Oben Links.');
|
||||
});
|
||||
|
||||
it('theme translation is EN', function () {
|
||||
settingsCache.set('default_locale', {value: 'en'});
|
||||
common.i18n.loadThemeTranslations();
|
||||
|
||||
let rendered = helpers.t.call({}, 'Top left Button', {
|
||||
hash: {}
|
||||
});
|
||||
|
||||
rendered.should.eql('Left Button on Top');
|
||||
});
|
||||
|
||||
it('[fallback] no theme translation file found for FR', function () {
|
||||
settingsCache.set('default_locale', {value: 'fr'});
|
||||
common.i18n.loadThemeTranslations();
|
||||
|
||||
let rendered = helpers.t.call({}, 'Top left Button', {
|
||||
hash: {}
|
||||
});
|
||||
|
||||
rendered.should.eql('Left Button on Top');
|
||||
});
|
||||
|
||||
it('[fallback] no theme files at all, use key as translation', function () {
|
||||
settingsCache.set('active_theme', {value: 'casper-1.4'});
|
||||
settingsCache.set('default_locale', {value: 'de'});
|
||||
common.i18n.loadThemeTranslations();
|
||||
|
||||
let rendered = helpers.t.call({}, 'Top left Button', {
|
||||
hash: {}
|
||||
});
|
||||
|
||||
rendered.should.eql('Top left Button');
|
||||
});
|
||||
});
|
|
@ -40,7 +40,7 @@ describe('lib/fs: read csv', function () {
|
|||
return done(err);
|
||||
}
|
||||
|
||||
files.length.should.eql(12);
|
||||
files.length.should.eql(13);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
|
3
core/test/utils/fixtures/themes/casper/locales/de.json
Normal file
3
core/test/utils/fixtures/themes/casper/locales/de.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"Top left Button": "Oben Links."
|
||||
}
|
3
core/test/utils/fixtures/themes/casper/locales/en.json
Normal file
3
core/test/utils/fixtures/themes/casper/locales/en.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"Top left Button": "Left Button on Top"
|
||||
}
|
|
@ -56,7 +56,7 @@
|
|||
"ghost-storage-base": "0.0.1",
|
||||
"glob": "5.0.15",
|
||||
"got": "7.1.0",
|
||||
"gscan": "1.2.3",
|
||||
"gscan": "1.3.0",
|
||||
"html-to-text": "3.3.0",
|
||||
"image-size": "0.6.2",
|
||||
"intl": "1.2.5",
|
||||
|
|
|
@ -2667,9 +2667,9 @@ grunt@~0.4.0:
|
|||
underscore.string "~2.2.1"
|
||||
which "~1.0.5"
|
||||
|
||||
gscan@1.2.3:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/gscan/-/gscan-1.2.3.tgz#0a53809a651287705f25e0436a630f3056ca610f"
|
||||
gscan@1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/gscan/-/gscan-1.3.0.tgz#2e04e777b51be8f56981a866b7584f872c99ac0c"
|
||||
dependencies:
|
||||
bluebird "^3.4.6"
|
||||
chalk "^1.1.1"
|
||||
|
|
Loading…
Add table
Reference in a new issue