0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-06 22:40:14 -05:00

Improved i18n with unified getCandidateString fn

- the core i18n library and theme i18n library have slightly different methods of getting a candidate string
- both of them use forms of jsonpath, meaning they both require jsonpath as a dependency
- to try to get to a point of being able to rip more things out of ghost, we want to have less dependencies
- so instead of overloading the method, we pass in a stringMode as an argument
- eventually we might not need an overloaded class for themeI18n at all, which would simplify the codebase
This commit is contained in:
Hannah Wolfe 2021-05-04 14:28:15 +01:00
parent 470f2a8728
commit d8318654a9
3 changed files with 86 additions and 27 deletions

View file

@ -3,13 +3,14 @@ const i18n = require('../../../../shared/i18n');
const logging = require('../../../../shared/logging');
const settingsCache = require('../../../../server/services/settings/cache');
const config = require('../../../../shared/config');
const jp = require('jsonpath');
const isNil = require('lodash/isNil');
class ThemeI18n extends i18n.I18n {
constructor(options = {}) {
super(options);
// We don't care what gets passed in, themes use fulltext mode
this._stringMode = 'fulltext';
}
/**
@ -75,22 +76,6 @@ class ThemeI18n extends i18n.I18n {
this._locale = settingsCache.get('lang');
return this._locale;
}
/**
* Do the lookup with JSON path
*
* @param {String} msgPath
*/
_getCandidateString(msgPath) {
// 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
let path = jp.stringify(['$', msgPath]);
return jp.value(this._strings, path) || msgPath;
}
}
module.exports = ThemeI18n;

View file

@ -14,6 +14,7 @@ const logging = require('../logging');
class I18n {
constructor(options = {}) {
this._locale = options.locale || this.defaultLocale();
this._stringMode = options.stringMode || 'dot';
this._strings = null;
}
@ -69,17 +70,23 @@ class I18n {
* - Load proper language file into memory
*/
init() {
// This function is called during Ghost's initialization.
this._strings = this._loadStrings();
this._initializeIntl();
}
_loadStrings() {
let strings;
// Reading translation file for messages from core .json files and keeping its content in memory
// The English file is always loaded, until back-end translations are enabled in future versions.
try {
this._strings = this._readStringsFile(__dirname, 'translations', `${this.defaultLocale()}.json`);
strings = this._readStringsFile(__dirname, 'translations', `${this.defaultLocale()}.json`);
} catch (err) {
this._strings = null;
strings = null;
throw err;
}
this._initializeIntl();
return strings;
}
/**
@ -92,17 +99,29 @@ class I18n {
}
/**
* Do the lookup with JSON path
* Do the lookup within the JSON file using jsonpath
*
* @param {String} msgPath
*/
_getCandidateString(msgPath) {
// 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)
// Our default string mode is "dot" for dot-notation, e.g. $.something.like.this used in the backend
// Both jsonpath's dot-notation and bracket-notation start with '$' E.g.: $.store.book.title or $['store']['book']['title']
// While bracket-notation allows any Unicode characters in keys (i.e. for themes / fulltext mode) E.g. $['Read more']
// dot-notation allows only word characters in keys for backend messages (that is \w or [A-Za-z0-9_] in RegExp)
let jsonPath = `$.${msgPath}`;
return jp.value(this._strings, jsonPath);
let fallback = null;
if (this._stringMode === 'fulltext') {
jsonPath = jp.stringify(['$', msgPath]);
// In fulltext mode we can use the passed string as a fallback
fallback = msgPath;
}
try {
return jp.value(this._strings, jsonPath) || fallback;
} catch (error) {
throw new errors.IncorrectUsageError({message: `i18n.t() called with an invalid path: ${msgPath}`});
}
}
/**

View file

@ -1,4 +1,5 @@
const should = require('should');
const sinon = require('sinon');
const I18n = require('../../../core/shared/i18n').I18n;
@ -7,4 +8,58 @@ describe('I18n Class Behaviour', function () {
const i18n = new I18n();
i18n.locale().should.eql('en');
});
describe('dot notation (default behaviour)', function () {
const fakeStrings = {
test: {string: {path: 'I am correct'}}
};
let i18n;
beforeEach(function initBasicI18n() {
i18n = new I18n();
sinon.stub(i18n, '_loadStrings').returns(fakeStrings);
i18n.init();
});
it('correctly loads strings', function () {
i18n._strings.should.eql(fakeStrings);
});
it('correctly uses dot notation', function () {
i18n.t('test.string.path').should.eql('I am correct');
});
it('uses fallback correctly', function () {
i18n.t('unknown.string').should.eql('An error occurred');
});
it('errors for invalid strings', function () {
should(function () {
i18n.t('unknown string');
}).throw('i18n.t() called with an invalid path: unknown string');
});
});
describe('fulltext notation (theme behaviour)', function () {
const fakeStrings = {'Full text': 'I am correct'};
let i18n;
beforeEach(function initFulltextI18n() {
i18n = new I18n({stringMode: 'fulltext'});
sinon.stub(i18n, '_loadStrings').returns(fakeStrings);
i18n.init();
});
it('correctly loads strings', function () {
i18n._strings.should.eql(fakeStrings);
});
it('correctly uses fulltext with bracket notation', function () {
i18n.t('Full text').should.eql('I am correct');
});
it('uses fallback correctly', function () {
i18n.t('unknown string').should.eql('unknown string');
});
});
});