diff --git a/app/assets/javascripts/lib/utils/grammar.js b/app/assets/javascripts/lib/utils/grammar.js new file mode 100644 index 00000000000..18f9e2ed846 --- /dev/null +++ b/app/assets/javascripts/lib/utils/grammar.js @@ -0,0 +1,40 @@ +import { sprintf, s__ } from '~/locale'; + +/** + * Combines each given item into a noun series sentence fragment. It does this + * in a way that supports i18n by giving context and punctuation to the locale + * functions. + * + * **Examples:** + * + * - `["A", "B"] => "A and B"` + * - `["A", "B", "C"] => "A, B, and C"` + * + * **Why only nouns?** + * + * Some languages need a bit more context to translate other series. + * + * @param {String[]} items + */ +export const toNounSeriesText = items => { + if (items.length === 0) { + return ''; + } else if (items.length === 1) { + return items[0]; + } else if (items.length === 2) { + return sprintf(s__('nounSeries|%{firstItem} and %{lastItem}'), { + firstItem: items[0], + lastItem: items[1], + }); + } + + return items.reduce((item, nextItem, idx) => + idx === items.length - 1 + ? sprintf(s__('nounSeries|%{item}, and %{lastItem}'), { item, lastItem: nextItem }) + : sprintf(s__('nounSeries|%{item}, %{nextItem}'), { item, nextItem }), + ); +}; + +export default { + toNounSeriesText, +}; diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 06caccae59f..42806a9f232 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8706,6 +8706,15 @@ msgstr "" msgid "notification emails" msgstr "" +msgid "nounSeries|%{firstItem} and %{lastItem}" +msgstr "" + +msgid "nounSeries|%{item}, %{nextItem}" +msgstr "" + +msgid "nounSeries|%{item}, and %{lastItem}" +msgstr "" + msgid "or" msgstr "" diff --git a/spec/javascripts/lib/utils/grammar_spec.js b/spec/javascripts/lib/utils/grammar_spec.js new file mode 100644 index 00000000000..377b2ffb48c --- /dev/null +++ b/spec/javascripts/lib/utils/grammar_spec.js @@ -0,0 +1,35 @@ +import * as grammar from '~/lib/utils/grammar'; + +describe('utils/grammar', () => { + describe('toNounSeriesText', () => { + it('with empty items returns empty string', () => { + expect(grammar.toNounSeriesText([])).toBe(''); + }); + + it('with single item returns item', () => { + const items = ['Lorem Ipsum']; + + expect(grammar.toNounSeriesText(items)).toBe(items[0]); + }); + + it('with 2 items returns item1 and item2', () => { + const items = ['Dolar', 'Sit Amit']; + + expect(grammar.toNounSeriesText(items)).toBe(`${items[0]} and ${items[1]}`); + }); + + it('with 3 items returns comma separated series', () => { + const items = ['Lorem', 'Ipsum', 'dolar']; + const expected = 'Lorem, Ipsum, and dolar'; + + expect(grammar.toNounSeriesText(items)).toBe(expected); + }); + + it('with 6 items returns comma separated series', () => { + const items = ['Lorem', 'ipsum', 'dolar', 'sit', 'amit', 'consectetur']; + const expected = 'Lorem, ipsum, dolar, sit, amit, and consectetur'; + + expect(grammar.toNounSeriesText(items)).toBe(expected); + }); + }); +});