Caution: You are browsing the legacy symfony 1.x part of this website.
Cover of the book Symfony 5: The Fast Track

Symfony 5: The Fast Track is the best book to learn modern Symfony development, from zero to production. +300 pages in full color showing how to combine Symfony with Docker, APIs, queues & async tasks, Webpack, Single-Page Applications, etc.

Buy printed version

Ngày 19: Internationalization và Localization

1.2 / Propel

Ngày hôm qua, chúng ta đã hoàn thành chức năng tìm kiếm bằng cách thêm vào một vài tiện ích AJAX.

Hôm nay, chúng ta sẽ nói về việc internationalization (i18n) và localization (l10n) ứng dụng Jobeet.

From Wikipedia:

Internationalization is the process of designing a software application so that it can be adapted to various languages and regions without engineering changes.

Localization is the process of adapting software for a specific region or language by adding locale-specific components and translating text.

Như thường lệ, symfony framework không "làm lại cái bánh xe" và hỗ trợ i18n và l10n dựa trên ICU standard.

User

Không thể internationalization mà không có user. Khi website của bạn có các lựa chọn ngôn ngữ khác nhau cho cac vùng trên thế giới, user sẽ chọn ngôn ngữ phù hợp với mình.

note

Chúng ta đã nói về class symfony User ở ngày 13.

User Culture

Tính năng i18n và l10n của symfony dựa trên user culture. Culture là sự kết hợp giữa ngôn ngữ và quốc gia của user. Ví dụ, culture của user nói tiếng Pháp (fr) và sống ở nước Pháp sẽ là fr_FR.

Bạn có thể quản lý user culture thông qua phương thức setCulture()getCulture() của object User:

// in an action
$this->getUser()->setCulture('fr_BE');
echo $this->getUser()->getCulture();

tip

Mã ngôn ngữ là 2 kí tự viết thường, theo tiêu chuẩn ISO 639-1, và mã nước là 2 kí tự viết hoa, theo tiêu chuẩn ISO 3166-1.

Culture mặc định

Culture mặc định được xác định trong file settings.yml:

# apps/frontend/config/settings.yml
all:
  .settings:
    default_culture: it_IT

tip

Do culture được quản lý bởi object User, nên nó được chứa trong session. Trong quá trình phát triển, nếu bạn thay đổi culture mặc định, bạn cần phải xóa session cookie để thiết lập mới có hiệu quả.

Khi một user bắt đầu một session trên Jobeet website, chúng ta có thể quyết định culture phù hợp nhất, dựa trên thông tin cung cấp bởi Accept-Language HTTP header.

Phương thức getLanguages() của request object trả về mảng các ngôn ngữ phù hợp với user hiện tại, sắp xếp theo thứ tự ưu tiên:

// in an action
$languages = $request->getLanguages();

Nhưng website của bạn thường không có hết 136 ngôn ngữ chính trên thế giới. Phương thức getPreferredCulture() trả về ngôn ngữ thích hợp nhất bằng cách so sánh các ngôn ngữ phù hợp với user với các ngôn ngữ mà website hỗ trợ:

// in an action
$language = $request->getPreferredCulture(array('en', 'fr'));

Nếu không có ngôn ngữ nào phù hợp, ngôn ngữ đầu tiên trong mảng (ở đây là English) sẽ được chọn.

Culture trên URL

Website Jobeet hỗ trợ English và French. Do mỗi URL chỉ có thể tương ứng với một resource, nên culture phải được nhúng trong URL. Để thực hiện điều này, mở file routing.yml, và thêm biến :sf_culture vào tất cả routes trừ api_jobshomepage. Để đơn giản route, ta thêm /:sf_culture vào trước của url. Với collection route, ta thêm một prefix_path option bắt đầu với /:sf_culture.

# apps/frontend/config/routing.yml
affiliate:
  class: sfPropelRouteCollection
  options:
    model:          JobeetAffiliate
    actions:        [new, create]
    object_actions: { wait: get }
    prefix_path:    /:sf_culture/affiliate
 
category:
  url:     /:sf_culture/category/:slug.:sf_format
  class:   sfPropelRoute
  param:   { module: category, action: show, sf_format: html }
  options: { model: JobeetCategory, type: object }
  requirements:
    sf_format: (?:html|atom)
 
job_search:
  url:   /:sf_culture/search
  param: { module: job, action: search }
 
job:
  class: sfPropelRouteCollection
  options:
    model:          JobeetJob
    column:         token
    object_actions: { publish: put, extend: put }
    prefix_path:    /:sf_culture/job
  requirements:
    token: \w+
 
job_show_user:
  url:     /:sf_culture/job/:company_slug/:location_slug/:id/:position_slug
  class:   sfPropelRoute
  options:
    model: JobeetJob
    type: object
    method_for_criteria: doSelectActive
  param:   { module: job, action: show }
  requirements:
    id:        \d+
    sf_method: get

Khi biến sf_culture được sử dụng ở route, symfony sẽ tự động sử dụng giá trị này để xác định culture của user.

Do chúng ta cần trang chủ hỗ trợ nhiều ngôn ngữ (/en/, /fr/, ...), nên trang chủ mặc định (/) phải được chuyển sang ngôn ngữ phù hợp với culture của user. Nhưng nếu user chưa xác định culture, do anh ta truy cập vào Jobeet lần đầu tiên, hệ thống sẽ tự chon culture thích hợp.

Thêm phương thức isFirstRequest() vào myUser, trả về true nếu là request đầu tiên của một user session:

// apps/frontend/lib/myUser.class.php
public function isFirstRequest($boolean = null)
{
  if (is_null($boolean))
  {
    return $this->getAttribute('first_request', true);
  }
  else
  {
    $this->setAttribute('first_request', $boolean);
  }
}

Thêm route localized_homepage :

# apps/frontend/config/routing.yml
localized_homepage:
  url:   /:sf_culture/
  param: { module: job, action: index }
  requirements:
    sf_culture: (?:fr|en)

Sửa lại action index của module job để chuyển người dùng trang trang chủ với ngôn ngữ phù hợp:

// apps/frontend/modules/job/actions/actions.class.php
public function executeIndex(sfWebRequest $request)
{
  if (!$request->getParameter('sf_culture'))
  {
    if ($this->getUser()->isFirstRequest())
    {
      $culture = $request->getPreferredCulture(array('en', 'fr'));
      $this->getUser()->setCulture($culture);
      $this->getUser()->isFirstRequest(false);
    }
    else
    {
      $culture = $this->getUser()->getCulture();
    }
 
    $this->redirect('@localized_homepage');
  }
 
  $this->categories = JobeetCategoryPeer::getWithJobs();
}

Nếu biến sf_culture chưa có trong request, có nghĩa là user truy cập vào trang web từ URL /. Trong trường hợp này và session là mới, culture thích hợp nhất sẽ được chọn. Ngược lại, culture hiện tại chứa trong session sẽ được sử dụng.

Cuối cùng, chuyển user tới URL localized_homepage. Chú ý rằng, ta ko cần cung cấp biến sf_culture, symfony sẽ tự động làm việc này cho bạn.

Khi truy cập URL /it/, symfony sẽ trả về một 404 error do chúng ta giới hạn biến sf_cultureen, hoặc fr. Thêm yêu cầu này vào tất cả các route có chứa culture:

requirements:
  sf_culture: (?:fr|en)

Test Culture

Bây giờ, đã đến lúc test những gì chúng ta đã làm. Nhưng trước khi thêm các test, chúng ta cần sửa lại các test cũ. Do tất cả các URL đã thay đổi, ta cần sửa lại tất cả các functional test trong test/functional/frontend/ và thêm /en vào trước các URL. Đừng quên đổi lại các URL trong file lib/test/JobeetTestFunctional.class.php. Chạy test suite để kiểm tra xem bạn đã sửa đúng chưa:

$ php symfony test:functional frontend

User tester cung cấp phương thức isCulture() để kiểm tra culture hiện tại của user. Mở file jobActionsTest và thêm đoạn test sau:

// test/functional/frontend/jobActionsTest.php
$browser->setHttpHeader('ACCEPT_LANGUAGE', 'fr_FR,fr,en;q=0.7');
$browser->
  info('6 - User culture')->
 
  restart()->
 
  info('  6.1 - For the first request, symfony guesses the best culture')->
  get('/')->
  isRedirected()->followRedirect()->
  with('user')->isCulture('fr')->
 
  info('  6.2 - Available cultures are en and fr')->
  get('/it/')->
  with('response')->isStatusCode(404)
;
 
$browser->setHttpHeader('ACCEPT_LANGUAGE', 'en,fr;q=0.7');
$browser->
  info('  6.3 - The culture guessing is only for the first request')->
 
  get('/')->
  isRedirected()->followRedirect()->
  with('user')->isCulture('fr')
;

Chuyển đổi ngôn ngữ

Để user có thể chuyển sang ngôn ngữ khác, ta cần thêm một language form vào layout. Form framework không cung cấp form ngoài nhưng điều này là cần thiết đối với websites đa ngôn ngữ, do đó symfony core team đã phát triển sfFormExtraPlugin, chứa các validator, widget, và form hữu ích.

Cài đặt plugin với task plugin:install:

$ php symfony plugin:install sfFormExtraPlugin

Xóa cache để các class mới của plugin có tác dụng:

$ php symfony cc

note

sfFormExtraPlugin chứa các widget yêu cầu một số thư viện liên quan, như thư viện JavaScript. Bạn sẽ tìm thấy một widget cho rich date selectors, một cho WYSIWYG editor, ... Hãy dành chút thời gian đọc hướng dẫn bạn sẽ tìm thấy nhiều thông tin hữu ích.

Plugin sfFormExtraPlugin cung cấp một form sfFormLanguage để quản lý việc chọn ngôn ngữ. Thêm language form vào layout như sau:

note

Code dưới đây chứa một vài lỗi để chỉ cho bạn thấy cách viết code không đúng. Chúng tôi sẽ chỉ cho bạn cách viết đúng ngay sau đó.

// apps/frontend/templates/layout.php
<div id="footer">
  <div class="content">
    <!-- footer content -->
 
    <?php $form = new sfFormLanguage(
      $sf_user,
      array('languages' => array('en', 'fr'))
      )
    ?>
    <form action="<?php echo url_for('@change_language') ?>">
      <?php echo $form ?><input type="submit" value="ok" />
    </form>
  </div>
</div>

Vấn đề ở đây là gì? Đó chính là việc tạo form object không nằm ở View layer. Nó phải được tạo từ một action. Nhưng code nằm trong layout, và form phải được tạo với mọi action. Trong trường hợp này, bạn có thể sử dụng component. Một component tương tự như partial nhưng có thêm một vài đoạn code gắn với nó. CÓ thể coi nó là một lightweight action.

Include một component trong template được thực hiện thông qua helper include_component():

// apps/frontend/templates/layout.php
<div id="footer">
  <div class="content">
    <!-- footer content -->
 
    <?php include_component('language', 'language') ?>
  </div>
</div>

Helper nhận tham số là tên module và component. Tham số thứ 3 là các giá trị cung cấp cho component đó.

Tạo module language để chứa component và action thực hiện việc chuyển đổi ngôn ngữ cho user:

$ php symfony generate:module frontend language

Components được xác định trong file actions/components.class.php.

Nội dung file này:

// apps/frontend/modules/language/actions/components.class.php
class languageComponents extends sfComponents
{
  public function executeLanguage(sfWebRequest $request)
  {
    $this->form = new sfFormLanguage(
      $this->getUser(),
      array('languages' => array('en', 'fr'))
    );
  }
}

Như bạn có thể thấy, class component tương tự như một class action.

Template cho một component có cách đặt tên tương tự partial: một kí tự gạch dưới (_) sau đó là tên component:

// apps/frontend/modules/language/templates/_language.php
<form action="<?php echo url_for('@change_language') ?>">
  <?php echo $form ?><input type="submit" value="ok" />
</form>

Do plugin không cung cấp action thực hiện việc chuyển user culture, thêm vào file routing.yml route change_language:

# apps/frontend/config/routing.yml
change_language:
  url:   /change_language
  param: { module: language, action: changeLanguage }

Và tạo action tương ứng:

// apps/frontend/modules/language/actions/actions.class.php
class languageActions extends sfActions
{
  public function executeChangeLanguage(sfWebRequest $request)
  {
    $form = new sfFormLanguage(
      $this->getUser(),
      array('languages' => array('en', 'fr'))
    );
 
    $form->process($request);
 
    return $this->redirect('@localized_homepage');
  }
}

Phương thức process() của sfFormLanguage thực hiện việc đổi culture cho user, dựa vào form submission của user.

Internationalized Footer

Internationalization

Languages, Charset, và Encoding

Những ngôn ngữ khác nhau có tập các kí tự khác nhau. Tiếng Anh là ngôn ngữ đơn giản nhất do chỉ sử dụng các kí tự ASCII, tiếng Pháp phức tạp hơn một chút với các kí tự nhấn trọng âm như "é", những ngôn ngữ như Russian, Chinese, hay Arabic thì rất phức tạp do các kí tự nằm ngoài dải kí tự ASCII.

Khi làm việc với dữ liệu international, tốt hơn là sử dụng tiêu chuẩn unicode. Tư tưởng của unicode là thiết lập một tập chung chứa các kí tự của tất cả các ngôn ngữ. Vấn đề với unicode là một kí tự đơn cần 21 bits để mô tả. Do đó, với web, chúng ta sử dụng UTF-8, sẽ map Unicode code points với variable-length sequences của octets. Trong UTF-8, hầu hết các ngôn ngữ có mã kí tự với độ dài ít hơn 3 bits.

UTF-8 là encode mặc định trong symfony, được xác định trong file cấu hình settings.yml:

# apps/frontend/config/settings.yml
all:
  .settings:
    charset: utf-8

Để enable internationalization layer của symfony, bạn phải bật i18n setting trong settings.yml:

# apps/frontend/config/settings.yml
all:
  .settings:
    i18n: on

Templates

Một website đa ngôn ngữ có nghĩa là nội dung sẽ được dịch ra vài ngôn ngữ.

Trong một template, tất cả các câu phụ thuộc ngôn ngữ được chứa trong helper __() (chú ý rằng có 2 kí tự gạch dưới).

Helper __() là một phần của I18N helper group, chứa các helper để dễ dàng quản lý i18n trong template. Do helper group này mặc định không được load, bạn cần tự thêm nó vào template với use_helper('I18N') như chúng ta đã làm với Text helper group, hoặc có thể load nó ở global bằng cách thêm vào standard_helpers setting:

# apps/frontend/config/settings.yml
all:
  .settings:
    standard_helpers: [Partial, Cache, I18N]

Đây là cách sử dụng __() helper cho Jobeet footer:

// apps/frontend/templates/layout.php
<div id="footer">
  <div class="content">
    <span class="symfony">
      <img src="/legacy/images/jobeet-mini.png" />
      powered by <a href="/">
      <img src="/legacy/images/symfony.gif" alt="symfony framework" /></a>
    </span>
    <ul>
      <li>
        <a href=""><?php echo __('About Jobeet') ?></a>
      </li>
      <li class="feed">
        <?php echo link_to(__('Full feed'), '@job?sf_format=atom') ?>
      </li>
      <li>
        <a href=""><?php echo __('Jobeet API') ?></a>
      </li>
      <li class="last">
        <?php echo link_to(__('Become an affiliate'), '@affiliate_new') ?>
      </li>
    </ul>
    <?php include_component('language', 'language') ?>
  </div>
</div>

note

__() helper có thể dùng chuỗi là ngôn ngữ mặc định hoặc dùng một đại diện duy nhất cho mỗi chuỗi. Dùng cách nào là tùy mỗi người. Với Jobeet, chúng tôi sử dụng cách đầu tiên để template dễ đọc.

Khi symfony render một template, mỗi khi __() helper được gọi, symfony tìm bản dịch cho culture hiện tại của user. Nếu một bản dịch được tìm thấy, nó sẽ được sử dụng, nếu không, tham số đầu tiên của helper sẽ được dùng làm giá trị trả về.

Tất cả các bản dịch được chứa trong một catalogue. i18n framework cung cấp nhiều cách khác nhau để chứa bản dịch. Chúng tôi sử dụng format "XLIFF", đó là một chuẩn khá mềm dẻo. Nó cũng được sử dụng bởi admin generator và phần lớn các symfony plugin.

note

Các catalogue để chứa là gettext, MySQL, và SQLite. Bạn có thể xem chi tiết ở i18n API.

i18n:extract

Thay vì tạo file catalogue bằng tay, chúng ta sử dụng task có sẵn i18n:extract:

$ php symfony i18n:extract frontend fr --auto-save

Task i18n:extract tìm tất cả các chuỗi cần dịch sang fr trong application frontend và tạo/cập nhật vào catalogue tương ứng. Option --auto-save lưu chuỗi mới vào catalogue. Bạn cũng có thể dùng option --auto-delete để tự động xóa chuỗi không còn tồn tại.

Trong trường hợp của chúng ta, file được tạo ra như sau:

<!-- apps/frontend/i18n/fr/messages.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xliff PUBLIC "-//XLIFF//DTD XLIFF//EN"
  "http://www.oasis-open.org/committees/xliff/documents/xliff.dtd">
<xliff version="1.0">
  <file source-language="EN" target-language="fr" datatype="plaintext"
      original="messages" date="2008-12-14T12:11:22Z"
      product-name="messages">
    <header/>
    <body>
      <trans-unit id="1">
        <source>About Jobeet</source>
        <target/>
      </trans-unit>
      <trans-unit id="2">
        <source>Feed</source>
        <target/>
      </trans-unit>
      <trans-unit id="3">
        <source>Jobeet API</source>
        <target/>
      </trans-unit>
      <trans-unit id="4">
        <source>Become an affiliate</source>
        <target/>
      </trans-unit>
    </body>
  </file>
</xliff>

Mỗi phần dịch được quản lý bởi một trans-unit tag với một id duy nhất. Bây giờ bạn có thể sửa file này và thêm nội dung dịch ra tiếng Pháp:

<!-- apps/frontend/i18n/fr/messages.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xliff PUBLIC "-//XLIFF//DTD XLIFF//EN"
  "http://www.oasis-open.org/committees/xliff/documents/xliff.dtd">
<xliff version="1.0">
  <file source-language="EN" target-language="fr" datatype="plaintext"
      original="messages" date="2008-12-14T12:11:22Z"
      product-name="messages">
    <header/>
    <body>
      <trans-unit id="1">
        <source>About Jobeet</source>
        <target>A propos de Jobeet</target>
      </trans-unit>
      <trans-unit id="2">
        <source>Feed</source>
        <target>Fil RSS</target>
      </trans-unit>
      <trans-unit id="3">
        <source>Jobeet API</source>
        <target>API Jobeet</target>
      </trans-unit>
      <trans-unit id="4">
        <source>Become an affiliate</source>
        <target>Devenir un affilié</target>
      </trans-unit>
    </body>
  </file>
</xliff>

tip

Do XLIFF là một format chuẩn, nên có rất nhiều công cụ có sẵn giúp việc dịch được dễ dàng. Open Language Tools là một dự án Java Open-Source cung cấp một XLIFF editor.

tip

Do XLIFF làm một file-based format, nên nó cũng cùng quy luật như tất cả các file cấu hình khác của symfony. File I18n có thể chứa trong một project, một application, hay một module.

Translations với Arguments

Nguyên tắc cơ bản của internationalization là dịch toàn bộ câu. Nhưng một vài câu chứa giá trị động. Trong Jobeet, đó là ở trang chủ với link "more...":

// apps/frontend/modules/job/templates/indexSuccess.php
<div class="more_jobs">
  and <?php echo link_to($count, 'category', $category) ?> more...
</div>

Số công việc là một biến được thay thế bởi một placeholder trong bản dịch:

// apps/frontend/modules/job/templates/indexSuccess.php
<div class="more_jobs">
  <?php echo __('and %count% more...', array('%count%' => link_to($count, 'category', $category))) ?>
</div>

Chuỗi cần dịch bây giờ là "and %count% more...", và placeholder %count% sẽ được thay bởi số cụ thể khi chạy, nhờ có giá trị được cung cấp trong tham số thứ 2 của helper __().

Thêm một trans-unit tag mới vào file messages.xml, hoặc sử dụng task i18n:extract để tự động cập nhật file:

$ php symfony i18n:extract frontend fr --auto-save

Sau khi chạy lệnh, mở file XLIFF và thêm phần dịch ra tiếng Pháp:

<trans-unit id="5">
  <source>and %count% more...</source>
  <target>et %count% autres...</target>
</trans-unit>

Khi dịch, placeholder %count% cần được giữ nguyên và đặt ở nơi thích hợp.

Một số chuỗi phức tạp hơn do quy tắc về số nhiều của ngôn ngữ. Tùy thuộc vào giá trị của số mà câu thay đổi theo, nhưng mỗi ngôn ngữ lại có một nguyên tắc riêng. Một vài ngôn ngữ có quy tắc số nhiều phức tạp như Polish hay Russian.

Trong trang category, số công việc trong category hiện tại được hiển thị:

// apps/frontend/modules/category/templates/showSuccess.php
<strong><?php echo $pager->getNbResults() ?></strong> jobs in this category

Khi một câu có các nội dung dịch khác nhau tùy vào giá trị của số, helper format_number_choice() được sử dụng:

<?php echo format_number_choice(
    '[0]No job in this category|[1]One job in this category|(1,+Inf]%count% jobs in this category',
    array('%count%' => '<strong>'.$pager->getNbResults().'</strong>'),
    $pager->getNbResults()
  )
?>

format_number_choice() helper dùng 3 tham số:

  • Chuỗi sử dụng tùy vào giá trị của số
  • Mảng các placeholder cần thay thế
  • Số dùng để quyết định chuỗi nào được dùng

Chuỗi mô tả các bản dịch khác nhau tùy vào giá trị của số được format theo quy tắc:

  • Các chuỗi được cách nhau bởi dấu gạch đứng (|)
  • Mỗi chuỗi bao gồm một range theo sau là bản dịch

range có thể mô tả bất kì dải số nào:

  • [1,2]: chấp nhận các giá trị từ 1 đến 2
  • (1,2): chấp nhận các giá trị giữa 1 đến 2
  • {1,2,3,4}: chỉ chấp nhận các giá trị đã được liệt kê
  • [-Inf,0): chấp nhận các giá trị từ âm vô cùng đến < 0
  • {n: n % 10 > 1 && n % 10 < 5}: chấp nhận các số như 2, 3, 4, 22, 23, 24

Dịch chuỗi tương tự như các chuỗi khác:

<trans-unit id="6">
  <source>[0]No job in this category|[1]One job in this category|(1,+Inf]%count% jobs in this category</source>
  <target>[0]Aucune annonce dans cette catégorie|[1]Une annonce dans cette catégorie|(1,+Inf]%count% annonces dans cette catégorie</target>
</trans-unit>

Bây giờ bạn đã hiểu cách internationalize tất cả các loại chuỗi, hãy thêm __() vào tất cả các template của frontend application. Chúng ta sẽ không internationalize backend application.

Forms

Form classes chứa nhiều chuỗi cần được dịch, như labels, error messages, và help messages. Tất cả những chuỗi này được symfony tự động internationalized, bạn chỉ cần thêm bản dịch vào file XLIFF.

note

Không may thay, task i18n:extract không phân tích các form class để xác định các chuỗi cần dịch, bạn phải tự thêm bằng tay.

Propel Objects

Với Jobeet website, chúng ta sẽ không internationalize tất cả các bảng do không thể yêu cầu nhà tuyển dụng đưa lên bảng dịch cho tuyển dụng của họ sang ngôn ngữ khác. Ta chỉ dịch cho bảng category.

Propel plugin hỗ trợ bảng i18n. Với mỗi bảng chứa dữ liệu local, 2 bảng cần được tạo: một chứa các cột không phụ thuộc i18n, và một cho các cột cần được internationalized. 2 bảng này được liên kết theo quan hệ một-nhiều.

Cập nhật lại schema.yml:

# config/schema.yml
jobeet_category:
  _attributes:  { isI18N: true, i18nTable: jobeet_category_i18n }
  id:           ~
 
jobeet_category_i18n:
  id:           { type: integer, required: true, primaryKey: true, foreignTable: jobeet_category, foreignReference: id }
  culture:      { isCulture: true, type: varchar, size: 7, required: true, primaryKey: true }
  name:         { type: varchar(255), required: true }
  slug:         { type: varchar(255), required: true }

The _attributes entry defines options for the table.

And update the fixtures for categories:

# data/fixtures/010_categories.yml
JobeetCategory:
  design:        { }
  programming:   { }
  manager:       { }
  administrator: { }
 
JobeetCategoryI18n:
  design_en:        { id: design, culture: en, name: Design }
  programming_en:   { id: programming, culture: en, name: Programming }
  manager_en:       { id: manager, culture: en, name: Manager }
  administrator_en: { id: administrator, culture: en, name: Administrator }
 
  design_fr:        { id: design, culture: fr, name: Design }
  programming_fr:   { id: programming, culture: fr, name: Programmation }
  manager_fr:       { id: manager, culture: fr, name: Manager }
  administrator_fr: { id: administrator, culture: fr, name: Administrateur }

Rebuild the model to create the i18n stub classes:

$ php symfony propel:build-all --no-confirmation
$ php symfony cc

As the name and slug columns have been moved to the i18n table, move the setName() method from JobeetCategory to JobeetCategoryI18n:

// lib/model/JobeetCategoryI18n.php
public function setName($name)
{
  parent::setName($name);
 
  $this->setSlug(Jobeet::slugify($name));
}

We also need to fix the getForSlug() method in JobeetCategoryPeer:

// lib/model/JobeetCategoryPeer.php
static public function getForSlug($slug)
{
  $criteria = new Criteria();
  $criteria->addJoin(JobeetCategoryI18nPeer::ID, self::ID);
  $criteria->add(JobeetCategoryI18nPeer::CULTURE, 'en');
  $criteria->add(JobeetCategoryI18nPeer::SLUG, $slug);
 
  return self::doSelectOne($criteria);
}

tip

Do propel:build-all sẽ xóa tất cả các bảng và dữ liệu trong database, nên đừng quên tạo lại tài khoản để truy cập Jobeet backend bằng task guard:create-user. Bạn cũng có thể thêm một fixture file để việc này được tự động thực hiện.

When building the model, symfony creates proxy methods in the main JobeetCategory object to conveniently access the i18n columns defined in JobeetCategoryI18n:

$category = new JobeetCategory();
 
$category->setName('foo');       // sets the name for the current culture
$category->setName('foo', 'fr'); // sets the name for French
 
echo $category->getName();     // gets the name for the current culture
echo $category->getName('fr'); // gets the name for French

tip

To reduce the number of database requests, use the doSelectWithI18n() method instead of the regular doSelect() one. It will retrieve the main object and the i18n one in one request.

$categories = JobeetCategoryPeer::doSelectWithI18n($c, $culture);

Do route category phụ thuộc vào JobeetCategory model class và slug bây giờ là một phần của JobeetCategoryI18n, route sẽ không thể nhận Category object tự động. Để giúp routing system, hãy tạo một phương thức để lấy object:

// lib/model/JobeetCategoryPeer.php
class JobeetCategoryPeer extends BaseJobeetCategoryPeer
{
  static public function doSelectForSlug($parameters)
  {
    $criteria = new Criteria();
    $criteria->addJoin(JobeetCategoryI18nPeer::ID, JobeetCategoryPeer::ID);
    $criteria->add(JobeetCategoryI18nPeer::CULTURE, $parameters['sf_culture']);
    $criteria->add(JobeetCategoryI18nPeer::SLUG, $parameters['slug']);
 
    return self::doSelectOne($criteria);
  }
 
// ...

}

Sau đó, sử dụng option method để route category sử dụng method doSelectForSlug() để nhận object:

# apps/frontend/config/routing.yml
category:
  url:     /:sf_culture/category/:slug.:sf_format
  class:   sfPropelRoute
  param:   { module: category, action: show, sf_format: html }
  options: { model: JobeetCategory, type: object, method: doSelectForSlug }
  requirements:
    sf_format: (?:html|atom)

Chúng ta cần nạp lại fixtures để tạo lại các slug cho categories:

$ php symfony propel:data-load

Bây giờ, category route đã được international và URL cho mỗi category chứa category slug đã được dịch:

/frontend_dev.php/fr/category/programmation
/frontend_dev.php/en/category/programming

Admin Generator

Do một bug trong symfony 1.2.1, bạn cần comment title trong mục edit:

# apps/backend/modules/category/config/generator.yml
edit:
  #title: Editing Category "%%name%%" (#%%id%%)

Với backend, chúng ta muốn bản dịch French và English được sửa trong cùng một form:

Backend categories

Nhúng form i18n sử dụng method embedI18N():

// lib/form/JobeetCategoryForm.class.php
class JobeetCategoryForm extends BaseJobeetCategoryForm
{
  public function configure()
  {
    unset($this['jobeet_category_affiliate_list']);
 
    $this->embedI18n(array('en', 'fr'));
    $this->widgetSchema->setLabel('en', 'English');
    $this->widgetSchema->setLabel('fr', 'French');
  }
}

Giao diện admin generator hỗ trợ internationalization. Nó chứa sẵn bản dịch của hơn 20 ngôn ngữ, và dễ dàng thêm một bản dịch mới, hoặc sửa bản dịch có sẵn. Sửa file cho ngôn ngữ bạn muốn thay đổi (admin translations có thể tìm trong lib/vendor/symfony/lib/plugins/sfPropelPlugin/i18n/) ở application i18n. Do những file trong ứng dụng của bạn cũng được sử dụng, nên chỉ cần quan tâm đến các chuỗi chưa có trong file ứng dụng.

Bạn có thể để ý thấy rằng file dịch của admin generator có tên là sf_admin.fr.xml, thay vì fr/messages.xml. Thực tế là, messages là tên của catalogue, và được thay đổi để có thể dùng ở tất cả các phần của ứng dụng. Sử dụng một catalogue thay vì mặc định yêu cầu bạn phải chỉ rõ nơi dịch với helper __():

<?php echo __('About Jobeet', array(), 'jobeet') ?>

Trong lời gọi __() trên, symfony sẽ tìm chuỗi "About Jobeet" trong catalogue jobeet.

Tests

Fixing tests is an integral part of the internationalization migration. First, update the test fixtures for categories by copying the fixtures we have defined above in test/fixtures/010_categories.yml.

Rebuild the model for the test environment:

$ php symfony propel:build-all-load --no-confirmation --env=test

You can now launch all tests to check that they are running fine:

$ php symfony test:all

note

When we have developed the backend interface for Jobeet, we have not written functional tests. But whenever you create a module with the symfony command line, symfony also generate test stubs. These stubs are safe to remove.

Localization

Templates

Hỗ trợ các culture khác nhau cũng có nghĩa là hỗ trợ các cách khác nhau để format date và number. Trong một template, có vài helper giúp bạn thực hiện điều này dựa trên culture hiện tại của user:

Trong Date helper group:

Helper Mô tả
format_date() Formats a date
format_datetime() Formats a date

Trong Number helper group:

Helper Mô tả
format_number() Formats a number
format_currency() Formats a currency

Trong I18N helper group:

Helper Mô tả
format_country() Displays the name of a country
format_language() Displays the name of a language

Forms

Form framework cung cấp một vài widgets and validators cho dữ liệu local:

Hẹn gặp lại ngày mai

Internationalization và localization là tính năng được hỗ trợ sẵn trong symfony. Xây dựng một website đa ngôn ngữ thật đơn giản do symfony cung cấp tất cả các công cụ cơ bản và các task từ dòng lệnh để thực hiện điều đó nhanh chóng.

Để chuẩn bị cho hướng dẫn đặc biệt ngày mai, chúng ta sẽ chuyển rất nhiều files và khám phá sự khác nhau trong cách tổ chức một symfony project.