Extend lang with optional format and placeholders

This commit is contained in:
Bartek Fabiszewski 2019-12-24 14:50:25 +01:00
parent 54b25da4b7
commit ca2edfa08b
8 changed files with 43 additions and 18 deletions

View File

@ -17,6 +17,8 @@
* along with this program; if not, see <http://www.gnu.org/licenses/>. * along with this program; if not, see <http://www.gnu.org/licenses/>.
*/ */
import uUtils from './utils.js';
export default class uLang { export default class uLang {
constructor() { constructor() {
this.strings = {}; this.strings = {};
@ -35,10 +37,18 @@ export default class uLang {
} }
} }
_(name) { /**
* @param {string} name
* @param {...(string|number)=} params Optional parameters
* @return {string}
*/
_(name, ...params) {
if (typeof this.strings[name] === 'undefined') { if (typeof this.strings[name] === 'undefined') {
throw new Error('Unknown localized string'); throw new Error('Unknown localized string');
} }
if (params.length) {
return uUtils.sprintf(this.strings[name], ...params);
}
return this.strings[name]; return this.strings[name];
} }

View File

@ -74,7 +74,7 @@ export default class GoogleMapsApi {
}; };
window.gm_authFailure = () => { window.gm_authFailure = () => {
GoogleMapsApi.authError = true; GoogleMapsApi.authError = true;
let message = uUtils.sprintf($._('apifailure'), 'Google Maps'); let message = $._('apifailure', 'Google Maps');
message += '<br><br>' + $._('gmauthfailure'); message += '<br><br>' + $._('gmauthfailure');
message += '<br><br>' + $._('gmapilink'); message += '<br><br>' + $._('gmapilink');
if (GoogleMapsApi.gmInitialized) { if (GoogleMapsApi.gmInitialized) {

View File

@ -90,7 +90,7 @@ export default class MapViewModel extends ViewModel {
this.api.init() this.api.init()
.then(() => this.onReady()) .then(() => this.onReady())
.catch((e) => { .catch((e) => {
let txt = uUtils.sprintf($._('apifailure'), apiName); let txt = $._('apifailure', apiName);
if (e && e.message) { if (e && e.message) {
txt += ` (${e.message})`; txt += ` (${e.message})`;
} }
@ -181,7 +181,7 @@ export default class MapViewModel extends ViewModel {
${(pos.altitude !== null) ? `<img class="icon" alt="${$._('altitude')}" title="${$._('altitude')}" src="images/altitude_dark.svg">${$.getLocaleAltitude(pos.altitude, true)}<br>` : ''} ${(pos.altitude !== null) ? `<img class="icon" alt="${$._('altitude')}" title="${$._('altitude')}" src="images/altitude_dark.svg">${$.getLocaleAltitude(pos.altitude, true)}<br>` : ''}
${(pos.accuracy !== null) ? `<img class="icon" alt="${$._('accuracy')}" title="${$._('accuracy')}" src="images/accuracy_dark.svg">${$.getLocaleAccuracy(pos.accuracy, true)}${provider}<br>` : ''} ${(pos.accuracy !== null) ? `<img class="icon" alt="${$._('accuracy')}" title="${$._('accuracy')}" src="images/accuracy_dark.svg">${$.getLocaleAccuracy(pos.accuracy, true)}${provider}<br>` : ''}
</div>${stats}</div> </div>${stats}</div>
<div id="pfooter">${uUtils.sprintf($._('pointof'), id + 1, count)}</div> <div id="pfooter">${$._('pointof', id + 1, count)}</div>
</div>`; </div>`;
} }

View File

@ -158,7 +158,7 @@ export default class TrackViewModel extends ViewModel {
const form = this.importEl.parentElement; const form = this.importEl.parentElement;
const sizeMax = form.elements['MAX_FILE_SIZE'].value; const sizeMax = form.elements['MAX_FILE_SIZE'].value;
if (this.importEl.files && this.importEl.files.length === 1 && this.importEl.files[0].size > sizeMax) { if (this.importEl.files && this.importEl.files.length === 1 && this.importEl.files[0].size > sizeMax) {
uUtils.error(uUtils.sprintf($._('isizefailure'), sizeMax)); uUtils.error($._('isizefailure', sizeMax));
return; return;
} }
if (!auth.isAuthenticated) { if (!auth.isAuthenticated) {
@ -169,7 +169,7 @@ export default class TrackViewModel extends ViewModel {
.then((trackList) => { .then((trackList) => {
if (trackList.length) { if (trackList.length) {
if (trackList.length > 1) { if (trackList.length > 1) {
alert(uUtils.sprintf($._('imultiple'), trackList.length)); alert($._('imultiple', trackList.length));
} }
this.model.trackList = trackList.concat(this.model.trackList); this.model.trackList = trackList.concat(this.model.trackList);
this.model.currentTrackId = trackList[0].listValue; this.model.currentTrackId = trackList[0].listValue;

View File

@ -41,16 +41,23 @@ export default class uUtils {
* @param {...(string|number)=} params Optional parameters * @param {...(string|number)=} params Optional parameters
* @returns {string} * @returns {string}
*/ */
static sprintf(fmt, params) { // eslint-disable-line no-unused-vars static sprintf(fmt, ...params) {
const args = Array.prototype.slice.call(arguments);
const format = args.shift();
let i = 0; let i = 0;
return format.replace(/%%|%s|%d/g, (match) => { const ret = fmt.replace(/%%|%s|%d/g, (match) => {
if (match === '%%') { if (match === '%%') {
return '%'; return '%';
} else if (match === '%d' && isNaN(params[i])) {
throw new Error(`Wrong format specifier ${match} for ${params[i]} argument`);
} }
return (typeof args[i] !== 'undefined') ? args[i++] : match; if (typeof params[i] === 'undefined') {
throw new Error(`Missing argument for format specifier ${match}`);
}
return params[i++];
}); });
if (i < params.length) {
throw new Error(`Unused argument for format specifier ${fmt}`);
}
return ret;
} }
/** /**

View File

@ -39,6 +39,7 @@ describe('Lang tests', () => {
}; };
mockStrings = { mockStrings = {
string1: 'łańcuch1', string1: 'łańcuch1',
placeholders: '%s : %d',
units: 'jp', units: 'jp',
unitd: 'jo', unitd: 'jo',
unitdm: 'jo / 1000', unitdm: 'jo / 1000',
@ -66,6 +67,15 @@ describe('Lang tests', () => {
expect(lang._('string1')).toBe(mockStrings.string1); expect(lang._('string1')).toBe(mockStrings.string1);
}); });
it('should return localized string with replaced placeholders', () => {
// when
lang.init(mockConfig, mockStrings);
const p1 = 'str';
const p2 = 4;
// then
expect(lang._('placeholders', p1, p2)).toBe(`${p1} : ${p2}`);
});
it('should throw error on unknown string', () => { it('should throw error on unknown string', () => {
// when // when
lang.init(mockConfig, mockStrings); lang.init(mockConfig, mockStrings);

View File

@ -257,7 +257,6 @@ describe('MapViewModel tests', () => {
it('should get popup html content', () => { it('should get popup html content', () => {
// given // given
const id = 0; const id = 0;
spyOn(uUtils, 'sprintf');
state.currentTrack = TrackFactory.getTrack(2); state.currentTrack = TrackFactory.getTrack(2);
// when // when
const html = vm.getPopupHtml(id); const html = vm.getPopupHtml(id);
@ -265,8 +264,8 @@ describe('MapViewModel tests', () => {
// then // then
expect(element).toBeInstanceOf(HTMLDivElement); expect(element).toBeInstanceOf(HTMLDivElement);
expect(element.id).toBe('popup'); expect(element.id).toBe('popup');
expect(uUtils.sprintf.calls.mostRecent().args[1]).toBe(id + 1); expect(lang._.calls.mostRecent().args[1]).toBe(id + 1);
expect(uUtils.sprintf.calls.mostRecent().args[2]).toBe(state.currentTrack.length); expect(lang._.calls.mostRecent().args[2]).toBe(state.currentTrack.length);
}); });
it('should get popup with stats when track does not contain only latest positions', () => { it('should get popup with stats when track does not contain only latest positions', () => {

View File

@ -451,7 +451,7 @@ describe('TrackViewModel tests', () => {
return Promise.resolve(imported); return Promise.resolve(imported);
}); });
spyOn(uPositionSet, 'fetch').and.returnValue(Promise.resolve(positions)); spyOn(uPositionSet, 'fetch').and.returnValue(Promise.resolve(positions));
spyOn(uUtils, 'sprintf'); spyOn(window, 'alert');
const options = '<option selected value="1">track1</option><option value="2">track2</option>'; const options = '<option selected value="1">track1</option><option value="2">track2</option>';
trackEl.insertAdjacentHTML('afterbegin', options); trackEl.insertAdjacentHTML('afterbegin', options);
const optLength = trackEl.options.length; const optLength = trackEl.options.length;
@ -476,7 +476,7 @@ describe('TrackViewModel tests', () => {
expect(state.currentTrack).toBe(imported[0]); expect(state.currentTrack).toBe(imported[0]);
expect(vm.model.currentTrackId).toBe(imported[0].listValue); expect(vm.model.currentTrackId).toBe(imported[0].listValue);
expect(state.currentTrack.length).toBe(positions.length); expect(state.currentTrack.length).toBe(positions.length);
expect(uUtils.sprintf.calls.mostRecent().args[1]).toBe(imported.length); expect(window.alert).toHaveBeenCalledTimes(1);
expect(trackEl.options.length).toBe(optLength + imported.length); expect(trackEl.options.length).toBe(optLength + imported.length);
expect(vm.model.trackList.length).toBe(optLength + imported.length); expect(vm.model.trackList.length).toBe(optLength + imported.length);
expect(vm.model.inputFile).toBe(''); expect(vm.model.inputFile).toBe('');
@ -493,7 +493,6 @@ describe('TrackViewModel tests', () => {
]; ];
spyOn(uTrack, 'import').and.returnValue(Promise.resolve(imported)); spyOn(uTrack, 'import').and.returnValue(Promise.resolve(imported));
spyOn(uPositionSet, 'fetch').and.returnValue(Promise.resolve(positions)); spyOn(uPositionSet, 'fetch').and.returnValue(Promise.resolve(positions));
spyOn(uUtils, 'sprintf');
spyOn(uUtils, 'error'); spyOn(uUtils, 'error');
const options = '<option selected value="1">track1</option><option value="2">track2</option>'; const options = '<option selected value="1">track1</option><option value="2">track2</option>';
trackEl.insertAdjacentHTML('afterbegin', options); trackEl.insertAdjacentHTML('afterbegin', options);
@ -517,7 +516,7 @@ describe('TrackViewModel tests', () => {
expect(uTrack.import).not.toHaveBeenCalled(); expect(uTrack.import).not.toHaveBeenCalled();
expect(state.currentTrack).toBe(track1); expect(state.currentTrack).toBe(track1);
expect(vm.model.currentTrackId).toBe(track1.listValue); expect(vm.model.currentTrackId).toBe(track1.listValue);
expect(uUtils.sprintf.calls.mostRecent().args[1]).toBe(MAX_FILE_SIZE.toString()); expect(lang._.calls.mostRecent().args[1]).toBe(MAX_FILE_SIZE.toString());
expect(trackEl.options.length).toBe(optLength); expect(trackEl.options.length).toBe(optLength);
expect(vm.model.trackList.length).toBe(optLength); expect(vm.model.trackList.length).toBe(optLength);
done(); done();