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) ? `${$.getLocaleAltitude(pos.altitude, true)}
` : ''}
${(pos.accuracy !== null) ? `${$.getLocaleAccuracy(pos.accuracy, true)}${provider}
` : ''}
${stats}
-
+
`;
}
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();