Multilanguage web site

Pozdrav,

nisam našao baš neko dopadljivo rješenje za izradu multilanguage (ML) weba, pa sam se odlučio na osmišljavanje vlastitog sisitema.

Uvjeti koje sam si postavio je:
-mora biti maksimalno jednostavno za tipkati code. Sve stavke koje mogu biti prevedene, se moraju pisati unutar codea s praktički minimalnim dodatnim “naporom”.
-ne smije biti asinkronog učitavanja
-maksimalno optimiziran load, tj. da opcija ML, zahtjeva minimalno CPU prilikom učitavanja WEB-a (diranje u bazu je već trn u oku). …a kamoli da se učitava nešto što se na određenoj podstranici WEB-a ne koristi.

Postavljen pred vlastite uvjete, malo sam se zabio u zid tražeći odgovarajuće rješenje…no na kraju je ispalo toliko elegantno, da eto imam potrebu podjeliti. No možda i nije dovoljno elegantno…to će strogi forumaši presuditi :smile:

Uglavnom, sve uvjete sam ispunio…i stvar zapravo šljaka besprijekorno. Ideja je sljedeća:
Stavke koje trebaju biti prevedene, tagiraju se posebnom oznakom u codeu. Ja sam odabrao {{duple vitičaste zagrade }}, što možda i nije najbolji odabir jer sam krajičkom oka vidio da se to već koristi za nekog vraga u nekim frameworcima…
No, odabir tagova je zapravo varijabilan…tako da mogu customizirati tu oznaku za buduće projekte.

Uglavnom, već sam često hvalio mogućnost preprocesiranja codea, i ovdje opet koristim tu “extra moć”, da iz coda izvučem više nego što on daje sam po sebi pisan u sintaksi nekog jezika. Znači dupla vitičasta zagrada je interna sintaksa, koju razumije i obrađuje preprocesor.
Preproces će znači ući u sve fileove projekta, pronaći sadržaj u duplim vitičastim zagradama…te njihov content prevesti iz dostupnog dictionarya…i na taj način stvoriti novi code. Taj novi code, sa prevedim vitičastim zagradama će spremiti u novi file, na lokaciju tipa: ML/HR/fileName ili ML/EN/fileName …logično, prevedeni file će se nalaziti na pathu ovisno o jeziku na koji je preveden.

Framework koji isporučuje WEB, se treba malo podesiti da postane kompatibilan s ovakvim pristupom, ali zapravo vrlo jednostavno podešavanje je potrebno. Znači, framework kod includanja određenog file treba provjeriti dali taj file postoji u folderu ML, na zadanom jeziku otvaranja stranice. Ako file postoji tamo, includa ga od tamo…ako ne postoji od tamo, includa source file. Autoloader klasa se također vrlo jednostavno podesi da inkluda klasu prvo iz ML foldera…tek zatim source file.
U codu, to je vrlo blaga izmjena, poput:

//stari include
include $filePath; 
//novi include
include ML_getFile($filePath)

…jasno je, funkcija ML_getFile($filePath) …provjerava dali file postoji u ML folderu i koristi konstantu koja definira jezik na kojem se web isporučuje. (A dobro je da može primiti i vrijednost $language kao opcionalni parametar) Te ako file postoji tamo, vraća njegov path, u suprotnom vraća path koji je primila na ulazu.

I to je više manje to…stvar šljaka odlično. Jedina loša stvar ovog pristupa je da fileovi, koji sadrže neki element koji se prevodi, će biti kopirani u onoliko kopija koliko postoji ponuđenih prijevoda. Ali ta dodatna memorija ne opterećuje nikakav extra load, osim zanemarivo više utrošenog prostora na hostu gdje je web hostan.
Instalacija, tj. prevođenje file-ova je također jednokratan proces, koji se odvija lokalno (a može i na serveru) …iako taj proces je mrvicu zahtjevniji, to je ipak jednokratna radnja… I ta radnja se da prilično optimizirati, tako da ja u lokalnom radu ju automatski trigiram prije svakog otvaranja stranice. Ali ona tad prati koji fileovi su izmjenjeni…i samo njih pokušava prevesti. Tako da je na taj način i taj proces sveden na minimum…i može slobodno biti lokalno prisutan pred svako otvaranje weba.

Cijela priča se znači svodi:
-piši u vitičastim zagradama ono što želiš da bude prevedeno
-taj sadržaj će se automatski pojaviti u jednom dodatnom CMS, gdje admini imaju mogućnost unošenja prijevoda
-jedna instalacija prevede sadržaj iz vitičastih zagrada, pomoću tog dictionarya kojeg su ispunili admini. (Vjerovatno Google translate nudi neki API, pa se i taj dio prevođenja može automatizirati…gdje bi admini onda samo koregirali loše prijevode)
-framework se lagano podesi da učitava prevedeni file ako postoji, ili source file
-jedna instalacija prijevoda…i tarann…stvar radi odlično, i ja sam very happy about it. :slight_smile:

P.S. Vitičaste zagrade omogućavaju i razvoj sintakse koji će se naći unutar njih…koju će preprocesor prepoznavati. Tako već imam mogućnost bindat varijabilne parametre koji se nalaze usred nekog prijevoda. Npr. korisničko ime…ili tako nešto… No, bitno je primjetiti da ovakav pristup ostavlja unlimited prostor razvoja sintakse koji će developeru omogućiti implementaciju kompleksnijih prijevoda, na što jednostavniji način.

I hope, you like it…as I like it. :smiley:

I pitanje je, na koji način vi ugrađujete multilanguage opciju? Ako je to dio vašeg frameworka s kojim radite, kakav je usability toga? Što vi kao developer morate raditi/poštivati da bi multilanguage radio po zamišljenom??

Zasto nisi koristio gettext?

Zadnji implement ML sam radio na način da sam učitao language objekt iz baze i prema tome napravio funkciju za svaku riječ da se prevede kad se riječ treba prikazat. Korištenje npr: prijevod(“riječ”) gdje je prijevod funkcija koja returna prevedenu riječ…

Kako bi mi to točno pomoglo?
Nisam upoznat s time, ali ovako na prvi pogled vidim da je to nekakva php metoda dohvacanja prijevoda. No ovaj moj pristup vrijedi i za JS skripte…a i za ako treba prevesti i komentare u CSSu. Naravno da komentare u css-u necu prevoditi, ali čisto da dočaram kako je metoda široko primjenjiva, a zapravo max jednostavno.

Ista ideja bi se relativno jednostavno mogla prenesti na projekte koji nemaju veze sa PHP-om i JS-om.

Da, od tud sam i ja krenuo razmisljat. Ali recimo da u JSu na taj način daješ ime nekakvom gumbiću ili nešto. Nekakav ozbiljniji content se nebi trebao naći u JSu, ali sitnice se bome nađu…i što se mene tiče, bilo bi odveć kompliciranje da moram paziti da se baš nikakav tekstualni sadržaj ne stvori u JSu.

Uglavnom, imas sad neku tako sitnicu koju treba u JS prevesti. Ako ćeš tjerat riječ kroz funkciju, onda vrlo vjerovatno trebaš čekati response sa servera. A to često može biti vrlo nezgodno. Da bi izbjegao to asinkrono učitavanje…morao bi paziti da sa svakim JS fileom učitaš prijevod koji bi on mogao trebati. I ja sam se cijelo vrijeme vrtio upravo oko toga, da bi na kraju skontao to do čega sam došao. I još jedna stvar…sintaksa je puno zgodnija, ako se riječ ne mora provlačit kroz funkciju…
…a druga velika prednost, ovo moje se može koristiti i usred HTML dokumenta (kako sam vec napisao, mozes i komentar prevoditi ako poželiš)…prema tome, mogu se prevesti samo dijelovi nekog stringa, bez potrebe da se trkelja sa zbrajanjem i cijepanjem stringa, kako bi samo neki dio progurali kroz funkciju…i tako, hrpa prednosti…a nisam još opazio ozbiljniju manu.

Rješio sam na način kako je i Laravel svoje prijevode - https://laravel.com/docs/5.2/localization

Znači imam folder language, podfoldere po jezicima te mogućnost više fajlova kreiranih po sekcijama sajta (messages, form, about…):

  • hr/
  • messages.php
  • form.php
  • en/
  • messages.php
  • form.php
  • de/
  • messages.php
  • form.php

Kreiram klasu gdje injectam trenutni (defaultni jezik) i popis svih jezika:

$lang= new Language('hr', ['hr', 'en', 'de']);

A koristim ga na način:

echo $lang->get('form.contact.email');

Gdje se učita fajl form.php trenutnog jezika (ako već nije učitan), sprema se u array (ako već nije spremljen) te se potom čita iz tog spremljenog arraya. Array bude $array[‘en’][‘form’][]

Unutar get() methode postoji i replace te ispis nekog drugog jezika:

echo $lang->get('form.contact.email', ['email' => '[email protected]'], 'de');

“Hvala {email} na registraciji!” - gdje se {email} replacea s [email protected].

Priejvod sadržaja kreiram kroz bazu podataka i posebnu tablicu koja se veže na master tablicu koja se prevodi.

Tipa tablica news ima svoju child tablicu s prijevodima news_translation koja sadrži polja koja se prevode te vezu na news tablicu.

2 Likeova

Nekako mi se cini puno elegantnije nego tvoje rijesenje sa hrpom datoteka. Znam da ih ne generiras rucno ali svejedno, ne cini mi se uopce user/developer-friendly. Ali vidim da Laravel radi slicnu stvar pa valjda onda ima neki razlog iza toga.

Zar ti nije vrijedan taj benefit da možeš prevesti bilo što, bilo gdje? Može se prevesti čak info.txt file…koji unutar sebe nema nikakvu programsku logiku…ni php, ni js. A opet, unutar njega se može selektivno prevesti što god se poželi jednostavnim {{tagiranjem}} sadržaja za kojeg želimo da bude preveden.

Kako bi ti recimo preveo vrijednost nekog gumbića u JSu? Pretpostavljam, morao bi se potruditi da ta vrijednost do JS-a stigne generirana PHP-om? No pored imena gumbića, ima mali milijon stvari koji lako završe u JSu, a zahtjevaju prijevod. Ne vidim jednostavnijeg načina, nego da se taj sadržaj samo označi nekim tagom (tipa duplim vitičastim) …i da dalje o tome nemamo brige. U suprotnom, trebalo bi paziti za svaku sitnicu da stigne iz PHP-a…i da je JS adekvatno povezan s tim generiranim sadržajem. I to ne zvuči loše, ali svejedno…takav pristup bi bio ograničen za štošta napraviti. (Ja neke stvari nisam mogao rješiti tim načinom …a bilo mi je jako odbojno da imam različite metode za “ovo”, a druge za “ono”)

S druge strane, veliš da ti ne zvuči elegantno hrpa foldera. Da i mene je to na prvu poplašilo…ali kad sam vagnuo dobro i loše, nije bilo dvojbe.
Kao prvo, svi ti folderi stoje u jednom malenom folderu-u, oku jedva vidljivo…tako da strukturalno i vizualno nikom ne smeta. Štoviše, ako glavni kontroler ima lijepo definiran path prema file-ovima aplikacije, onda ovaj dodatni folder neće završiti niti blizu tih file-ova, tako da se neće niti editor buniti kod svojih metoda poput find/replace i ostalih kerefeka. To je recimo dosta bitno da se ne naruši radno okruženje.

Kao drugo, cijeli taj file, sa svom tom hrpom foldera će teško po memoriji biti do koljena nekakvom image folderu ili recimo bazi podataka. Znači i sa memorijskog aspekta, apsolutno zanemariv gubitak. …pogotovo jer to nije memorija koja troši ikakav bandwith ili išta, samo malo više uzme prostora što čuči na serveru.

A osim jednostavnosti korištenja imaš i direktan benefit kod loada stranice…jer ne moraš kod loadanja stranice uputiti niti jedan upit u bazu za bilo kakvim prijevodom.

Vidim nešto slično :slight_smile: …ali nisam baš najbolje sve skužio.

Kako npr. unutar file-a form.php označavaš koji dio želiš da bude preveden?

Prijevod se nalazi u language/hr/form.php gdje se nalazi array:

return [
    'login' => [
        'success' => 'Hvala {{email}} na registraciji!',
        'error' => 'Greška prilikom prijave! Molimo pokušajte ponovno.'
    ],
    'lost_password' =>
    ...
];

Unutar PHP template fajla prijevod pozivam na slijedeći način:

<?php echo $lang->get('form.messages.success', ['email' => $email]); ?>
<form>
...
</form>

A ako bi htio includati neki Javascript (ili drugi vanjski fajl) s prijevodima napravim slijedeće:

<?php echo $lang->include('js/skripta.js'); ?>

Skripta će potražiti sve {{var}} i zamjeniti ih s odgovarajućim prijevodima.

Iako ovo moje rješenje baš i nije najbolje jer se javascript učitava direktno u HTML kod pa se taj JS ne može keširati. Puno bolje rješenje sam našao na stackoverflowu - http://stackoverflow.com/a/22157210/2515720

Znači, u head učitaš sve prijevode koji se koriste u JS fajlu:

<head>
...
<script>
    var userID = "{{ Auth::user()->id }}";
    var isUser = "{{ Auth::user() }}"
</script>
</head>

A u javascript fajlu:

if(isUser)
{
    if(path.indexOf('/user/' + userID ) != -1) {
        $( "#tabs" ).tabs();
    };
}

Ne kužim što te spriječava da napraviš da se može keširati?

Nemoj samo js includati da radis echo codea …nego generiraj prevedeni JS i ispisi u head html-a propisni tag koji će includati zadani JS. Ako će ta JS skripta biti generirana uvijek sa istim imenom…browser će ju uredno keširati.

I naravno, prevedeni JS generiraš samo na promjenu JS sourca, ne na svako učitavanje stranice. To vjerovatno tako i radiš.

Nego, jesam li te dobro skužio. Ti imaš različiti pristup za php file.ove i za JS. U php trpas u array samo sadrzaj koji zelis prevesti…te po potrebi dohvacas samo prevedene linije…djelove arraya.
Dok kod JSa prevedes cijeli file…tj. sve tagirane elemente u filu?

Ja sam tako krenuo…prevodio sam provlačeći izraz kroz funkciju, a sa JS.om sam učitavao sav prijevod koji će mu trebati. No da bi to ispunio…opet sam se oslanjao na automate koji će samostalno analizirati koji prijevodi trebaju za koji JS. A ti automati su nešto lakši za izradu i stabilniji ako imamo specifično tagiranje, nego nekakvu funkciju oko potrebnog prijevoda. No ono što me je više privuklo tagiranju je jednostavnost pisanja sintakse, isto kao i preglednosti iste.
Nadalje…gornji pristup je ograničen. Ja recimo imam HTML template, koji su prilično mrtav html code dok ih njihov render ne oživi…koji im dodjeli određene varijable kod ispisivanja ih. No ti templatei imaju malene izraze (naslove, gumbiće) koje su konstantne kod svakog generiranja. Nisu konstantne jedino po pitanju multijezičnosti. I sad bi ja morao petljati s tim frazama, da i njih filam s php.om kod ispisivanja templatea. A ovako samo puknem te izraze u duple vitičaste…a template.e kasnije dohvacam sa funkcijom ml_get().

Ta univerzibilnost ovog pristupa me odbila da išta drugačije radim s JS.om i kak ti optimiziram nešto, kad realno ovaj univerzalni način i jeste i optimiziraniji u mnogim pogledima. A u nebitnom uzme komadić više MB.a.

Da, zapravo je kod mene problem generiranja. Do sada sam za cijeli JS radio echo u što nije bilo moguće keširati. Zapravo bi trebao includati JS fajl, provjeriti je li generiran, ako je, učitam ga, ako nije prvo ga generiram ? Znači ti kad spremaš generirati JS ti ga već napuniš prijevodima pa ga sejvaš kao skripta_hr.js ?

Tako je, lokalno preprocesor zamjeni tagirane djelove sa prijevodima. Može se to napraviti i online…ali naravno, samo po promjeni izvornog JS.a. Zatim taj generirani file se spremi na lokaciju gdje će ga znati pronaći funkcija ml_getFile(), ili kod tebe je to $lang->nesto()… No ta funkcija ne dohvaca content JS filea, nego samo path prema prevedenom fileu u ovisnosti patha source file.a i jezika čiji prijevod želimo. Ako taj file ne postoji, ml_get() će vratiti source path…što zapravo znači da ta skripta nije imala sadržaj pripremljen za prijevod…te ne postoji path prema prevedenom fileu.
A sad, dali ćes napraviti da prevedeni file bude script_hr.js… Ili da path izgleda multilanguage/hr/script.js … To je tvoja slobodna volja. No svakako bi bilo dobro da su svi fileovi u jednom folderu, kojeg će se lako moći seliti po potrebi.

Ovo vrijedi samo ako dohvacas sa $lang->nesto file.ove za koje znas da trebaju imati prijevod. Ja cijeli svoj JS includam sa tom metodom, pa nema smisla da forsiram generiranje prijevoda za filove koji nemaju ništa tagirano za prevest. A onog trenutka kad nesto tagiram u nekom od tih file.ova koji nisu imali prijevod, necu morati misliti da moram sad išta mijenjati kod includanja baš te skripte. Znači moj ml_get() vraća path prema prevedenom fileu, ako takav postoji…u suprotnom vraća source path.

Drugi poseban dio samostalno pazi da prevede sve filove koji imaju nešto tagirano. On razmišlja:
-Uzmi sve filove projekta
-Za svaki pogledaj jel postoji kakva izmjena od zadnji put
-Ako postoji, napravi nove prevedene filove za sve dostupne jezike onih prijevoda koje nađeš tagirane u file.u

Taj proces lokalno vrtim na svaki load, a online po uploadu novih filova.

S time da mrzim da postoje ikakve manual radnje nakon uploada, tako da sam osmislio file koji se zove online_direktiva. Taj file se generira lokalno, a sprema uputstva serveru što će napraviti nakon uploada. Tako da pomoću online direktive…online server automatski zna kada treba trigirati tako neki proces koji dolazi nakon uploada.

Definitivno pogledaj https://www.gnu.org/software/gettext/manual/gettext.html, a tu imas i nesto o JS i18n https://24ways.org/2007/javascript-internationalisation, a imas i ovaj projekt http://i18next.com/docs/

Blago tebi kad imas vremena toliko filozofirati o svom kodu i izmisljati za svaku stvar “toplu vodu” nanovo i to svakog puta i za svaki problem ;).

Ima i razlog za to :smiley:
Znaš vjerovatno kako sljepci razviju posebne vještine da se snađu u okolnostima koje su ih snašle…

…tako sam i ja u ovim vodama slijep za jednu stvar. Ja sam naime krenuo programirati s gotovo nikakvim znanjem engleskog. Putem se nešto i naučilo…ali dugo vremena sam brže mogao napravio neku stvar, nego izučiti neku stvar. To je degradiralo moje skilse proučavanja, ali definitivno ojačalo moje skilse stvaranja. Dan danas…ako u 10 minuta guglanja ne pronalazim ono što mi treba u savršeno pakovanom obliku, uopće ne gubim vrijeme na daljnje guglanje…kada znam da ću kroz koji dan imati dobro vlastito rješenje. Takav pristup me nikada nije degradirao spram kolega koji su kretali iz iste točke kao ja…koji su bili potkovani engleskim i koji su se uglavnom oslanjali na gotova rješenja. No kad takvi kolege naiđu na problem koji nigdje nije rješen…lupe često u zid. Ja kad se nađem pred problemom kojeg nikada nitko nije rješavao…imam običan dan kao i svaki drugi dan :slight_smile:

A vlastita rješenja su mi draža i iz aspekta što ne možemo bolje poznavati srž nečega, osim ako to sami ne napravimo. Kasnije…kada uvidimo druga dobra rješenja…uvijek možemo križati metodiku vlastitog rješenja s onim dobrim stvarima tuđeg rješenja…te tako dobivamo još bolje rješenje i od vlastitog i od tuđeg.

P.S. kad sam došao prvi dan u firmu gdje sam radio kao vanjski suradnik…mislili su na glas da sam Bog kad sam u jednom danu rješio problem za koji nisu imali nikakvu ideju što i kako xd. …a onda s druge strane ispadnem totalni noob, kad neku tako općenitu stvar napravim komplicirano po svome…a postoje tako lijepa utabanata rješenja. No sve ja to prihvatim s vremenom…

Što suma sumaru ne govori da je ovaj moj pristup bolji, ali tebi daje konačno objašnjenje zašto sam ja primoran bio ići ovim putem i izmišljati uvijek toplu vodu, a u konačnici…nije da se bunim rezultatima, pa da sam prisiljen nešto mijenjati.

P.S.2
Dok bi ja prostudirao ovaj tvoj prvi link…ode baba s kolačima. Pa dok bi negdje zapeo…pa studirao razno razne forume za postojeće solucije rješenja…uzme to vremena. Zato ja razmišljam kraćim putem: “Ako postoji odličan univerzalan alat, facebook bi ga koristio…i ja se nebi nikad sreo u facebooku sa ne prevedenim gumbićem.” (A sretnem se često). To razmišljanje ne mora bit točno, ali je zato daleko brže i time efikasnije.
To razmišljanje poduprem s kratkim pogledom na stackoverflow forum…čim vidim u kojem smjeru idu rasprave, shvatim dali postoji univerzalan alat ili ne…koji je vrijedan gubljenja vremena.
Treća stvar koju napravim: pitam na forumima koja logika stoji iza određenog ostvarenja cilja (ovdje multijezičnost). Ono što želim čuti su načini, tehnika, mogućnosti …ne imena frameworka koji koriste te tehnike. Ako nađem jednostavnost objašnjenja nekog pristupa/tehnike…samo onda mogu prihvatiti i alat koji je razvio taj pristup. Ali bez objašnjenja pristupa, alat vjerovatno neću niti razmatrat.

…i na kraju otvaram temu, iznosim na vidjelo učinjeno ili zamišljeno… jer dobre kritike su Bogom dane. No u kritikama me ne zanima čuti da netko kaže:
“To je možda dobro rješenje jer tako radi Lavarel” …ili isto tako ako kaže:
“To nije dobro rješenje jer tako ne radi Lavarel”

…ono što od kritika želim čuti, je konkretno što u predloženoj metodiki je ograničavajuća stvar. Što je konkretno loše i zašto je loše. Što je dobro i zašto je dobro. Tek vaganjem dobrog i lošeg se može zaključiti o ispravnosti odabrane metode.
Ti da si u svojim linkovima ponudio bolju metodu (a možda jesi), nebi te ništa koštalo da u kratkim crtama izneseš zašto je taj pristup superiorniji spram ovoga. Ti to naravno nisi dužan iznesti, ali ja to smatram forumskom kolegijalnošću…na kraju tu vidim i smisao foruma. To što si ti meni dao link od tone pod linkova…i reko “ajd tim putem” …meni ništa ne znači, osim ako imam veliko povjerenje u tebe. A povjerenje stječem tek ako netko zna i želi obrazložiti svoj pristup.