„Firehol für Icmpv6 anpassen“ … oder wie man ein Gmail Timeout Problem löst

Wenn man Mails von Gmail über den eigenen Mail Server verschicken will, muss dieser ICMPv6 Pakete zulassen. Wenn deine Firewall das allerdings nicht erlaubt, bekommt man bei Gmail einen kryptischen Timeout Fehler.

Um Firehol diesbezüglich anzupassen, habe ich folgende Section hinzugefügt:

ipv6 interface any v6interop proto icmpv6
 server ipv6error accept
 client ipv6neigh accept
 server ipv6neigh accept
 policy return

 

Spamassassin Konfiguration und Bayes User-Abhängig verwalten und managen mit Roundcube SaUserPrefs & Dovecot Antispam unter Ubuntu 16.04 mit Amavis

Als erstes benötigt man eine Datenbank – ich hab dafür der einfachheit halber PhpMyAdmin verwendet und einen neuen User ’spamassassin‘ samt gleichnamiger Datenbank angelegt.

Dann die einzelnen Tabellen (leider hatte ich ein bisschen Probleme mit der maximalen Keylength bei den Datenbanken, weswegen ich etwas tricksen musste):

CREATE TABLE `awl` (
  `username` varchar(100) NOT NULL DEFAULT '',
  `email` varbinary(255) NOT NULL DEFAULT '',
  `ip` varchar(40) NOT NULL DEFAULT '',
  `count` int(11) NOT NULL DEFAULT '0',
  `totscore` float NOT NULL DEFAULT '0',
  `signedby` varchar(255) NOT NULL DEFAULT '',
  `lastupdate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

CREATE TABLE `bayes_expire` (
  `id` int(11) NOT NULL DEFAULT '0',
  `runtime` int(11) NOT NULL DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `bayes_global_vars` (
  `variable` varchar(30) NOT NULL DEFAULT '',
  `value` varchar(200) NOT NULL DEFAULT ''
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `bayes_global_vars` (`variable`, `value`) VALUES
('VERSION', '3');


CREATE TABLE `bayes_seen` (
  `id` int(11) NOT NULL DEFAULT '0',
  `msgid` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '',
  `flag` char(1) NOT NULL DEFAULT ''
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;

CREATE TABLE `bayes_token` (
  `id` int(11) NOT NULL DEFAULT '0',
  `token` char(5) NOT NULL DEFAULT '',
  `spam_count` int(11) NOT NULL DEFAULT '0',
  `ham_count` int(11) NOT NULL DEFAULT '0',
  `atime` int(11) NOT NULL DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `bayes_vars` (
  `id` int(11) NOT NULL,
  `username` varchar(200) NOT NULL DEFAULT '',
  `spam_count` int(11) NOT NULL DEFAULT '0',
  `ham_count` int(11) NOT NULL DEFAULT '0',
  `token_count` int(11) NOT NULL DEFAULT '0',
  `last_expire` int(11) NOT NULL DEFAULT '0',
  `last_atime_delta` int(11) NOT NULL DEFAULT '0',
  `last_expire_reduce` int(11) NOT NULL DEFAULT '0',
  `oldest_token_age` int(11) NOT NULL DEFAULT '2147483647',
  `newest_token_age` int(11) NOT NULL DEFAULT '0'
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;

CREATE TABLE `userpref` (
  `username` varchar(100) NOT NULL DEFAULT '',
  `preference` varchar(50) NOT NULL DEFAULT '',
  `value` varchar(100) NOT NULL DEFAULT '',
  `prefid` int(11) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;

INSERT INTO `userpref` (`username`, `preference`, `value`, `prefid`) VALUES
('@GLOBAL', 'required_hits', '5.0', 1);

ALTER TABLE `awl`
  ADD PRIMARY KEY (`username`,`email`(100),`signedby`,`ip`);

ALTER TABLE `bayes_expire`
  ADD KEY `bayes_expire_idx1` (`id`);

ALTER TABLE `bayes_global_vars`
  ADD PRIMARY KEY (`variable`);

ALTER TABLE `bayes_seen`
  ADD PRIMARY KEY (`id`,`msgid`);

ALTER TABLE `bayes_token`
  ADD PRIMARY KEY (`id`,`token`),
  ADD KEY `bayes_token_idx1` (`id`,`atime`);

ALTER TABLE `bayes_vars`
  ADD PRIMARY KEY (`id`),
  ADD UNIQUE KEY `bayes_vars_idx1` (`username`);

ALTER TABLE `userpref`
  ADD PRIMARY KEY (`prefid`),
  ADD KEY `username` (`username`);

ALTER TABLE `bayes_vars`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;

ALTER TABLE `userpref`
  MODIFY `prefid` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;

/etc/default/spamassassin anpassen:

OPTIONS="--create-prefs --max-children 5 --helper-home-dir --sql-config --nouser-config -u vmail"

In /etc/spamassassin/v310.pre habe ich folgende Zeile einkommentiert:

loadplugin Mail::SpamAssassin::Plugin::AWL

und danach /etc/spamassassin/local.cf angepasst:

# Mysql User Scores
user_scores_dsn              dbi:mysql:spamassassin:localhost
user_scores_sql_username     spamassassin
user_scores_sql_password     DATENBANKPASSWORT
user_scores_sql_custom_query     SELECT preference, value FROM _TABLE_ WHERE username = _USERNAME_ OR username = '@GLOBAL' OR username = CONCAT('%',_DOMAIN_) ORDER BY username ASC

# Bayes

bayes_store_module           Mail::SpamAssassin::BayesStore::SQL
bayes_sql_dsn                dbi:mysql:spamassassin:localhost:3306
bayes_sql_username           spamassassin
bayes_sql_password           DATENBANKPASSWORT

#AWL
auto_whitelist_factory       Mail::SpamAssassin::SQLBasedAddrList
user_awl_dsn                 DBI:mysql:spamassassin:localhost
user_awl_sql_username        spamassassin
user_awl_sql_password        DATENBANKPASSWORT
user_awl_sql_table           awl

Danach noch in die /etc/amavis/conf.d/50-user

@sa_userconf_maps = ({
  '.' => 'sql:'
});
@sa_username_maps = new_RE (
  [ qr'^([^@]+@.*)'i => '${1}' ]
);

eintragen, damit Amavis den Username (Bei mir ist der Username die E-Mail Adresse) korrekt an Spamassassin übergibt.

Roundcube SAUserpref Plugin

Das Plugin ansich war bei mir schon installiert, weil ich Roundcube über das APT Package mit allen Plugins installiert habe. Als erstes habe ich in der /etc/roundcube/config.inc.php ’sauserprefs‘ in config[‚plugins‘] ergänzt, die Originalconfig aus /usr/share/roundcube/plugins/sauserprefs/config.inc.php.dist nach/etc/roundcube/plugins/sauserprefs/config.inc.php  kopiert und diese dann nach meinen Wünschen angepasst und mit den Zugangsdaten zur Mysql Tabelle ausgestattet. Wenn man das (richtig) gemacht hat, taucht im Einstellungsmenü von Roundcube der neue Reiter „Spam“ auf.

Dovecot Antispam Plugin

Mit dem Dovecot Antispam Plugin, kann man Dovecot automatisch E-Mails als Spam lernen lassen, wenn Sie manuell in den Spam Ordner verschoben werden und als Ham, wenn Sie wieder herausgenommen werden. Dafür habe ich die dovecot.conf folgendermassen angepasst:

protocol imap {
    mail_plugins = $mail_plugins antispam
    mail_max_userip_connections = 20
    imap_idle_notify_interval = 29 mins
}
plugin {
    # Zum Debuggen kann man folgende Zeilen einkommentieren
    #antispam_debug_target = syslog
    #antispam_verbose_debug = 1

    antispam_backend = pipe
    antispam_trash = Trash;trash;Deleted Items; Deleted Messages
    antispam_spam = Spam
    antispam_pipe_program_spam_arg = --spam
    antispam_pipe_program_notspam_arg = --ham
    antispam_pipe_program = /usr/bin/sa-learn
    antispam_pipe_program_args = --username=%Lu
}

Das wars auch schon.

Quellen:

https://wiki.dhits.nl/index.php/SpamAssassin
http://svn.apache.org/repos/asf/spamassassin/tags/spamassassin_current_release_3.4.x/sql/README
https://web-yard.net/wiki/Setup_(production)#2011-07-11:_dovecot-antispam_update
https://tty1.net/blog/2014/spamassassin-with-mysql_en.html

Dovecot & Postfixadmin: Password Schema aktualisieren

Aufgrund der Migration meines Mailservers (unter anderem Ubuntu 16.04 & Postfixadmin 3) habe ich mich entschlossen mein Password Schema von MD5-Crypt auf SHA512 zu aktualisieren. Nach einiger Bastelarbeit und vielen Problemen habe ich es nun endlich hinbekommen. Hier meine dafür notwendigen Schrittse:

In der Datenbank das Password Schema als Prefix im Passwort eintragen:

UPDATE admin SET password = CONCAT('{MD5-CRYPT}', password);
UPDATE mailbox SET password = CONCAT('{MD5-CRYPT}', password);

In der Dovecot Config habe ich folgendes hinzugefügt:

passdb {
 driver = sql
 args = /etc/dovecot/dovecot-sql.conf.ext
}
userdb {
 driver = prefetch
}
# The userdb below is used only by lda.
userdb {
 driver = sql
 args = /etc/dovecot/dovecot-sql.conf.ext
}

Und dann noch die jeweils benötigten Post-Login Scripts die dann automatisch das Passwort aktualisieren:

service imap {
 executable = imap imap-postlogin
 unix_listener imap-master {
 user = dovecot
 }
}

service imap-postlogin {
 executable = script-login /etc/dovecot/after_login.sh
 user = vmail
 unix_listener imap-postlogin {
 }
}

service pop3 {
 executable = pop3 pop3-postlogin
}
service pop3-postlogin {
 executable = script-login /etc/dovecot/after_login.sh
 user = vmail
 unix_listener pop3-postlogin {
 }
}

In der Datei /etc/dovecot/dovecot-sql.conf.ext  habe ich das default_password_scheme auskommentiert und die Queries folgendermaßen angepasst

user_query = SELECT '/srv/vmail/%d/%n' as home, 'maildir:/srv/vmail/%d/%n' as mail, 150 AS uid, 8 AS gid, concat('dirsize:storage=', quota) AS quota FROM mailbox WHERE username = '%u' AND active = '1'
password_query = SELECT username as user, password, '/srv/vmail/%d/%n' as userdb_home, 'maildir:/srv/vmail/%d/%n' as userdb_mail, 150 as userdb_uid, 8 as userdb_gid, '%w' as userdb_plain_pass FROM mailbox WHERE username = '%u' AND active = '1'
iterate_query = SELECT username AS user FROM mailbox

Dann habe ich folgendes Script angelegt: /etc/dovecot/after_login.sh

#!/bin/sh
DOVECOTPW=$(/usr/bin/doveadm pw -s SHA512-CRYPT -p $PLAIN_PASS)
/etc/dovecot/convertpw.php $USER $DOVECOTPW
exec "$@"

und dann das eigentlich Script um das Passwort zu ändern: /etc/dovecot/convertpw.php

#!/usr/bin/php
<?php
$mysqlhost = "127.0.0.1";
$mysqluser = "postfix"; // username which is used to connect to the database
$mysqlpass = "meinpasswort"; // password which is used to connect to the database
$mysqldb = "postfix"; // databasename where the passwords are stored
$mysqltable = "mailbox"; // table where the passwords are stored
$idfield = "username"; // fieldname where the userlogin is stored
$passfield = "password"; // fieldname where the passwords is stored

$usr = $argv[1];
$dov = $argv[2];
$mysqli = new mysqli("$mysqlhost", "$mysqluser", "$mysqlpass", $mysqldb);
if ($mysqli->connect_errno) {
 echo "Failed to connect to MySQL: (" . $mysqli->connect_errno . ") " . $mysqli->connect_error;
}
$result = $mysqli->query("SELECT $passfield FROM $mysqltable WHERE $idfield = '$usr' AND $passfield like '{SHA%'");
if($result->num_rows == 0) {
 $mysqli->query("UPDATE $mysqltable SET $passfield='".$dov."', modified=NOW() WHERE $idfield='".$usr."'");
}
exit;

Danach habe ich noch die config.inc.php von Postfixadmin angepasst:

$CONF['encrypt'] = 'dovecot:SHA512-CRYPT';
$CONF['dovecotpw'] = "/usr/bin/doveadm pw";

Bei mir ist der Besitzer des Ordners /etc/dovecot „vmail“ – diesbezüglich muss man noch die Rechte anpassen und die after_login.sh ausführbar machen. Danach noch dovecot neustarten und es sollte alles funktionieren.

Quellen:

https://kaworu.ch/blog/2016/04/20/strong-crypt-scheme-with-dovecot-postfixadmin-and-roundcube/
http://wiki2.dovecot.org/HowTo/ConvertPasswordSchemes

 

Mosh – Mobile Shell

Wer öfters einmal seine Server unterwegs (zum Beispiel im Zug) administriert oder wie ich derzeit einen UPC Router hat, der offene Verbindungen nach einigen Minuten idle automatisch kappt, dem könnte MoSh weiterhelfen.

Mosh öffnet zusätzlich zum SSH Zugang noch eine UDP Connection zum Server und bleibt so dauerhaft verbunden.

Unter Mac hab ich die Software mit Homebrew installiert:

brew install mosh

Am Ubuntu Server mit einem einfachen

apt-get install mosh

Auf meinem Server habe ich auch noch Firehol (iptables Firewall Generator) installiert und musste deswegen dessen Regeln noch anpassen:

Dafür habe ich in der firehol.conf die Ports definiert:

server_mosh_ports="udp/60000:61000"
client_mosh_ports="default"

und im Interface noch das Service hinzugefügt:

server "mosh" accept

Die Verbindung wird dann wie mit ssh z.B. mit ssh meinserver.de geöffnet.

Nun ist die Zeit bis mein neues UPC Modem kommt deutlich erträglicher – es treibt einen fast in den Wahnsinn alle paar Minuten die Verbindung zum Server neu aufzubauen während man Software installiert oder Wartungen durchführt.

Referenzen:
https://de.wikipedia.org/wiki/Mosh_(Software)
https://mosh.mit.edu/
https://firehol.org/

Fontcustom – Icon-Fonts einfach erstellen

Seit neuestem wird es immer beliebter, statt Sprites Iconfonts zu verwenden. Die Vorteile liegen auf der Hand: Die Bilder sind Vektoren die beliebig skaliert oder eingefärbt werden können. Ein Nachteil ist natürlich, das mehrfarbige Icons nicht möglich sind.

Bis vor kurzem habe ich zur Erstellung IcoMoon ( https://icomoon.io/ ) verwendet, das einfach zu bedienen ist und neben diverser Cross-Browser Fixes auch eine große Menge von Gratis Icons zur Verfügung stellt.

Vor kurzem habe ich allerdings auf Fontcustom ( https://github.com/FontCustom/fontcustom/ ) umgestellt. Die Bedienung ist einfach per Command Line und es lässt sich einfach in Grunt Abläufe sowie in Compass ( http://compass-style.org/ ) integrieren.

Wie auf der Webseite beschrieben ist eine Installation auf Mac OS X mit Brew ( http://brew.sh/ ) ganz einfach:

brew install fontforge --with-python
brew install eot-utils
sudo gem install fontcustom

Danach kann man sich in seinem Projekt eine Config Datei mit „fontcustom config“ erstellen lassen. Um Fontcustom in mein Compass Projekt integrieren, musste ich zwei Einstellungen vornehmen. Einerseits in meiner config.rb folgende Zeile hinzufügen:

fonts_dir = "view/fonts"

damit Compass weiß wo sich die Fontfiles befinden(Der Pfad ist natürlich willkürlich).

In meiner fontcustom.yml habe ich dann folgende Einstellungen vorgenommen:

input:
 vectors: view/img/icons
output:
 fonts: view/fonts
 css: view/sass
templates:
- scss-rails
preprocessor_path: ''

Der Vollständigkeit halber (und damit man sich meine Ordnerstruktur vorstellen kann) hier mein komplettes config.rb

http_path = "/"
css_dir = "view/css"
sass_dir = "view/sass"
images_dir = "view/img"
javascripts_dir = "view/js"
fonts_dir = "view/fonts"

# You can select your preferred output style here (can be overridden via the command line):
output_style = :compact
#:expanded or :nested or :compact or :compressed

# To enable relative paths to assets via compass helper functions. Uncomment:
relative_assets = true

In meiner screen.scss befindet sich dann noch eine Zeile mit @include „_fontcustom-rails.scss“ um die Font Definitionen auch in meine CSS File zu inkludieren.

Daten für Adobe InDesign Datenzusammenführung / Serienbrief vorbereiten

Bei der Erstellung einer CSV Datei für ein Adobe InDesign CC bin ich auf mehrere Probleme gestoßen – hier eine Kurzzusammenfassung:

  • InDesign verlangt CSVs die entweder mit Beistrich oder mit Tabulator aufgeteilt sind, Strichpunkt etc. funktioniert nicht
  • Anscheinend wird kein UTF-8 unterstützt: bei mir unter Mac OSX hat nur das Encoding „Western (Mac Roman)“ funktioniert.
  • Um Bilder aus der Datenquelle einzufügen muss man in der Kopfzeile der CSV Datei ein @ vor den Spaltennamen stellen, also z.B. „@photo“
  • Bilderpfade werden mit „:“ statt mit „/“ getrennt, wobei in meinen Versuchen Absolute Pfade am besten funktioniert haben also z.B. „Users:Christian:Desktop:images:myimage.jpg“

 

IE9 ignoriert CSS Definitionen

Diesmal möchte ich hier ein Problem verarbeiten, welches mich im Prinzip einen ganzen Arbeitstag gekostet hat. Beim Patienten handelt es sich um eine recht aufwendige, prinzipiell mobile-first gebaute Seite, die grundsätzlich auf dem Bootstrap Grid basiert.

Symptom: IE9 zeigte mir eine Subseite ohne die in Media Queries definierten Styles an. Das Problem trat aber nur im „echten“ IE9 auf, in einem IE10 der auf IE9 gestellt wurde jedoch nicht.TL;DR

Die Problemsuche

Erste (etwas sehr kurz gedachte) Vermutung war, dass der IE9 einfach meine Media Queries ignoriert. Ohne jetzt länger darüber nachzudenken, habe ich wie von Bootstrap empfohlen respond.js eingebunden:

   <!--[if lt IE 9]>
      <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
    <![endif]-->

Eigentlich in der gleichen Sekunde viel mir wieder ein (und am Conditional Comment auch auf) das respond.js ja nur für IE6-IE8 gedacht ist.

Mein zweiter Gedanke war der X-UA Compatible Header, der die meisten IE Rendering Probleme löst – der war allerdings „leider“ in meinem Framework vorhanden.

<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1" />

oder in PHP:

if (isset($_SERVER['HTTP_USER_AGENT']) && (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') !== false)) {
 header('X-UA-Compatible: IE=edge,chrome=1');
 }

Erst danach fiel mir auf, dass die von Bootstrap definierten Media Queries  funktionierten und auf den zweiten Blick auch einige von meinen.

Also begann ich alle meine Media Queries mit Testdefinitionen auszustatten, die jeweils einen eindeutigen erkennbaren Testblock Block Sichtbar schalteten. Dadurch konnte ich feststellen das ca. die Hälfte meiner Media Queries funktionierten.

Der nächste Versuch war, die Media Queries anders zu sortieren, was aber zu keiner Änderung führte. Danach begann ich das CSS so auszukommentieren das nur mehr meine Testdefinitionen enthalten waren und siehe da – die Media Queries funktionierten. Das darauffolgende Schrittweise ein- und wieder auskommentieren brachte dann endlich einen nachvollziehbaren Grund für meine Probleme: Es war nicht der Code ansich der die Probleme auslöste, sondern nur die Menge.

Eine kurze Recherche bestätigten dann meinen Verdacht: IE9 hat Limits unter anderem in der Anzahl an CSS Selektoren die es verarbeiten kann. Wenn man diese Limits überschreitet, werden alle anderen Definitionen ignoriert.

Die Lösung

Im Endeffekt habe ich einfach die bootstrap.css nicht mehr im SCSS inkludiert sondern mit einem eigenen <link> Statement. Es gibt anscheinend eine Vielzahl an alternativen, allerdings war die Problemlösung extrem zeitkritisch und deswegen habe ich nach kurzen Tests zu dieser einfachen, wenn auch nicht idealen, Lösung gegriffen.

TL;DR

IE9 hat ein Limit an Selektoren, rekursiven Includes und anscheinend einigen anderen Dingen pro CSS File. Den Code auf mehrere Files aufteilen kann Abhilfe schaffen, auch wenn diese Lösung fern von ideal ist.

Links zum Thema

http://spaceninja.com/2015/03/31/ie-css-limits/
http://blog.plataformatec.com.br/2014/09/splitting-long-css-files-to-overcome-ie9-4095-rules-limit/
http://blesscss.com/

 

 

Apple Family Sharing – „Vorgang konnte nicht abgeschlossen werden“

Das neue Service des Apple klingt prinzipiell sehr verlockend – die ganze „Familie“ kann gemeinsam alle gekauften Apps benutzen (gemeinsamer Kalender und Co. ist für mich nicht so spannend, das haben wir bereits durch unseren Caldav .  Leider nicht bei mir –  eine halben Stunde in der Apple Support Hotline ergab folgendes Ergebnis: Mein Hauptaccount geht nicht weil er als Firmenaccount eingestuft wurde, mein Nebenaccount (den ich mir übrigens nur angelegt habe, weil mein Hautaccount ja ein Firmenaccount ist und somit logischerweise?!? nicht für icloud verwendet werden kann) geht auch nicht, weil ich ihn als Developer Account verwende.

Kurz zusammengefasst: Familysharing geht nur für Privataccounts oder anders formuliert – wenn man Entwickler ist oder eine Firma betreibt, gibt es sowieso kein Privatleben in dem ein Family Sharing von Nutzen wäre.

WordPress Theme Tipp: Beitragsbilder

Um in einem Custom WordPress Theme Beitragsbilder verwenden zu können muss man z.B. in der functions.php folgende Zeile hinzufügen:

add_theme_support( ‚post-thumbnails‘ );

Danach kann man diese Bilder z.B. so auslesen:

 <?php
 $header_image_id = get_post_thumbnail_id( get_the_ID() );
 $image = wp_get_attachment_image_src( $header_image_id, 'thumbnail');
 ?>
 <img src="<?= $image[0] ?>" alt="<?= get_the_title(); ?>" />

(Natürlich sollte man noch einen Fallback für fehlende Bilder einbauen)

WordPress Multi-Language – Teil 1: Themes und Plugins

Im Zuge meiner Projekte musste ich eine bestehende WordPress Seite in Multi-Language umbauen. In den nächsten Tagen möchte ich dokumentieren welche Erfahrungen ich dabei gemacht habe. Im ersten Teil beschäftige ich mich mit der Übersetzung von Plugins und Themes.

Helferfunktionen

In meiner functions.php habe ich eine Funktion eingebaut, die mir die derzeitige Sprache returniert. Hier sieht man auch das ich im folgenden Verlauf der Umbauten einen qTranslate Fork (mqtranslate) verwende.

function currentLanguage() {
 if(function_exists('qtrans_getLanguage')) {
    return qtrans_getLanguage();
 }
 return substr(get_locale(),0,2);
}

Texte

Im ersten Schritt müssen alle im Theme verwendeten Strings mit gettext Tags (__() oder _e() ) samt einer eindeutigen Text Domäne versehen werden z.B. <h1><?= __("Meine Überschrift", "meintheme_theme"); ?>

Für kompliziertere Strings kann man gettext auch in Verwendung mit sprintf verwenden z.B. so: <?= sprintf(__("Ihre Suche ergab %d Treffer.","meintheme_theme"),$total_results) ?>

Bilder

Bilder habe ich immer mit der  entsprechenden Sprachkennung abgespeichert also z.B. header_de.jpg und header_en.jpg und diese dann so: <img src="view/img/header_<?= currentLanguage(); ?>" alt="<?= __("Header Image","meintheme_theme") " /> integriert

.MO / .PO Files erstellen

Danach muss man sogenannte .mo und .po Files erstellen. Dafür habe ich mir auch ein kleines Bash Script gebaut, das mir mit Hilfe von xgettext (auf OS X mit Hilfe von brew installiert) sogenannte .pot Files (Templates / Vorlagen für die einzelnen Sprachfiles) erzeugt.

touch messages.po
find ../ -type f -iname "*.php" | xgettext --from-code utf-8 --language=PHP -c/ --keyword=__ --keyword=_e -j -f -
cp messages.po messages.pot
rm messages.po

Dieses Script kommt in einen Subfolder languages/ im Theme Verzeichnis  und wird mit chmod +x ausführbar gemacht. Wenn man es ausführt erzeugt es einen .POT Datei die man danach für die .PO Files verwenden kann. Dazu installiert man sich POEdit und legtmit der Funktion „Datei/Neu aus POT Datei“ einen neuen Katalog an. Danach trägt man die Meta Daten ein (Übersetzer/Sprache usw.) und übersetzt alle Einträge. Danach z.B. für Englisch wie vorgeschlagen als en_US.po abspeichern. POedit kompiliert auch automatisch das MO File.

Info am Rande: Es gibt auch noch andere Wege POT Files zu erzeugen, dazu findet man im WordPress Codex mehr Infos. POEdit kann in der Pro Variante anscheinend auch WordPress Installationen auslesen und die notwendigen Files generieren.

MO Files im Theme laden

In der functions.php folgenden Part hinzufügen:

function meintheme_theme_setup(){
   load_theme_textdomain('meintheme_theme', get_template_directory() . '/languages');
}
add_action('after_setup_theme', 'meintheme_theme_setup');

MO Files im Plugin laden

Achtung bei Plugins – bei diesen ist der Vorgang geringfügig anders. Die .po Files müssen dort als namedesplugins_plugin-en_US.po abgespeichert  und z.B. so geladen werden:

class MeinPlugin {
    public function init_all() {
        load_plugin_textdomain('meinplugin_plugin', false, basename( dirname( __FILE__ ) ) . '/languages' );
    }
    public function init() {
        add_action('init', array($this,'init_all'));
    }
}

Multi-Language Ajax Calls

Bei Ajax Calls wird die Sprache nicht rechtzeitig? geladen – eine Google Suche hat mir folgenden Stackoverflow Snippet erbracht:

/* http://wordpress.stackexchange.com/questions/121732/gettext-does-not-translate-when-called-in-ajax http://wordpress.stackexchange.com/a/168760 */
function set_locale_for_frontend_ajax_calls() {
 if ( is_admin() && defined( 'DOING_AJAX' ) && DOING_AJAX && substr( $_SERVER['HTTP_REFERER'], 0, strlen( admin_url() ) ) != admin_url() ) {
 load_theme_textdomain( "meintheme_theme", get_template_directory() . '/languages' );
 }
}

Dann muss man noch die ajax Url mit Sprache definieren also z.B. so:

var ajaxurl = '<?php echo admin_url('admin-ajax.php?lang='.currentLanguage()); ?>';

 Sonstiges

Ich hab in meinen meisten Themes eine Theme-Options Seite für z.B. Kurztexte, Metatags etc. – dort habe ich Multi-Language anfangs einfach durch eine Eingabebox pro Sprache realisiert. Ich habe im weiteren Verlauf mqtranslate verwendet, damit kann man dann die dort verwendeten Sprach Tags auch z.B. in den Theme Options verwenden. Dazu mehr im 2. Teil.