1 | ## Overview
|
2 |
|
3 | The `GlSprintf` component lets you do `sprintf`-style string interpolation with
|
4 | child components. Each placeholder in the translated string, provided via the
|
5 | `message` prop, becomes a slot that you can use to insert any components or
|
6 | markup in the rendered output.
|
7 |
|
8 | > NOTE: `gl-sprintf` does not translate the message for you; you must provide
|
9 | > it already translated. In the following examples, it is assumed that
|
10 | > a `gettext`-style `__` translation function is available in your Vue
|
11 | > templates.
|
12 |
|
13 | ## Displaying messages with text between placeholders (e.g., links, buttons)
|
14 |
|
15 | Sentences should not be split up into different messages, otherwise they may
|
16 | not be translatable into certain languages. To help with this, `GlSprintf`
|
17 | interprets placeholders suffixed with `Start` and `End` to indicate the
|
18 | boundaries of a component to display within the message. Any text between
|
19 | them is passed, via the `content` scoped slot property, to the slot name common
|
20 | to the placeholders.
|
21 |
|
22 | For example, using `linkStart` and `linkEnd` placeholders in a message defines
|
23 | a `link` scoped slot:
|
24 |
|
25 | ```html
|
26 | <div>
|
27 | <gl-sprintf :message="__('Learn more about %{linkStart}zones%{linkEnd}')">
|
28 | <template #link="{ content }">
|
29 | <gl-link
|
30 | href="https://cloud.google.com/compute/docs/regions-zones/regions-zones"
|
31 | target="_blank"
|
32 | >{{ content }}</gl-link>
|
33 | </template>
|
34 | </gl-sprintf>
|
35 | </div>
|
36 | ```
|
37 |
|
38 | will render as:
|
39 |
|
40 | ```html
|
41 | <div>
|
42 | Learn more about
|
43 | <a
|
44 | href="https://cloud.google.com/compute/docs/regions-zones/regions-zones"
|
45 | target="_blank"
|
46 | rel="noopener noreferrer"
|
47 | >zones</a>
|
48 | </div>
|
49 | ```
|
50 |
|
51 | Note that _any_ arbitrary HTML tags or Vue component(s) can be used within
|
52 | a scoped slot, and that the content passed to it can be used in any way at all;
|
53 | for instance, as regular text, or in component attributes or slots.
|
54 |
|
55 | Here's a more complex example, which `<gl-sprintf>` lets you do in a breeze:
|
56 |
|
57 | ```html
|
58 | <div>
|
59 | <gl-sprintf :message="__('Written by %{authorStart}someone%{authorEnd}')">
|
60 | <template #author="{ content }">
|
61 | <my-vue-component v-gl-tooltip="content" @event="handleEvent(content)">
|
62 | {{ content }}
|
63 | </my-vue-component>
|
64 | <p>
|
65 | {{ content }}
|
66 | <div>{{ content }}</div>
|
67 | </p>
|
68 | </template>
|
69 | </gl-sprintf>
|
70 | </div>
|
71 | ```
|
72 |
|
73 | This is not feasible in a JS-only solution, since arbitrary Vue components
|
74 | cannot easily be used. In addition, a JS-only solution is more likely to be
|
75 | prone to XSS attacks, as the Vue compiler isn't available to help protect
|
76 | against them.
|
77 |
|
78 | ### Customizing start/end placeholders
|
79 |
|
80 | You can customize the start and end placeholders that `GlSprintf` looks for
|
81 | using the `placeholders` prop. For instance:
|
82 |
|
83 | ```html
|
84 | <div>
|
85 | <gl-sprintf
|
86 | :message="__('Learn more about %{my_custom_start}zones%{my_custom_end}')"
|
87 | :placeholders="{ link: ['my_custom_start', 'my_custom_end'] }"
|
88 | >
|
89 | <template #link="{ content }">
|
90 | <gl-link
|
91 | href="https://cloud.google.com/compute/docs/regions-zones/regions-zones"
|
92 | target="_blank"
|
93 | >{{ content }}</gl-link>
|
94 | </template>
|
95 | </gl-sprintf>
|
96 | </div>
|
97 | ```
|
98 |
|
99 | This can be useful if you are migrating an existing string to `GlSprintf` that
|
100 | uses different placeholder naming conventions, and don't want invalidate
|
101 | existing translations.
|
102 |
|
103 | ## Displaying components within a message
|
104 |
|
105 | Use slots to replace placeholders in the message with the slots' contents.
|
106 | There is a slot for every placeholder in the message. For example, the `author`
|
107 | slot name can be used when there is an `%{author}` placeholder in the message:
|
108 |
|
109 | ```html
|
110 | <script>
|
111 | export default {
|
112 | data() {
|
113 | return {
|
114 | authorName: 'Some author',
|
115 | };
|
116 | },
|
117 | };
|
118 | </script>
|
119 |
|
120 | <template>
|
121 | <div>
|
122 | <gl-sprintf :message="__('Written by %{author}')">
|
123 | <template #author>
|
124 | <span>{{ authorName }}</span>
|
125 | </template>
|
126 | </gl-sprintf>
|
127 | </div>
|
128 | </template>
|
129 | ```
|
130 |
|
131 | The example above renders to this HTML:
|
132 |
|
133 | ```html
|
134 | <div>Written by <span>Some author</span></div>
|
135 | ```
|
136 |
|
137 | ## Usage caveats
|
138 |
|
139 | ### White space
|
140 |
|
141 | `GlSprintf` does not handle white space in scoped slots specially; it is passed
|
142 | through and rendered just like regular text. This means that white space in the
|
143 | scoped slot templates *themselves*, including newlines and indentation, are
|
144 | passed through untouched (assuming the template compiler you're using doesn't
|
145 | trim text nodes at compile time; `vue-template-compiler` preserves white space
|
146 | by default, for instance).
|
147 |
|
148 | Most of the time you don't need to worry about this, since
|
149 | [browsers normalize white space][1] automatically, but here's an example, using
|
150 | punctuation, where you might want to be conscious of the white space in the
|
151 | template:
|
152 |
|
153 | ```html
|
154 | <div>
|
155 | <gl-sprintf :message="__('Foo %{boldStart}bar%{boldEnd}!')">
|
156 | <template #bold="{ content }">
|
157 | <b>
|
158 | {{ content }}
|
159 | </b>
|
160 | </template>
|
161 | </gl-sprintf>
|
162 | </div>
|
163 | ```
|
164 |
|
165 | As written, the literal markup rendered would be:
|
166 |
|
167 | ```html
|
168 | <div> Foo <b>
|
169 | bar
|
170 | </b>!
|
171 | </div>
|
172 | ```
|
173 |
|
174 | where the white space (including newlines) before and after `bar` is exactly
|
175 | the newlines and indentation in the source template. The browser will render
|
176 | this as:
|
177 |
|
178 | <div> Foo <b>
|
179 | bar
|
180 | </b>!
|
181 | </div>
|
182 |
|
183 | Note the single space between `bar` and `!`. To avoid that, remove the
|
184 | white space in the template, or use `v-text`:
|
185 |
|
186 | ```html
|
187 | <div>
|
188 | <gl-sprintf :message="__('Foo %{boldStart}bar%{boldEnd}!')">
|
189 | <template #bold="{ content }">
|
190 | <b>{{ content }}</b>
|
191 | <!-- OR -->
|
192 | <b v-text="content" />
|
193 | </template>
|
194 | </gl-sprintf>
|
195 | </div>
|
196 | ```
|
197 |
|
198 | ### Miscellaneous
|
199 |
|
200 | While there are a lot of caveats here, you don't need to worry about reading
|
201 | them _unless_ you find `GlSprintf` isn't rendering what you'd expect.
|
202 |
|
203 | - Since `GlSprintf` typically renders multiple elements, it can't be used as
|
204 | a component's root, it must be wrapped with at least one other root element,
|
205 | otherwise Vue will throw a `Multiple root nodes returned from render
|
206 | function` error.
|
207 | - If a slot for a given placeholder _isn't_ provided, the placeholder
|
208 | will be rendered as-is, e.g., literally `Written by %{author}` if the
|
209 | `author` slot _isn't_ provided, or literally `%{linkStart}foo%{linkEnd}` if
|
210 | the `link` slot isn't provided.
|
211 | - Content between `Start` and `End` placeholders is effectively thrown away if
|
212 | the scoped slot of the correct name doesn't consume the `content` property in
|
213 | some way, though the slot's components should still be rendered.
|
214 | - If there's no placeholder in the message for a provided named slot, the
|
215 | content of that slot is silently thrown away.
|
216 | - If only one of the `Start` or `End` placeholders is in the message, or they
|
217 | are in the wrong order, they are treated as plain slots, i.e., it is assumed
|
218 | there is no text to extract and pass to the scoped slot. This allows you to
|
219 | use plain slots whose names end in `Start` or `End`, e.g., `backEnd`, or
|
220 | `fromStart` in isolation, without their `Start`/`End` counterparts.
|
221 | - Text extraction between `Start` and `End` placeholders is only done one level
|
222 | deep. This is intentional, so as to avoid building complex sprintf messages
|
223 | that would better be implemented in components. As an example,
|
224 | `${linkStart}test%{icon}%{linkEnd}`, if provided both the `link` and `icon`
|
225 | slots, would pass `test%{icon}` as a literal string as content to the `link`
|
226 | scoped slot.
|
227 | - For more examples and edge cases, please see the test suite for `GlSprintf`.
|
228 | - To be successfully used in `GlSprintf`, slot names should:
|
229 | - start with a letter (`[A-Za-z]`)
|
230 | - only contain alpha-numeric characters (`[A-Za-z0-9]`), underscore (`_`) and
|
231 | dash (`-`),
|
232 | - should not end with underscore (`_`) or dash (`-`) So for example:
|
233 | `%{author}`, `%{author_name}`, `%{authorName}` or `%{author-name-100}` are
|
234 | all valid placeholders.
|
235 |
|
236 | ## Internet Explorer 11
|
237 |
|
238 | This component uses [`String.prototype.startsWith()`] and [`String.prototype.endsWith()`] under the
|
239 | hood. Make sure those methods are polyfilled if you plan on using the component on IE11.
|
240 |
|
241 | [1]: https://www.w3.org/TR/css-text-3/#white-space-phase-1
|
242 | [`String.prototype.startsWith()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
|
243 | [`String.prototype.endsWith()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith
|