[ Index ] |
|
Code source de Symfony 1.0.0 |
1 Chapter 13 - I18n And L10n 2 ========================== 3 4 If you ever developed an international application, you know that dealing with every aspect of text translation, local standards, and localized content can be a nightmare. Fortunately, symfony natively automates all the aspects of internationalization. 5 6 As it is a long word, developers often refer to internationalization as i18n (count the letters in the word to know why). Localization is referred to as l10n. They cover two different aspects of multilingual web applications. 7 8 An internationalized application contains several versions of the same content in various languages or formats. For instance, a webmail interface can offer the same service in several languages; only the interface changes. 9 10 A localized application contains distinct information according to the country from which it is browsed. Think about the contents of a news portal: When browsed from the United States, it displays the latest headlines about the United States, but when browsed from France, the headlines concern the French news. So an l10n application not only provides content translation, but the content can be different from one localized version to another. 11 12 All in all, dealing with i18n and l10n means that the application can take care of the following: 13 14 * Text translation (interface, assets, and content) 15 * Standards and formats (dates, amounts, numbers, and so on) 16 * Localized content (many versions of a given object according to a country) 17 18 This chapter covers the way symfony deals with those elements and how you can use it to develop internationalized and localized applications. 19 20 User Culture 21 ------------ 22 23 All the built-in i18n features in symfony are based on a parameter of the user session called the culture. The culture is the combination of the country and the language of the user, and it determines how the text and culture-dependent information are displayed. Since it is serialized in the user session, the culture is persistent between pages. 24 25 ### Setting the Default Culture 26 27 By default, the culture of new users is the `default_culture`. You can change this setting in the `i18n.yml` configuration file, as shown in Listing 13-1. 28 29 Listing 13-1 - Setting the Default Culture, in `myapp/config/i18n.yml` 30 31 all: 32 default_culture: fr_FR 33 34 >**NOTE** 35 >During development, you might be surprised that a culture change in the `i18n.yml` file doesn't change the current culture in the browser. That's because the session already has a culture from previous pages. If you want to see the application with the new default culture, you need to clear the domain cookies or restart your browser. 36 37 Keeping both the language and the country in the culture is necessary because you may have a different French translation for users from France, Belgium, or Canada, and a different Spanish content for users from Spain or Mexico. The language is coded in two lowercase characters, according to the ISO 639-1 standard (for instance, `en` for English). The country is coded in two uppercase characters, according to the ISO 3166-1 standard (for instance, `GB` for Great Britain). 38 39 ### Changing the Culture for a User 40 41 The user culture can be changed during the browsing session--for instance, when a user decides to switch from the English version to the French version of the application, or when a user logs in to the application and uses the language stored in his preferences. That's why the `sfUser` class offers getter and setter methods for the user culture. Listing 13-2 shows how to use these methods in an action. 42 43 Listing 13-2 - Setting and Retrieving the Culture in an Action 44 45 [php] 46 // Culture setter 47 $this->getUser()->setCulture('en_US'); 48 49 // Culture getter 50 $culture = $this->getUser()->getCulture(); 51 => en_US 52 53 >**SIDEBAR** 54 >Culture in the URL 55 > 56 >When using symfony's localization and internationalization features, pages tend to have different versions for a single URL--it all depends on the user session. This prevents you from caching or indexing your pages in a search engine. 57 > 58 >One solution is to make the culture appear in every URL, so that translated pages can be seen as different URLs to the outside world. In order to do that, add the `:sf_culture` token in every rule of your application `routing.yml`: 59 > 60 > 61 > page: 62 > url: /:sf_culture/:page 63 > requirements: { sf_culture: (?:fr|en|de) } 64 > params: ... 65 > 66 > article: 67 > url: /:sf_culture/:year/:month/:day/:slug 68 > requirements: { sf_culture: (?:fr|en|de) } 69 > params: ... 70 > 71 > 72 >To avoid manually setting the sf_culture request parameter in every `link_to()`, symfony automatically adds the user culture to the default routing parameters. It also works inbound because symfony will automatically change the user culture if the `sf_culture` parameter is found in the URL. 73 74 ### Determining the Culture Automatically 75 76 In many applications, the user culture is defined during the first request, based on the browser preferences. Users can define a list of accepted languages in their browser, and this data is sent to the server with each request, in the `Accept-Language` HTTP header. You can retrieve it in symfony through the `sfRequest` object. For instance, to get the list of preferred languages of a user in an action, type this: 77 78 [php] 79 $languages = $this->getRequest()->getLanguages(); 80 81 The HTTP header is a string, but symfony automatically parses it and converts it into an array. So the preferred language of the user is accessible with `$languages[0]` in the preceding example. 82 83 It can be useful to automatically set the user culture to the preferred browser languages in a site home page or in a filter for all pages. 84 85 >**CAUTION** 86 >The `Accept-Language` HTTP header is not very reliable information, since users rarely know how to modify it in their browser. Most of the time, the preferred browser language is the language of the interface, and browsers are not available in all languages. If you decide to set the culture automatically according to the browser preferred language, make sure you provide a way for the user to choose an alternate language. 87 88 Standards and Formats 89 --------------------- 90 91 The internals of a web application don't care about cultural particularities. Databases, for instance, use international standards to store dates, amounts, and so on. But when data is sent to or retrieved from users, a conversion needs to be made. Users won't understand timestamps, and they will prefer to declare their mother language as Français instead of French. So you will need assistance to do the conversion automatically, based on the user culture. 92 93 ### Outputting Data in the User's Culture 94 95 Once the culture is defined, the helpers depending on it will automatically have proper output. For instance, the `format_number()` helper automatically displays a number in a format familiar to the user, according to its culture, as shown in Listing 13-3. 96 97 Listing 13-3 - Displaying a Number for the User's Culture 98 99 [php] 100 <?php use_helper('Number') ?> 101 102 <?php $sf_user->setCulture('en_US') ?> 103 <?php echo format_number(12000.10) ?> 104 => '12,000.10' 105 106 <?php $sf_user->setCulture('fr_FR') ?> 107 <?php echo format_number(12000.10) ?> 108 => '12 000,10' 109 110 You don't need to explicitly pass the culture to the helpers. They will look for it themselves in the current session object. Listing 13-4 lists helpers that take into account the user culture for their output. 111 112 Listing 13-4 - Culture-Dependent Helpers 113 114 [php] 115 <?php use_helper('Date') ?> 116 117 <?php echo format_date(time()) ?> 118 => '9/14/06' 119 120 <?php echo format_datetime(time()) ?> 121 => 'September 14, 2006 6:11:07 PM CEST' 122 123 <?php use_helper('Number') ?> 124 125 <?php echo format_number(12000.10) ?> 126 => '12,000.10' 127 128 <?php echo format_currency(1350, 'USD') ?> 129 => '$1,350.00' 130 131 <?php use_helper('I18N') ?> 132 133 <?php echo format_country('US') ?> 134 => 'United States' 135 136 <?php format_language('en') ?> 137 => 'English' 138 139 <?php use_helper('Form') ?> 140 141 <?php echo input_date_tag('birth_date', mktime(0, 0, 0, 9, 14, 2006)) ?> 142 => input type="text" name="birth_date" id="birth_date" value="9/14/06" size="11" /> 143 144 <?php echo select_country_tag('country', 'US') ?> 145 => <select name="country" id="country"><option value="AF">Afghanistan</option> 146 ... 147 <option value="GB">United Kingdom</option> 148 <option value="US" selected="selected">United States</option> 149 <option value="UM">United States Minor Outlying Islands</option> 150 <option value="UY">Uruguay</option> 151 ... 152 </select> 153 154 The date helpers can accept an additional format parameter to force a culture-independent display, but you shouldn't use it if your application is internationalized. 155 156 ### Getting Data from a Localized Input 157 158 If it is necessary to show data in the user's culture, as for retrieving data, you should, as much as possible, push users of your application to input already internationalized data. This approach will save you from trying to figure out how to convert data with varying formats and uncertain locality. For instance, who might enter a monetary value with comma separators in an input box? 159 160 You can frame the user input format either by hiding the actual data (as in a `select_country_tag()`) or by separating the different components of complex data into several simple inputs. 161 162 For dates, however, this is often not possible. Users are used to entering dates in their cultural format, and you need to be able to convert such data to an internal (and international) format. This is where the `sfI18N` class applies. Listing 13-5 demonstrates how this class is used. 163 164 Listing 13-5 - Getting a Date from a Localized Format in an Action 165 166 [php] 167 $date= $this->getRequestParameter('birth_date'); 168 $user_culture = $this->getUser()->getCulture(); 169 170 // Getting a timestamp 171 $timestamp = sfI18N::getTimestampForCulture($date, $user_culture); 172 173 // Getting a structured date 174 list($d, $m, $y) = sfI18N::getDateForCulture($date, $user_culture); 175 176 Text Information in the Database 177 -------------------------------- 178 179 A localized application offers different content according to the user's culture. For instance, an online shop can offer products worldwide at the same price, but with a custom description for every country. This means that the database must be able to store different versions of a given piece of data, and for that, you need to design your schema in a particular way and use culture each time you manipulate localized model objects. 180 181 ### Creating Localized Schema 182 183 For each table that contains some localized data, you should split the table in two parts: one table that does not have any i18n column, and the other one with only the i18n columns. The two tables are to be linked by a one-to-many relationship. This setup lets you add more languages when required without changing your model. Let's consider an example using a `Product` table. 184 185 First, create tables in the `schema.yml` file, as shown in Listing 13-6. 186 187 Listing 13-6 - Sample Schema for i18n Data, in `config/schema.yml` 188 189 my_connection: 190 my_product: 191 _attributes: { phpName: Product, isI18N: true, i18nTable: my_product_i18n } 192 id: { type: integer, required: true, primaryKey: true, autoincrement: true } 193 price: { type: float } 194 195 my_product_i18n: 196 _attributes: { phpName: ProductI18n } 197 id: { type: integer, required: true, primaryKey: true, foreignTable: my_product, foreignReference: id } 198 culture: { isCulture: true, type: varchar, size: 7, required: true, primaryKey: true } 199 name: { type: varchar, size: 50 } 200 201 Notice the `isI18N` and `i18nTable` attributes in the first table, and the special `culture` column in the second. All these are symfony-specific Propel enhancements. 202 203 The symfony automations can make this much faster to write. If the table containing internationalized data has the same name as the main table with `_i18n` as a suffix, and they are related with a column named `id` in both tables, you can omit the `id` and `culture` columns in the `_i18n` table as well as the specific i18n attributes for the main table; symfony will infer them. It means that symfony will see the schema in Listing 13-7 as the same as the one in Listing 13-6. 204 205 Listing 13-7 - Sample Schema for i18n Data, Short Version, in `config/schema.yml` 206 207 my_connection: 208 my_product: 209 _attributes: { phpName: Product } 210 id: 211 price: float 212 my_product_i18n: 213 _attributes: { phpName: ProductI18n } 214 name: varchar(50) 215 216 ### Using the Generated I18n Objects 217 218 Once the corresponding object model is built (don't forget to call `symfony` `propel-build-model` and clear the cache with a `symfony cc` after each modification of the `schema.yml`), you can use your `Product` class with i18n support as if there were only one table, as shown in Listing 13-8. 219 220 Listing 13-8 - Dealing with i18n Objects 221 222 [php] 223 $product = ProductPeer::retrieveByPk(1); 224 $product->setCulture('fr'); 225 $product->setName('Nom du produit'); 226 $product->save(); 227 228 $product->setCulture('en'); 229 $product->setName('Product name'); 230 $product->save(); 231 232 echo $product->getName(); 233 => 'Product name' 234 235 $product->setCulture('fr'); 236 echo $product->getName(); 237 => 'Nom du produit' 238 239 If you'd rather not have to remember to change the culture each time you use an i18n object, you can also change the `hydrate()` method in the object class. See an example in Listing 13-9. 240 241 Listing 13-9 - Overriding the `hydrate()` Method to Set the Culture, in `myproject/lib/model/Product.php` 242 243 [php] 244 public function hydrate(ResultSet $rs, $startcol = 1) 245 { 246 parent::hydrate($rs, $startcol); 247 $this->setCulture(sfContext::getInstance()->getUser()->getCulture()); 248 } 249 250 As for queries with the peer objects, you can restrict the results to objects having a translation for the current culture by using the `doSelectWithI18n` method, instead of the usual `doSelect`, as shown in Listing 13-10. In addition, it will create the related i18n objects at the same time as the regular ones, resulting in a reduced number of queries to get the full content (refer to Chapter 18 for more information about this method's positive impacts on performance). 251 252 Listing 13-10 - Retrieving Objects with an i18n `Criteria` 253 254 [php] 255 $c = new Criteria(); 256 $c->add(ProductPeer::PRICE, 100, Criteria::LESS_THAN); 257 $products = ProductPeer::doSelectWithI18n($c, $culture); 258 // The $culture argument is optional 259 // The current user culture is used if no culture is given 260 261 So basically, you should never have to deal with the i18n objects directly, but instead pass the culture to the model (or let it guess it) each time you do a query with the regular objects. 262 263 Interface Translation 264 --------------------- 265 266 The user interface needs to be adapted for i18n applications. Templates must be able to display labels, messages, and navigation in several languages but with the same presentation. Symfony recommends that you build your templates with the default language, and that you provide a translation for the phrases used in your templates in a dictionary file. That way, you don't need to change your templates each time you modify, add, or remove a translation. 267 268 ### Configuring Translation 269 270 The templates are not translated by default, which means that you need to activate the template translation feature in the `settings.yml` file prior to everything else, as shown in Listing 13-11. 271 272 Listing 13-11 - Activating Interface Translation, in `myapp/config/settings.yml` 273 274 all: 275 .settings: 276 i18n: on 277 278 ### Using the Translation Helper 279 280 Let's say that you want to create a website in English and French, with English being the default language. Before even thinking about having the site translated, you probably wrote the templates something like the example shown in Listing 13-12. 281 282 Listing 13-12 - A Single-Language Template 283 284 [php] 285 Welcome to our website. Today's date is <?php echo format_date(date()) ?> 286 287 For symfony to translate the phrases of a template, they must be identified as text to be translated. This is the purpose of the `__()` helper (two underscores), a member of the I18N helper group. So all your templates need to enclose the phrases to translate in such function calls. Listing 13-12, for example, can be modified to look like Listing 13-13 (as you will see in the "Handling Complex Translation Needs" section later in this chapter, there is an even better way to call the translation helper in this example). 288 289 Listing 13-13 - A Multiple-Language-Ready Template 290 291 [php] 292 <?php use_helper('I18N') ?> 293 294 <?php echo __('Welcome to our website.') ?> 295 <?php echo __("Today's date is ") ?> 296 <?php echo format_date(date()) ?> 297 298 >**TIP** 299 >If your application uses the I18N helper group for every page, it is probably a good idea to include it in the `standard_helpers` setting in the `settings.yml` file, so that you avoid repeating `use_helper('I18N')` for each template. 300 301 ### Using Dictionary Files 302 303 Each time the `__()` function is called, symfony looks for a translation of its argument in the dictionary of the current user's culture. If it finds a corresponding phrase, the translation is sent back and displayed in the response. So the user interface translation relies on a dictionary file. 304 305 The dictionary files are written in the XML Localization Interchange File Format (XLIFF), named according to the pattern `messages.[language code].xml`, and stored in the application `i18n/` directory. 306 307 XLIFF is a standard format based on XML. As it is well known, you can use third-party translation tools to reference all text in your website and translate it. Translation firms know how to handle such files and to translate an entire site just by adding a new XLIFF translation. 308 309 >**TIP** 310 >In addition to the XLIFF standard, symfony also supports several other translation back-ends for dictionaries: gettext, MySQL, SQLite, and Creole. Refer to the API documentation for more information about configuring these back-ends. 311 312 Listing 13-14 shows an example of the XLIFF syntax with the messages.fr.xml file necessary to translate Listing 13-13 into French. 313 314 Listing 13-14 - An XLIFF Dictionary, in `myapp/i18n/messages.fr.xml` 315 316 [xml] 317 <?xml version="1.0" ?> 318 <xliff version="1.0"> 319 <file orginal="global" source-language="en_US" datatype="plaintext"> 320 <body> 321 <trans-unit id="1"> 322 <source>Welcome to our website.</source> 323 <target>Bienvenue sur notre site web.</target> 324 </trans-unit> 325 <trans-unit id="2"> 326 <source>Today's date is </source> 327 <target>La date d'aujourd'hui est </target> 328 </trans-unit> 329 </body> 330 </file> 331 </xliff> 332 333 The `source-language` attribute must always contain the full ISO code of your default culture. Each translation is written in a `trans-unit` tag with a unique `id` attribute. 334 335 With the default user culture (set to en_US), the phrases are not translated and the raw arguments of the `__()` calls are displayed. The result of Listing 13-13 is then similar to Listing 13-12. However, if the culture is changed to `fr_FR` or `fr_BE`, the translations from the `messages.fr.xml` file are displayed instead, and the result looks like Listing 13-15. 336 337 Listing 13-15 - A Translated Template 338 339 [php] 340 Bienvenue sur notre site web. La date d'aujourd'hui est 341 <?php echo format_date(date()) ?> 342 343 If additional translations need to be done, simply add a new `messages.``XX``.xml` translation file in the same directory. 344 345 ### Managing Dictionaries 346 347 If your `messages.XX.xml` file becomes too long to be readable, you can always split the translations into several dictionary files, named by theme. For instance, you can split the `messages.fr.xml` file into these three files in the application `i18n/` directory: 348 349 * `navigation.fr.xml` 350 * `terms_of_service.fr.xml` 351 * `search.fr.xml` 352 353 Note that as soon as a translation is not to be found in the default `messages.XX.xml` file, you must declare which dictionary is to be used each time you call the `__()` helper, using its third argument. For instance, to output a string that is translated in the `navigation.fr.xml` dictionary, write this: 354 355 [php] 356 <?php echo __('Welcome to our website', null, 'navigation') ?> 357 358 Another way to organize translation dictionaries is to split them by module. Instead of writing a single `messages.XX.xml` file for the whole application, you can write one in each `modules/[module_name]/i18n/` directory. It makes modules more independent from the application, which is necessary if you want to reuse them, such as in plug-ins (see Chapter 17). 359 360 ### Handling Other Elements Requiring Translation 361 362 The following are other elements that may require translation: 363 364 * Images, text documents, or any other type of assets can also vary according to the user culture. The best example is a piece of text with a special typography that is actually an image. For these, you can create subdirectories named after the user `culture`: 365 366 [php] 367 <?php echo image_tag($sf_user->getCulture().'/myText.gif') ?> 368 369 * Error messages from validation files are automatically output by a `__()`, so you just need to add their translation to a dictionary to have them translated. 370 * The default symfony pages (page not found, internal server error, restricted access, and so on) are in English and must be rewritten in an i18n application. You should probably create your own `default` module in your application and use `__()` in its templates. Refer to Chapter 19 to see how to customize these pages. 371 372 ### Handling Complex Translation Needs 373 374 Translation only makes sense if the `__()` argument is a full sentence. However, as you sometimes have formatting or variables mixed with words, you could be tempted to cut sentences into several chunks, thus calling the helper on senseless phrases. Fortunately, the `__()` helper offers a replacement feature based on tokens, which will help you to have a meaningful dictionary that is easier to handle by translators. As with HTML formatting, you can leave it in the helper call as well. Listing 13-16 shows an example. 375 376 Listing 13-16 - Translating Sentences That Contain Code 377 378 [php] 379 // Base example 380 Welcome to all the <b>new</b> users.<br /> 381 There are <?php echo count_logged() ?> persons logged. 382 383 // Bad way to enable text translation 384 <?php echo __('Welcome to all the') ?> 385 <b><?php echo __('new') ?></b> 386 <?php echo __('users') ?>.<br /> 387 <?php echo __('There are') ?> 388 <?php echo count_logged() ?> 389 <?php echo __('persons logged') ?> 390 391 // Good way to enable text translation 392 <?php echo __('Welcome to all the <b>new</b> users') ?> <br /> 393 <?php echo __('There are %1% persons logged', array('%1%' => count_logged())) ?> 394 395 In this example, the token is `%1%`, but it can be anything, since the replacement function used by the translation helper is `strtr()`. 396 397 One of the common problems with translation is the use of the plural form. According to the number of results, the text changes but not in the same way according to the language. For instance, the last sentence in Listing 13-16 is not correct if `count_logged()` returns 0 or 1. You could do a test on the return value of this function and choose which sentence to use accordingly, but that would represent a lot of code. Additionally, different languages have different grammar rules, and the declension rules of plural can be quite complex. As this problem is very common, symfony provides a helper to deal with it, called `format_number_choice()`. Listing 13-17 demonstrates how to use this helper. 398 399 Listing 13-17 - Translating Sentences Depending on the Value of Parameters 400 401 [php] 402 <?php echo format_number_choice( 403 '[0]Nobody is logged|[1]There is 1 person logged|(1,+Inf]There are%1% persons logged', array('%1%' => count_logged()), count_logged()) ?> 404 405 The first argument is the multiple possibilities of text. The second argument is the replacement pattern (as with the `__()` helper) and is optional. The third argument is the number on which the test is made to determine which text is taken. 406 407 The message/string choices are separated by the pipe (`|`) character followed by an array of acceptable values, using the following syntax: 408 409 * `[1,2]`: Accepts values between 1 and 2, inclusive 410 * `(1,2)`: Accepts values between 1 and 2, excluding 1 and 2 411 * `{1,2,3,4}`: Only values defined in the set are accepted 412 * `[-Inf,0)`: Accepts values greater or equal to negative infinity and strictly less than 0 413 414 Any nonempty combinations of the delimiters of square brackets and parentheses are acceptable. 415 416 The message will need to appear explicitly in the XLIFF file for the translation to work properly. Listing 13-18 shows an example. 417 418 Listing 13-18 - XLIFF Dictionary for a `format_number_choice()` Argument 419 420 ... 421 <trans-unit id="3"> 422 <source>[0]Nobody is logged|[1]There is 1 person logged|(1,+Inf]There are%1% persons logged</source> 423 <target>[0]Personne n'est connecté|[1]Une personne est connectée|(1,+Inf]Ily a %1% personnes en ligne</target> 424 </trans-unit> 425 ... 426 427 >**SIDEBAR** 428 >A few words about charsets 429 > 430 >Dealing with internationalized content in templates often leads to problems with charsets. If you use a localized charset, you will need to change it each time the user changes culture. In addition, the templates written in a given charset will not display the characters of another charset properly. 431 > 432 >This is why, as soon as you deal with more than one culture, all your templates must be saved in UTF-8, and the layout must declare the content with this charset. You won't have any unpleasant surprises if you always work with UTF-8, and you will save yourself from a big headache. 433 > 434 >Symfony applications rely on one central setting for the charset, in the `settings.yml` file. Changing this parameter will change the `content-type` header of all responses. 435 > 436 > all: 437 > .settings: 438 > charset: utf-8 439 440 ### Calling the Translation Helper Outside a Template 441 442 Not all the text that is displayed in a page comes from templates. That's why you often need to call the `__()` helper in other parts of your application: actions, filters, model classes, and so on. Listing 13-19 shows how to call the helper in an action by retrieving the current instance of the `I18N` object through the context singleton. 443 444 Listing 13-19 - Calling `__()` in an Action 445 446 [php] 447 $this->getContext()->getI18N()->__($text, $args, 'messages'); 448 449 Summary 450 ------- 451 452 Handling internationalization and localization in web applications is painless if you know how to deal with the user culture. The helpers automatically take it into account to output correctly formatted data, and the localized content from the database is seen as if it were part of a simple table. As for the interface translation, the `__()` helper and XLIFF dictionary ensure that you will have maximum versatility with minimum work.
titre
Description
Corps
titre
Description
Corps
titre
Description
Corps
titre
Corps
Généré le : Fri Mar 16 22:42:14 2007 | par Balluche grâce à PHPXref 0.7 |