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()
và
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_jobs
và homepage
. Để đơ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: sfDoctrineRouteCollection options: model: JobeetAffiliate actions: [new, create] object_actions: { wait: get } prefix_path: /:sf_culture/affiliate category: url: /:sf_culture/category/:slug.:sf_format class: sfDoctrineRoute 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: sfDoctrineRouteCollection 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: sfDoctrineRoute options: model: JobeetJob type: object method_for_query: retrieveActiveJob 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 = Doctrine::getTable('JobeetCategory')->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_culture
là en
, 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.
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.
Doctrine 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.
Doctrine 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/doctrine/schema.yml JobeetCategory: actAs: Timestampable: ~ I18n: fields: [name] actAs: Sluggable: { fields: [name], uniqueBy: [lang, name] } columns: name: { type: string(255), notnull: true }
Bằng cách bật I18n
behavior, model JobeetCategoryTranslation
được tự động tạo và chứa các fields
cần dịch đã được chỉ định.
Chú ý rằng chúng ta đơn giản là bật I18n
behavior và chuyển Sluggable
behavior
gắn vào model JobeetCategoryTranslation
sẽ được tạo tự động. Option uniqueBy
cho Sluggable
behavior biết các field nào quyết định slug là duy nhất hay không. Ở đây, slug là duy nhất với mỗi cặp lang
và name
.
Cập nhật fixtures cho categories:
# data/fixtures/categories.yml JobeetCategory: design: Translation: en: name: Design fr: name: design programming: Translation: en: name: Programming fr: name: Programmation manager: Translation: en: name: Manager fr: name: Manager administrator: Translation: en: name: Administrator fr: name: Administrateur
Chúng ta cũng cần override phương thức findOneBySlug()
trong JobeetCategoryTable
.
Do Doctrine cung cấp một số magic finder cho tất cả các cột trong một model, nên chúng ta cần tạo phương thức
findOneBySlug()
để override magic functionality mặc định mà Doctrine cung cấp.
Chúng ta cần thực hiện một số thay đổi để lấy category dựa trên
english slug trong bảng JobeetCategoryTranslation
table.
// lib/model/doctrine/JobeetCategoryTable.cass.php public function findOneBySlug($slug) { $q = $this->createQuery('a') ->leftJoin('a.Translation t') ->andWhere('t.lang = ?', 'en') ->andWhere('t.slug = ?', $slug); return $q->fetchOne(); }
Build lại model:
$ php symfony doctrine:build-all --no-confirmation $ php symfony cc
tip
Do doctrine:build-all-reload
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.
Khi sử dụng I18n
behavior, proxies được tạo giữa JobeetCategory
object và JobeetCategoryTranslation
object vì thế tất cả các function cũ dùng để lấy tên
category sẽ vẫn làm việc và nhận giá trị với culture hiện tại.
$category = new JobeetCategory(); $category->setName('foo'); // sets the name for the current culture $category->getName(); // gets the name for the current culture $this->setCulture('fr'); // from your actions class $category->setName('foo'); // sets the name for French echo $category->getName(); // gets the name for French
tip
Để giảm số request tới database, join bảng JobeetCategoryTranslation
trong câu truy vấn của bạn. Nó sẽ lấy object chính và một i18n trong một câu truy vấn.
$categories = Doctrine_Query::create() ->from('JobeetCategory c') ->leftJoin('c.Translation t WITH t.lang = ?', $culture) ->execute();
Từ khóa WITH
ở trên sẽ thêm điều kiện vào điều kiện
ON
của câu truy vấn. Vì thế, điều kiện ON
trong join sẽ trở thành:
LEFT JOIN c.Translation t ON c.id = t.id AND t.lang = ?
Do route category
phụ thuộc vào JobeetCategory
model class và
slug
bây giờ là một phần của JobeetCategoryTranslation
, 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:
Chúng ta đã override findOneBySlug()
, hãy refactor một chút để những phương thức này có thể được chia sẻ.
Chúng ta sẽ tạo phương thức mới findOneBySlugAndCulture()
và doSelectForSlug()
và sửa lại phương thức findOneBySlug()
bằng cách sử dụng findOneBySlugAndCulture()
.
// lib/model/doctrine/JobeetCategoryTable.class.php public function doSelectForSlug($parameters) { return $this->findOneBySlugAndCulture($parameters['slug'], $parameters['sf_culture']); } public function findOneBySlugAndCulture($slug, $culture = 'en') { $q = $this->createQuery('a') ->leftJoin('a.Translation t') ->andWhere('t.lang = ?', $culture) ->andWhere('t.slug = ?', $slug); return $q->fetchOne(); } public function findOneBySlug($slug) { return $this->findOneBySlugAndCulture($slug, 'en'); } // ...
}
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: sfDoctrineRoute 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 doctrine: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:
Nhúng form i18n sử dụng method embedI18N()
:
// lib/form/JobeetCategoryForm.class.php class JobeetCategoryForm extends BaseJobeetCategoryForm { public function configure() { unset( $this['jobeet_affiliates_list'], $this['created_at'], $this['updated_at'] ); $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/sfDoctrinePlugin/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
define above in test/fixtures/categories.yml
.
Rebuild the model for the test
environment:
$ php symfony doctrine:build-all-reload --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:
sfWidgetFormI18nDate
sfWidgetFormI18nDateTime
sfWidgetFormI18nSelectCurrency
sfValidatorI18nChoiceCountry
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.
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.