@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1@title Internationalization
2@group developer
3
4Describes Phorge translation and localization.
5
6Overview
7========
8
9Phorge partially supports internationalization, but many of the tools
10are missing or in a prototype state.
11
12This document describes what tools exist today, how to add new translations,
13and how to use the translation tools to make a codebase translatable.
14
15
16Adding a New Locale
17===================
18
19To add a new locale, subclass @{class:PhutilLocale}. This allows you to
20introduce a new locale, like "German" or "Klingon".
21
22Once you've created a locale, applications can add translations for that
23locale.
24
25For instructions on adding new classes, see
26@{article@contrib:Adding New Classes}.
27
28
29Adding Translations to Locale
30=============================
31
32To translate strings, subclass @{class:PhutilTranslation}. Translations need
33to belong to a locale: the locale defines an available language, and each
34translation subclass provides strings for it.
35
36Translations are separated from locales so that third-party applications can
37provide translations into different locales without needing to define those
38locales themselves.
39
40For instructions on adding new classes, see
41@{article@contrib:Adding New Classes}.
42
43
44Writing Translatable Code
45=========================
46
47Strings are marked for translation with @{function@arcanist:pht}.
48
49The `pht()` function takes a string (and possibly some parameters) and returns
50the translated version of that string in the current viewer's locale, if a
51translation is available.
52
53If text strings will ultimately be read by humans, they should essentially
54always be wrapped in `pht()`. For example:
55
56```lang=php
57$dialog->appendParagraph(pht('This is an example.'));
58```
59
60This allows the code to return the correct Spanish or German or Russian
61version of the text, if the viewer is using Phorge in one of those
62languages and a translation is available.
63
64Using `pht()` properly so that strings are translatable can be tricky. Briefly,
65the major rules are:
66
67 - Only pass static strings as the first parameter to `pht()`.
68 - Use parameters to create strings containing user names, object names, etc.
69 - Translate full sentences, not sentence fragments.
70 - Let the translation framework handle plural rules.
71 - Use @{class@arcanist:PhutilNumber} for numbers.
72 - Let the translation framework handle subject gender rules.
73 - Translate all human-readable text, even exceptions and error messages.
74
75See the next few sections for details on these rules.
76
77
78Use Static Strings
79==================
80
81The first parameter to `pht()` must always be a static string. Broadly, this
82means it should not contain variables or function or method calls (it's OK to
83split it across multiple lines and concatenate the parts together).
84
85These are good:
86
87```lang=php
88pht('The night is dark.');
89pht(
90 'Two roads diverged in a yellow wood, '.
91 'and sorry I could not travel both '.
92 'and be one traveler, long I stood.');
93
94```
95
96These won't work (they might appear to work, but are wrong):
97
98```lang=php, counterexample
99pht(some_function());
100pht('The duck says, '.$quack);
101pht($string);
102```
103
104The first argument must be a static string so it can be extracted by static
105analysis tools and dumped in a big file for translators. If it contains
106functions or variables, it can't be extracted, so translators won't be able to
107translate it.
108
109Lint will warn you about problems with use of static strings in calls to
110`pht()`.
111
112
113Parameters
114==========
115
116You can provide parameters to a translation string by using `sprintf()`-style
117patterns in the input string. For example:
118
119```lang=php
120pht('%s earned an award.', $actor);
121pht('%s closed %s.', $actor, $task);
122```
123
124This is primarily appropriate for usernames, object names, counts, and
125untranslatable strings like URIs or instructions to run commands from the CLI.
126
127Parameters normally should not be used to combine two pieces of translated
128text: see the next section for guidance.
129
130Sentence Fragments
131==================
132
133You should almost always pass the largest block of text to `pht()` that you
134can. Particularly, it's important to pass complete sentences, not try to build
135a translation by stringing together sentence fragments.
136
137There are several reasons for this:
138
139 - It gives translators more context, so they can be more confident they are
140 producing a satisfying, natural-sounding translation which will make sense
141 and sound good to native speakers.
142 - In some languages, one fragment may need to translate differently depending
143 on what the other fragment says.
144 - In some languages, the most natural-sounding translation may change the
145 order of words in the sentence.
146
147For example, suppose we want to translate these sentence to give the user some
148instructions about how to use an interface:
149
150> Turn the switch to the right.
151
152> Turn the switch to the left.
153
154> Turn the dial to the right.
155
156> Turn the dial to the left.
157
158Maybe we have a function like this:
159
160```
161function get_string($is_switch, $is_right) {
162 // ...
163}
164```
165
166One way to write the function body would be like this:
167
168```lang=php, counterexample
169$what = $is_switch ? pht('switch') : pht('dial');
170$dir = $is_right ? pht('right') : pht('left');
171
172return pht('Turn the ').$what.pht(' to the ').$dir.pht('.');
173```
174
175This will work fine in English, but won't work well in other languages.
176
177One problem with doing this is handling gendered nouns. Languages like Spanish
178have gendered nouns, where some nouns are "masculine" and others are
179"feminine". The gender of a noun affects which article (in English, the word
180"the" is an article) should be used with it.
181
182In English, we say "**the** knob" and "**the** switch", but a Spanish speaker
183would say "**la** perilla" and "**el** interruptor", because the noun for
184"knob" in Spanish is feminine (so it is used with the article "la") while the
185noun for "switch" is masculine (so it is used with the article "el").
186
187A Spanish speaker can not translate the string "Turn the" correctly without
188knowing which gender the noun has. Spanish has //two// translations for this
189string ("Gira el", "Gira la"), and the form depends on which noun is being
190used.
191
192Another problem is that this reduces flexibility. Translating fragments like
193this locks translators into a specific word order, when rearranging the words
194might make the sentence sound much more natural to a native speaker.
195
196For example, if the string read "The knob, to the right, turn it.", it
197would technically be English and most English readers would understand the
198meaning, but no native English speaker would speak or write like this.
199
200However, some languages have different subject-verb order rules or
201colloquialisms, and a word order which transliterates like this may sound more
202natural to a native speaker. By translating fragments instead of complete
203sentences, you lock translators into English word order.
204
205Finally, the last fragment is just a period. If a translator is presented with
206this string in an interface without much context, they have no hope of guessing
207how it is used in the software (it could be an end-of-sentence marker, or a
208decimal point, or a date separator, or a currency separator, all of which have
209very different translations in many locales). It will also conflict with all
210other translations of the same string in the codebase, so even if they are
211given context they can't translate it without technical problems.
212
213To avoid these issues, provide complete sentences for translation. This almost
214always takes the form of writing out alternatives in full. This is a good way
215to implement the example function:
216
217```lang=php
218if ($is_switch) {
219 if ($is_right) {
220 return pht('Turn the switch to the right.');
221 } else {
222 return pht('Turn the switch to the left.');
223 }
224} else {
225 if ($is_right) {
226 return pht('Turn the dial to the right.');
227 } else {
228 return pht('Turn the dial to the left.');
229 }
230}
231```
232
233Although this is more verbose, translators can now get genders correct,
234rearrange word order, and have far more context when translating. This enables
235better, natural-sounding translations which are more satisfying to native
236speakers.
237
238
239Singular and Plural
240===================
241
242Different languages have various rules for plural nouns.
243
244In English there are usually two plural noun forms: for one thing, and any
245other number of things. For example, we say that one chair is a "chair" and any
246other number of chairs are "chairs": "0 chairs", "1 chair", "2 chairs", etc.
247
248In other languages, there are different (and, in some cases, more) plural
249forms. For example, in Czech, there are separate forms for "one", "several",
250and "many".
251
252Because plural noun rules depend on the language, you should not write code
253which hard-codes English rules. For example, this won't translate well:
254
255```lang=php, counterexample
256if ($count == 1) {
257 return pht('This will take an hour.');
258} else {
259 return pht('This will take hours.');
260}
261```
262
263This code is hard-coding the English rule for plural nouns. In languages like
264Czech, the correct word for "hours" may be different if the count is 2 or 15,
265but a translator won't be able to provide the correct translation if the string
266is written like this.
267
268Instead, pass a generic string to the translation engine which //includes// the
269number of objects, and let it handle plural nouns. This is the correct way to
270write the translation:
271
272```lang=php
273return pht('This will take %s hour(s).', new PhutilNumber($count));
274```
275
276If you now load the web UI, you'll see "hour(s)" literally in the UI. To fix
277this so the translation sounds better in English, provide translations for this
278string in the @{class:PhabricatorUSEnglishTranslation} file:
279
280```lang=php
281'This will take %s hour(s).' => array(
282 'This will take an hour.',
283 'This will take hours.',
284),
285```
286
287The string will then sound natural in English, but non-English translators will
288also be able to produce a natural translation.
289
290Note that the translations don't actually include the number in this case. The
291number is being passed from the code, but that just lets the translation engine
292get the rules right: the number does not need to appear in the final
293translations shown to the user.
294
295Using PhutilNumber
296==================
297
298When translating numbers, you should almost always use `%s` and wrap the count
299or number in `new PhutilNumber($count)`. For example:
300
301```lang=php
302pht('You have %s experience point(s).', new PhutilNumber($xp));
303```
304
305This will let the translation engine handle plural noun rules correctly, and
306also format large numbers correctly in a locale-aware way with proper unit and
307decimal separators (for example, `1000000` may be printed as "1,000,000",
308with commas for readability).
309
310The exception to this rule is IDs which should not be written with unit
311separators. For example, this is correct for an object ID:
312
313```lang=php
314pht('This diff has ID %d.', $diff->getID());
315```
316
317Male and Female
318===============
319
320Different languages also use different words for talking about subjects who are
321male, female or have an unknown gender. In English this is mostly just
322pronouns (like "he" and "she") but there are more complex rules in other
323languages, and languages like Czech also require verb agreement.
324
325When a parameter refers to a gendered person, pass an object which implements
326@{interface@arcanist:PhutilPerson} to `pht()` so translators can provide
327gendered translation variants.
328
329```lang=php
330pht('%s wrote', $actor);
331```
332
333Translators will create these translations:
334
335```lang=php
336// English translation
337'%s wrote';
338
339// Czech translation
340array('%s napsal', '%s napsala');
341```
342
343(You usually don't need to worry very much about this rule, it is difficult to
344get wrong in standard code.)
345
346
347Exceptions and Errors
348=====================
349
350You should translate all human-readable text, even exceptions and error
351messages. This is primarily a rule of convenience which is straightforward
352and easy to follow, not a technical rule.
353
354Some exceptions and error messages don't //technically// need to be translated,
355as they will never be shown to a user, but many exceptions and error messages
356are (or will become) user-facing on some way. When writing a message, there is
357often no clear and objective way to determine which type of message you are
358writing. Rather than try to distinguish which are which, we simply translate
359all human-readable text. This rule is unambiguous and easy to follow.
360
361In cases where similar error or exception text is often repeated, it is
362probably appropriate to define an exception for that category of error rather
363than write the text out repeatedly, anyway. Two examples are
364@{class@arcanist:PhutilInvalidStateException} and
365@{class@arcanist:PhutilMethodNotImplementedException}, which mostly exist to
366produce a consistent message about a common error state in a convenient way.
367
368There are a handful of error strings in the codebase which may be used before
369the translation framework is loaded, or may be used during handling other
370errors, possibly raised from within the translation framework. This handful
371of special cases are left untranslated to prevent fatals and cycles in the
372error handler.
373
374
375Next Steps
376==========
377
378Continue by:
379
380 - adding a new locale or translation file with
381 @{article@contrib:Adding New Classes}.