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/>.
*/
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];
}

View File

@ -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 += '<br><br>' + $._('gmauthfailure');
message += '<br><br>' + $._('gmapilink');
if (GoogleMapsApi.gmInitialized) {

View File

@ -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) ? `<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>` : ''}
</div>${stats}</div>
<div id="pfooter">${uUtils.sprintf($._('pointof'), id + 1, count)}</div>
<div id="pfooter">${$._('pointof', id + 1, count)}</div>
</div>`;
}

View File

@ -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;

View File

@ -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;
}
/**

View File

@ -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);

View File

@ -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', () => {

View File

@ -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 = '<option selected value="1">track1</option><option value="2">track2</option>';
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 = '<option selected value="1">track1</option><option value="2">track2</option>';
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();