:-)
  • Мне потребовалось сделать мультиязычный сайт аж на 5 языках. В целом тут нет ничего сложного, кроме того, что пользователю надо будет заполнять 5 форм на разных языках. Логичный вывод - сделать обязательным только один язык, например английский. Остальные заполняются по желанию, а для незаполненных показывается на том же английском. Промучившись некоторое время с рецептами от Календаря Адвента, и не добившись успеха, я нашел пост [Optional translation form for I18n objects with Symfony and Doctrine], который мне помог. Я считаю, что полезным будет сделать его перевод. Так же я добавлю некоторые свои комментарии. (Перевод вольный, эстеты идут лесом)

    Некоторые люди утверждают, что Symfony - это подарок богов. Другие считают, что это преувеличение. Но как ни крути, в версиях 1.3/1.4 добавилось много нового и полезного, что может сэкономить вам вермя.
    Представим проект, где есть разные заметки (новости, интервью, и т.д.). И они могут быть на разных языках. И это довольно просто сделать с actAs: I18n. Но есть небольшая сложность: переводы должны быть опциональны, чтобы можно было написать статью только на французском, английском или немецком.

    Некоторые материалы по теме: раз, два, три, четыре, пять.

    Итак, начнем со схемы.

    Article:
      actAs:
        Timestampable: ~
        I18n:
          fields: [ title, body ]
          actAs:
            Sluggable: { fields: [ title ], uniqueBy: [ lang, title ] }
     
      columns:
        title: { type: string(255), notnull: true }
        body: { type: clob, notnull: true }
        author: { type: string(255), notnull: false }
     
    News:
      inheritance:
        extends: Article
        type: concrete

    А так же фикстуры:

    News:
      n1:
        author: 'Lenta.ru'
        Translation:
          ru:
            title: 'Нет вестей с Титана'
            body: |
              Титан – это шестой и самый крупный спутник Сатурна.
     
      n2:
        author: 'Bash.org'
        Translation:
          en:
            title: '#921792'
            body: |
              <Thomas> if women think they arent meant to cook
              <Thomas> why do they have milk and eggs inside them?

    Загружаем все это добро в базу данных:

    php symfony doctrine:build --all --and-load
    php symfony generate:app backend
    php symfony doctrine:generate-admin backend News

    Загляните в только что построенный модуль админки. Нажмите кнопку "редактировать" и оп.. а где все наши переводы? Если вы еще не в курсе, actAs:I18n разделяет таблицу на 2 части, в первой содержатся общие поля, не зависящие от перевода, а во второй те, которые требуют перевода.

    mysql> SELECT * FROM news;
    +----+-------------+---------------------+---------------------+
    | id | author      | created_at          | updated_at          |
    +----+-------------+---------------------+---------------------+
    |  1 | Lenta.ru    | 2010-01-29 12:14:46 | 2010-01-29 12:14:46 | 
    |  2 | Bash.org    | 2010-01-29 12:14:46 | 2010-01-29 12:14:46 | 
    +----+-------------+---------------------+---------------------+
     
    mysql> SELECT id, lang, title FROM news_translation;
    +----+------+-----------------------------------------------------+
    | id | lang | title                                               |
    +----+------+-----------------------------------------------------+
    |  1 | ru   | Титан – это шестой и самый крупный спутник Сатурна. | 
    |  2 | en   | <Thomas> if women think they arent meant to cook ...| 
    +----+------+-----------------------------------------------------+

    Чтобы нам было доступно редактирование переводов - надо воспользоваться функцией embedI18n.

    // lib/form/doctrine/NewsForm.class.php
    class NewsForm extends BaseNewsForm
    {
      /**
       * @see ArticleForm
       */
      public function configure()
      {
        parent::configure();
        $this->embedI18n(array('ru', 'en'));
      }
    }

    Вуа ля. Редактировать можно!

    Немного приберемся

    Наш код сейчас не так хорош, как мог бы быть:
    Каждый раз, когда мы будем добавлять новый тип статьи - нам надо будет менять метод configure;
    Каждый раз, когда мы будем добавлять/удалять новый язык для статей - нам надо будет менять все формы.

    К счастью, с Symfony 1.3, наследование форм повторяет наследование моделей. Смотрите, NewsForm наследует BaseNewsForm, которая в свою очередь наследует ArticleForm.

    # config/app.yml
    all:
      cultures:
        enabled:
          ru: Russian
          en: English
    // lib/form/doctrine/NewsForm.class.php
     
    // Revert the changes we added there
    class NewsForm extends BaseNewsForm
    {
      /**
       * @see ArticleForm
       */
      public function configure()
      {
        parent::configure();
      }
    }
     
    // lib/form/doctrine/ArticleForm.class.php
    class ArticleForm extends BaseArticleForm
    {
      /**
       * Available languages
       *
       * @var array $languages
       **/
      protected $langages;
     
      public function configure()
      {
        $this->languages = sfConfig::get('app_cultures_enabled');
     
        $langs = array_keys($this->languages);
     
        $this->embedI18n($langs);
        foreach($this->languages as $lang => $label)
        {
          $this->widgetSchema[$lang]->setLabel($label);
        }
      }
    }

    Перезагрузите теперь страницу. Вы можете теперь добавлять/удалять языки независимо от количества типов статей.

    Редактируем переводы

    Давайте теперь попробуем нашу форму для статей. Когда вы попробуете редактировать какую-нибудь новость, то... постыдная неудача. Нельзя сохранить, потому что нет английского или русского перевода. Давайте добавим условие, что если в форме есть пустые поля - мы эти формы не сохраняем.
    Для этого мы перекроем метод doBind.

    // lib/form/doctrine/ArticleForm.class.php
    class ArticleForm extends BaseArticleForm
    {
      /**
       * Available languages
       *
       * @var array $languages
       **/
      protected $langages;
     
      public function configure()
      {
        $this->languages = sfConfig::get('app_cultures_enabled');
     
        $langs = array_keys($this->languages);
     
        $this->embedI18n($langs);
        foreach($this->languages as $lang => $label)
        {
          $this->widgetSchema[$lang]->setLabel($label);
        }
      }
     
      /**
       * Cleans and binds values to the current form
       *
       * Ignore i18n forms when all their fields are empty
       *
       * @see sfForm::doBind
       **/
      protected function doBind(array $values)
      {
        foreach($this->languages as $lang => $label)
        {
          if($this->embeddedI18nFormIsEmpty($values[$lang]))
          {
            unset(
              $values[$lang],
              $this[$lang]
            );
          }
        }
     
        parent::doBind($values);
      }
     
      /**
       * Check if every fields, except for id and lang, are empty
       **/
      protected function embeddedI18nFormIsEmpty(array $values)
      {
        foreach($values as $key => $value)
        {
          if(in_array($key, array('id', 'lang')))
            continue;
     
          if('' !== trim($value))
          {
            return false;
          }
        }
        return true;
      }
    }

    В перекрытом методе doBind, мы проверяем каждую I18n форму, и, если надо делаем ей unset. Теперь все сохраняется как надо, однако же...

    Загляните в базу данных, вас там ждет сюрприз.

    mysql> SELECT id, lang, slug FROM news_translation;
    +----+------+------------------------------------------------------+
    | id | lang | slug                                                 |
    +----+------+------------------------------------------------------+
    |  1 | en   |                                                      | 
    |  1 | ru   | Титан – это шестой и самый крупный спутник Сатурна.  | 
    |  2 | en   | <Thomas> IF women think they arent meant TO cook ... | 
    +----+------+------------------------------------------------------+

    Где-то в процессе сохранения, Symfony создала пустой объект перевода. И вот решение для этой проблемы.

    // lib/form/doctrine/ArticleForm.class.php
     
      // Add this at the beginnig of the class:
      /**
       * I18n ignored forms
       **/
      protected $I18nFormsIgnored = array();
     
      // update the doBind method:
      /**
       * Unset i18n forms values when every field is empty
       **/
      protected function doBind(array $values)
      {
        foreach($this->languages as $lang => $label)
        {
          if($this->embeddedI18nFormIsEmpty($values[$lang]))
          {
            $this->I18nFormsIgnored[] = $lang;
            unset(
              $values[$lang],
              $this[$lang]
            );
          }
        }
     
        parent::doBind($values);
      }
     
      // And override the doUpdateObject method:
      /**
       * Updates the values of the object with the cleaned up values.
       *
       * @param  array $values An array of values
       *
       * @see sfFormDoctrine::doUpdateObject()
       */
      protected function doUpdateObject($values)
      {
        parent::doUpdateObject($values);
     
        foreach($this->I18nFormsIgnored as $lang)
        {
          unset($this->object->Translation[$lang]);
          unset($values[$lang]);
        }
      }

    На этот раз все сохраняется как надо!

    (Здесь я пропускаю абзац про убирание поля Slug. Там есть сложность с тем, что формы перевода не наследуются. Если вам интересно - посмотрите в оригинальной статье. Я считаю, что основное здесь - как раз перекрытие методов doBind и doUpdateObject.
    Тесты, приведенные автором можно посмотреть у него в статье, а можно тут и тут).

    На этом все. Надеюсь вам пригодится мой перевод.

    Tags: , , ,

  • 10 комментариев

    WP_Modern_Notepad
    • Иван пишет:

      Полезная статейка, пригодилось.

      только не protected function embeddedI18nFormIsEmpty(array $values), а protected function embeddedI18nFormIsEmpty($values)

      B «грамотическая ошибка» в имени метода $this->embeddedI18nFormEmpty. Должно быть $this->embeddedI18nFormIsEmpty

    • CharnaD пишет:

      Спасибо, исправил.

    • Albatros пишет:

      Спасибо, актуально!
      (хороша ложка к обеду) как раз боролся с редактированием переводов по отдельности, а не всех сразу!

    • Albatros пишет:

      что-то наверное не так…
      тестовый CRUD генерил так:
      symfony doctrine:generate-module backend news News

      в doBind отлично вычищаются лишние языки, но при сохранении формы до doUpdateObject($values) не доходит, вылетает с ошибкой при попытке создать пустой объект для незаполненного языка.
      (SQLSTATE[HY000]: General error: 1364 Field ‘title’ doesn’t have a default value)
      на запросе INSERT INTO news_translation (id, lang, slug) VALUES (’2′, ‘ru’, »)

      хотя никто не просит его создавать запись для русского языка.

      после создания пустой строки doUpdateObject($values) отрабатывается нормально, затирания не происходят…

    • CharnaD пишет:

      Я, к сожалению, некоторое время занят делами отвлеченными от симфони и не могу сейчас вдумчиво помочь. Могу разве что посоветовать почитать оригинал на английском или поискать ошибку самостоятельно. Могу точно сказать, что у меня все работало.

    • Albatros пишет:

      оно работает, на update отлично работает.
      но попробовал создать новый элемент, заполнил перевод 1 языка из 3х, в базе добавились 3 записи — заполненная и 2 пустых.

      я к тому что на insert почему-то не распространяется :(

    • Albatros пишет:

      да, действительно всё рабоает, проблема как обычно была в невнимательном копипасте =)
      зато поковырял исходники sfForms, было интересно.

      CharnaD, удали 2 предыдущих моих поста, они не по теме.

    • Руслан пишет:

      Здравствуйте.Только начал изучать symfony.Сложно.Отображение данных из таблицы получилось.А вот никак не получается ввод данных в таблицу.Если можно,на примере таблицы из 2-х полей объясните.Спасибо.И за статью тоже.

    • CharnaD пишет:

      Если вы только начали изучать Симфу — пройдите по Jobeet, гайду на http://www.symfony-project.org. Этот пост все-таки не для начинающих, а для продолжающих.

    • denys281 пишет:

      а как сделать один из языков дефолтным? чтобы форма не сохранялась если поле пустое? у меня в этом случае что-то не выходит сделать валидатор (

    Trackbacks

    Оставить комментарий

    Внимание: Комментарии проходят премодерацию. Не надо посылать их несколько раз.