Sunday, July 22, 2012

ExtBase - Übersetzungen per XLIFF einbinden

Seit Version 4.6 nutzt TYPO3 das XLIFF Format zum speichern von Sprachdateien. Das neue Format ist kompatibel mit Übersetzungsservern und bietet noch einige andere Vorteile wie z.B. Pluralformen oder Übersetzungsvorschläge.

Wer mit TYPO3 4.6 oder TYPO3 4.7 eine mehrsprachige Extbase Extension entwickelt, wird sich automatisch mit dem neuen Format auseinander setzen müssen, da der Extension Builder per Default nun auch .xlf-Sprachdateien generiert. Wie aber genau bindet man eine neue Sprache in seine Extension ein?

Als Basissprache wird immer von Englisch ausgegangen. Nach dem Anlegen einer neuen Extension mit dem Extension Builder hat man in der Regel eine Sprachdatei namens locallang.xlf, welche sich im Ordner Resources\Private\Language\ befindet.

Original XLIFF Sprachdatei in englischer Sprache

<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<xliff version="1.0">
 <file source-language="en" datatype="plaintext" original="messages" date="2012-07-40T13:35:40Z" product-name="loc_test">
  <header/>
  <body>

   <trans-unit id="tx_loctest_domain_model_testtable">
    <source>Testtable</source>
   </trans-unit>
   <trans-unit id="tx_loctest_domain_model_testtable.title">
    <source>Title</source>
   </trans-unit>
   <trans-unit id="tx_loctest_domain_model_testtable.text">
    <source>Description</source>
   </trans-unit>
   
  </body>
 </file>
</xliff>

Möchte man nun z.B. eine deutsche Übersetzung für die oben gezeigte Sprachdatei generieren, erstellt man im gleichen Ordner eine Kopie der bestehenden Datei und nennt diese de.locallang.xlf

Folgende Änderungen muss man in der kopierten Datei vornehmen. Zum einen muss man die Ziel-Sprache angeben (siehe unten in Zeile 3 target-language="de") und zum anderen muss man für jeden zu übersetzenden Eintrag jeweils einen neuen Eintrag für die Zielsprache anlegen (gekennzeichnet durch das target-Tag)

Übersetzte XLIFF Sprachdatei in deutscher Sprache

<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<xliff version="1.0">
 <file source-language="en" target-language="de" datatype="plaintext" original="messages" date="2012-07-40T13:35:40Z" product-name="loc_test">
  <header/>
  <body>

   <trans-unit id="tx_loctest_domain_model_testtable">
    <source>Testtable</source>
                                <target>Testtabelle</target>
   </trans-unit>
   <trans-unit id="tx_loctest_domain_model_testtable.title">
    <source>Title</source>
                                <target>Titel</target>
   </trans-unit>
   <trans-unit id="tx_loctest_domain_model_testtable.text">
    <source>Description</source>
                                <target>Beschreibung</target>
   </trans-unit>
   
  </body>
 </file>
</xliff>

Fall man bestimmte Einträge nicht übersetzt, greift ein Fallback-System und es wird jeweils der Eintrag der Originalsprache angezeigt. Dieses funktioniert nur dann, wenn man den ganzen Eintrag beginnend ab dem entsprechenden trans-unit-Tag löscht. Es reicht nicht, einfach das target-Tag wegzulassen, denn dann wird gar nichts als Übersetzung für den Eintrag angezeigt.

Friday, July 13, 2012

Infofenster bei Google Maps Karte automatisch schließen

Bei Google Maps hat man ja die Möglichkeit das Suchergebnis einer Maps-Suche als HTML Link zu kopieren und dieses aus seiner Webseite einzufügen. Das ganze sieht dann in etwas so aus.


Wie zu sehen ist, erscheint das Info-Fenster automatisch, was unter Umständen jedoch nicht gewünscht ist. Damit das Infofenster sich nicht automatisch öffnet, muss man den HTML Code von Google etwas anpassen.

HTML Code vorher

<iframe frameborder="0" height="350" marginheight="0" marginwidth="0" scrolling="no" src="https://maps.google.com/maps?f=q&source=s_q&hl=en&geocode=&q=Lise-Meitner-Stra%C3%9Fe+4,+Flensburg&aq=t&sll=35.101934,-95.712891&sspn=69.190487,135.263672&ie=UTF8&hq=&hnear=Lise-Meitner-Stra%C3%9Fe+4,+24941+Flensburg,+Schleswig-Holstein,+Germany&t=m&z=14&ll=54.76215,9.44458&output=embed" width="425"></iframe>
<small><a href="https://maps.google.com/maps?f=q&source=embed&hl=en&geocode=&q=Lise-Meitner-Stra%C3%9Fe+4,+Flensburg&aq=t&sll=35.101934,-95.712891&sspn=69.190487,135.263672&ie=UTF8&hq=&hnear=Lise-Meitner-Stra%C3%9Fe+4,+24941+Flensburg,+Schleswig-Holstein,+Germany&t=m&z=14&ll=54.76215,9.44458" style="color: blue; text-align: left;">View Larger Map</a></small>

HTML Code nachher

<iframe frameborder="0" height="350" marginheight="0" marginwidth="0" scrolling="no" src="https://maps.google.com/maps?f=q&source=s_q&hl=en&geocode=&q=Lise-Meitner-Stra%C3%9Fe+4,+Flensburg&aq=t&sll=35.101934,-95.712891&sspn=69.190487,135.263672&ie=UTF8&hq=&hnear=Lise-Meitner-Stra%C3%9Fe+4,+24941+Flensburg,+Schleswig-Holstein,+Germany&t=m&z=14&ll=54.76215,9.44458&output=embed&iwloc=near" width="425"></iframe>
<small><a href="https://maps.google.com/maps?f=q&source=embed&hl=en&geocode=&q=Lise-Meitner-Stra%C3%9Fe+4,+Flensburg&aq=t&sll=35.101934,-95.712891&sspn=69.190487,135.263672&ie=UTF8&hq=&hnear=Lise-Meitner-Stra%C3%9Fe+4,+24941+Flensburg,+Schleswig-Holstein,+Germany&t=m&z=14&ll=54.76215,9.44458" style="color: blue; text-align: left;">View Larger Map</a></small>

Durch das hinzufügen bzw. der Veränderung des Parameters &iwloc=near wird das Infofenster automatisch geschlossen. Falls der Parameter schon vorhanden ist (z.B. in Form von &iwloc=A oder &iwloc=addr), muss er auf  &iwloc=near geändert werden.

Thursday, July 12, 2012

Extbase Extension und die Übersetzung von Datensätzen im Backend

In fast jeder TYPO3 Extension müssen in irgendeiner Art und Weise Relationen abgebildet werden. Nehmen wir z.B. an, man hat eine Tabelle mit Artikeln und eine Tabelle mit Kategorien. Per n:m Relation möchte man einen Artikel mehreren Kategorien zuordnen. Das alles ist Standard und lässt sich mit dem ExtensionBuilder recht einfach konfigurieren. Mehrsprachigkeit ist im Prinzip auch kein Problem und funktioniert out-of-the-box im Frontend, im Backend kann es aber zu etwas Verwirrung für den Redakteur kommen, wenn es um die lokalisierten Datensätze (in diesem Fall die Kategorien) geht.

Der ExtensionBuilder sieht bei der Relation nicht vor, dass es sich auch um lokalisierte Datensätze handelt und zeigt jeweils den ursprünglichen Datensatz sowie die dazugehörige Übersetzung an (siehe Bild).


Damit hier nur die Datensätze der Default-Sprache angezeigt werden, muss man im TCA für das Relations-Feld eine Zeile hinzufügen.



'category' => array(
 'exclude' => 0,
 'label' => 'LLL:EXT:tx_testextension/Resources/Private/Language/locallang_db.xlf:tx_testextension_domain_model_article.category',
 'config' => array(
  'type' => 'select',
  'foreign_table' => 'tx_testextension_domain_model_category',
  'foreign_table_where' => ' AND (tx_testextension_domain_model_category.sys_language_uid = 0 OR tx_testextension_domain_model_category.l10n_parent = 0)',
  'minitems' => 0,
  'maxitems' => 1,
 ),
),

Neu hinzugekommen ist die Zeile "foreign_table_where", welche die Selektion der Datensätze auf die Default-Sprache reduziert. Nachdem man die Zeile hinzugefügt hat, werden in der Select-Box im Backend nur noch die Datensätze der Default-Sprache angezeigt.

Monday, July 9, 2012

Probleme mit TYPO3 4.7.2 und TemplaVoila

Mit TYPO3 4.7 habe ich bisher leider nur Pech gehabt. Direkt nachdem die Version 4.7 veröffentlicht wurde, habe ich diese in einem Projekt eingesetzt und musste dann feststellen, dass in CSS Styled Content ein ärgerlicher Bug enthalten war, der es unmöglich machte mit der Version zu arbeiten. Zum Glück konnte ich den Bug manuell patchen und somit erst mal mit der Version weiter arbeiten.

Nachdem nun der Bug gefixed wurde, habe ich in einem anderen Projekt auch auf TYPO3 4.7 gesetzt und stehe nun gleich vor dem nächsten großen Bug, der mir das Arbeiten mit TYPO3 erschwert.

Wenn man TYPO3 4.7.2 zusammen mit TemplaVoila 1.7.0 einsetzt, dann kann man (selbst als Admin) keine Inhalte mehr über das Seitenmodul anlegen. Es erscheint immer folgende Fehlermeldung:

Sorry, you didn't have proper permissions to perform this change.

Einen Bugfix scheint es noch nicht zu geben, der Bug ist aber schon mal im Bugtracker erfasst.

Update 11.07.2012
So, nun hat sich etwas getan und ich gehe mal davon aus, dass eine Lösung bald auf dem Weg sein wird.

Update 16.12.2012
Mittlerweile ist TemplaVoila 1.8 veröffentlicht und der Fehler wurde behoben.

Sunday, July 8, 2012

Neue Version der sf_yubikey Extension

Nachdem ich im Juni die TYPO3 Extension sf_yubikey veröffentlicht habe, habe ich bisher nur positives Feedback erhalten. Der Hersteller Yubico war so nett und hat die Extension direkt auf seiner Webseite erwähnt.

Gestern habe ich nun eine neue Version veröffentlicht, welche ein paar neue Features und Verbesserungen mitbringt. Zum einen ist es nun möglich, per Extension Settings den Authentication-Service zu aktivieren oder zu deaktivieren und zum anderen ist es nun möglich, die Extension auch für die Authentifizierung von FE-Benutzer zu nutzen. Somit kann man nun z.B. Portale mit TYPO3 Entwickeln, wo die Frontend-Benutzer sich neben dem Benutzernamen und dem Passwort zusätzlich noch mit einem OTP anmelden müssen.

Wer Kommentare oder Verbesserungsvorschläge zur Extension hat, möge diese bitte direkt im Redmine auf forge.typo3.org posten.

Thursday, July 5, 2012

FlexForms Optionen in Abhängigkeit von switchableControllerActions

Es gibt im Netz eine Vielzahl guter Tutorials wie man in einer TYPO3 Extbase Extension die Plugin-Optionen per Flexform konfigurierbar macht. Nun kann es aber vorkommen, dass man für bestimmte Konfigurations-Einstellungen unterschiedliche Einstellungsmöglichkeiten anzeigen möchte. Hierfür muss das Flexform so konfiguriert werden, dass bestimmte Einstellungsmöglichkeiten nur in Abhängigkeit von anderen Einstellungen möglich sind. Wie man dieses genau realisiert, zeige ich in diesem Artikel.

Als technische Grundlage für den Artikel dient die Extension "news" von Georg Ringer. Dort habe ich zum ersten mal bewusst diese Technik gesehen.

Als erstes erstellt man eine XML-Datei für die Flexform-Konfiguration. Als Beispiel dient die Datei /Configuration/FlexForms/flexform.xml


<T3DataStructure>
 <langDisable>1</langdisable>
 <sheets>
  <sDEF>
   <ROOT>
    <TCEforms>
     <sheetTitle>Settings</sheetTitle>
    </TCEforms>
    <type>array</type>
    <el>
     
     <switchableControllerActions>
      <TCEforms>
      <label>Mode</label>
      <onChange>reload</onChange>
      <config>
       <type>select</type>
       <items type="array">
        <numIndex index="1" type="array">
         <numIndex index="0">List-Mode</numIndex>
         <numIndex index="1">Testcontroller->list</numIndex>
        </numIndex>
        <numIndex index="2" type="array">
         <numIndex index="0">Single-Mode</numIndex>
         <numIndex index="1">Testcontroller->details</numIndex>
        </numIndex>
       </items>
      </config>
      </TCEforms>
     </switchableControllerActions>

     
     <settings.showrandom>
      <TCEforms>
       <label>Random</label>
       <config>
        <type>check</type>
       </config>
      </TCEforms>
     </settings.record>

     
     <settings.record>
      <TCEforms>
       <label>Record</label>
       <config>
        <type>group</type>
        <internal_type>db</internal_type>
        <allowed>tx_extbasetest_domain_model_test</allowed>
        <size>1</size>
        <maxitems>1</maxitems>
        <minitems>0</minitems>
        <show_thumbs>1</show_thumbs>
       </config>
      </TCEforms>
     </settings.record>

    </el>
   </ROOT>
  </sDEF>
 </sheets>
</T3DataStructure>

Wichtig sind die beiden settings-Knoten, welche später Abhängig vom ausgewählten Modus (hier der Knoten switchablecontrolleractions) angezeigt bzw. ausgeblendet werden.

Als nächstes wird das FlexForm.xml in die Extension eingebunden, sodass die Einstellungsmöglichkeiten im Plugin zur Verfügung stehen. Dafür wird in der ext_tables.php folgende Zeile hinzugefügt.


$pluginSignature = str_replace('_','',$_EXTKEY) . '_' . extbasetest;
$TCA['tt_content']['types']['list']['subtypes_addlist'][$pluginSignature] = 'pi_flexform';
t3lib_extMgm::addPiFlexFormValue($pluginSignature, 'FILE:EXT:' . $_EXTKEY . '/Configuration/FlexForms/flexform.xml');

Die FlexForm Konfiguration ist somit eigentlich schon fertig und man hat nun in den Plugin-Einstellungen zum einen eine ComboBox, wo man einstellen kann, ob man eine Listen- oder eine Detailansicht anzeigen möchte und zum anderen werden jeweils noch eine Checkbox und ein TYPO3 Datensatz Selektor angezeigt. Die letzten beiden Optionen sollen nun aber nur angezeigt werden, wenn wir in der ComboBox den Listenmodus aktiviert haben. Um dieses zu realisieren, bedienen wir uns des Hooks getFlexFormDSClass, welcher von t3lib/class.t3lib_befunc.php zur Verfügung gestellt wird.

In der ext_localconf.php muss dazu folgende Zeile hinzugefügt werden.


$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['getFlexFormDSClass'][$_EXTKEY] =
 'EXT:' . $_EXTKEY. '/Classes/Hooks/T3libBefunc.php:Tx_Extbasetest_Hooks_T3libBefunc';

Der Hook sorgt dafür, dass wir die Datenstruktur der Flexform zur Laufzeit verändern können. Wie aus der ext_localconf.php hervorgeht, wird der Hook über die Klasse Tx_Extbasetest_Hooks_T3libBefunc angesprochen. Diese muss nun angelegt werden.

class Tx_Extbasetest_Hooks_T3libBefunc {

 /**
  * Fields which are removed in details view
  *
  * @var array
  */
 public $removedFieldsInDetailsView = array(
   'sDEF' => 'record,showrandom'
  );

 /**
  * Hook function of t3lib_befunc
  * It is used to change the flexform if it is about news
  *
  * @param array &$dataStructure Flexform structure
  * @param array $conf some strange configuration
  * @param array $row row of current record
  * @param string $table table anme
  * @param string $fieldName some strange field name
  * @return void
  */
 public function getFlexFormDS_postProcessDS(&$dataStructure, $conf, $row, $table, $fieldName) {
  //t3lib_div::debug($row);
  if ($table === 'tt_content' && $row['list_type'] === 'extbasetest_extbasetest' && is_array($dataStructure)) {
   $this->updateFlexforms($dataStructure, $row);
  }
 }

 /**
  * Update flexform configuration if a action is selected
  *
  * @param array|string &$dataStructure flexform structur
  * @param array $row row of current record
  * @return void
  */
 protected function updateFlexforms(array &$dataStructure, array $row) {
  $selectedView = '';

   // get the first selected action
  $flexformSelection = t3lib_div::xml2array($row['pi_flexform']);
  if (is_array($flexformSelection) && is_array($flexformSelection['data'])) {
   $selectedView = $flexformSelection['data']['sDEF']['lDEF']['switchableControllerActions']['vDEF'];
   if (!empty($selectedView)) {
    $actionParts = t3lib_div::trimExplode(';', $selectedView, TRUE);
    $selectedView = $actionParts[0];
   }

   // new plugin element
  } elseif(t3lib_div::isFirstPartOfStr($row['uid'], 'NEW')) {
    // use List as starting view
    // @todo dynamic check, getting view from $flexformSelection
   $selectedView = 'Extbasetest->list';
  }

  if (!empty($selectedView)) {
    // modify the flexform structure depending on the first found action
   switch ($selectedView) {
    case 'Extbasetest->list':
     break;
    case 'Extbasetest->details':
     $this->deleteFromStructure($dataStructure, $this->removedFieldsInDetailsView);
     break;
   }
  }
 }

 /**
  * Remove fields from flexform structure
  *
  * @param array &$dataStructure flexform structure
  * @param array $fieldsToBeRemoved fields which need to be removed
  * @return void
  */
 private function deleteFromStructure(array &$dataStructure, array $fieldsToBeRemoved) {
  foreach ($fieldsToBeRemoved as $sheetName => $sheetFields) {
   $fieldsInSheet = t3lib_div::trimExplode(',', $sheetFields, TRUE);

   foreach ($fieldsInSheet as $fieldName) {
    unset($dataStructure['sheets'][$sheetName]['ROOT']['el']['settings.' . $fieldName]);
   }
  }
 }
}

Die Klasse habe ich fast 1:1 von der "news" Extension übernommen und angepasst. Anbei nun eine kurze Erläuterung, worauf man zu achten hat.

  • Im Array $removedFieldsInDetailsView wird festgelegt, welche Felder in der Detailansicht ausgeblendet werden sollen. Hier kann man auch mehrere Sheets angeben, falls das FlexForm über mehrere Sheets verfügt.
  • In der Methode  getFlexFormDS_postProcessDS muss man darauf achten, dass man die Variable $row['list_type'] auch mit dem Namen seines Plugins vergleicht.
  • In der Methode updateFlexforms muss man nun noch seine eigenen Controller ansprechen (in diesem Fall Extbasetest)
  • Aus der Methode deleteFromStructure geht hervor, dass die Settings im FlexForm.xml alls mit "settings." beginnen müssen, damit das Ein- bzw. Ausblenden der Optionen per Hook funktioniert.

Nachdem alle Einstellungen wie im Beispiel oben gezeigt vorgenommen wurden, müsste man in den Plugin-Optionen nun zwischen einer Listen- und einer Detail-Ansicht wechseln können und es müssten bei der Listenansicht die Zusatzoptionen angezeigt werden, welche in der Detailansicht nicht zur Verfügung stehen.

Update 18.10.2013

Das alles geht auch einfacher mit displayCond (Beispiel unten).

     <settings.record>
      <TCEforms>
       <label>Record</label>
       <displayCond>FIELD:switchableControllerActions:=:Testcontroller->list</displayCond>
       <config>
        <type>group</type>
        <internal_type>db</internal_type>
        <allowed>tx_extbasetest_domain_model_test</allowed>
        <size>1</size>
        <maxitems>1</maxitems>
        <minitems>0</minitems>
        <show_thumbs>1</show_thumbs>
       </config>
      </TCEforms>
     </settings.record>