![]() |
|
Code snippets for symfony 1.x |
|
In an i18n application, if your template uses the input_date_tag() helper, the format of the date sent to the submit action will depend on the user culture. But then, how can you handle this date to, say, store it in a database in a culture independent format?
The answer lies in the sfI18N class:
$date= sfContext::getInstance()->getRequest()->getParameter('birth_date'); $user_culture = sfContext::getInstance()->getUser()->getCulture(); list($d, $m, $y) = sfI18N::getDateForCulture( $date, $user_culture );
Now you have the day, month and year of the date in the $d, $m and $y variables, and you can do whatever you want with them.
If you use a Propel date setter, you can even call it directly with:
$person->setBirthDate("$y-$m-$d");
The same applies for the sfI18N::getTimestampForCulture() method.
By default, I18n content in database does not support fallback in default culture. This snippet allow you to enable I18n content fallback in order to always have a default value for your texts.
This snippet is sponsored by Dorigo consultants.
To enable the fallback, edit your table object class in lib/model/TableClassName.php and add the following code. Then search and replace TableClassName by your table object class name.
/** * Fetch the i18n object for this object culture. * * @return mixed A i18n object * @throws PropelException Any exceptions caught during processing will be * rethrown wrapped into a PropelException. */ public function getCurrentTableClassNameI18n() { if (!isset($this->current_i18n[$this->culture])) { $obj = TableClassNameI18nPeer::retrieveByPK($this->getId(), $this->culture); if ($obj) // Test if there is a translation for current culture { $this->setTableClassNameI18nForCulture($obj, $this->culture); } else // Create a translation for this culture { $new_i18n = new TableClassNameI18n(); $default_culture = sfConfig::get('sf_i18n_default_culture'); // We try to fetch the default culture translation to initialise the new culture. if (!isset($this->current_i18n[$default_culture])) { $obj = TableClassNameI18nPeer::retrieveByPK($this->getId(), $default_culture); if ($obj) // Test if there is a translation for current culture { $this->setTableClassNameI18nForCulture($obj, $default_culture); } } else { $obj = $this->current_i18n[$default_culture]; } if ($obj) { $obj->copyInto($new_i18n); } $new_i18n->setId($this->getId()); $new_i18n->setCulture($this->culture); $this->setTableClassNameI18nForCulture($new_i18n, $this->culture); } } return $this->current_i18n[$this->culture]; }
You now need to add default translation when you create a new object. We do that with this doSave function.
To use this function, edit your table object class in lib/model/TableClassName.php and add the following code. Then search and replace TableClassName by your table object class name.
/** * Stores the object in the database while setting default culture if necessary. * * If the object is new, it inserts it; otherwise an update is performed. * All related objects are also updated in this method. * * @param Connection $con The database connection * @return int The number of rows affected by this insert/update and any referring fk objects' save() operations. * @throws PropelException Any exceptions caught during processing will be * rethrown wrapped into a PropelException. * @see save() */ protected function doSave($con) { $default_culture = sfConfig::get('sf_i18n_default_culture'); // We try to fetch the default culture translation to initialise the new culture. if (!isset($this->current_i18n[$default_culture])) { $obj = TableClassNameI18nPeer::retrieveByPK($this->getId(), $default_culture, $con); if ($obj) // Test if there is a translation for current culture { $this->setTableClassNameI18nForCulture($obj, $default_culture); } } else { $obj = $this->current_i18n[$default_culture]; } if(!$obj && isset($this->current_i18n[$this->culture])) { $new_i18n = new TableClassNameI18n(); $this->current_i18n[$this->culture]->copyInto($new_i18n); $new_i18n->setId($this->getId()); $new_i18n->setCulture($default_culture); $this->setTableClassNameI18nForCulture($new_i18n, $default_culture); } return parent::doSave($con); }
To complete this snippet, here is a fallback version of doSelectWithI18n.
To enable the fallback, edit your table object peer class in lib/model/TableClassNamePeer.php and add the following code. Then search and replace TableClassName by your table object class name.
/** * Selects a collection of TableClassName objects pre-filled with their i18n objects. * * @param Criteria $criteria * @param string $culture The selected culture. * @param Connection $con An optional database connection * @return array Array of TableClassName objects. * @throws PropelException Any exceptions caught during processing will be * rethrown wrapped into a PropelException. */ public static function doSelectWithI18n(Criteria $c, $culture = null, $con = null) { if ($culture === null) { $culture = sfContext::getInstance()->getUser()->getCulture(); } $default_culture = sfConfig::get('sf_i18n_default_culture'); // Set the correct dbName if it has not been overridden if ($c->getDbName() == Propel::getDefaultDB()) { $c->setDbName(self::DATABASE_NAME); } TableClassNamePeer::addSelectColumns($c); $startcol = (TableClassNamePeer::NUM_COLUMNS - TableClassNamePeer::NUM_LAZY_LOAD_COLUMNS) + 1; TableClassNameI18nPeer::addSelectColumns($c); $c->addJoin(TableClassNamePeer::ID, TableClassNameI18nPeer::ID); $criterion = $c->getNewCriterion(TableClassNameI18nPeer::CULTURE, $culture); $criterion->addOr($c->getNewCriterion(TableClassNameI18nPeer::CULTURE, $default_culture)); $c->add($criterion); $rs = BasePeer::doSelect($c, $con); $results = array(); $uncultured_results = array(); while($rs->next()) { $omClass = TableClassNamePeer::getOMClass(); $cls = Propel::import($omClass); $obj1 = new $cls(); $obj1->hydrate($rs); $obj1->setCulture($culture); if(isset($results[$obj1->getId()])) { $obj1 = $results[$obj1->getId()]; } $omClass = TableClassNameI18nPeer::getOMClass($rs, $startcol); $cls = Propel::import($omClass); $obj2 = new $cls(); $obj2->hydrate($rs, $startcol); $obj1->setTableClassNameI18nForCulture($obj2, $obj2->getCulture()); $obj2->setTableClassName($obj1); if(!isset($uncultured_results[$obj1->getId()])) { $uncultured_results[$obj1->getId()] = $obj1; } if($obj2->getCulture() == $culture) { $uncultured_results[$obj1->getId()] = false; } if(!isset($results[$obj1->getId()])) { $results[$obj1->getId()] = $obj1; } elseif($obj2->getCulture() == $culture) { // Move result to the end of results array to fit eventual sort // criteria (ugly fix). unset($results[$obj1->getId()]); $results[$obj1->getId()] = $obj1; } } foreach($uncultured_results as $obj1) { if($obj1) { $obj1->setCulture($default_culture); $default_culture_object = $obj1->getCurrentTableClassNameI18n(); if($default_culture_object) { $obj2 = new TableClassNameI18n(); $default_culture_object->copyInto($obj2); $obj2->setCulture($culture); $obj2->setTableClassName($obj1); $obj1->setTableClassNameI18nForCulture($obj2, $obj2->getCulture()); } $obj1->setCulture($culture); } } return array_values($results); }
If you want to use a pager with a filter on translations, you will need this count method. Once this method present, the magic is done by :
$criteria->setDistinct(); $pager->setCriteria($criteria); $pager->setPeerMethod('doSelectWithI18n'); $pager->setPeerCountMethod('doCountWithI18n');
Note : The setDistinct is very important for this snippet to work. It should not falsify your results, and without it, the doCountWithI18n method could return bad results.
To enable the fallback, edit your table object peer class in lib/model/TableClassNamePeer.php and add the following code. Then search and replace TableClassName by your table object class name.
/** * Returns the number of rows matching criteria with I18N criteria. * * @param Criteria $criteria * @param boolean $distinct Whether to select only distinct columns (You can also set DISTINCT modifier in Criteria). * @param Connection $con An optional database connection * @param string $culture The selected culture. * @return int Number of matching rows. */ public static function doCountWithI18n(Criteria $criteria, $distinct = false, $con = null, $culture = null) { // we're going to modify criteria, so copy it first $criteria = clone $criteria; $default_culture = sfConfig::get('sf_i18n_default_culture'); if ($culture === null) { // We use current user culture. $culture = sfContext::getInstance()->getUser()->getCulture(); } // clear out anything that might confuse the ORDER BY clause $criteria->clearSelectColumns()->clearOrderByColumns(); $criteria->addSelectColumn(TableClassNamePeer::COUNT_DISTINCT); // just in case we're grouping: add those columns to the select statement foreach($criteria->getGroupByColumns() as $column) { $criteria->addSelectColumn($column); } $criteria->addJoin(TableClassNamePeer::ID, TableClassNameI18nPeer::ID); $criterion = $criteria->getNewCriterion(TableClassNameI18nPeer::CULTURE, $culture); $criterion->addOr($criteria->getNewCriterion(TableClassNameI18nPeer::CULTURE, $default_culture)); $criteria->add($criterion); $rs = TableClassNamePeer::doSelectRS($criteria, $con); if ($rs->next()) { return $rs->getInt(1); } else { // no rows returned; we infer that means 0 matches. return 0; } }
This filter use the accepted-languages browser setting to setup the user culture. The autodetection is run once by session.
This filter is sponsored by Dorigo consultants.
It check app.yml for accepted_languages setting in order to fit the available application locales.
It allow the use of sf_culture request parameter overide (this overide is avaible by default in symfony afaik). So you can use language changing links in your application.
It may be enhanced by a check of the really available locales but i don't know how it can be done.
To use this filter, create a file named SwitchLanguageFilter.class.php in lib/ or app/frontend/lib and fill it with this code and follow its documentation :
<?php /** * Setup user culture from request * * from http://www.symfony-project.com/snippets/snippet/80 * Thanks to François Zaninotto * Thanks to Garfield-fr on #symfony-fr * * Add the following lines in your app.yml to configure your application available languages. * all: * accepted: * languages: [en, fr] * * Then add this to your filters.yml * * # Filter that setup user culture * mySwitchLanguageFilter: * class: SwitchLanguageFilter * * @package SwitchLanguageFilter * @subpackage filter * @author Pierre-Yves Landuré <py.landure@dorigo.fr> */ class SwitchLanguageFilter extends sfFilter { /** * Check that the language is a valid application culture. * @param string $language The tested language code * @param string $default_language The default language code * * @return string $language if no accepted languages is set, * else $language if it is in accepted languages * else $default_language */ private function getAvailableCulture($language, $default_language = null) { $all_languages = sfConfig::get('app_accepted_languages', array()); if(count($all_languages)) { if(in_array($language, $all_languages)) // Test if language is available { return $language; } else // Else test if first part of language is available { $language_parts = explode('_', $language); if(count($language_parts)) { if(in_array($language_parts[0], $all_languages)) { return $language_parts[0]; } } } return $default_language; } return $language; } /** * The filter call. */ public function execute ($filterChain) { $context = $this->getContext(); $user = $context->getUser(); $default_culture = sfConfig::get('sf_i18n_default_culture'); $selected_culture = $user->getCulture(); if(!$user->getAttribute('sf_culture_autodetected', false)) { $browser_languages = $context->getRequest()->getLanguages(); foreach($browser_languages as $language) { $allowed_culture = $this->getAvailableCulture($language); if($allowed_culture) { $selected_culture = $allowed_culture; break; } } $user->setAttribute('sf_culture_autodetected', true); } $selected_culture = $context->getRequest()->getParameter('sf_culture', $selected_culture); $selected_culture = $this->getAvailableCulture($selected_culture, $default_culture); if($selected_culture != $user->getCulture()) { // The user wants to see the page in another language $user->setCulture($selected_culture); } $filterChain->execute(); } }
Symfony internationalization is normally managed using session variables. To switch culture you can visit a page that first changes you session variable and then send you back to the page you were visiting, but this potentially breaks your SEO.
Adding culture in every URL is a better solution, but if you want to go back to the page you were visiting, you have to deal with redirection by yourself.
This widget solves the redirection issue by creating localized urls.
Save your file here: apps/appname/lib/helper/I18nUrlHelper.php
/* * Generate a localized version for the URL you are visiting. */ function localized_current_url($sf_culture = null) { if (! $sf_culture) { throw new sfException(sprintf('Invalid parameter $sf_culture "%s".', $sf_culture)); } $routing = sfContext::getInstance()->getRouting(); $request = sfContext::getInstance()->getRequest(); $controller = sfContext::getInstance()->getController(); // depending on your routing configuration, you can set $route_name = $routing->getCurrentRouteName() $route_name = ''; $parameters = $controller->convertUrlStringToParameters($routing->getCurrentInternalUri()); $parameters[1]['sf_culture'] = $sf_culture; return $routing->generate($route_name, array_merge($request->getGetParameters(), $parameters[1])); }
Obviously, you must add the :sf_culture token in every rule of your application routing.yml:
product_page: url: /:sf_culture/product/:id requirements: { sf_culture: (?:en|es|fr|it|de) } param: { module: product, action: show } homepage: url: /:sf_culture requirements: { sf_culture: (?:en|es|fr|it|de) } param: { module: page, action: home } default_symfony: url: /symfony/:sf_culture/:action/* requirements: { sf_culture: (?:en|es|fr|it|de) } param: { module: default } default_index: url: /:sf_culture/:module param: { action: index } requirements: { sf_culture: (?:en|es|fr|it|de) } default: url: /:sf_culture/:module/:action/* requirements: { sf_culture: (?:en|es|fr|it|de) }
This code let you create a list of link to the localized versions. The list of languages/cultures is taken from your config file.
<div> <?php foreach(sfConfig::get('app_languages_available') as $language) { ?> <a href="<?php echo localized_current_url($language); ?>"><?php echo $language; ?></a> <?php } ?> </div>
The table i18n doesn't support default culture. This snippet add the support of the default culture.
This snippet is a port of the snippet "default culture content fallback for i18n tables" for Symfony 1.2
To enable the fallback, edit your table object class in lib/model/TableClassName.php and add the following code. Then search and replace TableClassName by your table object class name.
/** * Fetch the i18n object for this object culture. * * @param string $culture The culture to set * @return mixed A i18n object * @throws PropelException Any exceptions caught during processing will be rethrown wrapped into a PropelException. * @link http://snippets.symfony-project.org/snippet/237 -- modified for Symfony 1.2 */ public function getCurrentTableClassNameI18n($culture = null) { if (is_null($culture)) { $culture = is_null($this->culture) ? sfPropel::getDefaultCulture() : $this->culture; } if (!isset($this->current_i18n[$culture])) { $obj = TableClassNameI18nPeer::retrieveByPK($this->getId(), $culture); if ($obj !== null) { // Test if there is a translation for current culture $this->setTableClassNameI18nForCulture($obj, $culture); } else { // Create a translation for this culture $new_i18n = new TableClassNameI18n(); $default_culture = sfConfig::get('sf_default_culture'); // We try to fetch the default culture translation to initialise the new culture. if (!isset($this->current_i18n[$default_culture])) { $obj = TableClassNameI18nPeer::retrieveByPK($this->getId(), $default_culture); if ($obj !== null) { // Test if there is a translation for current culture $this->setTableClassNameI18nForCulture($obj, $default_culture); } } else { $obj = $this->current_i18n[$default_culture]; } if ($obj !== null) { $obj->copyInto($new_i18n); } $new_i18n->setId($this->getId()); $new_i18n->setCulture($culture); $this->setTableClassNameI18nForCulture($new_i18n, $culture); } } return $this->current_i18n[$culture]; }
You now need to add default translation when you create a new object. We do that with this doSave function.
To use this function, edit your table object class in lib/model/TableClassName.php and add the following code. Then search and replace TableClassName by your table object class name.
/** * Stores the object in the database while setting default culture if necessary. * * If the object is new, it inserts it; otherwise an update is performed. * All related objects are also updated in this method. * * @param Connection $con The database connection * @return int The number of rows affected by this insert/update and any referring fk objects' save() operations. * @throws PropelException Any exceptions caught during processing will be rethrown wrapped into a PropelException. * @see save() * @link http://snippets.symfony-project.org/snippet/237 -- modified for Symfony 1.2 */ protected function doSave(PropelPDO $con) { $default_culture = sfConfig::get('sf_default_culture'); $current_culture = is_null($this->culture) ? sfPropel::getDefaultCulture() : $this->culture; $obj = null; // We try to fetch the default culture translation to initialise the new culture. if (!isset($this->current_i18n[$default_culture])) { $obj = TableClassNameI18nPeer::retrieveByPK($this->getId(), $default_culture, $con); if ($obj !== null) { // Test if there is a translation for current culture $this->setTableClassNameI18nForCulture($obj, $default_culture); } } else { $obj = $this->current_i18n[$default_culture]; } if($obj === null && isset($this->current_i18n[$current_culture])) { $new_i18n = new TableClassNameI18n(); $this->current_i18n[$current_culture]->copyInto($new_i18n); $new_i18n->setId($this->getId()); $new_i18n->setCulture($default_culture); $this->setTableClassNameI18nForCulture($new_i18n, $default_culture); } return parent::doSave($con); }
To complete this snippet, here is a fallback version of doSelectWithI18n.
To enable the fallback, edit your table object peer class in lib/model/TableClassNamePeer.php and add the following code. Then search and replace TableClassName by your table object class name.
/** * Selects a collection of TableClassName objects pre-filled with their i18n objects. * * @param Criteria $criteria The criteria's object * @param string $culture The selected culture. * @param PropelPDO $con An optional database connection * @return array Array of TableClassName objects. * @throws PropelException Any exceptions caught during processing will be rethrown wrapped into a PropelException. * @link http://snippets.symfony-project.org/snippet/237 -- modified for Symfony 1.2 */ public static function doSelectWithI18n(Criteria $criteria, $culture = null, PropelPDO $con = null) { $criteria = clone $criteria; if ($culture === null) { $culture = sfContext::getInstance()->getUser()->getCulture(); } $default_culture = sfConfig::get('sf_default_culture'); // Set the correct dbName if it has not been overridden if ($criteria->getDbName() == Propel::getDefaultDB()) { $criteria->setDbName(self::DATABASE_NAME); } TableClassNamePeer::addSelectColumns($c); $startcol = (TableClassNamePeer::NUM_COLUMNS - TableClassNamePeer::NUM_LAZY_LOAD_COLUMNS) + 1; TableClassNameI18nPeer::addSelectColumns($c); $criteria->addJoin(TableClassNamePeer::ID, TableClassNameI18nPeer::ID); $criterion = $criteria->getNewCriterion(TableClassNameI18nPeer::CULTURE, $culture); $criterion->addOr($criteria->getNewCriterion(TableClassNameI18nPeer::CULTURE, $default_culture)); $criteria->add($criterion); $stmt = BasePeer::doSelect($c, $con); $results = array(); $uncultured_results = array(); while($row = $stmt->fetch(PDO::FETCH_NUM)) { $obj1 = new TableClassName(); $obj1->hydrate($row); $obj1->setCulture($culture); if(isset($results[$obj1->getId()])) { $obj1 = $results[$obj1->getId()]; } $omClass = TableClassNameI18nPeer::getOMClass($row, $startcol); $cls = Propel::importClass($omClass); $obj2 = new $cls(); $obj2->hydrate($row, $startcol); $obj1->setTableClassNameI18nForCulture($obj2, $obj2->getCulture()); $obj2->setTableClassName($obj1); if(!isset($uncultured_results[$obj1->getId()])) { $uncultured_results[$obj1->getId()] = $obj1; } if($obj2->getCulture() == $culture) { $uncultured_results[$obj1->getId()] = false; } if(!isset($results[$obj1->getId()])) { $results[$obj1->getId()] = $obj1; } elseif($obj2->getCulture() == $culture) { // Move result to the end of results array to fit eventual sort // criteria (ugly fix). unset($results[$obj1->getId()]); $results[$obj1->getId()] = $obj1; } } foreach ($uncultured_results as $obj1) { if ($obj1) { $obj1->setCulture($default_culture); $default_culture_object = $obj1->getCurrentTableClassNameI18n(); if ($default_culture_object) { $obj2 = new TableClassNameI18n(); $default_culture_object->copyInto($obj2); $obj2->setCulture($culture); $obj2->setTableClassName($obj1); $obj1->setTableClassNameI18nForCulture($obj2, $obj2->getCulture()); } $obj1->setCulture($culture); } } return array_values($results); }
Before enable the fallback, you must create a constant in your table object peer class in lib/model/TableClassNamePeer.php and replace table by your table name
const COUNT_DISTINCT = 'COUNT(DISTINCT table.ID)';
To enable the fallback, edit your table object peer class in lib/model/TableClassNamePeer.php and add the following code. Then search and replace TableClassName by your table object class name.
/** * Returns the number of rows matching criteria with I18N criteria. * * @param Criteria $criteria The criteria's object * @param boolean $distinct Whether to select only distinct columns (You can also set DISTINCT modifier in Criteria). * @param Connection $con An optional database connection * @param string $culture The selected culture. * @return int Number of matching rows. * @link http://snippets.symfony-project.org/snippet/237 -- modified for Symfony 1.2 */ public static function doCountWithI18n(Criteria $criteria = null, $distinct = false, PropelPDO $con = null, $culture = null) { // we're going to modify criteria, so copy it first if ($criteria === null){ $criteria = new Criteria(); } else { $criteria = clone $criteria; } $default_culture = sfConfig::get('sf_default_culture'); if ($culture === null) { // We use current user culture. $culture = sfContext::getInstance()->getUser()->getCulture(); } // clear out anything that might confuse the ORDER BY clause $criteria->clearSelectColumns()->clearOrderByColumns(); $criteria->addSelectColumn(TableClassNamePeer::COUNT_DISTINCT); // just in case we're grouping: add those columns to the select statement foreach($criteria->getGroupByColumns() as $column) { $criteria->addSelectColumn($column); } $criteria->addJoin(TableClassNamePeer::ID, TableClassNameI18nPeer::ID); $criterion = $criteria->getNewCriterion(TableClassNameI18nPeer::CULTURE, $culture); $criterion->addOr($criteria->getNewCriterion(TableClassNameI18nPeer::CULTURE, $default_culture)); $criteria->add($criterion); $rs = TableClassNamePeer::doSelectStmt($criteria, $con); if ($res = $rs->fetchColumn(0)) { return $res; } else { // no rows returned; we infer that means 0 matches. return 0; } }
This filter tries to set the user culture by reading the domain part of the url. I use it for SEO, giving a different domain for each lang of my site.
Add this to your settings.yml :
domain_culture:
fr: [.net, .fr, fr.mydomain.info]
it: [it.mydomain.info]
en: [.co.uk, .com, .mydomain.info]
Order is important : first match will assign the corresponding culture.
Create a lib/filters/domainCultureFilter.class.php file :
<?php class domainCultureFilter extends sfFilter { public function execute($filterChain) { $user = $this->getContext()->getUser(); $request = $this->getContext()->getRequest(); if (!$request->getParameter('sf_culture')) { if ($user->getAttribute('first_request', true)) { $user->setAttribute('first_request', false); $domainCulture=sfConfig::get('sf_domain_culture'); foreach ((array)$domainCulture as $culture=>$domains) { foreach ((array)$domains as $domain) { $pattern = '/'.$domain.'/'; if (preg_match( $pattern , $_SERVER['HTTP_HOST']) ) { $user->setCulture($culture); } break 2; } } } } // Execute next filter $filterChain->execute(); } }
Add the filter to filters.yml :
# insert your own filters here culture: class: domainCultureFilter
Comments are very welcome.
The following view class tries to load the template under directory name of the current culture, then fall back to the default template if such template cannot be found.
So if you access to "index" action of module "foo" and if current user culture is "zh_HK", the class will try to load: "module/foo/templates/zh_HK/indexSuccess.php"
If no such file exist, it will fall back to the default behaviour by loading "module/foo/templates/indexSuccess.php"
class sfCulturePHPView extends sfPHPView { protected $cultureDirectory = ''; public function initialize($context, $moduleName, $actionName, $viewName) { parent::initialize($context, $moduleName, $actionName, $viewName); } public function getDirectory() { if ($this->cultureDirectory) return $this->directory.'/'.$this->cultureDirectory; return $this->directory; } protected function preRenderCheck() { parent::preRenderCheck(); // determine whether the culture-aware template is usable $currentCulture = $this->context->getUser()->getCulture(); $template = $this->directory.'/'.$currentCulture.'/'.$this->template; if (is_readable($template)) $this->cultureDirectory = $currentCulture; else $this->cultureDirectory = ''; } }