From ca2edfa08bb9691109cae823040f37f1ba4cc5fd Mon Sep 17 00:00:00 2001 From: Bartek Fabiszewski Date: Tue, 24 Dec 2019 14:50:25 +0100 Subject: [PATCH] Extend lang with optional format and placeholders --- js/src/lang.js | 12 +++++++++++- js/src/mapapi/api_gmaps.js | 2 +- js/src/mapviewmodel.js | 4 ++-- js/src/trackviewmodel.js | 4 ++-- js/src/utils.js | 17 ++++++++++++----- js/test/lang.test.js | 10 ++++++++++ js/test/mapviewmodel.test.js | 5 ++--- js/test/trackviewmodel.test.js | 7 +++---- 8 files changed, 43 insertions(+), 18 deletions(-) diff --git a/js/src/lang.js b/js/src/lang.js index f5e899d..f1b0e4f 100644 --- a/js/src/lang.js +++ b/js/src/lang.js @@ -17,6 +17,8 @@ * along with this program; if not, see . */ +import uUtils from './utils.js'; + export default class uLang { constructor() { 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') { throw new Error('Unknown localized string'); } + if (params.length) { + return uUtils.sprintf(this.strings[name], ...params); + } return this.strings[name]; } diff --git a/js/src/mapapi/api_gmaps.js b/js/src/mapapi/api_gmaps.js index 0190dcf..6bc7b11 100644 --- a/js/src/mapapi/api_gmaps.js +++ b/js/src/mapapi/api_gmaps.js @@ -74,7 +74,7 @@ export default class GoogleMapsApi { }; window.gm_authFailure = () => { GoogleMapsApi.authError = true; - let message = uUtils.sprintf($._('apifailure'), 'Google Maps'); + let message = $._('apifailure', 'Google Maps'); message += '

' + $._('gmauthfailure'); message += '

' + $._('gmapilink'); if (GoogleMapsApi.gmInitialized) { diff --git a/js/src/mapviewmodel.js b/js/src/mapviewmodel.js index a8cd613..1f293e6 100644 --- a/js/src/mapviewmodel.js +++ b/js/src/mapviewmodel.js @@ -90,7 +90,7 @@ export default class MapViewModel extends ViewModel { this.api.init() .then(() => this.onReady()) .catch((e) => { - let txt = uUtils.sprintf($._('apifailure'), apiName); + let txt = $._('apifailure', apiName); if (e && e.message) { txt += ` (${e.message})`; } @@ -181,7 +181,7 @@ export default class MapViewModel extends ViewModel { ${(pos.altitude !== null) ? `${$._('altitude')}${$.getLocaleAltitude(pos.altitude, true)}
` : ''} ${(pos.accuracy !== null) ? `${$._('accuracy')}${$.getLocaleAccuracy(pos.accuracy, true)}${provider}
` : ''} ${stats} -
${uUtils.sprintf($._('pointof'), id + 1, count)}
+
${$._('pointof', id + 1, count)}
`; } diff --git a/js/src/trackviewmodel.js b/js/src/trackviewmodel.js index b8d15ff..5c57dbd 100644 --- a/js/src/trackviewmodel.js +++ b/js/src/trackviewmodel.js @@ -158,7 +158,7 @@ export default class TrackViewModel extends ViewModel { const form = this.importEl.parentElement; const sizeMax = form.elements['MAX_FILE_SIZE'].value; 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; } if (!auth.isAuthenticated) { @@ -169,7 +169,7 @@ export default class TrackViewModel extends ViewModel { .then((trackList) => { if (trackList.length) { if (trackList.length > 1) { - alert(uUtils.sprintf($._('imultiple'), trackList.length)); + alert($._('imultiple', trackList.length)); } this.model.trackList = trackList.concat(this.model.trackList); this.model.currentTrackId = trackList[0].listValue; diff --git a/js/src/utils.js b/js/src/utils.js index 218ab58..cc03fa1 100644 --- a/js/src/utils.js +++ b/js/src/utils.js @@ -41,16 +41,23 @@ export default class uUtils { * @param {...(string|number)=} params Optional parameters * @returns {string} */ - static sprintf(fmt, params) { // eslint-disable-line no-unused-vars - const args = Array.prototype.slice.call(arguments); - const format = args.shift(); + static sprintf(fmt, ...params) { let i = 0; - return format.replace(/%%|%s|%d/g, (match) => { + const ret = fmt.replace(/%%|%s|%d/g, (match) => { if (match === '%%') { 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; } /** diff --git a/js/test/lang.test.js b/js/test/lang.test.js index d68fd12..7d1838a 100644 --- a/js/test/lang.test.js +++ b/js/test/lang.test.js @@ -39,6 +39,7 @@ describe('Lang tests', () => { }; mockStrings = { string1: 'łańcuch1', + placeholders: '%s : %d', units: 'jp', unitd: 'jo', unitdm: 'jo / 1000', @@ -66,6 +67,15 @@ describe('Lang tests', () => { 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', () => { // when lang.init(mockConfig, mockStrings); diff --git a/js/test/mapviewmodel.test.js b/js/test/mapviewmodel.test.js index 09f85c7..6782af6 100644 --- a/js/test/mapviewmodel.test.js +++ b/js/test/mapviewmodel.test.js @@ -257,7 +257,6 @@ describe('MapViewModel tests', () => { it('should get popup html content', () => { // given const id = 0; - spyOn(uUtils, 'sprintf'); state.currentTrack = TrackFactory.getTrack(2); // when const html = vm.getPopupHtml(id); @@ -265,8 +264,8 @@ describe('MapViewModel tests', () => { // then expect(element).toBeInstanceOf(HTMLDivElement); expect(element.id).toBe('popup'); - expect(uUtils.sprintf.calls.mostRecent().args[1]).toBe(id + 1); - expect(uUtils.sprintf.calls.mostRecent().args[2]).toBe(state.currentTrack.length); + expect(lang._.calls.mostRecent().args[1]).toBe(id + 1); + expect(lang._.calls.mostRecent().args[2]).toBe(state.currentTrack.length); }); it('should get popup with stats when track does not contain only latest positions', () => { diff --git a/js/test/trackviewmodel.test.js b/js/test/trackviewmodel.test.js index 3ffcb86..7ff932a 100644 --- a/js/test/trackviewmodel.test.js +++ b/js/test/trackviewmodel.test.js @@ -451,7 +451,7 @@ describe('TrackViewModel tests', () => { return Promise.resolve(imported); }); spyOn(uPositionSet, 'fetch').and.returnValue(Promise.resolve(positions)); - spyOn(uUtils, 'sprintf'); + spyOn(window, 'alert'); const options = ''; trackEl.insertAdjacentHTML('afterbegin', options); const optLength = trackEl.options.length; @@ -476,7 +476,7 @@ describe('TrackViewModel tests', () => { expect(state.currentTrack).toBe(imported[0]); expect(vm.model.currentTrackId).toBe(imported[0].listValue); 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(vm.model.trackList.length).toBe(optLength + imported.length); expect(vm.model.inputFile).toBe(''); @@ -493,7 +493,6 @@ describe('TrackViewModel tests', () => { ]; spyOn(uTrack, 'import').and.returnValue(Promise.resolve(imported)); spyOn(uPositionSet, 'fetch').and.returnValue(Promise.resolve(positions)); - spyOn(uUtils, 'sprintf'); spyOn(uUtils, 'error'); const options = ''; trackEl.insertAdjacentHTML('afterbegin', options); @@ -517,7 +516,7 @@ describe('TrackViewModel tests', () => { expect(uTrack.import).not.toHaveBeenCalled(); expect(state.currentTrack).toBe(track1); 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(vm.model.trackList.length).toBe(optLength); done();