From b509588a28d0a102f4ad4b97d91c5b5944946834 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Tue, 26 Sep 2017 11:46:59 +0200 Subject: Add basic sprintf implementation to JavaScript --- app/assets/javascripts/locale/index.js | 3 ++ app/assets/javascripts/locale/sprintf.js | 26 +++++++++ app/assets/javascripts/vue_shared/translate.js | 2 + changelogs/unreleased/winh-sprintf.yml | 5 ++ doc/development/i18n_guide.md | 11 +++- spec/javascripts/locale/sprintf_spec.js | 74 ++++++++++++++++++++++++++ 6 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 app/assets/javascripts/locale/sprintf.js create mode 100644 changelogs/unreleased/winh-sprintf.yml create mode 100644 spec/javascripts/locale/sprintf_spec.js diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js index 7ba676d6d20..29dcd64df87 100644 --- a/app/assets/javascripts/locale/index.js +++ b/app/assets/javascripts/locale/index.js @@ -1,5 +1,7 @@ import Jed from 'jed'; +import sprintf from './sprintf'; + /** This is required to require all the translation folders in the current directory this saves us having to do this manually & keep up to date with new languages @@ -67,4 +69,5 @@ export { lang }; export { gettext as __ }; export { ngettext as n__ }; export { pgettext as s__ }; +export { sprintf }; export default locale; diff --git a/app/assets/javascripts/locale/sprintf.js b/app/assets/javascripts/locale/sprintf.js new file mode 100644 index 00000000000..003cbe820d6 --- /dev/null +++ b/app/assets/javascripts/locale/sprintf.js @@ -0,0 +1,26 @@ +import _ from 'underscore'; + +/** + Very limited implementation of sprintf supporting only named parameters. + + @param input (translated) text with parameters (e.g. '%{num_users} users use us') + @param parameters object mapping parameter names to values (e.g. { num_users: 5 }) + @param escapeParameters whether parameter values should be escaped (see http://underscorejs.org/#escape) + @returns {String} the text with parameters replaces (e.g. '5 users use us') + + @see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf + @see https://gitlab.com/gitlab-org/gitlab-ce/issues/37992 +**/ +export default (input, parameters, escapeParameters = true) => { + let output = input; + + if (parameters) { + Object.keys(parameters).forEach((parameterName) => { + const parameterValue = parameters[parameterName]; + const escapedParameterValue = escapeParameters ? _.escape(parameterValue) : parameterValue; + output = output.replace(new RegExp(`%{${parameterName}}`, 'g'), escapedParameterValue); + }); + } + + return output; +} diff --git a/app/assets/javascripts/vue_shared/translate.js b/app/assets/javascripts/vue_shared/translate.js index f83c4b00761..2c7886ec308 100644 --- a/app/assets/javascripts/vue_shared/translate.js +++ b/app/assets/javascripts/vue_shared/translate.js @@ -2,6 +2,7 @@ import { __, n__, s__, + sprintf, } from '../locale'; export default (Vue) => { @@ -37,6 +38,7 @@ export default (Vue) => { @returns {String} Translated context based text **/ s__, + sprintf, }, }); }; diff --git a/changelogs/unreleased/winh-sprintf.yml b/changelogs/unreleased/winh-sprintf.yml new file mode 100644 index 00000000000..f8ae5932ae4 --- /dev/null +++ b/changelogs/unreleased/winh-sprintf.yml @@ -0,0 +1,5 @@ +--- +title: Add basic sprintf implementation to JavaScript +merge_request: 14506 +author: +type: other diff --git a/doc/development/i18n_guide.md b/doc/development/i18n_guide.md index bd0ef39ca62..29c8941a8f7 100644 --- a/doc/development/i18n_guide.md +++ b/doc/development/i18n_guide.md @@ -183,13 +183,20 @@ aren't in the message with id `1 pipeline`. ### Interpolation -- In Ruby/HAML: +- In Ruby/HAML (see [sprintf]): ```ruby _("Hello %{name}") % { name: 'Joe' } ``` -- In JavaScript: Not supported at this moment. +- In JavaScript: Only named parameters are supported (see also [#37992]): + + ```javascript + __("Hello %{name}") % { name: 'Joe' } + ``` + +[sprintf]: http://ruby-doc.org/core/Kernel.html#method-i-sprintf +[#37992]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37992 ### Plurals diff --git a/spec/javascripts/locale/sprintf_spec.js b/spec/javascripts/locale/sprintf_spec.js new file mode 100644 index 00000000000..52e903b819f --- /dev/null +++ b/spec/javascripts/locale/sprintf_spec.js @@ -0,0 +1,74 @@ +import sprintf from '~/locale/sprintf'; + +describe('locale', () => { + describe('sprintf', () => { + it('does not modify string without parameters', () => { + const input = 'No parameters'; + + const output = sprintf(input); + + expect(output).toBe(input); + }); + + it('ignores extraneous parameters', () => { + const input = 'No parameters'; + + const output = sprintf(input, { ignore: 'this' }); + + expect(output).toBe(input); + }); + + it('ignores extraneous placeholders', () => { + const input = 'No %{parameters}'; + + const output = sprintf(input); + + expect(output).toBe(input); + }); + + it('replaces parameters', () => { + const input = '%{name} has %{count} parameters'; + const parameters = { + name: 'this', + count: 2, + }; + + const output = sprintf(input, parameters); + + expect(output).toBe('this has 2 parameters'); + }); + + it('replaces multiple occurrences', () => { + const input = 'to %{verb} or not to %{verb}'; + const parameters = { + verb: 'be', + }; + + const output = sprintf(input, parameters); + + expect(output).toBe('to be or not to be'); + }); + + it('escapes parameters', () => { + const input = 'contains %{userContent}'; + const parameters = { + userContent: '', + }; + + const output = sprintf(input, parameters); + + expect(output).toBe('contains <script>alert("malicious!")</script>'); + }); + + it('does not escape parameters for escapeParameters = false', () => { + const input = 'contains %{safeContent}'; + const parameters = { + safeContent: 'bold attempt', + }; + + const output = sprintf(input, parameters, false); + + expect(output).toBe('contains bold attempt'); + }); + }); +}); -- cgit v1.2.1