Remembering the user language choice

Recently, I had to develop a small web portal for a client. Since the portal consisted mostly of static pages which the client wanted to update himself, and I had limited time for development, WordPress platform was an obvious choice. We agreed to derive the design from the standard “Responsive” theme (WordPress Codex has a great article on how to do that).

The portal had to be multi-lingual, most pages would be in English, but some would have translations in Latvian. I found WordPress qTranslate plugin the best solution for my needs. It changes the way WordPress dashboard forms work, now a blog post has two fields “Title (Latvian)” and “Title (English)” as well as “Post content (Latvian)” and “Post content (English)”. If you don’t fill the fields for a particular language, the page would be treated as not translated and may be hidden in the navigation as well. You have to pick the default language of the site – what content to show if the user has not picked any language.

qTranslate provides several ways how you can do localization:

  • using a subdomain, e.g. lv.yourdomain.com and en.yourdomain.com
  • by adding a GET parameter to each page, eg. yourdomain.com/article_title/?lang=lv
  • using a directory prefix, e.g. yourdomain.com/lv/articletitle, yourdomain.com/en/articletitle

I picked the latter, as it seemed most rational of the three. Now every page had two addresses and everything seemed to be running just fine. However, I found it a bit annoying that a returning visitor always sees the default language, regardless which language he had chosen in the previous visit. It would seem logical that qTranslate could just set a cookie “last language” or something like that. I found out, it does not. Moreover, qTranslate authors are not going to implement it, they say “it’s a wanted behavior”.

Not wanting to mess with plugin’s PHP code, I decided to fix this at the web server level (as you may know, Apache Mod_Rewrite can both read and set cookies).

So, I needed two parts of the algorithm:

— when the user requests a page where the URL contains language code, save that language in a cookie. In “.htaccess language” it is

    #Only for requests which start with "/lv/" or "/en/". 
    RewriteCond %{REQUEST_URI} ^/(lv|en)/.*$ [NC]

    #no need to set a cookie if you already have one with the same value
    RewriteCond %{HTTP_COOKIE} !lang=%1 [NC] 

    #rewrite to the same address, but set a cookie to the value captured in first RewriteCond
    RewriteRule . - [cookie=lang:%1:.%{HTTP_HOST}:144000:/]

— when the user requests the “root” of the portal and there is a “lang” cookie set, do a redirect to the particular language root page

  #only for the root page /
  RewriteCond %{REQUEST_URI} ^/$ [NC]

  #only if you have a "lang" cookie
  RewriteCond %{HTTP_COOKIE} lang=(en|lv) [NC] \

  #redirect (not rewrite!) to either /en/ or /lv/
  RewriteRule ^(.*)$ /%1/ [R=302,L]

So, after all modifications my .htaccess file looks like this:

# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /

RewriteCond %{REQUEST_URI} ^/$ [NC]
RewriteCond %{HTTP_COOKIE} lang=(lv|en) [NC] 
RewriteRule ^(.*)$ /%1/ [R=302,L]

RewriteCond %{REQUEST_URI} ^/(lv|en)/.*$ [NC] 
RewriteCond %{HTTP_COOKIE} !lang=%1 [NC] 
RewriteRule . - [cookie=lang:%1:.%{HTTP_HOST}:144000:/]

RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress

Modulus

Rakstot par nesmukajiem datumiem, biju izmantojis šādu izteiksmi: $sundayEveningSpan = ((($now["wday"]-1)%7)*24*60*60);. Rezultātā mainīgais $sundayEveningSpan tiek uzstādīts (tam vajadzētu tikt uzstādītam) uz sekunžu skaitu starp šodienas plkst. 00:00 un šīs pirmdienas 00:00. Vārdu sakot, tiek aprēķināts, cik sekundes pagājušas šajā nedēļā.

Kā redzams, izteiksmē parādās operators %, kas atgriež izteiksmes $now["wday"]-1 vērtību pēc moduļa 7. Tas darīts tāpēc, ka PHP funkcijas GetDate() atgrieztajā masīvā wday lauks atgriež “Numeric representation of the day of the week: 0 (for Sunday) through 6 (for Saturday)”. Tātad svētdiena ir 0-tā diena, sestiena ir 6. diena. Tā kā man (un arī daudziem citiem latviešiem) nedēļa sākas ar pirmdienu, no dienas numura bija jāatņem vieninieks pēc moduļa 7, lai iegūtu pareizo piekārtojumu: pirmdienai 0, svētdienai (0-1) mod 7, kas vienāds ar 6 klasiskajā moduļu aritmētikā.

Tomēr izrādījās, ka šādi mana funkcija darbojas nekorekti. Izpētot tuvāk, secināju, ka PHP valodai arī ir savas īpatnības – modulis no negatīva skaitļa ir negatīvs. Tas arī oficiāli ir dokumentēts modulus operatora aprakstā. Tādēļ -1 % 7==-6, nevis 6, kā bija sagaidāms.

Lai novērstu šīs PHP īpatnības sekas, aizstājam -1 ar +6 (kas ir kongruenti pēc moduļa 7) un turpmāk nedēļas sākumu aprēķinām šādi: $sundayEveningSpan = ((($now["wday"]+6)%7)*24*60*60);.

Kā rāda testi, šāda pat problēma novērojama arī Visual Basic.Net un, ļoti iespējams, arī citās .Net valodās. Kā sacīts MSDN, “The result of x Mod y is the value produced by x – (x \ y) * y”. Tas nozīmē, ka gadījumā, kad pats x ir negatīvs, arī atgrieztais rezultāts būs negatīvs.

Tā, lūk. Atliek vien secināt, ka programmēšanas valodu veidotājiem ir vienalga, vai tiek saglabāta multiplikatīvās grupas pēc moduļa N un gredzena īpašība, kas tik svarīga ir matemātiķiem. Un tas, protams, ir tikai normāli – ja vajadzēs, vienmēr varēsim pieskaitīt 6 tā vietā, lai atņemtu 1.

Nesmukie laiki

Šodien mani kaunināja, ka es nesmuki datumus rakstot. Ka “02.06.2005. 07:21:22” patiesībā cilvēkam neko neizsaka. Cita lieta, ja paziņotu vienkārši “vakar” – tas jau daudz labāk izklausoties. Taisnība jau arī ir – cilvēks nav dators, cilvēkam BIOS-ā nav iešūts kalendārs.

Tāpēc funkcija, kas UNIX timestampam atgriež elementāros vārdiskos aprakstus. Tā sacīt, izkrāso pati, ja gribi citādi:

function TextInfo($spTime){
    $todayMidNight = mktime(0, 0, 0, date("m"), date("d"), date("y"));
    $yesterdayMidNight = $todayMidNight - 24*60*60;


    $nowTime = mktime();        //brīdis, pret kuru rēķinās
    $now = getdate($nowTime); //aprēķina brīža dati sadalīti pa "detaļām"

    //timestamp pagājušās svētdienas beigām
    $sundayEveningSpan = ((($now["wday"]+6)%7)*24*60*60); 

    $then = getdate($spTime); //laiks, pret kuru rēķinās sadalīts pa "detaļām"

    //vai sakrīt ar šodienu
    if ( ($now["mday"]==$then["mday"]) && 
        ($now["mon"]==$then["mon"]) && 
        ($now["year"]==$then["year"])) {
    if ($then["hours"] &lt; 6) return "šonakt"; //noticis šajā naktī
    if ($then["hours"] &lt; 10) return "šorīt"; //noticis rīta pusē
    return "šodien";  //citādi vienkārši informējam, ka šodien 
    }

    //ir noticis vakardien
    if ( ($todayMidNight &gt; $spTime) && ($spTime >= $yesterdayMidNight)) {
    if ( abs( $now["hours"] - $then["hours"] ) &lt; 3) return "vakar ap šo laiku";  //ja stundas līdzīgas pašreizējām
    if ($then["hours"] &lt; 6) return "vakar naktī"; //noticis vakar no rīta agri
    if ($then["hours"] &lt; 10) return "vakar no rīta"; 
    if ($then["hours"] &lt; 12) return "vakar priekšpusdienā"; 
    return "vakar";
    }

    //ir noticis šonedēļ
    if (( $todayMidNight > $spTime) &&  ($todayMidNight- $sundayEveningSpan < $spTime )) {
    return "šonedēļ";
    }

    //ir noticis pagājušajā nedēļā
    if (( $todayMidNight- $sundayEveningSpan > $spTime) && 
        ( $todayMidNight- $sundayEveningSpan - 24*60*60*7 &lt; $spTime )) {
    return "pagājušajā nedēļā";
    }

    //ja senāk par pagājušo nedēļu, vai nav senāk par gadu?
    $tSpan = $nowTime - $spTime;
    if (floor($tspan /(24*60*60*365) )>0) return " pirms vairāk nekā gada";

    //aizmirstam īsos un garos mēnešus
    $tSpanMonths = floor( $tSpan / (24*60*60*30) );     
    if ($tSpanMonths &gt; 0) {
    //starp 1 un 2 mēnešiem
    if ($tSpanMonths == 1) {
        $days = floor(($tSpan-24*60*60*30)/(24*60*60));
        if ( ($days!=1) && ($days!=21) && ($days!=31)) 
        return "pirms mēneša un ".$days." dienām";
                            Else
        return "pirms mēneša un ".$days." dienas";
        }
    return "pirms vairāk nekā ".$tSpanMonths." mēnešiem";
    } else {
    //jāskatās, vai bija tas pats mēnesis, vai iepriekšējais
    if ($then["mon"]==$now["mon"]) return "šajā mēnesī"; else return "pagājušajā mēnesī";
    }
}

Parametros Unix timestamps.

FrontPage markup

Īss veids, kā tikt vaļā no FrontPage radītajām sliktajām lietām html kodā, ja “tāpat jau nāksies tam visam iet ar roku cauri”. Darām apmēram tā:

$dati=$_POST["dati"];
$dati=strip_tags($dati, &quot;&lt;a&gt;&lt;b&gt;&lt;i&gt;&lt;p&gt;&lt;br&gt;&quot;);
$dati=preg_replace(&quot;/(class|style|align)=\"[^\"]+\"/i&quot;, &quot;&quot;, $dati);

Tādējādi no formas (vai vienalga kādā veidā) saņemtais mainīgais $dati tiek iztīrīts un tālāk ir cilvēkam lietojams. Līdz ar to tekstā nedrīkst parādīties substringi kā class="kautkas", bet par to jau FrontPage parūpēsies, aizvietojot tos ar attiecīgiem html entities.

Mazliet par elementāro objektorientāciju

Mazliet par elementāro objektorientāciju PHP (tālāk rakstītais uz PHP 4.x, bet, ļoti iespējams, attieksies arī uz PHP 5.x)
PHP izveidot klasi ir pavisam vienkārši. Rakstām

class TestKlase{}
un ir deklarēta jauna klase TestKlase. Līdz ar to turpmāk PHP kodā var rakstīt
$TesVar = new TestKlase();
un būs zināms pavisam droši, ka $TestVar ir klases TestKlase objekta instance. Klasē var definēt arī iekšējos mainīgos, sauktus par atribūtiem. To dara šādi – tūlīt aiz klases nosaukuma deklarāciajs raksta atsevišķas rindas, kas sākas ar var un turpinās ar atribūta vārdu (un, iespējams, sākumvērtības definīciju). Tas ir,
class TestKlase{
var $TestAtt1;
var $TestAtt2 = ‘1’;}
Līdz ar to, veidojot jaunu klases instanci, atribūts TestAtt2 iegūs vērtību ‘1’,bet TestAtt1 vērtību neiegūs vis. Vērtību inicializatori var būt tikai konstantes, ne dinamiski aprēķinātas vērtības (piemēram, datums). Par klases konstruktoru tiek uzskatīta funkcija, kuras nosaukums sakrīt ar klases vārdu.
class TestKlase{
function TestKlase(){
   $this->TestAtt1=”lalala”;
}
}
Kā redzams, šīs klases konstruktors uzstāda atribūta TestAtt1 vērtību uz string tipa mainīgo “lalala”.
Klases iekšpusē, lai piekļūtu dotās instances atribūtu vērtībām, jālieto mainīgais $this, kas vienmēr norāda uz doto klases instanci (tieši tāpat kā valodā C++).
Klases metodes rakstāmas tieši tāpat kā parastas PHP funkcijas (kas tās pēc būtības arī ir), ieskaitot mainīgo redzamības apgabalu lietojumu, tas ir, lai piekļūtu globāliem mainīgajiem metodē jādeklarē mainīgais kā globāls, piemēram
function TestMethod(){
global $TABULASNOSAUKUMS;
}
. Klases, kas ir objektu kolekcijas, ir viegli veidot, izmantojot array objektu un arraypush() metodi, bet tos pēc tam apstaigāt, izmantojot
foreach ($this->TestVars as $test){
printr($test);
}
Ērta funkcija klases instances apskatīšanai ir printr().